summaryrefslogtreecommitdiffstats
path: root/nova
diff options
context:
space:
mode:
authorRick Harris <rick.harris@rackspace.com>2011-05-31 20:49:49 +0000
committerRick Harris <rick.harris@rackspace.com>2011-05-31 20:49:49 +0000
commitb4845fa52affacf76ae1668079f1cc7dfa1b4004 (patch)
tree09c5e661f15551ef6fe16bfd719a9fdb8b86895d /nova
parent1b610e28e40c77271191349b6bfaa56c8f522c24 (diff)
parent3812bb4c7e6fcee4c87c1225d9c725db08527018 (diff)
downloadnova-b4845fa52affacf76ae1668079f1cc7dfa1b4004.tar.gz
nova-b4845fa52affacf76ae1668079f1cc7dfa1b4004.tar.xz
nova-b4845fa52affacf76ae1668079f1cc7dfa1b4004.zip
Resolving conflict and finish test_images
Diffstat (limited to 'nova')
-rw-r--r--nova/api/ec2/__init__.py10
-rw-r--r--nova/api/ec2/cloud.py93
-rw-r--r--nova/api/openstack/auth.py17
-rw-r--r--nova/api/openstack/contrib/volumes.py2
-rw-r--r--nova/api/openstack/extensions.py84
-rw-r--r--nova/api/openstack/images.py22
-rw-r--r--nova/api/openstack/limits.py4
-rw-r--r--nova/api/openstack/servers.py19
-rw-r--r--nova/api/openstack/views/limits.py36
-rw-r--r--nova/api/openstack/zones.py13
-rw-r--r--nova/auth/novarc.template2
-rw-r--r--nova/compute/api.py51
-rw-r--r--nova/compute/manager.py28
-rw-r--r--nova/db/api.py41
-rw-r--r--nova/db/sqlalchemy/api.py126
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/017_make_instance_type_id_an_integer.py68
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/018_rename_server_management_url.py60
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/019_add_volume_snapshot_support.py70
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/020_add_snapshot_id_to_volumes.py47
-rw-r--r--nova/db/sqlalchemy/models.py31
-rw-r--r--nova/exception.py12
-rw-r--r--nova/fakerabbit.py31
-rw-r--r--nova/flags.py10
-rw-r--r--nova/image/fake.py4
-rw-r--r--nova/image/glance.py8
-rw-r--r--nova/image/local.py4
-rw-r--r--nova/image/service.py4
-rw-r--r--nova/network/api.py45
-rw-r--r--nova/network/linux_net.py228
-rw-r--r--nova/network/manager.py67
-rw-r--r--nova/network/vmwareapi_net.py18
-rw-r--r--nova/network/xenapi_net.py18
-rw-r--r--nova/notifier/__init__.py14
-rw-r--r--nova/notifier/api.py83
-rw-r--r--nova/notifier/log_notifier.py34
-rw-r--r--nova/notifier/no_op_notifier.py (renamed from nova/tests/real_flags.py)15
-rw-r--r--nova/notifier/rabbit_notifier.py36
-rw-r--r--nova/quota.py110
-rw-r--r--nova/rpc.py271
-rw-r--r--nova/service.py70
-rw-r--r--nova/test.py14
-rw-r--r--nova/tests/api/openstack/extensions/foxinsocks.py26
-rw-r--r--nova/tests/api/openstack/fakes.py7
-rw-r--r--nova/tests/api/openstack/test_extensions.py45
-rw-r--r--nova/tests/api/openstack/test_images.py141
-rw-r--r--nova/tests/api/openstack/test_limits.py138
-rw-r--r--nova/tests/api/openstack/test_servers.py27
-rw-r--r--nova/tests/api/openstack/test_zones.py1
-rw-r--r--nova/tests/fake_flags.py28
-rw-r--r--nova/tests/image/test_glance.py2
-rw-r--r--nova/tests/integrated/api/client.py10
-rw-r--r--nova/tests/integrated/integrated_helpers.py18
-rw-r--r--nova/tests/integrated/test_servers.py106
-rw-r--r--nova/tests/public_key/dummy.fingerprint1
-rw-r--r--nova/tests/public_key/dummy.pub1
-rw-r--r--nova/tests/test_api.py23
-rw-r--r--nova/tests/test_cloud.py121
-rw-r--r--nova/tests/test_compute.py22
-rw-r--r--nova/tests/test_flags.py14
-rw-r--r--nova/tests/test_libvirt.py (renamed from nova/tests/test_virt.py)71
-rw-r--r--nova/tests/test_notifier.py117
-rw-r--r--nova/tests/test_quota.py82
-rw-r--r--nova/tests/test_rpc.py116
-rw-r--r--nova/tests/test_service.py59
-rw-r--r--nova/tests/test_volume.py50
-rw-r--r--nova/tests/test_xenapi.py49
-rw-r--r--nova/tests/xenapi/stubs.py26
-rw-r--r--nova/virt/connection.py2
-rw-r--r--nova/virt/disk.py50
-rw-r--r--nova/virt/images.py69
-rw-r--r--nova/virt/libvirt.xml.template2
-rw-r--r--nova/virt/libvirt/__init__.py0
-rw-r--r--nova/virt/libvirt/connection.py (renamed from nova/virt/libvirt_conn.py)692
-rw-r--r--nova/virt/libvirt/firewall.py642
-rw-r--r--nova/virt/libvirt/netutils.py97
-rw-r--r--nova/virt/vmwareapi/vmops.py6
-rw-r--r--nova/virt/xenapi/fake.py5
-rw-r--r--nova/virt/xenapi/vm_utils.py57
-rw-r--r--nova/virt/xenapi/vmops.py99
-rw-r--r--nova/virt/xenapi/volume_utils.py43
-rw-r--r--nova/virt/xenapi_conn.py8
-rw-r--r--nova/vnc/__init__.py2
-rw-r--r--nova/volume/api.py57
-rw-r--r--nova/volume/driver.py134
-rw-r--r--nova/volume/manager.py59
-rw-r--r--nova/wsgi.py5
86 files changed, 3720 insertions, 1530 deletions
diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py
index cd59340bd..1915d007d 100644
--- a/nova/api/ec2/__init__.py
+++ b/nova/api/ec2/__init__.py
@@ -327,6 +327,12 @@ class Executor(wsgi.Application):
ec2_id = ec2utils.id_to_ec2_id(ex.volume_id, 'vol-%08x')
message = _('Volume %s not found') % ec2_id
return self._error(req, context, type(ex).__name__, message)
+ except exception.SnapshotNotFound as ex:
+ LOG.info(_('SnapshotNotFound raised: %s'), unicode(ex),
+ context=context)
+ ec2_id = ec2utils.id_to_ec2_id(ex.snapshot_id, 'snap-%08x')
+ message = _('Snapshot %s not found') % ec2_id
+ return self._error(req, context, type(ex).__name__, message)
except exception.NotFound as ex:
LOG.info(_('NotFound raised: %s'), unicode(ex), context=context)
return self._error(req, context, type(ex).__name__, unicode(ex))
@@ -338,6 +344,10 @@ class Executor(wsgi.Application):
else:
return self._error(req, context, type(ex).__name__,
unicode(ex))
+ except exception.KeyPairExists as ex:
+ LOG.debug(_('KeyPairExists raised: %s'), unicode(ex),
+ context=context)
+ return self._error(req, context, type(ex).__name__, unicode(ex))
except Exception as ex:
extra = {'environment': req.environ}
LOG.exception(_('Unexpected error raised: %s'), unicode(ex),
diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py
index 1fa07d042..79cc3b3bf 100644
--- a/nova/api/ec2/cloud.py
+++ b/nova/api/ec2/cloud.py
@@ -27,6 +27,8 @@ import datetime
import IPy
import os
import urllib
+import tempfile
+import shutil
from nova import compute
from nova import context
@@ -281,14 +283,50 @@ class CloudController(object):
owner=None,
restorable_by=None,
**kwargs):
- return {'snapshotSet': [{'snapshotId': 'fixme',
- 'volumeId': 'fixme',
- 'status': 'fixme',
- 'startTime': 'fixme',
- 'progress': 'fixme',
- 'ownerId': 'fixme',
- 'volumeSize': 0,
- 'description': 'fixme'}]}
+ if snapshot_id:
+ snapshots = []
+ for ec2_id in snapshot_id:
+ internal_id = ec2utils.ec2_id_to_id(ec2_id)
+ snapshot = self.volume_api.get_snapshot(
+ context,
+ snapshot_id=internal_id)
+ snapshots.append(snapshot)
+ else:
+ snapshots = self.volume_api.get_all_snapshots(context)
+ snapshots = [self._format_snapshot(context, s) for s in snapshots]
+ return {'snapshotSet': snapshots}
+
+ def _format_snapshot(self, context, snapshot):
+ s = {}
+ s['snapshotId'] = ec2utils.id_to_ec2_id(snapshot['id'], 'snap-%08x')
+ s['volumeId'] = ec2utils.id_to_ec2_id(snapshot['volume_id'],
+ 'vol-%08x')
+ s['status'] = snapshot['status']
+ s['startTime'] = snapshot['created_at']
+ s['progress'] = snapshot['progress']
+ s['ownerId'] = snapshot['project_id']
+ s['volumeSize'] = snapshot['volume_size']
+ s['description'] = snapshot['display_description']
+
+ s['display_name'] = snapshot['display_name']
+ s['display_description'] = snapshot['display_description']
+ return s
+
+ def create_snapshot(self, context, volume_id, **kwargs):
+ LOG.audit(_("Create snapshot of volume %s"), volume_id,
+ context=context)
+ volume_id = ec2utils.ec2_id_to_id(volume_id)
+ snapshot = self.volume_api.create_snapshot(
+ context,
+ volume_id=volume_id,
+ name=kwargs.get('display_name'),
+ description=kwargs.get('display_description'))
+ return self._format_snapshot(context, snapshot)
+
+ def delete_snapshot(self, context, snapshot_id, **kwargs):
+ snapshot_id = ec2utils.ec2_id_to_id(snapshot_id)
+ self.volume_api.delete_snapshot(context, snapshot_id=snapshot_id)
+ return True
def describe_key_pairs(self, context, key_name=None, **kwargs):
key_pairs = db.key_pair_get_all_by_user(context, context.user_id)
@@ -316,6 +354,27 @@ class CloudController(object):
'keyMaterial': data['private_key']}
# TODO(vish): when context is no longer an object, pass it here
+ def import_public_key(self, context, key_name, public_key,
+ fingerprint=None):
+ LOG.audit(_("Import key %s"), key_name, context=context)
+ key = {}
+ key['user_id'] = context.user_id
+ key['name'] = key_name
+ key['public_key'] = public_key
+ if fingerprint is None:
+ tmpdir = tempfile.mkdtemp()
+ pubfile = os.path.join(tmpdir, 'temp.pub')
+ fh = open(pubfile, 'w')
+ fh.write(public_key)
+ fh.close()
+ (out, err) = utils.execute('ssh-keygen', '-q', '-l', '-f',
+ '%s' % (pubfile))
+ fingerprint = out.split(' ')[1]
+ shutil.rmtree(tmpdir)
+ key['fingerprint'] = fingerprint
+ db.key_pair_create(context, key)
+ return True
+
def delete_key_pair(self, context, key_name, **kwargs):
LOG.audit(_("Delete key pair %s"), key_name, context=context)
try:
@@ -596,16 +655,30 @@ class CloudController(object):
'volumeId': v['volumeId']}]
else:
v['attachmentSet'] = [{}]
+ if volume.get('snapshot_id') != None:
+ v['snapshotId'] = ec2utils.id_to_ec2_id(volume['snapshot_id'],
+ 'snap-%08x')
+ else:
+ v['snapshotId'] = None
v['display_name'] = volume['display_name']
v['display_description'] = volume['display_description']
return v
- def create_volume(self, context, size, **kwargs):
- LOG.audit(_("Create volume of %s GB"), size, context=context)
+ def create_volume(self, context, **kwargs):
+ size = kwargs.get('size')
+ if kwargs.get('snapshot_id') != None:
+ snapshot_id = ec2utils.ec2_id_to_id(kwargs['snapshot_id'])
+ LOG.audit(_("Create volume from snapshot %s"), snapshot_id,
+ context=context)
+ else:
+ snapshot_id = None
+ LOG.audit(_("Create volume of %s GB"), size, context=context)
+
volume = self.volume_api.create(
context,
size=size,
+ snapshot_id=snapshot_id,
name=kwargs.get('display_name'),
description=kwargs.get('display_description'))
# TODO(vish): Instance should be None at db layer instead of
diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py
index 311e6bde9..6c6ee22a2 100644
--- a/nova/api/openstack/auth.py
+++ b/nova/api/openstack/auth.py
@@ -17,7 +17,6 @@
import datetime
import hashlib
-import json
import time
import webob.exc
@@ -25,11 +24,9 @@ import webob.dec
from nova import auth
from nova import context
-from nova import db
from nova import exception
from nova import flags
from nova import log as logging
-from nova import manager
from nova import utils
from nova import wsgi
from nova.api.openstack import faults
@@ -102,11 +99,11 @@ class AuthMiddleware(wsgi.Middleware):
token, user = self._authorize_user(username, key, req)
if user and token:
res = webob.Response()
- res.headers['X-Auth-Token'] = token.token_hash
+ res.headers['X-Auth-Token'] = token['token_hash']
res.headers['X-Server-Management-Url'] = \
- token.server_management_url
- res.headers['X-Storage-Url'] = token.storage_url
- res.headers['X-CDN-Management-Url'] = token.cdn_management_url
+ token['server_management_url']
+ res.headers['X-Storage-Url'] = token['storage_url']
+ res.headers['X-CDN-Management-Url'] = token['cdn_management_url']
res.content_type = 'text/plain'
res.status = '204'
LOG.debug(_("Successfully authenticated '%s'") % username)
@@ -130,11 +127,11 @@ class AuthMiddleware(wsgi.Middleware):
except exception.NotFound:
return None
if token:
- delta = datetime.datetime.now() - token.created_at
+ delta = datetime.datetime.utcnow() - token['created_at']
if delta.days >= 2:
- self.db.auth_token_destroy(ctxt, token.token_hash)
+ self.db.auth_token_destroy(ctxt, token['token_hash'])
else:
- return self.auth.get_user(token.user_id)
+ return self.auth.get_user(token['user_id'])
return None
def _authorize_user(self, username, key, req):
diff --git a/nova/api/openstack/contrib/volumes.py b/nova/api/openstack/contrib/volumes.py
index 18de2ec71..b22bd2846 100644
--- a/nova/api/openstack/contrib/volumes.py
+++ b/nova/api/openstack/contrib/volumes.py
@@ -135,7 +135,7 @@ class VolumeController(wsgi.Controller):
vol = env['volume']
size = vol['size']
LOG.audit(_("Create volume of %s GB"), size, context=context)
- new_volume = self.volume_api.create(context, size,
+ new_volume = self.volume_api.create(context, size, None,
vol.get('display_name'),
vol.get('display_description'))
diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py
index 7ea7afef6..8e77b25fb 100644
--- a/nova/api/openstack/extensions.py
+++ b/nova/api/openstack/extensions.py
@@ -105,15 +105,14 @@ class ExtensionDescriptor(object):
actions = []
return actions
- def get_response_extensions(self):
- """List of extensions.ResponseExtension extension objects.
+ def get_request_extensions(self):
+ """List of extensions.RequestException extension objects.
- Response extensions are used to insert information into existing
- response data.
+ Request extensions are used to handle custom request data.
"""
- response_exts = []
- return response_exts
+ request_exts = []
+ return request_exts
class ActionExtensionController(common.OpenstackController):
@@ -137,7 +136,7 @@ class ActionExtensionController(common.OpenstackController):
return res
-class ResponseExtensionController(common.OpenstackController):
+class RequestExtensionController(common.OpenstackController):
def __init__(self, application):
self.application = application
@@ -148,20 +147,9 @@ class ResponseExtensionController(common.OpenstackController):
def process(self, req, *args, **kwargs):
res = req.get_response(self.application)
- content_type = req.best_match_content_type()
- # currently response handlers are un-ordered
+ # currently request handlers are un-ordered
for handler in self.handlers:
- res = handler(res)
- try:
- body = res.body
- headers = res.headers
- except AttributeError:
- default_xmlns = None
- body = self._serialize(res, content_type, default_xmlns)
- headers = {"Content-Type": content_type}
- res = webob.Response()
- res.body = body
- res.headers = headers
+ res = handler(req, res)
return res
@@ -226,24 +214,24 @@ class ExtensionMiddleware(wsgi.Middleware):
return action_controllers
- def _response_ext_controllers(self, application, ext_mgr, mapper):
- """Returns a dict of ResponseExtensionController-s by collection."""
- response_ext_controllers = {}
- for resp_ext in ext_mgr.get_response_extensions():
- if not resp_ext.key in response_ext_controllers.keys():
- controller = ResponseExtensionController(application)
- mapper.connect(resp_ext.url_route + '.:(format)',
+ def _request_ext_controllers(self, application, ext_mgr, mapper):
+ """Returns a dict of RequestExtensionController-s by collection."""
+ request_ext_controllers = {}
+ for req_ext in ext_mgr.get_request_extensions():
+ if not req_ext.key in request_ext_controllers.keys():
+ controller = RequestExtensionController(application)
+ mapper.connect(req_ext.url_route + '.:(format)',
action='process',
controller=controller,
- conditions=resp_ext.conditions)
+ conditions=req_ext.conditions)
- mapper.connect(resp_ext.url_route,
+ mapper.connect(req_ext.url_route,
action='process',
controller=controller,
- conditions=resp_ext.conditions)
- response_ext_controllers[resp_ext.key] = controller
+ conditions=req_ext.conditions)
+ request_ext_controllers[req_ext.key] = controller
- return response_ext_controllers
+ return request_ext_controllers
def __init__(self, application, ext_mgr=None):
@@ -271,13 +259,13 @@ class ExtensionMiddleware(wsgi.Middleware):
controller = action_controllers[action.collection]
controller.add_action(action.action_name, action.handler)
- # extended responses
- resp_controllers = self._response_ext_controllers(application, ext_mgr,
+ # extended requests
+ req_controllers = self._request_ext_controllers(application, ext_mgr,
mapper)
- for response_ext in ext_mgr.get_response_extensions():
- LOG.debug(_('Extended response: %s'), response_ext.key)
- controller = resp_controllers[response_ext.key]
- controller.add_handler(response_ext.handler)
+ for request_ext in ext_mgr.get_request_extensions():
+ LOG.debug(_('Extended request: %s'), request_ext.key)
+ controller = req_controllers[request_ext.key]
+ controller.add_handler(request_ext.handler)
self._router = routes.middleware.RoutesMiddleware(self._dispatch,
mapper)
@@ -347,17 +335,17 @@ class ExtensionManager(object):
pass
return actions
- def get_response_extensions(self):
- """Returns a list of ResponseExtension objects."""
- response_exts = []
+ def get_request_extensions(self):
+ """Returns a list of RequestExtension objects."""
+ request_exts = []
for alias, ext in self.extensions.iteritems():
try:
- response_exts.extend(ext.get_response_extensions())
+ request_exts.extend(ext.get_request_extensions())
except AttributeError:
- # NOTE(dprince): Extension aren't required to have response
+ # NOTE(dprince): Extension aren't required to have request
# extensions
pass
- return response_exts
+ return request_exts
def _check_extension(self, extension):
"""Checks for required methods in extension objects."""
@@ -421,9 +409,13 @@ class ExtensionManager(object):
self.extensions[alias] = ext
-class ResponseExtension(object):
- """Add data to responses from core nova OpenStack API controllers."""
+class RequestExtension(object):
+ """Extend requests and responses of core nova OpenStack API controllers.
+ Provide a way to add data to responses and handle custom request data
+ that is sent to core nova OpenStack API controllers.
+
+ """
def __init__(self, method, url_route, handler):
self.url_route = url_route
self.handler = handler
diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py
index 34d4c27fc..2e779da79 100644
--- a/nova/api/openstack/images.py
+++ b/nova/api/openstack/images.py
@@ -28,6 +28,8 @@ from nova.api.openstack.views import images as images_view
LOG = log.getLogger('nova.api.openstack.images')
FLAGS = flags.FLAGS
+SUPPORTED_FILTERS = ['name', 'status']
+
class Controller(common.OpenstackController):
"""Base `wsgi.Controller` for retrieving/displaying images."""
@@ -59,7 +61,8 @@ class Controller(common.OpenstackController):
:param req: `wsgi.Request` object
"""
context = req.environ['nova.context']
- images = self._image_service.index(context)
+ filters = self._get_filters(req)
+ images = self._image_service.index(context, filters)
images = common.limited(images, req)
builder = self.get_builder(req).build
return dict(images=[builder(image, detail=False) for image in images])
@@ -70,11 +73,26 @@ class Controller(common.OpenstackController):
:param req: `wsgi.Request` object.
"""
context = req.environ['nova.context']
- images = self._image_service.detail(context)
+ filters = self._get_filters(req)
+ images = self._image_service.detail(context, filters)
images = common.limited(images, req)
builder = self.get_builder(req).build
return dict(images=[builder(image, detail=True) for image in images])
+ def _get_filters(self, req):
+ """
+ Return a dictionary of query param filters from the request
+
+ :param req: the Request object coming from the wsgi layer
+ :retval a dict of key/value filters
+ """
+ filters = {}
+ for param in req.str_params:
+ if param in SUPPORTED_FILTERS or param.startswith('property-'):
+ filters[param] = req.str_params.get(param)
+
+ return filters
+
def show(self, req, id):
"""Return detailed information about a specific image.
diff --git a/nova/api/openstack/limits.py b/nova/api/openstack/limits.py
index 47bc238f1..bd0250a7f 100644
--- a/nova/api/openstack/limits.py
+++ b/nova/api/openstack/limits.py
@@ -30,6 +30,7 @@ from collections import defaultdict
from webob.dec import wsgify
+from nova import quota
from nova import wsgi
from nova.api.openstack import common
from nova.api.openstack import faults
@@ -64,7 +65,8 @@ class LimitsController(common.OpenstackController):
"""
Return all global and rate limit information.
"""
- abs_limits = {}
+ context = req.environ['nova.context']
+ abs_limits = quota.get_project_quotas(context, context.project_id)
rate_limits = req.environ.get("nova.limits", [])
builder = self._get_view_builder(req)
diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py
index 8f2de2afe..8e191c232 100644
--- a/nova/api/openstack/servers.py
+++ b/nova/api/openstack/servers.py
@@ -180,7 +180,8 @@ class Controller(common.OpenstackController):
key_name=key_name,
key_data=key_data,
metadata=env['server'].get('metadata', {}),
- injected_files=injected_files)
+ injected_files=injected_files,
+ admin_password=password)
except quota.QuotaError as error:
self._handle_quota_error(error)
@@ -190,8 +191,6 @@ class Controller(common.OpenstackController):
builder = self._get_view_builder(req)
server = builder.build(inst, is_detail=True)
server['server']['adminPass'] = password
- self.compute_api.set_admin_password(context, server['server']['id'],
- password)
return server
def _deserialize_create(self, request):
@@ -608,8 +607,8 @@ class ControllerV10(Controller):
def _parse_update(self, context, server_id, inst_dict, update_dict):
if 'adminPass' in inst_dict['server']:
- update_dict['admin_pass'] = inst_dict['server']['adminPass']
- self.compute_api.set_admin_password(context, server_id)
+ self.compute_api.set_admin_password(context, server_id,
+ inst_dict['server']['adminPass'])
def _action_rebuild(self, info, request, instance_id):
context = request.environ['nova.context']
@@ -709,14 +708,16 @@ class ControllerV11(Controller):
image_id = common.get_id_from_href(image_ref)
personalities = info["rebuild"].get("personality", [])
- metadata = info["rebuild"].get("metadata", {})
+ metadata = info["rebuild"].get("metadata")
+ name = info["rebuild"].get("name")
- self._validate_metadata(metadata)
+ if metadata:
+ self._validate_metadata(metadata)
self._decode_personalities(personalities)
try:
- self.compute_api.rebuild(context, instance_id, image_id, metadata,
- personalities)
+ self.compute_api.rebuild(context, instance_id, image_id, name,
+ metadata, personalities)
except exception.BuildInProgress:
msg = _("Instance %d is currently being rebuilt.") % instance_id
LOG.debug(msg)
diff --git a/nova/api/openstack/views/limits.py b/nova/api/openstack/views/limits.py
index 22d1c260d..e21c9f2fd 100644
--- a/nova/api/openstack/views/limits.py
+++ b/nova/api/openstack/views/limits.py
@@ -45,6 +45,34 @@ class ViewBuilder(object):
return output
+ def _build_absolute_limits(self, absolute_limits):
+ """Builder for absolute limits
+
+ absolute_limits should be given as a dict of limits.
+ For example: {"ram": 512, "gigabytes": 1024}.
+
+ """
+ limit_names = {
+ "ram": ["maxTotalRAMSize"],
+ "instances": ["maxTotalInstances"],
+ "cores": ["maxTotalCores"],
+ "metadata_items": ["maxServerMeta", "maxImageMeta"],
+ "injected_files": ["maxPersonality"],
+ "injected_file_content_bytes": ["maxPersonalitySize"],
+ }
+ limits = {}
+ for name, value in absolute_limits.iteritems():
+ if name in limit_names and value is not None:
+ for name in limit_names[name]:
+ limits[name] = value
+ return limits
+
+ def _build_rate_limits(self, rate_limits):
+ raise NotImplementedError()
+
+ def _build_rate_limit(self, rate_limit):
+ raise NotImplementedError()
+
class ViewBuilderV10(ViewBuilder):
"""Openstack API v1.0 limits view builder."""
@@ -63,9 +91,6 @@ class ViewBuilderV10(ViewBuilder):
"resetTime": rate_limit["resetTime"],
}
- def _build_absolute_limits(self, absolute_limit):
- return {}
-
class ViewBuilderV11(ViewBuilder):
"""Openstack API v1.1 limits view builder."""
@@ -79,7 +104,7 @@ class ViewBuilderV11(ViewBuilder):
# check for existing key
for limit in limits:
if limit["uri"] == rate_limit["URI"] and \
- limit["regex"] == limit["regex"]:
+ limit["regex"] == rate_limit["regex"]:
_rate_limit_key = limit
break
@@ -104,6 +129,3 @@ class ViewBuilderV11(ViewBuilder):
"unit": rate_limit["unit"],
"next-available": rate_limit["resetTime"],
}
-
- def _build_absolute_limits(self, absolute_limit):
- return {}
diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py
index 70653dc0e..af73d8f6d 100644
--- a/nova/api/openstack/zones.py
+++ b/nova/api/openstack/zones.py
@@ -18,6 +18,7 @@ import urlparse
from nova import crypto
from nova import db
+from nova import exception
from nova import flags
from nova import log as logging
from nova.api.openstack import common
@@ -51,14 +52,6 @@ def _scrub_zone(zone):
'deleted', 'deleted_at', 'updated_at'))
-def check_encryption_key(func):
- def wrapped(*args, **kwargs):
- if not FLAGS.build_plan_encryption_key:
- raise exception.Error(_("--build_plan_encryption_key not set"))
- return func(*args, **kwargs)
- return wrapped
-
-
class Controller(common.OpenstackController):
_serialization_metadata = {
@@ -116,7 +109,6 @@ class Controller(common.OpenstackController):
zone = api.zone_update(context, zone_id, env["zone"])
return dict(zone=_scrub_zone(zone))
- @check_encryption_key
def select(self, req):
"""Returns a weighted list of costs to create instances
of desired capabilities."""
@@ -137,6 +129,9 @@ class Controller(common.OpenstackController):
"""Remove all the confidential data and return a sanitized
version of the build plan. Include an encrypted full version
of the weighting entry so we can get back to it later."""
+ if not FLAGS.build_plan_encryption_key:
+ raise exception.FlagNotSet(flag='build_plan_encryption_key')
+
encryptor = crypto.encryptor(FLAGS.build_plan_encryption_key)
cooked = []
for entry in build_plan:
diff --git a/nova/auth/novarc.template b/nova/auth/novarc.template
index cda2ecc28..8170fcafe 100644
--- a/nova/auth/novarc.template
+++ b/nova/auth/novarc.template
@@ -1,4 +1,4 @@
-NOVA_KEY_DIR=$(pushd $(dirname $BASH_SOURCE)>/dev/null; pwd; popd>/dev/null)
+NOVA_KEY_DIR=$(dirname $(readlink -f ${BASH_SOURCE}))
export EC2_ACCESS_KEY="%(access)s:%(project)s"
export EC2_SECRET_KEY="%(secret)s"
export EC2_URL="%(ec2)s"
diff --git a/nova/compute/api.py b/nova/compute/api.py
index 4ddbbd0e2..d6330065e 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -95,14 +95,15 @@ class API(base.Base):
"""
if injected_files is None:
return
- limit = quota.allowed_injected_files(context)
+ limit = quota.allowed_injected_files(context, len(injected_files))
if len(injected_files) > limit:
raise quota.QuotaError(code="OnsetFileLimitExceeded")
path_limit = quota.allowed_injected_file_path_bytes(context)
- content_limit = quota.allowed_injected_file_content_bytes(context)
for path, content in injected_files:
if len(path) > path_limit:
raise quota.QuotaError(code="OnsetFilePathLimitExceeded")
+ content_limit = quota.allowed_injected_file_content_bytes(
+ context, len(content))
if len(content) > content_limit:
raise quota.QuotaError(code="OnsetFileContentLimitExceeded")
@@ -134,7 +135,8 @@ class API(base.Base):
display_name='', display_description='',
key_name=None, key_data=None, security_group='default',
availability_zone=None, user_data=None, metadata={},
- injected_files=None):
+ injected_files=None,
+ admin_password=None):
"""Create the number and type of instances requested.
Verifies that quota and other arguments are valid.
@@ -149,9 +151,13 @@ class API(base.Base):
pid = context.project_id
LOG.warn(_("Quota exceeeded for %(pid)s,"
" tried to run %(min_count)s instances") % locals())
- raise quota.QuotaError(_("Instance quota exceeded. You can only "
- "run %s more instances of this type.") %
- num_instances, "InstanceLimitExceeded")
+ if num_instances <= 0:
+ message = _("Instance quota exceeded. You cannot run any "
+ "more instances of this type.")
+ else:
+ message = _("Instance quota exceeded. You can only run %s "
+ "more instances of this type.") % num_instances
+ raise quota.QuotaError(message, "InstanceLimitExceeded")
self._check_metadata_properties_quota(context, metadata)
self._check_injected_file_quota(context, injected_files)
@@ -269,7 +275,8 @@ class API(base.Base):
'InstanceTypeFilter'
},
"availability_zone": availability_zone,
- "injected_files": injected_files}})
+ "injected_files": injected_files,
+ "admin_password": admin_password}})
for group_id in security_groups:
self.trigger_security_group_members_refresh(elevated, group_id)
@@ -508,15 +515,6 @@ class API(base.Base):
raise exception.Error(_("Unable to find host for Instance %s")
% instance_id)
- def _set_admin_password(self, context, instance_id, password):
- """Set the root/admin password for the given instance."""
- host = self._find_host(context, instance_id)
-
- rpc.cast(context,
- self.db.queue_get_for(context, FLAGS.compute_topic, host),
- {"method": "set_admin_password",
- "args": {"instance_id": instance_id, "new_pass": password}})
-
def snapshot(self, context, instance_id, name):
"""Snapshot the given instance.
@@ -537,7 +535,7 @@ class API(base.Base):
"""Reboot the given instance."""
self._cast_compute_message('reboot_instance', context, instance_id)
- def rebuild(self, context, instance_id, image_id, metadata=None,
+ def rebuild(self, context, instance_id, image_id, name=None, metadata=None,
files_to_inject=None):
"""Rebuild the given instance with the provided metadata."""
instance = db.api.instance_get(context, instance_id)
@@ -546,13 +544,16 @@ class API(base.Base):
msg = _("Instance already building")
raise exception.BuildInProgress(msg)
- metadata = metadata or {}
- self._check_metadata_properties_quota(context, metadata)
-
files_to_inject = files_to_inject or []
self._check_injected_file_quota(context, files_to_inject)
- self.db.instance_update(context, instance_id, {"metadata": metadata})
+ values = {}
+ if metadata is not None:
+ self._check_metadata_properties_quota(context, metadata)
+ values['metadata'] = metadata
+ if name is not None:
+ values['display_name'] = name
+ self.db.instance_update(context, instance_id, values)
rebuild_params = {
"image_id": image_id,
@@ -670,8 +671,12 @@ class API(base.Base):
def set_admin_password(self, context, instance_id, password=None):
"""Set the root/admin password for the given instance."""
- eventlet.spawn_n(self._set_admin_password(context, instance_id,
- password))
+ host = self._find_host(context, instance_id)
+
+ rpc.cast(context,
+ self.db.queue_get_for(context, FLAGS.compute_topic, host),
+ {"method": "set_admin_password",
+ "args": {"instance_id": instance_id, "new_pass": password}})
def inject_file(self, context, instance_id):
"""Write a file to the given instance."""
diff --git a/nova/compute/manager.py b/nova/compute/manager.py
index 923feaa59..3897b3a9e 100644
--- a/nova/compute/manager.py
+++ b/nova/compute/manager.py
@@ -221,6 +221,7 @@ class ComputeManager(manager.SchedulerDependentManager):
context = context.elevated()
instance_ref = self.db.instance_get(context, instance_id)
instance_ref.injected_files = kwargs.get('injected_files', [])
+ instance_ref.admin_pass = kwargs.get('admin_password', None)
if instance_ref['name'] in self.driver.list_instances():
raise exception.Error(_("Instance has already been created"))
LOG.audit(_("instance %s: starting..."), instance_id,
@@ -330,7 +331,7 @@ class ComputeManager(manager.SchedulerDependentManager):
@exception.wrap_exception
@checks_instance_lock
- def rebuild_instance(self, context, instance_id, image_id):
+ def rebuild_instance(self, context, instance_id, **kwargs):
"""Destroy and re-make this instance.
A 'rebuild' effectively purges all existing data from the system and
@@ -348,7 +349,8 @@ class ComputeManager(manager.SchedulerDependentManager):
self._update_state(context, instance_id, power_state.BUILDING)
self.driver.destroy(instance_ref)
- instance_ref.image_id = image_id
+ instance_ref.image_id = kwargs.get('image_id')
+ instance_ref.injected_files = kwargs.get('injected_files', [])
self.driver.spawn(instance_ref)
self._update_image_id(context, instance_id, image_id)
@@ -405,22 +407,28 @@ class ComputeManager(manager.SchedulerDependentManager):
@exception.wrap_exception
@checks_instance_lock
def set_admin_password(self, context, instance_id, new_pass=None):
- """Set the root/admin password for an instance on this host."""
+ """Set the root/admin password for an instance on this host.
+
+ This is generally only called by API password resets after an
+ image has been built.
+ """
+
context = context.elevated()
if new_pass is None:
# Generate a random password
new_pass = utils.generate_password(FLAGS.password_length)
- while True:
+ max_tries = 10
+
+ for i in xrange(max_tries):
instance_ref = self.db.instance_get(context, instance_id)
instance_id = instance_ref["id"]
instance_state = instance_ref["state"]
expected_state = power_state.RUNNING
if instance_state != expected_state:
- time.sleep(5)
- continue
+ raise exception.Error(_('Instance is not running'))
else:
try:
self.driver.set_admin_password(instance_ref, new_pass)
@@ -436,6 +444,12 @@ class ComputeManager(manager.SchedulerDependentManager):
except Exception, e:
# Catch all here because this could be anything.
LOG.exception(e)
+ if i == max_tries - 1:
+ # At some point this exception may make it back
+ # to the API caller, and we don't want to reveal
+ # too much. The real exception is logged above
+ raise exception.Error(_('Internal error'))
+ time.sleep(1)
continue
@exception.wrap_exception
@@ -628,7 +642,7 @@ class ComputeManager(manager.SchedulerDependentManager):
instance_type = self.db.instance_type_get_by_flavor_id(context,
migration_ref['new_flavor_id'])
self.db.instance_update(context, instance_id,
- dict(instance_type=instance_type['name'],
+ dict(instance_type_id=instance_type['id'],
memory_mb=instance_type['memory_mb'],
vcpus=instance_type['vcpus'],
local_gb=instance_type['local_gb']))
diff --git a/nova/db/api.py b/nova/db/api.py
index ef8aa1143..4e0aa60a2 100644
--- a/nova/db/api.py
+++ b/nova/db/api.py
@@ -47,6 +47,8 @@ flags.DEFINE_string('instance_name_template', 'instance-%08x',
'Template string to be used to generate instance names')
flags.DEFINE_string('volume_name_template', 'volume-%08x',
'Template string to be used to generate instance names')
+flags.DEFINE_string('snapshot_name_template', 'snapshot-%08x',
+ 'Template string to be used to generate snapshot names')
IMPL = utils.LazyPluggable(FLAGS['db_backend'],
@@ -403,7 +405,7 @@ def instance_create(context, values):
def instance_data_get_for_project(context, project_id):
- """Get (instance_count, core_count) for project."""
+ """Get (instance_count, total_cores, total_ram) for project."""
return IMPL.instance_data_get_for_project(context, project_id)
@@ -881,6 +883,43 @@ def volume_update(context, volume_id, values):
####################
+def snapshot_create(context, values):
+ """Create a snapshot from the values dictionary."""
+ return IMPL.snapshot_create(context, values)
+
+
+def snapshot_destroy(context, snapshot_id):
+ """Destroy the snapshot or raise if it does not exist."""
+ return IMPL.snapshot_destroy(context, snapshot_id)
+
+
+def snapshot_get(context, snapshot_id):
+ """Get a snapshot or raise if it does not exist."""
+ return IMPL.snapshot_get(context, snapshot_id)
+
+
+def snapshot_get_all(context):
+ """Get all snapshots."""
+ return IMPL.snapshot_get_all(context)
+
+
+def snapshot_get_all_by_project(context, project_id):
+ """Get all snapshots belonging to a project."""
+ return IMPL.snapshot_get_all_by_project(context, project_id)
+
+
+def snapshot_update(context, snapshot_id, values):
+ """Set the given properties on an snapshot and update it.
+
+ Raises NotFound if snapshot does not exist.
+
+ """
+ return IMPL.snapshot_update(context, snapshot_id, values)
+
+
+####################
+
+
def security_group_get_all(context):
"""Get all security groups."""
return IMPL.security_group_get_all(context)
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index 3681f30db..c3a971a82 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -771,6 +771,15 @@ def fixed_ip_update(context, address, values):
###################
+def _metadata_refs(metadata_dict):
+ metadata_refs = []
+ if metadata_dict:
+ for k, v in metadata_dict.iteritems():
+ metadata_ref = models.InstanceMetadata()
+ metadata_ref['key'] = k
+ metadata_ref['value'] = v
+ metadata_refs.append(metadata_ref)
+ return metadata_refs
@require_context
@@ -780,15 +789,7 @@ def instance_create(context, values):
context - request context object
values - dict containing column values.
"""
- metadata = values.get('metadata')
- metadata_refs = []
- if metadata:
- for k, v in metadata.iteritems():
- metadata_ref = models.InstanceMetadata()
- metadata_ref['key'] = k
- metadata_ref['value'] = v
- metadata_refs.append(metadata_ref)
- values['metadata'] = metadata_refs
+ values['metadata'] = _metadata_refs(values.get('metadata'))
instance_ref = models.Instance()
instance_ref.update(values)
@@ -803,12 +804,13 @@ def instance_create(context, values):
def instance_data_get_for_project(context, project_id):
session = get_session()
result = session.query(func.count(models.Instance.id),
- func.sum(models.Instance.vcpus)).\
+ func.sum(models.Instance.vcpus),
+ func.sum(models.Instance.memory_mb)).\
filter_by(project_id=project_id).\
filter_by(deleted=False).\
first()
# NOTE(vish): convert None to 0
- return (result[0] or 0, result[1] or 0)
+ return (result[0] or 0, result[1] or 0, result[2] or 0)
@require_context
@@ -873,6 +875,7 @@ def instance_get_all(context):
options(joinedload_all('fixed_ip.floating_ips')).\
options(joinedload('security_groups')).\
options(joinedload_all('fixed_ip.network')).\
+ options(joinedload('metadata')).\
options(joinedload('instance_type')).\
filter_by(deleted=can_read_deleted(context)).\
all()
@@ -885,6 +888,7 @@ def instance_get_all_by_user(context, user_id):
options(joinedload_all('fixed_ip.floating_ips')).\
options(joinedload('security_groups')).\
options(joinedload_all('fixed_ip.network')).\
+ options(joinedload('metadata')).\
options(joinedload('instance_type')).\
filter_by(deleted=can_read_deleted(context)).\
filter_by(user_id=user_id).\
@@ -1007,6 +1011,11 @@ def instance_set_state(context, instance_id, state, description=None):
@require_context
def instance_update(context, instance_id, values):
session = get_session()
+ metadata = values.get('metadata')
+ if metadata is not None:
+ instance_metadata_delete_all(context, instance_id)
+ instance_metadata_update_or_create(context, instance_id,
+ values.pop('metadata'))
with session.begin():
instance_ref = instance_get(context, instance_id, session=session)
instance_ref.update(values)
@@ -1497,7 +1506,7 @@ def auth_token_create(_context, token):
###################
-@require_admin_context
+@require_context
def quota_get(context, project_id, resource, session=None):
if not session:
session = get_session()
@@ -1511,7 +1520,7 @@ def quota_get(context, project_id, resource, session=None):
return result
-@require_admin_context
+@require_context
def quota_get_all_by_project(context, project_id):
session = get_session()
result = {'project_id': project_id}
@@ -1787,6 +1796,82 @@ def volume_update(context, volume_id, values):
@require_context
+def snapshot_create(context, values):
+ snapshot_ref = models.Snapshot()
+ snapshot_ref.update(values)
+
+ session = get_session()
+ with session.begin():
+ snapshot_ref.save(session=session)
+ return snapshot_ref
+
+
+@require_admin_context
+def snapshot_destroy(context, snapshot_id):
+ session = get_session()
+ with session.begin():
+ session.query(models.Snapshot).\
+ filter_by(id=snapshot_id).\
+ update({'deleted': 1,
+ 'deleted_at': datetime.datetime.utcnow(),
+ 'updated_at': literal_column('updated_at')})
+
+
+@require_context
+def snapshot_get(context, snapshot_id, session=None):
+ if not session:
+ session = get_session()
+ result = None
+
+ if is_admin_context(context):
+ result = session.query(models.Snapshot).\
+ filter_by(id=snapshot_id).\
+ filter_by(deleted=can_read_deleted(context)).\
+ first()
+ elif is_user_context(context):
+ result = session.query(models.Snapshot).\
+ filter_by(project_id=context.project_id).\
+ filter_by(id=snapshot_id).\
+ filter_by(deleted=False).\
+ first()
+ if not result:
+ raise exception.SnapshotNotFound(snapshot_id=snapshot_id)
+
+ return result
+
+
+@require_admin_context
+def snapshot_get_all(context):
+ session = get_session()
+ return session.query(models.Snapshot).\
+ filter_by(deleted=can_read_deleted(context)).\
+ all()
+
+
+@require_context
+def snapshot_get_all_by_project(context, project_id):
+ authorize_project_context(context, project_id)
+
+ session = get_session()
+ return session.query(models.Snapshot).\
+ filter_by(project_id=project_id).\
+ filter_by(deleted=can_read_deleted(context)).\
+ all()
+
+
+@require_context
+def snapshot_update(context, snapshot_id, values):
+ session = get_session()
+ with session.begin():
+ snapshot_ref = snapshot_get(context, snapshot_id, session=session)
+ snapshot_ref.update(values)
+ snapshot_ref.save(session=session)
+
+
+###################
+
+
+@require_context
def security_group_get_all(context):
session = get_session()
return session.query(models.SecurityGroup).\
@@ -2547,6 +2632,17 @@ def instance_metadata_delete(context, instance_id, key):
@require_context
+def instance_metadata_delete_all(context, instance_id):
+ session = get_session()
+ session.query(models.InstanceMetadata).\
+ filter_by(instance_id=instance_id).\
+ filter_by(deleted=False).\
+ update({'deleted': True,
+ 'deleted_at': datetime.datetime.utcnow(),
+ 'updated_at': literal_column('updated_at')})
+
+
+@require_context
def instance_metadata_get_item(context, instance_id, key):
session = get_session()
@@ -2565,6 +2661,9 @@ def instance_metadata_get_item(context, instance_id, key):
@require_context
def instance_metadata_update_or_create(context, instance_id, metadata):
session = get_session()
+
+ original_metadata = instance_metadata_get(context, instance_id)
+
meta_ref = None
for key, value in metadata.iteritems():
try:
@@ -2576,4 +2675,5 @@ def instance_metadata_update_or_create(context, instance_id, metadata):
"instance_id": instance_id,
"deleted": 0})
meta_ref.save(session=session)
+
return metadata
diff --git a/nova/db/sqlalchemy/migrate_repo/versions/017_make_instance_type_id_an_integer.py b/nova/db/sqlalchemy/migrate_repo/versions/017_make_instance_type_id_an_integer.py
new file mode 100644
index 000000000..cda890c94
--- /dev/null
+++ b/nova/db/sqlalchemy/migrate_repo/versions/017_make_instance_type_id_an_integer.py
@@ -0,0 +1,68 @@
+from sqlalchemy import Column, Integer, MetaData, String, Table
+from nova import log as logging
+
+meta = MetaData()
+
+
+def upgrade(migrate_engine):
+ meta.bind = migrate_engine
+ instances = Table('instances', meta, autoload=True,
+ autoload_with=migrate_engine)
+
+ types = {}
+ for instance in migrate_engine.execute(instances.select()):
+ if instance.instance_type_id is None:
+ types[instance.id] = None
+ continue
+ try:
+ types[instance.id] = int(instance.instance_type_id)
+ except ValueError:
+ logging.warn("Instance %s did not have instance_type_id "
+ "converted to an integer because its value is %s" %
+ (instance.id, instance.instance_type_id))
+ types[instance.id] = None
+
+ integer_column = Column('instance_type_id_int', Integer(), nullable=True)
+ string_column = instances.c.instance_type_id
+
+ integer_column.create(instances)
+ for instance_id, instance_type_id in types.iteritems():
+ update = instances.update().\
+ where(instances.c.id == instance_id).\
+ values(instance_type_id_int=instance_type_id)
+ migrate_engine.execute(update)
+
+ string_column.alter(name='instance_type_id_str')
+ integer_column.alter(name='instance_type_id')
+ string_column.drop()
+
+
+def downgrade(migrate_engine):
+ meta.bind = migrate_engine
+ instances = Table('instances', meta, autoload=True,
+ autoload_with=migrate_engine)
+
+ integer_column = instances.c.instance_type_id
+ string_column = Column('instance_type_id_str',
+ String(length=255, convert_unicode=False,
+ assert_unicode=None, unicode_error=None,
+ _warn_on_bytestring=False),
+ nullable=True)
+
+ types = {}
+ for instance in migrate_engine.execute(instances.select()):
+ if instance.instance_type_id is None:
+ types[instance.id] = None
+ else:
+ types[instance.id] = str(instance.instance_type_id)
+
+ string_column.create(instances)
+ for instance_id, instance_type_id in types.iteritems():
+ update = instances.update().\
+ where(instances.c.id == instance_id).\
+ values(instance_type_id_str=instance_type_id)
+ migrate_engine.execute(update)
+
+ integer_column.alter(name='instance_type_id_int')
+ string_column.alter(name='instance_type_id')
+ integer_column.drop()
diff --git a/nova/db/sqlalchemy/migrate_repo/versions/018_rename_server_management_url.py b/nova/db/sqlalchemy/migrate_repo/versions/018_rename_server_management_url.py
new file mode 100644
index 000000000..a169afb40
--- /dev/null
+++ b/nova/db/sqlalchemy/migrate_repo/versions/018_rename_server_management_url.py
@@ -0,0 +1,60 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 OpenStack LLC.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from sqlalchemy import Column, Integer, MetaData, String, Table
+#from nova import log as logging
+
+meta = MetaData()
+
+c_manageent = Column('server_manageent_url',
+ String(length=255, convert_unicode=False,
+ assert_unicode=None, unicode_error=None,
+ _warn_on_bytestring=False),
+ nullable=True)
+
+c_management = Column('server_management_url',
+ String(length=255, convert_unicode=False,
+ assert_unicode=None, unicode_error=None,
+ _warn_on_bytestring=False),
+ nullable=True)
+
+
+def upgrade(migrate_engine):
+ # Upgrade operations go here. Don't create your own engine;
+ # bind migrate_engine to your metadata
+ meta.bind = migrate_engine
+
+ tokens = Table('auth_tokens', meta, autoload=True,
+ autoload_with=migrate_engine)
+
+ tokens.create_column(c_management)
+ migrate_engine.execute(tokens.update()
+ .values(server_management_url=tokens.c.server_manageent_url))
+
+ tokens.c.server_manageent_url.drop()
+
+
+def downgrade(migrate_engine):
+ meta.bind = migrate_engine
+
+ tokens = Table('auth_tokens', meta, autoload=True,
+ autoload_with=migrate_engine)
+
+ tokens.create_column(c_manageent)
+ migrate_engine.execute(tokens.update()
+ .values(server_manageent_url=tokens.c.server_management_url))
+
+ tokens.c.server_management_url.drop()
diff --git a/nova/db/sqlalchemy/migrate_repo/versions/019_add_volume_snapshot_support.py b/nova/db/sqlalchemy/migrate_repo/versions/019_add_volume_snapshot_support.py
new file mode 100644
index 000000000..f16d6db56
--- /dev/null
+++ b/nova/db/sqlalchemy/migrate_repo/versions/019_add_volume_snapshot_support.py
@@ -0,0 +1,70 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 MORITA Kazutaka.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from sqlalchemy import Column, Table, MetaData
+from sqlalchemy import Integer, DateTime, Boolean, String
+
+from nova import log as logging
+
+meta = MetaData()
+
+snapshots = Table('snapshots', meta,
+ Column('created_at', DateTime(timezone=False)),
+ Column('updated_at', DateTime(timezone=False)),
+ Column('deleted_at', DateTime(timezone=False)),
+ Column('deleted', Boolean(create_constraint=True, name=None)),
+ Column('id', Integer(), primary_key=True, nullable=False),
+ Column('volume_id', Integer(), nullable=False),
+ Column('user_id',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False)),
+ Column('project_id',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False)),
+ Column('status',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False)),
+ Column('progress',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False)),
+ Column('volume_size', Integer()),
+ Column('scheduled_at', DateTime(timezone=False)),
+ Column('display_name',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False)),
+ Column('display_description',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False)))
+
+
+def upgrade(migrate_engine):
+ # Upgrade operations go here. Don't create your own engine;
+ # bind migrate_engine to your metadata
+ meta.bind = migrate_engine
+
+ try:
+ snapshots.create()
+ except Exception:
+ logging.info(repr(snapshots))
+ logging.exception('Exception while creating table')
+ meta.drop_all(tables=[snapshots])
+ raise
+
+
+def downgrade(migrate_engine):
+ # Operations to reverse the above upgrade go here.
+ snapshots.drop()
diff --git a/nova/db/sqlalchemy/migrate_repo/versions/020_add_snapshot_id_to_volumes.py b/nova/db/sqlalchemy/migrate_repo/versions/020_add_snapshot_id_to_volumes.py
new file mode 100644
index 000000000..10bd9d5c9
--- /dev/null
+++ b/nova/db/sqlalchemy/migrate_repo/versions/020_add_snapshot_id_to_volumes.py
@@ -0,0 +1,47 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 MORITA Kazutaka.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from sqlalchemy import Column, Table, MetaData, Integer
+
+from nova import log as logging
+
+
+meta = MetaData()
+
+
+# Table stub-definitions
+# Just for the ForeignKey and column creation to succeed, these are not the
+# actual definitions of instances or services.
+#
+volumes = Table('volumes', meta,
+ Column('id', Integer(), primary_key=True, nullable=False),
+ )
+
+#
+# New Column
+#
+
+snapshot_id = Column('snapshot_id', Integer())
+
+
+def upgrade(migrate_engine):
+ # Upgrade operations go here. Don't create your own engine;
+ # bind migrate_engine to your metadata
+ meta.bind = migrate_engine
+
+ # Add columns to existing tables
+ volumes.create_column(snapshot_id)
diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py
index 0b46d5a05..22a1a84e8 100644
--- a/nova/db/sqlalchemy/models.py
+++ b/nova/db/sqlalchemy/models.py
@@ -209,7 +209,7 @@ class Instance(BASE, NovaBase):
hostname = Column(String(255))
host = Column(String(255)) # , ForeignKey('hosts.id'))
- instance_type_id = Column(String(255))
+ instance_type_id = Column(Integer)
user_data = Column(Text)
@@ -287,6 +287,8 @@ class Volume(BASE, NovaBase):
user_id = Column(String(255))
project_id = Column(String(255))
+ snapshot_id = Column(String(255))
+
host = Column(String(255)) # , ForeignKey('hosts.id'))
size = Column(Integer)
availability_zone = Column(String(255)) # TODO(vish): foreign key?
@@ -329,6 +331,31 @@ class Quota(BASE, NovaBase):
hard_limit = Column(Integer, nullable=True)
+class Snapshot(BASE, NovaBase):
+ """Represents a block storage device that can be attached to a vm."""
+ __tablename__ = 'snapshots'
+ id = Column(Integer, primary_key=True, autoincrement=True)
+
+ @property
+ def name(self):
+ return FLAGS.snapshot_name_template % self.id
+
+ @property
+ def volume_name(self):
+ return FLAGS.volume_name_template % self.volume_id
+
+ user_id = Column(String(255))
+ project_id = Column(String(255))
+
+ volume_id = Column(Integer)
+ status = Column(String(255))
+ progress = Column(String(255))
+ volume_size = Column(Integer)
+
+ display_name = Column(String(255))
+ display_description = Column(String(255))
+
+
class ExportDevice(BASE, NovaBase):
"""Represates a shelf and blade that a volume can be exported on."""
__tablename__ = 'export_devices'
@@ -495,7 +522,7 @@ class AuthToken(BASE, NovaBase):
__tablename__ = 'auth_tokens'
token_hash = Column(String(255), primary_key=True)
user_id = Column(String(255))
- server_manageent_url = Column(String(255))
+ server_management_url = Column(String(255))
storage_url = Column(String(255))
cdn_management_url = Column(String(255))
diff --git a/nova/exception.py b/nova/exception.py
index 16c443c61..36ca51093 100644
--- a/nova/exception.py
+++ b/nova/exception.py
@@ -255,6 +255,10 @@ class NotFound(NovaException):
super(NotFound, self).__init__(**kwargs)
+class FlagNotSet(NotFound):
+ message = _("Required flag %(flag)s not set.")
+
+
class InstanceNotFound(NotFound):
message = _("Instance %(instance_id)s could not be found.")
@@ -267,6 +271,14 @@ class VolumeNotFoundForInstance(VolumeNotFound):
message = _("Volume not found for instance %(instance_id)s.")
+class SnapshotNotFound(NotFound):
+ message = _("Snapshot %(snapshot_id)s could not be found.")
+
+
+class VolumeIsBusy(Error):
+ message = _("deleting volume %(volume_name)s that has snapshot")
+
+
class ExportDeviceNotFoundForVolume(NotFound):
message = _("No export device found for volume %(volume_id)s.")
diff --git a/nova/fakerabbit.py b/nova/fakerabbit.py
index a7dee8caf..e7e9dab77 100644
--- a/nova/fakerabbit.py
+++ b/nova/fakerabbit.py
@@ -31,6 +31,7 @@ LOG = logging.getLogger("nova.fakerabbit")
EXCHANGES = {}
QUEUES = {}
+CONSUMERS = {}
class Message(base.BaseMessage):
@@ -96,17 +97,29 @@ class Backend(base.BaseBackend):
' key %(routing_key)s') % locals())
EXCHANGES[exchange].bind(QUEUES[queue].push, routing_key)
- def declare_consumer(self, queue, callback, *args, **kwargs):
- self.current_queue = queue
- self.current_callback = callback
+ def declare_consumer(self, queue, callback, consumer_tag, *args, **kwargs):
+ global CONSUMERS
+ LOG.debug("Adding consumer %s", consumer_tag)
+ CONSUMERS[consumer_tag] = (queue, callback)
+
+ def cancel(self, consumer_tag):
+ global CONSUMERS
+ LOG.debug("Removing consumer %s", consumer_tag)
+ del CONSUMERS[consumer_tag]
def consume(self, limit=None):
+ global CONSUMERS
+ num = 0
while True:
- item = self.get(self.current_queue)
- if item:
- self.current_callback(item)
- raise StopIteration()
- greenthread.sleep(0)
+ for (queue, callback) in CONSUMERS.itervalues():
+ item = self.get(queue)
+ if item:
+ callback(item)
+ num += 1
+ yield
+ if limit and num == limit:
+ raise StopIteration()
+ greenthread.sleep(0.1)
def get(self, queue, no_ack=False):
global QUEUES
@@ -134,5 +147,7 @@ class Backend(base.BaseBackend):
def reset_all():
global EXCHANGES
global QUEUES
+ global CONSUMERS
EXCHANGES = {}
QUEUES = {}
+ CONSUMERS = {}
diff --git a/nova/flags.py b/nova/flags.py
index 519793643..9eaac5596 100644
--- a/nova/flags.py
+++ b/nova/flags.py
@@ -110,7 +110,7 @@ class FlagValues(gflags.FlagValues):
return name in self.__dict__['__dirty']
def ClearDirty(self):
- self.__dict__['__is_dirty'] = []
+ self.__dict__['__dirty'] = []
def WasAlreadyParsed(self):
return self.__dict__['__was_already_parsed']
@@ -119,11 +119,12 @@ class FlagValues(gflags.FlagValues):
if '__stored_argv' not in self.__dict__:
return
new_flags = FlagValues(self)
- for k in self.__dict__['__dirty']:
+ for k in self.FlagDict().iterkeys():
new_flags[k] = gflags.FlagValues.__getitem__(self, k)
+ new_flags.Reset()
new_flags(self.__dict__['__stored_argv'])
- for k in self.__dict__['__dirty']:
+ for k in new_flags.FlagDict().iterkeys():
setattr(self, k, getattr(new_flags, k))
self.ClearDirty()
@@ -369,6 +370,9 @@ DEFINE_string('host', socket.gethostname(),
DEFINE_string('node_availability_zone', 'nova',
'availability zone of this node')
+DEFINE_string('notification_driver',
+ 'nova.notifier.no_op_notifier',
+ 'Default driver for sending notifications')
DEFINE_list('memcached_servers', None,
'Memcached servers or None for in process cache.')
diff --git a/nova/image/fake.py b/nova/image/fake.py
index b400b2adb..8e84c8597 100644
--- a/nova/image/fake.py
+++ b/nova/image/fake.py
@@ -52,11 +52,11 @@ class FakeImageService(service.BaseImageService):
self.create(None, image)
super(FakeImageService, self).__init__()
- def index(self, context):
+ def index(self, context, filters=None):
"""Returns list of images."""
return copy.deepcopy(self.images.values())
- def detail(self, context):
+ def detail(self, context, filters=None):
"""Return list of detailed image information."""
return copy.deepcopy(self.images.values())
diff --git a/nova/image/glance.py b/nova/image/glance.py
index 193e37273..dec797619 100644
--- a/nova/image/glance.py
+++ b/nova/image/glance.py
@@ -58,23 +58,23 @@ class GlanceImageService(service.BaseImageService):
else:
self.client = client
- def index(self, context):
+ def index(self, context, filters=None):
"""Calls out to Glance for a list of images available."""
# NOTE(sirp): We need to use `get_images_detailed` and not
# `get_images` here because we need `is_public` and `properties`
# included so we can filter by user
filtered = []
- image_metas = self.client.get_images_detailed()
+ image_metas = self.client.get_images_detailed(filters=filters)
for image_meta in image_metas:
if self._is_image_available(context, image_meta):
meta_subset = utils.subset_dict(image_meta, ('id', 'name'))
filtered.append(meta_subset)
return filtered
- def detail(self, context):
+ def detail(self, context, filters=None):
"""Calls out to Glance for a list of detailed image information."""
filtered = []
- image_metas = self.client.get_images_detailed()
+ image_metas = self.client.get_images_detailed(filters=filters)
for image_meta in image_metas:
if self._is_image_available(context, image_meta):
base_image_meta = self._translate_to_base(image_meta)
diff --git a/nova/image/local.py b/nova/image/local.py
index 918180bae..677d5302b 100644
--- a/nova/image/local.py
+++ b/nova/image/local.py
@@ -63,7 +63,7 @@ class LocalImageService(service.BaseImageService):
images.append(unhexed_image_id)
return images
- def index(self, context):
+ def index(self, context, *args, **kwargs):
filtered = []
image_metas = self.detail(context)
for image_meta in image_metas:
@@ -71,7 +71,7 @@ class LocalImageService(service.BaseImageService):
filtered.append(meta)
return filtered
- def detail(self, context):
+ def detail(self, context, *args, **kwargs):
images = []
for image_id in self._ids():
try:
diff --git a/nova/image/service.py b/nova/image/service.py
index ab6749049..5361cfc89 100644
--- a/nova/image/service.py
+++ b/nova/image/service.py
@@ -46,7 +46,7 @@ class BaseImageService(object):
# the ImageService subclass
SERVICE_IMAGE_ATTRS = []
- def index(self, context):
+ def index(self, context, *args, **kwargs):
"""List images.
:returns: a sequence of mappings with the following signature
@@ -55,7 +55,7 @@ class BaseImageService(object):
"""
raise NotImplementedError
- def detail(self, context):
+ def detail(self, context, *args, **kwargs):
"""Detailed information about an images.
:returns: a sequence of mappings with the following signature
diff --git a/nova/network/api.py b/nova/network/api.py
index 1d8193b28..e2eacdf42 100644
--- a/nova/network/api.py
+++ b/nova/network/api.py
@@ -16,9 +16,7 @@
# License for the specific language governing permissions and limitations
# under the License.
-"""
-Handles all requests relating to instances (guest vms).
-"""
+"""Handles all requests relating to instances (guest vms)."""
from nova import db
from nova import exception
@@ -28,6 +26,7 @@ from nova import quota
from nova import rpc
from nova.db import base
+
FLAGS = flags.FLAGS
LOG = logging.getLogger('nova.network')
@@ -37,19 +36,19 @@ class API(base.Base):
def allocate_floating_ip(self, context):
if quota.allowed_floating_ips(context, 1) < 1:
- LOG.warn(_("Quota exceeeded for %s, tried to allocate "
- "address"),
- context.project_id)
- raise quota.QuotaError(_("Address quota exceeded. You cannot "
- "allocate any more addresses"))
+ LOG.warn(_('Quota exceeeded for %s, tried to allocate '
+ 'address'),
+ context.project_id)
+ raise quota.QuotaError(_('Address quota exceeded. You cannot '
+ 'allocate any more addresses'))
# NOTE(vish): We don't know which network host should get the ip
# when we allocate, so just send it to any one. This
# will probably need to move into a network supervisor
# at some point.
return rpc.call(context,
FLAGS.network_topic,
- {"method": "allocate_floating_ip",
- "args": {"project_id": context.project_id}})
+ {'method': 'allocate_floating_ip',
+ 'args': {'project_id': context.project_id}})
def release_floating_ip(self, context, address,
affect_auto_assigned=False):
@@ -62,8 +61,8 @@ class API(base.Base):
# at some point.
rpc.cast(context,
FLAGS.network_topic,
- {"method": "deallocate_floating_ip",
- "args": {"floating_address": floating_ip['address']}})
+ {'method': 'deallocate_floating_ip',
+ 'args': {'floating_address': floating_ip['address']}})
def associate_floating_ip(self, context, floating_ip, fixed_ip,
affect_auto_assigned=False):
@@ -74,17 +73,17 @@ class API(base.Base):
return
# Check if the floating ip address is allocated
if floating_ip['project_id'] is None:
- raise exception.ApiError(_("Address (%s) is not allocated") %
+ raise exception.ApiError(_('Address (%s) is not allocated') %
floating_ip['address'])
# Check if the floating ip address is allocated to the same project
if floating_ip['project_id'] != context.project_id:
- LOG.warn(_("Address (%(address)s) is not allocated to your "
- "project (%(project)s)"),
+ LOG.warn(_('Address (%(address)s) is not allocated to your '
+ 'project (%(project)s)'),
{'address': floating_ip['address'],
'project': context.project_id})
- raise exception.ApiError(_("Address (%(address)s) is not "
- "allocated to your project"
- "(%(project)s)") %
+ raise exception.ApiError(_('Address (%(address)s) is not '
+ 'allocated to your project'
+ '(%(project)s)') %
{'address': floating_ip['address'],
'project': context.project_id})
# NOTE(vish): Perhaps we should just pass this on to compute and
@@ -92,9 +91,9 @@ class API(base.Base):
host = fixed_ip['network']['host']
rpc.cast(context,
self.db.queue_get_for(context, FLAGS.network_topic, host),
- {"method": "associate_floating_ip",
- "args": {"floating_address": floating_ip['address'],
- "fixed_address": fixed_ip['address']}})
+ {'method': 'associate_floating_ip',
+ 'args': {'floating_address': floating_ip['address'],
+ 'fixed_address': fixed_ip['address']}})
def disassociate_floating_ip(self, context, address,
affect_auto_assigned=False):
@@ -108,5 +107,5 @@ class API(base.Base):
host = floating_ip['fixed_ip']['network']['host']
rpc.cast(context,
self.db.queue_get_for(context, FLAGS.network_topic, host),
- {"method": "disassociate_floating_ip",
- "args": {"floating_address": floating_ip['address']}})
+ {'method': 'disassociate_floating_ip',
+ 'args': {'floating_address': floating_ip['address']}})
diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py
index b50a4b4ea..815cd29c3 100644
--- a/nova/network/linux_net.py
+++ b/nova/network/linux_net.py
@@ -15,26 +15,27 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
-"""
-Implements vlans, bridges, and iptables rules using linux utilities.
-"""
+"""Implements vlans, bridges, and iptables rules using linux utilities."""
+
+import calendar
import inspect
import os
-import calendar
from nova import db
from nova import exception
from nova import flags
from nova import log as logging
from nova import utils
+from IPy import IP
+
LOG = logging.getLogger("nova.linux_net")
def _bin_file(script):
- """Return the absolute path to scipt in the bin directory"""
- return os.path.abspath(os.path.join(__file__, "../../../bin", script))
+ """Return the absolute path to scipt in the bin directory."""
+ return os.path.abspath(os.path.join(__file__, '../../../bin', script))
FLAGS = flags.FLAGS
@@ -66,11 +67,13 @@ binary_name = os.path.basename(inspect.stack()[-1][1])
class IptablesRule(object):
- """An iptables rule
+ """An iptables rule.
You shouldn't need to use this class directly, it's only used by
- IptablesManager
+ IptablesManager.
+
"""
+
def __init__(self, chain, rule, wrap=True, top=False):
self.chain = chain
self.rule = rule
@@ -95,7 +98,7 @@ class IptablesRule(object):
class IptablesTable(object):
- """An iptables table"""
+ """An iptables table."""
def __init__(self):
self.rules = []
@@ -103,15 +106,16 @@ class IptablesTable(object):
self.unwrapped_chains = set()
def add_chain(self, name, wrap=True):
- """Adds a named chain to the table
+ """Adds a named chain to the table.
The chain name is wrapped to be unique for the component creating
it, so different components of Nova can safely create identically
named chains without interfering with one another.
At the moment, its wrapped name is <binary name>-<chain name>,
- so if nova-compute creates a chain named "OUTPUT", it'll actually
- end up named "nova-compute-OUTPUT".
+ so if nova-compute creates a chain named 'OUTPUT', it'll actually
+ end up named 'nova-compute-OUTPUT'.
+
"""
if wrap:
self.chains.add(name)
@@ -119,12 +123,13 @@ class IptablesTable(object):
self.unwrapped_chains.add(name)
def remove_chain(self, name, wrap=True):
- """Remove named chain
+ """Remove named chain.
This removal "cascades". All rule in the chain are removed, as are
all rules in other chains that jump to it.
If the chain is not found, this is merely logged.
+
"""
if wrap:
chain_set = self.chains
@@ -132,7 +137,7 @@ class IptablesTable(object):
chain_set = self.unwrapped_chains
if name not in chain_set:
- LOG.debug(_("Attempted to remove chain %s which doesn't exist"),
+ LOG.debug(_('Attempted to remove chain %s which does not exist'),
name)
return
@@ -147,17 +152,18 @@ class IptablesTable(object):
self.rules = filter(lambda r: jump_snippet not in r.rule, self.rules)
def add_rule(self, chain, rule, wrap=True, top=False):
- """Add a rule to the table
+ """Add a rule to the table.
This is just like what you'd feed to iptables, just without
- the "-A <chain name>" bit at the start.
+ the '-A <chain name>' bit at the start.
However, if you need to jump to one of your wrapped chains,
prepend its name with a '$' which will ensure the wrapping
is applied correctly.
+
"""
if wrap and chain not in self.chains:
- raise ValueError(_("Unknown chain: %r") % chain)
+ raise ValueError(_('Unknown chain: %r') % chain)
if '$' in rule:
rule = ' '.join(map(self._wrap_target_chain, rule.split(' ')))
@@ -170,23 +176,24 @@ class IptablesTable(object):
return s
def remove_rule(self, chain, rule, wrap=True, top=False):
- """Remove a rule from a chain
+ """Remove a rule from a chain.
Note: The rule must be exactly identical to the one that was added.
You cannot switch arguments around like you can with the iptables
CLI tool.
+
"""
try:
self.rules.remove(IptablesRule(chain, rule, wrap, top))
except ValueError:
- LOG.debug(_("Tried to remove rule that wasn't there:"
- " %(chain)r %(rule)r %(wrap)r %(top)r"),
+ LOG.debug(_('Tried to remove rule that was not there:'
+ ' %(chain)r %(rule)r %(wrap)r %(top)r'),
{'chain': chain, 'rule': rule,
'top': top, 'wrap': wrap})
class IptablesManager(object):
- """Wrapper for iptables
+ """Wrapper for iptables.
See IptablesTable for some usage docs
@@ -205,7 +212,9 @@ class IptablesManager(object):
For ipv4, the builtin PREROUTING, OUTPUT, and POSTROUTING nat chains are
wrapped in the same was as the builtin filter chains. Additionally, there's
a snat chain that is applied after the POSTROUTING chain.
+
"""
+
def __init__(self, execute=None):
if not execute:
self.execute = _execute
@@ -267,11 +276,12 @@ class IptablesManager(object):
@utils.synchronized('iptables', external=True)
def apply(self):
- """Apply the current in-memory set of iptables rules
+ """Apply the current in-memory set of iptables rules.
This will blow away any rules left over from previous runs of the
same component of Nova, and replace them with our current set of
rules. This happens atomically, thanks to iptables-restore.
+
"""
s = [('iptables', self.ipv4)]
if FLAGS.use_ipv6:
@@ -348,63 +358,63 @@ class IptablesManager(object):
def metadata_forward():
- """Create forwarding rule for metadata"""
- iptables_manager.ipv4['nat'].add_rule("PREROUTING",
- "-s 0.0.0.0/0 -d 169.254.169.254/32 "
- "-p tcp -m tcp --dport 80 -j DNAT "
- "--to-destination %s:%s" % \
+ """Create forwarding rule for metadata."""
+ iptables_manager.ipv4['nat'].add_rule('PREROUTING',
+ '-s 0.0.0.0/0 -d 169.254.169.254/32 '
+ '-p tcp -m tcp --dport 80 -j DNAT '
+ '--to-destination %s:%s' % \
(FLAGS.ec2_dmz_host, FLAGS.ec2_port))
iptables_manager.apply()
def init_host():
- """Basic networking setup goes here"""
+ """Basic networking setup goes here."""
# NOTE(devcamcar): Cloud public SNAT entries and the default
# SNAT rule for outbound traffic.
- iptables_manager.ipv4['nat'].add_rule("snat",
- "-s %s -j SNAT --to-source %s" % \
+ iptables_manager.ipv4['nat'].add_rule('snat',
+ '-s %s -j SNAT --to-source %s' % \
(FLAGS.fixed_range,
FLAGS.routing_source_ip))
- iptables_manager.ipv4['nat'].add_rule("POSTROUTING",
- "-s %s -d %s -j ACCEPT" % \
+ iptables_manager.ipv4['nat'].add_rule('POSTROUTING',
+ '-s %s -d %s -j ACCEPT' % \
(FLAGS.fixed_range, FLAGS.dmz_cidr))
- iptables_manager.ipv4['nat'].add_rule("POSTROUTING",
- "-s %(range)s -d %(range)s "
- "-j ACCEPT" % \
+ iptables_manager.ipv4['nat'].add_rule('POSTROUTING',
+ '-s %(range)s -d %(range)s '
+ '-j ACCEPT' % \
{'range': FLAGS.fixed_range})
iptables_manager.apply()
def bind_floating_ip(floating_ip, check_exit_code=True):
- """Bind ip to public interface"""
+ """Bind ip to public interface."""
_execute('sudo', 'ip', 'addr', 'add', floating_ip,
'dev', FLAGS.public_interface,
check_exit_code=check_exit_code)
def unbind_floating_ip(floating_ip):
- """Unbind a public ip from public interface"""
+ """Unbind a public ip from public interface."""
_execute('sudo', 'ip', 'addr', 'del', floating_ip,
'dev', FLAGS.public_interface)
def ensure_metadata_ip():
- """Sets up local metadata ip"""
+ """Sets up local metadata ip."""
_execute('sudo', 'ip', 'addr', 'add', '169.254.169.254/32',
'scope', 'link', 'dev', 'lo', check_exit_code=False)
def ensure_vlan_forward(public_ip, port, private_ip):
- """Sets up forwarding rules for vlan"""
- iptables_manager.ipv4['filter'].add_rule("FORWARD",
- "-d %s -p udp "
- "--dport 1194 "
- "-j ACCEPT" % private_ip)
- iptables_manager.ipv4['nat'].add_rule("PREROUTING",
- "-d %s -p udp "
- "--dport %s -j DNAT --to %s:1194" %
+ """Sets up forwarding rules for vlan."""
+ iptables_manager.ipv4['filter'].add_rule('FORWARD',
+ '-d %s -p udp '
+ '--dport 1194 '
+ '-j ACCEPT' % private_ip)
+ iptables_manager.ipv4['nat'].add_rule('PREROUTING',
+ '-d %s -p udp '
+ '--dport %s -j DNAT --to %s:1194' %
(public_ip, port, private_ip))
iptables_manager.ipv4['nat'].add_rule("OUTPUT",
"-d %s -p udp "
@@ -414,37 +424,38 @@ def ensure_vlan_forward(public_ip, port, private_ip):
def ensure_floating_forward(floating_ip, fixed_ip):
- """Ensure floating ip forwarding rule"""
+ """Ensure floating ip forwarding rule."""
for chain, rule in floating_forward_rules(floating_ip, fixed_ip):
iptables_manager.ipv4['nat'].add_rule(chain, rule)
iptables_manager.apply()
def remove_floating_forward(floating_ip, fixed_ip):
- """Remove forwarding for floating ip"""
+ """Remove forwarding for floating ip."""
for chain, rule in floating_forward_rules(floating_ip, fixed_ip):
iptables_manager.ipv4['nat'].remove_rule(chain, rule)
iptables_manager.apply()
def floating_forward_rules(floating_ip, fixed_ip):
- return [("PREROUTING", "-d %s -j DNAT --to %s" % (floating_ip, fixed_ip)),
- ("OUTPUT", "-d %s -j DNAT --to %s" % (floating_ip, fixed_ip)),
- ("floating-snat",
- "-s %s -j SNAT --to %s" % (fixed_ip, floating_ip))]
+ return [('PREROUTING', '-d %s -j DNAT --to %s' % (floating_ip, fixed_ip)),
+ ('OUTPUT', '-d %s -j DNAT --to %s' % (floating_ip, fixed_ip)),
+ ('floating-snat',
+ '-s %s -j SNAT --to %s' % (fixed_ip, floating_ip))]
def ensure_vlan_bridge(vlan_num, bridge, net_attrs=None):
- """Create a vlan and bridge unless they already exist"""
+ """Create a vlan and bridge unless they already exist."""
interface = ensure_vlan(vlan_num)
ensure_bridge(bridge, interface, net_attrs)
+@utils.synchronized('ensure_vlan', external=True)
def ensure_vlan(vlan_num):
- """Create a vlan unless it already exists"""
- interface = "vlan%s" % vlan_num
+ """Create a vlan unless it already exists."""
+ interface = 'vlan%s' % vlan_num
if not _device_exists(interface):
- LOG.debug(_("Starting VLAN inteface %s"), interface)
+ LOG.debug(_('Starting VLAN inteface %s'), interface)
_execute('sudo', 'vconfig', 'set_name_type', 'VLAN_PLUS_VID_NO_PAD')
_execute('sudo', 'vconfig', 'add', FLAGS.vlan_interface, vlan_num)
_execute('sudo', 'ip', 'link', 'set', interface, 'up')
@@ -464,12 +475,13 @@ def ensure_bridge(bridge, interface, net_attrs=None):
The code will attempt to move any ips that already exist on the interface
onto the bridge and reset the default gateway if necessary.
+
"""
if not _device_exists(bridge):
- LOG.debug(_("Starting Bridge interface for %s"), interface)
+ LOG.debug(_('Starting Bridge interface for %s'), interface)
_execute('sudo', 'brctl', 'addbr', bridge)
_execute('sudo', 'brctl', 'setfd', bridge, 0)
- # _execute("sudo brctl setageing %s 10" % bridge)
+ # _execute('sudo brctl setageing %s 10' % bridge)
_execute('sudo', 'brctl', 'stp', bridge, 'off')
_execute('sudo', 'ip', 'link', 'set', bridge, 'up')
if net_attrs:
@@ -477,15 +489,15 @@ def ensure_bridge(bridge, interface, net_attrs=None):
# bridge for it to respond to reqests properly
suffix = net_attrs['cidr'].rpartition('/')[2]
out, err = _execute('sudo', 'ip', 'addr', 'add',
- "%s/%s" %
+ '%s/%s' %
(net_attrs['gateway'], suffix),
'brd',
net_attrs['broadcast'],
'dev',
bridge,
check_exit_code=False)
- if err and err != "RTNETLINK answers: File exists\n":
- raise exception.Error("Failed to add ip: %s" % err)
+ if err and err != 'RTNETLINK answers: File exists\n':
+ raise exception.Error('Failed to add ip: %s' % err)
if(FLAGS.use_ipv6):
_execute('sudo', 'ip', '-f', 'inet6', 'addr',
'change', net_attrs['cidr_v6'],
@@ -501,17 +513,17 @@ def ensure_bridge(bridge, interface, net_attrs=None):
# interface, so we move any ips to the bridge
gateway = None
out, err = _execute('sudo', 'route', '-n')
- for line in out.split("\n"):
+ for line in out.split('\n'):
fields = line.split()
- if fields and fields[0] == "0.0.0.0" and fields[-1] == interface:
+ if fields and fields[0] == '0.0.0.0' and fields[-1] == interface:
gateway = fields[1]
_execute('sudo', 'route', 'del', 'default', 'gw', gateway,
'dev', interface, check_exit_code=False)
out, err = _execute('sudo', 'ip', 'addr', 'show', 'dev', interface,
'scope', 'global')
- for line in out.split("\n"):
+ for line in out.split('\n'):
fields = line.split()
- if fields and fields[0] == "inet":
+ if fields and fields[0] == 'inet':
params = fields[1:-1]
_execute(*_ip_bridge_cmd('del', params, fields[-1]))
_execute(*_ip_bridge_cmd('add', params, bridge))
@@ -522,18 +534,18 @@ def ensure_bridge(bridge, interface, net_attrs=None):
if (err and err != "device %s is already a member of a bridge; can't "
"enslave it to bridge %s.\n" % (interface, bridge)):
- raise exception.Error("Failed to add interface: %s" % err)
+ raise exception.Error('Failed to add interface: %s' % err)
- iptables_manager.ipv4['filter'].add_rule("FORWARD",
- "--in-interface %s -j ACCEPT" % \
+ iptables_manager.ipv4['filter'].add_rule('FORWARD',
+ '--in-interface %s -j ACCEPT' % \
bridge)
- iptables_manager.ipv4['filter'].add_rule("FORWARD",
- "--out-interface %s -j ACCEPT" % \
+ iptables_manager.ipv4['filter'].add_rule('FORWARD',
+ '--out-interface %s -j ACCEPT' % \
bridge)
def get_dhcp_leases(context, network_id):
- """Return a network's hosts config in dnsmasq leasefile format"""
+ """Return a network's hosts config in dnsmasq leasefile format."""
hosts = []
for fixed_ip_ref in db.network_get_associated_fixed_ips(context,
network_id):
@@ -542,7 +554,7 @@ def get_dhcp_leases(context, network_id):
def get_dhcp_hosts(context, network_id):
- """Get a string containing a network's hosts config in dhcp-host format"""
+ """Get network's hosts config in dhcp-host format."""
hosts = []
for fixed_ip_ref in db.network_get_associated_fixed_ips(context,
network_id):
@@ -555,10 +567,11 @@ def get_dhcp_hosts(context, network_id):
# aren't reloaded.
@utils.synchronized('dnsmasq_start')
def update_dhcp(context, network_id):
- """(Re)starts a dnsmasq server for a given network
+ """(Re)starts a dnsmasq server for a given network.
+
+ If a dnsmasq instance is already running then send a HUP
+ signal causing it to reload, otherwise spawn a new instance.
- if a dnsmasq instance is already running then send a HUP
- signal causing it to reload, otherwise spawn a new instance
"""
network_ref = db.network_get(context, network_id)
@@ -573,16 +586,16 @@ def update_dhcp(context, network_id):
# if dnsmasq is already running, then tell it to reload
if pid:
- out, _err = _execute('cat', "/proc/%d/cmdline" % pid,
+ out, _err = _execute('cat', '/proc/%d/cmdline' % pid,
check_exit_code=False)
if conffile in out:
try:
_execute('sudo', 'kill', '-HUP', pid)
return
except Exception as exc: # pylint: disable=W0703
- LOG.debug(_("Hupping dnsmasq threw %s"), exc)
+ LOG.debug(_('Hupping dnsmasq threw %s'), exc)
else:
- LOG.debug(_("Pid %d is stale, relaunching dnsmasq"), pid)
+ LOG.debug(_('Pid %d is stale, relaunching dnsmasq'), pid)
# FLAGFILE and DNSMASQ_INTERFACE in env
env = {'FLAGFILE': FLAGS.dhcpbridge_flagfile,
@@ -625,18 +638,18 @@ interface %s
try:
_execute('sudo', 'kill', pid)
except Exception as exc: # pylint: disable=W0703
- LOG.debug(_("killing radvd threw %s"), exc)
+ LOG.debug(_('killing radvd threw %s'), exc)
else:
- LOG.debug(_("Pid %d is stale, relaunching radvd"), pid)
+ LOG.debug(_('Pid %d is stale, relaunching radvd'), pid)
command = _ra_cmd(network_ref)
_execute(*command)
db.network_update(context, network_id,
- {"gateway_v6":
+ {'gateway_v6':
utils.get_my_linklocal(network_ref['bridge'])})
def _host_lease(fixed_ip_ref):
- """Return a host string for an address in leasefile format"""
+ """Return a host string for an address in leasefile format."""
instance_ref = fixed_ip_ref['instance']
if instance_ref['updated_at']:
timestamp = instance_ref['updated_at']
@@ -645,39 +658,39 @@ def _host_lease(fixed_ip_ref):
seconds_since_epoch = calendar.timegm(timestamp.utctimetuple())
- return "%d %s %s %s *" % (seconds_since_epoch + FLAGS.dhcp_lease_time,
+ return '%d %s %s %s *' % (seconds_since_epoch + FLAGS.dhcp_lease_time,
instance_ref['mac_address'],
fixed_ip_ref['address'],
instance_ref['hostname'] or '*')
def _host_dhcp(fixed_ip_ref):
- """Return a host string for an address in dhcp-host format"""
+ """Return a host string for an address in dhcp-host format."""
instance_ref = fixed_ip_ref['instance']
- return "%s,%s.%s,%s" % (instance_ref['mac_address'],
+ return '%s,%s.%s,%s' % (instance_ref['mac_address'],
instance_ref['hostname'],
FLAGS.dhcp_domain,
fixed_ip_ref['address'])
def _execute(*cmd, **kwargs):
- """Wrapper around utils._execute for fake_network"""
+ """Wrapper around utils._execute for fake_network."""
if FLAGS.fake_network:
- LOG.debug("FAKE NET: %s", " ".join(map(str, cmd)))
- return "fake", 0
+ LOG.debug('FAKE NET: %s', ' '.join(map(str, cmd)))
+ return 'fake', 0
else:
return utils.execute(*cmd, **kwargs)
def _device_exists(device):
- """Check if ethernet device exists"""
+ """Check if ethernet device exists."""
(_out, err) = _execute('ip', 'link', 'show', 'dev', device,
check_exit_code=False)
return not err
def _dnsmasq_cmd(net):
- """Builds dnsmasq command"""
+ """Builds dnsmasq command."""
cmd = ['sudo', '-E', 'dnsmasq',
'--strict-order',
'--bind-interfaces',
@@ -687,6 +700,7 @@ def _dnsmasq_cmd(net):
'--listen-address=%s' % net['gateway'],
'--except-interface=lo',
'--dhcp-range=%s,static,120s' % net['dhcp_start'],
+ '--dhcp-lease-max=%s' % IP(net['cidr']).len(),
'--dhcp-hostsfile=%s' % _dhcp_file(net['bridge'], 'conf'),
'--dhcp-script=%s' % FLAGS.dhcpbridge,
'--leasefile-ro']
@@ -696,7 +710,7 @@ def _dnsmasq_cmd(net):
def _ra_cmd(net):
- """Builds radvd command"""
+ """Builds radvd command."""
cmd = ['sudo', '-E', 'radvd',
# '-u', 'nobody',
'-C', '%s' % _ra_file(net['bridge'], 'conf'),
@@ -705,44 +719,43 @@ def _ra_cmd(net):
def _stop_dnsmasq(network):
- """Stops the dnsmasq instance for a given network"""
+ """Stops the dnsmasq instance for a given network."""
pid = _dnsmasq_pid_for(network)
if pid:
try:
_execute('sudo', 'kill', '-TERM', pid)
except Exception as exc: # pylint: disable=W0703
- LOG.debug(_("Killing dnsmasq threw %s"), exc)
+ LOG.debug(_('Killing dnsmasq threw %s'), exc)
def _dhcp_file(bridge, kind):
- """Return path to a pid, leases or conf file for a bridge"""
-
+ """Return path to a pid, leases or conf file for a bridge."""
if not os.path.exists(FLAGS.networks_path):
os.makedirs(FLAGS.networks_path)
- return os.path.abspath("%s/nova-%s.%s" % (FLAGS.networks_path,
+ return os.path.abspath('%s/nova-%s.%s' % (FLAGS.networks_path,
bridge,
kind))
def _ra_file(bridge, kind):
- """Return path to a pid or conf file for a bridge"""
+ """Return path to a pid or conf file for a bridge."""
if not os.path.exists(FLAGS.networks_path):
os.makedirs(FLAGS.networks_path)
- return os.path.abspath("%s/nova-ra-%s.%s" % (FLAGS.networks_path,
+ return os.path.abspath('%s/nova-ra-%s.%s' % (FLAGS.networks_path,
bridge,
kind))
def _dnsmasq_pid_for(bridge):
- """Returns the pid for prior dnsmasq instance for a bridge
+ """Returns the pid for prior dnsmasq instance for a bridge.
- Returns None if no pid file exists
+ Returns None if no pid file exists.
- If machine has rebooted pid might be incorrect (caller should check)
- """
+ If machine has rebooted pid might be incorrect (caller should check).
+ """
pid_file = _dhcp_file(bridge, 'pid')
if os.path.exists(pid_file):
@@ -751,13 +764,13 @@ def _dnsmasq_pid_for(bridge):
def _ra_pid_for(bridge):
- """Returns the pid for prior radvd instance for a bridge
+ """Returns the pid for prior radvd instance for a bridge.
- Returns None if no pid file exists
+ Returns None if no pid file exists.
- If machine has rebooted pid might be incorrect (caller should check)
- """
+ If machine has rebooted pid might be incorrect (caller should check).
+ """
pid_file = _ra_file(bridge, 'pid')
if os.path.exists(pid_file):
@@ -766,8 +779,7 @@ def _ra_pid_for(bridge):
def _ip_bridge_cmd(action, params, device):
- """Build commands to add/del ips to bridges/devices"""
-
+ """Build commands to add/del ips to bridges/devices."""
cmd = ['sudo', 'ip', 'addr', action]
cmd.extend(params)
cmd.extend(['dev', device])
diff --git a/nova/network/manager.py b/nova/network/manager.py
index 0dd7f2360..5a6fdde5a 100644
--- a/nova/network/manager.py
+++ b/nova/network/manager.py
@@ -16,8 +16,7 @@
# License for the specific language governing permissions and limitations
# under the License.
-"""
-Network Hosts are responsible for allocating ips and setting up network.
+"""Network Hosts are responsible for allocating ips and setting up network.
There are multiple backend drivers that handle specific types of networking
topologies. All of the network commands are issued to a subclass of
@@ -61,6 +60,8 @@ from nova import rpc
LOG = logging.getLogger("nova.network.manager")
+
+
FLAGS = flags.FLAGS
flags.DEFINE_string('flat_network_bridge', 'br100',
'Bridge for simple network instances')
@@ -111,7 +112,9 @@ class NetworkManager(manager.SchedulerDependentManager):
"""Implements common network manager functionality.
This class must be subclassed to support specific topologies.
+
"""
+
timeout_fixed_ips = True
def __init__(self, network_driver=None, *args, **kwargs):
@@ -122,9 +125,7 @@ class NetworkManager(manager.SchedulerDependentManager):
*args, **kwargs)
def init_host(self):
- """Do any initialization that needs to be run if this is a
- standalone service.
- """
+ """Do any initialization for a standalone service."""
self.driver.init_host()
self.driver.ensure_metadata_ip()
# Set up networking for the projects for which we're already
@@ -154,11 +155,11 @@ class NetworkManager(manager.SchedulerDependentManager):
self.host,
time)
if num:
- LOG.debug(_("Dissassociated %s stale fixed ip(s)"), num)
+ LOG.debug(_('Dissassociated %s stale fixed ip(s)'), num)
def set_network_host(self, context, network_id):
"""Safely sets the host of the network."""
- LOG.debug(_("setting network host"), context=context)
+ LOG.debug(_('setting network host'), context=context)
host = self.db.network_set_host(context,
network_id,
self.host)
@@ -224,39 +225,39 @@ class NetworkManager(manager.SchedulerDependentManager):
def lease_fixed_ip(self, context, mac, address):
"""Called by dhcp-bridge when ip is leased."""
- LOG.debug(_("Leasing IP %s"), address, context=context)
+ LOG.debug(_('Leasing IP %s'), address, context=context)
fixed_ip_ref = self.db.fixed_ip_get_by_address(context, address)
instance_ref = fixed_ip_ref['instance']
if not instance_ref:
- raise exception.Error(_("IP %s leased that isn't associated") %
+ raise exception.Error(_('IP %s leased that is not associated') %
address)
if instance_ref['mac_address'] != mac:
inst_addr = instance_ref['mac_address']
- raise exception.Error(_("IP %(address)s leased to bad"
- " mac %(inst_addr)s vs %(mac)s") % locals())
+ raise exception.Error(_('IP %(address)s leased to bad mac'
+ ' %(inst_addr)s vs %(mac)s') % locals())
now = datetime.datetime.utcnow()
self.db.fixed_ip_update(context,
fixed_ip_ref['address'],
{'leased': True,
'updated_at': now})
if not fixed_ip_ref['allocated']:
- LOG.warn(_("IP %s leased that was already deallocated"), address,
+ LOG.warn(_('IP %s leased that was already deallocated'), address,
context=context)
def release_fixed_ip(self, context, mac, address):
"""Called by dhcp-bridge when ip is released."""
- LOG.debug(_("Releasing IP %s"), address, context=context)
+ LOG.debug(_('Releasing IP %s'), address, context=context)
fixed_ip_ref = self.db.fixed_ip_get_by_address(context, address)
instance_ref = fixed_ip_ref['instance']
if not instance_ref:
- raise exception.Error(_("IP %s released that isn't associated") %
+ raise exception.Error(_('IP %s released that is not associated') %
address)
if instance_ref['mac_address'] != mac:
inst_addr = instance_ref['mac_address']
- raise exception.Error(_("IP %(address)s released from"
- " bad mac %(inst_addr)s vs %(mac)s") % locals())
+ raise exception.Error(_('IP %(address)s released from bad mac'
+ ' %(inst_addr)s vs %(mac)s') % locals())
if not fixed_ip_ref['leased']:
- LOG.warn(_("IP %s released that was not leased"), address,
+ LOG.warn(_('IP %s released that was not leased'), address,
context=context)
self.db.fixed_ip_update(context,
fixed_ip_ref['address'],
@@ -286,8 +287,8 @@ class NetworkManager(manager.SchedulerDependentManager):
return self.set_network_host(context, network_ref['id'])
host = rpc.call(context,
FLAGS.network_topic,
- {"method": "set_network_host",
- "args": {"network_id": network_ref['id']}})
+ {'method': 'set_network_host',
+ 'args': {'network_id': network_ref['id']}})
return host
def create_networks(self, context, cidr, num_networks, network_size,
@@ -302,7 +303,7 @@ class NetworkManager(manager.SchedulerDependentManager):
start = index * network_size
start_v6 = index * network_size_v6
significant_bits = 32 - int(math.log(network_size, 2))
- cidr = "%s/%s" % (fixed_net[start], significant_bits)
+ cidr = '%s/%s' % (fixed_net[start], significant_bits)
project_net = IPy.IP(cidr)
net = {}
net['bridge'] = FLAGS.flat_network_bridge
@@ -313,13 +314,13 @@ class NetworkManager(manager.SchedulerDependentManager):
net['broadcast'] = str(project_net.broadcast())
net['dhcp_start'] = str(project_net[2])
if num_networks > 1:
- net['label'] = "%s_%d" % (label, count)
+ net['label'] = '%s_%d' % (label, count)
else:
net['label'] = label
count += 1
if(FLAGS.use_ipv6):
- cidr_v6 = "%s/%s" % (fixed_net_v6[start_v6],
+ cidr_v6 = '%s/%s' % (fixed_net_v6[start_v6],
significant_bits_v6)
net['cidr_v6'] = cidr_v6
project_net_v6 = IPy.IP(cidr_v6)
@@ -386,13 +387,13 @@ class FlatManager(NetworkManager):
Metadata forwarding must be handled by the gateway, and since nova does
not do any setup in this mode, it must be done manually. Requests to
169.254.169.254 port 80 will need to be forwarded to the api server.
+
"""
+
timeout_fixed_ips = False
def init_host(self):
- """Do any initialization that needs to be run if this is a
- standalone service.
- """
+ """Do any initialization for a standalone service."""
#Fix for bug 723298 - do not call init_host on superclass
#Following code has been copied for NetworkManager.init_host
ctxt = context.get_admin_context()
@@ -433,12 +434,11 @@ class FlatDHCPManager(NetworkManager):
FlatDHCPManager will start up one dhcp server to give out addresses.
It never injects network settings into the guest. Otherwise it behaves
like FlatDHCPManager.
+
"""
def init_host(self):
- """Do any initialization that needs to be run if this is a
- standalone service.
- """
+ """Do any initialization for a standalone service."""
super(FlatDHCPManager, self).init_host()
self.driver.metadata_forward()
@@ -490,12 +490,11 @@ class VlanManager(NetworkManager):
A dhcp server is run for each subnet, so each project will have its own.
For this mode to be useful, each project will need a vpn to access the
instances in its subnet.
+
"""
def init_host(self):
- """Do any initialization that needs to be run if this is a
- standalone service.
- """
+ """Do any initialization for a standalone service."""
super(VlanManager, self).init_host()
self.driver.metadata_forward()
@@ -566,7 +565,7 @@ class VlanManager(NetworkManager):
net['vlan'] = vlan
net['bridge'] = 'br%s' % vlan
if(FLAGS.use_ipv6):
- cidr_v6 = "%s/%s" % (fixed_net_v6[start_v6],
+ cidr_v6 = '%s/%s' % (fixed_net_v6[start_v6],
significant_bits_v6)
net['cidr_v6'] = cidr_v6
@@ -600,8 +599,8 @@ class VlanManager(NetworkManager):
return self.set_network_host(context, network_ref['id'])
host = rpc.call(context,
FLAGS.network_topic,
- {"method": "set_network_host",
- "args": {"network_id": network_ref['id']}})
+ {'method': 'set_network_host',
+ 'args': {'network_id': network_ref['id']}})
return host
diff --git a/nova/network/vmwareapi_net.py b/nova/network/vmwareapi_net.py
index 9b2db7b8f..04210c011 100644
--- a/nova/network/vmwareapi_net.py
+++ b/nova/network/vmwareapi_net.py
@@ -15,9 +15,7 @@
# License for the specific language governing permissions and limitations
# under the License.
-"""
-Implements vlans for vmwareapi.
-"""
+"""Implements vlans for vmwareapi."""
from nova import db
from nova import exception
@@ -27,12 +25,12 @@ from nova import utils
from nova.virt.vmwareapi_conn import VMWareAPISession
from nova.virt.vmwareapi import network_utils
+
LOG = logging.getLogger("nova.network.vmwareapi_net")
+
FLAGS = flags.FLAGS
-flags.DEFINE_string('vlan_interface', 'vmnic0',
- 'Physical network adapter name in VMware ESX host for '
- 'vlan networking')
+FLAGS['vlan_interface'].SetDefault('vmnic0')
def ensure_vlan_bridge(vlan_num, bridge, net_attrs=None):
@@ -42,10 +40,10 @@ def ensure_vlan_bridge(vlan_num, bridge, net_attrs=None):
host_username = FLAGS.vmwareapi_host_username
host_password = FLAGS.vmwareapi_host_password
if not host_ip or host_username is None or host_password is None:
- raise Exception(_("Must specify vmwareapi_host_ip,"
- "vmwareapi_host_username "
- "and vmwareapi_host_password to use"
- "connection_type=vmwareapi"))
+ raise Exception(_('Must specify vmwareapi_host_ip, '
+ 'vmwareapi_host_username '
+ 'and vmwareapi_host_password to use '
+ 'connection_type=vmwareapi'))
session = VMWareAPISession(host_ip, host_username, host_password,
FLAGS.vmwareapi_api_retry_count)
vlan_interface = FLAGS.vlan_interface
diff --git a/nova/network/xenapi_net.py b/nova/network/xenapi_net.py
index 8c22a7d4b..709ef7f34 100644
--- a/nova/network/xenapi_net.py
+++ b/nova/network/xenapi_net.py
@@ -15,9 +15,7 @@
# License for the specific language governing permissions and limitations
# under the License.
-"""
-Implements vlans, bridges, and iptables rules using linux utilities.
-"""
+"""Implements vlans, bridges, and iptables rules using linux utilities."""
import os
@@ -26,22 +24,24 @@ from nova import exception
from nova import flags
from nova import log as logging
from nova import utils
-from nova.virt.xenapi_conn import XenAPISession
+from nova.virt import xenapi_conn
from nova.virt.xenapi import network_utils
+
LOG = logging.getLogger("nova.xenapi_net")
+
FLAGS = flags.FLAGS
def ensure_vlan_bridge(vlan_num, bridge, net_attrs=None):
"""Create a vlan and bridge unless they already exist."""
# Open xenapi session
- LOG.debug("ENTERING ensure_vlan_bridge in xenapi net")
+ LOG.debug('ENTERING ensure_vlan_bridge in xenapi net')
url = FLAGS.xenapi_connection_url
username = FLAGS.xenapi_connection_username
password = FLAGS.xenapi_connection_password
- session = XenAPISession(url, username, password)
+ session = xenapi_conn.XenAPISession(url, username, password)
# Check whether bridge already exists
# Retrieve network whose name_label is "bridge"
network_ref = network_utils.NetworkHelper.find_network_with_name_label(
@@ -50,14 +50,14 @@ def ensure_vlan_bridge(vlan_num, bridge, net_attrs=None):
if network_ref is None:
# If bridge does not exists
# 1 - create network
- description = "network for nova bridge %s" % bridge
+ description = 'network for nova bridge %s' % bridge
network_rec = {'name_label': bridge,
'name_description': description,
'other_config': {}}
network_ref = session.call_xenapi('network.create', network_rec)
# 2 - find PIF for VLAN
- expr = 'field "device" = "%s" and \
- field "VLAN" = "-1"' % FLAGS.vlan_interface
+ expr = "field 'device' = '%s' and \
+ field 'VLAN' = '-1'" % FLAGS.vlan_interface
pifs = session.call_xenapi('PIF.get_all_records_where', expr)
pif_ref = None
# Multiple PIF are ok: we are dealing with a pool
diff --git a/nova/notifier/__init__.py b/nova/notifier/__init__.py
new file mode 100644
index 000000000..482d54e4f
--- /dev/null
+++ b/nova/notifier/__init__.py
@@ -0,0 +1,14 @@
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
diff --git a/nova/notifier/api.py b/nova/notifier/api.py
new file mode 100644
index 000000000..a3e7a039e
--- /dev/null
+++ b/nova/notifier/api.py
@@ -0,0 +1,83 @@
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.import datetime
+
+import datetime
+import uuid
+
+from nova import flags
+from nova import utils
+
+
+FLAGS = flags.FLAGS
+
+flags.DEFINE_string('default_notification_level', 'INFO',
+ 'Default notification level for outgoing notifications')
+
+WARN = 'WARN'
+INFO = 'INFO'
+ERROR = 'ERROR'
+CRITICAL = 'CRITICAL'
+DEBUG = 'DEBUG'
+
+log_levels = (DEBUG, WARN, INFO, ERROR, CRITICAL)
+
+
+class BadPriorityException(Exception):
+ pass
+
+
+def notify(publisher_id, event_type, priority, payload):
+ """
+ Sends a notification using the specified driver
+
+ Notify parameters:
+
+ publisher_id - the source worker_type.host of the message
+ event_type - the literal type of event (ex. Instance Creation)
+ priority - patterned after the enumeration of Python logging levels in
+ the set (DEBUG, WARN, INFO, ERROR, CRITICAL)
+ payload - A python dictionary of attributes
+
+ Outgoing message format includes the above parameters, and appends the
+ following:
+
+ message_id - a UUID representing the id for this notification
+ timestamp - the GMT timestamp the notification was sent at
+
+ The composite message will be constructed as a dictionary of the above
+ attributes, which will then be sent via the transport mechanism defined
+ by the driver.
+
+ Message example:
+
+ {'message_id': str(uuid.uuid4()),
+ 'publisher_id': 'compute.host1',
+ 'timestamp': datetime.datetime.utcnow(),
+ 'priority': 'WARN',
+ 'event_type': 'compute.create_instance',
+ 'payload': {'instance_id': 12, ... }}
+
+ """
+ if priority not in log_levels:
+ raise BadPriorityException(
+ _('%s not in valid priorities' % priority))
+ driver = utils.import_object(FLAGS.notification_driver)
+ msg = dict(message_id=str(uuid.uuid4()),
+ publisher_id=publisher_id,
+ event_type=event_type,
+ priority=priority,
+ payload=payload,
+ timestamp=str(datetime.datetime.utcnow()))
+ driver.notify(msg)
diff --git a/nova/notifier/log_notifier.py b/nova/notifier/log_notifier.py
new file mode 100644
index 000000000..25dfc693b
--- /dev/null
+++ b/nova/notifier/log_notifier.py
@@ -0,0 +1,34 @@
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import json
+
+from nova import flags
+from nova import log as logging
+
+
+FLAGS = flags.FLAGS
+
+
+def notify(message):
+ """Notifies the recipient of the desired event given the model.
+ Log notifications using nova's default logging system"""
+
+ priority = message.get('priority',
+ FLAGS.default_notification_level)
+ priority = priority.lower()
+ logger = logging.getLogger(
+ 'nova.notification.%s' % message['event_type'])
+ getattr(logger, priority)(json.dumps(message))
diff --git a/nova/tests/real_flags.py b/nova/notifier/no_op_notifier.py
index 71da04992..029710505 100644
--- a/nova/tests/real_flags.py
+++ b/nova/notifier/no_op_notifier.py
@@ -1,7 +1,4 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright 2010 United States Government as represented by the
-# Administrator of the National Aeronautics and Space Administration.
+# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -16,11 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
-from nova import flags
-
-FLAGS = flags.FLAGS
-FLAGS.connection_type = 'libvirt'
-FLAGS.fake_rabbit = False
-FLAGS.fake_network = False
-FLAGS.verbose = False
+def notify(message):
+ """Notifies the recipient of the desired event given the model"""
+ pass
diff --git a/nova/notifier/rabbit_notifier.py b/nova/notifier/rabbit_notifier.py
new file mode 100644
index 000000000..d46670b58
--- /dev/null
+++ b/nova/notifier/rabbit_notifier.py
@@ -0,0 +1,36 @@
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+
+import nova.context
+
+from nova import flags
+from nova import rpc
+
+
+FLAGS = flags.FLAGS
+
+flags.DEFINE_string('notification_topic', 'notifications',
+ 'RabbitMQ topic used for Nova notifications')
+
+
+def notify(message):
+ """Sends a notification to the RabbitMQ"""
+ context = nova.context.get_admin_context()
+ priority = message.get('priority',
+ FLAGS.default_notification_level)
+ priority = priority.lower()
+ topic = '%s.%s' % (FLAGS.notification_topic, priority)
+ rpc.cast(context, topic, message)
diff --git a/nova/quota.py b/nova/quota.py
index a93cd0766..58766e846 100644
--- a/nova/quota.py
+++ b/nova/quota.py
@@ -28,6 +28,8 @@ flags.DEFINE_integer('quota_instances', 10,
'number of instances allowed per project')
flags.DEFINE_integer('quota_cores', 20,
'number of instance cores allowed per project')
+flags.DEFINE_integer('quota_ram', 50 * 1024,
+ 'megabytes of instance ram allowed per project')
flags.DEFINE_integer('quota_volumes', 10,
'number of volumes allowed per project')
flags.DEFINE_integer('quota_gigabytes', 1000,
@@ -44,14 +46,28 @@ flags.DEFINE_integer('quota_max_injected_file_path_bytes', 255,
'number of bytes allowed per injected file path')
-def get_quota(context, project_id):
- rval = {'instances': FLAGS.quota_instances,
- 'cores': FLAGS.quota_cores,
- 'volumes': FLAGS.quota_volumes,
- 'gigabytes': FLAGS.quota_gigabytes,
- 'floating_ips': FLAGS.quota_floating_ips,
- 'metadata_items': FLAGS.quota_metadata_items}
-
+def _get_default_quotas():
+ defaults = {
+ 'instances': FLAGS.quota_instances,
+ 'cores': FLAGS.quota_cores,
+ 'ram': FLAGS.quota_ram,
+ 'volumes': FLAGS.quota_volumes,
+ 'gigabytes': FLAGS.quota_gigabytes,
+ 'floating_ips': FLAGS.quota_floating_ips,
+ 'metadata_items': FLAGS.quota_metadata_items,
+ 'injected_files': FLAGS.quota_max_injected_files,
+ 'injected_file_content_bytes':
+ FLAGS.quota_max_injected_file_content_bytes,
+ }
+ # -1 in the quota flags means unlimited
+ for key in defaults.keys():
+ if defaults[key] == -1:
+ defaults[key] = None
+ return defaults
+
+
+def get_project_quotas(context, project_id):
+ rval = _get_default_quotas()
quota = db.quota_get_all_by_project(context, project_id)
for key in rval.keys():
if key in quota:
@@ -65,71 +81,81 @@ def _get_request_allotment(requested, used, quota):
return quota - used
-def allowed_instances(context, num_instances, instance_type):
- """Check quota and return min(num_instances, allowed_instances)."""
+def allowed_instances(context, requested_instances, instance_type):
+ """Check quota and return min(requested_instances, allowed_instances)."""
project_id = context.project_id
context = context.elevated()
- num_cores = num_instances * instance_type['vcpus']
- used_instances, used_cores = db.instance_data_get_for_project(context,
- project_id)
- quota = get_quota(context, project_id)
- allowed_instances = _get_request_allotment(num_instances, used_instances,
+ requested_cores = requested_instances * instance_type['vcpus']
+ requested_ram = requested_instances * instance_type['memory_mb']
+ usage = db.instance_data_get_for_project(context, project_id)
+ used_instances, used_cores, used_ram = usage
+ quota = get_project_quotas(context, project_id)
+ allowed_instances = _get_request_allotment(requested_instances,
+ used_instances,
quota['instances'])
- allowed_cores = _get_request_allotment(num_cores, used_cores,
+ allowed_cores = _get_request_allotment(requested_cores, used_cores,
quota['cores'])
+ allowed_ram = _get_request_allotment(requested_ram, used_ram, quota['ram'])
allowed_instances = min(allowed_instances,
- int(allowed_cores // instance_type['vcpus']))
- return min(num_instances, allowed_instances)
+ allowed_cores // instance_type['vcpus'],
+ allowed_ram // instance_type['memory_mb'])
+ return min(requested_instances, allowed_instances)
-def allowed_volumes(context, num_volumes, size):
- """Check quota and return min(num_volumes, allowed_volumes)."""
+def allowed_volumes(context, requested_volumes, size):
+ """Check quota and return min(requested_volumes, allowed_volumes)."""
project_id = context.project_id
context = context.elevated()
size = int(size)
- num_gigabytes = num_volumes * size
+ requested_gigabytes = requested_volumes * size
used_volumes, used_gigabytes = db.volume_data_get_for_project(context,
project_id)
- quota = get_quota(context, project_id)
- allowed_volumes = _get_request_allotment(num_volumes, used_volumes,
+ quota = get_project_quotas(context, project_id)
+ allowed_volumes = _get_request_allotment(requested_volumes, used_volumes,
quota['volumes'])
- allowed_gigabytes = _get_request_allotment(num_gigabytes, used_gigabytes,
+ allowed_gigabytes = _get_request_allotment(requested_gigabytes,
+ used_gigabytes,
quota['gigabytes'])
allowed_volumes = min(allowed_volumes,
int(allowed_gigabytes // size))
- return min(num_volumes, allowed_volumes)
+ return min(requested_volumes, allowed_volumes)
-def allowed_floating_ips(context, num_floating_ips):
- """Check quota and return min(num_floating_ips, allowed_floating_ips)."""
+def allowed_floating_ips(context, requested_floating_ips):
+ """Check quota and return min(requested, allowed) floating ips."""
project_id = context.project_id
context = context.elevated()
used_floating_ips = db.floating_ip_count_by_project(context, project_id)
- quota = get_quota(context, project_id)
- allowed_floating_ips = _get_request_allotment(num_floating_ips,
+ quota = get_project_quotas(context, project_id)
+ allowed_floating_ips = _get_request_allotment(requested_floating_ips,
used_floating_ips,
quota['floating_ips'])
- return min(num_floating_ips, allowed_floating_ips)
+ return min(requested_floating_ips, allowed_floating_ips)
-def allowed_metadata_items(context, num_metadata_items):
- """Check quota; return min(num_metadata_items,allowed_metadata_items)."""
- project_id = context.project_id
- context = context.elevated()
- quota = get_quota(context, project_id)
- allowed_metadata_items = _get_request_allotment(num_metadata_items, 0,
- quota['metadata_items'])
- return min(num_metadata_items, allowed_metadata_items)
+def _calculate_simple_quota(context, resource, requested):
+ """Check quota for resource; return min(requested, allowed)."""
+ quota = get_project_quotas(context, context.project_id)
+ allowed = _get_request_allotment(requested, 0, quota[resource])
+ return min(requested, allowed)
+
+
+def allowed_metadata_items(context, requested_metadata_items):
+ """Return the number of metadata items allowed."""
+ return _calculate_simple_quota(context, 'metadata_items',
+ requested_metadata_items)
-def allowed_injected_files(context):
+def allowed_injected_files(context, requested_injected_files):
"""Return the number of injected files allowed."""
- return FLAGS.quota_max_injected_files
+ return _calculate_simple_quota(context, 'injected_files',
+ requested_injected_files)
-def allowed_injected_file_content_bytes(context):
+def allowed_injected_file_content_bytes(context, requested_bytes):
"""Return the number of bytes allowed per injected file content."""
- return FLAGS.quota_max_injected_file_content_bytes
+ resource = 'injected_file_content_bytes'
+ return _calculate_simple_quota(context, resource, requested_bytes)
def allowed_injected_file_path_bytes(context):
diff --git a/nova/rpc.py b/nova/rpc.py
index 2116f22c3..c5277c6a9 100644
--- a/nova/rpc.py
+++ b/nova/rpc.py
@@ -28,12 +28,15 @@ import json
import sys
import time
import traceback
+import types
import uuid
from carrot import connection as carrot_connection
from carrot import messaging
from eventlet import greenpool
-from eventlet import greenthread
+from eventlet import pools
+from eventlet import queue
+import greenlet
from nova import context
from nova import exception
@@ -47,7 +50,10 @@ LOG = logging.getLogger('nova.rpc')
FLAGS = flags.FLAGS
-flags.DEFINE_integer('rpc_thread_pool_size', 1024, 'Size of RPC thread pool')
+flags.DEFINE_integer('rpc_thread_pool_size', 1024,
+ 'Size of RPC thread pool')
+flags.DEFINE_integer('rpc_conn_pool_size', 30,
+ 'Size of RPC connection pool')
class Connection(carrot_connection.BrokerConnection):
@@ -90,6 +96,22 @@ class Connection(carrot_connection.BrokerConnection):
return cls.instance()
+class Pool(pools.Pool):
+ """Class that implements a Pool of Connections."""
+
+ # TODO(comstud): Timeout connections not used in a while
+ def create(self):
+ LOG.debug('Creating new connection')
+ return Connection.instance(new=True)
+
+# Create a ConnectionPool to use for RPC calls. We'll order the
+# pool as a stack (LIFO), so that we can potentially loop through and
+# timeout old unused connections at some point
+ConnectionPool = Pool(
+ max_size=FLAGS.rpc_conn_pool_size,
+ order_as_stack=True)
+
+
class Consumer(messaging.Consumer):
"""Consumer base class.
@@ -131,7 +153,9 @@ class Consumer(messaging.Consumer):
self.connection = Connection.recreate()
self.backend = self.connection.create_backend()
self.declare()
- super(Consumer, self).fetch(no_ack, auto_ack, enable_callbacks)
+ return super(Consumer, self).fetch(no_ack,
+ auto_ack,
+ enable_callbacks)
if self.failed_connection:
LOG.error(_('Reconnected to queue'))
self.failed_connection = False
@@ -159,13 +183,13 @@ class AdapterConsumer(Consumer):
self.pool = greenpool.GreenPool(FLAGS.rpc_thread_pool_size)
super(AdapterConsumer, self).__init__(connection=connection,
topic=topic)
+ self.register_callback(self.process_data)
- def receive(self, *args, **kwargs):
- self.pool.spawn_n(self._receive, *args, **kwargs)
+ def process_data(self, message_data, message):
+ """Consumer callback to call a method on a proxy object.
- @exception.wrap_exception
- def _receive(self, message_data, message):
- """Magically looks for a method on the proxy object and calls it.
+ Parses the message for validity and fires off a thread to call the
+ proxy object method.
Message data should be a dictionary with two keys:
method: string representing the method to call
@@ -175,8 +199,8 @@ class AdapterConsumer(Consumer):
"""
LOG.debug(_('received %s') % message_data)
- msg_id = message_data.pop('_msg_id', None)
-
+ # This will be popped off in _unpack_context
+ msg_id = message_data.get('_msg_id', None)
ctxt = _unpack_context(message_data)
method = message_data.get('method')
@@ -188,8 +212,17 @@ class AdapterConsumer(Consumer):
# we just log the message and send an error string
# back to the caller
LOG.warn(_('no method for message: %s') % message_data)
- msg_reply(msg_id, _('No method for message: %s') % message_data)
+ if msg_id:
+ msg_reply(msg_id,
+ _('No method for message: %s') % message_data)
return
+ self.pool.spawn_n(self._process_data, msg_id, ctxt, method, args)
+
+ @exception.wrap_exception
+ def _process_data(self, msg_id, ctxt, method, args):
+ """Thread that maigcally looks for a method on the proxy
+ object and calls it.
+ """
node_func = getattr(self.proxy, str(method))
node_args = dict((str(k), v) for k, v in args.iteritems())
@@ -197,7 +230,18 @@ class AdapterConsumer(Consumer):
try:
rval = node_func(context=ctxt, **node_args)
if msg_id:
- msg_reply(msg_id, rval, None)
+ # Check if the result was a generator
+ if isinstance(rval, types.GeneratorType):
+ for x in rval:
+ msg_reply(msg_id, x, None)
+ else:
+ msg_reply(msg_id, rval, None)
+
+ # This final None tells multicall that it is done.
+ msg_reply(msg_id, None, None)
+ elif isinstance(rval, types.GeneratorType):
+ # NOTE(vish): this iterates through the generator
+ list(rval)
except Exception as e:
logging.exception('Exception during message handling')
if msg_id:
@@ -205,11 +249,6 @@ class AdapterConsumer(Consumer):
return
-class Publisher(messaging.Publisher):
- """Publisher base class."""
- pass
-
-
class TopicAdapterConsumer(AdapterConsumer):
"""Consumes messages on a specific topic."""
@@ -242,6 +281,58 @@ class FanoutAdapterConsumer(AdapterConsumer):
topic=topic, proxy=proxy)
+class ConsumerSet(object):
+ """Groups consumers to listen on together on a single connection."""
+
+ def __init__(self, connection, consumer_list):
+ self.consumer_list = set(consumer_list)
+ self.consumer_set = None
+ self.enabled = True
+ self.init(connection)
+
+ def init(self, conn):
+ if not conn:
+ conn = Connection.instance(new=True)
+ if self.consumer_set:
+ self.consumer_set.close()
+ self.consumer_set = messaging.ConsumerSet(conn)
+ for consumer in self.consumer_list:
+ consumer.connection = conn
+ # consumer.backend is set for us
+ self.consumer_set.add_consumer(consumer)
+
+ def reconnect(self):
+ self.init(None)
+
+ def wait(self, limit=None):
+ running = True
+ while running:
+ it = self.consumer_set.iterconsume(limit=limit)
+ if not it:
+ break
+ while True:
+ try:
+ it.next()
+ except StopIteration:
+ return
+ except greenlet.GreenletExit:
+ running = False
+ break
+ except Exception as e:
+ LOG.exception(_("Exception while processing consumer"))
+ self.reconnect()
+ # Break to outer loop
+ break
+
+ def close(self):
+ self.consumer_set.close()
+
+
+class Publisher(messaging.Publisher):
+ """Publisher base class."""
+ pass
+
+
class TopicPublisher(Publisher):
"""Publishes messages on a specific topic."""
@@ -306,16 +397,18 @@ def msg_reply(msg_id, reply=None, failure=None):
LOG.error(_("Returning exception %s to caller"), message)
LOG.error(tb)
failure = (failure[0].__name__, str(failure[1]), tb)
- conn = Connection.instance()
- publisher = DirectPublisher(connection=conn, msg_id=msg_id)
- try:
- publisher.send({'result': reply, 'failure': failure})
- except TypeError:
- publisher.send(
- {'result': dict((k, repr(v))
- for k, v in reply.__dict__.iteritems()),
- 'failure': failure})
- publisher.close()
+
+ with ConnectionPool.item() as conn:
+ publisher = DirectPublisher(connection=conn, msg_id=msg_id)
+ try:
+ publisher.send({'result': reply, 'failure': failure})
+ except TypeError:
+ publisher.send(
+ {'result': dict((k, repr(v))
+ for k, v in reply.__dict__.iteritems()),
+ 'failure': failure})
+
+ publisher.close()
class RemoteError(exception.Error):
@@ -347,8 +440,9 @@ def _unpack_context(msg):
if key.startswith('_context_'):
value = msg.pop(key)
context_dict[key[9:]] = value
+ context_dict['msg_id'] = msg.pop('_msg_id', None)
LOG.debug(_('unpacked context: %s'), context_dict)
- return context.RequestContext.from_dict(context_dict)
+ return RpcContext.from_dict(context_dict)
def _pack_context(msg, context):
@@ -360,70 +454,112 @@ def _pack_context(msg, context):
for args at some point.
"""
- context = dict([('_context_%s' % key, value)
- for (key, value) in context.to_dict().iteritems()])
- msg.update(context)
+ context_d = dict([('_context_%s' % key, value)
+ for (key, value) in context.to_dict().iteritems()])
+ msg.update(context_d)
-def call(context, topic, msg):
- """Sends a message on a topic and wait for a response."""
+class RpcContext(context.RequestContext):
+ def __init__(self, *args, **kwargs):
+ msg_id = kwargs.pop('msg_id', None)
+ self.msg_id = msg_id
+ super(RpcContext, self).__init__(*args, **kwargs)
+
+ def reply(self, *args, **kwargs):
+ msg_reply(self.msg_id, *args, **kwargs)
+
+
+def multicall(context, topic, msg):
+ """Make a call that returns multiple times."""
LOG.debug(_('Making asynchronous call on %s ...'), topic)
msg_id = uuid.uuid4().hex
msg.update({'_msg_id': msg_id})
LOG.debug(_('MSG_ID is %s') % (msg_id))
_pack_context(msg, context)
- class WaitMessage(object):
- def __call__(self, data, message):
- """Acks message and sets result."""
- message.ack()
- if data['failure']:
- self.result = RemoteError(*data['failure'])
- else:
- self.result = data['result']
-
- wait_msg = WaitMessage()
- conn = Connection.instance()
- consumer = DirectConsumer(connection=conn, msg_id=msg_id)
+ con_conn = ConnectionPool.get()
+ consumer = DirectConsumer(connection=con_conn, msg_id=msg_id)
+ wait_msg = MulticallWaiter(consumer)
consumer.register_callback(wait_msg)
- conn = Connection.instance()
- publisher = TopicPublisher(connection=conn, topic=topic)
+ publisher = TopicPublisher(connection=con_conn, topic=topic)
publisher.send(msg)
publisher.close()
- try:
- consumer.wait(limit=1)
- except StopIteration:
- pass
- consumer.close()
- # NOTE(termie): this is a little bit of a change from the original
- # non-eventlet code where returning a Failure
- # instance from a deferred call is very similar to
- # raising an exception
- if isinstance(wait_msg.result, Exception):
- raise wait_msg.result
- return wait_msg.result
+ return wait_msg
+
+
+class MulticallWaiter(object):
+ def __init__(self, consumer):
+ self._consumer = consumer
+ self._results = queue.Queue()
+ self._closed = False
+
+ def close(self):
+ self._closed = True
+ self._consumer.close()
+ ConnectionPool.put(self._consumer.connection)
+
+ def __call__(self, data, message):
+ """Acks message and sets result."""
+ message.ack()
+ if data['failure']:
+ self._results.put(RemoteError(*data['failure']))
+ else:
+ self._results.put(data['result'])
+
+ def __iter__(self):
+ return self.wait()
+
+ def wait(self):
+ while True:
+ rv = None
+ while rv is None and not self._closed:
+ try:
+ rv = self._consumer.fetch(enable_callbacks=True)
+ except Exception:
+ self.close()
+ raise
+ time.sleep(0.01)
+
+ result = self._results.get()
+ if isinstance(result, Exception):
+ self.close()
+ raise result
+ if result == None:
+ self.close()
+ raise StopIteration
+ yield result
+
+
+def call(context, topic, msg):
+ """Sends a message on a topic and wait for a response."""
+ rv = multicall(context, topic, msg)
+ # NOTE(vish): return the last result from the multicall
+ rv = list(rv)
+ if not rv:
+ return
+ return rv[-1]
def cast(context, topic, msg):
"""Sends a message on a topic without waiting for a response."""
LOG.debug(_('Making asynchronous cast on %s...'), topic)
_pack_context(msg, context)
- conn = Connection.instance()
- publisher = TopicPublisher(connection=conn, topic=topic)
- publisher.send(msg)
- publisher.close()
+ with ConnectionPool.item() as conn:
+ publisher = TopicPublisher(connection=conn, topic=topic)
+ publisher.send(msg)
+ publisher.close()
def fanout_cast(context, topic, msg):
"""Sends a message on a fanout exchange without waiting for a response."""
LOG.debug(_('Making asynchronous fanout cast...'))
_pack_context(msg, context)
- conn = Connection.instance()
- publisher = FanoutPublisher(topic, connection=conn)
- publisher.send(msg)
- publisher.close()
+ with ConnectionPool.item() as conn:
+ publisher = FanoutPublisher(topic, connection=conn)
+ publisher.send(msg)
+ publisher.close()
def generic_response(message_data, message):
@@ -459,6 +595,7 @@ def send_message(topic, message, wait=True):
if wait:
consumer.wait()
+ consumer.close()
if __name__ == '__main__':
diff --git a/nova/service.py b/nova/service.py
index 2532b9df2..74f9f04d8 100644
--- a/nova/service.py
+++ b/nova/service.py
@@ -19,14 +19,11 @@
"""Generic Node baseclass for all workers that run on hosts."""
+import greenlet
import inspect
import os
-import sys
-import time
-from eventlet import event
from eventlet import greenthread
-from eventlet import greenpool
from nova import context
from nova import db
@@ -91,27 +88,37 @@ class Service(object):
if 'nova-compute' == self.binary:
self.manager.update_available_resource(ctxt)
- conn1 = rpc.Connection.instance(new=True)
- conn2 = rpc.Connection.instance(new=True)
- conn3 = rpc.Connection.instance(new=True)
- if self.report_interval:
- consumer_all = rpc.TopicAdapterConsumer(
- connection=conn1,
- topic=self.topic,
- proxy=self)
- consumer_node = rpc.TopicAdapterConsumer(
- connection=conn2,
- topic='%s.%s' % (self.topic, self.host),
- proxy=self)
- fanout = rpc.FanoutAdapterConsumer(
- connection=conn3,
- topic=self.topic,
- proxy=self)
-
- self.timers.append(consumer_all.attach_to_eventlet())
- self.timers.append(consumer_node.attach_to_eventlet())
- self.timers.append(fanout.attach_to_eventlet())
+ self.conn = rpc.Connection.instance(new=True)
+ logging.debug("Creating Consumer connection for Service %s" %
+ self.topic)
+
+ # Share this same connection for these Consumers
+ consumer_all = rpc.TopicAdapterConsumer(
+ connection=self.conn,
+ topic=self.topic,
+ proxy=self)
+ consumer_node = rpc.TopicAdapterConsumer(
+ connection=self.conn,
+ topic='%s.%s' % (self.topic, self.host),
+ proxy=self)
+ fanout = rpc.FanoutAdapterConsumer(
+ connection=self.conn,
+ topic=self.topic,
+ proxy=self)
+ consumer_set = rpc.ConsumerSet(
+ connection=self.conn,
+ consumer_list=[consumer_all, consumer_node, fanout])
+
+ # Wait forever, processing these consumers
+ def _wait():
+ try:
+ consumer_set.wait()
+ finally:
+ consumer_set.close()
+
+ self.consumer_set_thread = greenthread.spawn(_wait)
+ if self.report_interval:
pulse = utils.LoopingCall(self.report_state)
pulse.start(interval=self.report_interval, now=False)
self.timers.append(pulse)
@@ -174,6 +181,11 @@ class Service(object):
logging.warn(_('Service killed that has no database entry'))
def stop(self):
+ self.consumer_set_thread.kill()
+ try:
+ self.consumer_set_thread.wait()
+ except greenlet.GreenletExit:
+ pass
for x in self.timers:
try:
x.stop()
@@ -240,6 +252,10 @@ class WsgiService(object):
def wait(self):
self.wsgi_app.wait()
+ def get_socket_info(self, api_name):
+ """Returns the (host, port) that an API was started on."""
+ return self.wsgi_app.socket_info[api_name]
+
class ApiService(WsgiService):
"""Class for our nova-api service."""
@@ -318,8 +334,10 @@ def _run_wsgi(paste_config_file, apis):
logging.debug(_('App Config: %(api)s\n%(config)r') % locals())
logging.info(_('Running %s API'), api)
app = wsgi.load_paste_app(paste_config_file, api)
- apps.append((app, getattr(FLAGS, '%s_listen_port' % api),
- getattr(FLAGS, '%s_listen' % api)))
+ apps.append((app,
+ getattr(FLAGS, '%s_listen_port' % api),
+ getattr(FLAGS, '%s_listen' % api),
+ api))
if len(apps) == 0:
logging.error(_('No known API applications configured in %s.'),
paste_config_file)
diff --git a/nova/test.py b/nova/test.py
index 00a16dd68..5fe4f238d 100644
--- a/nova/test.py
+++ b/nova/test.py
@@ -31,17 +31,15 @@ import uuid
import unittest
import mox
-import shutil
import stubout
from eventlet import greenthread
-from nova import context
-from nova import db
from nova import fakerabbit
from nova import flags
from nova import rpc
from nova import service
from nova import wsgi
+from nova.virt import fake
FLAGS = flags.FLAGS
@@ -85,6 +83,7 @@ class TestCase(unittest.TestCase):
self._monkey_patch_attach()
self._monkey_patch_wsgi()
self._original_flags = FLAGS.FlagValuesDict()
+ rpc.ConnectionPool = rpc.Pool(max_size=FLAGS.rpc_conn_pool_size)
def tearDown(self):
"""Runs after each test method to tear down test environment."""
@@ -99,6 +98,10 @@ class TestCase(unittest.TestCase):
if FLAGS.fake_rabbit:
fakerabbit.reset_all()
+ if FLAGS.connection_type == 'fake':
+ if hasattr(fake.FakeConnection, '_instance'):
+ del fake.FakeConnection._instance
+
# Reset any overriden flags
self.reset_flags()
@@ -212,12 +215,13 @@ class TestCase(unittest.TestCase):
for key in d1keys:
d1value = d1[key]
d2value = d2[key]
-
try:
error = abs(float(d1value) - float(d2value))
within_tolerance = error <= tolerance
- except ValueError:
+ except (ValueError, TypeError):
# If both values aren't convertable to float, just ignore
+ # ValueError if arg is a str, TypeError if it's something else
+ # (like None)
within_tolerance = False
if hasattr(d1value, 'keys') and hasattr(d2value, 'keys'):
diff --git a/nova/tests/api/openstack/extensions/foxinsocks.py b/nova/tests/api/openstack/extensions/foxinsocks.py
index 0860b51ac..dbdd0928a 100644
--- a/nova/tests/api/openstack/extensions/foxinsocks.py
+++ b/nova/tests/api/openstack/extensions/foxinsocks.py
@@ -63,31 +63,33 @@ class Foxinsocks(object):
self._delete_tweedle))
return actions
- def get_response_extensions(self):
- response_exts = []
+ def get_request_extensions(self):
+ request_exts = []
- def _goose_handler(res):
+ def _goose_handler(req, res):
#NOTE: This only handles JSON responses.
# You can use content type header to test for XML.
data = json.loads(res.body)
- data['flavor']['googoose'] = "Gooey goo for chewy chewing!"
- return data
+ data['flavor']['googoose'] = req.GET.get('chewing')
+ res.body = json.dumps(data)
+ return res
- resp_ext = extensions.ResponseExtension('GET', '/v1.1/flavors/:(id)',
+ req_ext1 = extensions.RequestExtension('GET', '/v1.1/flavors/:(id)',
_goose_handler)
- response_exts.append(resp_ext)
+ request_exts.append(req_ext1)
- def _bands_handler(res):
+ def _bands_handler(req, res):
#NOTE: This only handles JSON responses.
# You can use content type header to test for XML.
data = json.loads(res.body)
data['big_bands'] = 'Pig Bands!'
- return data
+ res.body = json.dumps(data)
+ return res
- resp_ext2 = extensions.ResponseExtension('GET', '/v1.1/flavors/:(id)',
+ req_ext2 = extensions.RequestExtension('GET', '/v1.1/flavors/:(id)',
_bands_handler)
- response_exts.append(resp_ext2)
- return response_exts
+ request_exts.append(req_ext2)
+ return request_exts
def _add_tweedle(self, input_dict, req, id):
diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py
index 8b0729c35..8e0156afa 100644
--- a/nova/tests/api/openstack/fakes.py
+++ b/nova/tests/api/openstack/fakes.py
@@ -166,11 +166,11 @@ def stub_out_glance(stubs, initial_fixtures=None):
def __init__(self, initial_fixtures):
self.fixtures = initial_fixtures or []
- def fake_get_images(self):
+ def fake_get_images(self, filters=None):
return [dict(id=f['id'], name=f['name'])
for f in self.fixtures]
- def fake_get_images_detailed(self):
+ def fake_get_images_detailed(self, filters=None):
return copy.deepcopy(self.fixtures)
def fake_get_image_meta(self, image_id):
@@ -228,6 +228,9 @@ class FakeToken(object):
# FIXME(sirp): let's not use id here
id = 0
+ def __getitem__(self, key):
+ return getattr(self, key)
+
def __init__(self, **kwargs):
FakeToken.id += 1
self.id = FakeToken.id
diff --git a/nova/tests/api/openstack/test_extensions.py b/nova/tests/api/openstack/test_extensions.py
index 481d34ed1..544298602 100644
--- a/nova/tests/api/openstack/test_extensions.py
+++ b/nova/tests/api/openstack/test_extensions.py
@@ -45,10 +45,10 @@ class StubController(nova.wsgi.Controller):
class StubExtensionManager(object):
- def __init__(self, resource_ext=None, action_ext=None, response_ext=None):
+ def __init__(self, resource_ext=None, action_ext=None, request_ext=None):
self.resource_ext = resource_ext
self.action_ext = action_ext
- self.response_ext = response_ext
+ self.request_ext = request_ext
def get_name(self):
return "Tweedle Beetle Extension"
@@ -71,11 +71,11 @@ class StubExtensionManager(object):
action_exts.append(self.action_ext)
return action_exts
- def get_response_extensions(self):
- response_exts = []
- if self.response_ext:
- response_exts.append(self.response_ext)
- return response_exts
+ def get_request_extensions(self):
+ request_extensions = []
+ if self.request_ext:
+ request_extensions.append(self.request_ext)
+ return request_extensions
class ExtensionControllerTest(unittest.TestCase):
@@ -183,10 +183,10 @@ class ActionExtensionTest(unittest.TestCase):
self.assertEqual(404, response.status_int)
-class ResponseExtensionTest(unittest.TestCase):
+class RequestExtensionTest(unittest.TestCase):
def setUp(self):
- super(ResponseExtensionTest, self).setUp()
+ super(RequestExtensionTest, self).setUp()
self.stubs = stubout.StubOutForTesting()
fakes.FakeAuthManager.reset_fake_data()
fakes.FakeAuthDatabase.data = {}
@@ -195,42 +195,39 @@ class ResponseExtensionTest(unittest.TestCase):
def tearDown(self):
self.stubs.UnsetAll()
- super(ResponseExtensionTest, self).tearDown()
+ super(RequestExtensionTest, self).tearDown()
def test_get_resources_with_stub_mgr(self):
- test_resp = "Gooey goo for chewy chewing!"
-
- def _resp_handler(res):
+ def _req_handler(req, res):
# only handle JSON responses
data = json.loads(res.body)
- data['flavor']['googoose'] = test_resp
- return data
+ data['flavor']['googoose'] = req.GET.get('chewing')
+ res.body = json.dumps(data)
+ return res
- resp_ext = extensions.ResponseExtension('GET',
+ req_ext = extensions.RequestExtension('GET',
'/v1.1/flavors/:(id)',
- _resp_handler)
+ _req_handler)
- manager = StubExtensionManager(None, None, resp_ext)
+ manager = StubExtensionManager(None, None, req_ext)
app = fakes.wsgi_app()
ext_midware = extensions.ExtensionMiddleware(app, manager)
- request = webob.Request.blank("/v1.1/flavors/1")
+ request = webob.Request.blank("/v1.1/flavors/1?chewing=bluegoo")
request.environ['api.version'] = '1.1'
response = request.get_response(ext_midware)
self.assertEqual(200, response.status_int)
response_data = json.loads(response.body)
- self.assertEqual(test_resp, response_data['flavor']['googoose'])
+ self.assertEqual('bluegoo', response_data['flavor']['googoose'])
def test_get_resources_with_mgr(self):
- test_resp = "Gooey goo for chewy chewing!"
-
app = fakes.wsgi_app()
ext_midware = extensions.ExtensionMiddleware(app)
- request = webob.Request.blank("/v1.1/flavors/1")
+ request = webob.Request.blank("/v1.1/flavors/1?chewing=newblue")
request.environ['api.version'] = '1.1'
response = request.get_response(ext_midware)
self.assertEqual(200, response.status_int)
response_data = json.loads(response.body)
- self.assertEqual(test_resp, response_data['flavor']['googoose'])
+ self.assertEqual('newblue', response_data['flavor']['googoose'])
self.assertEqual("Pig Bands!", response_data['big_bands'])
diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py
index 2c329f920..9f1f28611 100644
--- a/nova/tests/api/openstack/test_images.py
+++ b/nova/tests/api/openstack/test_images.py
@@ -28,6 +28,7 @@ import shutil
import tempfile
import xml.dom.minidom as minidom
+import mox
import stubout
import webob
@@ -708,6 +709,146 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
self.assertDictListMatch(expected, response_list)
+ def test_image_filter_with_name(self):
+ mocker = mox.Mox()
+ image_service = mocker.CreateMockAnything()
+ context = object()
+ filters = {'name': 'testname'}
+ image_service.index(context, filters).AndReturn([])
+ mocker.ReplayAll()
+ request = webob.Request.blank(
+ '/v1.1/images?name=testname')
+ request.environ['nova.context'] = context
+ controller = images.ControllerV11(image_service=image_service)
+ controller.index(request)
+ mocker.VerifyAll()
+
+ def test_image_filter_with_status(self):
+ mocker = mox.Mox()
+ image_service = mocker.CreateMockAnything()
+ context = object()
+ filters = {'status': 'ACTIVE'}
+ image_service.index(context, filters).AndReturn([])
+ mocker.ReplayAll()
+ request = webob.Request.blank(
+ '/v1.1/images?status=ACTIVE')
+ request.environ['nova.context'] = context
+ controller = images.ControllerV11(image_service=image_service)
+ controller.index(request)
+ mocker.VerifyAll()
+
+ def test_image_filter_with_property(self):
+ mocker = mox.Mox()
+ image_service = mocker.CreateMockAnything()
+ context = object()
+ filters = {'property-test': '3'}
+ image_service.index(context, filters).AndReturn([])
+ mocker.ReplayAll()
+ request = webob.Request.blank(
+ '/v1.1/images?property-test=3')
+ request.environ['nova.context'] = context
+ controller = images.ControllerV11(image_service=image_service)
+ controller.index(request)
+ mocker.VerifyAll()
+
+ def test_image_filter_not_supported(self):
+ mocker = mox.Mox()
+ image_service = mocker.CreateMockAnything()
+ context = object()
+ filters = {'status': 'ACTIVE'}
+ image_service.index(context, filters).AndReturn([])
+ mocker.ReplayAll()
+ request = webob.Request.blank(
+ '/v1.1/images?status=ACTIVE&UNSUPPORTEDFILTER=testname')
+ request.environ['nova.context'] = context
+ controller = images.ControllerV11(image_service=image_service)
+ controller.index(request)
+ mocker.VerifyAll()
+
+ def test_image_no_filters(self):
+ mocker = mox.Mox()
+ image_service = mocker.CreateMockAnything()
+ context = object()
+ filters = {}
+ image_service.index(context, filters).AndReturn([])
+ mocker.ReplayAll()
+ request = webob.Request.blank(
+ '/v1.1/images')
+ request.environ['nova.context'] = context
+ controller = images.ControllerV11(image_service=image_service)
+ controller.index(request)
+ mocker.VerifyAll()
+
+ def test_image_detail_filter_with_name(self):
+ mocker = mox.Mox()
+ image_service = mocker.CreateMockAnything()
+ context = object()
+ filters = {'name': 'testname'}
+ image_service.detail(context, filters).AndReturn([])
+ mocker.ReplayAll()
+ request = webob.Request.blank(
+ '/v1.1/images/detail?name=testname')
+ request.environ['nova.context'] = context
+ controller = images.ControllerV11(image_service=image_service)
+ controller.detail(request)
+ mocker.VerifyAll()
+
+ def test_image_detail_filter_with_status(self):
+ mocker = mox.Mox()
+ image_service = mocker.CreateMockAnything()
+ context = object()
+ filters = {'status': 'ACTIVE'}
+ image_service.detail(context, filters).AndReturn([])
+ mocker.ReplayAll()
+ request = webob.Request.blank(
+ '/v1.1/images/detail?status=ACTIVE')
+ request.environ['nova.context'] = context
+ controller = images.ControllerV11(image_service=image_service)
+ controller.detail(request)
+ mocker.VerifyAll()
+
+ def test_image_detail_filter_with_property(self):
+ mocker = mox.Mox()
+ image_service = mocker.CreateMockAnything()
+ context = object()
+ filters = {'property-test': '3'}
+ image_service.detail(context, filters).AndReturn([])
+ mocker.ReplayAll()
+ request = webob.Request.blank(
+ '/v1.1/images/detail?property-test=3')
+ request.environ['nova.context'] = context
+ controller = images.ControllerV11(image_service=image_service)
+ controller.detail(request)
+ mocker.VerifyAll()
+
+ def test_image_detail_filter_not_supported(self):
+ mocker = mox.Mox()
+ image_service = mocker.CreateMockAnything()
+ context = object()
+ filters = {'status': 'ACTIVE'}
+ image_service.detail(context, filters).AndReturn([])
+ mocker.ReplayAll()
+ request = webob.Request.blank(
+ '/v1.1/images/detail?status=ACTIVE&UNSUPPORTEDFILTER=testname')
+ request.environ['nova.context'] = context
+ controller = images.ControllerV11(image_service=image_service)
+ controller.detail(request)
+ mocker.VerifyAll()
+
+ def test_image_detail_no_filters(self):
+ mocker = mox.Mox()
+ image_service = mocker.CreateMockAnything()
+ context = object()
+ filters = {}
+ image_service.detail(context, filters).AndReturn([])
+ mocker.ReplayAll()
+ request = webob.Request.blank(
+ '/v1.1/images/detail')
+ request.environ['nova.context'] = context
+ controller = images.ControllerV11(image_service=image_service)
+ controller.detail(request)
+ mocker.VerifyAll()
+
def test_get_image_found(self):
req = webob.Request.blank('/v1.0/images/123')
res = req.get_response(fakes.wsgi_app())
diff --git a/nova/tests/api/openstack/test_limits.py b/nova/tests/api/openstack/test_limits.py
index 45bd4d501..70f59eda6 100644
--- a/nova/tests/api/openstack/test_limits.py
+++ b/nova/tests/api/openstack/test_limits.py
@@ -27,6 +27,7 @@ import webob
from xml.dom.minidom import parseString
+import nova.context
from nova.api.openstack import limits
@@ -47,6 +48,13 @@ class BaseLimitTestSuite(unittest.TestCase):
self.time = 0.0
self.stubs = stubout.StubOutForTesting()
self.stubs.Set(limits.Limit, "_get_time", self._get_time)
+ self.absolute_limits = {}
+
+ def stub_get_project_quotas(context, project_id):
+ return self.absolute_limits
+
+ self.stubs.Set(nova.quota, "get_project_quotas",
+ stub_get_project_quotas)
def tearDown(self):
"""Run after each test."""
@@ -75,6 +83,8 @@ class LimitsControllerV10Test(BaseLimitTestSuite):
"action": "index",
"controller": "",
})
+ context = nova.context.RequestContext('testuser', 'testproject')
+ request.environ["nova.context"] = context
return request
def _populate_limits(self, request):
@@ -86,6 +96,18 @@ class LimitsControllerV10Test(BaseLimitTestSuite):
request.environ["nova.limits"] = _limits
return request
+ def _setup_absolute_limits(self):
+ self.absolute_limits = {
+ 'instances': 5,
+ 'cores': 8,
+ 'ram': 2 ** 13,
+ 'volumes': 21,
+ 'gigabytes': 34,
+ 'metadata_items': 55,
+ 'injected_files': 89,
+ 'injected_file_content_bytes': 144,
+ }
+
def test_empty_index_json(self):
"""Test getting empty limit details in JSON."""
request = self._get_index_request()
@@ -103,6 +125,7 @@ class LimitsControllerV10Test(BaseLimitTestSuite):
"""Test getting limit details in JSON."""
request = self._get_index_request()
request = self._populate_limits(request)
+ self._setup_absolute_limits()
response = request.get_response(self.controller)
expected = {
"limits": {
@@ -124,7 +147,15 @@ class LimitsControllerV10Test(BaseLimitTestSuite):
"remaining": 5,
"unit": "HOUR",
}],
- "absolute": {},
+ "absolute": {
+ "maxTotalInstances": 5,
+ "maxTotalCores": 8,
+ "maxTotalRAMSize": 2 ** 13,
+ "maxServerMeta": 55,
+ "maxImageMeta": 55,
+ "maxPersonality": 89,
+ "maxPersonalitySize": 144,
+ },
},
}
body = json.loads(response.body)
@@ -188,6 +219,8 @@ class LimitsControllerV11Test(BaseLimitTestSuite):
"action": "index",
"controller": "",
})
+ context = nova.context.RequestContext('testuser', 'testproject')
+ request.environ["nova.context"] = context
return request
def _populate_limits(self, request):
@@ -218,6 +251,11 @@ class LimitsControllerV11Test(BaseLimitTestSuite):
"""Test getting limit details in JSON."""
request = self._get_index_request()
request = self._populate_limits(request)
+ self.absolute_limits = {
+ 'ram': 512,
+ 'instances': 5,
+ 'cores': 21,
+ }
response = request.get_response(self.controller)
expected = {
"limits": {
@@ -257,12 +295,110 @@ class LimitsControllerV11Test(BaseLimitTestSuite):
},
],
+ "absolute": {
+ "maxTotalRAMSize": 512,
+ "maxTotalInstances": 5,
+ "maxTotalCores": 21,
+ },
+ },
+ }
+ body = json.loads(response.body)
+ self.assertEqual(expected, body)
+
+ def _populate_limits_diff_regex(self, request):
+ """Put limit info into a request."""
+ _limits = [
+ limits.Limit("GET", "*", ".*", 10, 60).display(),
+ limits.Limit("GET", "*", "*.*", 10, 60).display(),
+ ]
+ request.environ["nova.limits"] = _limits
+ return request
+
+ def test_index_diff_regex(self):
+ """Test getting limit details in JSON."""
+ request = self._get_index_request()
+ request = self._populate_limits_diff_regex(request)
+ response = request.get_response(self.controller)
+ expected = {
+ "limits": {
+ "rate": [
+ {
+ "regex": ".*",
+ "uri": "*",
+ "limit": [
+ {
+ "verb": "GET",
+ "next-available": 0,
+ "unit": "MINUTE",
+ "value": 10,
+ "remaining": 10,
+ },
+ ],
+ },
+ {
+ "regex": "*.*",
+ "uri": "*",
+ "limit": [
+ {
+ "verb": "GET",
+ "next-available": 0,
+ "unit": "MINUTE",
+ "value": 10,
+ "remaining": 10,
+ },
+ ],
+ },
+
+ ],
"absolute": {},
},
}
body = json.loads(response.body)
self.assertEqual(expected, body)
+ def _test_index_absolute_limits_json(self, expected):
+ request = self._get_index_request()
+ response = request.get_response(self.controller)
+ body = json.loads(response.body)
+ self.assertEqual(expected, body['limits']['absolute'])
+
+ def test_index_ignores_extra_absolute_limits_json(self):
+ self.absolute_limits = {'unknown_limit': 9001}
+ self._test_index_absolute_limits_json({})
+
+ def test_index_absolute_ram_json(self):
+ self.absolute_limits = {'ram': 1024}
+ self._test_index_absolute_limits_json({'maxTotalRAMSize': 1024})
+
+ def test_index_absolute_cores_json(self):
+ self.absolute_limits = {'cores': 17}
+ self._test_index_absolute_limits_json({'maxTotalCores': 17})
+
+ def test_index_absolute_instances_json(self):
+ self.absolute_limits = {'instances': 19}
+ self._test_index_absolute_limits_json({'maxTotalInstances': 19})
+
+ def test_index_absolute_metadata_json(self):
+ # NOTE: both server metadata and image metadata are overloaded
+ # into metadata_items
+ self.absolute_limits = {'metadata_items': 23}
+ expected = {
+ 'maxServerMeta': 23,
+ 'maxImageMeta': 23,
+ }
+ self._test_index_absolute_limits_json(expected)
+
+ def test_index_absolute_injected_files(self):
+ self.absolute_limits = {
+ 'injected_files': 17,
+ 'injected_file_content_bytes': 86753,
+ }
+ expected = {
+ 'maxPersonality': 17,
+ 'maxPersonalitySize': 86753,
+ }
+ self._test_index_absolute_limits_json(expected)
+
class LimitMiddlewareTest(BaseLimitTestSuite):
"""
diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py
index e8182b6a9..fbde5c9ce 100644
--- a/nova/tests/api/openstack/test_servers.py
+++ b/nova/tests/api/openstack/test_servers.py
@@ -138,6 +138,16 @@ def find_host(self, context, instance_id):
return "nova"
+class MockSetAdminPassword(object):
+ def __init__(self):
+ self.instance_id = None
+ self.password = None
+
+ def __call__(self, context, instance_id, password):
+ self.instance_id = instance_id
+ self.password = password
+
+
class ServersTest(test.TestCase):
def setUp(self):
@@ -764,8 +774,7 @@ class ServersTest(test.TestCase):
def server_update(context, id, params):
filtered_dict = dict(
- display_name='server_test',
- admin_pass='bacon',
+ display_name='server_test'
)
self.assertEqual(params, filtered_dict)
return filtered_dict
@@ -773,6 +782,8 @@ class ServersTest(test.TestCase):
self.stubs.Set(nova.db.api, 'instance_update',
server_update)
self.stubs.Set(nova.compute.api.API, "_find_host", find_host)
+ mock_method = MockSetAdminPassword()
+ self.stubs.Set(nova.compute.api.API, 'set_admin_password', mock_method)
req = webob.Request.blank('/v1.0/servers/1')
req.method = 'PUT'
@@ -780,6 +791,8 @@ class ServersTest(test.TestCase):
req.body = self.body
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 204)
+ self.assertEqual(mock_method.instance_id, '1')
+ self.assertEqual(mock_method.password, 'bacon')
def test_update_server_adminPass_ignored_v1_1(self):
inst_dict = dict(name='server_test', adminPass='bacon')
@@ -996,16 +1009,6 @@ class ServersTest(test.TestCase):
self.assertEqual(res.status_int, 501)
def test_server_change_password_v1_1(self):
-
- class MockSetAdminPassword(object):
- def __init__(self):
- self.instance_id = None
- self.password = None
-
- def __call__(self, context, instance_id, password):
- self.instance_id = instance_id
- self.password = password
-
mock_method = MockSetAdminPassword()
self.stubs.Set(nova.compute.api.API, 'set_admin_password', mock_method)
body = {'changePassword': {'adminPass': '1234pass'}}
diff --git a/nova/tests/api/openstack/test_zones.py b/nova/tests/api/openstack/test_zones.py
index b42b3e7d8..fa2e05033 100644
--- a/nova/tests/api/openstack/test_zones.py
+++ b/nova/tests/api/openstack/test_zones.py
@@ -21,6 +21,7 @@ import json
import nova.db
from nova import context
from nova import crypto
+from nova import exception
from nova import flags
from nova import test
from nova.api.openstack import zones
diff --git a/nova/tests/fake_flags.py b/nova/tests/fake_flags.py
index 5d7ca98b5..ecefc464a 100644
--- a/nova/tests/fake_flags.py
+++ b/nova/tests/fake_flags.py
@@ -21,24 +21,24 @@ from nova import flags
FLAGS = flags.FLAGS
flags.DECLARE('volume_driver', 'nova.volume.manager')
-FLAGS.volume_driver = 'nova.volume.driver.FakeISCSIDriver'
-FLAGS.connection_type = 'fake'
-FLAGS.fake_rabbit = True
+FLAGS['volume_driver'].SetDefault('nova.volume.driver.FakeISCSIDriver')
+FLAGS['connection_type'].SetDefault('fake')
+FLAGS['fake_rabbit'].SetDefault(True)
flags.DECLARE('auth_driver', 'nova.auth.manager')
-FLAGS.auth_driver = 'nova.auth.dbdriver.DbDriver'
+FLAGS['auth_driver'].SetDefault('nova.auth.dbdriver.DbDriver')
flags.DECLARE('network_size', 'nova.network.manager')
flags.DECLARE('num_networks', 'nova.network.manager')
flags.DECLARE('fake_network', 'nova.network.manager')
-FLAGS.network_size = 8
-FLAGS.num_networks = 2
-FLAGS.fake_network = True
-FLAGS.image_service = 'nova.image.local.LocalImageService'
+FLAGS['network_size'].SetDefault(8)
+FLAGS['num_networks'].SetDefault(2)
+FLAGS['fake_network'].SetDefault(True)
+FLAGS['image_service'].SetDefault('nova.image.local.LocalImageService')
flags.DECLARE('num_shelves', 'nova.volume.driver')
flags.DECLARE('blades_per_shelf', 'nova.volume.driver')
flags.DECLARE('iscsi_num_targets', 'nova.volume.driver')
-FLAGS.num_shelves = 2
-FLAGS.blades_per_shelf = 4
-FLAGS.iscsi_num_targets = 8
-FLAGS.verbose = True
-FLAGS.sqlite_db = "tests.sqlite"
-FLAGS.use_ipv6 = True
+FLAGS['num_shelves'].SetDefault(2)
+FLAGS['blades_per_shelf'].SetDefault(4)
+FLAGS['iscsi_num_targets'].SetDefault(8)
+FLAGS['verbose'].SetDefault(True)
+FLAGS['sqlite_db'].SetDefault("tests.sqlite")
+FLAGS['use_ipv6'].SetDefault(True)
diff --git a/nova/tests/image/test_glance.py b/nova/tests/image/test_glance.py
index 109905ded..6d108d494 100644
--- a/nova/tests/image/test_glance.py
+++ b/nova/tests/image/test_glance.py
@@ -34,7 +34,7 @@ class StubGlanceClient(object):
def get_image_meta(self, image_id):
return self.images[image_id]
- def get_images_detailed(self):
+ def get_images_detailed(self, filters=None):
return self.images.itervalues()
def get_image(self, image_id):
diff --git a/nova/tests/integrated/api/client.py b/nova/tests/integrated/api/client.py
index 7e20c9b00..eb9a3056e 100644
--- a/nova/tests/integrated/api/client.py
+++ b/nova/tests/integrated/api/client.py
@@ -152,7 +152,10 @@ class TestOpenStackClient(object):
def _decode_json(self, response):
body = response.read()
LOG.debug(_("Decoding JSON: %s") % (body))
- return json.loads(body)
+ if body:
+ return json.loads(body)
+ else:
+ return ""
def api_get(self, relative_uri, **kwargs):
kwargs.setdefault('check_response_status', [200])
@@ -166,7 +169,7 @@ class TestOpenStackClient(object):
headers['Content-Type'] = 'application/json'
kwargs['body'] = json.dumps(body)
- kwargs.setdefault('check_response_status', [200])
+ kwargs.setdefault('check_response_status', [200, 202])
response = self.api_request(relative_uri, **kwargs)
return self._decode_json(response)
@@ -185,6 +188,9 @@ class TestOpenStackClient(object):
def post_server(self, server):
return self.api_post('/servers', server)['server']
+ def post_server_action(self, server_id, data):
+ return self.api_post('/servers/%s/action' % server_id, data)
+
def delete_server(self, server_id):
return self.api_delete('/servers/%s' % server_id)
diff --git a/nova/tests/integrated/integrated_helpers.py b/nova/tests/integrated/integrated_helpers.py
index 2e5d67017..7f590441e 100644
--- a/nova/tests/integrated/integrated_helpers.py
+++ b/nova/tests/integrated/integrated_helpers.py
@@ -154,13 +154,10 @@ class _IntegratedTestBase(test.TestCase):
# set up services
self.start_service('compute')
self.start_service('volume')
- # NOTE(justinsb): There's a bug here which is eluding me...
- # If we start the network_service, all is good, but then subsequent
- # tests fail: CloudTestCase.test_ajax_console in particular.
- #self.start_service('network')
+ self.start_service('network')
self.start_service('scheduler')
- self.auth_url = self._start_api_service()
+ self._start_api_service()
self.context = IntegratedUnitTestContext(self.auth_url)
@@ -174,8 +171,10 @@ class _IntegratedTestBase(test.TestCase):
if not api_service:
raise Exception("API Service was None")
- auth_url = 'http://localhost:8774/v1.1'
- return auth_url
+ self.api_service = api_service
+
+ host, port = api_service.get_socket_info('osapi')
+ self.auth_url = 'http://%s:%s/v1.1' % (host, port)
def tearDown(self):
self.context.cleanup()
@@ -184,6 +183,11 @@ class _IntegratedTestBase(test.TestCase):
def _get_flags(self):
"""An opportunity to setup flags, before the services are started."""
f = {}
+
+ # Auto-assign ports to allow concurrent tests
+ f['ec2_listen_port'] = 0
+ f['osapi_listen_port'] = 0
+
f['image_service'] = 'nova.image.fake.FakeImageService'
f['fake_network'] = True
return f
diff --git a/nova/tests/integrated/test_servers.py b/nova/tests/integrated/test_servers.py
index e89d0100a..a67fa1bb5 100644
--- a/nova/tests/integrated/test_servers.py
+++ b/nova/tests/integrated/test_servers.py
@@ -179,6 +179,112 @@ class ServersTest(integrated_helpers._IntegratedTestBase):
# Cleanup
self._delete_server(created_server_id)
+ def test_create_and_rebuild_server(self):
+ """Rebuild a server."""
+
+ # create a server with initially has no metadata
+ server = self._build_minimal_create_server_request()
+ server_post = {'server': server}
+ created_server = self.api.post_server(server_post)
+ LOG.debug("created_server: %s" % created_server)
+ self.assertTrue(created_server['id'])
+ created_server_id = created_server['id']
+
+ # rebuild the server with metadata
+ post = {}
+ post['rebuild'] = {
+ "imageRef": "https://localhost/v1.1/32278/images/2",
+ "name": "blah"
+ }
+
+ self.api.post_server_action(created_server_id, post)
+ LOG.debug("rebuilt server: %s" % created_server)
+ self.assertTrue(created_server['id'])
+
+ found_server = self.api.get_server(created_server_id)
+ self.assertEqual(created_server_id, found_server['id'])
+ self.assertEqual({}, found_server.get('metadata'))
+ self.assertEqual('blah', found_server.get('name'))
+
+ # Cleanup
+ self._delete_server(created_server_id)
+
+ def test_create_and_rebuild_server_with_metadata(self):
+ """Rebuild a server with metadata."""
+
+ # create a server with initially has no metadata
+ server = self._build_minimal_create_server_request()
+ server_post = {'server': server}
+ created_server = self.api.post_server(server_post)
+ LOG.debug("created_server: %s" % created_server)
+ self.assertTrue(created_server['id'])
+ created_server_id = created_server['id']
+
+ # rebuild the server with metadata
+ post = {}
+ post['rebuild'] = {
+ "imageRef": "https://localhost/v1.1/32278/images/2",
+ "name": "blah"
+ }
+
+ metadata = {}
+ for i in range(30):
+ metadata['key_%s' % i] = 'value_%s' % i
+
+ post['rebuild']['metadata'] = metadata
+
+ self.api.post_server_action(created_server_id, post)
+ LOG.debug("rebuilt server: %s" % created_server)
+ self.assertTrue(created_server['id'])
+
+ found_server = self.api.get_server(created_server_id)
+ self.assertEqual(created_server_id, found_server['id'])
+ self.assertEqual(metadata, found_server.get('metadata'))
+ self.assertEqual('blah', found_server.get('name'))
+
+ # Cleanup
+ self._delete_server(created_server_id)
+
+ def test_create_and_rebuild_server_with_metadata_removal(self):
+ """Rebuild a server with metadata."""
+
+ # create a server with initially has no metadata
+ server = self._build_minimal_create_server_request()
+ server_post = {'server': server}
+
+ metadata = {}
+ for i in range(30):
+ metadata['key_%s' % i] = 'value_%s' % i
+
+ server_post['server']['metadata'] = metadata
+
+ created_server = self.api.post_server(server_post)
+ LOG.debug("created_server: %s" % created_server)
+ self.assertTrue(created_server['id'])
+ created_server_id = created_server['id']
+
+ # rebuild the server with metadata
+ post = {}
+ post['rebuild'] = {
+ "imageRef": "https://localhost/v1.1/32278/images/2",
+ "name": "blah"
+ }
+
+ metadata = {}
+ post['rebuild']['metadata'] = metadata
+
+ self.api.post_server_action(created_server_id, post)
+ LOG.debug("rebuilt server: %s" % created_server)
+ self.assertTrue(created_server['id'])
+
+ found_server = self.api.get_server(created_server_id)
+ self.assertEqual(created_server_id, found_server['id'])
+ self.assertEqual(metadata, found_server.get('metadata'))
+ self.assertEqual('blah', found_server.get('name'))
+
+ # Cleanup
+ self._delete_server(created_server_id)
+
if __name__ == "__main__":
unittest.main()
diff --git a/nova/tests/public_key/dummy.fingerprint b/nova/tests/public_key/dummy.fingerprint
new file mode 100644
index 000000000..715bca27a
--- /dev/null
+++ b/nova/tests/public_key/dummy.fingerprint
@@ -0,0 +1 @@
+1c:87:d1:d9:32:fd:62:3c:78:2b:c0:ad:c0:15:88:df
diff --git a/nova/tests/public_key/dummy.pub b/nova/tests/public_key/dummy.pub
new file mode 100644
index 000000000..d4cf2bc0d
--- /dev/null
+++ b/nova/tests/public_key/dummy.pub
@@ -0,0 +1 @@
+ssh-dss AAAAB3NzaC1kc3MAAACBAMGJlY9XEIm2X234pdO5yFWMp2JuOQx8U0E815IVXhmKxYCBK9ZakgZOIQmPbXoGYyV+mziDPp6HJ0wKYLQxkwLEFr51fAZjWQvRss0SinURRuLkockDfGFtD4pYJthekr/rlqMKlBSDUSpGq8jUWW60UJ18FGooFpxR7ESqQRx/AAAAFQC96LRglaUeeP+E8U/yblEJocuiWwAAAIA3XiMR8Skiz/0aBm5K50SeQznQuMJTyzt9S9uaz5QZWiFu69hOyGSFGw8fqgxEkXFJIuHobQQpGYQubLW0NdaYRqyE/Vud3JUJUb8Texld6dz8vGemyB5d1YvtSeHIo8/BGv2msOqR3u5AZTaGCBD9DhpSGOKHEdNjTtvpPd8S8gAAAIBociGZ5jf09iHLVENhyXujJbxfGRPsyNTyARJfCOGl0oFV6hEzcQyw8U/ePwjgvjc2UizMWLl8tsb2FXKHRdc2v+ND3Us+XqKQ33X3ADP4FZ/+Oj213gMyhCmvFTP0u5FmHog9My4CB7YcIWRuUR42WlhQ2IfPvKwUoTk3R+T6Og== www-data@mk
diff --git a/nova/tests/test_api.py b/nova/tests/test_api.py
index 97f401b87..7c0331eff 100644
--- a/nova/tests/test_api.py
+++ b/nova/tests/test_api.py
@@ -224,6 +224,29 @@ class ApiEc2TestCase(test.TestCase):
self.manager.delete_project(project)
self.manager.delete_user(user)
+ def test_create_duplicate_key_pair(self):
+ """Test that, after successfully generating a keypair,
+ requesting a second keypair with the same name fails sanely"""
+ self.expect_http()
+ self.mox.ReplayAll()
+ keyname = "".join(random.choice("sdiuisudfsdcnpaqwertasd") \
+ for x in range(random.randint(4, 8)))
+ user = self.manager.create_user('fake', 'fake', 'fake')
+ project = self.manager.create_project('fake', 'fake', 'fake')
+ # NOTE(vish): create depends on pool, so call helper directly
+ self.ec2.create_key_pair('test')
+
+ try:
+ self.ec2.create_key_pair('test')
+ except EC2ResponseError, e:
+ if e.code == 'KeyPairExists':
+ pass
+ else:
+ self.fail("Unexpected EC2ResponseError: %s "
+ "(expected KeyPairExists)" % e.code)
+ else:
+ self.fail('Exception not raised.')
+
def test_get_all_security_groups(self):
"""Test that we can retrieve security groups"""
self.expect_http()
diff --git a/nova/tests/test_cloud.py b/nova/tests/test_cloud.py
index c8559615a..55ea6be02 100644
--- a/nova/tests/test_cloud.py
+++ b/nova/tests/test_cloud.py
@@ -17,13 +17,9 @@
# under the License.
from base64 import b64decode
-import json
from M2Crypto import BIO
from M2Crypto import RSA
import os
-import shutil
-import tempfile
-import time
from eventlet import greenthread
@@ -33,12 +29,10 @@ from nova import db
from nova import flags
from nova import log as logging
from nova import rpc
-from nova import service
from nova import test
from nova import utils
from nova import exception
from nova.auth import manager
-from nova.compute import power_state
from nova.api.ec2 import cloud
from nova.api.ec2 import ec2utils
from nova.image import local
@@ -79,14 +73,21 @@ class CloudTestCase(test.TestCase):
self.stubs.Set(local.LocalImageService, 'show', fake_show)
self.stubs.Set(local.LocalImageService, 'show_by_name', fake_show)
+ # NOTE(vish): set up a manual wait so rpc.cast has a chance to finish
+ rpc_cast = rpc.cast
+
+ def finish_cast(*args, **kwargs):
+ rpc_cast(*args, **kwargs)
+ greenthread.sleep(0.2)
+
+ self.stubs.Set(rpc, 'cast', finish_cast)
+
def tearDown(self):
network_ref = db.project_get_network(self.context,
self.project.id)
db.network_disassociate(self.context, network_ref['id'])
self.manager.delete_project(self.project)
self.manager.delete_user(self.user)
- self.compute.kill()
- self.network.kill()
super(CloudTestCase, self).tearDown()
def _create_key(self, name):
@@ -113,7 +114,6 @@ class CloudTestCase(test.TestCase):
self.cloud.describe_addresses(self.context)
self.cloud.release_address(self.context,
public_ip=address)
- greenthread.sleep(0.3)
db.floating_ip_destroy(self.context, address)
def test_associate_disassociate_address(self):
@@ -129,12 +129,10 @@ class CloudTestCase(test.TestCase):
self.cloud.associate_address(self.context,
instance_id=ec2_id,
public_ip=address)
- greenthread.sleep(0.3)
self.cloud.disassociate_address(self.context,
public_ip=address)
self.cloud.release_address(self.context,
public_ip=address)
- greenthread.sleep(0.3)
self.network.deallocate_fixed_ip(self.context, fixed)
db.instance_destroy(self.context, inst['id'])
db.floating_ip_destroy(self.context, address)
@@ -171,6 +169,25 @@ class CloudTestCase(test.TestCase):
db.volume_destroy(self.context, vol1['id'])
db.volume_destroy(self.context, vol2['id'])
+ def test_create_volume_from_snapshot(self):
+ """Makes sure create_volume works when we specify a snapshot."""
+ vol = db.volume_create(self.context, {'size': 1})
+ snap = db.snapshot_create(self.context, {'volume_id': vol['id'],
+ 'volume_size': vol['size'],
+ 'status': "available"})
+ snapshot_id = ec2utils.id_to_ec2_id(snap['id'], 'snap-%08x')
+
+ result = self.cloud.create_volume(self.context,
+ snapshot_id=snapshot_id)
+ volume_id = result['volumeId']
+ result = self.cloud.describe_volumes(self.context)
+ self.assertEqual(len(result['volumeSet']), 2)
+ self.assertEqual(result['volumeSet'][1]['volumeId'], volume_id)
+
+ db.volume_destroy(self.context, ec2utils.ec2_id_to_id(volume_id))
+ db.snapshot_destroy(self.context, snap['id'])
+ db.volume_destroy(self.context, vol['id'])
+
def test_describe_availability_zones(self):
"""Makes sure describe_availability_zones works and filters results."""
service1 = db.service_create(self.context, {'host': 'host1_zones',
@@ -188,6 +205,52 @@ class CloudTestCase(test.TestCase):
db.service_destroy(self.context, service1['id'])
db.service_destroy(self.context, service2['id'])
+ def test_describe_snapshots(self):
+ """Makes sure describe_snapshots works and filters results."""
+ vol = db.volume_create(self.context, {})
+ snap1 = db.snapshot_create(self.context, {'volume_id': vol['id']})
+ snap2 = db.snapshot_create(self.context, {'volume_id': vol['id']})
+ result = self.cloud.describe_snapshots(self.context)
+ self.assertEqual(len(result['snapshotSet']), 2)
+ snapshot_id = ec2utils.id_to_ec2_id(snap2['id'], 'snap-%08x')
+ result = self.cloud.describe_snapshots(self.context,
+ snapshot_id=[snapshot_id])
+ self.assertEqual(len(result['snapshotSet']), 1)
+ self.assertEqual(
+ ec2utils.ec2_id_to_id(result['snapshotSet'][0]['snapshotId']),
+ snap2['id'])
+ db.snapshot_destroy(self.context, snap1['id'])
+ db.snapshot_destroy(self.context, snap2['id'])
+ db.volume_destroy(self.context, vol['id'])
+
+ def test_create_snapshot(self):
+ """Makes sure create_snapshot works."""
+ vol = db.volume_create(self.context, {'status': "available"})
+ volume_id = ec2utils.id_to_ec2_id(vol['id'], 'vol-%08x')
+
+ result = self.cloud.create_snapshot(self.context,
+ volume_id=volume_id)
+ snapshot_id = result['snapshotId']
+ result = self.cloud.describe_snapshots(self.context)
+ self.assertEqual(len(result['snapshotSet']), 1)
+ self.assertEqual(result['snapshotSet'][0]['snapshotId'], snapshot_id)
+
+ db.snapshot_destroy(self.context, ec2utils.ec2_id_to_id(snapshot_id))
+ db.volume_destroy(self.context, vol['id'])
+
+ def test_delete_snapshot(self):
+ """Makes sure delete_snapshot works."""
+ vol = db.volume_create(self.context, {'status': "available"})
+ snap = db.snapshot_create(self.context, {'volume_id': vol['id'],
+ 'status': "available"})
+ snapshot_id = ec2utils.id_to_ec2_id(snap['id'], 'snap-%08x')
+
+ result = self.cloud.delete_snapshot(self.context,
+ snapshot_id=snapshot_id)
+ self.assertTrue(result)
+
+ db.volume_destroy(self.context, vol['id'])
+
def test_describe_instances(self):
"""Makes sure describe_instances works and filters results."""
inst1 = db.instance_create(self.context, {'reservation_id': 'a',
@@ -306,31 +369,25 @@ class CloudTestCase(test.TestCase):
'instance_type': instance_type,
'max_count': max_count}
rv = self.cloud.run_instances(self.context, **kwargs)
- greenthread.sleep(0.3)
instance_id = rv['instancesSet'][0]['instanceId']
output = self.cloud.get_console_output(context=self.context,
instance_id=[instance_id])
self.assertEquals(b64decode(output['output']), 'FAKE CONSOLE?OUTPUT')
# TODO(soren): We need this until we can stop polling in the rpc code
# for unit tests.
- greenthread.sleep(0.3)
rv = self.cloud.terminate_instances(self.context, [instance_id])
- greenthread.sleep(0.3)
def test_ajax_console(self):
kwargs = {'image_id': 'ami-1'}
rv = self.cloud.run_instances(self.context, **kwargs)
instance_id = rv['instancesSet'][0]['instanceId']
- greenthread.sleep(0.3)
output = self.cloud.get_ajax_console(context=self.context,
instance_id=[instance_id])
self.assertEquals(output['url'],
'%s/?token=FAKETOKEN' % FLAGS.ajax_console_proxy_url)
# TODO(soren): We need this until we can stop polling in the rpc code
# for unit tests.
- greenthread.sleep(0.3)
rv = self.cloud.terminate_instances(self.context, [instance_id])
- greenthread.sleep(0.3)
def test_key_generation(self):
result = self._create_key('test')
@@ -354,6 +411,36 @@ class CloudTestCase(test.TestCase):
self.assertTrue(filter(lambda k: k['keyName'] == 'test1', keys))
self.assertTrue(filter(lambda k: k['keyName'] == 'test2', keys))
+ def test_import_public_key(self):
+ # test when user provides all values
+ result1 = self.cloud.import_public_key(self.context,
+ 'testimportkey1',
+ 'mytestpubkey',
+ 'mytestfprint')
+ self.assertTrue(result1)
+ keydata = db.key_pair_get(self.context,
+ self.context.user.id,
+ 'testimportkey1')
+ self.assertEqual('mytestpubkey', keydata['public_key'])
+ self.assertEqual('mytestfprint', keydata['fingerprint'])
+ # test when user omits fingerprint
+ pubkey_path = os.path.join(os.path.dirname(__file__), 'public_key')
+ f = open(pubkey_path + '/dummy.pub', 'r')
+ dummypub = f.readline().rstrip()
+ f.close
+ f = open(pubkey_path + '/dummy.fingerprint', 'r')
+ dummyfprint = f.readline().rstrip()
+ f.close
+ result2 = self.cloud.import_public_key(self.context,
+ 'testimportkey2',
+ dummypub)
+ self.assertTrue(result2)
+ keydata = db.key_pair_get(self.context,
+ self.context.user.id,
+ 'testimportkey2')
+ self.assertEqual(dummypub, keydata['public_key'])
+ self.assertEqual(dummyfprint, keydata['fingerprint'])
+
def test_delete_key_pair(self):
self._create_key('test')
self.cloud.delete_key_pair(self.context, 'test')
diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py
index 55e7ae0c4..9170837b6 100644
--- a/nova/tests/test_compute.py
+++ b/nova/tests/test_compute.py
@@ -334,6 +334,28 @@ class ComputeTestCase(test.TestCase):
self.compute.terminate_instance(self.context, instance_id)
+ def test_finish_resize(self):
+ """Contrived test to ensure finish_resize doesn't raise anything"""
+
+ def fake(*args, **kwargs):
+ pass
+
+ self.stubs.Set(self.compute.driver, 'finish_resize', fake)
+ context = self.context.elevated()
+ instance_id = self._create_instance()
+ self.compute.prep_resize(context, instance_id, 1)
+ migration_ref = db.migration_get_by_instance_and_status(context,
+ instance_id, 'pre-migrating')
+ try:
+ self.compute.finish_resize(context, instance_id,
+ int(migration_ref['id']), {})
+ except KeyError, e:
+ # Only catch key errors. We want other reasons for the test to
+ # fail to actually error out so we don't obscure anything
+ self.fail()
+
+ self.compute.terminate_instance(self.context, instance_id)
+
def test_resize_instance(self):
"""Ensure instance can be migrated/resized"""
instance_id = self._create_instance()
diff --git a/nova/tests/test_flags.py b/nova/tests/test_flags.py
index 707300fcf..05319d91f 100644
--- a/nova/tests/test_flags.py
+++ b/nova/tests/test_flags.py
@@ -91,6 +91,20 @@ class FlagsTestCase(test.TestCase):
self.assert_('runtime_answer' in self.global_FLAGS)
self.assertEqual(self.global_FLAGS.runtime_answer, 60)
+ def test_long_vs_short_flags(self):
+ flags.DEFINE_string('duplicate_answer_long', 'val', 'desc',
+ flag_values=self.global_FLAGS)
+ argv = ['flags_test', '--duplicate_answer=60', 'extra_arg']
+ args = self.global_FLAGS(argv)
+
+ self.assert_('duplicate_answer' not in self.global_FLAGS)
+ self.assert_(self.global_FLAGS.duplicate_answer_long, 60)
+
+ flags.DEFINE_integer('duplicate_answer', 60, 'desc',
+ flag_values=self.global_FLAGS)
+ self.assertEqual(self.global_FLAGS.duplicate_answer, 60)
+ self.assertEqual(self.global_FLAGS.duplicate_answer_long, 'val')
+
def test_flag_leak_left(self):
self.assertEqual(FLAGS.flags_unittest, 'foo')
FLAGS.flags_unittest = 'bar'
diff --git a/nova/tests/test_virt.py b/nova/tests/test_libvirt.py
index d743f94f7..4efdd6ae9 100644
--- a/nova/tests/test_virt.py
+++ b/nova/tests/test_libvirt.py
@@ -32,7 +32,8 @@ from nova import utils
from nova.api.ec2 import cloud
from nova.auth import manager
from nova.compute import power_state
-from nova.virt import libvirt_conn
+from nova.virt.libvirt import connection
+from nova.virt.libvirt import firewall
libvirt = None
FLAGS = flags.FLAGS
@@ -83,7 +84,7 @@ class CacheConcurrencyTestCase(test.TestCase):
def test_same_fname_concurrency(self):
"""Ensures that the same fname cache runs at a sequentially"""
- conn = libvirt_conn.LibvirtConnection
+ conn = connection.LibvirtConnection
wait1 = eventlet.event.Event()
done1 = eventlet.event.Event()
eventlet.spawn(conn._cache_image, _concurrency,
@@ -104,7 +105,7 @@ class CacheConcurrencyTestCase(test.TestCase):
def test_different_fname_concurrency(self):
"""Ensures that two different fname caches are concurrent"""
- conn = libvirt_conn.LibvirtConnection
+ conn = connection.LibvirtConnection
wait1 = eventlet.event.Event()
done1 = eventlet.event.Event()
eventlet.spawn(conn._cache_image, _concurrency,
@@ -125,7 +126,7 @@ class CacheConcurrencyTestCase(test.TestCase):
class LibvirtConnTestCase(test.TestCase):
def setUp(self):
super(LibvirtConnTestCase, self).setUp()
- libvirt_conn._late_load_cheetah()
+ connection._late_load_cheetah()
self.flags(fake_call=True)
self.manager = manager.AuthManager()
@@ -171,8 +172,8 @@ class LibvirtConnTestCase(test.TestCase):
return False
global libvirt
libvirt = __import__('libvirt')
- libvirt_conn.libvirt = __import__('libvirt')
- libvirt_conn.libxml2 = __import__('libxml2')
+ connection.libvirt = __import__('libvirt')
+ connection.libxml2 = __import__('libxml2')
return True
def create_fake_libvirt_mock(self, **kwargs):
@@ -182,7 +183,7 @@ class LibvirtConnTestCase(test.TestCase):
class FakeLibvirtConnection(object):
pass
- # A fake libvirt_conn.IptablesFirewallDriver
+ # A fake connection.IptablesFirewallDriver
class FakeIptablesFirewallDriver(object):
def __init__(self, **kwargs):
@@ -198,11 +199,11 @@ class LibvirtConnTestCase(test.TestCase):
for key, val in kwargs.items():
fake.__setattr__(key, val)
- # Inevitable mocks for libvirt_conn.LibvirtConnection
- self.mox.StubOutWithMock(libvirt_conn.utils, 'import_class')
- libvirt_conn.utils.import_class(mox.IgnoreArg()).AndReturn(fakeip)
- self.mox.StubOutWithMock(libvirt_conn.LibvirtConnection, '_conn')
- libvirt_conn.LibvirtConnection._conn = fake
+ # Inevitable mocks for connection.LibvirtConnection
+ self.mox.StubOutWithMock(connection.utils, 'import_class')
+ connection.utils.import_class(mox.IgnoreArg()).AndReturn(fakeip)
+ self.mox.StubOutWithMock(connection.LibvirtConnection, '_conn')
+ connection.LibvirtConnection._conn = fake
def create_service(self, **kwargs):
service_ref = {'host': kwargs.get('host', 'dummy'),
@@ -214,7 +215,7 @@ class LibvirtConnTestCase(test.TestCase):
return db.service_create(context.get_admin_context(), service_ref)
def test_preparing_xml_info(self):
- conn = libvirt_conn.LibvirtConnection(True)
+ conn = connection.LibvirtConnection(True)
instance_ref = db.instance_create(self.context, self.test_instance)
result = conn._prepare_xml_info(instance_ref, False)
@@ -229,7 +230,7 @@ class LibvirtConnTestCase(test.TestCase):
self.assertTrue(len(result['nics']) == 2)
def test_get_nic_for_xml_v4(self):
- conn = libvirt_conn.LibvirtConnection(True)
+ conn = connection.LibvirtConnection(True)
network, mapping = _create_network_info()[0]
self.flags(use_ipv6=False)
params = conn._get_nic_for_xml(network, mapping)['extra_params']
@@ -237,7 +238,7 @@ class LibvirtConnTestCase(test.TestCase):
self.assertTrue(params.find('PROJMASKV6') == -1)
def test_get_nic_for_xml_v6(self):
- conn = libvirt_conn.LibvirtConnection(True)
+ conn = connection.LibvirtConnection(True)
network, mapping = _create_network_info()[0]
self.flags(use_ipv6=True)
params = conn._get_nic_for_xml(network, mapping)['extra_params']
@@ -282,7 +283,7 @@ class LibvirtConnTestCase(test.TestCase):
def test_multi_nic(self):
instance_data = dict(self.test_instance)
network_info = _create_network_info(2)
- conn = libvirt_conn.LibvirtConnection(True)
+ conn = connection.LibvirtConnection(True)
instance_ref = db.instance_create(self.context, instance_data)
xml = conn.to_xml(instance_ref, False, network_info)
tree = xml_to_tree(xml)
@@ -313,7 +314,7 @@ class LibvirtConnTestCase(test.TestCase):
'instance_id': instance_ref['id']})
self.flags(libvirt_type='lxc')
- conn = libvirt_conn.LibvirtConnection(True)
+ conn = connection.LibvirtConnection(True)
uri = conn.get_uri()
self.assertEquals(uri, 'lxc:///')
@@ -419,7 +420,7 @@ class LibvirtConnTestCase(test.TestCase):
for (libvirt_type, (expected_uri, checks)) in type_uri_map.iteritems():
FLAGS.libvirt_type = libvirt_type
- conn = libvirt_conn.LibvirtConnection(True)
+ conn = connection.LibvirtConnection(True)
uri = conn.get_uri()
self.assertEquals(uri, expected_uri)
@@ -446,7 +447,7 @@ class LibvirtConnTestCase(test.TestCase):
FLAGS.libvirt_uri = testuri
for (libvirt_type, (expected_uri, checks)) in type_uri_map.iteritems():
FLAGS.libvirt_type = libvirt_type
- conn = libvirt_conn.LibvirtConnection(True)
+ conn = connection.LibvirtConnection(True)
uri = conn.get_uri()
self.assertEquals(uri, testuri)
db.instance_destroy(user_context, instance_ref['id'])
@@ -470,13 +471,13 @@ class LibvirtConnTestCase(test.TestCase):
self.create_fake_libvirt_mock(getVersion=getVersion,
getType=getType,
listDomainsID=listDomainsID)
- self.mox.StubOutWithMock(libvirt_conn.LibvirtConnection,
+ self.mox.StubOutWithMock(connection.LibvirtConnection,
'get_cpu_info')
- libvirt_conn.LibvirtConnection.get_cpu_info().AndReturn('cpuinfo')
+ connection.LibvirtConnection.get_cpu_info().AndReturn('cpuinfo')
# Start test
self.mox.ReplayAll()
- conn = libvirt_conn.LibvirtConnection(False)
+ conn = connection.LibvirtConnection(False)
conn.update_available_resource(self.context, 'dummy')
service_ref = db.service_get(self.context, service_ref['id'])
compute_node = service_ref['compute_node'][0]
@@ -510,7 +511,7 @@ class LibvirtConnTestCase(test.TestCase):
self.create_fake_libvirt_mock()
self.mox.ReplayAll()
- conn = libvirt_conn.LibvirtConnection(False)
+ conn = connection.LibvirtConnection(False)
self.assertRaises(exception.ComputeServiceUnavailable,
conn.update_available_resource,
self.context, 'dummy')
@@ -545,7 +546,7 @@ class LibvirtConnTestCase(test.TestCase):
# Start test
self.mox.ReplayAll()
try:
- conn = libvirt_conn.LibvirtConnection(False)
+ conn = connection.LibvirtConnection(False)
conn.firewall_driver.setattr('setup_basic_filtering', fake_none)
conn.firewall_driver.setattr('prepare_instance_filter', fake_none)
conn.firewall_driver.setattr('instance_filter_exists', fake_none)
@@ -594,7 +595,7 @@ class LibvirtConnTestCase(test.TestCase):
# Start test
self.mox.ReplayAll()
- conn = libvirt_conn.LibvirtConnection(False)
+ conn = connection.LibvirtConnection(False)
self.assertRaises(libvirt.libvirtError,
conn._live_migration,
self.context, instance_ref, 'dest', '',
@@ -623,7 +624,7 @@ class LibvirtConnTestCase(test.TestCase):
# Start test
self.mox.ReplayAll()
- conn = libvirt_conn.LibvirtConnection(False)
+ conn = connection.LibvirtConnection(False)
conn.firewall_driver.setattr('setup_basic_filtering', fake_none)
conn.firewall_driver.setattr('prepare_instance_filter', fake_none)
@@ -647,7 +648,7 @@ class LibvirtConnTestCase(test.TestCase):
self.assertTrue(count)
def test_get_host_ip_addr(self):
- conn = libvirt_conn.LibvirtConnection(False)
+ conn = connection.LibvirtConnection(False)
ip = conn.get_host_ip_addr()
self.assertEquals(ip, FLAGS.my_ip)
@@ -671,7 +672,7 @@ class IptablesFirewallTestCase(test.TestCase):
class FakeLibvirtConnection(object):
pass
self.fake_libvirt_connection = FakeLibvirtConnection()
- self.fw = libvirt_conn.IptablesFirewallDriver(
+ self.fw = firewall.IptablesFirewallDriver(
get_connection=lambda: self.fake_libvirt_connection)
def tearDown(self):
@@ -849,7 +850,7 @@ class IptablesFirewallTestCase(test.TestCase):
self.assertEquals(len(rulesv4), 2)
self.assertEquals(len(rulesv6), 0)
- def multinic_iptables_test(self):
+ def test_multinic_iptables(self):
ipv4_rules_per_network = 2
ipv6_rules_per_network = 3
networks_count = 5
@@ -869,6 +870,16 @@ class IptablesFirewallTestCase(test.TestCase):
self.assertEquals(ipv6_network_rules,
ipv6_rules_per_network * networks_count)
+ def test_do_refresh_security_group_rules(self):
+ instance_ref = self._create_instance_ref()
+ self.mox.StubOutWithMock(self.fw,
+ 'add_filters_for_instance',
+ use_mock_anything=True)
+ self.fw.add_filters_for_instance(instance_ref, mox.IgnoreArg())
+ self.fw.instances[instance_ref['id']] = instance_ref
+ self.mox.ReplayAll()
+ self.fw.do_refresh_security_group_rules("fake")
+
class NWFilterTestCase(test.TestCase):
def setUp(self):
@@ -885,7 +896,7 @@ class NWFilterTestCase(test.TestCase):
self.fake_libvirt_connection = Mock()
- self.fw = libvirt_conn.NWFilterFirewall(
+ self.fw = firewall.NWFilterFirewall(
lambda: self.fake_libvirt_connection)
def tearDown(self):
diff --git a/nova/tests/test_notifier.py b/nova/tests/test_notifier.py
new file mode 100644
index 000000000..b6b0fcc68
--- /dev/null
+++ b/nova/tests/test_notifier.py
@@ -0,0 +1,117 @@
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import nova
+
+from nova import context
+from nova import flags
+from nova import rpc
+import nova.notifier.api
+from nova.notifier.api import notify
+from nova.notifier import no_op_notifier
+from nova.notifier import rabbit_notifier
+from nova import test
+
+import stubout
+
+
+class NotifierTestCase(test.TestCase):
+ """Test case for notifications"""
+ def setUp(self):
+ super(NotifierTestCase, self).setUp()
+ self.stubs = stubout.StubOutForTesting()
+
+ def tearDown(self):
+ self.stubs.UnsetAll()
+ super(NotifierTestCase, self).tearDown()
+
+ def test_send_notification(self):
+ self.notify_called = False
+
+ def mock_notify(cls, *args):
+ self.notify_called = True
+
+ self.stubs.Set(nova.notifier.no_op_notifier, 'notify',
+ mock_notify)
+
+ class Mock(object):
+ pass
+ notify('publisher_id', 'event_type',
+ nova.notifier.api.WARN, dict(a=3))
+ self.assertEqual(self.notify_called, True)
+
+ def test_verify_message_format(self):
+ """A test to ensure changing the message format is prohibitively
+ annoying"""
+
+ def message_assert(message):
+ fields = [('publisher_id', 'publisher_id'),
+ ('event_type', 'event_type'),
+ ('priority', 'WARN'),
+ ('payload', dict(a=3))]
+ for k, v in fields:
+ self.assertEqual(message[k], v)
+ self.assertTrue(len(message['message_id']) > 0)
+ self.assertTrue(len(message['timestamp']) > 0)
+
+ self.stubs.Set(nova.notifier.no_op_notifier, 'notify',
+ message_assert)
+ notify('publisher_id', 'event_type',
+ nova.notifier.api.WARN, dict(a=3))
+
+ def test_send_rabbit_notification(self):
+ self.stubs.Set(nova.flags.FLAGS, 'notification_driver',
+ 'nova.notifier.rabbit_notifier')
+ self.mock_cast = False
+
+ def mock_cast(cls, *args):
+ self.mock_cast = True
+
+ class Mock(object):
+ pass
+
+ self.stubs.Set(nova.rpc, 'cast', mock_cast)
+ notify('publisher_id', 'event_type',
+ nova.notifier.api.WARN, dict(a=3))
+
+ self.assertEqual(self.mock_cast, True)
+
+ def test_invalid_priority(self):
+ def mock_cast(cls, *args):
+ pass
+
+ class Mock(object):
+ pass
+
+ self.stubs.Set(nova.rpc, 'cast', mock_cast)
+ self.assertRaises(nova.notifier.api.BadPriorityException,
+ notify, 'publisher_id',
+ 'event_type', 'not a priority', dict(a=3))
+
+ def test_rabbit_priority_queue(self):
+ self.stubs.Set(nova.flags.FLAGS, 'notification_driver',
+ 'nova.notifier.rabbit_notifier')
+ self.stubs.Set(nova.flags.FLAGS, 'notification_topic',
+ 'testnotify')
+
+ self.test_topic = None
+
+ def mock_cast(context, topic, msg):
+ self.test_topic = topic
+
+ self.stubs.Set(nova.rpc, 'cast', mock_cast)
+ notify('publisher_id',
+ 'event_type', 'DEBUG', dict(a=3))
+ self.assertEqual(self.test_topic, 'testnotify.debug')
diff --git a/nova/tests/test_quota.py b/nova/tests/test_quota.py
index 7ace2ad7d..ad73a3a69 100644
--- a/nova/tests/test_quota.py
+++ b/nova/tests/test_quota.py
@@ -104,6 +104,10 @@ class QuotaTestCase(test.TestCase):
num_instances = quota.allowed_instances(self.context, 100,
self._get_instance_type('m1.small'))
self.assertEqual(num_instances, 10)
+ db.quota_create(self.context, self.project.id, 'ram', 3 * 2048)
+ num_instances = quota.allowed_instances(self.context, 100,
+ self._get_instance_type('m1.small'))
+ self.assertEqual(num_instances, 3)
# metadata_items
too_many_items = FLAGS.quota_metadata_items + 1000
@@ -120,7 +124,8 @@ class QuotaTestCase(test.TestCase):
def test_unlimited_instances(self):
FLAGS.quota_instances = 2
- FLAGS.quota_cores = 1000
+ FLAGS.quota_ram = -1
+ FLAGS.quota_cores = -1
instance_type = self._get_instance_type('m1.small')
num_instances = quota.allowed_instances(self.context, 100,
instance_type)
@@ -133,8 +138,25 @@ class QuotaTestCase(test.TestCase):
instance_type)
self.assertEqual(num_instances, 101)
+ def test_unlimited_ram(self):
+ FLAGS.quota_instances = -1
+ FLAGS.quota_ram = 2 * 2048
+ FLAGS.quota_cores = -1
+ instance_type = self._get_instance_type('m1.small')
+ num_instances = quota.allowed_instances(self.context, 100,
+ instance_type)
+ self.assertEqual(num_instances, 2)
+ db.quota_create(self.context, self.project.id, 'ram', None)
+ num_instances = quota.allowed_instances(self.context, 100,
+ instance_type)
+ self.assertEqual(num_instances, 100)
+ num_instances = quota.allowed_instances(self.context, 101,
+ instance_type)
+ self.assertEqual(num_instances, 101)
+
def test_unlimited_cores(self):
- FLAGS.quota_instances = 1000
+ FLAGS.quota_instances = -1
+ FLAGS.quota_ram = -1
FLAGS.quota_cores = 2
instance_type = self._get_instance_type('m1.small')
num_instances = quota.allowed_instances(self.context, 100,
@@ -150,7 +172,7 @@ class QuotaTestCase(test.TestCase):
def test_unlimited_volumes(self):
FLAGS.quota_volumes = 10
- FLAGS.quota_gigabytes = 1000
+ FLAGS.quota_gigabytes = -1
volumes = quota.allowed_volumes(self.context, 100, 1)
self.assertEqual(volumes, 10)
db.quota_create(self.context, self.project.id, 'volumes', None)
@@ -160,7 +182,7 @@ class QuotaTestCase(test.TestCase):
self.assertEqual(volumes, 101)
def test_unlimited_gigabytes(self):
- FLAGS.quota_volumes = 1000
+ FLAGS.quota_volumes = -1
FLAGS.quota_gigabytes = 10
volumes = quota.allowed_volumes(self.context, 100, 1)
self.assertEqual(volumes, 10)
@@ -228,6 +250,7 @@ class QuotaTestCase(test.TestCase):
volume.API().create,
self.context,
size=10,
+ snapshot_id=None,
name='',
description='')
for volume_id in volume_ids:
@@ -241,6 +264,7 @@ class QuotaTestCase(test.TestCase):
volume.API().create,
self.context,
size=10,
+ snapshot_id=None,
name='',
description='')
for volume_id in volume_ids:
@@ -274,10 +298,47 @@ class QuotaTestCase(test.TestCase):
image_id='fake',
metadata=metadata)
- def test_allowed_injected_files(self):
- self.assertEqual(
- quota.allowed_injected_files(self.context),
- FLAGS.quota_max_injected_files)
+ def test_default_allowed_injected_files(self):
+ FLAGS.quota_max_injected_files = 55
+ self.assertEqual(quota.allowed_injected_files(self.context, 100), 55)
+
+ def test_overridden_allowed_injected_files(self):
+ FLAGS.quota_max_injected_files = 5
+ db.quota_create(self.context, self.project.id, 'injected_files', 77)
+ self.assertEqual(quota.allowed_injected_files(self.context, 100), 77)
+
+ def test_unlimited_default_allowed_injected_files(self):
+ FLAGS.quota_max_injected_files = -1
+ self.assertEqual(quota.allowed_injected_files(self.context, 100), 100)
+
+ def test_unlimited_db_allowed_injected_files(self):
+ FLAGS.quota_max_injected_files = 5
+ db.quota_create(self.context, self.project.id, 'injected_files', None)
+ self.assertEqual(quota.allowed_injected_files(self.context, 100), 100)
+
+ def test_default_allowed_injected_file_content_bytes(self):
+ FLAGS.quota_max_injected_file_content_bytes = 12345
+ limit = quota.allowed_injected_file_content_bytes(self.context, 23456)
+ self.assertEqual(limit, 12345)
+
+ def test_overridden_allowed_injected_file_content_bytes(self):
+ FLAGS.quota_max_injected_file_content_bytes = 12345
+ db.quota_create(self.context, self.project.id,
+ 'injected_file_content_bytes', 5678)
+ limit = quota.allowed_injected_file_content_bytes(self.context, 23456)
+ self.assertEqual(limit, 5678)
+
+ def test_unlimited_default_allowed_injected_file_content_bytes(self):
+ FLAGS.quota_max_injected_file_content_bytes = -1
+ limit = quota.allowed_injected_file_content_bytes(self.context, 23456)
+ self.assertEqual(limit, 23456)
+
+ def test_unlimited_db_allowed_injected_file_content_bytes(self):
+ FLAGS.quota_max_injected_file_content_bytes = 12345
+ db.quota_create(self.context, self.project.id,
+ 'injected_file_content_bytes', None)
+ limit = quota.allowed_injected_file_content_bytes(self.context, 23456)
+ self.assertEqual(limit, 23456)
def _create_with_injected_files(self, files):
api = compute.API(image_service=self.StubImageService())
@@ -304,11 +365,6 @@ class QuotaTestCase(test.TestCase):
self.assertRaises(quota.QuotaError,
self._create_with_injected_files, files)
- def test_allowed_injected_file_content_bytes(self):
- self.assertEqual(
- quota.allowed_injected_file_content_bytes(self.context),
- FLAGS.quota_max_injected_file_content_bytes)
-
def test_max_injected_file_content_bytes(self):
max = FLAGS.quota_max_injected_file_content_bytes
content = ''.join(['a' for i in xrange(max)])
diff --git a/nova/tests/test_rpc.py b/nova/tests/test_rpc.py
index 44d7c91eb..ffd748efe 100644
--- a/nova/tests/test_rpc.py
+++ b/nova/tests/test_rpc.py
@@ -31,7 +31,6 @@ LOG = logging.getLogger('nova.tests.rpc')
class RpcTestCase(test.TestCase):
- """Test cases for rpc"""
def setUp(self):
super(RpcTestCase, self).setUp()
self.conn = rpc.Connection.instance(True)
@@ -43,14 +42,55 @@ class RpcTestCase(test.TestCase):
self.context = context.get_admin_context()
def test_call_succeed(self):
- """Get a value through rpc call"""
value = 42
result = rpc.call(self.context, 'test', {"method": "echo",
"args": {"value": value}})
self.assertEqual(value, result)
+ def test_call_succeed_despite_multiple_returns(self):
+ value = 42
+ result = rpc.call(self.context, 'test', {"method": "echo_three_times",
+ "args": {"value": value}})
+ self.assertEqual(value + 2, result)
+
+ def test_call_succeed_despite_multiple_returns_yield(self):
+ value = 42
+ result = rpc.call(self.context, 'test',
+ {"method": "echo_three_times_yield",
+ "args": {"value": value}})
+ self.assertEqual(value + 2, result)
+
+ def test_multicall_succeed_once(self):
+ value = 42
+ result = rpc.multicall(self.context,
+ 'test',
+ {"method": "echo",
+ "args": {"value": value}})
+ for i, x in enumerate(result):
+ if i > 0:
+ self.fail('should only receive one response')
+ self.assertEqual(value + i, x)
+
+ def test_multicall_succeed_three_times(self):
+ value = 42
+ result = rpc.multicall(self.context,
+ 'test',
+ {"method": "echo_three_times",
+ "args": {"value": value}})
+ for i, x in enumerate(result):
+ self.assertEqual(value + i, x)
+
+ def test_multicall_succeed_three_times_yield(self):
+ value = 42
+ result = rpc.multicall(self.context,
+ 'test',
+ {"method": "echo_three_times_yield",
+ "args": {"value": value}})
+ for i, x in enumerate(result):
+ self.assertEqual(value + i, x)
+
def test_context_passed(self):
- """Makes sure a context is passed through rpc call"""
+ """Makes sure a context is passed through rpc call."""
value = 42
result = rpc.call(self.context,
'test', {"method": "context",
@@ -58,11 +98,12 @@ class RpcTestCase(test.TestCase):
self.assertEqual(self.context.to_dict(), result)
def test_call_exception(self):
- """Test that exception gets passed back properly
+ """Test that exception gets passed back properly.
rpc.call returns a RemoteError object. The value of the
exception is converted to a string, so we convert it back
to an int in the test.
+
"""
value = 42
self.assertRaises(rpc.RemoteError,
@@ -81,7 +122,7 @@ class RpcTestCase(test.TestCase):
self.assertEqual(int(exc.value), value)
def test_nested_calls(self):
- """Test that we can do an rpc.call inside another call"""
+ """Test that we can do an rpc.call inside another call."""
class Nested(object):
@staticmethod
def echo(context, queue, value):
@@ -108,25 +149,80 @@ class RpcTestCase(test.TestCase):
"value": value}})
self.assertEqual(value, result)
+ def test_connectionpool_single(self):
+ """Test that ConnectionPool recycles a single connection."""
+ conn1 = rpc.ConnectionPool.get()
+ rpc.ConnectionPool.put(conn1)
+ conn2 = rpc.ConnectionPool.get()
+ rpc.ConnectionPool.put(conn2)
+ self.assertEqual(conn1, conn2)
+
+ def test_connectionpool_double(self):
+ """Test that ConnectionPool returns and reuses separate connections.
+
+ When called consecutively we should get separate connections and upon
+ returning them those connections should be reused for future calls
+ before generating a new connection.
+
+ """
+ conn1 = rpc.ConnectionPool.get()
+ conn2 = rpc.ConnectionPool.get()
+
+ self.assertNotEqual(conn1, conn2)
+ rpc.ConnectionPool.put(conn1)
+ rpc.ConnectionPool.put(conn2)
+
+ conn3 = rpc.ConnectionPool.get()
+ conn4 = rpc.ConnectionPool.get()
+ self.assertEqual(conn1, conn3)
+ self.assertEqual(conn2, conn4)
+
+ def test_connectionpool_limit(self):
+ """Test connection pool limit and connection uniqueness."""
+ max_size = FLAGS.rpc_conn_pool_size
+ conns = []
+
+ for i in xrange(max_size):
+ conns.append(rpc.ConnectionPool.get())
+
+ self.assertFalse(rpc.ConnectionPool.free_items)
+ self.assertEqual(rpc.ConnectionPool.current_size,
+ rpc.ConnectionPool.max_size)
+ self.assertEqual(len(set(conns)), max_size)
+
class TestReceiver(object):
- """Simple Proxy class so the consumer has methods to call
+ """Simple Proxy class so the consumer has methods to call.
+
+ Uses static methods because we aren't actually storing any state.
- Uses static methods because we aren't actually storing any state"""
+ """
@staticmethod
def echo(context, value):
- """Simply returns whatever value is sent in"""
+ """Simply returns whatever value is sent in."""
LOG.debug(_("Received %s"), value)
return value
@staticmethod
def context(context, value):
- """Returns dictionary version of context"""
+ """Returns dictionary version of context."""
LOG.debug(_("Received %s"), context)
return context.to_dict()
@staticmethod
+ def echo_three_times(context, value):
+ context.reply(value)
+ context.reply(value + 1)
+ context.reply(value + 2)
+
+ @staticmethod
+ def echo_three_times_yield(context, value):
+ yield value
+ yield value + 1
+ yield value + 2
+
+ @staticmethod
def fail(context, value):
- """Raises an exception with the value sent in"""
+ """Raises an exception with the value sent in."""
raise Exception(value)
diff --git a/nova/tests/test_service.py b/nova/tests/test_service.py
index d48de2057..d1cc8bd61 100644
--- a/nova/tests/test_service.py
+++ b/nova/tests/test_service.py
@@ -106,7 +106,10 @@ class ServiceTestCase(test.TestCase):
# NOTE(vish): Create was moved out of mox replay to make sure that
# the looping calls are created in StartService.
- app = service.Service.create(host=host, binary=binary)
+ app = service.Service.create(host=host, binary=binary, topic=topic)
+
+ self.mox.StubOutWithMock(service.rpc.Connection, 'instance')
+ service.rpc.Connection.instance(new=mox.IgnoreArg())
self.mox.StubOutWithMock(rpc,
'TopicAdapterConsumer',
@@ -114,6 +117,11 @@ class ServiceTestCase(test.TestCase):
self.mox.StubOutWithMock(rpc,
'FanoutAdapterConsumer',
use_mock_anything=True)
+
+ self.mox.StubOutWithMock(rpc,
+ 'ConsumerSet',
+ use_mock_anything=True)
+
rpc.TopicAdapterConsumer(connection=mox.IgnoreArg(),
topic=topic,
proxy=mox.IsA(service.Service)).AndReturn(
@@ -129,9 +137,14 @@ class ServiceTestCase(test.TestCase):
proxy=mox.IsA(service.Service)).AndReturn(
rpc.FanoutAdapterConsumer)
- rpc.TopicAdapterConsumer.attach_to_eventlet()
- rpc.TopicAdapterConsumer.attach_to_eventlet()
- rpc.FanoutAdapterConsumer.attach_to_eventlet()
+ def wait_func(self, limit=None):
+ return None
+
+ mock_cset = self.mox.CreateMock(rpc.ConsumerSet,
+ {'wait': wait_func})
+ rpc.ConsumerSet(connection=mox.IgnoreArg(),
+ consumer_list=mox.IsA(list)).AndReturn(mock_cset)
+ wait_func(mox.IgnoreArg())
service_create = {'host': host,
'binary': binary,
@@ -287,8 +300,42 @@ class ServiceTestCase(test.TestCase):
# Creating mocks
self.mox.StubOutWithMock(service.rpc.Connection, 'instance')
service.rpc.Connection.instance(new=mox.IgnoreArg())
- service.rpc.Connection.instance(new=mox.IgnoreArg())
- service.rpc.Connection.instance(new=mox.IgnoreArg())
+
+ self.mox.StubOutWithMock(rpc,
+ 'TopicAdapterConsumer',
+ use_mock_anything=True)
+ self.mox.StubOutWithMock(rpc,
+ 'FanoutAdapterConsumer',
+ use_mock_anything=True)
+
+ self.mox.StubOutWithMock(rpc,
+ 'ConsumerSet',
+ use_mock_anything=True)
+
+ rpc.TopicAdapterConsumer(connection=mox.IgnoreArg(),
+ topic=topic,
+ proxy=mox.IsA(service.Service)).AndReturn(
+ rpc.TopicAdapterConsumer)
+
+ rpc.TopicAdapterConsumer(connection=mox.IgnoreArg(),
+ topic='%s.%s' % (topic, host),
+ proxy=mox.IsA(service.Service)).AndReturn(
+ rpc.TopicAdapterConsumer)
+
+ rpc.FanoutAdapterConsumer(connection=mox.IgnoreArg(),
+ topic=topic,
+ proxy=mox.IsA(service.Service)).AndReturn(
+ rpc.FanoutAdapterConsumer)
+
+ def wait_func(self, limit=None):
+ return None
+
+ mock_cset = self.mox.CreateMock(rpc.ConsumerSet,
+ {'wait': wait_func})
+ rpc.ConsumerSet(connection=mox.IgnoreArg(),
+ consumer_list=mox.IsA(list)).AndReturn(mock_cset)
+ wait_func(mox.IgnoreArg())
+
self.mox.StubOutWithMock(serv.manager.driver,
'update_available_resource')
serv.manager.driver.update_available_resource(mox.IgnoreArg(), host)
diff --git a/nova/tests/test_volume.py b/nova/tests/test_volume.py
index 236d12434..4f10ee6af 100644
--- a/nova/tests/test_volume.py
+++ b/nova/tests/test_volume.py
@@ -45,10 +45,11 @@ class VolumeTestCase(test.TestCase):
self.context = context.get_admin_context()
@staticmethod
- def _create_volume(size='0'):
+ def _create_volume(size='0', snapshot_id=None):
"""Create a volume object."""
vol = {}
vol['size'] = size
+ vol['snapshot_id'] = snapshot_id
vol['user_id'] = 'fake'
vol['project_id'] = 'fake'
vol['availability_zone'] = FLAGS.storage_availability_zone
@@ -69,6 +70,25 @@ class VolumeTestCase(test.TestCase):
self.context,
volume_id)
+ def test_create_volume_from_snapshot(self):
+ """Test volume can be created from a snapshot."""
+ volume_src_id = self._create_volume()
+ self.volume.create_volume(self.context, volume_src_id)
+ snapshot_id = self._create_snapshot(volume_src_id)
+ self.volume.create_snapshot(self.context, volume_src_id, snapshot_id)
+ volume_dst_id = self._create_volume(0, snapshot_id)
+ self.volume.create_volume(self.context, volume_dst_id, snapshot_id)
+ self.assertEqual(volume_dst_id, db.volume_get(
+ context.get_admin_context(),
+ volume_dst_id).id)
+ self.assertEqual(snapshot_id, db.volume_get(
+ context.get_admin_context(),
+ volume_dst_id).snapshot_id)
+
+ self.volume.delete_volume(self.context, volume_dst_id)
+ self.volume.delete_snapshot(self.context, snapshot_id)
+ self.volume.delete_volume(self.context, volume_src_id)
+
def test_too_big_volume(self):
"""Ensure failure if a too large of a volume is requested."""
# FIXME(vish): validation needs to move into the data layer in
@@ -176,6 +196,34 @@ class VolumeTestCase(test.TestCase):
# This will allow us to test cross-node interactions
pass
+ @staticmethod
+ def _create_snapshot(volume_id, size='0'):
+ """Create a snapshot object."""
+ snap = {}
+ snap['volume_size'] = size
+ snap['user_id'] = 'fake'
+ snap['project_id'] = 'fake'
+ snap['volume_id'] = volume_id
+ snap['status'] = "creating"
+ return db.snapshot_create(context.get_admin_context(), snap)['id']
+
+ def test_create_delete_snapshot(self):
+ """Test snapshot can be created and deleted."""
+ volume_id = self._create_volume()
+ self.volume.create_volume(self.context, volume_id)
+ snapshot_id = self._create_snapshot(volume_id)
+ self.volume.create_snapshot(self.context, volume_id, snapshot_id)
+ self.assertEqual(snapshot_id,
+ db.snapshot_get(context.get_admin_context(),
+ snapshot_id).id)
+
+ self.volume.delete_snapshot(self.context, snapshot_id)
+ self.assertRaises(exception.NotFound,
+ db.snapshot_get,
+ self.context,
+ snapshot_id)
+ self.volume.delete_volume(self.context, volume_id)
+
class DriverTestCase(test.TestCase):
"""Base Test class for Drivers."""
diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py
index be1e35697..9d56c1644 100644
--- a/nova/tests/test_xenapi.py
+++ b/nova/tests/test_xenapi.py
@@ -395,6 +395,29 @@ class XenAPIVMTestCase(test.TestCase):
os_type="linux")
self.check_vm_params_for_linux()
+ def test_spawn_vhd_glance_swapdisk(self):
+ # Change the default host_call_plugin to one that'll return
+ # a swap disk
+ orig_func = stubs.FakeSessionForVMTests.host_call_plugin
+
+ stubs.FakeSessionForVMTests.host_call_plugin = \
+ stubs.FakeSessionForVMTests.host_call_plugin_swap
+
+ try:
+ # We'll steal the above glance linux test
+ self.test_spawn_vhd_glance_linux()
+ finally:
+ # Make sure to put this back
+ stubs.FakeSessionForVMTests.host_call_plugin = orig_func
+
+ # We should have 2 VBDs.
+ self.assertEqual(len(self.vm['VBDs']), 2)
+ # Now test that we have 1.
+ self.tearDown()
+ self.setUp()
+ self.test_spawn_vhd_glance_linux()
+ self.assertEqual(len(self.vm['VBDs']), 1)
+
def test_spawn_vhd_glance_windows(self):
FLAGS.xenapi_image_service = 'glance'
self._test_spawn(glance_stubs.FakeGlance.IMAGE_VHD, None, None,
@@ -569,11 +592,29 @@ class XenAPIDiffieHellmanTestCase(test.TestCase):
bob_shared = self.bob.compute_shared(alice_pub)
self.assertEquals(alice_shared, bob_shared)
- def test_encryption(self):
- msg = "This is a top-secret message"
- enc = self.alice.encrypt(msg)
+ def _test_encryption(self, message):
+ enc = self.alice.encrypt(message)
+ self.assertFalse(enc.endswith('\n'))
dec = self.bob.decrypt(enc)
- self.assertEquals(dec, msg)
+ self.assertEquals(dec, message)
+
+ def test_encrypt_simple_message(self):
+ self._test_encryption('This is a simple message.')
+
+ def test_encrypt_message_with_newlines_at_end(self):
+ self._test_encryption('This message has a newline at the end.\n')
+
+ def test_encrypt_many_newlines_at_end(self):
+ self._test_encryption('Message with lotsa newlines.\n\n\n')
+
+ def test_encrypt_newlines_inside_message(self):
+ self._test_encryption('Message\nwith\ninterior\nnewlines.')
+
+ def test_encrypt_with_leading_newlines(self):
+ self._test_encryption('\n\nMessage with leading newlines.')
+
+ def test_encrypt_really_long_message(self):
+ self._test_encryption(''.join(['abcd' for i in xrange(1024)]))
def tearDown(self):
super(XenAPIDiffieHellmanTestCase, self).tearDown()
diff --git a/nova/tests/xenapi/stubs.py b/nova/tests/xenapi/stubs.py
index 4833ccb07..35308d95f 100644
--- a/nova/tests/xenapi/stubs.py
+++ b/nova/tests/xenapi/stubs.py
@@ -17,6 +17,7 @@
"""Stubouts, mocks and fixtures for the test suite"""
import eventlet
+import json
from nova.virt import xenapi_conn
from nova.virt.xenapi import fake
from nova.virt.xenapi import volume_utils
@@ -37,7 +38,7 @@ def stubout_instance_snapshot(stubs):
sr_ref=sr_ref, sharable=False)
vdi_rec = session.get_xenapi().VDI.get_record(vdi_ref)
vdi_uuid = vdi_rec['uuid']
- return vdi_uuid
+ return [dict(vdi_type='os', vdi_uuid=vdi_uuid)]
stubs.Set(vm_utils.VMHelper, 'fetch_image', fake_fetch_image)
@@ -132,11 +133,30 @@ class FakeSessionForVMTests(fake.SessionBase):
def __init__(self, uri):
super(FakeSessionForVMTests, self).__init__(uri)
- def host_call_plugin(self, _1, _2, _3, _4, _5):
+ def host_call_plugin(self, _1, _2, plugin, method, _5):
+ sr_ref = fake.get_all('SR')[0]
+ vdi_ref = fake.create_vdi('', False, sr_ref, False)
+ vdi_rec = fake.get_record('VDI', vdi_ref)
+ if plugin == "glance" and method == "download_vhd":
+ ret_str = json.dumps([dict(vdi_type='os',
+ vdi_uuid=vdi_rec['uuid'])])
+ else:
+ ret_str = vdi_rec['uuid']
+ return '<string>%s</string>' % ret_str
+
+ def host_call_plugin_swap(self, _1, _2, plugin, method, _5):
sr_ref = fake.get_all('SR')[0]
vdi_ref = fake.create_vdi('', False, sr_ref, False)
vdi_rec = fake.get_record('VDI', vdi_ref)
- return '<string>%s</string>' % vdi_rec['uuid']
+ if plugin == "glance" and method == "download_vhd":
+ swap_vdi_ref = fake.create_vdi('', False, sr_ref, False)
+ swap_vdi_rec = fake.get_record('VDI', swap_vdi_ref)
+ ret_str = json.dumps(
+ [dict(vdi_type='os', vdi_uuid=vdi_rec['uuid']),
+ dict(vdi_type='swap', vdi_uuid=swap_vdi_rec['uuid'])])
+ else:
+ ret_str = vdi_rec['uuid']
+ return '<string>%s</string>' % ret_str
def VM_start(self, _1, ref, _2, _3):
vm = fake.get_record('VM', ref)
diff --git a/nova/virt/connection.py b/nova/virt/connection.py
index 99a8849f1..aeec17c98 100644
--- a/nova/virt/connection.py
+++ b/nova/virt/connection.py
@@ -27,9 +27,9 @@ from nova import utils
from nova.virt import driver
from nova.virt import fake
from nova.virt import hyperv
-from nova.virt import libvirt_conn
from nova.virt import vmwareapi_conn
from nova.virt import xenapi_conn
+from nova.virt.libvirt import connection as libvirt_conn
LOG = logging.getLogger("nova.virt.connection")
diff --git a/nova/virt/disk.py b/nova/virt/disk.py
index ddea1a1f7..f8aea1f34 100644
--- a/nova/virt/disk.py
+++ b/nova/virt/disk.py
@@ -81,34 +81,36 @@ def inject_data(image, key=None, net=None, partition=None, nbd=False):
else:
mapped_device = device
- # We can only loopback mount raw images. If the device isn't there,
- # it's normally because it's a .vmdk or a .vdi etc
- if not os.path.exists(mapped_device):
- raise exception.Error('Mapped device was not found (we can'
- ' only inject raw disk images): %s' %
- mapped_device)
-
- # Configure ext2fs so that it doesn't auto-check every N boots
- out, err = utils.execute('sudo', 'tune2fs',
- '-c', 0, '-i', 0, mapped_device)
-
- tmpdir = tempfile.mkdtemp()
try:
- # mount loopback to dir
- out, err = utils.execute(
- 'sudo', 'mount', mapped_device, tmpdir)
- if err:
- raise exception.Error(_('Failed to mount filesystem: %s')
- % err)
-
+ # We can only loopback mount raw images. If the device isn't there,
+ # it's normally because it's a .vmdk or a .vdi etc
+ if not os.path.exists(mapped_device):
+ raise exception.Error('Mapped device was not found (we can'
+ ' only inject raw disk images): %s' %
+ mapped_device)
+
+ # Configure ext2fs so that it doesn't auto-check every N boots
+ out, err = utils.execute('sudo', 'tune2fs',
+ '-c', 0, '-i', 0, mapped_device)
+
+ tmpdir = tempfile.mkdtemp()
try:
- inject_data_into_fs(tmpdir, key, net, utils.execute)
+ # mount loopback to dir
+ out, err = utils.execute(
+ 'sudo', 'mount', mapped_device, tmpdir)
+ if err:
+ raise exception.Error(_('Failed to mount filesystem: %s')
+ % err)
+
+ try:
+ inject_data_into_fs(tmpdir, key, net, utils.execute)
+ finally:
+ # unmount device
+ utils.execute('sudo', 'umount', mapped_device)
finally:
- # unmount device
- utils.execute('sudo', 'umount', mapped_device)
+ # remove temporary directory
+ utils.execute('rmdir', tmpdir)
finally:
- # remove temporary directory
- utils.execute('rmdir', tmpdir)
if not partition is None:
# remove partitions
utils.execute('sudo', 'kpartx', '-d', device)
diff --git a/nova/virt/images.py b/nova/virt/images.py
index 2e3f2ee4d..02c898fda 100644
--- a/nova/virt/images.py
+++ b/nova/virt/images.py
@@ -21,19 +21,10 @@
Handling of VM disk images.
"""
-import os.path
-import shutil
-import sys
-import time
-import urllib2
-import urlparse
-
from nova import context
from nova import flags
from nova import log as logging
from nova import utils
-from nova.auth import manager
-from nova.auth import signer
FLAGS = flags.FLAGS
@@ -52,66 +43,6 @@ def fetch(image_id, path, _user, _project):
return metadata
-# NOTE(vish): The methods below should be unnecessary, but I'm leaving
-# them in case the glance client does not work on windows.
-def _fetch_image_no_curl(url, path, headers):
- request = urllib2.Request(url)
- for (k, v) in headers.iteritems():
- request.add_header(k, v)
-
- def urlretrieve(urlfile, fpath):
- chunk = 1 * 1024 * 1024
- f = open(fpath, "wb")
- while 1:
- data = urlfile.read(chunk)
- if not data:
- break
- f.write(data)
-
- urlopened = urllib2.urlopen(request)
- urlretrieve(urlopened, path)
- LOG.debug(_("Finished retreving %(url)s -- placed in %(path)s") % locals())
-
-
-def _fetch_s3_image(image, path, user, project):
- url = image_url(image)
-
- # This should probably move somewhere else, like e.g. a download_as
- # method on User objects and at the same time get rewritten to use
- # a web client.
- headers = {}
- headers['Date'] = time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime())
-
- (_, _, url_path, _, _, _) = urlparse.urlparse(url)
- access = manager.AuthManager().get_access_key(user, project)
- signature = signer.Signer(user.secret.encode()).s3_authorization(headers,
- 'GET',
- url_path)
- headers['Authorization'] = 'AWS %s:%s' % (access, signature)
-
- if sys.platform.startswith('win'):
- return _fetch_image_no_curl(url, path, headers)
- else:
- cmd = ['/usr/bin/curl', '--fail', '--silent', url]
- for (k, v) in headers.iteritems():
- cmd += ['-H', '\'%s: %s\'' % (k, v)]
-
- cmd += ['-o', path]
- return utils.execute(*cmd)
-
-
-def _fetch_local_image(image, path, user, project):
- source = _image_path(os.path.join(image, 'image'))
- if sys.platform.startswith('win'):
- return shutil.copy(source, path)
- else:
- return utils.execute('cp', source, path)
-
-
-def _image_path(path):
- return os.path.join(FLAGS.images_path, path)
-
-
# TODO(vish): xenapi should use the glance client code directly instead
# of retrieving the image using this method.
def image_url(image):
diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template
index de2497a76..20986d4d5 100644
--- a/nova/virt/libvirt.xml.template
+++ b/nova/virt/libvirt.xml.template
@@ -116,7 +116,7 @@
</serial>
#if $getVar('vncserver_host', False)
- <graphics type='vnc' port='-1' autoport='yes' keymap='en-us' listen='${vncserver_host}'/>
+ <graphics type='vnc' port='-1' autoport='yes' keymap='${vnc_keymap}' listen='${vncserver_host}'/>
#end if
</devices>
</domain>
diff --git a/nova/virt/libvirt/__init__.py b/nova/virt/libvirt/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/nova/virt/libvirt/__init__.py
diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt/connection.py
index 6ee23d1df..f563d0681 100644
--- a/nova/virt/libvirt_conn.py
+++ b/nova/virt/libvirt/connection.py
@@ -57,7 +57,6 @@ from nova import context
from nova import db
from nova import exception
from nova import flags
-from nova import ipv6
from nova import log as logging
from nova import utils
from nova import vnc
@@ -67,20 +66,23 @@ from nova.compute import power_state
from nova.virt import disk
from nova.virt import driver
from nova.virt import images
+from nova.virt.libvirt import netutils
+
libvirt = None
libxml2 = None
Template = None
+
LOG = logging.getLogger('nova.virt.libvirt_conn')
+
FLAGS = flags.FLAGS
flags.DECLARE('live_migration_retry_count', 'nova.compute.manager')
# TODO(vish): These flags should probably go into a shared location
flags.DEFINE_string('rescue_image_id', 'ami-rescue', 'Rescue ami image')
flags.DEFINE_string('rescue_kernel_id', 'aki-rescue', 'Rescue aki image')
flags.DEFINE_string('rescue_ramdisk_id', 'ari-rescue', 'Rescue ari image')
-
flags.DEFINE_string('libvirt_xml_template',
utils.abspath('virt/libvirt.xml.template'),
'Libvirt XML Template')
@@ -102,7 +104,7 @@ flags.DEFINE_string('ajaxterm_portrange',
'10000-12000',
'Range of ports that ajaxterm should randomly try to bind')
flags.DEFINE_string('firewall_driver',
- 'nova.virt.libvirt_conn.IptablesFirewallDriver',
+ 'nova.virt.libvirt.firewall.IptablesFirewallDriver',
'Firewall driver (defaults to iptables)')
flags.DEFINE_string('cpuinfo_xml_template',
utils.abspath('virt/cpuinfo.xml.template'),
@@ -144,70 +146,6 @@ def _late_load_cheetah():
Template = t.Template
-def _get_net_and_mask(cidr):
- net = IPy.IP(cidr)
- return str(net.net()), str(net.netmask())
-
-
-def _get_net_and_prefixlen(cidr):
- net = IPy.IP(cidr)
- return str(net.net()), str(net.prefixlen())
-
-
-def _get_ip_version(cidr):
- net = IPy.IP(cidr)
- return int(net.version())
-
-
-def _get_network_info(instance):
- # TODO(adiantum) If we will keep this function
- # we should cache network_info
- admin_context = context.get_admin_context()
-
- ip_addresses = db.fixed_ip_get_all_by_instance(admin_context,
- instance['id'])
- networks = db.network_get_all_by_instance(admin_context,
- instance['id'])
- flavor = db.instance_type_get_by_id(admin_context,
- instance['instance_type_id'])
- network_info = []
-
- for network in networks:
- network_ips = [ip for ip in ip_addresses
- if ip['network_id'] == network['id']]
-
- def ip_dict(ip):
- return {
- 'ip': ip['address'],
- 'netmask': network['netmask'],
- 'enabled': '1'}
-
- def ip6_dict():
- prefix = network['cidr_v6']
- mac = instance['mac_address']
- project_id = instance['project_id']
- return {
- 'ip': ipv6.to_global(prefix, mac, project_id),
- 'netmask': network['netmask_v6'],
- 'enabled': '1'}
-
- mapping = {
- 'label': network['label'],
- 'gateway': network['gateway'],
- 'broadcast': network['broadcast'],
- 'mac': instance['mac_address'],
- 'rxtx_cap': flavor['rxtx_cap'],
- 'dns': [network['dns']],
- 'ips': [ip_dict(ip) for ip in network_ips]}
-
- if FLAGS.use_ipv6:
- mapping['ip6s'] = [ip6_dict()]
- mapping['gateway6'] = network['gateway_v6']
-
- network_info.append((network, mapping))
- return network_info
-
-
class LibvirtConnection(driver.ComputeDriver):
def __init__(self, read_only):
@@ -550,19 +488,27 @@ class LibvirtConnection(driver.ComputeDriver):
@exception.wrap_exception
def pause(self, instance, callback):
- raise exception.ApiError("pause not supported for libvirt.")
+ """Pause VM instance"""
+ dom = self._lookup_by_name(instance.name)
+ dom.suspend()
@exception.wrap_exception
def unpause(self, instance, callback):
- raise exception.ApiError("unpause not supported for libvirt.")
+ """Unpause paused VM instance"""
+ dom = self._lookup_by_name(instance.name)
+ dom.resume()
@exception.wrap_exception
def suspend(self, instance, callback):
- raise exception.ApiError("suspend not supported for libvirt")
+ """Suspend the specified instance"""
+ dom = self._lookup_by_name(instance.name)
+ dom.managedSave(0)
@exception.wrap_exception
def resume(self, instance, callback):
- raise exception.ApiError("resume not supported for libvirt")
+ """resume the specified instance"""
+ dom = self._lookup_by_name(instance.name)
+ dom.create()
@exception.wrap_exception
def rescue(self, instance):
@@ -807,7 +753,7 @@ class LibvirtConnection(driver.ComputeDriver):
def _create_image(self, inst, libvirt_xml, suffix='', disk_images=None,
network_info=None):
if not network_info:
- network_info = _get_network_info(inst)
+ network_info = netutils.get_network_info(inst)
if not suffix:
suffix = ''
@@ -966,10 +912,10 @@ class LibvirtConnection(driver.ComputeDriver):
if FLAGS.allow_project_net_traffic:
template = "<parameter name=\"%s\"value=\"%s\" />\n"
- net, mask = _get_net_and_mask(network['cidr'])
+ net, mask = netutils.get_net_and_mask(network['cidr'])
values = [("PROJNET", net), ("PROJMASK", mask)]
if FLAGS.use_ipv6:
- net_v6, prefixlen_v6 = _get_net_and_prefixlen(
+ net_v6, prefixlen_v6 = netutils.get_net_and_prefixlen(
network['cidr_v6'])
values.extend([("PROJNETV6", net_v6),
("PROJMASKV6", prefixlen_v6)])
@@ -996,7 +942,7 @@ class LibvirtConnection(driver.ComputeDriver):
# TODO(adiantum) remove network_info creation code
# when multinics will be completed
if not network_info:
- network_info = _get_network_info(instance)
+ network_info = netutils.get_network_info(instance)
nics = []
for (network, mapping) in network_info:
@@ -1024,6 +970,7 @@ class LibvirtConnection(driver.ComputeDriver):
if FLAGS.vnc_enabled:
if FLAGS.libvirt_type != 'lxc':
xml_info['vncserver_host'] = FLAGS.vncserver_host
+ xml_info['vnc_keymap'] = FLAGS.vnc_keymap
if not rescue:
if instance['kernel_id']:
xml_info['kernel'] = xml_info['basepath'] + "/kernel"
@@ -1591,598 +1538,3 @@ class LibvirtConnection(driver.ComputeDriver):
def get_host_stats(self, refresh=False):
"""See xenapi_conn.py implementation."""
pass
-
-
-class FirewallDriver(object):
- def prepare_instance_filter(self, instance, network_info=None):
- """Prepare filters for the instance.
-
- At this point, the instance isn't running yet."""
- raise NotImplementedError()
-
- def unfilter_instance(self, instance):
- """Stop filtering instance"""
- raise NotImplementedError()
-
- def apply_instance_filter(self, instance):
- """Apply instance filter.
-
- Once this method returns, the instance should be firewalled
- appropriately. This method should as far as possible be a
- no-op. It's vastly preferred to get everything set up in
- prepare_instance_filter.
- """
- raise NotImplementedError()
-
- def refresh_security_group_rules(self, security_group_id):
- """Refresh security group rules from data store
-
- Gets called when a rule has been added to or removed from
- the security group."""
- raise NotImplementedError()
-
- def refresh_security_group_members(self, security_group_id):
- """Refresh security group members from data store
-
- Gets called when an instance gets added to or removed from
- the security group."""
- raise NotImplementedError()
-
- def setup_basic_filtering(self, instance, network_info=None):
- """Create rules to block spoofing and allow dhcp.
-
- This gets called when spawning an instance, before
- :method:`prepare_instance_filter`.
-
- """
- raise NotImplementedError()
-
- def instance_filter_exists(self, instance):
- """Check nova-instance-instance-xxx exists"""
- raise NotImplementedError()
-
-
-class NWFilterFirewall(FirewallDriver):
- """
- This class implements a network filtering mechanism versatile
- enough for EC2 style Security Group filtering by leveraging
- libvirt's nwfilter.
-
- First, all instances get a filter ("nova-base-filter") applied.
- This filter provides some basic security such as protection against
- MAC spoofing, IP spoofing, and ARP spoofing.
-
- This filter drops all incoming ipv4 and ipv6 connections.
- Outgoing connections are never blocked.
-
- Second, every security group maps to a nwfilter filter(*).
- NWFilters can be updated at runtime and changes are applied
- immediately, so changes to security groups can be applied at
- runtime (as mandated by the spec).
-
- Security group rules are named "nova-secgroup-<id>" where <id>
- is the internal id of the security group. They're applied only on
- hosts that have instances in the security group in question.
-
- Updates to security groups are done by updating the data model
- (in response to API calls) followed by a request sent to all
- the nodes with instances in the security group to refresh the
- security group.
-
- Each instance has its own NWFilter, which references the above
- mentioned security group NWFilters. This was done because
- interfaces can only reference one filter while filters can
- reference multiple other filters. This has the added benefit of
- actually being able to add and remove security groups from an
- instance at run time. This functionality is not exposed anywhere,
- though.
-
- Outstanding questions:
-
- The name is unique, so would there be any good reason to sync
- the uuid across the nodes (by assigning it from the datamodel)?
-
-
- (*) This sentence brought to you by the redundancy department of
- redundancy.
-
- """
-
- def __init__(self, get_connection, **kwargs):
- self._libvirt_get_connection = get_connection
- self.static_filters_configured = False
- self.handle_security_groups = False
-
- def apply_instance_filter(self, instance):
- """No-op. Everything is done in prepare_instance_filter"""
- pass
-
- def _get_connection(self):
- return self._libvirt_get_connection()
- _conn = property(_get_connection)
-
- def nova_dhcp_filter(self):
- """The standard allow-dhcp-server filter is an <ip> one, so it uses
- ebtables to allow traffic through. Without a corresponding rule in
- iptables, it'll get blocked anyway."""
-
- return '''<filter name='nova-allow-dhcp-server' chain='ipv4'>
- <uuid>891e4787-e5c0-d59b-cbd6-41bc3c6b36fc</uuid>
- <rule action='accept' direction='out'
- priority='100'>
- <udp srcipaddr='0.0.0.0'
- dstipaddr='255.255.255.255'
- srcportstart='68'
- dstportstart='67'/>
- </rule>
- <rule action='accept' direction='in'
- priority='100'>
- <udp srcipaddr='$DHCPSERVER'
- srcportstart='67'
- dstportstart='68'/>
- </rule>
- </filter>'''
-
- def nova_ra_filter(self):
- return '''<filter name='nova-allow-ra-server' chain='root'>
- <uuid>d707fa71-4fb5-4b27-9ab7-ba5ca19c8804</uuid>
- <rule action='accept' direction='inout'
- priority='100'>
- <icmpv6 srcipaddr='$RASERVER'/>
- </rule>
- </filter>'''
-
- def setup_basic_filtering(self, instance, network_info=None):
- """Set up basic filtering (MAC, IP, and ARP spoofing protection)"""
- logging.info('called setup_basic_filtering in nwfilter')
-
- if not network_info:
- network_info = _get_network_info(instance)
-
- if self.handle_security_groups:
- # No point in setting up a filter set that we'll be overriding
- # anyway.
- return
-
- logging.info('ensuring static filters')
- self._ensure_static_filters()
-
- if instance['image_id'] == str(FLAGS.vpn_image_id):
- base_filter = 'nova-vpn'
- else:
- base_filter = 'nova-base'
-
- for (network, mapping) in network_info:
- nic_id = mapping['mac'].replace(':', '')
- instance_filter_name = self._instance_filter_name(instance, nic_id)
- self._define_filter(self._filter_container(instance_filter_name,
- [base_filter]))
-
- def _ensure_static_filters(self):
- if self.static_filters_configured:
- return
-
- self._define_filter(self._filter_container('nova-base',
- ['no-mac-spoofing',
- 'no-ip-spoofing',
- 'no-arp-spoofing',
- 'allow-dhcp-server']))
- self._define_filter(self._filter_container('nova-vpn',
- ['allow-dhcp-server']))
- self._define_filter(self.nova_base_ipv4_filter)
- self._define_filter(self.nova_base_ipv6_filter)
- self._define_filter(self.nova_dhcp_filter)
- self._define_filter(self.nova_ra_filter)
- if FLAGS.allow_project_net_traffic:
- self._define_filter(self.nova_project_filter)
- if FLAGS.use_ipv6:
- self._define_filter(self.nova_project_filter_v6)
-
- self.static_filters_configured = True
-
- def _filter_container(self, name, filters):
- xml = '''<filter name='%s' chain='root'>%s</filter>''' % (
- name,
- ''.join(["<filterref filter='%s'/>" % (f,) for f in filters]))
- return xml
-
- def nova_base_ipv4_filter(self):
- retval = "<filter name='nova-base-ipv4' chain='ipv4'>"
- for protocol in ['tcp', 'udp', 'icmp']:
- for direction, action, priority in [('out', 'accept', 399),
- ('in', 'drop', 400)]:
- retval += """<rule action='%s' direction='%s' priority='%d'>
- <%s />
- </rule>""" % (action, direction,
- priority, protocol)
- retval += '</filter>'
- return retval
-
- def nova_base_ipv6_filter(self):
- retval = "<filter name='nova-base-ipv6' chain='ipv6'>"
- for protocol in ['tcp-ipv6', 'udp-ipv6', 'icmpv6']:
- for direction, action, priority in [('out', 'accept', 399),
- ('in', 'drop', 400)]:
- retval += """<rule action='%s' direction='%s' priority='%d'>
- <%s />
- </rule>""" % (action, direction,
- priority, protocol)
- retval += '</filter>'
- return retval
-
- def nova_project_filter(self):
- retval = "<filter name='nova-project' chain='ipv4'>"
- for protocol in ['tcp', 'udp', 'icmp']:
- retval += """<rule action='accept' direction='in' priority='200'>
- <%s srcipaddr='$PROJNET' srcipmask='$PROJMASK' />
- </rule>""" % protocol
- retval += '</filter>'
- return retval
-
- def nova_project_filter_v6(self):
- retval = "<filter name='nova-project-v6' chain='ipv6'>"
- for protocol in ['tcp-ipv6', 'udp-ipv6', 'icmpv6']:
- retval += """<rule action='accept' direction='inout'
- priority='200'>
- <%s srcipaddr='$PROJNETV6'
- srcipmask='$PROJMASKV6' />
- </rule>""" % (protocol)
- retval += '</filter>'
- return retval
-
- def _define_filter(self, xml):
- if callable(xml):
- xml = xml()
- # execute in a native thread and block current greenthread until done
- tpool.execute(self._conn.nwfilterDefineXML, xml)
-
- def unfilter_instance(self, instance):
- # Nothing to do
- pass
-
- def prepare_instance_filter(self, instance, network_info=None):
- """
- Creates an NWFilter for the given instance. In the process,
- it makes sure the filters for the security groups as well as
- the base filter are all in place.
- """
- if not network_info:
- network_info = _get_network_info(instance)
-
- ctxt = context.get_admin_context()
-
- instance_secgroup_filter_name = \
- '%s-secgroup' % (self._instance_filter_name(instance))
- #% (instance_filter_name,)
-
- instance_secgroup_filter_children = ['nova-base-ipv4',
- 'nova-base-ipv6',
- 'nova-allow-dhcp-server']
-
- if FLAGS.use_ipv6:
- networks = [network for (network, _m) in network_info if
- network['gateway_v6']]
-
- if networks:
- instance_secgroup_filter_children.\
- append('nova-allow-ra-server')
-
- for security_group in \
- db.security_group_get_by_instance(ctxt, instance['id']):
-
- self.refresh_security_group_rules(security_group['id'])
-
- instance_secgroup_filter_children.append('nova-secgroup-%s' %
- security_group['id'])
-
- self._define_filter(
- self._filter_container(instance_secgroup_filter_name,
- instance_secgroup_filter_children))
-
- network_filters = self.\
- _create_network_filters(instance, network_info,
- instance_secgroup_filter_name)
-
- for (name, children) in network_filters:
- self._define_filters(name, children)
-
- def _create_network_filters(self, instance, network_info,
- instance_secgroup_filter_name):
- if instance['image_id'] == str(FLAGS.vpn_image_id):
- base_filter = 'nova-vpn'
- else:
- base_filter = 'nova-base'
-
- result = []
- for (_n, mapping) in network_info:
- nic_id = mapping['mac'].replace(':', '')
- instance_filter_name = self._instance_filter_name(instance, nic_id)
- instance_filter_children = [base_filter,
- instance_secgroup_filter_name]
-
- if FLAGS.allow_project_net_traffic:
- instance_filter_children.append('nova-project')
- if FLAGS.use_ipv6:
- instance_filter_children.append('nova-project-v6')
-
- result.append((instance_filter_name, instance_filter_children))
-
- return result
-
- def _define_filters(self, filter_name, filter_children):
- self._define_filter(self._filter_container(filter_name,
- filter_children))
-
- def refresh_security_group_rules(self, security_group_id):
- return self._define_filter(
- self.security_group_to_nwfilter_xml(security_group_id))
-
- def security_group_to_nwfilter_xml(self, security_group_id):
- security_group = db.security_group_get(context.get_admin_context(),
- security_group_id)
- rule_xml = ""
- v6protocol = {'tcp': 'tcp-ipv6', 'udp': 'udp-ipv6', 'icmp': 'icmpv6'}
- for rule in security_group.rules:
- rule_xml += "<rule action='accept' direction='in' priority='300'>"
- if rule.cidr:
- version = _get_ip_version(rule.cidr)
- if(FLAGS.use_ipv6 and version == 6):
- net, prefixlen = _get_net_and_prefixlen(rule.cidr)
- rule_xml += "<%s srcipaddr='%s' srcipmask='%s' " % \
- (v6protocol[rule.protocol], net, prefixlen)
- else:
- net, mask = _get_net_and_mask(rule.cidr)
- rule_xml += "<%s srcipaddr='%s' srcipmask='%s' " % \
- (rule.protocol, net, mask)
- if rule.protocol in ['tcp', 'udp']:
- rule_xml += "dstportstart='%s' dstportend='%s' " % \
- (rule.from_port, rule.to_port)
- elif rule.protocol == 'icmp':
- LOG.info('rule.protocol: %r, rule.from_port: %r, '
- 'rule.to_port: %r', rule.protocol,
- rule.from_port, rule.to_port)
- if rule.from_port != -1:
- rule_xml += "type='%s' " % rule.from_port
- if rule.to_port != -1:
- rule_xml += "code='%s' " % rule.to_port
-
- rule_xml += '/>\n'
- rule_xml += "</rule>\n"
- xml = "<filter name='nova-secgroup-%s' " % security_group_id
- if(FLAGS.use_ipv6):
- xml += "chain='root'>%s</filter>" % rule_xml
- else:
- xml += "chain='ipv4'>%s</filter>" % rule_xml
- return xml
-
- def _instance_filter_name(self, instance, nic_id=None):
- if not nic_id:
- return 'nova-instance-%s' % (instance['name'])
- return 'nova-instance-%s-%s' % (instance['name'], nic_id)
-
- def instance_filter_exists(self, instance):
- """Check nova-instance-instance-xxx exists"""
- network_info = _get_network_info(instance)
- for (network, mapping) in network_info:
- nic_id = mapping['mac'].replace(':', '')
- instance_filter_name = self._instance_filter_name(instance, nic_id)
- try:
- self._conn.nwfilterLookupByName(instance_filter_name)
- except libvirt.libvirtError:
- name = instance.name
- LOG.debug(_('The nwfilter(%(instance_filter_name)s) for'
- '%(name)s is not found.') % locals())
- return False
- return True
-
-
-class IptablesFirewallDriver(FirewallDriver):
- def __init__(self, execute=None, **kwargs):
- from nova.network import linux_net
- self.iptables = linux_net.iptables_manager
- self.instances = {}
- self.nwfilter = NWFilterFirewall(kwargs['get_connection'])
-
- self.iptables.ipv4['filter'].add_chain('sg-fallback')
- self.iptables.ipv4['filter'].add_rule('sg-fallback', '-j DROP')
- self.iptables.ipv6['filter'].add_chain('sg-fallback')
- self.iptables.ipv6['filter'].add_rule('sg-fallback', '-j DROP')
-
- def setup_basic_filtering(self, instance, network_info=None):
- """Use NWFilter from libvirt for this."""
- if not network_info:
- network_info = _get_network_info(instance)
- return self.nwfilter.setup_basic_filtering(instance, network_info)
-
- def apply_instance_filter(self, instance):
- """No-op. Everything is done in prepare_instance_filter"""
- pass
-
- def unfilter_instance(self, instance):
- if self.instances.pop(instance['id'], None):
- self.remove_filters_for_instance(instance)
- self.iptables.apply()
- else:
- LOG.info(_('Attempted to unfilter instance %s which is not '
- 'filtered'), instance['id'])
-
- def prepare_instance_filter(self, instance, network_info=None):
- if not network_info:
- network_info = _get_network_info(instance)
- self.instances[instance['id']] = instance
- self.add_filters_for_instance(instance, network_info)
- self.iptables.apply()
-
- def _create_filter(self, ips, chain_name):
- return ['-d %s -j $%s' % (ip, chain_name) for ip in ips]
-
- def _filters_for_instance(self, chain_name, network_info):
- ips_v4 = [ip['ip'] for (_n, mapping) in network_info
- for ip in mapping['ips']]
- ipv4_rules = self._create_filter(ips_v4, chain_name)
-
- ipv6_rules = []
- if FLAGS.use_ipv6:
- ips_v6 = [ip['ip'] for (_n, mapping) in network_info
- for ip in mapping['ip6s']]
- ipv6_rules = self._create_filter(ips_v6, chain_name)
-
- return ipv4_rules, ipv6_rules
-
- def _add_filters(self, chain_name, ipv4_rules, ipv6_rules):
- for rule in ipv4_rules:
- self.iptables.ipv4['filter'].add_rule(chain_name, rule)
-
- if FLAGS.use_ipv6:
- for rule in ipv6_rules:
- self.iptables.ipv6['filter'].add_rule(chain_name, rule)
-
- def add_filters_for_instance(self, instance, network_info=None):
- chain_name = self._instance_chain_name(instance)
- if FLAGS.use_ipv6:
- self.iptables.ipv6['filter'].add_chain(chain_name)
- self.iptables.ipv4['filter'].add_chain(chain_name)
- ipv4_rules, ipv6_rules = self._filters_for_instance(chain_name,
- network_info)
- self._add_filters('local', ipv4_rules, ipv6_rules)
- ipv4_rules, ipv6_rules = self.instance_rules(instance, network_info)
- self._add_filters(chain_name, ipv4_rules, ipv6_rules)
-
- def remove_filters_for_instance(self, instance):
- chain_name = self._instance_chain_name(instance)
-
- self.iptables.ipv4['filter'].remove_chain(chain_name)
- if FLAGS.use_ipv6:
- self.iptables.ipv6['filter'].remove_chain(chain_name)
-
- def instance_rules(self, instance, network_info=None):
- if not network_info:
- network_info = _get_network_info(instance)
- ctxt = context.get_admin_context()
-
- ipv4_rules = []
- ipv6_rules = []
-
- # Always drop invalid packets
- ipv4_rules += ['-m state --state ' 'INVALID -j DROP']
- ipv6_rules += ['-m state --state ' 'INVALID -j DROP']
-
- # Allow established connections
- ipv4_rules += ['-m state --state ESTABLISHED,RELATED -j ACCEPT']
- ipv6_rules += ['-m state --state ESTABLISHED,RELATED -j ACCEPT']
-
- dhcp_servers = [network['gateway'] for (network, _m) in network_info]
-
- for dhcp_server in dhcp_servers:
- ipv4_rules.append('-s %s -p udp --sport 67 --dport 68 '
- '-j ACCEPT' % (dhcp_server,))
-
- #Allow project network traffic
- if FLAGS.allow_project_net_traffic:
- cidrs = [network['cidr'] for (network, _m) in network_info]
- for cidr in cidrs:
- ipv4_rules.append('-s %s -j ACCEPT' % (cidr,))
-
- # We wrap these in FLAGS.use_ipv6 because they might cause
- # a DB lookup. The other ones are just list operations, so
- # they're not worth the clutter.
- if FLAGS.use_ipv6:
- # Allow RA responses
- gateways_v6 = [network['gateway_v6'] for (network, _) in
- network_info]
- for gateway_v6 in gateways_v6:
- ipv6_rules.append(
- '-s %s/128 -p icmpv6 -j ACCEPT' % (gateway_v6,))
-
- #Allow project network traffic
- if FLAGS.allow_project_net_traffic:
- cidrv6s = [network['cidr_v6'] for (network, _m)
- in network_info]
-
- for cidrv6 in cidrv6s:
- ipv6_rules.append('-s %s -j ACCEPT' % (cidrv6,))
-
- security_groups = db.security_group_get_by_instance(ctxt,
- instance['id'])
-
- # then, security group chains and rules
- for security_group in security_groups:
- rules = db.security_group_rule_get_by_security_group(ctxt,
- security_group['id'])
-
- for rule in rules:
- logging.info('%r', rule)
-
- if not rule.cidr:
- # Eventually, a mechanism to grant access for security
- # groups will turn up here. It'll use ipsets.
- continue
-
- version = _get_ip_version(rule.cidr)
- if version == 4:
- rules = ipv4_rules
- else:
- rules = ipv6_rules
-
- protocol = rule.protocol
- if version == 6 and rule.protocol == 'icmp':
- protocol = 'icmpv6'
-
- args = ['-p', protocol, '-s', rule.cidr]
-
- if rule.protocol in ['udp', 'tcp']:
- if rule.from_port == rule.to_port:
- args += ['--dport', '%s' % (rule.from_port,)]
- else:
- args += ['-m', 'multiport',
- '--dports', '%s:%s' % (rule.from_port,
- rule.to_port)]
- elif rule.protocol == 'icmp':
- icmp_type = rule.from_port
- icmp_code = rule.to_port
-
- if icmp_type == -1:
- icmp_type_arg = None
- else:
- icmp_type_arg = '%s' % icmp_type
- if not icmp_code == -1:
- icmp_type_arg += '/%s' % icmp_code
-
- if icmp_type_arg:
- if version == 4:
- args += ['-m', 'icmp', '--icmp-type',
- icmp_type_arg]
- elif version == 6:
- args += ['-m', 'icmp6', '--icmpv6-type',
- icmp_type_arg]
-
- args += ['-j ACCEPT']
- rules += [' '.join(args)]
-
- ipv4_rules += ['-j $sg-fallback']
- ipv6_rules += ['-j $sg-fallback']
-
- return ipv4_rules, ipv6_rules
-
- def instance_filter_exists(self, instance):
- """Check nova-instance-instance-xxx exists"""
- return self.nwfilter.instance_filter_exists(instance)
-
- def refresh_security_group_members(self, security_group):
- pass
-
- def refresh_security_group_rules(self, security_group):
- self.do_refresh_security_group_rules(security_group)
- self.iptables.apply()
-
- @utils.synchronized('iptables', external=True)
- def do_refresh_security_group_rules(self, security_group):
- for instance in self.instances.values():
- self.remove_filters_for_instance(instance)
- self.add_filters_for_instance(instance)
-
- def _security_group_chain_name(self, security_group_id):
- return 'nova-sg-%s' % (security_group_id,)
-
- def _instance_chain_name(self, instance):
- return 'inst-%s' % (instance['id'],)
diff --git a/nova/virt/libvirt/firewall.py b/nova/virt/libvirt/firewall.py
new file mode 100644
index 000000000..7e00662cd
--- /dev/null
+++ b/nova/virt/libvirt/firewall.py
@@ -0,0 +1,642 @@
+# 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.
+# 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.
+
+
+from eventlet import tpool
+
+from nova import context
+from nova import db
+from nova import flags
+from nova import log as logging
+from nova import utils
+from nova.virt.libvirt import netutils
+
+
+LOG = logging.getLogger("nova.virt.libvirt.firewall")
+FLAGS = flags.FLAGS
+
+
+try:
+ import libvirt
+except ImportError:
+ LOG.warn(_("Libvirt module could not be loaded. NWFilterFirewall will "
+ "not work correctly."))
+
+
+class FirewallDriver(object):
+ def prepare_instance_filter(self, instance, network_info=None):
+ """Prepare filters for the instance.
+
+ At this point, the instance isn't running yet."""
+ raise NotImplementedError()
+
+ def unfilter_instance(self, instance):
+ """Stop filtering instance"""
+ raise NotImplementedError()
+
+ def apply_instance_filter(self, instance):
+ """Apply instance filter.
+
+ Once this method returns, the instance should be firewalled
+ appropriately. This method should as far as possible be a
+ no-op. It's vastly preferred to get everything set up in
+ prepare_instance_filter.
+ """
+ raise NotImplementedError()
+
+ def refresh_security_group_rules(self,
+ security_group_id,
+ network_info=None):
+ """Refresh security group rules from data store
+
+ Gets called when a rule has been added to or removed from
+ the security group."""
+ raise NotImplementedError()
+
+ def refresh_security_group_members(self, security_group_id):
+ """Refresh security group members from data store
+
+ Gets called when an instance gets added to or removed from
+ the security group."""
+ raise NotImplementedError()
+
+ def setup_basic_filtering(self, instance, network_info=None):
+ """Create rules to block spoofing and allow dhcp.
+
+ This gets called when spawning an instance, before
+ :method:`prepare_instance_filter`.
+
+ """
+ raise NotImplementedError()
+
+ def instance_filter_exists(self, instance):
+ """Check nova-instance-instance-xxx exists"""
+ raise NotImplementedError()
+
+
+class NWFilterFirewall(FirewallDriver):
+ """
+ This class implements a network filtering mechanism versatile
+ enough for EC2 style Security Group filtering by leveraging
+ libvirt's nwfilter.
+
+ First, all instances get a filter ("nova-base-filter") applied.
+ This filter provides some basic security such as protection against
+ MAC spoofing, IP spoofing, and ARP spoofing.
+
+ This filter drops all incoming ipv4 and ipv6 connections.
+ Outgoing connections are never blocked.
+
+ Second, every security group maps to a nwfilter filter(*).
+ NWFilters can be updated at runtime and changes are applied
+ immediately, so changes to security groups can be applied at
+ runtime (as mandated by the spec).
+
+ Security group rules are named "nova-secgroup-<id>" where <id>
+ is the internal id of the security group. They're applied only on
+ hosts that have instances in the security group in question.
+
+ Updates to security groups are done by updating the data model
+ (in response to API calls) followed by a request sent to all
+ the nodes with instances in the security group to refresh the
+ security group.
+
+ Each instance has its own NWFilter, which references the above
+ mentioned security group NWFilters. This was done because
+ interfaces can only reference one filter while filters can
+ reference multiple other filters. This has the added benefit of
+ actually being able to add and remove security groups from an
+ instance at run time. This functionality is not exposed anywhere,
+ though.
+
+ Outstanding questions:
+
+ The name is unique, so would there be any good reason to sync
+ the uuid across the nodes (by assigning it from the datamodel)?
+
+
+ (*) This sentence brought to you by the redundancy department of
+ redundancy.
+
+ """
+
+ def __init__(self, get_connection, **kwargs):
+ self._libvirt_get_connection = get_connection
+ self.static_filters_configured = False
+ self.handle_security_groups = False
+
+ def apply_instance_filter(self, instance):
+ """No-op. Everything is done in prepare_instance_filter"""
+ pass
+
+ def _get_connection(self):
+ return self._libvirt_get_connection()
+ _conn = property(_get_connection)
+
+ def nova_dhcp_filter(self):
+ """The standard allow-dhcp-server filter is an <ip> one, so it uses
+ ebtables to allow traffic through. Without a corresponding rule in
+ iptables, it'll get blocked anyway."""
+
+ return '''<filter name='nova-allow-dhcp-server' chain='ipv4'>
+ <uuid>891e4787-e5c0-d59b-cbd6-41bc3c6b36fc</uuid>
+ <rule action='accept' direction='out'
+ priority='100'>
+ <udp srcipaddr='0.0.0.0'
+ dstipaddr='255.255.255.255'
+ srcportstart='68'
+ dstportstart='67'/>
+ </rule>
+ <rule action='accept' direction='in'
+ priority='100'>
+ <udp srcipaddr='$DHCPSERVER'
+ srcportstart='67'
+ dstportstart='68'/>
+ </rule>
+ </filter>'''
+
+ def nova_ra_filter(self):
+ return '''<filter name='nova-allow-ra-server' chain='root'>
+ <uuid>d707fa71-4fb5-4b27-9ab7-ba5ca19c8804</uuid>
+ <rule action='accept' direction='inout'
+ priority='100'>
+ <icmpv6 srcipaddr='$RASERVER'/>
+ </rule>
+ </filter>'''
+
+ def setup_basic_filtering(self, instance, network_info=None):
+ """Set up basic filtering (MAC, IP, and ARP spoofing protection)"""
+ logging.info('called setup_basic_filtering in nwfilter')
+
+ if not network_info:
+ network_info = netutils.get_network_info(instance)
+
+ if self.handle_security_groups:
+ # No point in setting up a filter set that we'll be overriding
+ # anyway.
+ return
+
+ logging.info('ensuring static filters')
+ self._ensure_static_filters()
+
+ if instance['image_id'] == str(FLAGS.vpn_image_id):
+ base_filter = 'nova-vpn'
+ else:
+ base_filter = 'nova-base'
+
+ for (network, mapping) in network_info:
+ nic_id = mapping['mac'].replace(':', '')
+ instance_filter_name = self._instance_filter_name(instance, nic_id)
+ self._define_filter(self._filter_container(instance_filter_name,
+ [base_filter]))
+
+ def _ensure_static_filters(self):
+ if self.static_filters_configured:
+ return
+
+ self._define_filter(self._filter_container('nova-base',
+ ['no-mac-spoofing',
+ 'no-ip-spoofing',
+ 'no-arp-spoofing',
+ 'allow-dhcp-server']))
+ self._define_filter(self._filter_container('nova-vpn',
+ ['allow-dhcp-server']))
+ self._define_filter(self.nova_base_ipv4_filter)
+ self._define_filter(self.nova_base_ipv6_filter)
+ self._define_filter(self.nova_dhcp_filter)
+ self._define_filter(self.nova_ra_filter)
+ if FLAGS.allow_project_net_traffic:
+ self._define_filter(self.nova_project_filter)
+ if FLAGS.use_ipv6:
+ self._define_filter(self.nova_project_filter_v6)
+
+ self.static_filters_configured = True
+
+ def _filter_container(self, name, filters):
+ xml = '''<filter name='%s' chain='root'>%s</filter>''' % (
+ name,
+ ''.join(["<filterref filter='%s'/>" % (f,) for f in filters]))
+ return xml
+
+ def nova_base_ipv4_filter(self):
+ retval = "<filter name='nova-base-ipv4' chain='ipv4'>"
+ for protocol in ['tcp', 'udp', 'icmp']:
+ for direction, action, priority in [('out', 'accept', 399),
+ ('in', 'drop', 400)]:
+ retval += """<rule action='%s' direction='%s' priority='%d'>
+ <%s />
+ </rule>""" % (action, direction,
+ priority, protocol)
+ retval += '</filter>'
+ return retval
+
+ def nova_base_ipv6_filter(self):
+ retval = "<filter name='nova-base-ipv6' chain='ipv6'>"
+ for protocol in ['tcp-ipv6', 'udp-ipv6', 'icmpv6']:
+ for direction, action, priority in [('out', 'accept', 399),
+ ('in', 'drop', 400)]:
+ retval += """<rule action='%s' direction='%s' priority='%d'>
+ <%s />
+ </rule>""" % (action, direction,
+ priority, protocol)
+ retval += '</filter>'
+ return retval
+
+ def nova_project_filter(self):
+ retval = "<filter name='nova-project' chain='ipv4'>"
+ for protocol in ['tcp', 'udp', 'icmp']:
+ retval += """<rule action='accept' direction='in' priority='200'>
+ <%s srcipaddr='$PROJNET' srcipmask='$PROJMASK' />
+ </rule>""" % protocol
+ retval += '</filter>'
+ return retval
+
+ def nova_project_filter_v6(self):
+ retval = "<filter name='nova-project-v6' chain='ipv6'>"
+ for protocol in ['tcp-ipv6', 'udp-ipv6', 'icmpv6']:
+ retval += """<rule action='accept' direction='inout'
+ priority='200'>
+ <%s srcipaddr='$PROJNETV6'
+ srcipmask='$PROJMASKV6' />
+ </rule>""" % (protocol)
+ retval += '</filter>'
+ return retval
+
+ def _define_filter(self, xml):
+ if callable(xml):
+ xml = xml()
+ # execute in a native thread and block current greenthread until done
+ tpool.execute(self._conn.nwfilterDefineXML, xml)
+
+ def unfilter_instance(self, instance):
+ # Nothing to do
+ pass
+
+ def prepare_instance_filter(self, instance, network_info=None):
+ """
+ Creates an NWFilter for the given instance. In the process,
+ it makes sure the filters for the security groups as well as
+ the base filter are all in place.
+ """
+ if not network_info:
+ network_info = netutils.get_network_info(instance)
+
+ ctxt = context.get_admin_context()
+
+ instance_secgroup_filter_name = \
+ '%s-secgroup' % (self._instance_filter_name(instance))
+ #% (instance_filter_name,)
+
+ instance_secgroup_filter_children = ['nova-base-ipv4',
+ 'nova-base-ipv6',
+ 'nova-allow-dhcp-server']
+
+ if FLAGS.use_ipv6:
+ networks = [network for (network, _m) in network_info if
+ network['gateway_v6']]
+
+ if networks:
+ instance_secgroup_filter_children.\
+ append('nova-allow-ra-server')
+
+ for security_group in \
+ db.security_group_get_by_instance(ctxt, instance['id']):
+
+ self.refresh_security_group_rules(security_group['id'])
+
+ instance_secgroup_filter_children.append('nova-secgroup-%s' %
+ security_group['id'])
+
+ self._define_filter(
+ self._filter_container(instance_secgroup_filter_name,
+ instance_secgroup_filter_children))
+
+ network_filters = self.\
+ _create_network_filters(instance, network_info,
+ instance_secgroup_filter_name)
+
+ for (name, children) in network_filters:
+ self._define_filters(name, children)
+
+ def _create_network_filters(self, instance, network_info,
+ instance_secgroup_filter_name):
+ if instance['image_id'] == str(FLAGS.vpn_image_id):
+ base_filter = 'nova-vpn'
+ else:
+ base_filter = 'nova-base'
+
+ result = []
+ for (_n, mapping) in network_info:
+ nic_id = mapping['mac'].replace(':', '')
+ instance_filter_name = self._instance_filter_name(instance, nic_id)
+ instance_filter_children = [base_filter,
+ instance_secgroup_filter_name]
+
+ if FLAGS.allow_project_net_traffic:
+ instance_filter_children.append('nova-project')
+ if FLAGS.use_ipv6:
+ instance_filter_children.append('nova-project-v6')
+
+ result.append((instance_filter_name, instance_filter_children))
+
+ return result
+
+ def _define_filters(self, filter_name, filter_children):
+ self._define_filter(self._filter_container(filter_name,
+ filter_children))
+
+ def refresh_security_group_rules(self,
+ security_group_id,
+ network_info=None):
+ return self._define_filter(
+ self.security_group_to_nwfilter_xml(security_group_id))
+
+ def security_group_to_nwfilter_xml(self, security_group_id):
+ security_group = db.security_group_get(context.get_admin_context(),
+ security_group_id)
+ rule_xml = ""
+ v6protocol = {'tcp': 'tcp-ipv6', 'udp': 'udp-ipv6', 'icmp': 'icmpv6'}
+ for rule in security_group.rules:
+ rule_xml += "<rule action='accept' direction='in' priority='300'>"
+ if rule.cidr:
+ version = netutils.get_ip_version(rule.cidr)
+ if(FLAGS.use_ipv6 and version == 6):
+ net, prefixlen = netutils.get_net_and_prefixlen(rule.cidr)
+ rule_xml += "<%s srcipaddr='%s' srcipmask='%s' " % \
+ (v6protocol[rule.protocol], net, prefixlen)
+ else:
+ net, mask = netutils.get_net_and_mask(rule.cidr)
+ rule_xml += "<%s srcipaddr='%s' srcipmask='%s' " % \
+ (rule.protocol, net, mask)
+ if rule.protocol in ['tcp', 'udp']:
+ rule_xml += "dstportstart='%s' dstportend='%s' " % \
+ (rule.from_port, rule.to_port)
+ elif rule.protocol == 'icmp':
+ LOG.info('rule.protocol: %r, rule.from_port: %r, '
+ 'rule.to_port: %r', rule.protocol,
+ rule.from_port, rule.to_port)
+ if rule.from_port != -1:
+ rule_xml += "type='%s' " % rule.from_port
+ if rule.to_port != -1:
+ rule_xml += "code='%s' " % rule.to_port
+
+ rule_xml += '/>\n'
+ rule_xml += "</rule>\n"
+ xml = "<filter name='nova-secgroup-%s' " % security_group_id
+ if(FLAGS.use_ipv6):
+ xml += "chain='root'>%s</filter>" % rule_xml
+ else:
+ xml += "chain='ipv4'>%s</filter>" % rule_xml
+ return xml
+
+ def _instance_filter_name(self, instance, nic_id=None):
+ if not nic_id:
+ return 'nova-instance-%s' % (instance['name'])
+ return 'nova-instance-%s-%s' % (instance['name'], nic_id)
+
+ def instance_filter_exists(self, instance):
+ """Check nova-instance-instance-xxx exists"""
+ network_info = netutils.get_network_info(instance)
+ for (network, mapping) in network_info:
+ nic_id = mapping['mac'].replace(':', '')
+ instance_filter_name = self._instance_filter_name(instance, nic_id)
+ try:
+ self._conn.nwfilterLookupByName(instance_filter_name)
+ except libvirt.libvirtError:
+ name = instance.name
+ LOG.debug(_('The nwfilter(%(instance_filter_name)s) for'
+ '%(name)s is not found.') % locals())
+ return False
+ return True
+
+
+class IptablesFirewallDriver(FirewallDriver):
+ def __init__(self, execute=None, **kwargs):
+ from nova.network import linux_net
+ self.iptables = linux_net.iptables_manager
+ self.instances = {}
+ self.nwfilter = NWFilterFirewall(kwargs['get_connection'])
+
+ self.iptables.ipv4['filter'].add_chain('sg-fallback')
+ self.iptables.ipv4['filter'].add_rule('sg-fallback', '-j DROP')
+ self.iptables.ipv6['filter'].add_chain('sg-fallback')
+ self.iptables.ipv6['filter'].add_rule('sg-fallback', '-j DROP')
+
+ def setup_basic_filtering(self, instance, network_info=None):
+ """Use NWFilter from libvirt for this."""
+ if not network_info:
+ network_info = netutils.get_network_info(instance)
+ return self.nwfilter.setup_basic_filtering(instance, network_info)
+
+ def apply_instance_filter(self, instance):
+ """No-op. Everything is done in prepare_instance_filter"""
+ pass
+
+ def unfilter_instance(self, instance):
+ if self.instances.pop(instance['id'], None):
+ self.remove_filters_for_instance(instance)
+ self.iptables.apply()
+ else:
+ LOG.info(_('Attempted to unfilter instance %s which is not '
+ 'filtered'), instance['id'])
+
+ def prepare_instance_filter(self, instance, network_info=None):
+ if not network_info:
+ network_info = netutils.get_network_info(instance)
+ self.instances[instance['id']] = instance
+ self.add_filters_for_instance(instance, network_info)
+ self.iptables.apply()
+
+ def _create_filter(self, ips, chain_name):
+ return ['-d %s -j $%s' % (ip, chain_name) for ip in ips]
+
+ def _filters_for_instance(self, chain_name, network_info):
+ ips_v4 = [ip['ip'] for (_n, mapping) in network_info
+ for ip in mapping['ips']]
+ ipv4_rules = self._create_filter(ips_v4, chain_name)
+
+ ipv6_rules = []
+ if FLAGS.use_ipv6:
+ ips_v6 = [ip['ip'] for (_n, mapping) in network_info
+ for ip in mapping['ip6s']]
+ ipv6_rules = self._create_filter(ips_v6, chain_name)
+
+ return ipv4_rules, ipv6_rules
+
+ def _add_filters(self, chain_name, ipv4_rules, ipv6_rules):
+ for rule in ipv4_rules:
+ self.iptables.ipv4['filter'].add_rule(chain_name, rule)
+
+ if FLAGS.use_ipv6:
+ for rule in ipv6_rules:
+ self.iptables.ipv6['filter'].add_rule(chain_name, rule)
+
+ def add_filters_for_instance(self, instance, network_info=None):
+ chain_name = self._instance_chain_name(instance)
+ if FLAGS.use_ipv6:
+ self.iptables.ipv6['filter'].add_chain(chain_name)
+ self.iptables.ipv4['filter'].add_chain(chain_name)
+ ipv4_rules, ipv6_rules = self._filters_for_instance(chain_name,
+ network_info)
+ self._add_filters('local', ipv4_rules, ipv6_rules)
+ ipv4_rules, ipv6_rules = self.instance_rules(instance, network_info)
+ self._add_filters(chain_name, ipv4_rules, ipv6_rules)
+
+ def remove_filters_for_instance(self, instance):
+ chain_name = self._instance_chain_name(instance)
+
+ self.iptables.ipv4['filter'].remove_chain(chain_name)
+ if FLAGS.use_ipv6:
+ self.iptables.ipv6['filter'].remove_chain(chain_name)
+
+ def instance_rules(self, instance, network_info=None):
+ if not network_info:
+ network_info = netutils.get_network_info(instance)
+ ctxt = context.get_admin_context()
+
+ ipv4_rules = []
+ ipv6_rules = []
+
+ # Always drop invalid packets
+ ipv4_rules += ['-m state --state ' 'INVALID -j DROP']
+ ipv6_rules += ['-m state --state ' 'INVALID -j DROP']
+
+ # Allow established connections
+ ipv4_rules += ['-m state --state ESTABLISHED,RELATED -j ACCEPT']
+ ipv6_rules += ['-m state --state ESTABLISHED,RELATED -j ACCEPT']
+
+ dhcp_servers = [network['gateway'] for (network, _m) in network_info]
+
+ for dhcp_server in dhcp_servers:
+ ipv4_rules.append('-s %s -p udp --sport 67 --dport 68 '
+ '-j ACCEPT' % (dhcp_server,))
+
+ #Allow project network traffic
+ if FLAGS.allow_project_net_traffic:
+ cidrs = [network['cidr'] for (network, _m) in network_info]
+ for cidr in cidrs:
+ ipv4_rules.append('-s %s -j ACCEPT' % (cidr,))
+
+ # We wrap these in FLAGS.use_ipv6 because they might cause
+ # a DB lookup. The other ones are just list operations, so
+ # they're not worth the clutter.
+ if FLAGS.use_ipv6:
+ # Allow RA responses
+ gateways_v6 = [network['gateway_v6'] for (network, _) in
+ network_info]
+ for gateway_v6 in gateways_v6:
+ ipv6_rules.append(
+ '-s %s/128 -p icmpv6 -j ACCEPT' % (gateway_v6,))
+
+ #Allow project network traffic
+ if FLAGS.allow_project_net_traffic:
+ cidrv6s = [network['cidr_v6'] for (network, _m)
+ in network_info]
+
+ for cidrv6 in cidrv6s:
+ ipv6_rules.append('-s %s -j ACCEPT' % (cidrv6,))
+
+ security_groups = db.security_group_get_by_instance(ctxt,
+ instance['id'])
+
+ # then, security group chains and rules
+ for security_group in security_groups:
+ rules = db.security_group_rule_get_by_security_group(ctxt,
+ security_group['id'])
+
+ for rule in rules:
+ logging.info('%r', rule)
+
+ if not rule.cidr:
+ # Eventually, a mechanism to grant access for security
+ # groups will turn up here. It'll use ipsets.
+ continue
+
+ version = netutils.get_ip_version(rule.cidr)
+ if version == 4:
+ rules = ipv4_rules
+ else:
+ rules = ipv6_rules
+
+ protocol = rule.protocol
+ if version == 6 and rule.protocol == 'icmp':
+ protocol = 'icmpv6'
+
+ args = ['-p', protocol, '-s', rule.cidr]
+
+ if rule.protocol in ['udp', 'tcp']:
+ if rule.from_port == rule.to_port:
+ args += ['--dport', '%s' % (rule.from_port,)]
+ else:
+ args += ['-m', 'multiport',
+ '--dports', '%s:%s' % (rule.from_port,
+ rule.to_port)]
+ elif rule.protocol == 'icmp':
+ icmp_type = rule.from_port
+ icmp_code = rule.to_port
+
+ if icmp_type == -1:
+ icmp_type_arg = None
+ else:
+ icmp_type_arg = '%s' % icmp_type
+ if not icmp_code == -1:
+ icmp_type_arg += '/%s' % icmp_code
+
+ if icmp_type_arg:
+ if version == 4:
+ args += ['-m', 'icmp', '--icmp-type',
+ icmp_type_arg]
+ elif version == 6:
+ args += ['-m', 'icmp6', '--icmpv6-type',
+ icmp_type_arg]
+
+ args += ['-j ACCEPT']
+ rules += [' '.join(args)]
+
+ ipv4_rules += ['-j $sg-fallback']
+ ipv6_rules += ['-j $sg-fallback']
+
+ return ipv4_rules, ipv6_rules
+
+ def instance_filter_exists(self, instance):
+ """Check nova-instance-instance-xxx exists"""
+ return self.nwfilter.instance_filter_exists(instance)
+
+ def refresh_security_group_members(self, security_group):
+ pass
+
+ def refresh_security_group_rules(self, security_group, network_info=None):
+ self.do_refresh_security_group_rules(security_group, network_info)
+ self.iptables.apply()
+
+ @utils.synchronized('iptables', external=True)
+ def do_refresh_security_group_rules(self,
+ security_group,
+ network_info=None):
+ for instance in self.instances.values():
+ self.remove_filters_for_instance(instance)
+ if not network_info:
+ network_info = netutils.get_network_info(instance)
+ self.add_filters_for_instance(instance, network_info)
+
+ def _security_group_chain_name(self, security_group_id):
+ return 'nova-sg-%s' % (security_group_id,)
+
+ def _instance_chain_name(self, instance):
+ return 'inst-%s' % (instance['id'],)
diff --git a/nova/virt/libvirt/netutils.py b/nova/virt/libvirt/netutils.py
new file mode 100644
index 000000000..4d596078a
--- /dev/null
+++ b/nova/virt/libvirt/netutils.py
@@ -0,0 +1,97 @@
+# 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.
+# 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.
+
+
+"""Network-releated utilities for supporting libvirt connection code."""
+
+
+import IPy
+
+from nova import context
+from nova import db
+from nova import flags
+from nova import ipv6
+from nova import utils
+
+
+FLAGS = flags.FLAGS
+
+
+def get_net_and_mask(cidr):
+ net = IPy.IP(cidr)
+ return str(net.net()), str(net.netmask())
+
+
+def get_net_and_prefixlen(cidr):
+ net = IPy.IP(cidr)
+ return str(net.net()), str(net.prefixlen())
+
+
+def get_ip_version(cidr):
+ net = IPy.IP(cidr)
+ return int(net.version())
+
+
+def get_network_info(instance):
+ # TODO(adiantum) If we will keep this function
+ # we should cache network_info
+ admin_context = context.get_admin_context()
+
+ ip_addresses = db.fixed_ip_get_all_by_instance(admin_context,
+ instance['id'])
+ networks = db.network_get_all_by_instance(admin_context,
+ instance['id'])
+ flavor = db.instance_type_get_by_id(admin_context,
+ instance['instance_type_id'])
+ network_info = []
+
+ for network in networks:
+ network_ips = [ip for ip in ip_addresses
+ if ip['network_id'] == network['id']]
+
+ def ip_dict(ip):
+ return {
+ 'ip': ip['address'],
+ 'netmask': network['netmask'],
+ 'enabled': '1'}
+
+ def ip6_dict():
+ prefix = network['cidr_v6']
+ mac = instance['mac_address']
+ project_id = instance['project_id']
+ return {
+ 'ip': ipv6.to_global(prefix, mac, project_id),
+ 'netmask': network['netmask_v6'],
+ 'enabled': '1'}
+
+ mapping = {
+ 'label': network['label'],
+ 'gateway': network['gateway'],
+ 'broadcast': network['broadcast'],
+ 'mac': instance['mac_address'],
+ 'rxtx_cap': flavor['rxtx_cap'],
+ 'dns': [network['dns']],
+ 'ips': [ip_dict(ip) for ip in network_ips]}
+
+ if FLAGS.use_ipv6:
+ mapping['ip6s'] = [ip6_dict()]
+ mapping['gateway6'] = network['gateway_v6']
+
+ network_info.append((network, mapping))
+ return network_info
diff --git a/nova/virt/vmwareapi/vmops.py b/nova/virt/vmwareapi/vmops.py
index c3e79a92f..6d7149841 100644
--- a/nova/virt/vmwareapi/vmops.py
+++ b/nova/virt/vmwareapi/vmops.py
@@ -590,11 +590,11 @@ class VMWareVMOps(object):
def pause(self, instance, callback):
"""Pause a VM instance."""
- raise exception.APIError("pause not supported for vmwareapi")
+ raise exception.ApiError("pause not supported for vmwareapi")
def unpause(self, instance, callback):
"""Un-Pause a VM instance."""
- raise exception.APIError("unpause not supported for vmwareapi")
+ raise exception.ApiError("unpause not supported for vmwareapi")
def suspend(self, instance, callback):
"""Suspend the specified instance."""
@@ -673,7 +673,7 @@ class VMWareVMOps(object):
def get_diagnostics(self, instance):
"""Return data about VM diagnostics."""
- raise exception.APIError("get_diagnostics not implemented for "
+ raise exception.ApiError("get_diagnostics not implemented for "
"vmwareapi")
def get_console_output(self, instance):
diff --git a/nova/virt/xenapi/fake.py b/nova/virt/xenapi/fake.py
index e36ef3288..76988b172 100644
--- a/nova/virt/xenapi/fake.py
+++ b/nova/virt/xenapi/fake.py
@@ -159,7 +159,10 @@ def after_VBD_create(vbd_ref, vbd_rec):
vbd_rec['device'] = ''
vm_ref = vbd_rec['VM']
vm_rec = _db_content['VM'][vm_ref]
- vm_rec['VBDs'] = [vbd_ref]
+ if vm_rec.get('VBDs', None):
+ vm_rec['VBDs'].append(vbd_ref)
+ else:
+ vm_rec['VBDs'] = [vbd_ref]
vm_name_label = _db_content['VM'][vm_ref]['name_label']
vbd_rec['vm_name_label'] = vm_name_label
diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py
index c8f342aa8..06ee8ee9b 100644
--- a/nova/virt/xenapi/vm_utils.py
+++ b/nova/virt/xenapi/vm_utils.py
@@ -19,6 +19,7 @@ Helper methods for operations related to the management of VM records and
their attributes like VDIs, VIFs, as well as their lookup functions.
"""
+import json
import os
import pickle
import re
@@ -48,6 +49,8 @@ FLAGS = flags.FLAGS
flags.DEFINE_string('default_os_type', 'linux', 'Default OS type')
flags.DEFINE_integer('block_device_creation_timeout', 10,
'time to wait for a block device to be created')
+flags.DEFINE_integer('max_kernel_ramdisk_size', 16 * 1024 * 1024,
+ 'maximum size in bytes of kernel or ramdisk images')
XENAPI_POWER_STATE = {
'Halted': power_state.SHUTDOWN,
@@ -374,6 +377,9 @@ class VMHelper(HelperBase):
xenapi_image_service = ['glance', 'objectstore']
glance_address = 'address for glance services'
glance_port = 'port for glance services'
+
+ Returns: A single filename if image_type is KERNEL_RAMDISK
+ A list of dictionaries that describe VDIs, otherwise
"""
access = AuthManager().get_access_key(user, project)
@@ -388,6 +394,10 @@ class VMHelper(HelperBase):
@classmethod
def _fetch_image_glance_vhd(cls, session, instance_id, image, access,
image_type):
+ """Tell glance to download an image and put the VHDs into the SR
+
+ Returns: A list of dictionaries that describe VDIs
+ """
LOG.debug(_("Asking xapi to fetch vhd image %(image)s")
% locals())
@@ -406,18 +416,26 @@ class VMHelper(HelperBase):
kwargs = {'params': pickle.dumps(params)}
task = session.async_call_plugin('glance', 'download_vhd', kwargs)
- vdi_uuid = session.wait_for_task(task, instance_id)
+ result = session.wait_for_task(task, instance_id)
+ # 'download_vhd' will return a json encoded string containing
+ # a list of dictionaries describing VDIs. The dictionary will
+ # contain 'vdi_type' and 'vdi_uuid' keys. 'vdi_type' can be
+ # 'os' or 'swap' right now.
+ vdis = json.loads(result)
+ for vdi in vdis:
+ LOG.debug(_("xapi 'download_vhd' returned VDI of "
+ "type '%(vdi_type)s' with UUID '%(vdi_uuid)s'" % vdi))
cls.scan_sr(session, instance_id, sr_ref)
+ # Pull out the UUID of the first VDI
+ vdi_uuid = vdis[0]['vdi_uuid']
# Set the name-label to ease debugging
vdi_ref = session.get_xenapi().VDI.get_by_uuid(vdi_uuid)
- name_label = get_name_label_for_image(image)
- session.get_xenapi().VDI.set_name_label(vdi_ref, name_label)
+ primary_name_label = get_name_label_for_image(image)
+ session.get_xenapi().VDI.set_name_label(vdi_ref, primary_name_label)
- LOG.debug(_("xapi 'download_vhd' returned VDI UUID %(vdi_uuid)s")
- % locals())
- return vdi_uuid
+ return vdis
@classmethod
def _fetch_image_glance_disk(cls, session, instance_id, image, access,
@@ -429,6 +447,8 @@ class VMHelper(HelperBase):
plugin; instead, it streams the disks through domU to the VDI
directly.
+ Returns: A single filename if image_type is KERNEL_RAMDISK
+ A list of dictionaries that describe VDIs, otherwise
"""
# FIXME(sirp): Since the Glance plugin seems to be required for the
# VHD disk, it may be worth using the plugin for both VHD and RAW and
@@ -444,6 +464,12 @@ class VMHelper(HelperBase):
if image_type == ImageType.DISK:
# Make room for MBR.
vdi_size += MBR_SIZE_BYTES
+ elif image_type == ImageType.KERNEL_RAMDISK and \
+ vdi_size > FLAGS.max_kernel_ramdisk_size:
+ max_size = FLAGS.max_kernel_ramdisk_size
+ raise exception.Error(
+ _("Kernel/Ramdisk image is too large: %(vdi_size)d bytes, "
+ "max %(max_size)d bytes") % locals())
name_label = get_name_label_for_image(image)
vdi_ref = cls.create_vdi(session, sr_ref, name_label, vdi_size, False)
@@ -468,7 +494,8 @@ class VMHelper(HelperBase):
LOG.debug(_("Kernel/Ramdisk VDI %s destroyed"), vdi_ref)
return filename
else:
- return session.get_xenapi().VDI.get_uuid(vdi_ref)
+ vdi_uuid = session.get_xenapi().VDI.get_uuid(vdi_ref)
+ return [dict(vdi_type='os', vdi_uuid=vdi_uuid)]
@classmethod
def determine_disk_image_type(cls, instance):
@@ -527,6 +554,11 @@ class VMHelper(HelperBase):
@classmethod
def _fetch_image_glance(cls, session, instance_id, image, access,
image_type):
+ """Fetch image from glance based on image type.
+
+ Returns: A single filename if image_type is KERNEL_RAMDISK
+ A list of dictionaries that describe VDIs, otherwise
+ """
if image_type == ImageType.DISK_VHD:
return cls._fetch_image_glance_vhd(
session, instance_id, image, access, image_type)
@@ -537,6 +569,11 @@ class VMHelper(HelperBase):
@classmethod
def _fetch_image_objectstore(cls, session, instance_id, image, access,
secret, image_type):
+ """Fetch an image from objectstore.
+
+ Returns: A single filename if image_type is KERNEL_RAMDISK
+ A list of dictionaries that describe VDIs, otherwise
+ """
url = images.image_url(image)
LOG.debug(_("Asking xapi to fetch %(url)s as %(access)s") % locals())
if image_type == ImageType.KERNEL_RAMDISK:
@@ -554,8 +591,10 @@ class VMHelper(HelperBase):
if image_type == ImageType.DISK_RAW:
args['raw'] = 'true'
task = session.async_call_plugin('objectstore', fn, args)
- uuid = session.wait_for_task(task, instance_id)
- return uuid
+ uuid_or_fn = session.wait_for_task(task, instance_id)
+ if image_type != ImageType.KERNEL_RAMDISK:
+ return [dict(vdi_type='os', vdi_uuid=uuid_or_fn)]
+ return uuid_or_fn
@classmethod
def determine_is_pv(cls, session, instance_id, vdi_ref, disk_image_type,
diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py
index 13d7d215b..2b3fb6a39 100644
--- a/nova/virt/xenapi/vmops.py
+++ b/nova/virt/xenapi/vmops.py
@@ -25,7 +25,6 @@ import M2Crypto
import os
import pickle
import subprocess
-import tempfile
import uuid
from nova import context
@@ -92,7 +91,8 @@ class VMOps(object):
def finish_resize(self, instance, disk_info):
vdi_uuid = self.link_disks(instance, disk_info['base_copy'],
disk_info['cow'])
- vm_ref = self._create_vm(instance, vdi_uuid)
+ vm_ref = self._create_vm(instance,
+ [dict(vdi_type='os', vdi_uuid=vdi_uuid)])
self.resize_instance(instance, vdi_uuid)
self._spawn(instance, vm_ref)
@@ -106,24 +106,25 @@ class VMOps(object):
LOG.debug(_("Starting instance %s"), instance.name)
self._session.call_xenapi('VM.start', vm_ref, False, False)
- def _create_disk(self, instance):
+ def _create_disks(self, instance):
user = AuthManager().get_user(instance.user_id)
project = AuthManager().get_project(instance.project_id)
disk_image_type = VMHelper.determine_disk_image_type(instance)
- vdi_uuid = VMHelper.fetch_image(self._session, instance.id,
- instance.image_id, user, project, disk_image_type)
- return vdi_uuid
+ vdis = VMHelper.fetch_image(self._session,
+ instance.id, instance.image_id, user, project,
+ disk_image_type)
+ return vdis
def spawn(self, instance, network_info=None):
- vdi_uuid = self._create_disk(instance)
- vm_ref = self._create_vm(instance, vdi_uuid, network_info)
+ vdis = self._create_disks(instance)
+ vm_ref = self._create_vm(instance, vdis, network_info)
self._spawn(instance, vm_ref)
def spawn_rescue(self, instance):
"""Spawn a rescue instance."""
self.spawn(instance)
- def _create_vm(self, instance, vdi_uuid, network_info=None):
+ def _create_vm(self, instance, vdis, network_info=None):
"""Create VM instance."""
instance_name = instance.name
vm_ref = VMHelper.lookup(self._session, instance_name)
@@ -142,28 +143,43 @@ class VMOps(object):
user = AuthManager().get_user(instance.user_id)
project = AuthManager().get_project(instance.project_id)
- # Are we building from a pre-existing disk?
- vdi_ref = self._session.call_xenapi('VDI.get_by_uuid', vdi_uuid)
-
disk_image_type = VMHelper.determine_disk_image_type(instance)
kernel = None
if instance.kernel_id:
kernel = VMHelper.fetch_image(self._session, instance.id,
- instance.kernel_id, user, project, ImageType.KERNEL_RAMDISK)
+ instance.kernel_id, user, project,
+ ImageType.KERNEL_RAMDISK)
ramdisk = None
if instance.ramdisk_id:
ramdisk = VMHelper.fetch_image(self._session, instance.id,
- instance.ramdisk_id, user, project, ImageType.KERNEL_RAMDISK)
-
- use_pv_kernel = VMHelper.determine_is_pv(self._session, instance.id,
- vdi_ref, disk_image_type, instance.os_type)
- vm_ref = VMHelper.create_vm(self._session, instance, kernel, ramdisk,
- use_pv_kernel)
-
+ instance.ramdisk_id, user, project,
+ ImageType.KERNEL_RAMDISK)
+
+ # Create the VM ref and attach the first disk
+ first_vdi_ref = self._session.call_xenapi('VDI.get_by_uuid',
+ vdis[0]['vdi_uuid'])
+ use_pv_kernel = VMHelper.determine_is_pv(self._session,
+ instance.id, first_vdi_ref, disk_image_type,
+ instance.os_type)
+ vm_ref = VMHelper.create_vm(self._session, instance,
+ kernel, ramdisk, use_pv_kernel)
VMHelper.create_vbd(session=self._session, vm_ref=vm_ref,
- vdi_ref=vdi_ref, userdevice=0, bootable=True)
+ vdi_ref=first_vdi_ref, userdevice=0, bootable=True)
+
+ # Attach any other disks
+ # userdevice 1 is reserved for rescue
+ userdevice = 2
+ for vdi in vdis[1:]:
+ # vdi['vdi_type'] is either 'os' or 'swap', but we don't
+ # really care what it is right here.
+ vdi_ref = self._session.call_xenapi('VDI.get_by_uuid',
+ vdi['vdi_uuid'])
+ VMHelper.create_vbd(session=self._session, vm_ref=vm_ref,
+ vdi_ref=vdi_ref, userdevice=userdevice,
+ bootable=False)
+ userdevice += 1
# TODO(tr3buchet) - check to make sure we have network info, otherwise
# create it now. This goes away once nova-multi-nic hits.
@@ -173,7 +189,7 @@ class VMOps(object):
# Alter the image before VM start for, e.g. network injection
if FLAGS.xenapi_inject_image:
VMHelper.preconfigure_instance(self._session, instance,
- vdi_ref, network_info)
+ first_vdi_ref, network_info)
self.create_vifs(vm_ref, network_info)
self.inject_network_info(instance, network_info, vm_ref)
@@ -203,6 +219,13 @@ class VMOps(object):
for path, contents in instance.injected_files:
LOG.debug(_("Injecting file path: '%s'") % path)
self.inject_file(instance, path, contents)
+
+ def _set_admin_password():
+ admin_password = instance.admin_pass
+ if admin_password:
+ LOG.debug(_("Setting admin password"))
+ self.set_admin_password(instance, admin_password)
+
# NOTE(armando): Do we really need to do this in virt?
# NOTE(tr3buchet): not sure but wherever we do it, we need to call
# reset_network afterwards
@@ -215,6 +238,7 @@ class VMOps(object):
LOG.debug(_('Instance %s: booted'), instance_name)
timer.stop()
_inject_files()
+ _set_admin_password()
return True
except Exception, exc:
LOG.warn(exc)
@@ -254,7 +278,8 @@ class VMOps(object):
instance_name = instance_or_vm.name
vm_ref = VMHelper.lookup(self._session, instance_name)
if vm_ref is None:
- raise exception.InstanceNotFound(instance_id=instance_obj.id)
+ raise exception.NotFound(_("No opaque_ref could be determined "
+ "for '%s'.") % instance_or_vm)
return vm_ref
def _acquire_bootlock(self, vm):
@@ -458,6 +483,9 @@ class VMOps(object):
# Successful return code from password is '0'
if resp_dict['returncode'] != '0':
raise RuntimeError(resp_dict['message'])
+ db.instance_update(context.get_admin_context(),
+ instance['id'],
+ dict(admin_pass=new_pass))
return resp_dict['message']
def inject_file(self, instance, path, contents):
@@ -1162,19 +1190,14 @@ class SimpleDH(object):
mpi = M2Crypto.m2.bn_to_mpi(bn)
return mpi
- def _run_ssl(self, text, which):
- base_cmd = ('cat %(tmpfile)s | openssl enc -aes-128-cbc '
- '-a -pass pass:%(shared)s -nosalt %(dec_flag)s')
- if which.lower()[0] == 'd':
- dec_flag = ' -d'
- else:
- dec_flag = ''
- fd, tmpfile = tempfile.mkstemp()
- os.close(fd)
- file(tmpfile, 'w').write(text)
- shared = self._shared
- cmd = base_cmd % locals()
- proc = _runproc(cmd)
+ def _run_ssl(self, text, extra_args=None):
+ if not extra_args:
+ extra_args = ''
+ cmd = 'enc -aes-128-cbc -A -a -pass pass:%s -nosalt %s' % (
+ self._shared, extra_args)
+ proc = _runproc('openssl %s' % cmd)
+ proc.stdin.write(text)
+ proc.stdin.close()
proc.wait()
err = proc.stderr.read()
if err:
@@ -1182,7 +1205,7 @@ class SimpleDH(object):
return proc.stdout.read()
def encrypt(self, text):
- return self._run_ssl(text, 'enc')
+ return self._run_ssl(text).strip('\n')
def decrypt(self, text):
- return self._run_ssl(text, 'dec')
+ return self._run_ssl(text, '-d')
diff --git a/nova/virt/xenapi/volume_utils.py b/nova/virt/xenapi/volume_utils.py
index 72284ac02..7821a4f7e 100644
--- a/nova/virt/xenapi/volume_utils.py
+++ b/nova/virt/xenapi/volume_utils.py
@@ -204,14 +204,17 @@ def _get_volume_id(path_or_id):
if isinstance(path_or_id, int):
return path_or_id
# n must contain at least the volume_id
- # /vol- is for remote volumes
- # -vol- is for local volumes
+ # :volume- is for remote volumes
+ # -volume- is for local volumes
# see compute/manager->setup_compute_volume
- volume_id = path_or_id[path_or_id.find('/vol-') + 1:]
+ volume_id = path_or_id[path_or_id.find(':volume-') + 1:]
if volume_id == path_or_id:
- volume_id = path_or_id[path_or_id.find('-vol-') + 1:]
- volume_id = volume_id.replace('--', '-')
- return volume_id
+ volume_id = path_or_id[path_or_id.find('-volume--') + 1:]
+ volume_id = volume_id.replace('volume--', '')
+ else:
+ volume_id = volume_id.replace('volume-', '')
+ volume_id = volume_id[0:volume_id.find('-')]
+ return int(volume_id)
def _get_target_host(iscsi_string):
@@ -244,25 +247,23 @@ def _get_target(volume_id):
Gets iscsi name and portal from volume name and host.
For this method to work the following are needed:
1) volume_ref['host'] must resolve to something rather than loopback
- 2) ietd must bind only to the address as resolved above
- If any of the two conditions are not met, fall back on Flags.
"""
- volume_ref = db.volume_get_by_ec2_id(context.get_admin_context(),
- volume_id)
+ volume_ref = db.volume_get(context.get_admin_context(),
+ volume_id)
result = (None, None)
try:
- (r, _e) = utils.execute("sudo iscsiadm -m discovery -t "
- "sendtargets -p %s" %
- volume_ref['host'])
+ (r, _e) = utils.execute('sudo', 'iscsiadm',
+ '-m', 'discovery',
+ '-t', 'sendtargets',
+ '-p', volume_ref['host'])
except exception.ProcessExecutionError, exc:
LOG.exception(exc)
else:
- targets = r.splitlines()
- if len(_e) == 0 and len(targets) == 1:
- for target in targets:
- if volume_id in target:
- (location, _sep, iscsi_name) = target.partition(" ")
- break
- iscsi_portal = location.split(",")[0]
- result = (iscsi_name, iscsi_portal)
+ volume_name = "volume-%08x" % volume_id
+ for target in r.splitlines():
+ if FLAGS.iscsi_ip_prefix in target and volume_name in target:
+ (location, _sep, iscsi_name) = target.partition(" ")
+ break
+ iscsi_portal = location.split(",")[0]
+ result = (iscsi_name, iscsi_portal)
return result
diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py
index eb572f295..6d828e109 100644
--- a/nova/virt/xenapi_conn.py
+++ b/nova/virt/xenapi_conn.py
@@ -169,15 +169,15 @@ class XenAPIConnection(driver.ComputeDriver):
def __init__(self, url, user, pw):
super(XenAPIConnection, self).__init__()
- session = XenAPISession(url, user, pw)
- self._vmops = VMOps(session)
- self._volumeops = VolumeOps(session)
+ self._session = XenAPISession(url, user, pw)
+ self._vmops = VMOps(self._session)
+ self._volumeops = VolumeOps(self._session)
self._host_state = None
@property
def HostState(self):
if not self._host_state:
- self._host_state = HostState(self.session)
+ self._host_state = HostState(self._session)
return self._host_state
def init_host(self, host):
diff --git a/nova/vnc/__init__.py b/nova/vnc/__init__.py
index b5b00e44e..859bfd65f 100644
--- a/nova/vnc/__init__.py
+++ b/nova/vnc/__init__.py
@@ -32,3 +32,5 @@ flags.DEFINE_string('vncserver_host', '0.0.0.0',
'the host interface on which vnc server should listen')
flags.DEFINE_bool('vnc_enabled', True,
'enable vnc related features')
+flags.DEFINE_string('vnc_keymap', 'en-us',
+ 'keymap for vnc')
diff --git a/nova/volume/api.py b/nova/volume/api.py
index 09befb647..5804955f7 100644
--- a/nova/volume/api.py
+++ b/nova/volume/api.py
@@ -39,7 +39,14 @@ LOG = logging.getLogger('nova.volume')
class API(base.Base):
"""API for interacting with the volume manager."""
- def create(self, context, size, name, description):
+ def create(self, context, size, snapshot_id, name, description):
+ if snapshot_id != None:
+ snapshot = self.get_snapshot(context, snapshot_id)
+ if snapshot['status'] != "available":
+ raise exception.ApiError(
+ _("Snapshot status must be available"))
+ size = snapshot['volume_size']
+
if quota.allowed_volumes(context, 1, size) < 1:
pid = context.project_id
LOG.warn(_("Quota exceeeded for %(pid)s, tried to create"
@@ -51,6 +58,7 @@ class API(base.Base):
'size': size,
'user_id': context.user_id,
'project_id': context.project_id,
+ 'snapshot_id': snapshot_id,
'availability_zone': FLAGS.storage_availability_zone,
'status': "creating",
'attach_status': "detached",
@@ -62,7 +70,8 @@ class API(base.Base):
FLAGS.scheduler_topic,
{"method": "create_volume",
"args": {"topic": FLAGS.volume_topic,
- "volume_id": volume['id']}})
+ "volume_id": volume['id'],
+ "snapshot_id": snapshot_id}})
return volume
def delete(self, context, volume_id):
@@ -90,6 +99,15 @@ class API(base.Base):
return self.db.volume_get_all(context)
return self.db.volume_get_all_by_project(context, context.project_id)
+ def get_snapshot(self, context, snapshot_id):
+ rv = self.db.snapshot_get(context, snapshot_id)
+ return dict(rv.iteritems())
+
+ def get_all_snapshots(self, context):
+ if context.is_admin:
+ return self.db.snapshot_get_all(context)
+ return self.db.snapshot_get_all_by_project(context, context.project_id)
+
def check_attach(self, context, volume_id):
volume = self.get(context, volume_id)
# TODO(vish): abstract status checking?
@@ -110,3 +128,38 @@ class API(base.Base):
self.db.queue_get_for(context, FLAGS.compute_topic, host),
{"method": "remove_volume",
"args": {'volume_id': volume_id}})
+
+ def create_snapshot(self, context, volume_id, name, description):
+ volume = self.get(context, volume_id)
+ if volume['status'] != "available":
+ raise exception.ApiError(_("Volume status must be available"))
+
+ options = {
+ 'volume_id': volume_id,
+ 'user_id': context.user_id,
+ 'project_id': context.project_id,
+ 'status': "creating",
+ 'progress': '0%',
+ 'volume_size': volume['size'],
+ 'display_name': name,
+ 'display_description': description}
+
+ snapshot = self.db.snapshot_create(context, options)
+ rpc.cast(context,
+ FLAGS.scheduler_topic,
+ {"method": "create_snapshot",
+ "args": {"topic": FLAGS.volume_topic,
+ "volume_id": volume_id,
+ "snapshot_id": snapshot['id']}})
+ return snapshot
+
+ def delete_snapshot(self, context, snapshot_id):
+ snapshot = self.get_snapshot(context, snapshot_id)
+ if snapshot['status'] != "available":
+ raise exception.ApiError(_("Snapshot status must be available"))
+ self.db.snapshot_update(context, snapshot_id, {'status': 'deleting'})
+ rpc.cast(context,
+ FLAGS.scheduler_topic,
+ {"method": "delete_snapshot",
+ "args": {"topic": FLAGS.volume_topic,
+ "snapshot_id": snapshot_id}})
diff --git a/nova/volume/driver.py b/nova/volume/driver.py
index 55307ad9b..87e13277f 100644
--- a/nova/volume/driver.py
+++ b/nova/volume/driver.py
@@ -90,42 +90,97 @@ class VolumeDriver(object):
raise exception.Error(_("volume group %s doesn't exist")
% FLAGS.volume_group)
- def create_volume(self, volume):
- """Creates a logical volume. Can optionally return a Dictionary of
- changes to the volume object to be persisted."""
- if int(volume['size']) == 0:
- sizestr = '100M'
- else:
- sizestr = '%sG' % volume['size']
+ def _create_volume(self, volume_name, sizestr):
self._try_execute('sudo', 'lvcreate', '-L', sizestr, '-n',
- volume['name'],
- FLAGS.volume_group)
+ volume_name, FLAGS.volume_group)
- def delete_volume(self, volume):
- """Deletes a logical volume."""
+ def _copy_volume(self, srcstr, deststr, size_in_g):
+ self._execute('sudo', 'dd', 'if=%s' % srcstr, 'of=%s' % deststr,
+ 'count=%d' % (size_in_g * 1024), 'bs=1M')
+
+ def _volume_not_present(self, volume_name):
+ path_name = '%s/%s' % (FLAGS.volume_group, volume_name)
try:
- self._try_execute('sudo', 'lvdisplay',
- '%s/%s' %
- (FLAGS.volume_group,
- volume['name']))
+ self._try_execute('sudo', 'lvdisplay', path_name)
except Exception as e:
- # If the volume isn't present, then don't attempt to delete
+ # If the volume isn't present
return True
+ return False
+ def _delete_volume(self, volume, size_in_g):
+ """Deletes a logical volume."""
# zero out old volumes to prevent data leaking between users
# TODO(ja): reclaiming space should be done lazy and low priority
- self._execute('sudo', 'dd', 'if=/dev/zero',
- 'of=%s' % self.local_path(volume),
- 'count=%d' % (volume['size'] * 1024),
- 'bs=1M')
+ self._copy_volume('/dev/zero', self.local_path(volume), size_in_g)
self._try_execute('sudo', 'lvremove', '-f', "%s/%s" %
(FLAGS.volume_group,
- volume['name']))
+ self._escape_snapshot(volume['name'])))
+
+ def _sizestr(self, size_in_g):
+ if int(size_in_g) == 0:
+ return '100M'
+ return '%sG' % size_in_g
+
+ # Linux LVM reserves name that starts with snapshot, so that
+ # such volume name can't be created. Mangle it.
+ def _escape_snapshot(self, snapshot_name):
+ if not snapshot_name.startswith('snapshot'):
+ return snapshot_name
+ return '_' + snapshot_name
+
+ def create_volume(self, volume):
+ """Creates a logical volume. Can optionally return a Dictionary of
+ changes to the volume object to be persisted."""
+ self._create_volume(volume['name'], self._sizestr(volume['size']))
+
+ def create_volume_from_snapshot(self, volume, snapshot):
+ """Creates a volume from a snapshot."""
+ self._create_volume(volume['name'], self._sizestr(volume['size']))
+ self._copy_volume(self.local_path(snapshot), self.local_path(volume),
+ snapshot['volume_size'])
+
+ def delete_volume(self, volume):
+ """Deletes a logical volume."""
+ if self._volume_not_present(volume['name']):
+ # If the volume isn't present, then don't attempt to delete
+ return True
+
+ # TODO(yamahata): lvm can't delete origin volume only without
+ # deleting derived snapshots. Can we do something fancy?
+ out, err = self._execute('sudo', 'lvdisplay', '--noheading',
+ '-C', '-o', 'Attr',
+ '%s/%s' % (FLAGS.volume_group,
+ volume['name']))
+ # fake_execute returns None resulting unit test error
+ if out:
+ out = out.strip()
+ if (out[0] == 'o') or (out[0] == 'O'):
+ raise exception.VolumeIsBusy(volume_name=volume['name'])
+
+ self._delete_volume(volume, volume['size'])
+
+ def create_snapshot(self, snapshot):
+ """Creates a snapshot."""
+ orig_lv_name = "%s/%s" % (FLAGS.volume_group, snapshot['volume_name'])
+ self._try_execute('sudo', 'lvcreate', '-L',
+ self._sizestr(snapshot['volume_size']),
+ '--name', self._escape_snapshot(snapshot['name']),
+ '--snapshot', orig_lv_name)
+
+ def delete_snapshot(self, snapshot):
+ """Deletes a snapshot."""
+ if self._volume_not_present(self._escape_snapshot(snapshot['name'])):
+ # If the snapshot isn't present, then don't attempt to delete
+ return True
+
+ # TODO(yamahata): zeroing out the whole snapshot triggers COW.
+ # it's quite slow.
+ self._delete_volume(snapshot, snapshot['volume_size'])
def local_path(self, volume):
# NOTE(vish): stops deprecation warning
escaped_group = FLAGS.volume_group.replace('-', '--')
- escaped_name = volume['name'].replace('-', '--')
+ escaped_name = self._escape_snapshot(volume['name']).replace('-', '--')
return "/dev/mapper/%s-%s" % (escaped_group, escaped_name)
def ensure_export(self, context, volume):
@@ -559,6 +614,18 @@ class RBDDriver(VolumeDriver):
self._try_execute('rbd', '--pool', FLAGS.rbd_pool,
'rm', volume['name'])
+ def create_snapshot(self, snapshot):
+ """Creates an rbd snapshot"""
+ self._try_execute('rbd', '--pool', FLAGS.rbd_pool,
+ 'snap', 'create', '--snap', snapshot['name'],
+ snapshot['volume_name'])
+
+ def delete_snapshot(self, snapshot):
+ """Deletes an rbd snapshot"""
+ self._try_execute('rbd', '--pool', FLAGS.rbd_pool,
+ 'snap', 'rm', '--snap', snapshot['name'],
+ snapshot['volume_name'])
+
def local_path(self, volume):
"""Returns the path of the rbd volume."""
# This is the same as the remote path
@@ -600,18 +667,31 @@ class SheepdogDriver(VolumeDriver):
def create_volume(self, volume):
"""Creates a sheepdog volume"""
- if int(volume['size']) == 0:
- sizestr = '100M'
- else:
- sizestr = '%sG' % volume['size']
self._try_execute('qemu-img', 'create',
"sheepdog:%s" % volume['name'],
- sizestr)
+ self._sizestr(volume['size']))
+
+ def create_volume_from_snapshot(self, volume, snapshot):
+ """Creates a sheepdog volume from a snapshot."""
+ self._try_execute('qemu-img', 'create', '-b',
+ "sheepdog:%s:%s" % (snapshot['volume_name'],
+ snapshot['name']),
+ "sheepdog:%s" % volume['name'])
def delete_volume(self, volume):
"""Deletes a logical volume"""
self._try_execute('collie', 'vdi', 'delete', volume['name'])
+ def create_snapshot(self, snapshot):
+ """Creates a sheepdog snapshot"""
+ self._try_execute('qemu-img', 'snapshot', '-c', snapshot['name'],
+ "sheepdog:%s" % snapshot['volume_name'])
+
+ def delete_snapshot(self, snapshot):
+ """Deletes a sheepdog snapshot"""
+ self._try_execute('collie', 'vdi', 'delete', snapshot['volume_name'],
+ '-s', snapshot['name'])
+
def local_path(self, volume):
return "sheepdog:%s" % volume['name']
diff --git a/nova/volume/manager.py b/nova/volume/manager.py
index 2178389ce..ff53f0701 100644
--- a/nova/volume/manager.py
+++ b/nova/volume/manager.py
@@ -90,7 +90,7 @@ class VolumeManager(manager.SchedulerDependentManager):
else:
LOG.info(_("volume %s: skipping export"), volume['name'])
- def create_volume(self, context, volume_id):
+ def create_volume(self, context, volume_id, snapshot_id=None):
"""Creates and exports the volume."""
context = context.elevated()
volume_ref = self.db.volume_get(context, volume_id)
@@ -108,7 +108,13 @@ class VolumeManager(manager.SchedulerDependentManager):
vol_size = volume_ref['size']
LOG.debug(_("volume %(vol_name)s: creating lv of"
" size %(vol_size)sG") % locals())
- model_update = self.driver.create_volume(volume_ref)
+ if snapshot_id == None:
+ model_update = self.driver.create_volume(volume_ref)
+ else:
+ snapshot_ref = self.db.snapshot_get(context, snapshot_id)
+ model_update = self.driver.create_volume_from_snapshot(
+ volume_ref,
+ snapshot_ref)
if model_update:
self.db.volume_update(context, volume_ref['id'], model_update)
@@ -142,6 +148,12 @@ class VolumeManager(manager.SchedulerDependentManager):
self.driver.remove_export(context, volume_ref)
LOG.debug(_("volume %s: deleting"), volume_ref['name'])
self.driver.delete_volume(volume_ref)
+ except exception.VolumeIsBusy, e:
+ LOG.debug(_("volume %s: volume is busy"), volume_ref['name'])
+ self.driver.ensure_export(context, volume_ref)
+ self.db.volume_update(context, volume_ref['id'],
+ {'status': 'available'})
+ return True
except Exception:
self.db.volume_update(context,
volume_ref['id'],
@@ -152,6 +164,49 @@ class VolumeManager(manager.SchedulerDependentManager):
LOG.debug(_("volume %s: deleted successfully"), volume_ref['name'])
return True
+ def create_snapshot(self, context, volume_id, snapshot_id):
+ """Creates and exports the snapshot."""
+ context = context.elevated()
+ snapshot_ref = self.db.snapshot_get(context, snapshot_id)
+ LOG.info(_("snapshot %s: creating"), snapshot_ref['name'])
+
+ try:
+ snap_name = snapshot_ref['name']
+ LOG.debug(_("snapshot %(snap_name)s: creating") % locals())
+ model_update = self.driver.create_snapshot(snapshot_ref)
+ if model_update:
+ self.db.snapshot_update(context, snapshot_ref['id'],
+ model_update)
+
+ except Exception:
+ self.db.snapshot_update(context,
+ snapshot_ref['id'], {'status': 'error'})
+ raise
+
+ self.db.snapshot_update(context,
+ snapshot_ref['id'], {'status': 'available',
+ 'progress': '100%'})
+ LOG.debug(_("snapshot %s: created successfully"), snapshot_ref['name'])
+ return snapshot_id
+
+ def delete_snapshot(self, context, snapshot_id):
+ """Deletes and unexports snapshot."""
+ context = context.elevated()
+ snapshot_ref = self.db.snapshot_get(context, snapshot_id)
+
+ try:
+ LOG.debug(_("snapshot %s: deleting"), snapshot_ref['name'])
+ self.driver.delete_snapshot(snapshot_ref)
+ except Exception:
+ self.db.snapshot_update(context,
+ snapshot_ref['id'],
+ {'status': 'error_deleting'})
+ raise
+
+ self.db.snapshot_destroy(context, snapshot_id)
+ LOG.debug(_("snapshot %s: deleted successfully"), snapshot_ref['name'])
+ return True
+
def setup_compute_volume(self, context, volume_id):
"""Setup remote volume on compute host.
diff --git a/nova/wsgi.py b/nova/wsgi.py
index e60a8820d..ea9bb963d 100644
--- a/nova/wsgi.py
+++ b/nova/wsgi.py
@@ -59,13 +59,16 @@ class Server(object):
def __init__(self, threads=1000):
self.pool = eventlet.GreenPool(threads)
+ self.socket_info = {}
- def start(self, application, port, host='0.0.0.0', backlog=128):
+ def start(self, application, port, host='0.0.0.0', key=None, backlog=128):
"""Run a WSGI server with the given application."""
arg0 = sys.argv[0]
logging.audit(_('Starting %(arg0)s on %(host)s:%(port)s') % locals())
socket = eventlet.listen((host, port), backlog=backlog)
self.pool.spawn_n(self._run, application, socket)
+ if key:
+ self.socket_info[key] = socket.getsockname()
def wait(self):
"""Wait until all servers have completed running."""