summaryrefslogtreecommitdiffstats
path: root/nova
diff options
context:
space:
mode:
authorJustin Santa Barbara <justin@fathomdb.com>2011-03-24 10:59:25 -0700
committerJustin Santa Barbara <justin@fathomdb.com>2011-03-24 10:59:25 -0700
commit038a629cb7cb61a58838c3fc91a204ca2892dbed (patch)
tree33e2a01e91599bb2ce98040e93139c709b826066 /nova
parenta1e2959312b51757653447de3e8c9e92029da6fd (diff)
parent5b1abbb34c0a35d7d6d142ae9afd2cde74b1782e (diff)
downloadnova-038a629cb7cb61a58838c3fc91a204ca2892dbed.tar.gz
nova-038a629cb7cb61a58838c3fc91a204ca2892dbed.tar.xz
nova-038a629cb7cb61a58838c3fc91a204ca2892dbed.zip
Merged with trunk
Diffstat (limited to 'nova')
-rw-r--r--nova/api/ec2/__init__.py6
-rw-r--r--nova/api/ec2/admin.py19
-rw-r--r--nova/api/ec2/cloud.py2
-rw-r--r--nova/api/openstack/__init__.py46
-rw-r--r--nova/api/openstack/common.py17
-rw-r--r--nova/api/openstack/faults.py39
-rw-r--r--nova/api/openstack/flavors.py12
-rw-r--r--nova/api/openstack/images.py6
-rw-r--r--nova/api/openstack/limits.py358
-rw-r--r--nova/api/openstack/servers.py148
-rw-r--r--nova/api/openstack/users.py17
-rw-r--r--nova/api/openstack/views/__init__.py0
-rw-r--r--nova/api/openstack/views/addresses.py42
-rw-r--r--nova/api/openstack/views/flavors.py34
-rw-r--r--nova/api/openstack/views/images.py34
-rw-r--r--nova/api/openstack/views/servers.py125
-rw-r--r--nova/api/openstack/zones.py16
-rw-r--r--nova/auth/dbdriver.py2
-rw-r--r--nova/auth/fakeldap.py10
-rw-r--r--nova/auth/ldapdriver.py4
-rw-r--r--nova/auth/manager.py13
-rw-r--r--nova/compute/api.py42
-rw-r--r--nova/compute/manager.py173
-rw-r--r--nova/compute/power_state.py18
-rw-r--r--nova/crypto.py3
-rw-r--r--nova/db/api.py6
-rw-r--r--nova/db/base.py2
-rw-r--r--nova/db/sqlalchemy/api.py30
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/008_add_instance_types.py6
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/012_add_ipv6_flatmanager.py154
-rw-r--r--nova/db/sqlalchemy/migrate_repo/versions/013_add_flavors_to_migrations.py50
-rw-r--r--nova/db/sqlalchemy/models.py9
-rw-r--r--nova/exception.py2
-rw-r--r--nova/flags.py5
-rw-r--r--nova/image/glance.py36
-rw-r--r--nova/image/local.py17
-rw-r--r--nova/image/service.py16
-rw-r--r--nova/manager.py31
-rw-r--r--nova/network/linux_net.py53
-rw-r--r--nova/network/manager.py21
-rw-r--r--nova/objectstore/handler.py14
-rw-r--r--nova/rpc.py87
-rw-r--r--nova/scheduler/api.py59
-rw-r--r--nova/scheduler/driver.py7
-rw-r--r--nova/scheduler/manager.py14
-rw-r--r--nova/scheduler/zone_manager.py32
-rw-r--r--nova/service.py12
-rw-r--r--nova/tests/api/openstack/__init__.py2
-rw-r--r--nova/tests/api/openstack/fakes.py94
-rw-r--r--nova/tests/api/openstack/test_accounts.py26
-rw-r--r--nova/tests/api/openstack/test_adminapi.py1
-rw-r--r--nova/tests/api/openstack/test_auth.py30
-rw-r--r--nova/tests/api/openstack/test_flavors.py71
-rw-r--r--nova/tests/api/openstack/test_images.py56
-rw-r--r--nova/tests/api/openstack/test_limits.py584
-rw-r--r--nova/tests/api/openstack/test_ratelimiting.py243
-rw-r--r--nova/tests/api/openstack/test_servers.py152
-rw-r--r--nova/tests/api/openstack/test_users.py68
-rw-r--r--nova/tests/api/openstack/test_zones.py29
-rw-r--r--nova/tests/api/test_wsgi.py2
-rw-r--r--nova/tests/db/fakes.py30
-rw-r--r--nova/tests/hyperv_unittest.py2
-rw-r--r--nova/tests/image/__init__.py16
-rw-r--r--nova/tests/image/test_glance.py188
-rw-r--r--nova/tests/integrated/api/client.py4
-rw-r--r--nova/tests/integrated/integrated_helpers.py188
-rw-r--r--nova/tests/integrated/test_login.py79
-rw-r--r--nova/tests/network/__init__.py67
-rw-r--r--nova/tests/network/base.py154
-rw-r--r--nova/tests/objectstore_unittest.py10
-rw-r--r--nova/tests/test_api.py14
-rw-r--r--nova/tests/test_auth.py7
-rw-r--r--nova/tests/test_compute.py94
-rw-r--r--nova/tests/test_flat_network.py161
-rw-r--r--nova/tests/test_localization.py3
-rw-r--r--nova/tests/test_middleware.py4
-rw-r--r--nova/tests/test_misc.py43
-rw-r--r--nova/tests/test_network.py371
-rw-r--r--nova/tests/test_rpc.py4
-rw-r--r--nova/tests/test_service.py24
-rw-r--r--nova/tests/test_test.py2
-rw-r--r--nova/tests/test_utils.py78
-rw-r--r--nova/tests/test_virt.py17
-rw-r--r--nova/tests/test_vlan_network.py242
-rw-r--r--nova/tests/test_volume.py9
-rw-r--r--nova/tests/test_xenapi.py8
-rw-r--r--nova/tests/test_zones.py34
-rw-r--r--nova/tests/xenapi/stubs.py7
-rw-r--r--nova/utils.py116
-rw-r--r--nova/virt/connection.py4
-rw-r--r--nova/virt/driver.py234
-rw-r--r--nova/virt/fake.py39
-rw-r--r--nova/virt/hyperv.py22
-rw-r--r--nova/virt/interfaces.template16
-rw-r--r--nova/virt/libvirt.xml.template4
-rw-r--r--nova/virt/libvirt_conn.py180
-rw-r--r--nova/virt/xenapi/vm_utils.py10
-rw-r--r--nova/virt/xenapi/vmops.py292
-rw-r--r--nova/virt/xenapi_conn.py23
-rw-r--r--nova/volume/driver.py17
-rw-r--r--nova/volume/manager.py5
101 files changed, 4718 insertions, 1311 deletions
diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py
index fccebca5d..20701cfa8 100644
--- a/nova/api/ec2/__init__.py
+++ b/nova/api/ec2/__init__.py
@@ -31,7 +31,7 @@ from nova import log as logging
from nova import utils
from nova import wsgi
from nova.api.ec2 import apirequest
-from nova.api.ec2 import cloud
+from nova.api.ec2 import ec2utils
from nova.auth import manager
@@ -319,13 +319,13 @@ class Executor(wsgi.Application):
except exception.InstanceNotFound as ex:
LOG.info(_('InstanceNotFound raised: %s'), unicode(ex),
context=context)
- ec2_id = cloud.id_to_ec2_id(ex.instance_id)
+ ec2_id = ec2utils.id_to_ec2_id(ex.instance_id)
message = _('Instance %s not found') % ec2_id
return self._error(req, context, type(ex).__name__, message)
except exception.VolumeNotFound as ex:
LOG.info(_('VolumeNotFound raised: %s'), unicode(ex),
context=context)
- ec2_id = cloud.id_to_ec2_id(ex.volume_id, 'vol-%08x')
+ 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.NotFound as ex:
diff --git a/nova/api/ec2/admin.py b/nova/api/ec2/admin.py
index d9a4ef999..d8d90ad83 100644
--- a/nova/api/ec2/admin.py
+++ b/nova/api/ec2/admin.py
@@ -28,6 +28,7 @@ from nova import exception
from nova import flags
from nova import log as logging
from nova import utils
+from nova.api.ec2 import ec2utils
from nova.auth import manager
@@ -60,7 +61,7 @@ def project_dict(project):
def host_dict(host, compute_service, instances, volume_service, volumes, now):
"""Convert a host model object to a result dict"""
- rv = {'hostanme': host, 'instance_count': len(instances),
+ rv = {'hostname': host, 'instance_count': len(instances),
'volume_count': len(volumes)}
if compute_service:
latest = compute_service['updated_at'] or compute_service['created_at']
@@ -92,15 +93,18 @@ def vpn_dict(project, vpn_instance):
'public_ip': project.vpn_ip,
'public_port': project.vpn_port}
if vpn_instance:
- rv['instance_id'] = vpn_instance['ec2_id']
+ rv['instance_id'] = ec2utils.id_to_ec2_id(vpn_instance['id'])
rv['created_at'] = utils.isotime(vpn_instance['created_at'])
address = vpn_instance.get('fixed_ip', None)
if address:
rv['internal_ip'] = address['address']
- if utils.vpn_ping(project.vpn_ip, project.vpn_port):
- rv['state'] = 'running'
+ if project.vpn_ip and project.vpn_port:
+ if utils.vpn_ping(project.vpn_ip, project.vpn_port):
+ rv['state'] = 'running'
+ else:
+ rv['state'] = 'down'
else:
- rv['state'] = 'down'
+ rv['state'] = 'down - invalid project vpn config'
else:
rv['state'] = 'pending'
return rv
@@ -116,7 +120,8 @@ class AdminController(object):
def describe_instance_types(self, context, **_kwargs):
"""Returns all active instance types data (vcpus, memory, etc.)"""
- return {'instanceTypeSet': [db.instance_type_get_all(context)]}
+ return {'instanceTypeSet': [instance_dict(v) for v in
+ db.instance_type_get_all(context).values()]}
def describe_user(self, _context, name, **_kwargs):
"""Returns user data, including access and secret keys."""
@@ -279,7 +284,7 @@ class AdminController(object):
", ensure it isn't running, and try "
"again in a few minutes")
instance = self._vpn_for(context, project)
- return {'instance_id': instance['ec2_id']}
+ return {'instance_id': ec2utils.id_to_ec2_id(instance['id'])}
def describe_vpns(self, context):
vpns = []
diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py
index 40a9da0e7..e257e44e7 100644
--- a/nova/api/ec2/cloud.py
+++ b/nova/api/ec2/cloud.py
@@ -959,7 +959,7 @@ class CloudController(object):
raise exception.NotFound(_('Image %s not found') % image_id)
internal_id = image['id']
del(image['id'])
- raise Exception(image)
+
image['properties']['is_public'] = (operation_type == 'add')
return self.image_service.update(context, internal_id, image)
diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py
index ce3cff337..143b1d2b2 100644
--- a/nova/api/openstack/__init__.py
+++ b/nova/api/openstack/__init__.py
@@ -33,6 +33,7 @@ from nova.api.openstack import backup_schedules
from nova.api.openstack import consoles
from nova.api.openstack import flavors
from nova.api.openstack import images
+from nova.api.openstack import limits
from nova.api.openstack import servers
from nova.api.openstack import shared_ip_groups
from nova.api.openstack import users
@@ -71,9 +72,14 @@ class APIRouter(wsgi.Router):
return cls()
def __init__(self):
+ self.server_members = {}
mapper = routes.Mapper()
+ self._setup_routes(mapper)
+ super(APIRouter, self).__init__(mapper)
- server_members = {'action': 'POST'}
+ def _setup_routes(self, mapper):
+ server_members = self.server_members
+ server_members['action'] = 'POST'
if FLAGS.allow_admin_api:
LOG.debug(_("Including admin operations in API."))
@@ -98,10 +104,6 @@ class APIRouter(wsgi.Router):
controller=accounts.Controller(),
collection={'detail': 'GET'})
- mapper.resource("server", "servers", controller=servers.Controller(),
- collection={'detail': 'GET'},
- member=server_members)
-
mapper.resource("backup_schedule", "backup_schedule",
controller=backup_schedules.Controller(),
parent_resource=dict(member_name='server',
@@ -114,13 +116,38 @@ class APIRouter(wsgi.Router):
mapper.resource("image", "images", controller=images.Controller(),
collection={'detail': 'GET'})
+
mapper.resource("flavor", "flavors", controller=flavors.Controller(),
collection={'detail': 'GET'})
+
mapper.resource("shared_ip_group", "shared_ip_groups",
collection={'detail': 'GET'},
controller=shared_ip_groups.Controller())
- super(APIRouter, self).__init__(mapper)
+ _limits = limits.LimitsController()
+ mapper.resource("limit", "limits", controller=_limits)
+
+
+class APIRouterV10(APIRouter):
+ """Define routes specific to OpenStack API V1.0."""
+
+ def _setup_routes(self, mapper):
+ super(APIRouterV10, self)._setup_routes(mapper)
+ mapper.resource("server", "servers",
+ controller=servers.ControllerV10(),
+ collection={'detail': 'GET'},
+ member=self.server_members)
+
+
+class APIRouterV11(APIRouter):
+ """Define routes specific to OpenStack API V1.1."""
+
+ def _setup_routes(self, mapper):
+ super(APIRouterV11, self)._setup_routes(mapper)
+ mapper.resource("server", "servers",
+ controller=servers.ControllerV11(),
+ collection={'detail': 'GET'},
+ member=self.server_members)
class Versions(wsgi.Application):
@@ -128,8 +155,11 @@ class Versions(wsgi.Application):
def __call__(self, req):
"""Respond to a request for all OpenStack API versions."""
response = {
- "versions": [
- dict(status="CURRENT", id="v1.0")]}
+ "versions": [
+ dict(status="DEPRECATED", id="v1.0"),
+ dict(status="CURRENT", id="v1.1"),
+ ],
+ }
metadata = {
"application/xml": {
"attributes": dict(version=["status", "id"])}}
diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py
index 74ac21024..bff050347 100644
--- a/nova/api/openstack/common.py
+++ b/nova/api/openstack/common.py
@@ -15,7 +15,9 @@
# License for the specific language governing permissions and limitations
# under the License.
-import webob.exc
+from urlparse import urlparse
+
+import webob
from nova import exception
@@ -74,3 +76,16 @@ def get_image_id_from_image_hash(image_service, context, image_hash):
if abs(hash(image_id)) == int(image_hash):
return image_id
raise exception.NotFound(image_hash)
+
+
+def get_id_from_href(href):
+ """Return the id portion of a url as an int.
+
+ Given: http://www.foo.com/bar/123?q=4
+ Returns: 123
+
+ """
+ try:
+ return int(urlparse(href).path.split('/')[-1])
+ except:
+ raise webob.exc.HTTPBadRequest(_('could not parse id from href'))
diff --git a/nova/api/openstack/faults.py b/nova/api/openstack/faults.py
index 2fd733299..0e9c4b26f 100644
--- a/nova/api/openstack/faults.py
+++ b/nova/api/openstack/faults.py
@@ -61,3 +61,42 @@ class Fault(webob.exc.HTTPException):
content_type = req.best_match_content_type()
self.wrapped_exc.body = serializer.serialize(fault_data, content_type)
return self.wrapped_exc
+
+
+class OverLimitFault(webob.exc.HTTPException):
+ """
+ Rate-limited request response.
+ """
+
+ _serialization_metadata = {
+ "application/xml": {
+ "attributes": {
+ "overLimitFault": "code",
+ },
+ },
+ }
+
+ def __init__(self, message, details, retry_time):
+ """
+ Initialize new `OverLimitFault` with relevant information.
+ """
+ self.wrapped_exc = webob.exc.HTTPForbidden()
+ self.content = {
+ "overLimitFault": {
+ "code": self.wrapped_exc.status_int,
+ "message": message,
+ "details": details,
+ },
+ }
+
+ @webob.dec.wsgify(RequestClass=wsgi.Request)
+ def __call__(self, request):
+ """
+ Return the wrapped exception with a serialized body conforming to our
+ error format.
+ """
+ serializer = wsgi.Serializer(self._serialization_metadata)
+ content_type = request.best_match_content_type()
+ content = serializer.serialize(self.content, content_type)
+ self.wrapped_exc.body = content
+ return self.wrapped_exc
diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py
index f3d040ba3..c99b945fb 100644
--- a/nova/api/openstack/flavors.py
+++ b/nova/api/openstack/flavors.py
@@ -22,6 +22,7 @@ from nova import context
from nova.api.openstack import faults
from nova.api.openstack import common
from nova.compute import instance_types
+from nova.api.openstack.views import flavors as flavors_views
from nova import wsgi
import nova.api.openstack
@@ -47,13 +48,18 @@ class Controller(wsgi.Controller):
def show(self, req, id):
"""Return data about the given flavor id."""
ctxt = req.environ['nova.context']
- values = db.instance_type_get_by_flavor_id(ctxt, id)
+ flavor = db.api.instance_type_get_by_flavor_id(ctxt, id)
+ values = {
+ "id": flavor["flavorid"],
+ "name": flavor["name"],
+ "ram": flavor["memory_mb"],
+ "disk": flavor["local_gb"],
+ }
return dict(flavor=values)
- raise faults.Fault(exc.HTTPNotFound())
def _all_ids(self, req):
"""Return the list of all flavorids."""
ctxt = req.environ['nova.context']
- inst_types = db.instance_type_get_all(ctxt)
+ inst_types = db.api.instance_type_get_all(ctxt)
flavor_ids = [inst_types[i]['flavorid'] for i in inst_types.keys()]
return sorted(flavor_ids)
diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py
index 98f0dd96b..99c14275a 100644
--- a/nova/api/openstack/images.py
+++ b/nova/api/openstack/images.py
@@ -143,6 +143,7 @@ class Controller(wsgi.Controller):
image = self._service.show(req.environ['nova.context'], image_id)
_convert_image_id_to_hash(image)
+ self._format_image_dates(image)
return dict(image=image)
def delete(self, req, id):
@@ -164,3 +165,8 @@ class Controller(wsgi.Controller):
# Users may not modify public images, and that's all that
# we support for now.
raise faults.Fault(exc.HTTPNotFound())
+
+ def _format_image_dates(self, image):
+ for attr in ['created_at', 'updated_at', 'deleted_at']:
+ if image.get(attr) is not None:
+ image[attr] = image[attr].strftime('%Y-%m-%dT%H:%M:%SZ')
diff --git a/nova/api/openstack/limits.py b/nova/api/openstack/limits.py
new file mode 100644
index 000000000..efc7d193d
--- /dev/null
+++ b/nova/api/openstack/limits.py
@@ -0,0 +1,358 @@
+# 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
+
+"""
+Module dedicated functions/classes dealing with rate limiting requests.
+"""
+
+import copy
+import httplib
+import json
+import math
+import re
+import time
+import urllib
+import webob.exc
+
+from collections import defaultdict
+
+from webob.dec import wsgify
+
+from nova import wsgi
+from nova.api.openstack import faults
+from nova.wsgi import Controller
+from nova.wsgi import Middleware
+
+
+# Convenience constants for the limits dictionary passed to Limiter().
+PER_SECOND = 1
+PER_MINUTE = 60
+PER_HOUR = 60 * 60
+PER_DAY = 60 * 60 * 24
+
+
+class LimitsController(Controller):
+ """
+ Controller for accessing limits in the OpenStack API.
+ """
+
+ _serialization_metadata = {
+ "application/xml": {
+ "attributes": {
+ "limit": ["verb", "URI", "regex", "value", "unit",
+ "resetTime", "remaining", "name"],
+ },
+ "plurals": {
+ "rate": "limit",
+ },
+ },
+ }
+
+ def index(self, req):
+ """
+ Return all global and rate limit information.
+ """
+ abs_limits = {}
+ rate_limits = req.environ.get("nova.limits", [])
+
+ return {
+ "limits": {
+ "rate": rate_limits,
+ "absolute": abs_limits,
+ },
+ }
+
+
+class Limit(object):
+ """
+ Stores information about a limit for HTTP requets.
+ """
+
+ UNITS = {
+ 1: "SECOND",
+ 60: "MINUTE",
+ 60 * 60: "HOUR",
+ 60 * 60 * 24: "DAY",
+ }
+
+ def __init__(self, verb, uri, regex, value, unit):
+ """
+ Initialize a new `Limit`.
+
+ @param verb: HTTP verb (POST, PUT, etc.)
+ @param uri: Human-readable URI
+ @param regex: Regular expression format for this limit
+ @param value: Integer number of requests which can be made
+ @param unit: Unit of measure for the value parameter
+ """
+ self.verb = verb
+ self.uri = uri
+ self.regex = regex
+ self.value = int(value)
+ self.unit = unit
+ self.unit_string = self.display_unit().lower()
+ self.remaining = int(value)
+
+ if value <= 0:
+ raise ValueError("Limit value must be > 0")
+
+ self.last_request = None
+ self.next_request = None
+
+ self.water_level = 0
+ self.capacity = self.unit
+ self.request_value = float(self.capacity) / float(self.value)
+ self.error_message = _("Only %(value)s %(verb)s request(s) can be "\
+ "made to %(uri)s every %(unit_string)s." % self.__dict__)
+
+ def __call__(self, verb, url):
+ """
+ Represents a call to this limit from a relevant request.
+
+ @param verb: string http verb (POST, GET, etc.)
+ @param url: string URL
+ """
+ if self.verb != verb or not re.match(self.regex, url):
+ return
+
+ now = self._get_time()
+
+ if self.last_request is None:
+ self.last_request = now
+
+ leak_value = now - self.last_request
+
+ self.water_level -= leak_value
+ self.water_level = max(self.water_level, 0)
+ self.water_level += self.request_value
+
+ difference = self.water_level - self.capacity
+
+ self.last_request = now
+
+ if difference > 0:
+ self.water_level -= self.request_value
+ self.next_request = now + difference
+ return difference
+
+ cap = self.capacity
+ water = self.water_level
+ val = self.value
+
+ self.remaining = math.floor(((cap - water) / cap) * val)
+ self.next_request = now
+
+ def _get_time(self):
+ """Retrieve the current time. Broken out for testability."""
+ return time.time()
+
+ def display_unit(self):
+ """Display the string name of the unit."""
+ return self.UNITS.get(self.unit, "UNKNOWN")
+
+ def display(self):
+ """Return a useful representation of this class."""
+ return {
+ "verb": self.verb,
+ "URI": self.uri,
+ "regex": self.regex,
+ "value": self.value,
+ "remaining": int(self.remaining),
+ "unit": self.display_unit(),
+ "resetTime": int(self.next_request or self._get_time()),
+ }
+
+# "Limit" format is a dictionary with the HTTP verb, human-readable URI,
+# a regular-expression to match, value and unit of measure (PER_DAY, etc.)
+
+DEFAULT_LIMITS = [
+ Limit("POST", "*", ".*", 10, PER_MINUTE),
+ Limit("POST", "*/servers", "^/servers", 50, PER_DAY),
+ Limit("PUT", "*", ".*", 10, PER_MINUTE),
+ Limit("GET", "*changes-since*", ".*changes-since.*", 3, PER_MINUTE),
+ Limit("DELETE", "*", ".*", 100, PER_MINUTE),
+]
+
+
+class RateLimitingMiddleware(Middleware):
+ """
+ Rate-limits requests passing through this middleware. All limit information
+ is stored in memory for this implementation.
+ """
+
+ def __init__(self, application, limits=None):
+ """
+ Initialize new `RateLimitingMiddleware`, which wraps the given WSGI
+ application and sets up the given limits.
+
+ @param application: WSGI application to wrap
+ @param limits: List of dictionaries describing limits
+ """
+ Middleware.__init__(self, application)
+ self._limiter = Limiter(limits or DEFAULT_LIMITS)
+
+ @wsgify(RequestClass=wsgi.Request)
+ def __call__(self, req):
+ """
+ Represents a single call through this middleware. We should record the
+ request if we have a limit relevant to it. If no limit is relevant to
+ the request, ignore it.
+
+ If the request should be rate limited, return a fault telling the user
+ they are over the limit and need to retry later.
+ """
+ verb = req.method
+ url = req.url
+ context = req.environ.get("nova.context")
+
+ if context:
+ username = context.user_id
+ else:
+ username = None
+
+ delay, error = self._limiter.check_for_delay(verb, url, username)
+
+ if delay:
+ msg = _("This request was rate-limited.")
+ retry = time.time() + delay
+ return faults.OverLimitFault(msg, error, retry)
+
+ req.environ["nova.limits"] = self._limiter.get_limits(username)
+
+ return self.application
+
+
+class Limiter(object):
+ """
+ Rate-limit checking class which handles limits in memory.
+ """
+
+ def __init__(self, limits):
+ """
+ Initialize the new `Limiter`.
+
+ @param limits: List of `Limit` objects
+ """
+ self.limits = copy.deepcopy(limits)
+ self.levels = defaultdict(lambda: copy.deepcopy(limits))
+
+ def get_limits(self, username=None):
+ """
+ Return the limits for a given user.
+ """
+ return [limit.display() for limit in self.levels[username]]
+
+ def check_for_delay(self, verb, url, username=None):
+ """
+ Check the given verb/user/user triplet for limit.
+
+ @return: Tuple of delay (in seconds) and error message (or None, None)
+ """
+ delays = []
+
+ for limit in self.levels[username]:
+ delay = limit(verb, url)
+ if delay:
+ delays.append((delay, limit.error_message))
+
+ if delays:
+ delays.sort()
+ return delays[0]
+
+ return None, None
+
+
+class WsgiLimiter(object):
+ """
+ Rate-limit checking from a WSGI application. Uses an in-memory `Limiter`.
+
+ To use:
+ POST /<username> with JSON data such as:
+ {
+ "verb" : GET,
+ "path" : "/servers"
+ }
+
+ and receive a 204 No Content, or a 403 Forbidden with an X-Wait-Seconds
+ header containing the number of seconds to wait before the action would
+ succeed.
+ """
+
+ def __init__(self, limits=None):
+ """
+ Initialize the new `WsgiLimiter`.
+
+ @param limits: List of `Limit` objects
+ """
+ self._limiter = Limiter(limits or DEFAULT_LIMITS)
+
+ @wsgify(RequestClass=wsgi.Request)
+ def __call__(self, request):
+ """
+ Handles a call to this application. Returns 204 if the request is
+ acceptable to the limiter, else a 403 is returned with a relevant
+ header indicating when the request *will* succeed.
+ """
+ if request.method != "POST":
+ raise webob.exc.HTTPMethodNotAllowed()
+
+ try:
+ info = dict(json.loads(request.body))
+ except ValueError:
+ raise webob.exc.HTTPBadRequest()
+
+ username = request.path_info_pop()
+ verb = info.get("verb")
+ path = info.get("path")
+
+ delay, error = self._limiter.check_for_delay(verb, path, username)
+
+ if delay:
+ headers = {"X-Wait-Seconds": "%.2f" % delay}
+ return webob.exc.HTTPForbidden(headers=headers, explanation=error)
+ else:
+ return webob.exc.HTTPNoContent()
+
+
+class WsgiLimiterProxy(object):
+ """
+ Rate-limit requests based on answers from a remote source.
+ """
+
+ def __init__(self, limiter_address):
+ """
+ Initialize the new `WsgiLimiterProxy`.
+
+ @param limiter_address: IP/port combination of where to request limit
+ """
+ self.limiter_address = limiter_address
+
+ def check_for_delay(self, verb, path, username=None):
+ body = json.dumps({"verb": verb, "path": path})
+ headers = {"Content-Type": "application/json"}
+
+ conn = httplib.HTTPConnection(self.limiter_address)
+
+ if username:
+ conn.request("POST", "/%s" % (username), body, headers)
+ else:
+ conn.request("POST", "/", body, headers)
+
+ resp = conn.getresponse()
+
+ if 200 >= resp.status < 300:
+ return None, None
+
+ return resp.getheader("X-Wait-Seconds"), resp.read() or None
diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py
index 42cf693de..1b4f28f59 100644
--- a/nova/api/openstack/servers.py
+++ b/nova/api/openstack/servers.py
@@ -21,6 +21,7 @@ from webob import exc
from xml.dom import minidom
from nova import compute
+from nova import context
from nova import exception
from nova import flags
from nova import log as logging
@@ -29,69 +30,19 @@ from nova import utils
from nova import wsgi
from nova.api.openstack import common
from nova.api.openstack import faults
+import nova.api.openstack.views.addresses
+import nova.api.openstack.views.flavors
+import nova.api.openstack.views.servers
from nova.auth import manager as auth_manager
from nova.compute import instance_types
from nova.compute import power_state
+import nova.api.openstack
LOG = logging.getLogger('server')
-
-
FLAGS = flags.FLAGS
-def _translate_detail_keys(inst):
- """ Coerces into dictionary format, mapping everything to Rackspace-like
- attributes for return"""
- power_mapping = {
- None: 'build',
- power_state.NOSTATE: 'build',
- power_state.RUNNING: 'active',
- power_state.BLOCKED: 'active',
- power_state.SUSPENDED: 'suspended',
- power_state.PAUSED: 'paused',
- power_state.SHUTDOWN: 'active',
- power_state.SHUTOFF: 'active',
- power_state.CRASHED: 'error',
- power_state.FAILED: 'error'}
- inst_dict = {}
-
- mapped_keys = dict(status='state', imageId='image_id',
- flavorId='instance_type', name='display_name', id='id')
-
- for k, v in mapped_keys.iteritems():
- inst_dict[k] = inst[v]
-
- inst_dict['status'] = power_mapping[inst_dict['status']]
- inst_dict['addresses'] = dict(public=[], private=[])
-
- # grab single private fixed ip
- private_ips = utils.get_from_path(inst, 'fixed_ip/address')
- inst_dict['addresses']['private'] = private_ips
-
- # grab all public floating ips
- public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address')
- inst_dict['addresses']['public'] = public_ips
-
- # Return the metadata as a dictionary
- metadata = {}
- for item in inst['metadata']:
- metadata[item['key']] = item['value']
- inst_dict['metadata'] = metadata
-
- inst_dict['hostId'] = ''
- if inst['host']:
- inst_dict['hostId'] = hashlib.sha224(inst['host']).hexdigest()
-
- return dict(server=inst_dict)
-
-
-def _translate_keys(inst):
- """ Coerces into dictionary format, excluding all model attributes
- save for id and name """
- return dict(server=dict(id=inst['id'], name=inst['display_name']))
-
-
class Controller(wsgi.Controller):
""" The Server API controller for the OpenStack API """
@@ -99,36 +50,49 @@ class Controller(wsgi.Controller):
'application/xml': {
"attributes": {
"server": ["id", "imageId", "name", "flavorId", "hostId",
- "status", "progress", "adminPass"]}}}
+ "status", "progress", "adminPass", "flavorRef",
+ "imageRef"]}}}
def __init__(self):
self.compute_api = compute.API()
self._image_service = utils.import_object(FLAGS.image_service)
super(Controller, self).__init__()
+ def ips(self, req, id):
+ try:
+ instance = self.compute_api.get(req.environ['nova.context'], id)
+ except exception.NotFound:
+ return faults.Fault(exc.HTTPNotFound())
+
+ builder = self._get_addresses_view_builder(req)
+ return builder.build(instance)
+
def index(self, req):
""" Returns a list of server names and ids for a given user """
- return self._items(req, entity_maker=_translate_keys)
+ return self._items(req, is_detail=False)
def detail(self, req):
""" Returns a list of server details for a given user """
- return self._items(req, entity_maker=_translate_detail_keys)
+ return self._items(req, is_detail=True)
- def _items(self, req, entity_maker):
+ def _items(self, req, is_detail):
"""Returns a list of servers for a given user.
- entity_maker - either _translate_detail_keys or _translate_keys
+ builder - the response model builder
"""
instance_list = self.compute_api.get_all(req.environ['nova.context'])
limited_list = common.limited(instance_list, req)
- res = [entity_maker(inst)['server'] for inst in limited_list]
- return dict(servers=res)
+ builder = self._get_view_builder(req)
+ servers = [builder.build(inst, is_detail)['server']
+ for inst in limited_list]
+ return dict(servers=servers)
def show(self, req, id):
""" Returns server details by server id """
try:
instance = self.compute_api.get(req.environ['nova.context'], id)
- return _translate_detail_keys(instance)
+ builder = self._get_view_builder(req)
+ return builder.build(instance, is_detail=True)
except exception.NotFound:
return faults.Fault(exc.HTTPNotFound())
@@ -156,8 +120,9 @@ class Controller(wsgi.Controller):
key_name = key_pair['name']
key_data = key_pair['public_key']
+ requested_image_id = self._image_id_from_req_data(env)
image_id = common.get_image_id_from_image_hash(self._image_service,
- context, env['server']['imageId'])
+ context, requested_image_id)
kernel_id, ramdisk_id = self._get_kernel_ramdisk_from_image(
req, image_id)
@@ -171,13 +136,16 @@ class Controller(wsgi.Controller):
for k, v in env['server']['metadata'].items():
metadata.append({'key': k, 'value': v})
- personality = env['server'].get('personality', [])
- injected_files = self._get_injected_files(personality)
+ personality = env['server'].get('personality')
+ injected_files = []
+ if personality:
+ injected_files = self._get_injected_files(personality)
+ flavor_id = self._flavor_id_from_req_data(env)
try:
- instances = self.compute_api.create(
+ (inst,) = self.compute_api.create(
context,
- instance_types.get_by_flavor_id(env['server']['flavorId']),
+ instance_types.get_by_flavor_id(flavor_id),
image_id,
kernel_id=kernel_id,
ramdisk_id=ramdisk_id,
@@ -190,7 +158,11 @@ class Controller(wsgi.Controller):
except quota.QuotaError as error:
self._handle_quota_error(error)
- server = _translate_keys(instances[0])
+ inst['instance_type'] = flavor_id
+ inst['image_id'] = requested_image_id
+
+ builder = self._get_view_builder(req)
+ server = builder.build(inst, is_detail=True)
password = "%s%s" % (server['server']['name'][:4],
utils.generate_password(12))
server['server']['adminPass'] = password
@@ -219,6 +191,7 @@ class Controller(wsgi.Controller):
underlying compute service.
"""
injected_files = []
+
for item in personality:
try:
path = item['path']
@@ -544,6 +517,45 @@ class Controller(wsgi.Controller):
return kernel_id, ramdisk_id
+class ControllerV10(Controller):
+ def _image_id_from_req_data(self, data):
+ return data['server']['imageId']
+
+ def _flavor_id_from_req_data(self, data):
+ return data['server']['flavorId']
+
+ def _get_view_builder(self, req):
+ addresses_builder = nova.api.openstack.views.addresses.ViewBuilderV10()
+ return nova.api.openstack.views.servers.ViewBuilderV10(
+ addresses_builder)
+
+ def _get_addresses_view_builder(self, req):
+ return nova.api.openstack.views.addresses.ViewBuilderV10(req)
+
+
+class ControllerV11(Controller):
+ def _image_id_from_req_data(self, data):
+ href = data['server']['imageRef']
+ return common.get_id_from_href(href)
+
+ def _flavor_id_from_req_data(self, data):
+ href = data['server']['flavorRef']
+ return common.get_id_from_href(href)
+
+ def _get_view_builder(self, req):
+ base_url = req.application_url
+ flavor_builder = nova.api.openstack.views.flavors.ViewBuilderV11(
+ base_url)
+ image_builder = nova.api.openstack.views.images.ViewBuilderV11(
+ base_url)
+ addresses_builder = nova.api.openstack.views.addresses.ViewBuilderV11()
+ return nova.api.openstack.views.servers.ViewBuilderV11(
+ addresses_builder, flavor_builder, image_builder)
+
+ def _get_addresses_view_builder(self, req):
+ return nova.api.openstack.views.addresses.ViewBuilderV11(req)
+
+
class ServerCreateRequestXMLDeserializer(object):
"""
Deserializer to handle xml-formatted server create requests.
diff --git a/nova/api/openstack/users.py b/nova/api/openstack/users.py
index ebd0f4512..d3ab3d553 100644
--- a/nova/api/openstack/users.py
+++ b/nova/api/openstack/users.py
@@ -13,13 +13,14 @@
# License for the specific language governing permissions and limitations
# under the License.
-import common
+from webob import exc
from nova import exception
from nova import flags
from nova import log as logging
from nova import wsgi
-
+from nova.api.openstack import common
+from nova.api.openstack import faults
from nova.auth import manager
FLAGS = flags.FLAGS
@@ -63,7 +64,17 @@ class Controller(wsgi.Controller):
def show(self, req, id):
"""Return data about the given user id"""
- user = self.manager.get_user(id)
+
+ #NOTE(justinsb): The drivers are a little inconsistent in how they
+ # deal with "NotFound" - some throw, some return None.
+ try:
+ user = self.manager.get_user(id)
+ except exception.NotFound:
+ user = None
+
+ if user is None:
+ raise faults.Fault(exc.HTTPNotFound())
+
return dict(user=_translate_keys(user))
def delete(self, req, id):
diff --git a/nova/api/openstack/views/__init__.py b/nova/api/openstack/views/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/nova/api/openstack/views/__init__.py
diff --git a/nova/api/openstack/views/addresses.py b/nova/api/openstack/views/addresses.py
new file mode 100644
index 000000000..90c77855b
--- /dev/null
+++ b/nova/api/openstack/views/addresses.py
@@ -0,0 +1,42 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010-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.
+
+from nova import utils
+from nova.api.openstack import common
+
+
+class ViewBuilder(object):
+ ''' Models a server addresses response as a python dictionary.'''
+
+ def build(self, inst):
+ raise NotImplementedError()
+
+
+class ViewBuilderV10(ViewBuilder):
+ def build(self, inst):
+ private_ips = utils.get_from_path(inst, 'fixed_ip/address')
+ public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address')
+ return dict(public=public_ips, private=private_ips)
+
+
+class ViewBuilderV11(ViewBuilder):
+ def build(self, inst):
+ private_ips = utils.get_from_path(inst, 'fixed_ip/address')
+ private_ips = [dict(version=4, addr=a) for a in private_ips]
+ public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address')
+ public_ips = [dict(version=4, addr=a) for a in public_ips]
+ return dict(public=public_ips, private=private_ips)
diff --git a/nova/api/openstack/views/flavors.py b/nova/api/openstack/views/flavors.py
new file mode 100644
index 000000000..18bd779c0
--- /dev/null
+++ b/nova/api/openstack/views/flavors.py
@@ -0,0 +1,34 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010-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.
+
+from nova.api.openstack import common
+
+
+class ViewBuilder(object):
+ def __init__(self):
+ pass
+
+ def build(self, flavor_obj):
+ raise NotImplementedError()
+
+
+class ViewBuilderV11(ViewBuilder):
+ def __init__(self, base_url):
+ self.base_url = base_url
+
+ def generate_href(self, flavor_id):
+ return "%s/flavors/%s" % (self.base_url, flavor_id)
diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py
new file mode 100644
index 000000000..a6c6ad7d1
--- /dev/null
+++ b/nova/api/openstack/views/images.py
@@ -0,0 +1,34 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010-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.
+
+from nova.api.openstack import common
+
+
+class ViewBuilder(object):
+ def __init__(self):
+ pass
+
+ def build(self, image_obj):
+ raise NotImplementedError()
+
+
+class ViewBuilderV11(ViewBuilder):
+ def __init__(self, base_url):
+ self.base_url = base_url
+
+ def generate_href(self, image_id):
+ return "%s/images/%s" % (self.base_url, image_id)
diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py
new file mode 100644
index 000000000..f93435198
--- /dev/null
+++ b/nova/api/openstack/views/servers.py
@@ -0,0 +1,125 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010-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 hashlib
+
+from nova.compute import power_state
+import nova.compute
+import nova.context
+from nova.api.openstack import common
+from nova.api.openstack.views import addresses as addresses_view
+from nova.api.openstack.views import flavors as flavors_view
+from nova.api.openstack.views import images as images_view
+from nova import utils
+
+
+class ViewBuilder(object):
+ """Model a server response as a python dictionary.
+
+ Public methods: build
+ Abstract methods: _build_image, _build_flavor
+
+ """
+
+ def __init__(self, addresses_builder):
+ self.addresses_builder = addresses_builder
+
+ def build(self, inst, is_detail):
+ """Return a dict that represenst a server."""
+ if is_detail:
+ return self._build_detail(inst)
+ else:
+ return self._build_simple(inst)
+
+ def _build_simple(self, inst):
+ """Return a simple model of a server."""
+ return dict(server=dict(id=inst['id'], name=inst['display_name']))
+
+ def _build_detail(self, inst):
+ """Returns a detailed model of a server."""
+ power_mapping = {
+ None: 'build',
+ power_state.NOSTATE: 'build',
+ power_state.RUNNING: 'active',
+ power_state.BLOCKED: 'active',
+ power_state.SUSPENDED: 'suspended',
+ power_state.PAUSED: 'paused',
+ power_state.SHUTDOWN: 'active',
+ power_state.SHUTOFF: 'active',
+ power_state.CRASHED: 'error',
+ power_state.FAILED: 'error'}
+
+ inst_dict = {
+ 'id': int(inst['id']),
+ 'name': inst['display_name'],
+ 'addresses': self.addresses_builder.build(inst),
+ 'status': power_mapping[inst.get('state')]}
+
+ ctxt = nova.context.get_admin_context()
+ compute_api = nova.compute.API()
+ if compute_api.has_finished_migration(ctxt, inst['id']):
+ inst_dict['status'] = 'resize-confirm'
+
+ # Return the metadata as a dictionary
+ metadata = {}
+ for item in inst.get('metadata', []):
+ metadata[item['key']] = item['value']
+ inst_dict['metadata'] = metadata
+
+ inst_dict['hostId'] = ''
+ if inst.get('host'):
+ inst_dict['hostId'] = hashlib.sha224(inst['host']).hexdigest()
+
+ self._build_image(inst_dict, inst)
+ self._build_flavor(inst_dict, inst)
+
+ return dict(server=inst_dict)
+
+ def _build_image(self, response, inst):
+ """Return the image sub-resource of a server."""
+ raise NotImplementedError()
+
+ def _build_flavor(self, response, inst):
+ """Return the flavor sub-resource of a server."""
+ raise NotImplementedError()
+
+
+class ViewBuilderV10(ViewBuilder):
+ """Model an Openstack API V1.0 server response."""
+
+ def _build_image(self, response, inst):
+ response['imageId'] = inst['image_id']
+
+ def _build_flavor(self, response, inst):
+ response['flavorId'] = inst['instance_type']
+
+
+class ViewBuilderV11(ViewBuilder):
+ """Model an Openstack API V1.0 server response."""
+
+ def __init__(self, addresses_builder, flavor_builder, image_builder):
+ ViewBuilder.__init__(self, addresses_builder)
+ self.flavor_builder = flavor_builder
+ self.image_builder = image_builder
+
+ def _build_image(self, response, inst):
+ image_id = inst["image_id"]
+ response["imageRef"] = self.image_builder.generate_href(image_id)
+
+ def _build_flavor(self, response, inst):
+ flavor_id = inst["instance_type"]
+ response["flavorRef"] = self.flavor_builder.generate_href(flavor_id)
diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py
index 8fe84275a..d4a59993b 100644
--- a/nova/api/openstack/zones.py
+++ b/nova/api/openstack/zones.py
@@ -15,9 +15,9 @@
import common
+from nova import db
from nova import flags
from nova import wsgi
-from nova import db
from nova.scheduler import api
@@ -52,7 +52,7 @@ class Controller(wsgi.Controller):
"""Return all zones in brief"""
# Ask the ZoneManager in the Scheduler for most recent data,
# or fall-back to the database ...
- items = api.API().get_zone_list(req.environ['nova.context'])
+ items = api.get_zone_list(req.environ['nova.context'])
if not items:
items = db.zone_get_all(req.environ['nova.context'])
@@ -67,8 +67,16 @@ class Controller(wsgi.Controller):
def info(self, req):
"""Return name and capabilities for this zone."""
- return dict(zone=dict(name=FLAGS.zone_name,
- capabilities=FLAGS.zone_capabilities))
+ items = api.get_zone_capabilities(req.environ['nova.context'])
+
+ zone = dict(name=FLAGS.zone_name)
+ caps = FLAGS.zone_capabilities
+ for cap in caps:
+ key, value = cap.split('=')
+ zone[key] = value
+ for item, (min_value, max_value) in items.iteritems():
+ zone[item] = "%s,%s" % (min_value, max_value)
+ return dict(zone=zone)
def show(self, req, id):
"""Return data about the given zone id"""
diff --git a/nova/auth/dbdriver.py b/nova/auth/dbdriver.py
index d8dad8edd..d1e3f2ed5 100644
--- a/nova/auth/dbdriver.py
+++ b/nova/auth/dbdriver.py
@@ -162,6 +162,8 @@ class DbDriver(object):
values['description'] = description
db.project_update(context.get_admin_context(), project_id, values)
+ if not self.is_in_project(manager_uid, project_id):
+ self.add_to_project(manager_uid, project_id)
def add_to_project(self, uid, project_id):
"""Add user to project"""
diff --git a/nova/auth/fakeldap.py b/nova/auth/fakeldap.py
index 4466051f0..79afb9109 100644
--- a/nova/auth/fakeldap.py
+++ b/nova/auth/fakeldap.py
@@ -90,12 +90,12 @@ MOD_DELETE = 1
MOD_REPLACE = 2
-class NO_SUCH_OBJECT(Exception): # pylint: disable-msg=C0103
+class NO_SUCH_OBJECT(Exception): # pylint: disable=C0103
"""Duplicate exception class from real LDAP module."""
pass
-class OBJECT_CLASS_VIOLATION(Exception): # pylint: disable-msg=C0103
+class OBJECT_CLASS_VIOLATION(Exception): # pylint: disable=C0103
"""Duplicate exception class from real LDAP module."""
pass
@@ -268,7 +268,7 @@ class FakeLDAP(object):
# get the attributes from the store
attrs = store.hgetall(key)
# turn the values from the store into lists
- # pylint: disable-msg=E1103
+ # pylint: disable=E1103
attrs = dict([(k, _from_json(v))
for k, v in attrs.iteritems()])
# filter the objects by query
@@ -277,12 +277,12 @@ class FakeLDAP(object):
attrs = dict([(k, v) for k, v in attrs.iteritems()
if not fields or k in fields])
objects.append((key[len(self.__prefix):], attrs))
- # pylint: enable-msg=E1103
+ # pylint: enable=E1103
if objects == []:
raise NO_SUCH_OBJECT()
return objects
@property
- def __prefix(self): # pylint: disable-msg=R0201
+ def __prefix(self): # pylint: disable=R0201
"""Get the prefix to use for all keys."""
return 'ldap:'
diff --git a/nova/auth/ldapdriver.py b/nova/auth/ldapdriver.py
index 5da7751a0..fcac55510 100644
--- a/nova/auth/ldapdriver.py
+++ b/nova/auth/ldapdriver.py
@@ -275,6 +275,8 @@ class LdapDriver(object):
attr.append((self.ldap.MOD_REPLACE, 'description', description))
dn = self.__project_to_dn(project_id)
self.conn.modify_s(dn, attr)
+ if not self.is_in_project(manager_uid, project_id):
+ self.add_to_project(manager_uid, project_id)
@sanitize
def add_to_project(self, uid, project_id):
@@ -632,6 +634,6 @@ class LdapDriver(object):
class FakeLdapDriver(LdapDriver):
"""Fake Ldap Auth driver"""
- def __init__(self): # pylint: disable-msg=W0231
+ def __init__(self): # pylint: disable=W0231
__import__('nova.auth.fakeldap')
self.ldap = sys.modules['nova.auth.fakeldap']
diff --git a/nova/auth/manager.py b/nova/auth/manager.py
index 450ab803a..486845399 100644
--- a/nova/auth/manager.py
+++ b/nova/auth/manager.py
@@ -22,7 +22,7 @@ Nova authentication management
import os
import shutil
-import string # pylint: disable-msg=W0402
+import string # pylint: disable=W0402
import tempfile
import uuid
import zipfile
@@ -96,10 +96,19 @@ class AuthBase(object):
class User(AuthBase):
- """Object representing a user"""
+ """Object representing a user
+
+ The following attributes are defined:
+ :id: A system identifier for the user. A string (for LDAP)
+ :name: The user name, potentially in some more friendly format
+ :access: The 'username' for EC2 authentication
+ :secret: The 'password' for EC2 authenticatoin
+ :admin: ???
+ """
def __init__(self, id, name, access, secret, admin):
AuthBase.__init__(self)
+ assert isinstance(id, basestring)
self.id = id
self.name = name
self.access = access
diff --git a/nova/compute/api.py b/nova/compute/api.py
index 32577af82..01eead4ac 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -253,6 +253,16 @@ class API(base.Base):
return [dict(x.iteritems()) for x in instances]
+ def has_finished_migration(self, context, instance_id):
+ """Retrieves whether or not a finished migration exists for
+ an instance"""
+ try:
+ db.migration_get_by_instance_and_status(context, instance_id,
+ 'finished')
+ return True
+ except exception.NotFound:
+ return False
+
def ensure_default_security_group(self, context):
""" Create security group for the security context if it
does not already exist
@@ -464,6 +474,8 @@ class API(base.Base):
params = {'migration_id': migration_ref['id']}
self._cast_compute_message('revert_resize', context, instance_id,
migration_ref['dest_compute'], params=params)
+ self.db.migration_update(context, migration_ref['id'],
+ {'status': 'reverted'})
def confirm_resize(self, context, instance_id):
"""Confirms a migration/resize, deleting the 'old' instance in the
@@ -479,17 +491,41 @@ class API(base.Base):
self._cast_compute_message('confirm_resize', context, instance_id,
migration_ref['source_compute'], params=params)
- self.db.migration_update(context, migration_id,
+ self.db.migration_update(context, migration_ref['id'],
{'status': 'confirmed'})
self.db.instance_update(context, instance_id,
{'host': migration_ref['dest_compute'], })
- def resize(self, context, instance_id, flavor):
+ def resize(self, context, instance_id, flavor_id):
"""Resize a running instance."""
+ instance = self.db.instance_get(context, instance_id)
+ current_instance_type = self.db.instance_type_get_by_name(
+ context, instance['instance_type'])
+
+ new_instance_type = self.db.instance_type_get_by_flavor_id(
+ context, flavor_id)
+ current_instance_type_name = current_instance_type['name']
+ new_instance_type_name = new_instance_type['name']
+ LOG.debug(_("Old instance type %(current_instance_type_name)s, "
+ " new instance type %(new_instance_type_name)s") % locals())
+ if not new_instance_type:
+ raise exception.ApiError(_("Requested flavor %(flavor_id)d "
+ "does not exist") % locals())
+
+ current_memory_mb = current_instance_type['memory_mb']
+ new_memory_mb = new_instance_type['memory_mb']
+ if current_memory_mb > new_memory_mb:
+ raise exception.ApiError(_("Invalid flavor: cannot downsize"
+ "instances"))
+ if current_memory_mb == new_memory_mb:
+ raise exception.ApiError(_("Invalid flavor: cannot use"
+ "the same flavor. "))
+
self._cast_scheduler_message(context,
{"method": "prep_resize",
"args": {"topic": FLAGS.compute_topic,
- "instance_id": instance_id, }},)
+ "instance_id": instance_id,
+ "flavor_id": flavor_id}})
def pause(self, context, instance_id):
"""Pause the given instance."""
diff --git a/nova/compute/manager.py b/nova/compute/manager.py
index 92deca813..468771f46 100644
--- a/nova/compute/manager.py
+++ b/nova/compute/manager.py
@@ -2,6 +2,7 @@
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
+# Copyright 2011 Justin Santa Barbara
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -39,10 +40,12 @@ import os
import random
import string
import socket
+import sys
import tempfile
-import time
import functools
+from eventlet import greenthread
+
from nova import exception
from nova import flags
from nova import log as logging
@@ -50,6 +53,7 @@ from nova import manager
from nova import rpc
from nova import utils
from nova.compute import power_state
+from nova.virt import driver
FLAGS = flags.FLAGS
flags.DEFINE_string('instances_path', '$state_path/instances',
@@ -64,8 +68,11 @@ flags.DEFINE_string('console_host', socket.gethostname(),
'Console proxy host to use to connect to instances on'
'this host.')
flags.DEFINE_integer('live_migration_retry_count', 30,
- ("Retry count needed in live_migration."
- " sleep 1 sec for each count"))
+ "Retry count needed in live_migration."
+ " sleep 1 sec for each count")
+flags.DEFINE_integer("rescue_timeout", 0,
+ "Automatically unrescue an instance after N seconds."
+ " Set to 0 to disable.")
LOG = logging.getLogger('nova.compute.manager')
@@ -104,7 +111,7 @@ def checks_instance_lock(function):
return decorated_function
-class ComputeManager(manager.Manager):
+class ComputeManager(manager.SchedulerDependentManager):
"""Manages the running instances from creation to destruction."""
@@ -114,10 +121,19 @@ class ComputeManager(manager.Manager):
# and redocument the module docstring
if not compute_driver:
compute_driver = FLAGS.compute_driver
- self.driver = utils.import_object(compute_driver)
+
+ try:
+ self.driver = utils.check_isinstance(
+ utils.import_object(compute_driver),
+ driver.ComputeDriver)
+ except ImportError as e:
+ LOG.error(_("Unable to load the virtualization driver: %s") % (e))
+ sys.exit(1)
+
self.network_manager = utils.import_object(FLAGS.network_manager)
self.volume_manager = utils.import_object(FLAGS.volume_manager)
- super(ComputeManager, self).__init__(*args, **kwargs)
+ super(ComputeManager, self).__init__(service_name="compute",
+ *args, **kwargs)
def init_host(self):
"""Do any initialization that needs to be run if this is a
@@ -125,6 +141,12 @@ class ComputeManager(manager.Manager):
"""
self.driver.init_host(host=self.host)
+ def periodic_tasks(self, context=None):
+ """Tasks to be run at a periodic interval."""
+ super(ComputeManager, self).periodic_tasks(context)
+ if FLAGS.rescue_timeout > 0:
+ self.driver.poll_rescued_instances(FLAGS.rescue_timeout)
+
def _update_state(self, context, instance_id):
"""Update the state of an instance from the driver info."""
# FIXME(ja): include other fields from state?
@@ -220,9 +242,10 @@ class ComputeManager(manager.Manager):
self.db.instance_update(context,
instance_id,
{'launched_at': now})
- except Exception: # pylint: disable-msg=W0702
- LOG.exception(_("instance %s: Failed to spawn"), instance_id,
- context=context)
+ except Exception: # pylint: disable=W0702
+ LOG.exception(_("Instance '%s' failed to spawn. Is virtualization"
+ " enabled in the BIOS?"), instance_id,
+ context=context)
self.db.instance_set_state(context,
instance_id,
power_state.SHUTDOWN)
@@ -429,25 +452,41 @@ class ComputeManager(manager.Manager):
instance_ref = self.db.instance_get(context, instance_id)
migration_ref = self.db.migration_get(context, migration_id)
- #TODO(mdietz): we may want to split these into separate methods.
- if migration_ref['source_compute'] == FLAGS.host:
- self.driver._start(instance_ref)
- self.db.migration_update(context, migration_id,
- {'status': 'reverted'})
- else:
- self.driver.destroy(instance_ref)
- topic = self.db.queue_get_for(context, FLAGS.compute_topic,
- instance_ref['host'])
- rpc.cast(context, topic,
- {'method': 'revert_resize',
- 'args': {
- 'migration_id': migration_ref['id'],
- 'instance_id': instance_id, },
- })
+ self.driver.destroy(instance_ref)
+ topic = self.db.queue_get_for(context, FLAGS.compute_topic,
+ instance_ref['host'])
+ rpc.cast(context, topic,
+ {'method': 'finish_revert_resize',
+ 'args': {
+ 'migration_id': migration_ref['id'],
+ 'instance_id': instance_id, },
+ })
@exception.wrap_exception
@checks_instance_lock
- def prep_resize(self, context, instance_id):
+ def finish_revert_resize(self, context, instance_id, migration_id):
+ """Finishes the second half of reverting a resize, powering back on
+ the source instance and reverting the resized attributes in the
+ database"""
+ instance_ref = self.db.instance_get(context, instance_id)
+ migration_ref = self.db.migration_get(context, migration_id)
+ instance_type = self.db.instance_type_get_by_flavor_id(context,
+ migration_ref['old_flavor_id'])
+
+ # Just roll back the record. There's no need to resize down since
+ # the 'old' VM already has the preferred attributes
+ self.db.instance_update(context, instance_id,
+ dict(memory_mb=instance_type['memory_mb'],
+ vcpus=instance_type['vcpus'],
+ local_gb=instance_type['local_gb']))
+
+ self.driver.revert_resize(instance_ref)
+ self.db.migration_update(context, migration_id,
+ {'status': 'reverted'})
+
+ @exception.wrap_exception
+ @checks_instance_lock
+ def prep_resize(self, context, instance_id, flavor_id):
"""Initiates the process of moving a running instance to another
host, possibly changing the RAM and disk size in the process"""
context = context.elevated()
@@ -456,12 +495,17 @@ class ComputeManager(manager.Manager):
raise exception.Error(_(
'Migration error: destination same as source!'))
+ instance_type = self.db.instance_type_get_by_flavor_id(context,
+ flavor_id)
migration_ref = self.db.migration_create(context,
{'instance_id': instance_id,
'source_compute': instance_ref['host'],
'dest_compute': FLAGS.host,
'dest_host': self.driver.get_host_ip_addr(),
+ 'old_flavor_id': instance_type['flavorid'],
+ 'new_flavor_id': flavor_id,
'status': 'pre-migrating'})
+
LOG.audit(_('instance %s: migrating to '), instance_id,
context=context)
topic = self.db.queue_get_for(context, FLAGS.compute_topic,
@@ -487,8 +531,6 @@ class ComputeManager(manager.Manager):
self.db.migration_update(context, migration_id,
{'status': 'post-migrating', })
- #TODO(mdietz): This is where we would update the VM record
- #after resizing
service = self.db.service_get_by_host_and_topic(context,
migration_ref['dest_compute'], FLAGS.compute_topic)
topic = self.db.queue_get_for(context, FLAGS.compute_topic,
@@ -509,7 +551,19 @@ class ComputeManager(manager.Manager):
migration_ref = self.db.migration_get(context, migration_id)
instance_ref = self.db.instance_get(context,
migration_ref['instance_id'])
-
+ # TODO(mdietz): apply the rest of the instance_type attributes going
+ # after they're supported
+ 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'],
+ memory_mb=instance_type['memory_mb'],
+ vcpus=instance_type['vcpus'],
+ local_gb=instance_type['local_gb']))
+
+ # reload the updated instance ref
+ # FIXME(mdietz): is there reload functionality?
+ instance_ref = self.db.instance_get(context, instance_id)
self.driver.finish_resize(instance_ref, disk_info)
self.db.migration_update(context, migration_id,
@@ -692,7 +746,7 @@ class ComputeManager(manager.Manager):
volume_id,
instance_id,
mountpoint)
- except Exception as exc: # pylint: disable-msg=W0702
+ except Exception as exc: # pylint: disable=W0702
# NOTE(vish): The inline callback eats the exception info so we
# log the traceback here and reraise the same
# ecxception below.
@@ -792,7 +846,7 @@ class ComputeManager(manager.Manager):
return self.driver.update_available_resource(context, self.host)
- def pre_live_migration(self, context, instance_id):
+ def pre_live_migration(self, context, instance_id, time=None):
"""Preparations for live migration at dest host.
:param context: security context
@@ -800,6 +854,9 @@ class ComputeManager(manager.Manager):
"""
+ if not time:
+ time = greenthread
+
# Getting instance info
instance_ref = self.db.instance_get(context, instance_id)
ec2_id = instance_ref['hostname']
@@ -968,3 +1025,59 @@ class ComputeManager(manager.Manager):
for volume in instance_ref['volumes']:
self.db.volume_update(ctxt, volume['id'], {'status': 'in-use'})
+
+ def periodic_tasks(self, context=None):
+ """Tasks to be run at a periodic interval."""
+ error_list = super(ComputeManager, self).periodic_tasks(context)
+ if error_list is None:
+ error_list = []
+
+ try:
+ self._poll_instance_states(context)
+ except Exception as ex:
+ LOG.warning(_("Error during instance poll: %s"),
+ unicode(ex))
+ error_list.append(ex)
+ return error_list
+
+ def _poll_instance_states(self, context):
+ vm_instances = self.driver.list_instances_detail()
+ vm_instances = dict((vm.name, vm) for vm in vm_instances)
+
+ # Keep a list of VMs not in the DB, cross them off as we find them
+ vms_not_found_in_db = list(vm_instances.keys())
+
+ db_instances = self.db.instance_get_all_by_host(context, self.host)
+
+ for db_instance in db_instances:
+ name = db_instance['name']
+ vm_instance = vm_instances.get(name)
+ if vm_instance is None:
+ LOG.info(_("Found instance '%(name)s' in DB but no VM. "
+ "Setting state to shutoff.") % locals())
+ vm_state = power_state.SHUTOFF
+ else:
+ vm_state = vm_instance.state
+ vms_not_found_in_db.remove(name)
+
+ db_state = db_instance['state']
+ if vm_state != db_state:
+ LOG.info(_("DB/VM state mismatch. Changing state from "
+ "'%(db_state)s' to '%(vm_state)s'") % locals())
+ self.db.instance_set_state(context,
+ db_instance['id'],
+ vm_state)
+
+ if vm_state == power_state.SHUTOFF:
+ # TODO(soren): This is what the compute manager does when you
+ # terminate an instance. At some point I figure we'll have a
+ # "terminated" state and some sort of cleanup job that runs
+ # occasionally, cleaning them out.
+ self.db.instance_destroy(context, db_instance['id'])
+
+ # Are there VMs not in the DB?
+ for vm_not_found_in_db in vms_not_found_in_db:
+ name = vm_not_found_in_db
+ # TODO(justinsb): What to do here? Adopt it? Shut it down?
+ LOG.warning(_("Found VM not in DB: '%(name)s'. Ignoring")
+ % locals())
diff --git a/nova/compute/power_state.py b/nova/compute/power_state.py
index adfc2dff0..ef013b2ef 100644
--- a/nova/compute/power_state.py
+++ b/nova/compute/power_state.py
@@ -2,6 +2,7 @@
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
+# Copyright 2011 Justin Santa Barbara
# All Rights Reserved.
# Copyright (c) 2010 Citrix Systems, Inc.
#
@@ -19,6 +20,7 @@
"""The various power states that a VM can be in."""
+#NOTE(justinsb): These are the virDomainState values from libvirt
NOSTATE = 0x00
RUNNING = 0x01
BLOCKED = 0x02
@@ -29,9 +31,10 @@ CRASHED = 0x06
SUSPENDED = 0x07
FAILED = 0x08
-
-def name(code):
- d = {
+# TODO(justinsb): Power state really needs to be a proper class,
+# so that we're not locked into the libvirt status codes and can put mapping
+# logic here rather than spread throughout the code
+_STATE_MAP = {
NOSTATE: 'pending',
RUNNING: 'running',
BLOCKED: 'blocked',
@@ -41,4 +44,11 @@ def name(code):
CRASHED: 'crashed',
SUSPENDED: 'suspended',
FAILED: 'failed to spawn'}
- return d[code]
+
+
+def name(code):
+ return _STATE_MAP[code]
+
+
+def valid_states():
+ return _STATE_MAP.keys()
diff --git a/nova/crypto.py b/nova/crypto.py
index 2a8d4abca..b112e5b92 100644
--- a/nova/crypto.py
+++ b/nova/crypto.py
@@ -26,6 +26,7 @@ import gettext
import hashlib
import os
import shutil
+import string
import struct
import tempfile
import time
@@ -267,7 +268,7 @@ def _sign_csr(csr_text, ca_folder):
'./openssl.cnf', '-infiles', inbound)
out, _err = utils.execute('openssl', 'x509', '-in', outbound,
'-serial', '-noout')
- serial = out.rpartition("=")[2]
+ serial = string.strip(out.rpartition("=")[2])
os.chdir(start)
with open(outbound, "r") as crtfile:
return (serial, crtfile.read())
diff --git a/nova/db/api.py b/nova/db/api.py
index 3cb0e5811..afc1bff2f 100644
--- a/nova/db/api.py
+++ b/nova/db/api.py
@@ -214,7 +214,7 @@ def certificate_update(context, certificate_id, values):
Raises NotFound if service does not exist.
"""
- return IMPL.service_update(context, certificate_id, values)
+ return IMPL.certificate_update(context, certificate_id, values)
###################
@@ -608,7 +608,7 @@ def network_get_all(context):
return IMPL.network_get_all(context)
-# pylint: disable-msg=C0103
+# pylint: disable=C0103
def network_get_associated_fixed_ips(context, network_id):
"""Get all network's ips that have been associated."""
return IMPL.network_get_associated_fixed_ips(context, network_id)
@@ -1118,7 +1118,7 @@ def instance_type_create(context, values):
return IMPL.instance_type_create(context, values)
-def instance_type_get_all(context, inactive=0):
+def instance_type_get_all(context, inactive=False):
"""Get all instance types"""
return IMPL.instance_type_get_all(context, inactive)
diff --git a/nova/db/base.py b/nova/db/base.py
index 1d1e80866..a0f2180c6 100644
--- a/nova/db/base.py
+++ b/nova/db/base.py
@@ -33,4 +33,4 @@ class Base(object):
def __init__(self, db_driver=None):
if not db_driver:
db_driver = FLAGS.db_driver
- self.db = utils.import_object(db_driver) # pylint: disable-msg=C0103
+ self.db = utils.import_object(db_driver) # pylint: disable=C0103
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index 44540617f..d7b5aff46 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -762,6 +762,15 @@ def instance_create(context, values):
context - request context object
values - dict containing column values.
"""
+ metadata = values.get('metadata')
+ metadata_refs = []
+ if metadata:
+ for metadata_item in metadata:
+ metadata_ref = models.InstanceMetadata()
+ metadata_ref.update(metadata_item)
+ metadata_refs.append(metadata_ref)
+ values['metadata'] = metadata_refs
+
instance_ref = models.Instance()
instance_ref.update(values)
@@ -797,6 +806,11 @@ def instance_destroy(context, instance_id):
update({'deleted': 1,
'deleted_at': datetime.datetime.utcnow(),
'updated_at': literal_column('updated_at')})
+ session.query(models.InstanceMetadata).\
+ filter_by(instance_id=instance_id).\
+ update({'deleted': 1,
+ 'deleted_at': datetime.datetime.utcnow(),
+ 'updated_at': literal_column('updated_at')})
@require_context
@@ -1240,7 +1254,7 @@ def network_get_all(context):
# NOTE(vish): pylint complains because of the long method name, but
# it fits with the names of the rest of the methods
-# pylint: disable-msg=C0103
+# pylint: disable=C0103
@require_admin_context
@@ -2206,8 +2220,8 @@ def migration_get_by_instance_and_status(context, instance_id, status):
filter_by(instance_id=instance_id).\
filter_by(status=status).first()
if not result:
- raise exception.NotFound(_("No migration found with instance id %s")
- % migration_id)
+ raise exception.NotFound(_("No migration found for instance "
+ "%(instance_id)s with status %(status)s") % locals())
return result
@@ -2322,13 +2336,13 @@ def instance_type_create(_context, values):
instance_type_ref = models.InstanceTypes()
instance_type_ref.update(values)
instance_type_ref.save()
- except:
- raise exception.DBError
+ except Exception, e:
+ raise exception.DBError(e)
return instance_type_ref
@require_context
-def instance_type_get_all(context, inactive=0):
+def instance_type_get_all(context, inactive=False):
"""
Returns a dict describing all instance_types with name as key.
"""
@@ -2339,7 +2353,7 @@ def instance_type_get_all(context, inactive=0):
all()
else:
inst_types = session.query(models.InstanceTypes).\
- filter_by(deleted=inactive).\
+ filter_by(deleted=False).\
order_by("name").\
all()
if inst_types:
@@ -2383,7 +2397,7 @@ def instance_type_destroy(context, name):
session = get_session()
instance_type_ref = session.query(models.InstanceTypes).\
filter_by(name=name)
- records = instance_type_ref.update(dict(deleted=1))
+ records = instance_type_ref.update(dict(deleted=True))
if records == 0:
raise exception.NotFound
else:
diff --git a/nova/db/sqlalchemy/migrate_repo/versions/008_add_instance_types.py b/nova/db/sqlalchemy/migrate_repo/versions/008_add_instance_types.py
index 66609054e..5e2cb69d9 100644
--- a/nova/db/sqlalchemy/migrate_repo/versions/008_add_instance_types.py
+++ b/nova/db/sqlalchemy/migrate_repo/versions/008_add_instance_types.py
@@ -55,7 +55,7 @@ def upgrade(migrate_engine):
try:
instance_types.create()
except Exception:
- logging.info(repr(table))
+ logging.info(repr(instance_types))
logging.exception('Exception while creating instance_types table')
raise
@@ -72,11 +72,11 @@ def upgrade(migrate_engine):
# FIXME(kpepple) should we be seeding created_at / updated_at ?
# now = datetime.datatime.utcnow()
i.execute({'name': name, 'memory_mb': values["memory_mb"],
- 'vcpus': values["vcpus"], 'deleted': 0,
+ 'vcpus': values["vcpus"], 'deleted': False,
'local_gb': values["local_gb"],
'flavorid': values["flavorid"]})
except Exception:
- logging.info(repr(table))
+ logging.info(repr(instance_types))
logging.exception('Exception while seeding instance_types table')
raise
diff --git a/nova/db/sqlalchemy/migrate_repo/versions/012_add_ipv6_flatmanager.py b/nova/db/sqlalchemy/migrate_repo/versions/012_add_ipv6_flatmanager.py
new file mode 100644
index 000000000..e87085668
--- /dev/null
+++ b/nova/db/sqlalchemy/migrate_repo/versions/012_add_ipv6_flatmanager.py
@@ -0,0 +1,154 @@
+# Copyright (c) 2011 NTT.
+# 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 *
+from migrate import *
+
+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.
+#
+instances = Table('instances', meta,
+ Column('id', Integer(), primary_key=True, nullable=False),
+ )
+
+#
+# Tables to alter
+#
+networks = Table('networks', 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('injected', Boolean(create_constraint=True, name=None)),
+ Column('cidr',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False)),
+ Column('netmask',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False)),
+ Column('bridge',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False)),
+ Column('gateway',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False)),
+ Column('broadcast',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False)),
+ Column('dns',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False)),
+ Column('vlan', Integer()),
+ Column('vpn_public_address',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False)),
+ Column('vpn_public_port', Integer()),
+ Column('vpn_private_address',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False)),
+ Column('dhcp_start',
+ 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('host',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False)),
+ Column('cidr_v6',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False)),
+ Column('ra_server', String(length=255,
+ convert_unicode=False,
+ assert_unicode=None,
+ unicode_error=None,
+ _warn_on_bytestring=False)),
+ Column(
+ 'label',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False)))
+
+fixed_ips = Table('fixed_ips', 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('address',
+ String(length=255, convert_unicode=False, assert_unicode=None,
+ unicode_error=None, _warn_on_bytestring=False)),
+ Column('network_id',
+ Integer(),
+ ForeignKey('networks.id'),
+ nullable=True),
+ Column('instance_id',
+ Integer(),
+ ForeignKey('instances.id'),
+ nullable=True),
+ Column('allocated', Boolean(create_constraint=True, name=None)),
+ Column('leased', Boolean(create_constraint=True, name=None)),
+ Column('reserved', Boolean(create_constraint=True, name=None)),
+ Column("addressV6", String(length=255,
+ convert_unicode=False,
+ assert_unicode=None,
+ unicode_error=None,
+ _warn_on_bytestring=False)),
+ Column("netmaskV6", String(length=3,
+ convert_unicode=False,
+ assert_unicode=None,
+ unicode_error=None,
+ _warn_on_bytestring=False)),
+ Column("gatewayV6", String(length=255,
+ convert_unicode=False,
+ assert_unicode=None,
+ unicode_error=None,
+ _warn_on_bytestring=False)),
+ )
+#
+# New Tables
+#
+# None
+
+#
+# Columns to add to existing tables
+#
+networks_netmask_v6 = Column(
+ 'netmask_v6',
+ 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
+
+ # Alter column name
+ networks.c.ra_server.alter(name='gateway_v6')
+ # Add new column to existing table
+ networks.create_column(networks_netmask_v6)
+
+ # drop existing columns from table
+ fixed_ips.c.addressV6.drop()
+ fixed_ips.c.netmaskV6.drop()
+ fixed_ips.c.gatewayV6.drop()
diff --git a/nova/db/sqlalchemy/migrate_repo/versions/013_add_flavors_to_migrations.py b/nova/db/sqlalchemy/migrate_repo/versions/013_add_flavors_to_migrations.py
new file mode 100644
index 000000000..3fb92e85c
--- /dev/null
+++ b/nova/db/sqlalchemy/migrate_repo/versions/013_add_flavors_to_migrations.py
@@ -0,0 +1,50 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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.from sqlalchemy import *
+
+from sqlalchemy import *
+from migrate import *
+
+from nova import log as logging
+
+
+meta = MetaData()
+
+migrations = Table('migrations', meta,
+ Column('id', Integer(), primary_key=True, nullable=False),
+ )
+
+#
+# Tables to alter
+#
+#
+
+old_flavor_id = Column('old_flavor_id', Integer())
+new_flavor_id = Column('new_flavor_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
+ migrations.create_column(old_flavor_id)
+ migrations.create_column(new_flavor_id)
+
+
+def downgrade(migrate_engine):
+ meta.bind = migrate_engine
+ migrations.drop_column(old_flavor_id)
+ migrations.drop_column(new_flavor_id)
diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py
index 1845e85eb..3b95ac23e 100644
--- a/nova/db/sqlalchemy/models.py
+++ b/nova/db/sqlalchemy/models.py
@@ -436,6 +436,8 @@ class Migration(BASE, NovaBase):
source_compute = Column(String(255))
dest_compute = Column(String(255))
dest_host = Column(String(255))
+ old_flavor_id = Column(Integer())
+ new_flavor_id = Column(Integer())
instance_id = Column(Integer, ForeignKey('instances.id'), nullable=True)
#TODO(_cerberus_): enum
status = Column(String(255))
@@ -454,8 +456,8 @@ class Network(BASE, NovaBase):
cidr = Column(String(255), unique=True)
cidr_v6 = Column(String(255), unique=True)
- ra_server = Column(String(255))
-
+ gateway_v6 = Column(String(255))
+ netmask_v6 = Column(String(255))
netmask = Column(String(255))
bridge = Column(String(255))
gateway = Column(String(255))
@@ -508,9 +510,6 @@ class FixedIp(BASE, NovaBase):
allocated = Column(Boolean, default=False)
leased = Column(Boolean, default=False)
reserved = Column(Boolean, default=False)
- addressV6 = Column(String(255))
- netmaskV6 = Column(String(3))
- gatewayV6 = Column(String(255))
class User(BASE, NovaBase):
diff --git a/nova/exception.py b/nova/exception.py
index 93c5fe3d7..4e2bbdbaf 100644
--- a/nova/exception.py
+++ b/nova/exception.py
@@ -46,7 +46,7 @@ class Error(Exception):
class ApiError(Error):
- def __init__(self, message='Unknown', code='Unknown'):
+ def __init__(self, message='Unknown', code='ApiError'):
self.message = message
self.code = code
super(ApiError, self).__init__('%s: %s' % (code, message))
diff --git a/nova/flags.py b/nova/flags.py
index 9123e9ac7..bf83b8e0f 100644
--- a/nova/flags.py
+++ b/nova/flags.py
@@ -358,5 +358,6 @@ DEFINE_string('node_availability_zone', 'nova',
'availability zone of this node')
DEFINE_string('zone_name', 'nova', 'name of this zone')
-DEFINE_string('zone_capabilities', 'kypervisor:xenserver;os:linux',
- 'Key/Value tags which represent capabilities of this zone')
+DEFINE_list('zone_capabilities',
+ ['hypervisor=xenserver;kvm', 'os=linux;windows'],
+ 'Key/Multi-value list representng capabilities of this zone')
diff --git a/nova/image/glance.py b/nova/image/glance.py
index 15fca69b8..9984a3ba1 100644
--- a/nova/image/glance.py
+++ b/nova/image/glance.py
@@ -18,6 +18,8 @@
from __future__ import absolute_import
+import datetime
+
from glance.common import exception as glance_exception
from nova import exception
@@ -37,8 +39,11 @@ GlanceClient = utils.import_class('glance.client.Client')
class GlanceImageService(service.BaseImageService):
"""Provides storage and retrieval of disk image objects within Glance."""
- def __init__(self):
- self.client = GlanceClient(FLAGS.glance_host, FLAGS.glance_port)
+ def __init__(self, client=None):
+ if client is None:
+ self.client = GlanceClient(FLAGS.glance_host, FLAGS.glance_port)
+ else:
+ self.client = client
def index(self, context):
"""
@@ -50,7 +55,8 @@ class GlanceImageService(service.BaseImageService):
"""
Calls out to Glance for a list of detailed image information
"""
- return self.client.get_images_detailed()
+ return [self._convert_timestamps_to_datetimes(image)
+ for image in self.client.get_images_detailed()]
def show(self, context, image_id):
"""
@@ -60,8 +66,23 @@ class GlanceImageService(service.BaseImageService):
image = self.client.get_image_meta(image_id)
except glance_exception.NotFound:
raise exception.NotFound
+ return self._convert_timestamps_to_datetimes(image)
+
+ def _convert_timestamps_to_datetimes(self, image):
+ """
+ Returns image with known timestamp fields converted to datetime objects
+ """
+ for attr in ['created_at', 'updated_at', 'deleted_at']:
+ if image.get(attr):
+ image[attr] = self._parse_glance_iso8601_timestamp(image[attr])
return image
+ def _parse_glance_iso8601_timestamp(self, timestamp):
+ """
+ Parse a subset of iso8601 timestamps into datetime objects
+ """
+ return datetime.datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%f")
+
def show_by_name(self, context, name):
"""
Returns a dict containing image data for the given name.
@@ -88,7 +109,7 @@ class GlanceImageService(service.BaseImageService):
raise exception.NotFound
for chunk in image_chunks:
data.write(chunk)
- return metadata
+ return self._convert_timestamps_to_datetimes(metadata)
def create(self, context, metadata, data=None):
"""
@@ -97,7 +118,8 @@ class GlanceImageService(service.BaseImageService):
:raises AlreadyExists if the image already exist.
"""
- return self.client.add_image(metadata, data)
+ return self._convert_timestamps_to_datetimes(
+ self.client.add_image(metadata, data))
def update(self, context, image_id, metadata, data=None):
"""Replace the contents of the given image with the new data.
@@ -106,10 +128,10 @@ class GlanceImageService(service.BaseImageService):
"""
try:
- result = self.client.update_image(image_id, metadata, data)
+ metadata = self.client.update_image(image_id, metadata, data)
except glance_exception.NotFound:
raise exception.NotFound
- return result
+ return self._convert_timestamps_to_datetimes(metadata)
def delete(self, context, image_id):
"""
diff --git a/nova/image/local.py b/nova/image/local.py
index c4ac3baaa..609d6c42a 100644
--- a/nova/image/local.py
+++ b/nova/image/local.py
@@ -20,8 +20,9 @@ import os.path
import random
import shutil
-from nova import flags
from nova import exception
+from nova import flags
+from nova import log as logging
from nova.image import service
@@ -29,6 +30,8 @@ FLAGS = flags.FLAGS
flags.DEFINE_string('images_path', '$state_path/images',
'path to decrypted images')
+LOG = logging.getLogger('nova.image.local')
+
class LocalImageService(service.BaseImageService):
"""Image service storing images to local disk.
@@ -47,7 +50,17 @@ class LocalImageService(service.BaseImageService):
def _ids(self):
"""The list of all image ids."""
- return [int(i, 16) for i in os.listdir(self._path)]
+ images = []
+ for image_dir in os.listdir(self._path):
+ try:
+ unhexed_image_id = int(image_dir, 16)
+ except ValueError:
+ LOG.error(
+ _("%s is not in correct directory naming format"\
+ % image_dir))
+ else:
+ images.append(unhexed_image_id)
+ return images
def index(self, context):
return [dict(image_id=i['id'], name=i.get('name'))
diff --git a/nova/image/service.py b/nova/image/service.py
index c09052cab..e907381c9 100644
--- a/nova/image/service.py
+++ b/nova/image/service.py
@@ -40,9 +40,9 @@ class BaseImageService(object):
:retval: a sequence of mappings with the following signature
{'id': opaque id of image,
'name': name of image,
- 'created_at': creation timestamp,
- 'updated_at': modification timestamp,
- 'deleted_at': deletion timestamp or None,
+ 'created_at': creation datetime object,
+ 'updated_at': modification datetime object,
+ 'deleted_at': deletion datetime object or None,
'deleted': boolean indicating if image has been deleted,
'status': string description of image status,
'is_public': boolean indicating if image is public
@@ -64,9 +64,9 @@ class BaseImageService(object):
{'id': opaque id of image,
'name': name of image,
- 'created_at': creation timestamp,
- 'updated_at': modification timestamp,
- 'deleted_at': deletion timestamp or None,
+ 'created_at': creation datetime object,
+ 'updated_at': modification datetime object,
+ 'deleted_at': deletion datetime object or None,
'deleted': boolean indicating if image has been deleted,
'status': string description of image status,
'is_public': boolean indicating if image is public
@@ -88,7 +88,7 @@ class BaseImageService(object):
def create(self, context, metadata, data=None):
"""
- Store the image metadata and data and return the new image id.
+ Store the image metadata and data and return the new image metadata.
:raises AlreadyExists if the image already exist.
@@ -96,7 +96,7 @@ class BaseImageService(object):
raise NotImplementedError
def update(self, context, image_id, metadata, data=None):
- """Update the given image with the new metadata and data.
+ """Update the given image metadata and data and return the metadata
:raises NotFound if the image does not exist.
diff --git a/nova/manager.py b/nova/manager.py
index 3d38504bd..804a50479 100644
--- a/nova/manager.py
+++ b/nova/manager.py
@@ -53,11 +53,14 @@ This module provides Manager, a base class for managers.
from nova import utils
from nova import flags
+from nova import log as logging
from nova.db import base
-
+from nova.scheduler import api
FLAGS = flags.FLAGS
+LOG = logging.getLogger('nova.manager')
+
class Manager(base.Base):
def __init__(self, host=None, db_driver=None):
@@ -74,3 +77,29 @@ class Manager(base.Base):
"""Do any initialization that needs to be run if this is a standalone
service. Child classes should override this method."""
pass
+
+
+class SchedulerDependentManager(Manager):
+ """Periodically send capability updates to the Scheduler services.
+ Services that need to update the Scheduler of their capabilities
+ should derive from this class. Otherwise they can derive from
+ manager.Manager directly. Updates are only sent after
+ update_service_capabilities is called with non-None values."""
+
+ def __init__(self, host=None, db_driver=None, service_name="undefined"):
+ self.last_capabilities = None
+ self.service_name = service_name
+ super(SchedulerDependentManager, self).__init__(host, db_driver)
+
+ def update_service_capabilities(self, capabilities):
+ """Remember these capabilities to send on next periodic update."""
+ self.last_capabilities = capabilities
+
+ def periodic_tasks(self, context=None):
+ """Pass data back to the scheduler at a periodic interval"""
+ if self.last_capabilities:
+ LOG.debug(_("Notifying Schedulers of capabilities ..."))
+ api.update_service_capabilities(context, self.service_name,
+ self.host, self.last_capabilities)
+
+ super(SchedulerDependentManager, self).periodic_tasks(context)
diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py
index 7106e6164..796d6ba31 100644
--- a/nova/network/linux_net.py
+++ b/nova/network/linux_net.py
@@ -21,8 +21,6 @@ import inspect
import os
import calendar
-from eventlet import semaphore
-
from nova import db
from nova import exception
from nova import flags
@@ -272,37 +270,30 @@ class IptablesManager(object):
self.ipv4['nat'].add_chain('floating-snat')
self.ipv4['nat'].add_rule('snat', '-j $floating-snat')
- self.semaphore = semaphore.Semaphore()
-
- @utils.synchronized('iptables')
+ @utils.synchronized('iptables', external=True)
def apply(self):
"""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.
-
- We wrap the call in a semaphore lock, so that we don't race with
- ourselves. In the event of a race with another component running
- an iptables-* command at the same time, we retry up to 5 times.
"""
- with self.semaphore:
- s = [('iptables', self.ipv4)]
- if FLAGS.use_ipv6:
- s += [('ip6tables', self.ipv6)]
-
- for cmd, tables in s:
- for table in tables:
- current_table, _ = self.execute('sudo',
- '%s-save' % (cmd,),
- '-t', '%s' % (table,),
- attempts=5)
- current_lines = current_table.split('\n')
- new_filter = self._modify_rules(current_lines,
- tables[table])
- self.execute('sudo', '%s-restore' % (cmd,),
- process_input='\n'.join(new_filter),
- attempts=5)
+ s = [('iptables', self.ipv4)]
+ if FLAGS.use_ipv6:
+ s += [('ip6tables', self.ipv6)]
+
+ for cmd, tables in s:
+ for table in tables:
+ current_table, _ = self.execute('sudo',
+ '%s-save' % (cmd,),
+ '-t', '%s' % (table,),
+ attempts=5)
+ current_lines = current_table.split('\n')
+ new_filter = self._modify_rules(current_lines,
+ tables[table])
+ self.execute('sudo', '%s-restore' % (cmd,),
+ process_input='\n'.join(new_filter),
+ attempts=5)
def _modify_rules(self, current_lines, table, binary=None):
unwrapped_chains = table.unwrapped_chains
@@ -557,6 +548,7 @@ def get_dhcp_hosts(context, network_id):
# NOTE(ja): Sending a HUP only reloads the hostfile, so any
# configuration options (like dchp-range, vlan, ...)
# aren't reloaded.
+@utils.synchronized('dnsmasq_start')
def update_dhcp(context, network_id):
"""(Re)starts a dnsmasq server for a given network
@@ -582,7 +574,7 @@ def update_dhcp(context, network_id):
try:
_execute('sudo', 'kill', '-HUP', pid)
return
- except Exception as exc: # pylint: disable-msg=W0703
+ except Exception as exc: # pylint: disable=W0703
LOG.debug(_("Hupping dnsmasq threw %s"), exc)
else:
LOG.debug(_("Pid %d is stale, relaunching dnsmasq"), pid)
@@ -594,6 +586,7 @@ def update_dhcp(context, network_id):
_execute(*command, addl_env=env)
+@utils.synchronized('radvd_start')
def update_ra(context, network_id):
network_ref = db.network_get(context, network_id)
@@ -626,14 +619,14 @@ interface %s
if conffile in out:
try:
_execute('sudo', 'kill', pid)
- except Exception as exc: # pylint: disable-msg=W0703
+ except Exception as exc: # pylint: disable=W0703
LOG.debug(_("killing radvd threw %s"), exc)
else:
LOG.debug(_("Pid %d is stale, relaunching radvd"), pid)
command = _ra_cmd(network_ref)
_execute(*command)
db.network_update(context, network_id,
- {"ra_server":
+ {"gateway_v6":
utils.get_my_linklocal(network_ref['bridge'])})
@@ -713,7 +706,7 @@ def _stop_dnsmasq(network):
if pid:
try:
_execute('sudo', 'kill', '-TERM', pid)
- except Exception as exc: # pylint: disable-msg=W0703
+ except Exception as exc: # pylint: disable=W0703
LOG.debug(_("Killing dnsmasq threw %s"), exc)
diff --git a/nova/network/manager.py b/nova/network/manager.py
index 3dfc48934..d994f7dc8 100644
--- a/nova/network/manager.py
+++ b/nova/network/manager.py
@@ -73,7 +73,7 @@ flags.DEFINE_string('flat_interface', None,
flags.DEFINE_string('flat_network_dhcp_start', '10.0.0.2',
'Dhcp start for FlatDhcp')
flags.DEFINE_integer('vlan_start', 100, 'First VLAN for private networks')
-flags.DEFINE_integer('num_networks', 1000, 'Number of networks to support')
+flags.DEFINE_integer('num_networks', 1, 'Number of networks to support')
flags.DEFINE_string('vpn_ip', '$my_ip',
'Public IP for the cloudpipe VPN servers')
flags.DEFINE_integer('vpn_start', 1000, 'First Vpn port for private networks')
@@ -105,7 +105,7 @@ class AddressAlreadyAllocated(exception.Error):
pass
-class NetworkManager(manager.Manager):
+class NetworkManager(manager.SchedulerDependentManager):
"""Implements common network manager functionality.
This class must be subclassed to support specific topologies.
@@ -116,7 +116,8 @@ class NetworkManager(manager.Manager):
if not network_driver:
network_driver = FLAGS.network_driver
self.driver = utils.import_object(network_driver)
- super(NetworkManager, self).__init__(*args, **kwargs)
+ super(NetworkManager, self).__init__(service_name='network',
+ *args, **kwargs)
def init_host(self):
"""Do any initialization that needs to be run if this is a
@@ -167,7 +168,7 @@ class NetworkManager(manager.Manager):
# with a network, or a cluster of computes with a network
# and use that network here with a method like
# network_get_by_compute_host
- network_ref = self.db.network_get_by_bridge(context,
+ network_ref = self.db.network_get_by_bridge(context.elevated(),
FLAGS.flat_network_bridge)
address = self.db.fixed_ip_associate_pool(context.elevated(),
network_ref['id'],
@@ -292,9 +293,11 @@ class NetworkManager(manager.Manager):
fixed_net = IPy.IP(cidr)
fixed_net_v6 = IPy.IP(cidr_v6)
significant_bits_v6 = 64
+ network_size_v6 = 1 << 64
count = 1
for index in range(num_networks):
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)
project_net = IPy.IP(cidr)
@@ -313,8 +316,12 @@ class NetworkManager(manager.Manager):
count += 1
if(FLAGS.use_ipv6):
- cidr_v6 = "%s/%s" % (fixed_net_v6[0], significant_bits_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)
+ net['gateway_v6'] = str(project_net_v6[1])
+ net['netmask_v6'] = str(project_net_v6.prefixlen())
network_ref = self.db.network_create_safe(context, net)
@@ -322,12 +329,12 @@ class NetworkManager(manager.Manager):
self._create_fixed_ips(context, network_ref['id'])
@property
- def _bottom_reserved_ips(self): # pylint: disable-msg=R0201
+ def _bottom_reserved_ips(self): # pylint: disable=R0201
"""Number of reserved ips at the bottom of the range."""
return 2 # network, gateway
@property
- def _top_reserved_ips(self): # pylint: disable-msg=R0201
+ def _top_reserved_ips(self): # pylint: disable=R0201
"""Number of reserved ips at the top of the range."""
return 1 # broadcast
diff --git a/nova/objectstore/handler.py b/nova/objectstore/handler.py
index 05ddace4b..554c72848 100644
--- a/nova/objectstore/handler.py
+++ b/nova/objectstore/handler.py
@@ -167,7 +167,7 @@ class S3(ErrorHandlingResource):
def __init__(self):
ErrorHandlingResource.__init__(self)
- def getChild(self, name, request): # pylint: disable-msg=C0103
+ def getChild(self, name, request): # pylint: disable=C0103
"""Returns either the image or bucket resource"""
request.context = get_context(request)
if name == '':
@@ -177,7 +177,7 @@ class S3(ErrorHandlingResource):
else:
return BucketResource(name)
- def render_GET(self, request): # pylint: disable-msg=R0201
+ def render_GET(self, request): # pylint: disable=R0201
"""Renders the GET request for a list of buckets as XML"""
LOG.debug(_('List of buckets requested'), context=request.context)
buckets = [b for b in bucket.Bucket.all()
@@ -355,7 +355,7 @@ class ImagesResource(resource.Resource):
else:
return ImageResource(name)
- def render_GET(self, request): # pylint: disable-msg=R0201
+ def render_GET(self, request): # pylint: disable=R0201
""" returns a json listing of all images
that a user has permissions to see """
@@ -384,7 +384,7 @@ class ImagesResource(resource.Resource):
request.finish()
return server.NOT_DONE_YET
- def render_PUT(self, request): # pylint: disable-msg=R0201
+ def render_PUT(self, request): # pylint: disable=R0201
""" create a new registered image """
image_id = get_argument(request, 'image_id', u'')
@@ -413,7 +413,7 @@ class ImagesResource(resource.Resource):
p.start()
return ''
- def render_POST(self, request): # pylint: disable-msg=R0201
+ def render_POST(self, request): # pylint: disable=R0201
"""Update image attributes: public/private"""
# image_id required for all requests
@@ -441,7 +441,7 @@ class ImagesResource(resource.Resource):
image_object.update_user_editable_fields(clean_args)
return ''
- def render_DELETE(self, request): # pylint: disable-msg=R0201
+ def render_DELETE(self, request): # pylint: disable=R0201
"""Delete a registered image"""
image_id = get_argument(request, "image_id", u"")
image_object = image.Image(image_id)
@@ -471,7 +471,7 @@ def get_application():
application = service.Application("objectstore")
# Disabled because of lack of proper introspection in Twisted
# or possibly different versions of twisted?
- # pylint: disable-msg=E1101
+ # pylint: disable=E1101
objectStoreService = internet.TCPServer(FLAGS.s3_port, factory,
interface=FLAGS.s3_listen_host)
objectStoreService.setServiceParent(application)
diff --git a/nova/rpc.py b/nova/rpc.py
index fbb90299b..388f78d69 100644
--- a/nova/rpc.py
+++ b/nova/rpc.py
@@ -62,7 +62,7 @@ class Connection(carrot_connection.BrokerConnection):
params['backend_cls'] = fakerabbit.Backend
# NOTE(vish): magic is fun!
- # pylint: disable-msg=W0142
+ # pylint: disable=W0142
if new:
return cls(**params)
else:
@@ -114,7 +114,7 @@ class Consumer(messaging.Consumer):
if self.failed_connection:
# NOTE(vish): connection is defined in the parent class, we can
# recreate it as long as we create the backend too
- # pylint: disable-msg=W0201
+ # pylint: disable=W0201
self.connection = Connection.recreate()
self.backend = self.connection.create_backend()
self.declare()
@@ -125,7 +125,7 @@ class Consumer(messaging.Consumer):
# NOTE(vish): This is catching all errors because we really don't
# want exceptions to be logged 10 times a second if some
# persistent failure occurs.
- except Exception: # pylint: disable-msg=W0703
+ except Exception: # pylint: disable=W0703
if not self.failed_connection:
LOG.exception(_("Failed to fetch message from queue"))
self.failed_connection = True
@@ -137,24 +137,7 @@ class Consumer(messaging.Consumer):
return timer
-class Publisher(messaging.Publisher):
- """Publisher base class"""
- pass
-
-
-class TopicConsumer(Consumer):
- """Consumes messages on a specific topic"""
- exchange_type = "topic"
-
- def __init__(self, connection=None, topic="broadcast"):
- self.queue = topic
- self.routing_key = topic
- self.exchange = FLAGS.control_exchange
- self.durable = False
- super(TopicConsumer, self).__init__(connection=connection)
-
-
-class AdapterConsumer(TopicConsumer):
+class AdapterConsumer(Consumer):
"""Calls methods on a proxy object based on method and args"""
def __init__(self, connection=None, topic="broadcast", proxy=None):
LOG.debug(_('Initing the Adapter Consumer for %s') % topic)
@@ -207,6 +190,41 @@ class AdapterConsumer(TopicConsumer):
return
+class Publisher(messaging.Publisher):
+ """Publisher base class"""
+ pass
+
+
+class TopicAdapterConsumer(AdapterConsumer):
+ """Consumes messages on a specific topic"""
+ exchange_type = "topic"
+
+ def __init__(self, connection=None, topic="broadcast", proxy=None):
+ self.queue = topic
+ self.routing_key = topic
+ self.exchange = FLAGS.control_exchange
+ self.durable = False
+ super(TopicAdapterConsumer, self).__init__(connection=connection,
+ topic=topic, proxy=proxy)
+
+
+class FanoutAdapterConsumer(AdapterConsumer):
+ """Consumes messages from a fanout exchange"""
+ exchange_type = "fanout"
+
+ def __init__(self, connection=None, topic="broadcast", proxy=None):
+ self.exchange = "%s_fanout" % topic
+ self.routing_key = topic
+ unique = uuid.uuid4().hex
+ self.queue = "%s_fanout_%s" % (topic, unique)
+ self.durable = False
+ LOG.info(_("Created '%(exchange)s' fanout exchange "
+ "with '%(key)s' routing key"),
+ dict(exchange=self.exchange, key=self.routing_key))
+ super(FanoutAdapterConsumer, self).__init__(connection=connection,
+ topic=topic, proxy=proxy)
+
+
class TopicPublisher(Publisher):
"""Publishes messages on a specific topic"""
exchange_type = "topic"
@@ -218,6 +236,19 @@ class TopicPublisher(Publisher):
super(TopicPublisher, self).__init__(connection=connection)
+class FanoutPublisher(Publisher):
+ """Publishes messages to a fanout exchange."""
+ exchange_type = "fanout"
+
+ def __init__(self, topic, connection=None):
+ self.exchange = "%s_fanout" % topic
+ self.queue = "%s_fanout" % topic
+ self.durable = False
+ LOG.info(_("Creating '%(exchange)s' fanout exchange"),
+ dict(exchange=self.exchange))
+ super(FanoutPublisher, self).__init__(connection=connection)
+
+
class DirectConsumer(Consumer):
"""Consumes messages directly on a channel specified by msg_id"""
exchange_type = "direct"
@@ -311,7 +342,7 @@ def _pack_context(msg, context):
def call(context, topic, msg):
"""Sends a message on a topic and wait for a response"""
- LOG.debug(_("Making asynchronous call..."))
+ 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))
@@ -352,7 +383,7 @@ def call(context, topic, msg):
def cast(context, topic, msg):
"""Sends a message on a topic without waiting for a response"""
- LOG.debug(_("Making asynchronous cast..."))
+ LOG.debug(_("Making asynchronous cast on %s..."), topic)
_pack_context(msg, context)
conn = Connection.instance()
publisher = TopicPublisher(connection=conn, topic=topic)
@@ -360,6 +391,16 @@ def cast(context, topic, 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()
+
+
def generic_response(message_data, message):
"""Logs a result and exits"""
LOG.debug(_('response %s'), message_data)
diff --git a/nova/scheduler/api.py b/nova/scheduler/api.py
index 2405f1343..19a05b716 100644
--- a/nova/scheduler/api.py
+++ b/nova/scheduler/api.py
@@ -25,25 +25,40 @@ FLAGS = flags.FLAGS
LOG = logging.getLogger('nova.scheduler.api')
-class API(object):
- """API for interacting with the scheduler."""
-
- def _call_scheduler(self, method, context, params=None):
- """Generic handler for RPC calls to the scheduler.
-
- :param params: Optional dictionary of arguments to be passed to the
- scheduler worker
-
- :retval: Result returned by scheduler worker
- """
- if not params:
- params = {}
- queue = FLAGS.scheduler_topic
- kwargs = {'method': method, 'args': params}
- return rpc.call(context, queue, kwargs)
-
- def get_zone_list(self, context):
- items = self._call_scheduler('get_zone_list', context)
- for item in items:
- item['api_url'] = item['api_url'].replace('\\/', '/')
- return items
+def _call_scheduler(method, context, params=None):
+ """Generic handler for RPC calls to the scheduler.
+
+ :param params: Optional dictionary of arguments to be passed to the
+ scheduler worker
+
+ :retval: Result returned by scheduler worker
+ """
+ if not params:
+ params = {}
+ queue = FLAGS.scheduler_topic
+ kwargs = {'method': method, 'args': params}
+ return rpc.call(context, queue, kwargs)
+
+
+def get_zone_list(context):
+ """Return a list of zones assoicated with this zone."""
+ items = _call_scheduler('get_zone_list', context)
+ for item in items:
+ item['api_url'] = item['api_url'].replace('\\/', '/')
+ return items
+
+
+def get_zone_capabilities(context, service=None):
+ """Returns a dict of key, value capabilities for this zone,
+ or for a particular class of services running in this zone."""
+ return _call_scheduler('get_zone_capabilities', context=context,
+ params=dict(service=service))
+
+
+def update_service_capabilities(context, service_name, host, capabilities):
+ """Send an update to all the scheduler services informing them
+ of the capabilities of this service."""
+ kwargs = dict(method='update_service_capabilities',
+ args=dict(service_name=service_name, host=host,
+ capabilities=capabilities))
+ return rpc.fanout_cast(context, 'scheduler', kwargs)
diff --git a/nova/scheduler/driver.py b/nova/scheduler/driver.py
index ed3dfe1c0..ce05d9f6a 100644
--- a/nova/scheduler/driver.py
+++ b/nova/scheduler/driver.py
@@ -49,6 +49,13 @@ class WillNotSchedule(exception.Error):
class Scheduler(object):
"""The base class that all Scheduler clases should inherit from."""
+ def __init__(self):
+ self.zone_manager = None
+
+ def set_zone_manager(self, zone_manager):
+ """Called by the Scheduler Service to supply a ZoneManager."""
+ self.zone_manager = zone_manager
+
@staticmethod
def service_is_up(service):
"""Check whether a service is up based on last heartbeat."""
diff --git a/nova/scheduler/manager.py b/nova/scheduler/manager.py
index 053a53356..7d62cfc4e 100644
--- a/nova/scheduler/manager.py
+++ b/nova/scheduler/manager.py
@@ -41,10 +41,11 @@ flags.DEFINE_string('scheduler_driver',
class SchedulerManager(manager.Manager):
"""Chooses a host to run instances on."""
def __init__(self, scheduler_driver=None, *args, **kwargs):
+ self.zone_manager = zone_manager.ZoneManager()
if not scheduler_driver:
scheduler_driver = FLAGS.scheduler_driver
self.driver = utils.import_object(scheduler_driver)
- self.zone_manager = zone_manager.ZoneManager()
+ self.driver.set_zone_manager(self.zone_manager)
super(SchedulerManager, self).__init__(*args, **kwargs)
def __getattr__(self, key):
@@ -59,6 +60,17 @@ class SchedulerManager(manager.Manager):
"""Get a list of zones from the ZoneManager."""
return self.zone_manager.get_zone_list()
+ def get_zone_capabilities(self, context=None, service=None):
+ """Get the normalized set of capabilites for this zone,
+ or for a particular service."""
+ return self.zone_manager.get_zone_capabilities(context, service)
+
+ def update_service_capabilities(self, context=None, service_name=None,
+ host=None, capabilities={}):
+ """Process a capability update from a service node."""
+ self.zone_manager.update_service_capabilities(service_name,
+ host, capabilities)
+
def _schedule(self, method, context, topic, *args, **kwargs):
"""Tries to call schedule_* method on the driver to retrieve host.
diff --git a/nova/scheduler/zone_manager.py b/nova/scheduler/zone_manager.py
index edf9000cc..c1a50dbc3 100644
--- a/nova/scheduler/zone_manager.py
+++ b/nova/scheduler/zone_manager.py
@@ -105,12 +105,36 @@ class ZoneManager(object):
def __init__(self):
self.last_zone_db_check = datetime.min
self.zone_states = {}
+ self.service_states = {} # { <service> : { <host> : { cap k : v }}}
self.green_pool = greenpool.GreenPool()
def get_zone_list(self):
"""Return the list of zones we know about."""
return [zone.to_dict() for zone in self.zone_states.values()]
+ def get_zone_capabilities(self, context, service=None):
+ """Roll up all the individual host info to generic 'service'
+ capabilities. Each capability is aggregated into
+ <cap>_min and <cap>_max values."""
+ service_dict = self.service_states
+ if service:
+ service_dict = {service: self.service_states.get(service, {})}
+
+ # TODO(sandy) - be smarter about fabricating this structure.
+ # But it's likely to change once we understand what the Best-Match
+ # code will need better.
+ combined = {} # { <service>_<cap> : (min, max), ... }
+ for service_name, host_dict in service_dict.iteritems():
+ for host, caps_dict in host_dict.iteritems():
+ for cap, value in caps_dict.iteritems():
+ key = "%s_%s" % (service_name, cap)
+ min_value, max_value = combined.get(key, (value, value))
+ min_value = min(min_value, value)
+ max_value = max(max_value, value)
+ combined[key] = (min_value, max_value)
+
+ return combined
+
def _refresh_from_db(self, context):
"""Make our zone state map match the db."""
# Add/update existing zones ...
@@ -141,3 +165,11 @@ class ZoneManager(object):
self.last_zone_db_check = datetime.now()
self._refresh_from_db(context)
self._poll_zones(context)
+
+ def update_service_capabilities(self, service_name, host, capabilities):
+ """Update the per-service capabilities based on this notification."""
+ logging.debug(_("Received %(service_name)s service update from "
+ "%(host)s: %(capabilities)s") % locals())
+ service_caps = self.service_states.get(service_name, {})
+ service_caps[host] = capabilities
+ self.service_states[service_name] = service_caps
diff --git a/nova/service.py b/nova/service.py
index d60df987c..47c0b96c0 100644
--- a/nova/service.py
+++ b/nova/service.py
@@ -97,18 +97,24 @@ class Service(object):
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.AdapterConsumer(
+ consumer_all = rpc.TopicAdapterConsumer(
connection=conn1,
topic=self.topic,
proxy=self)
- consumer_node = rpc.AdapterConsumer(
+ 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())
pulse = utils.LoopingCall(self.report_state)
pulse.start(interval=self.report_interval, now=False)
@@ -217,7 +223,7 @@ class Service(object):
logging.error(_("Recovered model server connection!"))
# TODO(vish): this should probably only catch connection errors
- except Exception: # pylint: disable-msg=W0702
+ except Exception: # pylint: disable=W0702
if not getattr(self, "model_disconnected", False):
self.model_disconnected = True
logging.exception(_("model server went away"))
diff --git a/nova/tests/api/openstack/__init__.py b/nova/tests/api/openstack/__init__.py
index e18120285..bac7181f7 100644
--- a/nova/tests/api/openstack/__init__.py
+++ b/nova/tests/api/openstack/__init__.py
@@ -20,7 +20,7 @@ from nova import test
from nova import context
from nova import flags
-from nova.api.openstack.ratelimiting import RateLimitingMiddleware
+from nova.api.openstack.limits import RateLimitingMiddleware
from nova.api.openstack.common import limited
from nova.tests.api.openstack import fakes
from webob import Request
diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py
index 0bbb1c890..56143114d 100644
--- a/nova/tests/api/openstack/fakes.py
+++ b/nova/tests/api/openstack/fakes.py
@@ -15,6 +15,7 @@
# License for the specific language governing permissions and limitations
# under the License.
+import copy
import datetime
import json
import random
@@ -34,7 +35,7 @@ from nova import utils
import nova.api.openstack.auth
from nova.api import openstack
from nova.api.openstack import auth
-from nova.api.openstack import ratelimiting
+from nova.api.openstack import limits
from nova.auth.manager import User, Project
from nova.image import glance
from nova.image import local
@@ -72,13 +73,18 @@ def fake_wsgi(self, req):
return self.application
-def wsgi_app(inner_application=None):
- if not inner_application:
- inner_application = openstack.APIRouter()
+def wsgi_app(inner_app10=None, inner_app11=None):
+ if not inner_app10:
+ inner_app10 = openstack.APIRouterV10()
+ if not inner_app11:
+ inner_app11 = openstack.APIRouterV11()
mapper = urlmap.URLMap()
- api = openstack.FaultWrapper(auth.AuthMiddleware(
- ratelimiting.RateLimitingMiddleware(inner_application)))
- mapper['/v1.0'] = api
+ api10 = openstack.FaultWrapper(auth.AuthMiddleware(
+ limits.RateLimitingMiddleware(inner_app10)))
+ api11 = openstack.FaultWrapper(auth.AuthMiddleware(
+ limits.RateLimitingMiddleware(inner_app11)))
+ mapper['/v1.0'] = api10
+ mapper['/v1.1'] = api11
mapper['/'] = openstack.FaultWrapper(openstack.Versions())
return mapper
@@ -115,13 +121,13 @@ def stub_out_auth(stubs):
def stub_out_rate_limiting(stubs):
def fake_rate_init(self, app):
- super(ratelimiting.RateLimitingMiddleware, self).__init__(app)
+ super(limits.RateLimitingMiddleware, self).__init__(app)
self.application = app
- stubs.Set(nova.api.openstack.ratelimiting.RateLimitingMiddleware,
+ stubs.Set(nova.api.openstack.limits.RateLimitingMiddleware,
'__init__', fake_rate_init)
- stubs.Set(nova.api.openstack.ratelimiting.RateLimitingMiddleware,
+ stubs.Set(nova.api.openstack.limits.RateLimitingMiddleware,
'__call__', fake_wsgi)
@@ -149,22 +155,23 @@ def stub_out_glance(stubs, initial_fixtures=None):
for f in self.fixtures]
def fake_get_images_detailed(self):
- return self.fixtures
+ return copy.deepcopy(self.fixtures)
def fake_get_image_meta(self, image_id):
- for f in self.fixtures:
- if f['id'] == image_id:
- return f
+ image = self._find_image(image_id)
+ if image:
+ return copy.deepcopy(image)
raise glance_exc.NotFound
def fake_add_image(self, image_meta, data=None):
+ image_meta = copy.deepcopy(image_meta)
id = ''.join(random.choice(string.letters) for _ in range(20))
image_meta['id'] = id
self.fixtures.append(image_meta)
return image_meta
def fake_update_image(self, image_id, image_meta, data=None):
- f = self.fake_get_image_meta(image_id)
+ f = self._find_image(image_id)
if not f:
raise glance_exc.NotFound
@@ -172,7 +179,7 @@ def stub_out_glance(stubs, initial_fixtures=None):
return f
def fake_delete_image(self, image_id):
- f = self.fake_get_image_meta(image_id)
+ f = self._find_image(image_id)
if not f:
raise glance_exc.NotFound
@@ -181,6 +188,12 @@ def stub_out_glance(stubs, initial_fixtures=None):
##def fake_delete_all(self):
## self.fixtures = []
+ def _find_image(self, image_id):
+ for f in self.fixtures:
+ if f['id'] == image_id:
+ return f
+ return None
+
GlanceClient = glance_client.Client
fake = FakeGlanceClient(initial_fixtures)
@@ -233,52 +246,57 @@ class FakeAuthDatabase(object):
class FakeAuthManager(object):
- auth_data = {}
+ #NOTE(justinsb): Accessing static variables through instances is FUBAR
+ #NOTE(justinsb): This should also be private!
+ auth_data = []
projects = {}
@classmethod
def clear_fakes(cls):
- cls.auth_data = {}
+ cls.auth_data = []
cls.projects = {}
@classmethod
def reset_fake_data(cls):
- cls.auth_data = dict(acc1=User('guy1', 'guy1', 'acc1',
- 'fortytwo!', False))
+ u1 = User('id1', 'guy1', 'acc1', 'secret1', False)
+ cls.auth_data = [u1]
cls.projects = dict(testacct=Project('testacct',
'testacct',
- 'guy1',
+ 'id1',
'test',
[]))
- def add_user(self, key, user):
- FakeAuthManager.auth_data[key] = user
+ def add_user(self, user):
+ FakeAuthManager.auth_data.append(user)
def get_users(self):
- return FakeAuthManager.auth_data.values()
+ return FakeAuthManager.auth_data
def get_user(self, uid):
- for k, v in FakeAuthManager.auth_data.iteritems():
- if v.id == uid:
- return v
+ for user in FakeAuthManager.auth_data:
+ if user.id == uid:
+ return user
+ return None
+
+ def get_user_from_access_key(self, key):
+ for user in FakeAuthManager.auth_data:
+ if user.access == key:
+ return user
return None
def delete_user(self, uid):
- for k, v in FakeAuthManager.auth_data.items():
- if v.id == uid:
- del FakeAuthManager.auth_data[k]
+ for user in FakeAuthManager.auth_data:
+ if user.id == uid:
+ FakeAuthManager.auth_data.remove(user)
return None
def create_user(self, name, access=None, secret=None, admin=False):
u = User(name, name, access, secret, admin)
- FakeAuthManager.auth_data[access] = u
+ FakeAuthManager.auth_data.append(u)
return u
def modify_user(self, user_id, access=None, secret=None, admin=None):
- user = None
- for k, v in FakeAuthManager.auth_data.iteritems():
- if v.id == user_id:
- user = v
+ user = self.get_user(user_id)
if user:
user.access = access
user.secret = secret
@@ -325,12 +343,6 @@ class FakeAuthManager(object):
if (user.id in p.member_ids) or
(user.id == p.project_manager_id)]
- def get_user_from_access_key(self, key):
- try:
- return FakeAuthManager.auth_data[key]
- except KeyError:
- raise exc.NotFound
-
class FakeRateLimiter(object):
def __init__(self, application):
diff --git a/nova/tests/api/openstack/test_accounts.py b/nova/tests/api/openstack/test_accounts.py
index 60edce769..64abcf48c 100644
--- a/nova/tests/api/openstack/test_accounts.py
+++ b/nova/tests/api/openstack/test_accounts.py
@@ -19,11 +19,9 @@ import json
import stubout
import webob
-import nova.api
-import nova.api.openstack.auth
-from nova import context
from nova import flags
from nova import test
+from nova.api.openstack import accounts
from nova.auth.manager import User
from nova.tests.api.openstack import fakes
@@ -44,9 +42,9 @@ class AccountsTest(test.TestCase):
def setUp(self):
super(AccountsTest, self).setUp()
self.stubs = stubout.StubOutForTesting()
- self.stubs.Set(nova.api.openstack.accounts.Controller, '__init__',
+ self.stubs.Set(accounts.Controller, '__init__',
fake_init)
- self.stubs.Set(nova.api.openstack.accounts.Controller, '_check_admin',
+ self.stubs.Set(accounts.Controller, '_check_admin',
fake_admin_check)
fakes.FakeAuthManager.clear_fakes()
fakes.FakeAuthDatabase.data = {}
@@ -57,10 +55,10 @@ class AccountsTest(test.TestCase):
self.allow_admin = FLAGS.allow_admin_api
FLAGS.allow_admin_api = True
fakemgr = fakes.FakeAuthManager()
- joeuser = User('guy1', 'guy1', 'acc1', 'fortytwo!', False)
- superuser = User('guy2', 'guy2', 'acc2', 'swordfish', True)
- fakemgr.add_user(joeuser.access, joeuser)
- fakemgr.add_user(superuser.access, superuser)
+ joeuser = User('id1', 'guy1', 'acc1', 'secret1', False)
+ superuser = User('id2', 'guy2', 'acc2', 'secret2', True)
+ fakemgr.add_user(joeuser)
+ fakemgr.add_user(superuser)
fakemgr.create_project('test1', joeuser)
fakemgr.create_project('test2', superuser)
@@ -76,7 +74,7 @@ class AccountsTest(test.TestCase):
self.assertEqual(res_dict['account']['id'], 'test1')
self.assertEqual(res_dict['account']['name'], 'test1')
- self.assertEqual(res_dict['account']['manager'], 'guy1')
+ self.assertEqual(res_dict['account']['manager'], 'id1')
self.assertEqual(res.status_int, 200)
def test_account_delete(self):
@@ -88,7 +86,7 @@ class AccountsTest(test.TestCase):
def test_account_create(self):
body = dict(account=dict(description='test account',
- manager='guy1'))
+ manager='id1'))
req = webob.Request.blank('/v1.0/accounts/newacct')
req.headers["Content-Type"] = "application/json"
req.method = 'PUT'
@@ -101,14 +99,14 @@ class AccountsTest(test.TestCase):
self.assertEqual(res_dict['account']['id'], 'newacct')
self.assertEqual(res_dict['account']['name'], 'newacct')
self.assertEqual(res_dict['account']['description'], 'test account')
- self.assertEqual(res_dict['account']['manager'], 'guy1')
+ self.assertEqual(res_dict['account']['manager'], 'id1')
self.assertTrue('newacct' in
fakes.FakeAuthManager.projects)
self.assertEqual(len(fakes.FakeAuthManager.projects.values()), 3)
def test_account_update(self):
body = dict(account=dict(description='test account',
- manager='guy2'))
+ manager='id2'))
req = webob.Request.blank('/v1.0/accounts/test1')
req.headers["Content-Type"] = "application/json"
req.method = 'PUT'
@@ -121,5 +119,5 @@ class AccountsTest(test.TestCase):
self.assertEqual(res_dict['account']['id'], 'test1')
self.assertEqual(res_dict['account']['name'], 'test1')
self.assertEqual(res_dict['account']['description'], 'test account')
- self.assertEqual(res_dict['account']['manager'], 'guy2')
+ self.assertEqual(res_dict['account']['manager'], 'id2')
self.assertEqual(len(fakes.FakeAuthManager.projects.values()), 2)
diff --git a/nova/tests/api/openstack/test_adminapi.py b/nova/tests/api/openstack/test_adminapi.py
index 4568cb9f5..e87255b18 100644
--- a/nova/tests/api/openstack/test_adminapi.py
+++ b/nova/tests/api/openstack/test_adminapi.py
@@ -23,7 +23,6 @@ from paste import urlmap
from nova import flags
from nova import test
from nova.api import openstack
-from nova.api.openstack import ratelimiting
from nova.api.openstack import auth
from nova.tests.api.openstack import fakes
diff --git a/nova/tests/api/openstack/test_auth.py b/nova/tests/api/openstack/test_auth.py
index 0448ed701..8f189c744 100644
--- a/nova/tests/api/openstack/test_auth.py
+++ b/nova/tests/api/openstack/test_auth.py
@@ -39,7 +39,7 @@ class Test(test.TestCase):
self.stubs.Set(nova.api.openstack.auth.AuthMiddleware,
'__init__', fakes.fake_auth_init)
self.stubs.Set(context, 'RequestContext', fakes.FakeRequestContext)
- fakes.FakeAuthManager.auth_data = {}
+ fakes.FakeAuthManager.clear_fakes()
fakes.FakeAuthDatabase.data = {}
fakes.stub_out_rate_limiting(self.stubs)
fakes.stub_out_networking(self.stubs)
@@ -51,8 +51,8 @@ class Test(test.TestCase):
def test_authorize_user(self):
f = fakes.FakeAuthManager()
- f.add_user('user1_key',
- nova.auth.manager.User(1, 'user1', None, None, None))
+ user = nova.auth.manager.User('id1', 'user1', 'user1_key', None, None)
+ f.add_user(user)
req = webob.Request.blank('/v1.0/')
req.headers['X-Auth-User'] = 'user1'
@@ -66,9 +66,9 @@ class Test(test.TestCase):
def test_authorize_token(self):
f = fakes.FakeAuthManager()
- u = nova.auth.manager.User(1, 'user1', None, None, None)
- f.add_user('user1_key', u)
- f.create_project('user1_project', u)
+ user = nova.auth.manager.User('id1', 'user1', 'user1_key', None, None)
+ f.add_user(user)
+ f.create_project('user1_project', user)
req = webob.Request.blank('/v1.0/', {'HTTP_HOST': 'foo'})
req.headers['X-Auth-User'] = 'user1'
@@ -83,8 +83,7 @@ class Test(test.TestCase):
self.assertEqual(result.headers['X-Storage-Url'], "")
token = result.headers['X-Auth-Token']
- self.stubs.Set(nova.api.openstack, 'APIRouter',
- fakes.FakeRouter)
+ self.stubs.Set(nova.api.openstack, 'APIRouterV10', fakes.FakeRouter)
req = webob.Request.blank('/v1.0/fake')
req.headers['X-Auth-Token'] = token
result = req.get_response(fakes.wsgi_app())
@@ -124,8 +123,8 @@ class Test(test.TestCase):
def test_bad_user_good_key(self):
f = fakes.FakeAuthManager()
- u = nova.auth.manager.User(1, 'user1', None, None, None)
- f.add_user('user1_key', u)
+ user = nova.auth.manager.User('id1', 'user1', 'user1_key', None, None)
+ f.add_user(user)
req = webob.Request.blank('/v1.0/')
req.headers['X-Auth-User'] = 'unknown_user'
@@ -179,7 +178,7 @@ class TestLimiter(test.TestCase):
self.stubs.Set(nova.api.openstack.auth.AuthMiddleware,
'__init__', fakes.fake_auth_init)
self.stubs.Set(context, 'RequestContext', fakes.FakeRequestContext)
- fakes.FakeAuthManager.auth_data = {}
+ fakes.FakeAuthManager.clear_fakes()
fakes.FakeAuthDatabase.data = {}
fakes.stub_out_networking(self.stubs)
@@ -190,9 +189,9 @@ class TestLimiter(test.TestCase):
def test_authorize_token(self):
f = fakes.FakeAuthManager()
- u = nova.auth.manager.User(1, 'user1', None, None, None)
- f.add_user('user1_key', u)
- f.create_project('test', u)
+ user = nova.auth.manager.User('id1', 'user1', 'user1_key', None, None)
+ f.add_user(user)
+ f.create_project('test', user)
req = webob.Request.blank('/v1.0/')
req.headers['X-Auth-User'] = 'user1'
@@ -201,8 +200,7 @@ class TestLimiter(test.TestCase):
self.assertEqual(len(result.headers['X-Auth-Token']), 40)
token = result.headers['X-Auth-Token']
- self.stubs.Set(nova.api.openstack, 'APIRouter',
- fakes.FakeRouter)
+ self.stubs.Set(nova.api.openstack, 'APIRouterV10', fakes.FakeRouter)
req = webob.Request.blank('/v1.0/fake')
req.method = 'POST'
req.headers['X-Auth-Token'] = token
diff --git a/nova/tests/api/openstack/test_flavors.py b/nova/tests/api/openstack/test_flavors.py
index 8280a505f..4f504808c 100644
--- a/nova/tests/api/openstack/test_flavors.py
+++ b/nova/tests/api/openstack/test_flavors.py
@@ -15,17 +15,39 @@
# License for the specific language governing permissions and limitations
# under the License.
+import json
import stubout
import webob
from nova import test
import nova.api
from nova import context
-from nova import db
from nova.api.openstack import flavors
+from nova import db
from nova.tests.api.openstack import fakes
+def stub_flavor(flavorid, name, memory_mb="256", local_gb="10"):
+ return {
+ "flavorid": str(flavorid),
+ "name": name,
+ "memory_mb": memory_mb,
+ "local_gb": local_gb,
+ }
+
+
+def return_instance_type_by_flavor_id(context, flavorid):
+ return stub_flavor(flavorid, "flavor %s" % (flavorid,))
+
+
+def return_instance_types(context, num=2):
+ instance_types = {}
+ for i in xrange(1, num + 1):
+ name = "flavor %s" % (i,)
+ instance_types[name] = stub_flavor(i, name)
+ return instance_types
+
+
class FlavorsTest(test.TestCase):
def setUp(self):
super(FlavorsTest, self).setUp()
@@ -35,6 +57,10 @@ class FlavorsTest(test.TestCase):
fakes.stub_out_networking(self.stubs)
fakes.stub_out_rate_limiting(self.stubs)
fakes.stub_out_auth(self.stubs)
+ self.stubs.Set(nova.db.api, "instance_type_get_all",
+ return_instance_types)
+ self.stubs.Set(nova.db.api, "instance_type_get_by_flavor_id",
+ return_instance_type_by_flavor_id)
self.context = context.get_admin_context()
def tearDown(self):
@@ -45,8 +71,49 @@ class FlavorsTest(test.TestCase):
req = webob.Request.blank('/v1.0/flavors')
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 200)
+ flavors = json.loads(res.body)["flavors"]
+ expected = [
+ {
+ "id": "1",
+ "name": "flavor 1",
+ },
+ {
+ "id": "2",
+ "name": "flavor 2",
+ },
+ ]
+ self.assertEqual(flavors, expected)
+
+ def test_get_flavor_list_detail(self):
+ req = webob.Request.blank('/v1.0/flavors/detail')
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ flavors = json.loads(res.body)["flavors"]
+ expected = [
+ {
+ "id": "1",
+ "name": "flavor 1",
+ "ram": "256",
+ "disk": "10",
+ },
+ {
+ "id": "2",
+ "name": "flavor 2",
+ "ram": "256",
+ "disk": "10",
+ },
+ ]
+ self.assertEqual(flavors, expected)
def test_get_flavor_by_id(self):
- req = webob.Request.blank('/v1.0/flavors/1')
+ req = webob.Request.blank('/v1.0/flavors/12')
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 200)
+ flavor = json.loads(res.body)["flavor"]
+ expected = {
+ "id": "12",
+ "name": "flavor 12",
+ "ram": "256",
+ "disk": "10",
+ }
+ self.assertEqual(flavor, expected)
diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py
index 76f758929..feb32ed9f 100644
--- a/nova/tests/api/openstack/test_images.py
+++ b/nova/tests/api/openstack/test_images.py
@@ -22,6 +22,7 @@ and as a WSGI layer
import json
import datetime
+import os
import shutil
import tempfile
@@ -151,6 +152,17 @@ class LocalImageServiceTest(test.TestCase,
self.stubs.UnsetAll()
super(LocalImageServiceTest, self).tearDown()
+ def test_get_all_ids_with_incorrect_directory_formats(self):
+ # create some old-style image directories (starting with 'ami-')
+ for x in [1, 2, 3]:
+ tempfile.mkstemp(prefix='ami-', dir=self.tempdir)
+ # create some valid image directories names
+ for x in ["1485baed", "1a60f0ee", "3123a73d"]:
+ os.makedirs(os.path.join(self.tempdir, x))
+ found_image_ids = self.service._ids()
+ self.assertEqual(True, isinstance(found_image_ids, list))
+ self.assertEqual(3, len(found_image_ids), len(found_image_ids))
+
class GlanceImageServiceTest(test.TestCase,
BaseImageServiceTests):
@@ -177,13 +189,13 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
"""Test of the OpenStack API /images application controller"""
# Registered images at start of each test.
-
+ now = datetime.datetime.utcnow()
IMAGE_FIXTURES = [
{'id': '23g2ogk23k4hhkk4k42l',
'imageId': '23g2ogk23k4hhkk4k42l',
'name': 'public image #1',
- 'created_at': str(datetime.datetime.utcnow()),
- 'updated_at': str(datetime.datetime.utcnow()),
+ 'created_at': now.isoformat(),
+ 'updated_at': now.isoformat(),
'deleted_at': None,
'deleted': False,
'is_public': True,
@@ -192,8 +204,8 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
{'id': 'slkduhfas73kkaskgdas',
'imageId': 'slkduhfas73kkaskgdas',
'name': 'public image #2',
- 'created_at': str(datetime.datetime.utcnow()),
- 'updated_at': str(datetime.datetime.utcnow()),
+ 'created_at': now.isoformat(),
+ 'updated_at': now.isoformat(),
'deleted_at': None,
'deleted': False,
'is_public': True,
@@ -235,20 +247,20 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
res = req.get_response(fakes.wsgi_app())
res_dict = json.loads(res.body)
- def _is_equivalent_subset(x, y):
- if set(x) <= set(y):
- for k, v in x.iteritems():
- if x[k] != y[k]:
- if x[k] == 'active' and y[k] == 'available':
- continue
- return False
- return True
- return False
-
- for image in res_dict['images']:
- for image_fixture in self.IMAGE_FIXTURES:
- if _is_equivalent_subset(image, image_fixture):
- break
- else:
- self.assertEquals(1, 2, "image %s not in fixtures!" %
- str(image))
+ for image in self.IMAGE_FIXTURES:
+ expected = {
+ 'id': abs(hash(image['imageId'])),
+ 'name': image['name'],
+ 'status': 'active',
+ }
+ self.assertTrue(expected in res_dict['images'])
+
+ def test_show_image(self):
+ expected = self.IMAGE_FIXTURES[0]
+ id = abs(hash(expected['id']))
+ expected_time = self.now.strftime('%Y-%m-%dT%H:%M:%SZ')
+ req = webob.Request.blank('/v1.0/images/%s' % id)
+ res = req.get_response(fakes.wsgi_app())
+ actual = json.loads(res.body)['image']
+ self.assertEqual(expected_time, actual['created_at'])
+ self.assertEqual(expected_time, actual['updated_at'])
diff --git a/nova/tests/api/openstack/test_limits.py b/nova/tests/api/openstack/test_limits.py
new file mode 100644
index 000000000..05cfacc60
--- /dev/null
+++ b/nova/tests/api/openstack/test_limits.py
@@ -0,0 +1,584 @@
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""
+Tests dealing with HTTP rate-limiting.
+"""
+
+import httplib
+import json
+import StringIO
+import stubout
+import time
+import unittest
+import webob
+
+from xml.dom.minidom import parseString
+
+from nova.api.openstack import limits
+from nova.api.openstack.limits import Limit
+
+
+TEST_LIMITS = [
+ Limit("GET", "/delayed", "^/delayed", 1, limits.PER_MINUTE),
+ Limit("POST", "*", ".*", 7, limits.PER_MINUTE),
+ Limit("POST", "/servers", "^/servers", 3, limits.PER_MINUTE),
+ Limit("PUT", "*", "", 10, limits.PER_MINUTE),
+ Limit("PUT", "/servers", "^/servers", 5, limits.PER_MINUTE),
+]
+
+
+class BaseLimitTestSuite(unittest.TestCase):
+ """Base test suite which provides relevant stubs and time abstraction."""
+
+ def setUp(self):
+ """Run before each test."""
+ self.time = 0.0
+ self.stubs = stubout.StubOutForTesting()
+ self.stubs.Set(limits.Limit, "_get_time", self._get_time)
+
+ def tearDown(self):
+ """Run after each test."""
+ self.stubs.UnsetAll()
+
+ def _get_time(self):
+ """Return the "time" according to this test suite."""
+ return self.time
+
+
+class LimitsControllerTest(BaseLimitTestSuite):
+ """
+ Tests for `limits.LimitsController` class.
+ """
+
+ def setUp(self):
+ """Run before each test."""
+ BaseLimitTestSuite.setUp(self)
+ self.controller = limits.LimitsController()
+
+ def _get_index_request(self, accept_header="application/json"):
+ """Helper to set routing arguments."""
+ request = webob.Request.blank("/")
+ request.accept = accept_header
+ request.environ["wsgiorg.routing_args"] = (None, {
+ "action": "index",
+ "controller": "",
+ })
+ return request
+
+ def _populate_limits(self, request):
+ """Put limit info into a request."""
+ _limits = [
+ Limit("GET", "*", ".*", 10, 60).display(),
+ Limit("POST", "*", ".*", 5, 60 * 60).display(),
+ ]
+ request.environ["nova.limits"] = _limits
+ return request
+
+ def test_empty_index_json(self):
+ """Test getting empty limit details in JSON."""
+ request = self._get_index_request()
+ response = request.get_response(self.controller)
+ expected = {
+ "limits": {
+ "rate": [],
+ "absolute": {},
+ },
+ }
+ body = json.loads(response.body)
+ self.assertEqual(expected, body)
+
+ def test_index_json(self):
+ """Test getting limit details in JSON."""
+ request = self._get_index_request()
+ request = self._populate_limits(request)
+ response = request.get_response(self.controller)
+ expected = {
+ "limits": {
+ "rate": [{
+ "regex": ".*",
+ "resetTime": 0,
+ "URI": "*",
+ "value": 10,
+ "verb": "GET",
+ "remaining": 10,
+ "unit": "MINUTE",
+ },
+ {
+ "regex": ".*",
+ "resetTime": 0,
+ "URI": "*",
+ "value": 5,
+ "verb": "POST",
+ "remaining": 5,
+ "unit": "HOUR",
+ }],
+ "absolute": {},
+ },
+ }
+ body = json.loads(response.body)
+ self.assertEqual(expected, body)
+
+ def test_empty_index_xml(self):
+ """Test getting limit details in XML."""
+ request = self._get_index_request("application/xml")
+ response = request.get_response(self.controller)
+
+ expected = "<limits><rate/><absolute/></limits>"
+ body = response.body.replace("\n", "").replace(" ", "")
+
+ self.assertEqual(expected, body)
+
+ def test_index_xml(self):
+ """Test getting limit details in XML."""
+ request = self._get_index_request("application/xml")
+ request = self._populate_limits(request)
+ response = request.get_response(self.controller)
+
+ expected = parseString("""
+ <limits>
+ <rate>
+ <limit URI="*" regex=".*" remaining="10" resetTime="0"
+ unit="MINUTE" value="10" verb="GET"/>
+ <limit URI="*" regex=".*" remaining="5" resetTime="0"
+ unit="HOUR" value="5" verb="POST"/>
+ </rate>
+ <absolute/>
+ </limits>
+ """.replace(" ", ""))
+ body = parseString(response.body.replace(" ", ""))
+
+ self.assertEqual(expected.toxml(), body.toxml())
+
+
+class LimitMiddlewareTest(BaseLimitTestSuite):
+ """
+ Tests for the `limits.RateLimitingMiddleware` class.
+ """
+
+ @webob.dec.wsgify
+ def _empty_app(self, request):
+ """Do-nothing WSGI app."""
+ pass
+
+ def setUp(self):
+ """Prepare middleware for use through fake WSGI app."""
+ BaseLimitTestSuite.setUp(self)
+ _limits = [
+ Limit("GET", "*", ".*", 1, 60),
+ ]
+ self.app = limits.RateLimitingMiddleware(self._empty_app, _limits)
+
+ def test_good_request(self):
+ """Test successful GET request through middleware."""
+ request = webob.Request.blank("/")
+ response = request.get_response(self.app)
+ self.assertEqual(200, response.status_int)
+
+ def test_limited_request_json(self):
+ """Test a rate-limited (403) GET request through middleware."""
+ request = webob.Request.blank("/")
+ response = request.get_response(self.app)
+ self.assertEqual(200, response.status_int)
+
+ request = webob.Request.blank("/")
+ response = request.get_response(self.app)
+ self.assertEqual(response.status_int, 403)
+
+ body = json.loads(response.body)
+ expected = "Only 1 GET request(s) can be made to * every minute."
+ value = body["overLimitFault"]["details"].strip()
+ self.assertEqual(value, expected)
+
+ def test_limited_request_xml(self):
+ """Test a rate-limited (403) response as XML"""
+ request = webob.Request.blank("/")
+ response = request.get_response(self.app)
+ self.assertEqual(200, response.status_int)
+
+ request = webob.Request.blank("/")
+ request.accept = "application/xml"
+ response = request.get_response(self.app)
+ self.assertEqual(response.status_int, 403)
+
+ root = parseString(response.body).childNodes[0]
+ expected = "Only 1 GET request(s) can be made to * every minute."
+
+ details = root.getElementsByTagName("details")
+ self.assertEqual(details.length, 1)
+
+ value = details.item(0).firstChild.data.strip()
+ self.assertEqual(value, expected)
+
+
+class LimitTest(BaseLimitTestSuite):
+ """
+ Tests for the `limits.Limit` class.
+ """
+
+ def test_GET_no_delay(self):
+ """Test a limit handles 1 GET per second."""
+ limit = Limit("GET", "*", ".*", 1, 1)
+ delay = limit("GET", "/anything")
+ self.assertEqual(None, delay)
+ self.assertEqual(0, limit.next_request)
+ self.assertEqual(0, limit.last_request)
+
+ def test_GET_delay(self):
+ """Test two calls to 1 GET per second limit."""
+ limit = Limit("GET", "*", ".*", 1, 1)
+ delay = limit("GET", "/anything")
+ self.assertEqual(None, delay)
+
+ delay = limit("GET", "/anything")
+ self.assertEqual(1, delay)
+ self.assertEqual(1, limit.next_request)
+ self.assertEqual(0, limit.last_request)
+
+ self.time += 4
+
+ delay = limit("GET", "/anything")
+ self.assertEqual(None, delay)
+ self.assertEqual(4, limit.next_request)
+ self.assertEqual(4, limit.last_request)
+
+
+class LimiterTest(BaseLimitTestSuite):
+ """
+ Tests for the in-memory `limits.Limiter` class.
+ """
+
+ def setUp(self):
+ """Run before each test."""
+ BaseLimitTestSuite.setUp(self)
+ self.limiter = limits.Limiter(TEST_LIMITS)
+
+ def _check(self, num, verb, url, username=None):
+ """Check and yield results from checks."""
+ for x in xrange(num):
+ yield self.limiter.check_for_delay(verb, url, username)[0]
+
+ def _check_sum(self, num, verb, url, username=None):
+ """Check and sum results from checks."""
+ results = self._check(num, verb, url, username)
+ return sum(item for item in results if item)
+
+ def test_no_delay_GET(self):
+ """
+ Simple test to ensure no delay on a single call for a limit verb we
+ didn"t set.
+ """
+ delay = self.limiter.check_for_delay("GET", "/anything")
+ self.assertEqual(delay, (None, None))
+
+ def test_no_delay_PUT(self):
+ """
+ Simple test to ensure no delay on a single call for a known limit.
+ """
+ delay = self.limiter.check_for_delay("PUT", "/anything")
+ self.assertEqual(delay, (None, None))
+
+ def test_delay_PUT(self):
+ """
+ Ensure the 11th PUT will result in a delay of 6.0 seconds until
+ the next request will be granced.
+ """
+ expected = [None] * 10 + [6.0]
+ results = list(self._check(11, "PUT", "/anything"))
+
+ self.assertEqual(expected, results)
+
+ def test_delay_POST(self):
+ """
+ Ensure the 8th POST will result in a delay of 6.0 seconds until
+ the next request will be granced.
+ """
+ expected = [None] * 7
+ results = list(self._check(7, "POST", "/anything"))
+ self.assertEqual(expected, results)
+
+ expected = 60.0 / 7.0
+ results = self._check_sum(1, "POST", "/anything")
+ self.failUnlessAlmostEqual(expected, results, 8)
+
+ def test_delay_GET(self):
+ """
+ Ensure the 11th GET will result in NO delay.
+ """
+ expected = [None] * 11
+ results = list(self._check(11, "GET", "/anything"))
+
+ self.assertEqual(expected, results)
+
+ def test_delay_PUT_servers(self):
+ """
+ Ensure PUT on /servers limits at 5 requests, and PUT elsewhere is still
+ OK after 5 requests...but then after 11 total requests, PUT limiting
+ kicks in.
+ """
+ # First 6 requests on PUT /servers
+ expected = [None] * 5 + [12.0]
+ results = list(self._check(6, "PUT", "/servers"))
+ self.assertEqual(expected, results)
+
+ # Next 5 request on PUT /anything
+ expected = [None] * 4 + [6.0]
+ results = list(self._check(5, "PUT", "/anything"))
+ self.assertEqual(expected, results)
+
+ def test_delay_PUT_wait(self):
+ """
+ Ensure after hitting the limit and then waiting for the correct
+ amount of time, the limit will be lifted.
+ """
+ expected = [None] * 10 + [6.0]
+ results = list(self._check(11, "PUT", "/anything"))
+ self.assertEqual(expected, results)
+
+ # Advance time
+ self.time += 6.0
+
+ expected = [None, 6.0]
+ results = list(self._check(2, "PUT", "/anything"))
+ self.assertEqual(expected, results)
+
+ def test_multiple_delays(self):
+ """
+ Ensure multiple requests still get a delay.
+ """
+ expected = [None] * 10 + [6.0] * 10
+ results = list(self._check(20, "PUT", "/anything"))
+ self.assertEqual(expected, results)
+
+ self.time += 1.0
+
+ expected = [5.0] * 10
+ results = list(self._check(10, "PUT", "/anything"))
+ self.assertEqual(expected, results)
+
+ def test_multiple_users(self):
+ """
+ Tests involving multiple users.
+ """
+ # User1
+ expected = [None] * 10 + [6.0] * 10
+ results = list(self._check(20, "PUT", "/anything", "user1"))
+ self.assertEqual(expected, results)
+
+ # User2
+ expected = [None] * 10 + [6.0] * 5
+ results = list(self._check(15, "PUT", "/anything", "user2"))
+ self.assertEqual(expected, results)
+
+ self.time += 1.0
+
+ # User1 again
+ expected = [5.0] * 10
+ results = list(self._check(10, "PUT", "/anything", "user1"))
+ self.assertEqual(expected, results)
+
+ self.time += 1.0
+
+ # User1 again
+ expected = [4.0] * 5
+ results = list(self._check(5, "PUT", "/anything", "user2"))
+ self.assertEqual(expected, results)
+
+
+class WsgiLimiterTest(BaseLimitTestSuite):
+ """
+ Tests for `limits.WsgiLimiter` class.
+ """
+
+ def setUp(self):
+ """Run before each test."""
+ BaseLimitTestSuite.setUp(self)
+ self.app = limits.WsgiLimiter(TEST_LIMITS)
+
+ def _request_data(self, verb, path):
+ """Get data decribing a limit request verb/path."""
+ return json.dumps({"verb": verb, "path": path})
+
+ def _request(self, verb, url, username=None):
+ """Make sure that POSTing to the given url causes the given username
+ to perform the given action. Make the internal rate limiter return
+ delay and make sure that the WSGI app returns the correct response.
+ """
+ if username:
+ request = webob.Request.blank("/%s" % username)
+ else:
+ request = webob.Request.blank("/")
+
+ request.method = "POST"
+ request.body = self._request_data(verb, url)
+ response = request.get_response(self.app)
+
+ if "X-Wait-Seconds" in response.headers:
+ self.assertEqual(response.status_int, 403)
+ return response.headers["X-Wait-Seconds"]
+
+ self.assertEqual(response.status_int, 204)
+
+ def test_invalid_methods(self):
+ """Only POSTs should work."""
+ requests = []
+ for method in ["GET", "PUT", "DELETE", "HEAD", "OPTIONS"]:
+ request = webob.Request.blank("/")
+ request.body = self._request_data("GET", "/something")
+ response = request.get_response(self.app)
+ self.assertEqual(response.status_int, 405)
+
+ def test_good_url(self):
+ delay = self._request("GET", "/something")
+ self.assertEqual(delay, None)
+
+ def test_escaping(self):
+ delay = self._request("GET", "/something/jump%20up")
+ self.assertEqual(delay, None)
+
+ def test_response_to_delays(self):
+ delay = self._request("GET", "/delayed")
+ self.assertEqual(delay, None)
+
+ delay = self._request("GET", "/delayed")
+ self.assertEqual(delay, '60.00')
+
+ def test_response_to_delays_usernames(self):
+ delay = self._request("GET", "/delayed", "user1")
+ self.assertEqual(delay, None)
+
+ delay = self._request("GET", "/delayed", "user2")
+ self.assertEqual(delay, None)
+
+ delay = self._request("GET", "/delayed", "user1")
+ self.assertEqual(delay, '60.00')
+
+ delay = self._request("GET", "/delayed", "user2")
+ self.assertEqual(delay, '60.00')
+
+
+class FakeHttplibSocket(object):
+ """
+ Fake `httplib.HTTPResponse` replacement.
+ """
+
+ def __init__(self, response_string):
+ """Initialize new `FakeHttplibSocket`."""
+ self._buffer = StringIO.StringIO(response_string)
+
+ def makefile(self, _mode, _other):
+ """Returns the socket's internal buffer."""
+ return self._buffer
+
+
+class FakeHttplibConnection(object):
+ """
+ Fake `httplib.HTTPConnection`.
+ """
+
+ def __init__(self, app, host):
+ """
+ Initialize `FakeHttplibConnection`.
+ """
+ self.app = app
+ self.host = host
+
+ def request(self, method, path, body="", headers={}):
+ """
+ Requests made via this connection actually get translated and routed
+ into our WSGI app, we then wait for the response and turn it back into
+ an `httplib.HTTPResponse`.
+ """
+ req = webob.Request.blank(path)
+ req.method = method
+ req.headers = headers
+ req.host = self.host
+ req.body = body
+
+ resp = str(req.get_response(self.app))
+ resp = "HTTP/1.0 %s" % resp
+ sock = FakeHttplibSocket(resp)
+ self.http_response = httplib.HTTPResponse(sock)
+ self.http_response.begin()
+
+ def getresponse(self):
+ """Return our generated response from the request."""
+ return self.http_response
+
+
+def wire_HTTPConnection_to_WSGI(host, app):
+ """Monkeypatches HTTPConnection so that if you try to connect to host, you
+ are instead routed straight to the given WSGI app.
+
+ After calling this method, when any code calls
+
+ httplib.HTTPConnection(host)
+
+ the connection object will be a fake. Its requests will be sent directly
+ to the given WSGI app rather than through a socket.
+
+ Code connecting to hosts other than host will not be affected.
+
+ This method may be called multiple times to map different hosts to
+ different apps.
+ """
+ class HTTPConnectionDecorator(object):
+ """Wraps the real HTTPConnection class so that when you instantiate
+ the class you might instead get a fake instance."""
+
+ def __init__(self, wrapped):
+ self.wrapped = wrapped
+
+ def __call__(self, connection_host, *args, **kwargs):
+ if connection_host == host:
+ return FakeHttplibConnection(app, host)
+ else:
+ return self.wrapped(connection_host, *args, **kwargs)
+
+ httplib.HTTPConnection = HTTPConnectionDecorator(httplib.HTTPConnection)
+
+
+class WsgiLimiterProxyTest(BaseLimitTestSuite):
+ """
+ Tests for the `limits.WsgiLimiterProxy` class.
+ """
+
+ def setUp(self):
+ """
+ Do some nifty HTTP/WSGI magic which allows for WSGI to be called
+ directly by something like the `httplib` library.
+ """
+ BaseLimitTestSuite.setUp(self)
+ self.app = limits.WsgiLimiter(TEST_LIMITS)
+ wire_HTTPConnection_to_WSGI("169.254.0.1:80", self.app)
+ self.proxy = limits.WsgiLimiterProxy("169.254.0.1:80")
+
+ def test_200(self):
+ """Successful request test."""
+ delay = self.proxy.check_for_delay("GET", "/anything")
+ self.assertEqual(delay, (None, None))
+
+ def test_403(self):
+ """Forbidden request test."""
+ delay = self.proxy.check_for_delay("GET", "/delayed")
+ self.assertEqual(delay, (None, None))
+
+ delay, error = self.proxy.check_for_delay("GET", "/delayed")
+ error = error.strip()
+
+ expected = ("60.00", "403 Forbidden\n\nOnly 1 GET request(s) can be "\
+ "made to /delayed every minute.")
+
+ self.assertEqual((delay, error), expected)
diff --git a/nova/tests/api/openstack/test_ratelimiting.py b/nova/tests/api/openstack/test_ratelimiting.py
deleted file mode 100644
index 9ae90ee20..000000000
--- a/nova/tests/api/openstack/test_ratelimiting.py
+++ /dev/null
@@ -1,243 +0,0 @@
-import httplib
-import StringIO
-import time
-import webob
-
-from nova import test
-import nova.api.openstack.ratelimiting as ratelimiting
-
-
-class LimiterTest(test.TestCase):
-
- def setUp(self):
- super(LimiterTest, self).setUp()
- self.limits = {
- 'a': (5, ratelimiting.PER_SECOND),
- 'b': (5, ratelimiting.PER_MINUTE),
- 'c': (5, ratelimiting.PER_HOUR),
- 'd': (1, ratelimiting.PER_SECOND),
- 'e': (100, ratelimiting.PER_SECOND)}
- self.rl = ratelimiting.Limiter(self.limits)
-
- def exhaust(self, action, times_until_exhausted, **kwargs):
- for i in range(times_until_exhausted):
- when = self.rl.perform(action, **kwargs)
- self.assertEqual(when, None)
- num, period = self.limits[action]
- delay = period * 1.0 / num
- # Verify that we are now thoroughly delayed
- for i in range(10):
- when = self.rl.perform(action, **kwargs)
- self.assertAlmostEqual(when, delay, 2)
-
- def test_second(self):
- self.exhaust('a', 5)
- time.sleep(0.2)
- self.exhaust('a', 1)
- time.sleep(1)
- self.exhaust('a', 5)
-
- def test_minute(self):
- self.exhaust('b', 5)
-
- def test_one_per_period(self):
- def allow_once_and_deny_once():
- when = self.rl.perform('d')
- self.assertEqual(when, None)
- when = self.rl.perform('d')
- self.assertAlmostEqual(when, 1, 2)
- return when
- time.sleep(allow_once_and_deny_once())
- time.sleep(allow_once_and_deny_once())
- allow_once_and_deny_once()
-
- def test_we_can_go_indefinitely_if_we_spread_out_requests(self):
- for i in range(200):
- when = self.rl.perform('e')
- self.assertEqual(when, None)
- time.sleep(0.01)
-
- def test_users_get_separate_buckets(self):
- self.exhaust('c', 5, username='alice')
- self.exhaust('c', 5, username='bob')
- self.exhaust('c', 5, username='chuck')
- self.exhaust('c', 0, username='chuck')
- self.exhaust('c', 0, username='bob')
- self.exhaust('c', 0, username='alice')
-
-
-class FakeLimiter(object):
- """Fake Limiter class that you can tell how to behave."""
-
- def __init__(self, test):
- self._action = self._username = self._delay = None
- self.test = test
-
- def mock(self, action, username, delay):
- self._action = action
- self._username = username
- self._delay = delay
-
- def perform(self, action, username):
- self.test.assertEqual(action, self._action)
- self.test.assertEqual(username, self._username)
- return self._delay
-
-
-class WSGIAppTest(test.TestCase):
-
- def setUp(self):
- super(WSGIAppTest, self).setUp()
- self.limiter = FakeLimiter(self)
- self.app = ratelimiting.WSGIApp(self.limiter)
-
- def test_invalid_methods(self):
- requests = []
- for method in ['GET', 'PUT', 'DELETE']:
- req = webob.Request.blank('/limits/michael/breakdance',
- dict(REQUEST_METHOD=method))
- requests.append(req)
- for req in requests:
- self.assertEqual(req.get_response(self.app).status_int, 405)
-
- def test_invalid_urls(self):
- requests = []
- for prefix in ['limit', '', 'limiter2', 'limiter/limits', 'limiter/1']:
- req = webob.Request.blank('/%s/michael/breakdance' % prefix,
- dict(REQUEST_METHOD='POST'))
- requests.append(req)
- for req in requests:
- self.assertEqual(req.get_response(self.app).status_int, 404)
-
- def verify(self, url, username, action, delay=None):
- """Make sure that POSTing to the given url causes the given username
- to perform the given action. Make the internal rate limiter return
- delay and make sure that the WSGI app returns the correct response.
- """
- req = webob.Request.blank(url, dict(REQUEST_METHOD='POST'))
- self.limiter.mock(action, username, delay)
- resp = req.get_response(self.app)
- if not delay:
- self.assertEqual(resp.status_int, 200)
- else:
- self.assertEqual(resp.status_int, 403)
- self.assertEqual(resp.headers['X-Wait-Seconds'], "%.2f" % delay)
-
- def test_good_urls(self):
- self.verify('/limiter/michael/hoot', 'michael', 'hoot')
-
- def test_escaping(self):
- self.verify('/limiter/michael/jump%20up', 'michael', 'jump up')
-
- def test_response_to_delays(self):
- self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1)
- self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1.56)
- self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1000)
-
-
-class FakeHttplibSocket(object):
- """a fake socket implementation for httplib.HTTPResponse, trivial"""
-
- def __init__(self, response_string):
- self._buffer = StringIO.StringIO(response_string)
-
- def makefile(self, _mode, _other):
- """Returns the socket's internal buffer"""
- return self._buffer
-
-
-class FakeHttplibConnection(object):
- """A fake httplib.HTTPConnection
-
- Requests made via this connection actually get translated and routed into
- our WSGI app, we then wait for the response and turn it back into
- an httplib.HTTPResponse.
- """
- def __init__(self, app, host, is_secure=False):
- self.app = app
- self.host = host
-
- def request(self, method, path, data='', headers={}):
- req = webob.Request.blank(path)
- req.method = method
- req.body = data
- req.headers = headers
- req.host = self.host
- # Call the WSGI app, get the HTTP response
- resp = str(req.get_response(self.app))
- # For some reason, the response doesn't have "HTTP/1.0 " prepended; I
- # guess that's a function the web server usually provides.
- resp = "HTTP/1.0 %s" % resp
- sock = FakeHttplibSocket(resp)
- self.http_response = httplib.HTTPResponse(sock)
- self.http_response.begin()
-
- def getresponse(self):
- return self.http_response
-
-
-def wire_HTTPConnection_to_WSGI(host, app):
- """Monkeypatches HTTPConnection so that if you try to connect to host, you
- are instead routed straight to the given WSGI app.
-
- After calling this method, when any code calls
-
- httplib.HTTPConnection(host)
-
- the connection object will be a fake. Its requests will be sent directly
- to the given WSGI app rather than through a socket.
-
- Code connecting to hosts other than host will not be affected.
-
- This method may be called multiple times to map different hosts to
- different apps.
- """
- class HTTPConnectionDecorator(object):
- """Wraps the real HTTPConnection class so that when you instantiate
- the class you might instead get a fake instance."""
-
- def __init__(self, wrapped):
- self.wrapped = wrapped
-
- def __call__(self, connection_host, *args, **kwargs):
- if connection_host == host:
- return FakeHttplibConnection(app, host)
- else:
- return self.wrapped(connection_host, *args, **kwargs)
-
- httplib.HTTPConnection = HTTPConnectionDecorator(httplib.HTTPConnection)
-
-
-class WSGIAppProxyTest(test.TestCase):
-
- def setUp(self):
- """Our WSGIAppProxy is going to call across an HTTPConnection to a
- WSGIApp running a limiter. The proxy will send input, and the proxy
- should receive that same input, pass it to the limiter who gives a
- result, and send the expected result back.
-
- The HTTPConnection isn't real -- it's monkeypatched to point straight
- at the WSGIApp. And the limiter isn't real -- it's a fake that
- behaves the way we tell it to.
- """
- super(WSGIAppProxyTest, self).setUp()
- self.limiter = FakeLimiter(self)
- app = ratelimiting.WSGIApp(self.limiter)
- wire_HTTPConnection_to_WSGI('100.100.100.100:80', app)
- self.proxy = ratelimiting.WSGIAppProxy('100.100.100.100:80')
-
- def test_200(self):
- self.limiter.mock('conquer', 'caesar', None)
- when = self.proxy.perform('conquer', 'caesar')
- self.assertEqual(when, None)
-
- def test_403(self):
- self.limiter.mock('grumble', 'proletariat', 1.5)
- when = self.proxy.perform('grumble', 'proletariat')
- self.assertEqual(when, 1.5)
-
- def test_failure(self):
- def shouldRaise():
- self.limiter.mock('murder', 'brutus', None)
- self.proxy.perform('stab', 'brutus')
- self.assertRaises(AssertionError, shouldRaise)
diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py
index 03e00af2a..9462bd40b 100644
--- a/nova/tests/api/openstack/test_servers.py
+++ b/nova/tests/api/openstack/test_servers.py
@@ -24,6 +24,7 @@ from xml.dom import minidom
import stubout
import webob
+from nova import context
from nova import db
from nova import flags
from nova import test
@@ -81,7 +82,7 @@ def stub_instance(id, user_id=1, private_address=None, public_addresses=None):
"admin_pass": "",
"user_id": user_id,
"project_id": "",
- "image_id": 10,
+ "image_id": "10",
"kernel_id": "",
"ramdisk_id": "",
"launch_index": 0,
@@ -94,7 +95,7 @@ def stub_instance(id, user_id=1, private_address=None, public_addresses=None):
"local_gb": 0,
"hostname": "",
"host": None,
- "instance_type": "",
+ "instance_type": "1",
"user_data": "",
"reservation_id": "",
"mac_address": "",
@@ -160,7 +161,7 @@ class ServersTest(test.TestCase):
req = webob.Request.blank('/v1.0/servers/1')
res = req.get_response(fakes.wsgi_app())
res_dict = json.loads(res.body)
- self.assertEqual(res_dict['server']['id'], '1')
+ self.assertEqual(res_dict['server']['id'], 1)
self.assertEqual(res_dict['server']['name'], 'server1')
def test_get_server_by_id_with_addresses(self):
@@ -171,7 +172,7 @@ class ServersTest(test.TestCase):
req = webob.Request.blank('/v1.0/servers/1')
res = req.get_response(fakes.wsgi_app())
res_dict = json.loads(res.body)
- self.assertEqual(res_dict['server']['id'], '1')
+ self.assertEqual(res_dict['server']['id'], 1)
self.assertEqual(res_dict['server']['name'], 'server1')
addresses = res_dict['server']['addresses']
self.assertEqual(len(addresses["public"]), len(public))
@@ -179,6 +180,25 @@ class ServersTest(test.TestCase):
self.assertEqual(len(addresses["private"]), 1)
self.assertEqual(addresses["private"][0], private)
+ def test_get_server_by_id_with_addresses_v11(self):
+ private = "192.168.0.3"
+ public = ["1.2.3.4"]
+ new_return_server = return_server_with_addresses(private, public)
+ self.stubs.Set(nova.db.api, 'instance_get', new_return_server)
+ req = webob.Request.blank('/v1.1/servers/1')
+ req.environ['api.version'] = '1.1'
+ res = req.get_response(fakes.wsgi_app())
+ res_dict = json.loads(res.body)
+ self.assertEqual(res_dict['server']['id'], 1)
+ self.assertEqual(res_dict['server']['name'], 'server1')
+ addresses = res_dict['server']['addresses']
+ self.assertEqual(len(addresses["public"]), len(public))
+ self.assertEqual(addresses["public"][0],
+ {"version": 4, "addr": public[0]})
+ self.assertEqual(len(addresses["private"]), 1)
+ self.assertEqual(addresses["private"][0],
+ {"version": 4, "addr": private})
+
def test_get_server_list(self):
req = webob.Request.blank('/v1.0/servers')
res = req.get_response(fakes.wsgi_app())
@@ -219,7 +239,7 @@ class ServersTest(test.TestCase):
servers = json.loads(res.body)['servers']
self.assertEqual([s['id'] for s in servers], [1, 2])
- def _test_create_instance_helper(self):
+ def _setup_for_create_instance(self):
"""Shared implementation for tests below that create instance"""
def instance_create(context, inst):
return {'id': '1', 'display_name': 'server_test'}
@@ -256,14 +276,17 @@ class ServersTest(test.TestCase):
self.stubs.Set(nova.api.openstack.common,
"get_image_id_from_image_hash", image_id_from_hash)
+ def _test_create_instance_helper(self):
+ self._setup_for_create_instance()
+
body = dict(server=dict(
- name='server_test', imageId=2, flavorId=2,
+ name='server_test', imageId=3, flavorId=2,
metadata={'hello': 'world', 'open': 'stack'},
personality={}))
req = webob.Request.blank('/v1.0/servers')
req.method = 'POST'
req.body = json.dumps(body)
- req.headers["Content-Type"] = "application/json"
+ req.headers["content-type"] = "application/json"
res = req.get_response(fakes.wsgi_app())
@@ -271,8 +294,9 @@ class ServersTest(test.TestCase):
self.assertEqual('serv', server['adminPass'][:4])
self.assertEqual(16, len(server['adminPass']))
self.assertEqual('server_test', server['name'])
- self.assertEqual('1', server['id'])
-
+ self.assertEqual(1, server['id'])
+ self.assertEqual(2, server['flavorId'])
+ self.assertEqual(3, server['imageId'])
self.assertEqual(res.status_int, 200)
def test_create_instance(self):
@@ -282,6 +306,56 @@ class ServersTest(test.TestCase):
fakes.stub_out_key_pair_funcs(self.stubs, have_key_pair=False)
self._test_create_instance_helper()
+ def test_create_instance_v11(self):
+ self._setup_for_create_instance()
+
+ imageRef = 'http://localhost/v1.1/images/2'
+ flavorRef = 'http://localhost/v1.1/flavors/3'
+ body = {
+ 'server': {
+ 'name': 'server_test',
+ 'imageRef': imageRef,
+ 'flavorRef': flavorRef,
+ 'metadata': {
+ 'hello': 'world',
+ 'open': 'stack',
+ },
+ 'personality': {},
+ },
+ }
+
+ req = webob.Request.blank('/v1.1/servers')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+
+ res = req.get_response(fakes.wsgi_app())
+
+ server = json.loads(res.body)['server']
+ self.assertEqual('serv', server['adminPass'][:4])
+ self.assertEqual(16, len(server['adminPass']))
+ self.assertEqual('server_test', server['name'])
+ self.assertEqual(1, server['id'])
+ self.assertEqual(flavorRef, server['flavorRef'])
+ self.assertEqual(imageRef, server['imageRef'])
+ self.assertEqual(res.status_int, 200)
+
+ def test_create_instance_v11_bad_href(self):
+ self._setup_for_create_instance()
+
+ imageRef = 'http://localhost/v1.1/images/asdf'
+ flavorRef = 'http://localhost/v1.1/flavors/3'
+ body = dict(server=dict(
+ name='server_test', imageRef=imageRef, flavorRef=flavorRef,
+ metadata={'hello': 'world', 'open': 'stack'},
+ personality={}))
+ req = webob.Request.blank('/v1.1/servers')
+ req.method = 'POST'
+ req.body = json.dumps(body)
+ req.headers["content-type"] = "application/json"
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+
def test_update_no_body(self):
req = webob.Request.blank('/v1.0/servers/1')
req.method = 'PUT'
@@ -339,19 +413,32 @@ class ServersTest(test.TestCase):
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status, '404 Not Found')
- def test_get_all_server_details(self):
+ def test_get_all_server_details_v1_0(self):
req = webob.Request.blank('/v1.0/servers/detail')
res = req.get_response(fakes.wsgi_app())
res_dict = json.loads(res.body)
- i = 0
- for s in res_dict['servers']:
+ for i, s in enumerate(res_dict['servers']):
self.assertEqual(s['id'], i)
self.assertEqual(s['hostId'], '')
self.assertEqual(s['name'], 'server%d' % i)
- self.assertEqual(s['imageId'], 10)
+ self.assertEqual(s['imageId'], '10')
+ self.assertEqual(s['flavorId'], '1')
+ self.assertEqual(s['metadata']['seq'], i)
+
+ def test_get_all_server_details_v1_1(self):
+ req = webob.Request.blank('/v1.1/servers/detail')
+ req.environ['api.version'] = '1.1'
+ res = req.get_response(fakes.wsgi_app())
+ res_dict = json.loads(res.body)
+
+ for i, s in enumerate(res_dict['servers']):
+ self.assertEqual(s['id'], i)
+ self.assertEqual(s['hostId'], '')
+ self.assertEqual(s['name'], 'server%d' % i)
+ self.assertEqual(s['imageRef'], 'http://localhost/v1.1/images/10')
+ self.assertEqual(s['flavorRef'], 'http://localhost/v1.1/flavors/1')
self.assertEqual(s['metadata']['seq'], i)
- i += 1
def test_get_all_server_details_with_host(self):
'''
@@ -491,16 +578,6 @@ class ServersTest(test.TestCase):
req.body = json.dumps(body)
res = req.get_response(fakes.wsgi_app())
- def test_server_resize(self):
- body = dict(server=dict(
- name='server_test', imageId=2, flavorId=2, metadata={},
- personality={}))
- req = webob.Request.blank('/v1.0/servers/1/action')
- req.method = 'POST'
- req.content_type = 'application/json'
- req.body = json.dumps(body)
- res = req.get_response(fakes.wsgi_app())
-
def test_delete_server_instance(self):
req = webob.Request.blank('/v1.0/servers/1')
req.method = 'DELETE'
@@ -556,6 +633,18 @@ class ServersTest(test.TestCase):
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 400)
+ def test_resized_server_has_correct_status(self):
+ req = self.webreq('/1', 'GET', dict(resize=dict(flavorId=3)))
+
+ def fake_migration_get(*args):
+ return {}
+
+ self.stubs.Set(nova.db, 'migration_get_by_instance_and_status',
+ fake_migration_get)
+ res = req.get_response(fakes.wsgi_app())
+ body = json.loads(res.body)
+ self.assertEqual(body['server']['status'], 'resize-confirm')
+
def test_confirm_resize_server(self):
req = self.webreq('/1/action', 'POST', dict(confirmResize=None))
@@ -910,7 +999,7 @@ class TestServerInstanceCreation(test.TestCase):
def _setup_mock_compute_api_for_personality(self):
- class MockComputeAPI(object):
+ class MockComputeAPI(nova.compute.API):
def __init__(self):
self.injected_files = None
@@ -1093,6 +1182,15 @@ class TestServerInstanceCreation(test.TestCase):
self.assertEquals(response.status_int, 400)
self.assertEquals(injected_files, None)
+ def test_create_instance_with_null_personality(self):
+ personality = None
+ body_dict = self._create_personality_request_dict(personality)
+ body_dict['server']['personality'] = None
+ request = self._get_create_request_json(body_dict)
+ compute_api, response = \
+ self._run_create_instance_with_mock_compute_api(request)
+ self.assertEquals(response.status_int, 200)
+
def test_create_instance_with_three_personalities(self):
files = [
('/etc/sudoers', 'ALL ALL=NOPASSWD: ALL\n'),
@@ -1132,7 +1230,3 @@ class TestServerInstanceCreation(test.TestCase):
server = dom.childNodes[0]
self.assertEquals(server.nodeName, 'server')
self.assertTrue(server.getAttribute('adminPass').startswith('fake'))
-
-
-if __name__ == "__main__":
- unittest.main()
diff --git a/nova/tests/api/openstack/test_users.py b/nova/tests/api/openstack/test_users.py
index 2dda4319b..effb2f592 100644
--- a/nova/tests/api/openstack/test_users.py
+++ b/nova/tests/api/openstack/test_users.py
@@ -18,11 +18,10 @@ import json
import stubout
import webob
-import nova.api
-import nova.api.openstack.auth
-from nova import context
from nova import flags
from nova import test
+from nova import utils
+from nova.api.openstack import users
from nova.auth.manager import User, Project
from nova.tests.api.openstack import fakes
@@ -43,14 +42,14 @@ class UsersTest(test.TestCase):
def setUp(self):
super(UsersTest, self).setUp()
self.stubs = stubout.StubOutForTesting()
- self.stubs.Set(nova.api.openstack.users.Controller, '__init__',
+ self.stubs.Set(users.Controller, '__init__',
fake_init)
- self.stubs.Set(nova.api.openstack.users.Controller, '_check_admin',
+ self.stubs.Set(users.Controller, '_check_admin',
fake_admin_check)
- fakes.FakeAuthManager.auth_data = {}
+ fakes.FakeAuthManager.clear_fakes()
fakes.FakeAuthManager.projects = dict(testacct=Project('testacct',
'testacct',
- 'guy1',
+ 'id1',
'test',
[]))
fakes.FakeAuthDatabase.data = {}
@@ -61,10 +60,8 @@ class UsersTest(test.TestCase):
self.allow_admin = FLAGS.allow_admin_api
FLAGS.allow_admin_api = True
fakemgr = fakes.FakeAuthManager()
- fakemgr.add_user('acc1', User('guy1', 'guy1', 'acc1',
- 'fortytwo!', False))
- fakemgr.add_user('acc2', User('guy2', 'guy2', 'acc2',
- 'swordfish', True))
+ fakemgr.add_user(User('id1', 'guy1', 'acc1', 'secret1', False))
+ fakemgr.add_user(User('id2', 'guy2', 'acc2', 'secret2', True))
def tearDown(self):
self.stubs.UnsetAll()
@@ -80,28 +77,44 @@ class UsersTest(test.TestCase):
self.assertEqual(len(res_dict['users']), 2)
def test_get_user_by_id(self):
- req = webob.Request.blank('/v1.0/users/guy2')
+ req = webob.Request.blank('/v1.0/users/id2')
res = req.get_response(fakes.wsgi_app())
res_dict = json.loads(res.body)
- self.assertEqual(res_dict['user']['id'], 'guy2')
+ self.assertEqual(res_dict['user']['id'], 'id2')
self.assertEqual(res_dict['user']['name'], 'guy2')
- self.assertEqual(res_dict['user']['secret'], 'swordfish')
+ self.assertEqual(res_dict['user']['secret'], 'secret2')
self.assertEqual(res_dict['user']['admin'], True)
self.assertEqual(res.status_int, 200)
def test_user_delete(self):
- req = webob.Request.blank('/v1.0/users/guy1')
+ # Check the user exists
+ req = webob.Request.blank('/v1.0/users/id1')
+ res = req.get_response(fakes.wsgi_app())
+ res_dict = json.loads(res.body)
+
+ self.assertEqual(res_dict['user']['id'], 'id1')
+ self.assertEqual(res.status_int, 200)
+
+ # Delete the user
+ req = webob.Request.blank('/v1.0/users/id1')
req.method = 'DELETE'
res = req.get_response(fakes.wsgi_app())
- self.assertTrue('guy1' not in [u.id for u in
- fakes.FakeAuthManager.auth_data.values()])
+ self.assertTrue('id1' not in [u.id for u in
+ fakes.FakeAuthManager.auth_data])
self.assertEqual(res.status_int, 200)
+ # Check the user is not returned (and returns 404)
+ req = webob.Request.blank('/v1.0/users/id1')
+ res = req.get_response(fakes.wsgi_app())
+ res_dict = json.loads(res.body)
+ self.assertEqual(res.status_int, 404)
+
def test_user_create(self):
+ secret = utils.generate_password()
body = dict(user=dict(name='test_guy',
access='acc3',
- secret='invasionIsInNormandy',
+ secret=secret,
admin=True))
req = webob.Request.blank('/v1.0/users')
req.headers["Content-Type"] = "application/json"
@@ -112,20 +125,25 @@ class UsersTest(test.TestCase):
res_dict = json.loads(res.body)
self.assertEqual(res.status_int, 200)
+
+ # NOTE(justinsb): This is a questionable assertion in general
+ # fake sets id=name, but others might not...
self.assertEqual(res_dict['user']['id'], 'test_guy')
+
self.assertEqual(res_dict['user']['name'], 'test_guy')
self.assertEqual(res_dict['user']['access'], 'acc3')
- self.assertEqual(res_dict['user']['secret'], 'invasionIsInNormandy')
+ self.assertEqual(res_dict['user']['secret'], secret)
self.assertEqual(res_dict['user']['admin'], True)
self.assertTrue('test_guy' in [u.id for u in
- fakes.FakeAuthManager.auth_data.values()])
- self.assertEqual(len(fakes.FakeAuthManager.auth_data.values()), 3)
+ fakes.FakeAuthManager.auth_data])
+ self.assertEqual(len(fakes.FakeAuthManager.auth_data), 3)
def test_user_update(self):
+ new_secret = utils.generate_password()
body = dict(user=dict(name='guy2',
access='acc2',
- secret='invasionIsInNormandy'))
- req = webob.Request.blank('/v1.0/users/guy2')
+ secret=new_secret))
+ req = webob.Request.blank('/v1.0/users/id2')
req.headers["Content-Type"] = "application/json"
req.method = 'PUT'
req.body = json.dumps(body)
@@ -134,8 +152,8 @@ class UsersTest(test.TestCase):
res_dict = json.loads(res.body)
self.assertEqual(res.status_int, 200)
- self.assertEqual(res_dict['user']['id'], 'guy2')
+ self.assertEqual(res_dict['user']['id'], 'id2')
self.assertEqual(res_dict['user']['name'], 'guy2')
self.assertEqual(res_dict['user']['access'], 'acc2')
- self.assertEqual(res_dict['user']['secret'], 'invasionIsInNormandy')
+ self.assertEqual(res_dict['user']['secret'], new_secret)
self.assertEqual(res_dict['user']['admin'], True)
diff --git a/nova/tests/api/openstack/test_zones.py b/nova/tests/api/openstack/test_zones.py
index 38399bb3f..a3f191aaa 100644
--- a/nova/tests/api/openstack/test_zones.py
+++ b/nova/tests/api/openstack/test_zones.py
@@ -75,6 +75,10 @@ def zone_get_all_db(context):
]
+def zone_capabilities(method, context, params):
+ return dict()
+
+
class ZonesTest(test.TestCase):
def setUp(self):
super(ZonesTest, self).setUp()
@@ -93,13 +97,18 @@ class ZonesTest(test.TestCase):
self.stubs.Set(nova.db, 'zone_create', zone_create)
self.stubs.Set(nova.db, 'zone_delete', zone_delete)
+ self.old_zone_name = FLAGS.zone_name
+ self.old_zone_capabilities = FLAGS.zone_capabilities
+
def tearDown(self):
self.stubs.UnsetAll()
FLAGS.allow_admin_api = self.allow_admin
+ FLAGS.zone_name = self.old_zone_name
+ FLAGS.zone_capabilities = self.old_zone_capabilities
super(ZonesTest, self).tearDown()
def test_get_zone_list_scheduler(self):
- self.stubs.Set(api.API, '_call_scheduler', zone_get_all_scheduler)
+ self.stubs.Set(api, '_call_scheduler', zone_get_all_scheduler)
req = webob.Request.blank('/v1.0/zones')
res = req.get_response(fakes.wsgi_app())
res_dict = json.loads(res.body)
@@ -108,8 +117,7 @@ class ZonesTest(test.TestCase):
self.assertEqual(len(res_dict['zones']), 2)
def test_get_zone_list_db(self):
- self.stubs.Set(api.API, '_call_scheduler',
- zone_get_all_scheduler_empty)
+ self.stubs.Set(api, '_call_scheduler', zone_get_all_scheduler_empty)
self.stubs.Set(nova.db, 'zone_get_all', zone_get_all_db)
req = webob.Request.blank('/v1.0/zones')
req.headers["Content-Type"] = "application/json"
@@ -167,3 +175,18 @@ class ZonesTest(test.TestCase):
self.assertEqual(res_dict['zone']['id'], 1)
self.assertEqual(res_dict['zone']['api_url'], 'http://example.com')
self.assertFalse('username' in res_dict['zone'])
+
+ def test_zone_info(self):
+ FLAGS.zone_name = 'darksecret'
+ FLAGS.zone_capabilities = ['cap1=a;b', 'cap2=c;d']
+ self.stubs.Set(api, '_call_scheduler', zone_capabilities)
+
+ body = dict(zone=dict(username='zeb', password='sneaky'))
+ req = webob.Request.blank('/v1.0/zones/info')
+
+ res = req.get_response(fakes.wsgi_app())
+ res_dict = json.loads(res.body)
+ self.assertEqual(res.status_int, 200)
+ self.assertEqual(res_dict['zone']['name'], 'darksecret')
+ self.assertEqual(res_dict['zone']['cap1'], 'a;b')
+ self.assertEqual(res_dict['zone']['cap2'], 'c;d')
diff --git a/nova/tests/api/test_wsgi.py b/nova/tests/api/test_wsgi.py
index b1a849cf9..1ecdd1cfb 100644
--- a/nova/tests/api/test_wsgi.py
+++ b/nova/tests/api/test_wsgi.py
@@ -80,7 +80,7 @@ class ControllerTest(test.TestCase):
"attributes": {
"test": ["id"]}}}
- def show(self, req, id): # pylint: disable-msg=W0622,C0103
+ def show(self, req, id): # pylint: disable=W0622,C0103
return {"test": {"id": id}}
def __init__(self):
diff --git a/nova/tests/db/fakes.py b/nova/tests/db/fakes.py
index 5e9a3aa3b..2d25d5fc5 100644
--- a/nova/tests/db/fakes.py
+++ b/nova/tests/db/fakes.py
@@ -28,13 +28,33 @@ def stub_out_db_instance_api(stubs):
""" Stubs out the db API for creating Instances """
INSTANCE_TYPES = {
- 'm1.tiny': dict(memory_mb=512, vcpus=1, local_gb=0, flavorid=1),
- 'm1.small': dict(memory_mb=2048, vcpus=1, local_gb=20, flavorid=2),
+ 'm1.tiny': dict(memory_mb=512,
+ vcpus=1,
+ local_gb=0,
+ flavorid=1,
+ rxtx_cap=1),
+ 'm1.small': dict(memory_mb=2048,
+ vcpus=1,
+ local_gb=20,
+ flavorid=2,
+ rxtx_cap=2),
'm1.medium':
- dict(memory_mb=4096, vcpus=2, local_gb=40, flavorid=3),
- 'm1.large': dict(memory_mb=8192, vcpus=4, local_gb=80, flavorid=4),
+ dict(memory_mb=4096,
+ vcpus=2,
+ local_gb=40,
+ flavorid=3,
+ rxtx_cap=3),
+ 'm1.large': dict(memory_mb=8192,
+ vcpus=4,
+ local_gb=80,
+ flavorid=4,
+ rxtx_cap=4),
'm1.xlarge':
- dict(memory_mb=16384, vcpus=8, local_gb=160, flavorid=5)}
+ dict(memory_mb=16384,
+ vcpus=8,
+ local_gb=160,
+ flavorid=5,
+ rxtx_cap=5)}
class FakeModel(object):
""" Stubs out for model """
diff --git a/nova/tests/hyperv_unittest.py b/nova/tests/hyperv_unittest.py
index 3980ae3cb..042819b9c 100644
--- a/nova/tests/hyperv_unittest.py
+++ b/nova/tests/hyperv_unittest.py
@@ -51,7 +51,7 @@ class HyperVTestCase(test.TestCase):
instance_ref = db.instance_create(self.context, instance)
conn = hyperv.get_connection(False)
- conn._create_vm(instance_ref) # pylint: disable-msg=W0212
+ conn._create_vm(instance_ref) # pylint: disable=W0212
found = [n for n in conn.list_instances()
if n == instance_ref['name']]
self.assertTrue(len(found) == 1)
diff --git a/nova/tests/image/__init__.py b/nova/tests/image/__init__.py
new file mode 100644
index 000000000..b94e2e54e
--- /dev/null
+++ b/nova/tests/image/__init__.py
@@ -0,0 +1,16 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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/tests/image/test_glance.py b/nova/tests/image/test_glance.py
new file mode 100644
index 000000000..f1f8504f3
--- /dev/null
+++ b/nova/tests/image/test_glance.py
@@ -0,0 +1,188 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# 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 unittest
+
+from nova.image import glance
+
+
+class StubGlanceClient(object):
+
+ def __init__(self, images, add_response=None, update_response=None):
+ self.images = images
+ self.add_response = add_response
+ self.update_response = update_response
+
+ def get_image_meta(self, id):
+ return self.images[id]
+
+ def get_images_detailed(self):
+ return self.images.itervalues()
+
+ def get_image(self, id):
+ return self.images[id], []
+
+ def add_image(self, metadata, data):
+ return self.add_response
+
+ def update_image(self, image_id, metadata, data):
+ return self.update_response
+
+
+class NullWriter(object):
+
+ def write(self, *arg, **kwargs):
+ pass
+
+
+class TestGlanceImageServiceDatetimes(unittest.TestCase):
+
+ def setUp(self):
+ self.client = StubGlanceClient(None)
+ self.service = glance.GlanceImageService(self.client)
+
+ def test_show_passes_through_to_client(self):
+ self.client.images = {'xyz': {'foo': 'bar'}}
+ self.assertEqual(self.service.show({}, 'xyz'), {'foo': 'bar'})
+
+ def test_detail_passes_through_to_client(self):
+ self.client.images = {1: {'foo': 'bar'}}
+ self.assertEqual(list(self.service.detail({})), [{'foo': 'bar'}])
+
+ def test_show_makes_create_datetimes(self):
+ create_time = datetime.datetime.utcnow()
+ self.client.images = {'xyz': {
+ 'id': "id",
+ 'name': "my awesome image",
+ 'created_at': create_time.isoformat(),
+ }}
+ actual = self.service.show({}, 'xyz')
+ self.assertEqual(actual['created_at'], create_time)
+
+ def test_show_makes_update_datetimes(self):
+ update_time = datetime.datetime.utcnow()
+ self.client.images = {'abc': {
+ 'id': "id",
+ 'name': "my okay image",
+ 'updated_at': update_time.isoformat(),
+ }}
+ actual = self.service.show({}, 'abc')
+ self.assertEqual(actual['updated_at'], update_time)
+
+ def test_show_makes_delete_datetimes(self):
+ delete_time = datetime.datetime.utcnow()
+ self.client.images = {'123': {
+ 'id': "123",
+ 'name': "my lame image",
+ 'deleted_at': delete_time.isoformat(),
+ }}
+ actual = self.service.show({}, '123')
+ self.assertEqual(actual['deleted_at'], delete_time)
+
+ def test_show_handles_deleted_at_none(self):
+ self.client.images = {'747': {
+ 'id': "747",
+ 'name': "not deleted",
+ 'deleted_at': None,
+ }}
+ actual = self.service.show({}, '747')
+ self.assertEqual(actual['deleted_at'], None)
+
+ def test_detail_handles_timestamps(self):
+ now = datetime.datetime.utcnow()
+ image1 = {
+ 'id': 1,
+ 'name': 'image 1',
+ 'created_at': now.isoformat(),
+ 'updated_at': now.isoformat(),
+ 'deleted_at': None,
+ }
+ image2 = {
+ 'id': 2,
+ 'name': 'image 2',
+ 'deleted_at': now.isoformat(),
+ }
+ self.client.images = {1: image1, 2: image2}
+ i1, i2 = self.service.detail({})
+ self.assertEqual(i1['created_at'], now)
+ self.assertEqual(i1['updated_at'], now)
+ self.assertEqual(i1['deleted_at'], None)
+ self.assertEqual(i2['deleted_at'], now)
+
+ def test_get_handles_timestamps(self):
+ now = datetime.datetime.utcnow()
+ self.client.images = {'abcd': {
+ 'id': 'abcd',
+ 'name': 'nifty image',
+ 'created_at': now.isoformat(),
+ 'updated_at': now.isoformat(),
+ 'deleted_at': now.isoformat(),
+ }}
+ actual = self.service.get({}, 'abcd', NullWriter())
+ for attr in ('created_at', 'updated_at', 'deleted_at'):
+ self.assertEqual(actual[attr], now)
+
+ def test_get_handles_deleted_at_none(self):
+ self.client.images = {'abcd': {'deleted_at': None}}
+ actual = self.service.get({}, 'abcd', NullWriter())
+ self.assertEqual(actual['deleted_at'], None)
+
+ def test_create_handles_timestamps(self):
+ now = datetime.datetime.utcnow()
+ self.client.add_response = {
+ 'id': 'abcd',
+ 'name': 'blah',
+ 'created_at': now.isoformat(),
+ 'updated_at': now.isoformat(),
+ 'deleted_at': now.isoformat(),
+ }
+ actual = self.service.create({}, {})
+ for attr in ('created_at', 'updated_at', 'deleted_at'):
+ self.assertEqual(actual[attr], now)
+
+ def test_create_handles_deleted_at_none(self):
+ self.client.add_response = {
+ 'id': 'abcd',
+ 'name': 'blah',
+ 'deleted_at': None,
+ }
+ actual = self.service.create({}, {})
+ self.assertEqual(actual['deleted_at'], None)
+
+ def test_update_handles_timestamps(self):
+ now = datetime.datetime.utcnow()
+ self.client.update_response = {
+ 'id': 'abcd',
+ 'name': 'blah',
+ 'created_at': now.isoformat(),
+ 'updated_at': now.isoformat(),
+ 'deleted_at': now.isoformat(),
+ }
+ actual = self.service.update({}, 'dummy_id', {})
+ for attr in ('created_at', 'updated_at', 'deleted_at'):
+ self.assertEqual(actual[attr], now)
+
+ def test_create_handles_deleted_at_none(self):
+ self.client.update_response = {
+ 'id': 'abcd',
+ 'name': 'blah',
+ 'deleted_at': None,
+ }
+ actual = self.service.update({}, 'dummy_id', {})
+ self.assertEqual(actual['deleted_at'], None)
diff --git a/nova/tests/integrated/api/client.py b/nova/tests/integrated/api/client.py
index 245eb8c69..fc7c344e7 100644
--- a/nova/tests/integrated/api/client.py
+++ b/nova/tests/integrated/api/client.py
@@ -108,9 +108,7 @@ class TestOpenStackClient(object):
http_status = response.status
LOG.debug(_("%(auth_uri)s => code %(http_status)s") % locals())
- # Until bug732866 is fixed, we can't check this properly...
- #if http_status == 401:
- if http_status != 204:
+ if http_status == 401:
raise OpenStackApiAuthenticationException(response=response)
auth_headers = {}
diff --git a/nova/tests/integrated/integrated_helpers.py b/nova/tests/integrated/integrated_helpers.py
new file mode 100644
index 000000000..47093636e
--- /dev/null
+++ b/nova/tests/integrated/integrated_helpers.py
@@ -0,0 +1,188 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 Justin Santa Barbara
+# 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.
+
+"""
+Provides common functionality for integrated unit tests
+"""
+
+import random
+import string
+
+from nova import exception
+from nova import flags
+from nova import service
+from nova import test # For the flags
+from nova.auth import manager
+from nova.exception import Error
+from nova.log import logging
+from nova.tests.integrated.api import client
+
+
+FLAGS = flags.FLAGS
+
+LOG = logging.getLogger('nova.tests.integrated')
+
+
+def generate_random_alphanumeric(length):
+ """Creates a random alphanumeric string of specified length"""
+ return ''.join(random.choice(string.ascii_uppercase + string.digits)
+ for _x in range(length))
+
+
+def generate_random_numeric(length):
+ """Creates a random numeric string of specified length"""
+ return ''.join(random.choice(string.digits)
+ for _x in range(length))
+
+
+def generate_new_element(items, prefix, numeric=False):
+ """Creates a random string with prefix, that is not in 'items' list"""
+ while True:
+ if numeric:
+ candidate = prefix + generate_random_numeric(8)
+ else:
+ candidate = prefix + generate_random_alphanumeric(8)
+ if not candidate in items:
+ return candidate
+ print "Random collision on %s" % candidate
+
+
+class TestUser(object):
+ def __init__(self, name, secret, auth_url):
+ self.name = name
+ self.secret = secret
+ self.auth_url = auth_url
+
+ if not auth_url:
+ raise exception.Error("auth_url is required")
+ self.openstack_api = client.TestOpenStackClient(self.name,
+ self.secret,
+ self.auth_url)
+
+
+class IntegratedUnitTestContext(object):
+ __INSTANCE = None
+
+ def __init__(self):
+ self.auth_manager = manager.AuthManager()
+
+ self.wsgi_server = None
+ self.wsgi_apps = []
+ self.api_service = None
+
+ self.services = []
+ self.auth_url = None
+ self.project_name = None
+
+ self.setup()
+
+ def setup(self):
+ self._start_services()
+
+ self._create_test_user()
+
+ def _create_test_user(self):
+ self.test_user = self._create_unittest_user()
+
+ # No way to currently pass this through the OpenStack API
+ self.project_name = 'openstack'
+ self._configure_project(self.project_name, self.test_user)
+
+ def _start_services(self):
+ # WSGI shutdown broken :-(
+ # bug731668
+ if not self.api_service:
+ self._start_api_service()
+
+ def cleanup(self):
+ for service in self.services:
+ service.kill()
+ self.services = []
+ # TODO(justinsb): Shutdown WSGI & anything else we startup
+ # bug731668
+ # WSGI shutdown broken :-(
+ # self.wsgi_server.terminate()
+ # self.wsgi_server = None
+ self.test_user = None
+
+ def _create_unittest_user(self):
+ users = self.auth_manager.get_users()
+ user_names = [user.name for user in users]
+ auth_name = generate_new_element(user_names, 'unittest_user_')
+ auth_key = generate_random_alphanumeric(16)
+
+ # Right now there's a bug where auth_name and auth_key are reversed
+ # bug732907
+ auth_key = auth_name
+
+ self.auth_manager.create_user(auth_name, auth_name, auth_key, False)
+ return TestUser(auth_name, auth_key, self.auth_url)
+
+ def _configure_project(self, project_name, user):
+ projects = self.auth_manager.get_projects()
+ project_names = [project.name for project in projects]
+ if not project_name in project_names:
+ project = self.auth_manager.create_project(project_name,
+ user.name,
+ description=None,
+ member_users=None)
+ else:
+ self.auth_manager.add_to_project(user.name, project_name)
+
+ def _start_api_service(self):
+ api_service = service.ApiService.create()
+ api_service.start()
+
+ if not api_service:
+ raise Exception("API Service was None")
+
+ # WSGI shutdown broken :-(
+ #self.services.append(volume_service)
+ self.api_service = api_service
+
+ self.auth_url = 'http://localhost:8774/v1.0'
+
+ return api_service
+
+ # WSGI shutdown broken :-(
+ # bug731668
+ #@staticmethod
+ #def get():
+ # if not IntegratedUnitTestContext.__INSTANCE:
+ # IntegratedUnitTestContext.startup()
+ # #raise Error("Must call IntegratedUnitTestContext::startup")
+ # return IntegratedUnitTestContext.__INSTANCE
+
+ @staticmethod
+ def startup():
+ # Because WSGI shutdown is broken at the moment, we have to recycle
+ # bug731668
+ if IntegratedUnitTestContext.__INSTANCE:
+ #raise Error("Multiple calls to IntegratedUnitTestContext.startup")
+ IntegratedUnitTestContext.__INSTANCE.setup()
+ else:
+ IntegratedUnitTestContext.__INSTANCE = IntegratedUnitTestContext()
+ return IntegratedUnitTestContext.__INSTANCE
+
+ @staticmethod
+ def shutdown():
+ if not IntegratedUnitTestContext.__INSTANCE:
+ raise Error("Must call IntegratedUnitTestContext::startup")
+ IntegratedUnitTestContext.__INSTANCE.cleanup()
+ # WSGI shutdown broken :-(
+ # bug731668
+ #IntegratedUnitTestContext.__INSTANCE = None
diff --git a/nova/tests/integrated/test_login.py b/nova/tests/integrated/test_login.py
new file mode 100644
index 000000000..501f8c919
--- /dev/null
+++ b/nova/tests/integrated/test_login.py
@@ -0,0 +1,79 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 Justin Santa Barbara
+# 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 unittest
+
+from nova import flags
+from nova import test
+from nova.log import logging
+from nova.tests.integrated import integrated_helpers
+from nova.tests.integrated.api import client
+
+
+LOG = logging.getLogger('nova.tests.integrated')
+
+FLAGS = flags.FLAGS
+FLAGS.verbose = True
+
+
+class LoginTest(test.TestCase):
+ def setUp(self):
+ super(LoginTest, self).setUp()
+ context = integrated_helpers.IntegratedUnitTestContext.startup()
+ self.user = context.test_user
+ self.api = self.user.openstack_api
+
+ def tearDown(self):
+ integrated_helpers.IntegratedUnitTestContext.shutdown()
+ super(LoginTest, self).tearDown()
+
+ def test_login(self):
+ """Simple check - we list flavors - so we know we're logged in"""
+ flavors = self.api.get_flavors()
+ for flavor in flavors:
+ LOG.debug(_("flavor: %s") % flavor)
+
+ def test_bad_login_password(self):
+ """Test that I get a 401 with a bad username"""
+ bad_credentials_api = client.TestOpenStackClient(self.user.name,
+ "notso_password",
+ self.user.auth_url)
+
+ self.assertRaises(client.OpenStackApiAuthenticationException,
+ bad_credentials_api.get_flavors)
+
+ def test_bad_login_username(self):
+ """Test that I get a 401 with a bad password"""
+ bad_credentials_api = client.TestOpenStackClient("notso_username",
+ self.user.secret,
+ self.user.auth_url)
+
+ self.assertRaises(client.OpenStackApiAuthenticationException,
+ bad_credentials_api.get_flavors)
+
+ def test_bad_login_both_bad(self):
+ """Test that I get a 401 with both bad username and bad password"""
+ bad_credentials_api = client.TestOpenStackClient("notso_username",
+ "notso_password",
+ self.user.auth_url)
+
+ self.assertRaises(client.OpenStackApiAuthenticationException,
+ bad_credentials_api.get_flavors)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/nova/tests/network/__init__.py b/nova/tests/network/__init__.py
new file mode 100644
index 000000000..97f96b6fa
--- /dev/null
+++ b/nova/tests/network/__init__.py
@@ -0,0 +1,67 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+"""
+Utility methods
+"""
+import os
+
+from nova import context
+from nova import db
+from nova import flags
+from nova import log as logging
+from nova import utils
+
+FLAGS = flags.FLAGS
+LOG = logging.getLogger('nova.tests.network')
+
+
+def binpath(script):
+ """Returns the absolute path to a script in bin"""
+ return os.path.abspath(os.path.join(__file__, "../../../../bin", script))
+
+
+def lease_ip(private_ip):
+ """Run add command on dhcpbridge"""
+ network_ref = db.fixed_ip_get_network(context.get_admin_context(),
+ private_ip)
+ instance_ref = db.fixed_ip_get_instance(context.get_admin_context(),
+ private_ip)
+ cmd = (binpath('nova-dhcpbridge'), 'add',
+ instance_ref['mac_address'],
+ private_ip, 'fake')
+ env = {'DNSMASQ_INTERFACE': network_ref['bridge'],
+ 'TESTING': '1',
+ 'FLAGFILE': FLAGS.dhcpbridge_flagfile}
+ (out, err) = utils.execute(*cmd, addl_env=env)
+ LOG.debug("ISSUE_IP: %s, %s ", out, err)
+
+
+def release_ip(private_ip):
+ """Run del command on dhcpbridge"""
+ network_ref = db.fixed_ip_get_network(context.get_admin_context(),
+ private_ip)
+ instance_ref = db.fixed_ip_get_instance(context.get_admin_context(),
+ private_ip)
+ cmd = (binpath('nova-dhcpbridge'), 'del',
+ instance_ref['mac_address'],
+ private_ip, 'fake')
+ env = {'DNSMASQ_INTERFACE': network_ref['bridge'],
+ 'TESTING': '1',
+ 'FLAGFILE': FLAGS.dhcpbridge_flagfile}
+ (out, err) = utils.execute(*cmd, addl_env=env)
+ LOG.debug("RELEASE_IP: %s, %s ", out, err)
diff --git a/nova/tests/network/base.py b/nova/tests/network/base.py
new file mode 100644
index 000000000..988a1de72
--- /dev/null
+++ b/nova/tests/network/base.py
@@ -0,0 +1,154 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+"""
+Base class of Unit Tests for all network models
+"""
+import IPy
+import os
+
+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 test
+from nova import utils
+from nova.auth import manager
+
+FLAGS = flags.FLAGS
+LOG = logging.getLogger('nova.tests.network')
+
+
+class NetworkTestCase(test.TestCase):
+ """Test cases for network code"""
+ def setUp(self):
+ super(NetworkTestCase, self).setUp()
+ # NOTE(vish): if you change these flags, make sure to change the
+ # flags in the corresponding section in nova-dhcpbridge
+ self.flags(connection_type='fake',
+ fake_call=True,
+ fake_network=True)
+ self.manager = manager.AuthManager()
+ self.user = self.manager.create_user('netuser', 'netuser', 'netuser')
+ self.projects = []
+ self.network = utils.import_object(FLAGS.network_manager)
+ self.context = context.RequestContext(project=None, user=self.user)
+ for i in range(FLAGS.num_networks):
+ name = 'project%s' % i
+ project = self.manager.create_project(name, 'netuser', name)
+ self.projects.append(project)
+ # create the necessary network data for the project
+ user_context = context.RequestContext(project=self.projects[i],
+ user=self.user)
+ host = self.network.get_network_host(user_context.elevated())
+ instance_ref = self._create_instance(0)
+ self.instance_id = instance_ref['id']
+ instance_ref = self._create_instance(1)
+ self.instance2_id = instance_ref['id']
+
+ def tearDown(self):
+ # TODO(termie): this should really be instantiating clean datastores
+ # in between runs, one failure kills all the tests
+ db.instance_destroy(context.get_admin_context(), self.instance_id)
+ db.instance_destroy(context.get_admin_context(), self.instance2_id)
+ for project in self.projects:
+ self.manager.delete_project(project)
+ self.manager.delete_user(self.user)
+ super(NetworkTestCase, self).tearDown()
+
+ def _create_instance(self, project_num, mac=None):
+ if not mac:
+ mac = utils.generate_mac()
+ project = self.projects[project_num]
+ self.context._project = project
+ self.context.project_id = project.id
+ return db.instance_create(self.context,
+ {'project_id': project.id,
+ 'mac_address': mac})
+
+ def _create_address(self, project_num, instance_id=None):
+ """Create an address in given project num"""
+ if instance_id is None:
+ instance_id = self.instance_id
+ self.context._project = self.projects[project_num]
+ self.context.project_id = self.projects[project_num].id
+ return self.network.allocate_fixed_ip(self.context, instance_id)
+
+ def _deallocate_address(self, project_num, address):
+ self.context._project = self.projects[project_num]
+ self.context.project_id = self.projects[project_num].id
+ self.network.deallocate_fixed_ip(self.context, address)
+
+ def _is_allocated_in_project(self, address, project_id):
+ """Returns true if address is in specified project"""
+ project_net = db.network_get_by_bridge(context.get_admin_context(),
+ FLAGS.flat_network_bridge)
+ network = db.fixed_ip_get_network(context.get_admin_context(),
+ address)
+ instance = db.fixed_ip_get_instance(context.get_admin_context(),
+ address)
+ # instance exists until release
+ return instance is not None and network['id'] == project_net['id']
+
+ def test_private_ipv6(self):
+ """Make sure ipv6 is OK"""
+ if FLAGS.use_ipv6:
+ instance_ref = self._create_instance(0)
+ address = self._create_address(0, instance_ref['id'])
+ network_ref = db.project_get_network(
+ context.get_admin_context(),
+ self.context.project_id)
+ address_v6 = db.instance_get_fixed_address_v6(
+ context.get_admin_context(),
+ instance_ref['id'])
+ self.assertEqual(instance_ref['mac_address'],
+ utils.to_mac(address_v6))
+ instance_ref2 = db.fixed_ip_get_instance_v6(
+ context.get_admin_context(),
+ address_v6)
+ self.assertEqual(instance_ref['id'], instance_ref2['id'])
+ self.assertEqual(address_v6,
+ utils.to_global_ipv6(
+ network_ref['cidr_v6'],
+ instance_ref['mac_address']))
+ self._deallocate_address(0, address)
+ db.instance_destroy(context.get_admin_context(),
+ instance_ref['id'])
+
+ def test_available_ips(self):
+ """Make sure the number of available ips for the network is correct
+
+ The number of available IP addresses depends on the test
+ environment's setup.
+
+ Network size is set in test fixture's setUp method.
+
+ There are ips reserved at the bottom and top of the range.
+ services (network, gateway, CloudPipe, broadcast)
+ """
+ network = db.project_get_network(context.get_admin_context(),
+ self.projects[0].id)
+ net_size = flags.FLAGS.network_size
+ admin_context = context.get_admin_context()
+ total_ips = (db.network_count_available_ips(admin_context,
+ network['id']) +
+ db.network_count_reserved_ips(admin_context,
+ network['id']) +
+ db.network_count_allocated_ips(admin_context,
+ network['id']))
+ self.assertEqual(total_ips, net_size)
diff --git a/nova/tests/objectstore_unittest.py b/nova/tests/objectstore_unittest.py
index 5a1be08eb..4e2ac205e 100644
--- a/nova/tests/objectstore_unittest.py
+++ b/nova/tests/objectstore_unittest.py
@@ -179,7 +179,7 @@ class ObjectStoreTestCase(test.TestCase):
class TestHTTPChannel(http.HTTPChannel):
"""Dummy site required for twisted.web"""
- def checkPersistence(self, _, __): # pylint: disable-msg=C0103
+ def checkPersistence(self, _, __): # pylint: disable=C0103
"""Otherwise we end up with an unclean reactor."""
return False
@@ -209,10 +209,10 @@ class S3APITestCase(test.TestCase):
root = S3()
self.site = TestSite(root)
- # pylint: disable-msg=E1101
+ # pylint: disable=E1101
self.listening_port = reactor.listenTCP(0, self.site,
interface='127.0.0.1')
- # pylint: enable-msg=E1101
+ # pylint: enable=E1101
self.tcp_port = self.listening_port.getHost().port
if not boto.config.has_section('Boto'):
@@ -231,11 +231,11 @@ class S3APITestCase(test.TestCase):
self.conn.get_http_connection = get_http_connection
- def _ensure_no_buckets(self, buckets): # pylint: disable-msg=C0111
+ def _ensure_no_buckets(self, buckets): # pylint: disable=C0111
self.assertEquals(len(buckets), 0, "Bucket list was not empty")
return True
- def _ensure_one_bucket(self, buckets, name): # pylint: disable-msg=C0111
+ def _ensure_one_bucket(self, buckets, name): # pylint: disable=C0111
self.assertEquals(len(buckets), 1,
"Bucket list didn't have exactly one element in it")
self.assertEquals(buckets[0].name, name, "Wrong name")
diff --git a/nova/tests/test_api.py b/nova/tests/test_api.py
index d5c54a1c3..fa0e56597 100644
--- a/nova/tests/test_api.py
+++ b/nova/tests/test_api.py
@@ -20,6 +20,7 @@
import boto
from boto.ec2 import regioninfo
+from boto.exception import EC2ResponseError
import datetime
import httplib
import random
@@ -124,7 +125,7 @@ class ApiEc2TestCase(test.TestCase):
self.mox.StubOutWithMock(self.ec2, 'new_http_connection')
self.http = FakeHttplibConnection(
self.app, '%s:8773' % (self.host), False)
- # pylint: disable-msg=E1103
+ # pylint: disable=E1103
self.ec2.new_http_connection(host, is_secure).AndReturn(self.http)
return self.http
@@ -177,6 +178,17 @@ class ApiEc2TestCase(test.TestCase):
self.manager.delete_project(project)
self.manager.delete_user(user)
+ def test_terminate_invalid_instance(self):
+ """Attempt to terminate an invalid instance"""
+ self.expect_http()
+ self.mox.ReplayAll()
+ user = self.manager.create_user('fake', 'fake', 'fake')
+ project = self.manager.create_project('fake', 'fake', 'fake')
+ self.assertRaises(EC2ResponseError, self.ec2.terminate_instances,
+ "i-00000005")
+ self.manager.delete_project(project)
+ self.manager.delete_user(user)
+
def test_get_all_key_pairs(self):
"""Test that, after creating a user and project and generating
a key pair, that the API call to list key pairs works properly"""
diff --git a/nova/tests/test_auth.py b/nova/tests/test_auth.py
index 2a7817032..885596f56 100644
--- a/nova/tests/test_auth.py
+++ b/nova/tests/test_auth.py
@@ -299,6 +299,13 @@ class AuthManagerTestCase(object):
self.assertEqual('test2', project.project_manager_id)
self.assertEqual('new desc', project.description)
+ def test_modify_project_adds_new_manager(self):
+ with user_and_project_generator(self.manager):
+ with user_generator(self.manager, name='test2'):
+ self.manager.modify_project('testproj', 'test2', 'new desc')
+ project = self.manager.get_project('testproj')
+ self.assertTrue('test2' in project.member_ids)
+
def test_can_delete_project(self):
with user_generator(self.manager):
self.manager.create_project('testproj', 'test1')
diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py
index 3651f4cef..d1ef68de4 100644
--- a/nova/tests/test_compute.py
+++ b/nova/tests/test_compute.py
@@ -44,6 +44,14 @@ flags.DECLARE('stub_network', 'nova.compute.manager')
flags.DECLARE('live_migration_retry_count', 'nova.compute.manager')
+class FakeTime(object):
+ def __init__(self):
+ self.counter = 0
+
+ def sleep(self, t):
+ self.counter += t
+
+
class ComputeTestCase(test.TestCase):
"""Test case for compute"""
def setUp(self):
@@ -82,6 +90,21 @@ class ComputeTestCase(test.TestCase):
inst.update(params)
return db.instance_create(self.context, inst)['id']
+ def _create_instance_type(self, params={}):
+ """Create a test instance"""
+ context = self.context.elevated()
+ inst = {}
+ inst['name'] = 'm1.small'
+ inst['memory_mb'] = '1024'
+ inst['vcpus'] = '1'
+ inst['local_gb'] = '20'
+ inst['flavorid'] = '1'
+ inst['swap'] = '2048'
+ inst['rxtx_quota'] = 100
+ inst['rxtx_cap'] = 200
+ inst.update(params)
+ return db.instance_type_create(context, inst)['id']
+
def _create_group(self):
values = {'name': 'testgroup',
'description': 'testgroup',
@@ -299,15 +322,53 @@ class ComputeTestCase(test.TestCase):
"""Ensure instance can be migrated/resized"""
instance_id = self._create_instance()
context = self.context.elevated()
+
self.compute.run_instance(self.context, instance_id)
db.instance_update(self.context, instance_id, {'host': 'foo'})
- self.compute.prep_resize(context, instance_id)
+ self.compute.prep_resize(context, instance_id, 1)
migration_ref = db.migration_get_by_instance_and_status(context,
instance_id, 'pre-migrating')
self.compute.resize_instance(context, instance_id,
migration_ref['id'])
self.compute.terminate_instance(context, instance_id)
+ def test_resize_invalid_flavor_fails(self):
+ """Ensure invalid flavors raise"""
+ instance_id = self._create_instance()
+ context = self.context.elevated()
+ self.compute.run_instance(self.context, instance_id)
+
+ self.assertRaises(exception.NotFound, self.compute_api.resize,
+ context, instance_id, 200)
+
+ self.compute.terminate_instance(context, instance_id)
+
+ def test_resize_down_fails(self):
+ """Ensure resizing down raises and fails"""
+ context = self.context.elevated()
+ instance_id = self._create_instance()
+
+ self.compute.run_instance(self.context, instance_id)
+ db.instance_update(self.context, instance_id,
+ {'instance_type': 'm1.xlarge'})
+
+ self.assertRaises(exception.ApiError, self.compute_api.resize,
+ context, instance_id, 1)
+
+ self.compute.terminate_instance(context, instance_id)
+
+ def test_resize_same_size_fails(self):
+ """Ensure invalid flavors raise"""
+ context = self.context.elevated()
+ instance_id = self._create_instance()
+
+ self.compute.run_instance(self.context, instance_id)
+
+ self.assertRaises(exception.ApiError, self.compute_api.resize,
+ context, instance_id, 1)
+
+ self.compute.terminate_instance(context, instance_id)
+
def test_get_by_flavor_id(self):
type = instance_types.get_by_flavor_id(1)
self.assertEqual(type, 'm1.tiny')
@@ -318,10 +379,8 @@ class ComputeTestCase(test.TestCase):
instance_id = self._create_instance()
self.compute.run_instance(self.context, instance_id)
self.assertRaises(exception.Error, self.compute.prep_resize,
- self.context, instance_id)
+ self.context, instance_id, 1)
self.compute.terminate_instance(self.context, instance_id)
- type = instance_types.get_by_flavor_id("1")
- self.assertEqual(type, 'm1.tiny')
def _setup_other_managers(self):
self.volume_manager = utils.import_object(FLAGS.volume_manager)
@@ -342,7 +401,7 @@ class ComputeTestCase(test.TestCase):
self.mox.ReplayAll()
self.assertRaises(exception.NotFound,
self.compute.pre_live_migration,
- c, instance_ref['id'])
+ c, instance_ref['id'], time=FakeTime())
def test_pre_live_migration_instance_has_volume(self):
"""Confirm setup_compute_volume is called when volume is mounted."""
@@ -395,7 +454,7 @@ class ComputeTestCase(test.TestCase):
self.compute.driver = drivermock
self.mox.ReplayAll()
- ret = self.compute.pre_live_migration(c, i_ref['id'])
+ ret = self.compute.pre_live_migration(c, i_ref['id'], time=FakeTime())
self.assertEqual(ret, None)
def test_pre_live_migration_setup_compute_node_fail(self):
@@ -428,7 +487,7 @@ class ComputeTestCase(test.TestCase):
self.mox.ReplayAll()
self.assertRaises(exception.ProcessExecutionError,
self.compute.pre_live_migration,
- c, i_ref['id'])
+ c, i_ref['id'], time=FakeTime())
def test_live_migration_works_correctly_with_volume(self):
"""Confirm check_for_export to confirm volume health check."""
@@ -575,3 +634,24 @@ class ComputeTestCase(test.TestCase):
db.instance_destroy(c, instance_id)
db.volume_destroy(c, v_ref['id'])
db.floating_ip_destroy(c, flo_addr)
+
+ def test_run_kill_vm(self):
+ """Detect when a vm is terminated behind the scenes"""
+ instance_id = self._create_instance()
+
+ self.compute.run_instance(self.context, instance_id)
+
+ instances = db.instance_get_all(context.get_admin_context())
+ LOG.info(_("Running instances: %s"), instances)
+ self.assertEqual(len(instances), 1)
+
+ instance_name = instances[0].name
+ self.compute.driver.test_remove_vm(instance_name)
+
+ # Force the compute manager to do its periodic poll
+ error_list = self.compute.periodic_tasks(context.get_admin_context())
+ self.assertFalse(error_list)
+
+ instances = db.instance_get_all(context.get_admin_context())
+ LOG.info(_("After force-killing instances: %s"), instances)
+ self.assertEqual(len(instances), 0)
diff --git a/nova/tests/test_flat_network.py b/nova/tests/test_flat_network.py
new file mode 100644
index 000000000..dcc617e25
--- /dev/null
+++ b/nova/tests/test_flat_network.py
@@ -0,0 +1,161 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+"""
+Unit Tests for flat network code
+"""
+import IPy
+import os
+import unittest
+
+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 test
+from nova import utils
+from nova.auth import manager
+from nova.tests.network import base
+
+
+FLAGS = flags.FLAGS
+LOG = logging.getLogger('nova.tests.network')
+
+
+class FlatNetworkTestCase(base.NetworkTestCase):
+ """Test cases for network code"""
+ def test_public_network_association(self):
+ """Makes sure that we can allocate a public ip"""
+ # TODO(vish): better way of adding floating ips
+
+ self.context._project = self.projects[0]
+ self.context.project_id = self.projects[0].id
+ pubnet = IPy.IP(flags.FLAGS.floating_range)
+ address = str(pubnet[0])
+ try:
+ db.floating_ip_get_by_address(context.get_admin_context(), address)
+ except exception.NotFound:
+ db.floating_ip_create(context.get_admin_context(),
+ {'address': address,
+ 'host': FLAGS.host})
+
+ self.assertRaises(NotImplementedError,
+ self.network.allocate_floating_ip,
+ self.context, self.projects[0].id)
+
+ fix_addr = self._create_address(0)
+ float_addr = address
+ self.assertRaises(NotImplementedError,
+ self.network.associate_floating_ip,
+ self.context, float_addr, fix_addr)
+
+ address = db.instance_get_floating_address(context.get_admin_context(),
+ self.instance_id)
+ self.assertEqual(address, None)
+
+ self.assertRaises(NotImplementedError,
+ self.network.disassociate_floating_ip,
+ self.context, float_addr)
+
+ address = db.instance_get_floating_address(context.get_admin_context(),
+ self.instance_id)
+ self.assertEqual(address, None)
+
+ self.assertRaises(NotImplementedError,
+ self.network.deallocate_floating_ip,
+ self.context, float_addr)
+
+ self.network.deallocate_fixed_ip(self.context, fix_addr)
+ db.floating_ip_destroy(context.get_admin_context(), float_addr)
+
+ def test_allocate_deallocate_fixed_ip(self):
+ """Makes sure that we can allocate and deallocate a fixed ip"""
+ address = self._create_address(0)
+ self.assertTrue(self._is_allocated_in_project(address,
+ self.projects[0].id))
+ self._deallocate_address(0, address)
+
+ # check if the fixed ip address is really deallocated
+ self.assertFalse(self._is_allocated_in_project(address,
+ self.projects[0].id))
+
+ def test_side_effects(self):
+ """Ensures allocating and releasing has no side effects"""
+ address = self._create_address(0)
+ address2 = self._create_address(1, self.instance2_id)
+
+ self.assertTrue(self._is_allocated_in_project(address,
+ self.projects[0].id))
+ self.assertTrue(self._is_allocated_in_project(address2,
+ self.projects[1].id))
+
+ self._deallocate_address(0, address)
+ self.assertFalse(self._is_allocated_in_project(address,
+ self.projects[0].id))
+
+ # First address release shouldn't affect the second
+ self.assertTrue(self._is_allocated_in_project(address2,
+ self.projects[0].id))
+
+ self._deallocate_address(1, address2)
+ self.assertFalse(self._is_allocated_in_project(address2,
+ self.projects[1].id))
+
+ def test_ips_are_reused(self):
+ """Makes sure that ip addresses that are deallocated get reused"""
+ address = self._create_address(0)
+ self.network.deallocate_fixed_ip(self.context, address)
+
+ address2 = self._create_address(0)
+ self.assertEqual(address, address2)
+
+ self.network.deallocate_fixed_ip(self.context, address2)
+
+ def test_too_many_addresses(self):
+ """Test for a NoMoreAddresses exception when all fixed ips are used.
+ """
+ admin_context = context.get_admin_context()
+ network = db.project_get_network(admin_context, self.projects[0].id)
+ num_available_ips = db.network_count_available_ips(admin_context,
+ network['id'])
+ addresses = []
+ instance_ids = []
+ for i in range(num_available_ips):
+ instance_ref = self._create_instance(0)
+ instance_ids.append(instance_ref['id'])
+ address = self._create_address(0, instance_ref['id'])
+ addresses.append(address)
+
+ ip_count = db.network_count_available_ips(context.get_admin_context(),
+ network['id'])
+ self.assertEqual(ip_count, 0)
+ self.assertRaises(db.NoMoreAddresses,
+ self.network.allocate_fixed_ip,
+ self.context,
+ 'foo')
+
+ for i in range(num_available_ips):
+ self.network.deallocate_fixed_ip(self.context, addresses[i])
+ db.instance_destroy(context.get_admin_context(), instance_ids[i])
+ ip_count = db.network_count_available_ips(context.get_admin_context(),
+ network['id'])
+ self.assertEqual(ip_count, num_available_ips)
+
+ def run(self, result=None):
+ if(FLAGS.network_manager == 'nova.network.manager.FlatManager'):
+ super(FlatNetworkTestCase, self).run(result)
diff --git a/nova/tests/test_localization.py b/nova/tests/test_localization.py
index 393d71038..a25809a79 100644
--- a/nova/tests/test_localization.py
+++ b/nova/tests/test_localization.py
@@ -21,9 +21,10 @@ import sys
import unittest
import nova
+from nova import test
-class LocalizationTestCase(unittest.TestCase):
+class LocalizationTestCase(test.TestCase):
def test_multiple_positional_format_placeholders(self):
pat = re.compile("\W_\(")
single_pat = re.compile("\W%\W")
diff --git a/nova/tests/test_middleware.py b/nova/tests/test_middleware.py
index 9d49167ba..6564a6955 100644
--- a/nova/tests/test_middleware.py
+++ b/nova/tests/test_middleware.py
@@ -40,12 +40,12 @@ def conditional_forbid(req):
class LockoutTestCase(test.TestCase):
"""Test case for the Lockout middleware."""
- def setUp(self): # pylint: disable-msg=C0103
+ def setUp(self): # pylint: disable=C0103
super(LockoutTestCase, self).setUp()
utils.set_time_override()
self.lockout = ec2.Lockout(conditional_forbid)
- def tearDown(self): # pylint: disable-msg=C0103
+ def tearDown(self): # pylint: disable=C0103
utils.clear_time_override()
super(LockoutTestCase, self).tearDown()
diff --git a/nova/tests/test_misc.py b/nova/tests/test_misc.py
index 1fbaf304f..4e17e1ce0 100644
--- a/nova/tests/test_misc.py
+++ b/nova/tests/test_misc.py
@@ -18,8 +18,12 @@ import errno
import os
import select
+from eventlet import greenpool
+from eventlet import greenthread
+
from nova import test
-from nova.utils import parse_mailmap, str_dict_replace, synchronized
+from nova import utils
+from nova.utils import parse_mailmap, str_dict_replace
class ProjectTestCase(test.TestCase):
@@ -63,7 +67,7 @@ class ProjectTestCase(test.TestCase):
class LockTestCase(test.TestCase):
def test_synchronized_wrapped_function_metadata(self):
- @synchronized('whatever')
+ @utils.synchronized('whatever')
def foo():
"""Bar"""
pass
@@ -72,11 +76,42 @@ class LockTestCase(test.TestCase):
self.assertEquals(foo.__name__, 'foo', "Wrapped function's name "
"got mangled")
- def test_synchronized(self):
+ def test_synchronized_internally(self):
+ """We can lock across multiple green threads"""
+ saved_sem_num = len(utils._semaphores)
+ seen_threads = list()
+
+ @utils.synchronized('testlock2', external=False)
+ def f(id):
+ for x in range(10):
+ seen_threads.append(id)
+ greenthread.sleep(0)
+
+ threads = []
+ pool = greenpool.GreenPool(10)
+ for i in range(10):
+ threads.append(pool.spawn(f, i))
+
+ for thread in threads:
+ thread.wait()
+
+ self.assertEquals(len(seen_threads), 100)
+ # Looking at the seen threads, split it into chunks of 10, and verify
+ # that the last 9 match the first in each chunk.
+ for i in range(10):
+ for j in range(9):
+ self.assertEquals(seen_threads[i * 10],
+ seen_threads[i * 10 + 1 + j])
+
+ self.assertEqual(saved_sem_num, len(utils._semaphores),
+ "Semaphore leak detected")
+
+ def test_synchronized_externally(self):
+ """We can lock across multiple processes"""
rpipe1, wpipe1 = os.pipe()
rpipe2, wpipe2 = os.pipe()
- @synchronized('testlock')
+ @utils.synchronized('testlock1', external=True)
def f(rpipe, wpipe):
try:
os.write(wpipe, "foo")
diff --git a/nova/tests/test_network.py b/nova/tests/test_network.py
index 1e634b388..77f6aaff3 100644
--- a/nova/tests/test_network.py
+++ b/nova/tests/test_network.py
@@ -20,21 +20,10 @@ Unit Tests for network code
"""
import IPy
import os
-import time
-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 test
-from nova import utils
-from nova.auth import manager
from nova.network import linux_net
-FLAGS = flags.FLAGS
-LOG = logging.getLogger('nova.tests.network')
-
class IptablesManagerTestCase(test.TestCase):
sample_filter = ['#Generated by iptables-save on Fri Feb 18 15:17:05 2011',
@@ -175,363 +164,3 @@ class IptablesManagerTestCase(test.TestCase):
self.assertTrue('-A %s -j run_tests.py-%s' \
% (chain, chain) in new_lines,
"Built-in chain %s not wrapped" % (chain,))
-
-
-class NetworkTestCase(test.TestCase):
- """Test cases for network code"""
- def setUp(self):
- super(NetworkTestCase, self).setUp()
- # NOTE(vish): if you change these flags, make sure to change the
- # flags in the corresponding section in nova-dhcpbridge
- self.flags(connection_type='fake',
- fake_call=True,
- fake_network=True)
- self.manager = manager.AuthManager()
- self.user = self.manager.create_user('netuser', 'netuser', 'netuser')
- self.projects = []
- self.network = utils.import_object(FLAGS.network_manager)
- self.context = context.RequestContext(project=None, user=self.user)
- for i in range(FLAGS.num_networks):
- name = 'project%s' % i
- project = self.manager.create_project(name, 'netuser', name)
- self.projects.append(project)
- # create the necessary network data for the project
- user_context = context.RequestContext(project=self.projects[i],
- user=self.user)
- host = self.network.get_network_host(user_context.elevated())
- instance_ref = self._create_instance(0)
- self.instance_id = instance_ref['id']
- instance_ref = self._create_instance(1)
- self.instance2_id = instance_ref['id']
-
- def tearDown(self):
- # TODO(termie): this should really be instantiating clean datastores
- # in between runs, one failure kills all the tests
- db.instance_destroy(context.get_admin_context(), self.instance_id)
- db.instance_destroy(context.get_admin_context(), self.instance2_id)
- for project in self.projects:
- self.manager.delete_project(project)
- self.manager.delete_user(self.user)
- super(NetworkTestCase, self).tearDown()
-
- def _create_instance(self, project_num, mac=None):
- if not mac:
- mac = utils.generate_mac()
- project = self.projects[project_num]
- self.context._project = project
- self.context.project_id = project.id
- return db.instance_create(self.context,
- {'project_id': project.id,
- 'mac_address': mac})
-
- def _create_address(self, project_num, instance_id=None):
- """Create an address in given project num"""
- if instance_id is None:
- instance_id = self.instance_id
- self.context._project = self.projects[project_num]
- self.context.project_id = self.projects[project_num].id
- return self.network.allocate_fixed_ip(self.context, instance_id)
-
- def _deallocate_address(self, project_num, address):
- self.context._project = self.projects[project_num]
- self.context.project_id = self.projects[project_num].id
- self.network.deallocate_fixed_ip(self.context, address)
-
- def test_private_ipv6(self):
- """Make sure ipv6 is OK"""
- if FLAGS.use_ipv6:
- instance_ref = self._create_instance(0)
- address = self._create_address(0, instance_ref['id'])
- network_ref = db.project_get_network(
- context.get_admin_context(),
- self.context.project_id)
- address_v6 = db.instance_get_fixed_address_v6(
- context.get_admin_context(),
- instance_ref['id'])
- self.assertEqual(instance_ref['mac_address'],
- utils.to_mac(address_v6))
- instance_ref2 = db.fixed_ip_get_instance_v6(
- context.get_admin_context(),
- address_v6)
- self.assertEqual(instance_ref['id'], instance_ref2['id'])
- self.assertEqual(address_v6,
- utils.to_global_ipv6(
- network_ref['cidr_v6'],
- instance_ref['mac_address']))
- self._deallocate_address(0, address)
- db.instance_destroy(context.get_admin_context(),
- instance_ref['id'])
-
- def test_public_network_association(self):
- """Makes sure that we can allocaate a public ip"""
- # TODO(vish): better way of adding floating ips
- self.context._project = self.projects[0]
- self.context.project_id = self.projects[0].id
- pubnet = IPy.IP(flags.FLAGS.floating_range)
- address = str(pubnet[0])
- try:
- db.floating_ip_get_by_address(context.get_admin_context(), address)
- except exception.NotFound:
- db.floating_ip_create(context.get_admin_context(),
- {'address': address,
- 'host': FLAGS.host})
- float_addr = self.network.allocate_floating_ip(self.context,
- self.projects[0].id)
- fix_addr = self._create_address(0)
- lease_ip(fix_addr)
- self.assertEqual(float_addr, str(pubnet[0]))
- self.network.associate_floating_ip(self.context, float_addr, fix_addr)
- address = db.instance_get_floating_address(context.get_admin_context(),
- self.instance_id)
- self.assertEqual(address, float_addr)
- self.network.disassociate_floating_ip(self.context, float_addr)
- address = db.instance_get_floating_address(context.get_admin_context(),
- self.instance_id)
- self.assertEqual(address, None)
- self.network.deallocate_floating_ip(self.context, float_addr)
- self.network.deallocate_fixed_ip(self.context, fix_addr)
- release_ip(fix_addr)
- db.floating_ip_destroy(context.get_admin_context(), float_addr)
-
- def test_allocate_deallocate_fixed_ip(self):
- """Makes sure that we can allocate and deallocate a fixed ip"""
- address = self._create_address(0)
- self.assertTrue(is_allocated_in_project(address, self.projects[0].id))
- lease_ip(address)
- self._deallocate_address(0, address)
-
- # Doesn't go away until it's dhcp released
- self.assertTrue(is_allocated_in_project(address, self.projects[0].id))
-
- release_ip(address)
- self.assertFalse(is_allocated_in_project(address, self.projects[0].id))
-
- def test_side_effects(self):
- """Ensures allocating and releasing has no side effects"""
- address = self._create_address(0)
- address2 = self._create_address(1, self.instance2_id)
-
- self.assertTrue(is_allocated_in_project(address, self.projects[0].id))
- self.assertTrue(is_allocated_in_project(address2, self.projects[1].id))
- self.assertFalse(is_allocated_in_project(address, self.projects[1].id))
-
- # Addresses are allocated before they're issued
- lease_ip(address)
- lease_ip(address2)
-
- self._deallocate_address(0, address)
- release_ip(address)
- self.assertFalse(is_allocated_in_project(address, self.projects[0].id))
-
- # First address release shouldn't affect the second
- self.assertTrue(is_allocated_in_project(address2, self.projects[1].id))
-
- self._deallocate_address(1, address2)
- release_ip(address2)
- self.assertFalse(is_allocated_in_project(address2,
- self.projects[1].id))
-
- def test_subnet_edge(self):
- """Makes sure that private ips don't overlap"""
- first = self._create_address(0)
- lease_ip(first)
- instance_ids = []
- for i in range(1, FLAGS.num_networks):
- instance_ref = self._create_instance(i, mac=utils.generate_mac())
- instance_ids.append(instance_ref['id'])
- address = self._create_address(i, instance_ref['id'])
- instance_ref = self._create_instance(i, mac=utils.generate_mac())
- instance_ids.append(instance_ref['id'])
- address2 = self._create_address(i, instance_ref['id'])
- instance_ref = self._create_instance(i, mac=utils.generate_mac())
- instance_ids.append(instance_ref['id'])
- address3 = self._create_address(i, instance_ref['id'])
- lease_ip(address)
- lease_ip(address2)
- lease_ip(address3)
- self.context._project = self.projects[i]
- self.context.project_id = self.projects[i].id
- self.assertFalse(is_allocated_in_project(address,
- self.projects[0].id))
- self.assertFalse(is_allocated_in_project(address2,
- self.projects[0].id))
- self.assertFalse(is_allocated_in_project(address3,
- self.projects[0].id))
- self.network.deallocate_fixed_ip(self.context, address)
- self.network.deallocate_fixed_ip(self.context, address2)
- self.network.deallocate_fixed_ip(self.context, address3)
- release_ip(address)
- release_ip(address2)
- release_ip(address3)
- for instance_id in instance_ids:
- db.instance_destroy(context.get_admin_context(), instance_id)
- self.context._project = self.projects[0]
- self.context.project_id = self.projects[0].id
- self.network.deallocate_fixed_ip(self.context, first)
- self._deallocate_address(0, first)
- release_ip(first)
-
- def test_vpn_ip_and_port_looks_valid(self):
- """Ensure the vpn ip and port are reasonable"""
- self.assert_(self.projects[0].vpn_ip)
- self.assert_(self.projects[0].vpn_port >= FLAGS.vpn_start)
- self.assert_(self.projects[0].vpn_port <= FLAGS.vpn_start +
- FLAGS.num_networks)
-
- def test_too_many_networks(self):
- """Ensure error is raised if we run out of networks"""
- projects = []
- networks_left = (FLAGS.num_networks -
- db.network_count(context.get_admin_context()))
- for i in range(networks_left):
- project = self.manager.create_project('many%s' % i, self.user)
- projects.append(project)
- db.project_get_network(context.get_admin_context(), project.id)
- project = self.manager.create_project('last', self.user)
- projects.append(project)
- self.assertRaises(db.NoMoreNetworks,
- db.project_get_network,
- context.get_admin_context(),
- project.id)
- for project in projects:
- self.manager.delete_project(project)
-
- def test_ips_are_reused(self):
- """Makes sure that ip addresses that are deallocated get reused"""
- address = self._create_address(0)
- lease_ip(address)
- self.network.deallocate_fixed_ip(self.context, address)
- release_ip(address)
-
- address2 = self._create_address(0)
- self.assertEqual(address, address2)
- lease_ip(address)
- self.network.deallocate_fixed_ip(self.context, address2)
- release_ip(address)
-
- def test_available_ips(self):
- """Make sure the number of available ips for the network is correct
-
- The number of available IP addresses depends on the test
- environment's setup.
-
- Network size is set in test fixture's setUp method.
-
- There are ips reserved at the bottom and top of the range.
- services (network, gateway, CloudPipe, broadcast)
- """
- network = db.project_get_network(context.get_admin_context(),
- self.projects[0].id)
- net_size = flags.FLAGS.network_size
- admin_context = context.get_admin_context()
- total_ips = (db.network_count_available_ips(admin_context,
- network['id']) +
- db.network_count_reserved_ips(admin_context,
- network['id']) +
- db.network_count_allocated_ips(admin_context,
- network['id']))
- self.assertEqual(total_ips, net_size)
-
- def test_too_many_addresses(self):
- """Test for a NoMoreAddresses exception when all fixed ips are used.
- """
- admin_context = context.get_admin_context()
- network = db.project_get_network(admin_context, self.projects[0].id)
- num_available_ips = db.network_count_available_ips(admin_context,
- network['id'])
- addresses = []
- instance_ids = []
- for i in range(num_available_ips):
- instance_ref = self._create_instance(0)
- instance_ids.append(instance_ref['id'])
- address = self._create_address(0, instance_ref['id'])
- addresses.append(address)
- lease_ip(address)
-
- ip_count = db.network_count_available_ips(context.get_admin_context(),
- network['id'])
- self.assertEqual(ip_count, 0)
- self.assertRaises(db.NoMoreAddresses,
- self.network.allocate_fixed_ip,
- self.context,
- 'foo')
-
- for i in range(num_available_ips):
- self.network.deallocate_fixed_ip(self.context, addresses[i])
- release_ip(addresses[i])
- db.instance_destroy(context.get_admin_context(), instance_ids[i])
- ip_count = db.network_count_available_ips(context.get_admin_context(),
- network['id'])
- self.assertEqual(ip_count, num_available_ips)
-
- def test_dhcp_lease_output(self):
- admin_ctxt = context.get_admin_context()
- address = self._create_address(0, self.instance_id)
- lease_ip(address)
- network_ref = db.network_get_by_instance(admin_ctxt, self.instance_id)
- leases = linux_net.get_dhcp_leases(context.get_admin_context(),
- network_ref['id'])
- for line in leases.split('\n'):
- seconds, mac, ip, hostname, client_id = line.split(' ')
- self.assertTrue(int(seconds) > time.time(), 'Lease expires in '
- 'the past')
- octets = mac.split(':')
- self.assertEqual(len(octets), 6, "Wrong number of octets "
- "in %s" % (max,))
- for octet in octets:
- self.assertEqual(len(octet), 2, "Oddly sized octet: %s"
- % (octet,))
- # This will throw an exception if the octet is invalid
- int(octet, 16)
-
- # And this will raise an exception in case of an invalid IP
- IPy.IP(ip)
-
- release_ip(address)
-
-
-def is_allocated_in_project(address, project_id):
- """Returns true if address is in specified project"""
- project_net = db.project_get_network(context.get_admin_context(),
- project_id)
- network = db.fixed_ip_get_network(context.get_admin_context(), address)
- instance = db.fixed_ip_get_instance(context.get_admin_context(), address)
- # instance exists until release
- return instance is not None and network['id'] == project_net['id']
-
-
-def binpath(script):
- """Returns the absolute path to a script in bin"""
- return os.path.abspath(os.path.join(__file__, "../../../bin", script))
-
-
-def lease_ip(private_ip):
- """Run add command on dhcpbridge"""
- network_ref = db.fixed_ip_get_network(context.get_admin_context(),
- private_ip)
- instance_ref = db.fixed_ip_get_instance(context.get_admin_context(),
- private_ip)
- cmd = (binpath('nova-dhcpbridge'), 'add',
- instance_ref['mac_address'],
- private_ip, 'fake')
- env = {'DNSMASQ_INTERFACE': network_ref['bridge'],
- 'TESTING': '1',
- 'FLAGFILE': FLAGS.dhcpbridge_flagfile}
- (out, err) = utils.execute(*cmd, addl_env=env)
- LOG.debug("ISSUE_IP: %s, %s ", out, err)
-
-
-def release_ip(private_ip):
- """Run del command on dhcpbridge"""
- network_ref = db.fixed_ip_get_network(context.get_admin_context(),
- private_ip)
- instance_ref = db.fixed_ip_get_instance(context.get_admin_context(),
- private_ip)
- cmd = (binpath('nova-dhcpbridge'), 'del',
- instance_ref['mac_address'],
- private_ip, 'fake')
- env = {'DNSMASQ_INTERFACE': network_ref['bridge'],
- 'TESTING': '1',
- 'FLAGFILE': FLAGS.dhcpbridge_flagfile}
- (out, err) = utils.execute(*cmd, addl_env=env)
- LOG.debug("RELEASE_IP: %s, %s ", out, err)
diff --git a/nova/tests/test_rpc.py b/nova/tests/test_rpc.py
index 4820e04fb..44d7c91eb 100644
--- a/nova/tests/test_rpc.py
+++ b/nova/tests/test_rpc.py
@@ -36,7 +36,7 @@ class RpcTestCase(test.TestCase):
super(RpcTestCase, self).setUp()
self.conn = rpc.Connection.instance(True)
self.receiver = TestReceiver()
- self.consumer = rpc.AdapterConsumer(connection=self.conn,
+ self.consumer = rpc.TopicAdapterConsumer(connection=self.conn,
topic='test',
proxy=self.receiver)
self.consumer.attach_to_eventlet()
@@ -97,7 +97,7 @@ class RpcTestCase(test.TestCase):
nested = Nested()
conn = rpc.Connection.instance(True)
- consumer = rpc.AdapterConsumer(connection=conn,
+ consumer = rpc.TopicAdapterConsumer(connection=conn,
topic='nested',
proxy=nested)
consumer.attach_to_eventlet()
diff --git a/nova/tests/test_service.py b/nova/tests/test_service.py
index 393f9d20b..d48de2057 100644
--- a/nova/tests/test_service.py
+++ b/nova/tests/test_service.py
@@ -109,20 +109,29 @@ class ServiceTestCase(test.TestCase):
app = service.Service.create(host=host, binary=binary)
self.mox.StubOutWithMock(rpc,
- 'AdapterConsumer',
+ 'TopicAdapterConsumer',
use_mock_anything=True)
- rpc.AdapterConsumer(connection=mox.IgnoreArg(),
+ self.mox.StubOutWithMock(rpc,
+ 'FanoutAdapterConsumer',
+ use_mock_anything=True)
+ rpc.TopicAdapterConsumer(connection=mox.IgnoreArg(),
topic=topic,
proxy=mox.IsA(service.Service)).AndReturn(
- rpc.AdapterConsumer)
+ rpc.TopicAdapterConsumer)
- rpc.AdapterConsumer(connection=mox.IgnoreArg(),
+ rpc.TopicAdapterConsumer(connection=mox.IgnoreArg(),
topic='%s.%s' % (topic, host),
proxy=mox.IsA(service.Service)).AndReturn(
- rpc.AdapterConsumer)
+ rpc.TopicAdapterConsumer)
+
+ rpc.FanoutAdapterConsumer(connection=mox.IgnoreArg(),
+ topic=topic,
+ proxy=mox.IsA(service.Service)).AndReturn(
+ rpc.FanoutAdapterConsumer)
- rpc.AdapterConsumer.attach_to_eventlet()
- rpc.AdapterConsumer.attach_to_eventlet()
+ rpc.TopicAdapterConsumer.attach_to_eventlet()
+ rpc.TopicAdapterConsumer.attach_to_eventlet()
+ rpc.FanoutAdapterConsumer.attach_to_eventlet()
service_create = {'host': host,
'binary': binary,
@@ -279,6 +288,7 @@ class ServiceTestCase(test.TestCase):
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(serv.manager.driver,
'update_available_resource')
serv.manager.driver.update_available_resource(mox.IgnoreArg(), host)
diff --git a/nova/tests/test_test.py b/nova/tests/test_test.py
index e237674e6..35c838065 100644
--- a/nova/tests/test_test.py
+++ b/nova/tests/test_test.py
@@ -34,7 +34,7 @@ class IsolationTestCase(test.TestCase):
def test_rpc_consumer_isolation(self):
connection = rpc.Connection.instance(new=True)
- consumer = rpc.TopicConsumer(connection, topic='compute')
+ consumer = rpc.TopicAdapterConsumer(connection, topic='compute')
consumer.register_callback(
lambda x, y: self.fail('I should never be called'))
consumer.attach_to_eventlet()
diff --git a/nova/tests/test_utils.py b/nova/tests/test_utils.py
index 34a407f1a..e08d229b0 100644
--- a/nova/tests/test_utils.py
+++ b/nova/tests/test_utils.py
@@ -14,11 +14,89 @@
# License for the specific language governing permissions and limitations
# under the License.
+import os
+import tempfile
+
from nova import test
from nova import utils
from nova import exception
+class ExecuteTestCase(test.TestCase):
+ def test_retry_on_failure(self):
+ fd, tmpfilename = tempfile.mkstemp()
+ _, tmpfilename2 = tempfile.mkstemp()
+ try:
+ fp = os.fdopen(fd, 'w+')
+ fp.write('''#!/bin/sh
+# If stdin fails to get passed during one of the runs, make a note.
+if ! grep -q foo
+then
+ echo 'failure' > "$1"
+fi
+# If stdin has failed to get passed during this or a previous run, exit early.
+if grep failure "$1"
+then
+ exit 1
+fi
+runs="$(cat $1)"
+if [ -z "$runs" ]
+then
+ runs=0
+fi
+runs=$(($runs + 1))
+echo $runs > "$1"
+exit 1
+''')
+ fp.close()
+ os.chmod(tmpfilename, 0755)
+ self.assertRaises(exception.ProcessExecutionError,
+ utils.execute,
+ tmpfilename, tmpfilename2, attempts=10,
+ process_input='foo',
+ delay_on_retry=False)
+ fp = open(tmpfilename2, 'r+')
+ runs = fp.read()
+ fp.close()
+ self.assertNotEquals(runs.strip(), 'failure', 'stdin did not '
+ 'always get passed '
+ 'correctly')
+ runs = int(runs.strip())
+ self.assertEquals(runs, 10,
+ 'Ran %d times instead of 10.' % (runs,))
+ finally:
+ os.unlink(tmpfilename)
+ os.unlink(tmpfilename2)
+
+ def test_unknown_kwargs_raises_error(self):
+ self.assertRaises(exception.Error,
+ utils.execute,
+ '/bin/true', this_is_not_a_valid_kwarg=True)
+
+ def test_no_retry_on_success(self):
+ fd, tmpfilename = tempfile.mkstemp()
+ _, tmpfilename2 = tempfile.mkstemp()
+ try:
+ fp = os.fdopen(fd, 'w+')
+ fp.write('''#!/bin/sh
+# If we've already run, bail out.
+grep -q foo "$1" && exit 1
+# Mark that we've run before.
+echo foo > "$1"
+# Check that stdin gets passed correctly.
+grep foo
+''')
+ fp.close()
+ os.chmod(tmpfilename, 0755)
+ utils.execute(tmpfilename,
+ tmpfilename2,
+ process_input='foo',
+ attempts=2)
+ finally:
+ os.unlink(tmpfilename)
+ os.unlink(tmpfilename2)
+
+
class GetFromPathTestCase(test.TestCase):
def test_tolerates_nones(self):
f = utils.get_from_path
diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py
index b214f5ce7..fb0ba53b1 100644
--- a/nova/tests/test_virt.py
+++ b/nova/tests/test_virt.py
@@ -77,13 +77,11 @@ class CacheConcurrencyTestCase(test.TestCase):
eventlet.sleep(0)
try:
self.assertFalse(done2.ready())
- self.assertTrue('fname' in conn._image_sems)
finally:
wait1.send()
done1.wait()
eventlet.sleep(0)
self.assertTrue(done2.ready())
- self.assertFalse('fname' in conn._image_sems)
def test_different_fname_concurrency(self):
"""Ensures that two different fname caches are concurrent"""
@@ -429,6 +427,15 @@ class LibvirtConnTestCase(test.TestCase):
def fake_raise(self):
raise libvirt.libvirtError('ERR')
+ class FakeTime(object):
+ def __init__(self):
+ self.counter = 0
+
+ def sleep(self, t):
+ self.counter += t
+
+ fake_timer = FakeTime()
+
self.create_fake_libvirt_mock(nwfilterLookupByName=fake_raise)
instance_ref = db.instance_create(self.context, self.test_instance)
@@ -438,11 +445,15 @@ class LibvirtConnTestCase(test.TestCase):
conn = libvirt_conn.LibvirtConnection(False)
conn.firewall_driver.setattr('setup_basic_filtering', fake_none)
conn.firewall_driver.setattr('prepare_instance_filter', fake_none)
- conn.ensure_filtering_rules_for_instance(instance_ref)
+ conn.ensure_filtering_rules_for_instance(instance_ref,
+ time=fake_timer)
except exception.Error, e:
c1 = (0 <= e.message.find('Timeout migrating for'))
self.assertTrue(c1)
+ self.assertEqual(29, fake_timer.counter, "Didn't wait the expected "
+ "amount of time")
+
db.instance_destroy(self.context, instance_ref['id'])
def test_live_migration_raises_exception(self):
diff --git a/nova/tests/test_vlan_network.py b/nova/tests/test_vlan_network.py
new file mode 100644
index 000000000..063b81832
--- /dev/null
+++ b/nova/tests/test_vlan_network.py
@@ -0,0 +1,242 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+"""
+Unit Tests for vlan network code
+"""
+import IPy
+import os
+
+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 test
+from nova import utils
+from nova.auth import manager
+from nova.tests.network import base
+from nova.tests.network import binpath,\
+ lease_ip, release_ip
+
+FLAGS = flags.FLAGS
+LOG = logging.getLogger('nova.tests.network')
+
+
+class VlanNetworkTestCase(base.NetworkTestCase):
+ """Test cases for network code"""
+ def test_public_network_association(self):
+ """Makes sure that we can allocaate a public ip"""
+ # TODO(vish): better way of adding floating ips
+ self.context._project = self.projects[0]
+ self.context.project_id = self.projects[0].id
+ pubnet = IPy.IP(flags.FLAGS.floating_range)
+ address = str(pubnet[0])
+ try:
+ db.floating_ip_get_by_address(context.get_admin_context(), address)
+ except exception.NotFound:
+ db.floating_ip_create(context.get_admin_context(),
+ {'address': address,
+ 'host': FLAGS.host})
+ float_addr = self.network.allocate_floating_ip(self.context,
+ self.projects[0].id)
+ fix_addr = self._create_address(0)
+ lease_ip(fix_addr)
+ self.assertEqual(float_addr, str(pubnet[0]))
+ self.network.associate_floating_ip(self.context, float_addr, fix_addr)
+ address = db.instance_get_floating_address(context.get_admin_context(),
+ self.instance_id)
+ self.assertEqual(address, float_addr)
+ self.network.disassociate_floating_ip(self.context, float_addr)
+ address = db.instance_get_floating_address(context.get_admin_context(),
+ self.instance_id)
+ self.assertEqual(address, None)
+ self.network.deallocate_floating_ip(self.context, float_addr)
+ self.network.deallocate_fixed_ip(self.context, fix_addr)
+ release_ip(fix_addr)
+ db.floating_ip_destroy(context.get_admin_context(), float_addr)
+
+ def test_allocate_deallocate_fixed_ip(self):
+ """Makes sure that we can allocate and deallocate a fixed ip"""
+ address = self._create_address(0)
+ self.assertTrue(self._is_allocated_in_project(address,
+ self.projects[0].id))
+ lease_ip(address)
+ self._deallocate_address(0, address)
+
+ # Doesn't go away until it's dhcp released
+ self.assertTrue(self._is_allocated_in_project(address,
+ self.projects[0].id))
+
+ release_ip(address)
+ self.assertFalse(self._is_allocated_in_project(address,
+ self.projects[0].id))
+
+ def test_side_effects(self):
+ """Ensures allocating and releasing has no side effects"""
+ address = self._create_address(0)
+ address2 = self._create_address(1, self.instance2_id)
+
+ self.assertTrue(self._is_allocated_in_project(address,
+ self.projects[0].id))
+ self.assertTrue(self._is_allocated_in_project(address2,
+ self.projects[1].id))
+ self.assertFalse(self._is_allocated_in_project(address,
+ self.projects[1].id))
+
+ # Addresses are allocated before they're issued
+ lease_ip(address)
+ lease_ip(address2)
+
+ self._deallocate_address(0, address)
+ release_ip(address)
+ self.assertFalse(self._is_allocated_in_project(address,
+ self.projects[0].id))
+
+ # First address release shouldn't affect the second
+ self.assertTrue(self._is_allocated_in_project(address2,
+ self.projects[1].id))
+
+ self._deallocate_address(1, address2)
+ release_ip(address2)
+ self.assertFalse(self._is_allocated_in_project(address2,
+ self.projects[1].id))
+
+ def test_subnet_edge(self):
+ """Makes sure that private ips don't overlap"""
+ first = self._create_address(0)
+ lease_ip(first)
+ instance_ids = []
+ for i in range(1, FLAGS.num_networks):
+ instance_ref = self._create_instance(i, mac=utils.generate_mac())
+ instance_ids.append(instance_ref['id'])
+ address = self._create_address(i, instance_ref['id'])
+ instance_ref = self._create_instance(i, mac=utils.generate_mac())
+ instance_ids.append(instance_ref['id'])
+ address2 = self._create_address(i, instance_ref['id'])
+ instance_ref = self._create_instance(i, mac=utils.generate_mac())
+ instance_ids.append(instance_ref['id'])
+ address3 = self._create_address(i, instance_ref['id'])
+ lease_ip(address)
+ lease_ip(address2)
+ lease_ip(address3)
+ self.context._project = self.projects[i]
+ self.context.project_id = self.projects[i].id
+ self.assertFalse(self._is_allocated_in_project(address,
+ self.projects[0].id))
+ self.assertFalse(self._is_allocated_in_project(address2,
+ self.projects[0].id))
+ self.assertFalse(self._is_allocated_in_project(address3,
+ self.projects[0].id))
+ self.network.deallocate_fixed_ip(self.context, address)
+ self.network.deallocate_fixed_ip(self.context, address2)
+ self.network.deallocate_fixed_ip(self.context, address3)
+ release_ip(address)
+ release_ip(address2)
+ release_ip(address3)
+ for instance_id in instance_ids:
+ db.instance_destroy(context.get_admin_context(), instance_id)
+ self.context._project = self.projects[0]
+ self.context.project_id = self.projects[0].id
+ self.network.deallocate_fixed_ip(self.context, first)
+ self._deallocate_address(0, first)
+ release_ip(first)
+
+ def test_vpn_ip_and_port_looks_valid(self):
+ """Ensure the vpn ip and port are reasonable"""
+ self.assert_(self.projects[0].vpn_ip)
+ self.assert_(self.projects[0].vpn_port >= FLAGS.vpn_start)
+ self.assert_(self.projects[0].vpn_port <= FLAGS.vpn_start +
+ FLAGS.num_networks)
+
+ def test_too_many_networks(self):
+ """Ensure error is raised if we run out of networks"""
+ projects = []
+ networks_left = (FLAGS.num_networks -
+ db.network_count(context.get_admin_context()))
+ for i in range(networks_left):
+ project = self.manager.create_project('many%s' % i, self.user)
+ projects.append(project)
+ db.project_get_network(context.get_admin_context(), project.id)
+ project = self.manager.create_project('last', self.user)
+ projects.append(project)
+ self.assertRaises(db.NoMoreNetworks,
+ db.project_get_network,
+ context.get_admin_context(),
+ project.id)
+ for project in projects:
+ self.manager.delete_project(project)
+
+ def test_ips_are_reused(self):
+ """Makes sure that ip addresses that are deallocated get reused"""
+ address = self._create_address(0)
+ lease_ip(address)
+ self.network.deallocate_fixed_ip(self.context, address)
+ release_ip(address)
+
+ address2 = self._create_address(0)
+ self.assertEqual(address, address2)
+ lease_ip(address)
+ self.network.deallocate_fixed_ip(self.context, address2)
+ release_ip(address)
+
+ def test_too_many_addresses(self):
+ """Test for a NoMoreAddresses exception when all fixed ips are used.
+ """
+ admin_context = context.get_admin_context()
+ network = db.project_get_network(admin_context, self.projects[0].id)
+ num_available_ips = db.network_count_available_ips(admin_context,
+ network['id'])
+ addresses = []
+ instance_ids = []
+ for i in range(num_available_ips):
+ instance_ref = self._create_instance(0)
+ instance_ids.append(instance_ref['id'])
+ address = self._create_address(0, instance_ref['id'])
+ addresses.append(address)
+ lease_ip(address)
+
+ ip_count = db.network_count_available_ips(context.get_admin_context(),
+ network['id'])
+ self.assertEqual(ip_count, 0)
+ self.assertRaises(db.NoMoreAddresses,
+ self.network.allocate_fixed_ip,
+ self.context,
+ 'foo')
+
+ for i in range(num_available_ips):
+ self.network.deallocate_fixed_ip(self.context, addresses[i])
+ release_ip(addresses[i])
+ db.instance_destroy(context.get_admin_context(), instance_ids[i])
+ ip_count = db.network_count_available_ips(context.get_admin_context(),
+ network['id'])
+ self.assertEqual(ip_count, num_available_ips)
+
+ def _is_allocated_in_project(self, address, project_id):
+ """Returns true if address is in specified project"""
+ project_net = db.project_get_network(context.get_admin_context(),
+ project_id)
+ network = db.fixed_ip_get_network(context.get_admin_context(),
+ address)
+ instance = db.fixed_ip_get_instance(context.get_admin_context(),
+ address)
+ # instance exists until release
+ return instance is not None and network['id'] == project_net['id']
+
+ def run(self, result=None):
+ if(FLAGS.network_manager == 'nova.network.manager.VlanManager'):
+ super(VlanNetworkTestCase, self).run(result)
diff --git a/nova/tests/test_volume.py b/nova/tests/test_volume.py
index 1b1d72092..5d68ca2ae 100644
--- a/nova/tests/test_volume.py
+++ b/nova/tests/test_volume.py
@@ -336,8 +336,8 @@ class ISCSITestCase(DriverTestCase):
self.mox.StubOutWithMock(self.volume.driver, '_execute')
for i in volume_id_list:
tid = db.volume_get_iscsi_target_num(self.context, i)
- self.volume.driver._execute("sudo ietadm --op show --tid=%(tid)d"
- % locals())
+ self.volume.driver._execute("sudo", "ietadm", "--op", "show",
+ "--tid=%(tid)d" % locals())
self.stream.truncate(0)
self.mox.ReplayAll()
@@ -355,8 +355,9 @@ class ISCSITestCase(DriverTestCase):
# the first vblade process isn't running
tid = db.volume_get_iscsi_target_num(self.context, volume_id_list[0])
self.mox.StubOutWithMock(self.volume.driver, '_execute')
- self.volume.driver._execute("sudo ietadm --op show --tid=%(tid)d"
- % locals()).AndRaise(exception.ProcessExecutionError())
+ self.volume.driver._execute("sudo", "ietadm", "--op", "show",
+ "--tid=%(tid)d" % locals()
+ ).AndRaise(exception.ProcessExecutionError())
self.mox.ReplayAll()
self.assertRaises(exception.ProcessExecutionError,
diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py
index 8b0affd5c..66a973a78 100644
--- a/nova/tests/test_xenapi.py
+++ b/nova/tests/test_xenapi.py
@@ -361,6 +361,14 @@ class XenAPIVMTestCase(test.TestCase):
glance_stubs.FakeGlance.IMAGE_RAMDISK)
self.check_vm_params_for_linux_with_external_kernel()
+ def test_spawn_with_network_qos(self):
+ self._create_instance()
+ for vif_ref in xenapi_fake.get_all('VIF'):
+ vif_rec = xenapi_fake.get_record('VIF', vif_ref)
+ self.assertEquals(vif_rec['qos_algorithm_type'], 'ratelimit')
+ self.assertEquals(vif_rec['qos_algorithm_params']['kbps'],
+ str(4 * 1024))
+
def tearDown(self):
super(XenAPIVMTestCase, self).tearDown()
self.manager.delete_project(self.project)
diff --git a/nova/tests/test_zones.py b/nova/tests/test_zones.py
index 5a52a0506..688dc704d 100644
--- a/nova/tests/test_zones.py
+++ b/nova/tests/test_zones.py
@@ -76,6 +76,40 @@ class ZoneManagerTestCase(test.TestCase):
self.assertEquals(len(zm.zone_states), 1)
self.assertEquals(zm.zone_states[1].username, 'user1')
+ def test_service_capabilities(self):
+ zm = zone_manager.ZoneManager()
+ caps = zm.get_zone_capabilities(self, None)
+ self.assertEquals(caps, {})
+
+ zm.update_service_capabilities("svc1", "host1", dict(a=1, b=2))
+ caps = zm.get_zone_capabilities(self, None)
+ self.assertEquals(caps, dict(svc1_a=(1, 1), svc1_b=(2, 2)))
+
+ zm.update_service_capabilities("svc1", "host1", dict(a=2, b=3))
+ caps = zm.get_zone_capabilities(self, None)
+ self.assertEquals(caps, dict(svc1_a=(2, 2), svc1_b=(3, 3)))
+
+ zm.update_service_capabilities("svc1", "host2", dict(a=20, b=30))
+ caps = zm.get_zone_capabilities(self, None)
+ self.assertEquals(caps, dict(svc1_a=(2, 20), svc1_b=(3, 30)))
+
+ zm.update_service_capabilities("svc10", "host1", dict(a=99, b=99))
+ caps = zm.get_zone_capabilities(self, None)
+ self.assertEquals(caps, dict(svc1_a=(2, 20), svc1_b=(3, 30),
+ svc10_a=(99, 99), svc10_b=(99, 99)))
+
+ zm.update_service_capabilities("svc1", "host3", dict(c=5))
+ caps = zm.get_zone_capabilities(self, None)
+ self.assertEquals(caps, dict(svc1_a=(2, 20), svc1_b=(3, 30),
+ svc1_c=(5, 5), svc10_a=(99, 99),
+ svc10_b=(99, 99)))
+
+ caps = zm.get_zone_capabilities(self, 'svc1')
+ self.assertEquals(caps, dict(svc1_a=(2, 20), svc1_b=(3, 30),
+ svc1_c=(5, 5)))
+ caps = zm.get_zone_capabilities(self, 'svc10')
+ self.assertEquals(caps, dict(svc10_a=(99, 99), svc10_b=(99, 99)))
+
def test_refresh_from_db_replace_existing(self):
zm = zone_manager.ZoneManager()
zone_state = zone_manager.ZoneState()
diff --git a/nova/tests/xenapi/stubs.py b/nova/tests/xenapi/stubs.py
index 70d46a1fb..7f9706a3d 100644
--- a/nova/tests/xenapi/stubs.py
+++ b/nova/tests/xenapi/stubs.py
@@ -228,6 +228,9 @@ class FakeSessionForMigrationTests(fake.SessionBase):
def VDI_get_by_uuid(*args):
return 'hurr'
+ def VDI_resize_online(*args):
+ pass
+
def VM_start(self, _1, ref, _2, _3):
vm = fake.get_record('VM', ref)
if vm['power_state'] != 'Halted':
@@ -240,7 +243,7 @@ class FakeSessionForMigrationTests(fake.SessionBase):
def stub_out_migration_methods(stubs):
def fake_get_snapshot(self, instance):
- return 'foo', 'bar'
+ return 'vm_ref', dict(image='foo', snap='bar')
@classmethod
def fake_get_vdi(cls, session, vm_ref):
@@ -249,7 +252,7 @@ def stub_out_migration_methods(stubs):
vdi_rec = session.get_xenapi().VDI.get_record(vdi_ref)
return vdi_ref, {'uuid': vdi_rec['uuid'], }
- def fake_shutdown(self, inst, vm, method='clean'):
+ def fake_shutdown(self, inst, vm, hard=True):
pass
@classmethod
diff --git a/nova/utils.py b/nova/utils.py
index 24b8da9ea..e4d8a70eb 100644
--- a/nova/utils.py
+++ b/nova/utils.py
@@ -41,6 +41,7 @@ from xml.sax import saxutils
from eventlet import event
from eventlet import greenthread
+from eventlet import semaphore
from eventlet.green import subprocess
None
from nova import exception
@@ -133,13 +134,14 @@ def fetchfile(url, target):
def execute(*cmd, **kwargs):
- process_input = kwargs.get('process_input', None)
- addl_env = kwargs.get('addl_env', None)
- check_exit_code = kwargs.get('check_exit_code', 0)
- stdin = kwargs.get('stdin', subprocess.PIPE)
- stdout = kwargs.get('stdout', subprocess.PIPE)
- stderr = kwargs.get('stderr', subprocess.PIPE)
- attempts = kwargs.get('attempts', 1)
+ process_input = kwargs.pop('process_input', None)
+ addl_env = kwargs.pop('addl_env', None)
+ check_exit_code = kwargs.pop('check_exit_code', 0)
+ delay_on_retry = kwargs.pop('delay_on_retry', True)
+ attempts = kwargs.pop('attempts', 1)
+ if len(kwargs):
+ raise exception.Error(_('Got unknown keyword args '
+ 'to utils.execute: %r') % kwargs)
cmd = map(str, cmd)
while attempts > 0:
@@ -149,8 +151,11 @@ def execute(*cmd, **kwargs):
env = os.environ.copy()
if addl_env:
env.update(addl_env)
- obj = subprocess.Popen(cmd, stdin=stdin,
- stdout=stdout, stderr=stderr, env=env)
+ obj = subprocess.Popen(cmd,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=env)
result = None
if process_input != None:
result = obj.communicate(process_input)
@@ -176,7 +181,8 @@ def execute(*cmd, **kwargs):
raise
else:
LOG.debug(_("%r failed. Retrying."), cmd)
- greenthread.sleep(random.randint(20, 200) / 100.0)
+ if delay_on_retry:
+ greenthread.sleep(random.randint(20, 200) / 100.0)
def ssh_execute(ssh, cmd, process_input=None,
@@ -329,6 +335,14 @@ def utcnow():
utcnow.override_time = None
+def is_older_than(before, seconds):
+ """Return True if before is older than 'seconds'"""
+ if utcnow() - before > datetime.timedelta(seconds=seconds):
+ return True
+ else:
+ return False
+
+
def utcnow_ts():
"""Timestamp version of our utcnow function."""
return time.mktime(utcnow().timetuple())
@@ -526,17 +540,76 @@ def loads(s):
return json.loads(s)
-def synchronized(name):
+_semaphores = {}
+
+
+class _NoopContextManager(object):
+ def __enter__(self):
+ pass
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ pass
+
+
+def synchronized(name, external=False):
+ """Synchronization decorator
+
+ Decorating a method like so:
+ @synchronized('mylock')
+ def foo(self, *args):
+ ...
+
+ ensures that only one thread will execute the bar method at a time.
+
+ Different methods can share the same lock:
+ @synchronized('mylock')
+ def foo(self, *args):
+ ...
+
+ @synchronized('mylock')
+ def bar(self, *args):
+ ...
+
+ This way only one of either foo or bar can be executing at a time.
+
+ The external keyword argument denotes whether this lock should work across
+ multiple processes. This means that if two different workers both run a
+ a method decorated with @synchronized('mylock', external=True), only one
+ of them will execute at a time.
+ """
+
def wrap(f):
@functools.wraps(f)
def inner(*args, **kwargs):
- LOG.debug(_("Attempting to grab %(lock)s for method "
- "%(method)s..." % {"lock": name,
+ # NOTE(soren): If we ever go natively threaded, this will be racy.
+ # See http://stackoverflow.com/questions/5390569/dyn\
+ # amically-allocating-and-destroying-mutexes
+ if name not in _semaphores:
+ _semaphores[name] = semaphore.Semaphore()
+ sem = _semaphores[name]
+ LOG.debug(_('Attempting to grab semaphore "%(lock)s" for method '
+ '"%(method)s"...' % {"lock": name,
"method": f.__name__}))
- lock = lockfile.FileLock(os.path.join(FLAGS.lock_path,
- 'nova-%s.lock' % name))
- with lock:
- return f(*args, **kwargs)
+ with sem:
+ if external:
+ LOG.debug(_('Attempting to grab file lock "%(lock)s" for '
+ 'method "%(method)s"...' %
+ {"lock": name, "method": f.__name__}))
+ lock_file_path = os.path.join(FLAGS.lock_path,
+ 'nova-%s.lock' % name)
+ lock = lockfile.FileLock(lock_file_path)
+ else:
+ lock = _NoopContextManager()
+
+ with lock:
+ retval = f(*args, **kwargs)
+
+ # If no-one else is waiting for it, delete it.
+ # See note about possible raciness above.
+ if not sem.balance < 1:
+ del _semaphores[name]
+
+ return retval
return inner
return wrap
@@ -588,3 +661,12 @@ def get_from_path(items, path):
return results
else:
return get_from_path(results, remainder)
+
+
+def check_isinstance(obj, cls):
+ """Checks that obj is of type cls, and lets PyLint infer types"""
+ if isinstance(obj, cls):
+ return obj
+ raise Exception(_("Expected object of type: %s") % (str(cls)))
+ # TODO(justinsb): Can we make this better??
+ return cls() # Ugly PyLint hack
diff --git a/nova/virt/connection.py b/nova/virt/connection.py
index 13181b730..af7001715 100644
--- a/nova/virt/connection.py
+++ b/nova/virt/connection.py
@@ -23,6 +23,8 @@ import sys
from nova import flags
from nova import log as logging
+from nova import utils
+from nova.virt import driver
from nova.virt import fake
from nova.virt import libvirt_conn
from nova.virt import xenapi_conn
@@ -72,4 +74,4 @@ def get_connection(read_only=False):
if conn is None:
LOG.error(_('Failed to open connection to the hypervisor'))
sys.exit(1)
- return conn
+ return utils.check_isinstance(conn, driver.ComputeDriver)
diff --git a/nova/virt/driver.py b/nova/virt/driver.py
new file mode 100644
index 000000000..0e3a4aa3b
--- /dev/null
+++ b/nova/virt/driver.py
@@ -0,0 +1,234 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 Justin Santa Barbara
+# 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.
+
+"""
+Driver base-classes:
+
+ (Beginning of) the contract that compute drivers must follow, and shared
+ types that support that contract
+"""
+
+from nova.compute import power_state
+
+
+class InstanceInfo(object):
+ def __init__(self, name, state):
+ self.name = name
+ assert state in power_state.valid_states(), "Bad state: %s" % state
+ self.state = state
+
+
+class ComputeDriver(object):
+ """Base class for compute drivers.
+
+ Lots of documentation is currently on fake.py.
+ """
+
+ def init_host(self, host):
+ """Adopt existing VM's running here"""
+ raise NotImplementedError()
+
+ def get_info(self, instance_name):
+ """Get the current status of an instance, by name (not ID!)
+
+ Returns a dict containing:
+ :state: the running state, one of the power_state codes
+ :max_mem: (int) the maximum memory in KBytes allowed
+ :mem: (int) the memory in KBytes used by the domain
+ :num_cpu: (int) the number of virtual CPUs for the domain
+ :cpu_time: (int) the CPU time used in nanoseconds
+ """
+ raise NotImplementedError()
+
+ def list_instances(self):
+ raise NotImplementedError()
+
+ def list_instances_detail(self):
+ """Return a list of InstanceInfo for all registered VMs"""
+ raise NotImplementedError()
+
+ def spawn(self, instance):
+ """Launch a VM for the specified instance"""
+ raise NotImplementedError()
+
+ def destroy(self, instance, cleanup=True):
+ """Shutdown specified VM"""
+ raise NotImplementedError()
+
+ def reboot(self, instance):
+ """Reboot specified VM"""
+ raise NotImplementedError()
+
+ def snapshot_instance(self, context, instance_id, image_id):
+ raise NotImplementedError()
+
+ def get_console_pool_info(self, console_type):
+ """???
+
+ Returns a dict containing:
+ :address: ???
+ :username: ???
+ :password: ???
+ """
+ raise NotImplementedError()
+
+ def get_console_output(self, instance):
+ raise NotImplementedError()
+
+ def get_ajax_console(self, instance):
+ raise NotImplementedError()
+
+ def get_diagnostics(self, instance):
+ """Return data about VM diagnostics"""
+ raise NotImplementedError()
+
+ def get_host_ip_addr(self):
+ raise NotImplementedError()
+
+ def attach_volume(self, context, instance_id, volume_id, mountpoint):
+ raise NotImplementedError()
+
+ def detach_volume(self, context, instance_id, volume_id):
+ raise NotImplementedError()
+
+ def compare_cpu(self, context, cpu_info):
+ raise NotImplementedError()
+
+ def migrate_disk_and_power_off(self, instance, dest):
+ """Transfers the VHD of a running instance to another host, then shuts
+ off the instance copies over the COW disk"""
+ raise NotImplementedError()
+
+ def snapshot(self, instance, image_id):
+ """ Create snapshot from a running VM instance """
+ raise NotImplementedError()
+
+ def finish_resize(self, instance, disk_info):
+ """Completes a resize, turning on the migrated instance"""
+ raise NotImplementedError()
+
+ def revert_resize(self, instance):
+ """Reverts a resize, powering back on the instance"""
+ raise NotImplementedError()
+
+ def pause(self, instance, callback):
+ """Pause VM instance"""
+ raise NotImplementedError()
+
+ def unpause(self, instance, callback):
+ """Unpause paused VM instance"""
+ raise NotImplementedError()
+
+ def suspend(self, instance, callback):
+ """suspend the specified instance"""
+ raise NotImplementedError()
+
+ def resume(self, instance, callback):
+ """resume the specified instance"""
+ raise NotImplementedError()
+
+ def rescue(self, instance, callback):
+ """Rescue the specified instance"""
+ raise NotImplementedError()
+
+ def unrescue(self, instance, callback):
+ """Unrescue the specified instance"""
+ raise NotImplementedError()
+
+ def update_available_resource(self, ctxt, host):
+ """Updates compute manager resource info on ComputeNode table.
+
+ This method is called when nova-compute launches, and
+ whenever admin executes "nova-manage service update_resource".
+
+ :param ctxt: security context
+ :param host: hostname that compute manager is currently running
+
+ """
+ raise NotImplementedError()
+
+ def live_migration(self, ctxt, instance_ref, dest,
+ post_method, recover_method):
+ """Spawning live_migration operation for distributing high-load.
+
+ :params ctxt: security context
+ :params instance_ref:
+ nova.db.sqlalchemy.models.Instance object
+ instance object that is migrated.
+ :params dest: destination host
+ :params post_method:
+ post operation method.
+ expected nova.compute.manager.post_live_migration.
+ :params recover_method:
+ recovery method when any exception occurs.
+ expected nova.compute.manager.recover_live_migration.
+
+ """
+ raise NotImplementedError()
+
+ def refresh_security_group_rules(self, security_group_id):
+ raise NotImplementedError()
+
+ def refresh_security_group_members(self, security_group_id):
+ raise NotImplementedError()
+
+ def reset_network(self, instance):
+ """reset networking for specified instance"""
+ raise NotImplementedError()
+
+ def ensure_filtering_rules_for_instance(self, instance_ref):
+ """Setting up filtering rules and waiting for its completion.
+
+ To migrate an instance, filtering rules to hypervisors
+ and firewalls are inevitable on destination host.
+ ( Waiting only for filtering rules to hypervisor,
+ since filtering rules to firewall rules can be set faster).
+
+ Concretely, the below method must be called.
+ - setup_basic_filtering (for nova-basic, etc.)
+ - prepare_instance_filter(for nova-instance-instance-xxx, etc.)
+
+ to_xml may have to be called since it defines PROJNET, PROJMASK.
+ but libvirt migrates those value through migrateToURI(),
+ so , no need to be called.
+
+ Don't use thread for this method since migration should
+ not be started when setting-up filtering rules operations
+ are not completed.
+
+ :params instance_ref: nova.db.sqlalchemy.models.Instance object
+
+ """
+ raise NotImplementedError()
+
+ def unfilter_instance(self, instance):
+ """Stop filtering instance"""
+ raise NotImplementedError()
+
+ def set_admin_password(self, context, instance_id, new_pass=None):
+ """Set the root/admin password for an instance on this server."""
+ raise NotImplementedError()
+
+ def inject_file(self, instance, b64_path, b64_contents):
+ """Create a file on the VM instance. The file path and contents
+ should be base64-encoded.
+ """
+ raise NotImplementedError()
+
+ def inject_network_info(self, instance):
+ """inject network info for specified instance"""
+ raise NotImplementedError()
diff --git a/nova/virt/fake.py b/nova/virt/fake.py
index 3a06284a1..5b0fe1877 100644
--- a/nova/virt/fake.py
+++ b/nova/virt/fake.py
@@ -26,7 +26,9 @@ semantics of real hypervisor connections.
"""
from nova import exception
+from nova import utils
from nova.compute import power_state
+from nova.virt import driver
def get_connection(_):
@@ -34,7 +36,14 @@ def get_connection(_):
return FakeConnection.instance()
-class FakeConnection(object):
+class FakeInstance(object):
+
+ def __init__(self, name, state):
+ self.name = name
+ self.state = state
+
+
+class FakeConnection(driver.ComputeDriver):
"""
The interface to this class talks in terms of 'instances' (Amazon EC2 and
internal Nova terminology), by which we mean 'running virtual machine'
@@ -90,6 +99,17 @@ class FakeConnection(object):
"""
return self.instances.keys()
+ def _map_to_instance_info(self, instance):
+ instance = utils.check_isinstance(instance, FakeInstance)
+ info = driver.InstanceInfo(instance.name, instance.state)
+ return info
+
+ def list_instances_detail(self):
+ info_list = []
+ for instance in self.instances.values():
+ info_list.append(self._map_to_instance_info(instance))
+ return info_list
+
def spawn(self, instance):
"""
Create a new instance/VM/domain on the virtualization platform.
@@ -109,9 +129,10 @@ class FakeConnection(object):
that it was before this call began.
"""
- fake_instance = FakeInstance()
- self.instances[instance.name] = fake_instance
- fake_instance._state = power_state.RUNNING
+ name = instance.name
+ state = power_state.RUNNING
+ fake_instance = FakeInstance(name, state)
+ self.instances[name] = fake_instance
def snapshot(self, instance, name):
"""
@@ -270,7 +291,7 @@ class FakeConnection(object):
raise exception.NotFound(_("Instance %s Not Found")
% instance_name)
i = self.instances[instance_name]
- return {'state': i._state,
+ return {'state': i.state,
'max_mem': 0,
'mem': 0,
'num_cpu': 2,
@@ -428,8 +449,6 @@ class FakeConnection(object):
"""This method is supported only by libvirt."""
raise NotImplementedError('This method is supported only by libvirt.')
-
-class FakeInstance(object):
-
- def __init__(self):
- self._state = power_state.NOSTATE
+ def test_remove_vm(self, instance_name):
+ """ Removes the named VM, as if it crashed. For testing"""
+ self.instances.pop(instance_name)
diff --git a/nova/virt/hyperv.py b/nova/virt/hyperv.py
index 29d18dac5..a1ed5ebbf 100644
--- a/nova/virt/hyperv.py
+++ b/nova/virt/hyperv.py
@@ -68,6 +68,7 @@ from nova import flags
from nova import log as logging
from nova.auth import manager
from nova.compute import power_state
+from nova.virt import driver
from nova.virt import images
wmi = None
@@ -108,8 +109,9 @@ def get_connection(_):
return HyperVConnection()
-class HyperVConnection(object):
+class HyperVConnection(driver.ComputeDriver):
def __init__(self):
+ super(HyperVConnection, self).__init__()
self._conn = wmi.WMI(moniker='//./root/virtualization')
self._cim_conn = wmi.WMI(moniker='//./root/cimv2')
@@ -124,6 +126,19 @@ class HyperVConnection(object):
for v in self._conn.Msvm_ComputerSystem(['ElementName'])]
return vms
+ def list_instances_detail(self):
+ # TODO(justinsb): This is a terrible implementation (1+N)
+ instance_infos = []
+ for instance_name in self.list_instances():
+ info = self.get_info(instance_name)
+
+ state = info['state']
+
+ instance_info = driver.InstanceInfo(instance_name, state)
+ instance_infos.append(instance_info)
+
+ return instance_infos
+
def spawn(self, instance):
""" Create a new VM and start it."""
vm = self._lookup(instance.name)
@@ -345,7 +360,7 @@ class HyperVConnection(object):
newinst = cl.new()
#Copy the properties from the original.
for prop in wmi_obj._properties:
- newinst.Properties_.Item(prop).Value =\
+ newinst.Properties_.Item(prop).Value = \
wmi_obj.Properties_.Item(prop).Value
return newinst
@@ -467,3 +482,6 @@ class HyperVConnection(object):
if vm is None:
raise exception.NotFound('Cannot detach volume from missing %s '
% instance_name)
+
+ def poll_rescued_instances(self, timeout):
+ pass
diff --git a/nova/virt/interfaces.template b/nova/virt/interfaces.template
index 87b92b84a..3b34e54f4 100644
--- a/nova/virt/interfaces.template
+++ b/nova/virt/interfaces.template
@@ -8,10 +8,16 @@ iface lo inet loopback
# The primary network interface
auto eth0
iface eth0 inet static
- address %(address)s
- netmask %(netmask)s
- broadcast %(broadcast)s
- gateway %(gateway)s
- dns-nameservers %(dns)s
+ address ${address}
+ netmask ${netmask}
+ broadcast ${broadcast}
+ gateway ${gateway}
+ dns-nameservers ${dns}
+#if $use_ipv6
+iface eth0 inet6 static
+ address ${address_v6}
+ netmask ${netmask_v6}
+ gateway ${gateway_v6}
+#end if
diff --git a/nova/virt/libvirt.xml.template b/nova/virt/libvirt.xml.template
index 88bfbc668..ef2d2cd6b 100644
--- a/nova/virt/libvirt.xml.template
+++ b/nova/virt/libvirt.xml.template
@@ -79,8 +79,8 @@
#if $getVar('extra_params', False)
${extra_params}
#end if
-#if $getVar('ra_server', False)
- <parameter name="RASERVER" value="${ra_server}" />
+#if $getVar('gateway_v6', False)
+ <parameter name="RASERVER" value="${gateway_v6}" />
#end if
</filterref>
</interface>
diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py
index 2559c2b81..2bb96f819 100644
--- a/nova/virt/libvirt_conn.py
+++ b/nova/virt/libvirt_conn.py
@@ -42,13 +42,11 @@ import shutil
import sys
import random
import subprocess
-import time
import uuid
from xml.dom import minidom
-
+from eventlet import greenthread
from eventlet import tpool
-from eventlet import semaphore
import IPy
@@ -63,6 +61,7 @@ from nova.auth import manager
from nova.compute import instance_types
from nova.compute import power_state
from nova.virt import disk
+from nova.virt import driver
from nova.virt import images
libvirt = None
@@ -134,8 +133,8 @@ def get_connection(read_only):
def _late_load_cheetah():
global Template
if Template is None:
- t = __import__('Cheetah.Template', globals(), locals(), ['Template'],
- -1)
+ t = __import__('Cheetah.Template', globals(), locals(),
+ ['Template'], -1)
Template = t.Template
@@ -154,12 +153,14 @@ def _get_ip_version(cidr):
return int(net.version())
-class LibvirtConnection(object):
+class LibvirtConnection(driver.ComputeDriver):
def __init__(self, read_only):
+ super(LibvirtConnection, self).__init__()
self.libvirt_uri = self.get_uri()
self.libvirt_xml = open(FLAGS.libvirt_xml_template).read()
+ self.interfaces_xml = open(FLAGS.injected_network_template).read()
self.cpuinfo_xml = open(FLAGS.cpuinfo_xml_template).read()
self._wrapped_conn = None
self.read_only = read_only
@@ -235,6 +236,29 @@ class LibvirtConnection(object):
return [self._conn.lookupByID(x).name()
for x in self._conn.listDomainsID()]
+ def _map_to_instance_info(self, domain):
+ """Gets info from a virsh domain object into an InstanceInfo"""
+
+ # domain.info() returns a list of:
+ # state: one of the state values (virDomainState)
+ # maxMemory: the maximum memory used by the domain
+ # memory: the current amount of memory used by the domain
+ # nbVirtCPU: the number of virtual CPU
+ # puTime: the time used by the domain in nanoseconds
+
+ (state, _max_mem, _mem, _num_cpu, _cpu_time) = domain.info()
+ name = domain.name()
+
+ return driver.InstanceInfo(name, state)
+
+ def list_instances_detail(self):
+ infos = []
+ for domain_id in self._conn.listDomainsID():
+ domain = self._conn.lookupByID(domain_id)
+ info = self._map_to_instance_info(domain)
+ infos.append(info)
+ return infos
+
def destroy(self, instance, cleanup=True):
try:
virt_dom = self._conn.lookupByName(instance['name'])
@@ -339,7 +363,11 @@ class LibvirtConnection(object):
def reboot(self, instance):
self.destroy(instance, False)
xml = self.to_xml(instance)
+ self.firewall_driver.setup_basic_filtering(instance)
+ self.firewall_driver.prepare_instance_filter(instance)
self._conn.createXML(xml, 0)
+ self.firewall_driver.apply_instance_filter(instance)
+
timer = utils.LoopingCall(f=None)
def _wait_for_reboot():
@@ -413,6 +441,10 @@ class LibvirtConnection(object):
self.reboot(instance)
@exception.wrap_exception
+ def poll_rescued_instances(self, timeout):
+ pass
+
+ @exception.wrap_exception
def spawn(self, instance):
xml = self.to_xml(instance)
db.instance_set_state(context.get_admin_context(),
@@ -552,13 +584,12 @@ class LibvirtConnection(object):
os.mkdir(base_dir)
base = os.path.join(base_dir, fname)
- if fname not in LibvirtConnection._image_sems:
- LibvirtConnection._image_sems[fname] = semaphore.Semaphore()
- with LibvirtConnection._image_sems[fname]:
+ @utils.synchronized(fname)
+ def call_if_not_exists(base, fn, *args, **kwargs):
if not os.path.exists(base):
fn(target=base, *args, **kwargs)
- if not LibvirtConnection._image_sems[fname].locked():
- del LibvirtConnection._image_sems[fname]
+
+ call_if_not_exists(base, fn, *args, **kwargs)
if cow:
utils.execute('qemu-img', 'create', '-f', 'qcow2', '-o',
@@ -659,16 +690,23 @@ class LibvirtConnection(object):
if network_ref['injected']:
admin_context = context.get_admin_context()
address = db.instance_get_fixed_address(admin_context, inst['id'])
- ra_server = network_ref['ra_server']
- if not ra_server:
- ra_server = "fd00::"
- with open(FLAGS.injected_network_template) as f:
- net = f.read() % {'address': address,
- 'netmask': network_ref['netmask'],
- 'gateway': network_ref['gateway'],
- 'broadcast': network_ref['broadcast'],
- 'dns': network_ref['dns'],
- 'ra_server': ra_server}
+ address_v6 = None
+ if FLAGS.use_ipv6:
+ address_v6 = db.instance_get_fixed_address_v6(admin_context,
+ inst['id'])
+
+ interfaces_info = {'address': address,
+ 'netmask': network_ref['netmask'],
+ 'gateway': network_ref['gateway'],
+ 'broadcast': network_ref['broadcast'],
+ 'dns': network_ref['dns'],
+ 'address_v6': address_v6,
+ 'gateway_v6': network_ref['gateway_v6'],
+ 'netmask_v6': network_ref['netmask_v6'],
+ 'use_ipv6': FLAGS.use_ipv6}
+
+ net = str(Template(self.interfaces_xml,
+ searchList=[interfaces_info]))
if key or net:
inst_name = inst['name']
img_id = inst.image_id
@@ -703,7 +741,7 @@ class LibvirtConnection(object):
instance['id'])
# Assume that the gateway also acts as the dhcp server.
dhcp_server = network['gateway']
- ra_server = network['ra_server']
+ gateway_v6 = network['gateway_v6']
if FLAGS.allow_project_net_traffic:
if FLAGS.use_ipv6:
@@ -748,8 +786,8 @@ class LibvirtConnection(object):
'local': instance_type['local_gb'],
'driver_type': driver_type}
- if ra_server:
- xml_info['ra_server'] = ra_server + "/128"
+ if gateway_v6:
+ xml_info['gateway_v6'] = gateway_v6 + "/128"
if not rescue:
if instance['kernel_id']:
xml_info['kernel'] = xml_info['basepath'] + "/kernel"
@@ -970,7 +1008,18 @@ class LibvirtConnection(object):
"""
- return self._conn.getVersion()
+ # NOTE(justinsb): getVersion moved between libvirt versions
+ # Trying to do be compatible with older versions is a lost cause
+ # But ... we can at least give the user a nice message
+ method = getattr(self._conn, 'getVersion', None)
+ if method is None:
+ raise exception.Error(_("libvirt version is too old"
+ " (does not support getVersion)"))
+ # NOTE(justinsb): If we wanted to get the version, we could:
+ # method = getattr(libvirt, 'getVersion', None)
+ # NOTE(justinsb): This would then rely on a proper version check
+
+ return method()
def get_cpu_info(self):
"""Get cpuinfo information.
@@ -991,24 +1040,35 @@ class LibvirtConnection(object):
+ xml.serialize())
cpu_info = dict()
- cpu_info['arch'] = xml.xpathEval('//host/cpu/arch')[0].getContent()
- cpu_info['model'] = xml.xpathEval('//host/cpu/model')[0].getContent()
- cpu_info['vendor'] = xml.xpathEval('//host/cpu/vendor')[0].getContent()
- topology_node = xml.xpathEval('//host/cpu/topology')[0]\
- .get_properties()
+ arch_nodes = xml.xpathEval('//host/cpu/arch')
+ if arch_nodes:
+ cpu_info['arch'] = arch_nodes[0].getContent()
+
+ model_nodes = xml.xpathEval('//host/cpu/model')
+ if model_nodes:
+ cpu_info['model'] = model_nodes[0].getContent()
+
+ vendor_nodes = xml.xpathEval('//host/cpu/vendor')
+ if vendor_nodes:
+ cpu_info['vendor'] = vendor_nodes[0].getContent()
+
+ topology_nodes = xml.xpathEval('//host/cpu/topology')
topology = dict()
- while topology_node != None:
- name = topology_node.get_name()
- topology[name] = topology_node.getContent()
- topology_node = topology_node.get_next()
-
- keys = ['cores', 'sockets', 'threads']
- tkeys = topology.keys()
- if list(set(tkeys)) != list(set(keys)):
- ks = ', '.join(keys)
- raise exception.Invalid(_("Invalid xml: topology(%(topology)s) "
- "must have %(ks)s") % locals())
+ if topology_nodes:
+ topology_node = topology_nodes[0].get_properties()
+ while topology_node:
+ name = topology_node.get_name()
+ topology[name] = topology_node.getContent()
+ topology_node = topology_node.get_next()
+
+ keys = ['cores', 'sockets', 'threads']
+ tkeys = topology.keys()
+ if set(tkeys) != set(keys):
+ ks = ', '.join(keys)
+ raise exception.Invalid(_("Invalid xml: topology"
+ "(%(topology)s) must have "
+ "%(ks)s") % locals())
feature_nodes = xml.xpathEval('//host/cpu/feature')
features = list()
@@ -1122,7 +1182,8 @@ class LibvirtConnection(object):
return
- def ensure_filtering_rules_for_instance(self, instance_ref):
+ def ensure_filtering_rules_for_instance(self, instance_ref,
+ time=None):
"""Setting up filtering rules and waiting for its completion.
To migrate an instance, filtering rules to hypervisors
@@ -1146,6 +1207,9 @@ class LibvirtConnection(object):
"""
+ if not time:
+ time = greenthread
+
# If any instances never launch at destination host,
# basic-filtering must be set here.
self.firewall_driver.setup_basic_filtering(instance_ref)
@@ -1295,10 +1359,10 @@ class FirewallDriver(object):
"""
raise NotImplementedError()
- def _ra_server_for_instance(self, instance):
+ def _gateway_v6_for_instance(self, instance):
network = db.network_get_by_instance(context.get_admin_context(),
instance['id'])
- return network['ra_server']
+ return network['gateway_v6']
class NWFilterFirewall(FirewallDriver):
@@ -1514,8 +1578,8 @@ class NWFilterFirewall(FirewallDriver):
'nova-base-ipv6',
'nova-allow-dhcp-server']
if FLAGS.use_ipv6:
- ra_server = self._ra_server_for_instance(instance)
- if ra_server:
+ gateway_v6 = self._gateway_v6_for_instance(instance)
+ if gateway_v6:
instance_secgroup_filter_children += ['nova-allow-ra-server']
ctxt = context.get_admin_context()
@@ -1683,9 +1747,9 @@ class IptablesFirewallDriver(FirewallDriver):
# they're not worth the clutter.
if FLAGS.use_ipv6:
# Allow RA responses
- ra_server = self._ra_server_for_instance(instance)
- if ra_server:
- ipv6_rules += ['-s %s/128 -p icmpv6 -j ACCEPT' % (ra_server,)]
+ gateway_v6 = self._gateway_v6_for_instance(instance)
+ if gateway_v6:
+ ipv6_rules += ['-s %s/128 -p icmpv6 -j ACCEPT' % (gateway_v6,)]
#Allow project network traffic
if FLAGS.allow_project_net_traffic:
@@ -1758,15 +1822,15 @@ class IptablesFirewallDriver(FirewallDriver):
pass
def refresh_security_group_rules(self, security_group):
- # We use the semaphore to make sure noone applies the rule set
- # after we've yanked the existing rules but before we've put in
- # the new ones.
- with self.iptables.semaphore:
- for instance in self.instances.values():
- self.remove_filters_for_instance(instance)
- self.add_filters_for_instance(instance)
+ 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,)
@@ -1786,10 +1850,10 @@ class IptablesFirewallDriver(FirewallDriver):
instance['id'])
return network['gateway']
- def _ra_server_for_instance(self, instance):
+ def _gateway_v6_for_instance(self, instance):
network = db.network_get_by_instance(context.get_admin_context(),
instance['id'])
- return network['ra_server']
+ return network['gateway_v6']
def _project_cidr_for_instance(self, instance):
network = db.network_get_by_instance(context.get_admin_context(),
diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py
index 763c5fe40..28ce215d8 100644
--- a/nova/virt/xenapi/vm_utils.py
+++ b/nova/virt/xenapi/vm_utils.py
@@ -233,18 +233,20 @@ class VMHelper(HelperBase):
raise StorageError(_('Unable to destroy VBD %s') % vbd_ref)
@classmethod
- def create_vif(cls, session, vm_ref, network_ref, mac_address, dev="0"):
+ def create_vif(cls, session, vm_ref, network_ref, mac_address,
+ dev, rxtx_cap=0):
"""Create a VIF record. Returns a Deferred that gives the new
VIF reference."""
vif_rec = {}
- vif_rec['device'] = dev
+ vif_rec['device'] = str(dev)
vif_rec['network'] = network_ref
vif_rec['VM'] = vm_ref
vif_rec['MAC'] = mac_address
vif_rec['MTU'] = '1500'
vif_rec['other_config'] = {}
- vif_rec['qos_algorithm_type'] = ''
- vif_rec['qos_algorithm_params'] = {}
+ vif_rec['qos_algorithm_type'] = "ratelimit" if rxtx_cap else ''
+ vif_rec['qos_algorithm_params'] = \
+ {"kbps": str(rxtx_cap * 1024)} if rxtx_cap else {}
LOG.debug(_('Creating VIF for VM %(vm_ref)s,'
' network %(network_ref)s.') % locals())
vif_ref = session.call_xenapi('VIF.create', vif_rec)
diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py
index 488a61e8e..af39a3def 100644
--- a/nova/virt/xenapi/vmops.py
+++ b/nova/virt/xenapi/vmops.py
@@ -36,6 +36,7 @@ from nova import utils
from nova.auth.manager import AuthManager
from nova.compute import power_state
+from nova.virt import driver
from nova.virt.xenapi.network_utils import NetworkHelper
from nova.virt.xenapi.vm_utils import VMHelper
from nova.virt.xenapi.vm_utils import ImageType
@@ -51,11 +52,14 @@ class VMOps(object):
def __init__(self, session):
self.XenAPI = session.get_imported_xenapi()
self._session = session
+ self.poll_rescue_last_ran = None
VMHelper.XenAPI = self.XenAPI
def list_instances(self):
"""List VM instances"""
+ # TODO(justinsb): Should we just always use the details method?
+ # Seems to be the same number of API calls..
vm_refs = []
for vm_ref in self._session.get_xenapi().VM.get_all():
vm_rec = self._session.get_xenapi().VM.get_record(vm_ref)
@@ -63,6 +67,33 @@ class VMOps(object):
vm_refs.append(vm_rec["name_label"])
return vm_refs
+ def list_instances_detail(self):
+ """List VM instances, returning InstanceInfo objects"""
+ instance_infos = []
+ for vm_ref in self._session.get_xenapi().VM.get_all():
+ vm_rec = self._session.get_xenapi().VM.get_record(vm_ref)
+ if not vm_rec["is_a_template"] and not vm_rec["is_control_domain"]:
+ name = vm_rec["name_label"]
+
+ # TODO(justinsb): This a roundabout way to map the state
+ openstack_format = VMHelper.compile_info(vm_rec)
+ state = openstack_format['state']
+
+ instance_info = driver.InstanceInfo(name, state)
+ instance_infos.append(instance_info)
+ return instance_infos
+
+ def revert_resize(self, instance):
+ vm_ref = VMHelper.lookup(self._session, instance.name)
+ self._start(instance, vm_ref)
+
+ 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)
+ self.resize_instance(instance, vdi_uuid)
+ self._spawn(instance, vm_ref)
+
def _start(self, instance, vm_ref=None):
"""Power on a VM instance"""
if not vm_ref:
@@ -73,7 +104,7 @@ 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_disk(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)
@@ -81,11 +112,12 @@ class VMOps(object):
instance.image_id, user, project, disk_image_type)
return vdi_uuid
- def spawn(self, instance):
- vdi_uuid = self.create_disk(instance)
- self._spawn_with_disk(instance, vdi_uuid=vdi_uuid)
+ def spawn(self, instance, network_info=None):
+ vdi_uuid = self._create_disk(instance)
+ vm_ref = self._create_vm(instance, vdi_uuid, network_info)
+ self._spawn(instance, vm_ref)
- def _spawn_with_disk(self, instance, vdi_uuid):
+ def _create_vm(self, instance, vdi_uuid, network_info=None):
"""Create VM instance"""
instance_name = instance.name
vm_ref = VMHelper.lookup(self._session, instance_name)
@@ -128,12 +160,19 @@ class VMOps(object):
VMHelper.create_vbd(session=self._session, vm_ref=vm_ref,
vdi_ref=vdi_ref, userdevice=0, bootable=True)
- # inject_network_info and create vifs
- networks = self.inject_network_info(instance)
- self.create_vifs(instance, networks)
+ # TODO(tr3buchet) - check to make sure we have network info, otherwise
+ # create it now. This goes away once nova-multi-nic hits.
+ if network_info is None:
+ network_info = self._get_network_info(instance)
+ self.create_vifs(vm_ref, network_info)
+ self.inject_network_info(instance, vm_ref, network_info)
+ return vm_ref
+ def _spawn(self, instance, vm_ref):
+ """Spawn a new instance"""
LOG.debug(_('Starting VM %s...'), vm_ref)
self._start(instance, vm_ref)
+ instance_name = instance.name
LOG.info(_('Spawning VM %(instance_name)s created %(vm_ref)s.')
% locals())
@@ -181,7 +220,7 @@ class VMOps(object):
timer.f = _wait_for_boot
# call to reset network to configure network from xenstore
- self.reset_network(instance)
+ self.reset_network(instance, vm_ref)
return timer.start(interval=0.5, now=True)
@@ -306,7 +345,7 @@ class VMOps(object):
try:
# transfer the base copy
template_vm_ref, template_vdi_uuids = self._get_snapshot(instance)
- base_copy_uuid = template_vdi_uuids[1]
+ base_copy_uuid = template_vdi_uuids['image']
vdi_ref, vm_vdi_rec = \
VMHelper.get_vdi_for_vm_safely(self._session, vm_ref)
cow_uuid = vm_vdi_rec['uuid']
@@ -321,7 +360,7 @@ class VMOps(object):
self._session.wait_for_task(task, instance.id)
# Now power down the instance and transfer the COW VHD
- self._shutdown(instance, vm_ref, method='clean')
+ self._shutdown(instance, vm_ref, hard=False)
params = {'host': dest,
'vdi_uuid': cow_uuid,
@@ -341,7 +380,7 @@ class VMOps(object):
# sensible so we don't need to blindly pass around dictionaries
return {'base_copy': base_copy_uuid, 'cow': cow_uuid}
- def attach_disk(self, instance, base_copy_uuid, cow_uuid):
+ def link_disks(self, instance, base_copy_uuid, cow_uuid):
"""Links the base copy VHD to the COW via the XAPI plugin"""
vm_ref = VMHelper.lookup(self._session, instance.name)
new_base_copy_uuid = str(uuid.uuid4())
@@ -362,9 +401,19 @@ class VMOps(object):
return new_cow_uuid
- def resize(self, instance, flavor):
+ def resize_instance(self, instance, vdi_uuid):
"""Resize a running instance by changing it's RAM and disk size """
- raise NotImplementedError()
+ #TODO(mdietz): this will need to be adjusted for swap later
+ #The new disk size must be in bytes
+
+ new_disk_size = str(instance.local_gb * 1024 * 1024 * 1024)
+ instance_name = instance.name
+ instance_local_gb = instance.local_gb
+ LOG.debug(_("Resizing VDI %(vdi_uuid)s for instance %(instance_name)s."
+ " Expanding to %(instance_local_gb)d GB") % locals())
+ vdi_ref = self._session.call_xenapi('VDI.get_by_uuid', vdi_uuid)
+ self._session.call_xenapi('VDI.resize_online', vdi_ref, new_disk_size)
+ LOG.debug(_("Resize instance %s complete") % (instance.name))
def reboot(self, instance):
"""Reboot VM instance"""
@@ -439,8 +488,9 @@ class VMOps(object):
"""Shutdown an instance"""
state = self.get_info(instance['name'])['state']
if state == power_state.SHUTDOWN:
- LOG.warn(_("VM %(vm)s already halted, skipping shutdown...") %
- locals())
+ instance_name = instance.name
+ LOG.warn(_("VM %(instance_name)s already halted,"
+ "skipping shutdown...") % locals())
return
instance_id = instance.id
@@ -458,6 +508,10 @@ class VMOps(object):
except self.XenAPI.Failure, exc:
LOG.exception(exc)
+ def _shutdown_rescue(self, rescue_vm_ref):
+ """Shutdown a rescue instance"""
+ self._session.call_xenapi("Async.VM.hard_shutdown", rescue_vm_ref)
+
def _destroy_vdis(self, instance, vm_ref):
"""Destroys all VDIs associated with a VM"""
instance_id = instance.id
@@ -475,6 +529,24 @@ class VMOps(object):
except self.XenAPI.Failure, exc:
LOG.exception(exc)
+ def _destroy_rescue_vdis(self, rescue_vm_ref):
+ """Destroys all VDIs associated with a rescued VM"""
+ vdi_refs = VMHelper.lookup_vm_vdis(self._session, rescue_vm_ref)
+ for vdi_ref in vdi_refs:
+ try:
+ self._session.call_xenapi("Async.VDI.destroy", vdi_ref)
+ except self.XenAPI.Failure:
+ continue
+
+ def _destroy_rescue_vbds(self, rescue_vm_ref):
+ """Destroys all VBDs tied to a rescue VM"""
+ vbd_refs = self._session.get_xenapi().VM.get_VBDs(rescue_vm_ref)
+ for vbd_ref in vbd_refs:
+ vbd_rec = self._session.get_xenapi().VBD.get_record(vbd_ref)
+ if vbd_rec["userdevice"] == "1": # primary VBD is always 1
+ VMHelper.unplug_vbd(self._session, vbd_ref)
+ VMHelper.destroy_vbd(self._session, vbd_ref)
+
def _destroy_kernel_ramdisk(self, instance, vm_ref):
"""
Three situations can occur:
@@ -525,6 +597,14 @@ class VMOps(object):
LOG.debug(_("Instance %(instance_id)s VM destroyed") % locals())
+ def _destroy_rescue_instance(self, rescue_vm_ref):
+ """Destroy a rescue instance"""
+ self._destroy_rescue_vbds(rescue_vm_ref)
+ self._shutdown_rescue(rescue_vm_ref)
+ self._destroy_rescue_vdis(rescue_vm_ref)
+
+ self._session.call_xenapi("Async.VM.destroy", rescue_vm_ref)
+
def destroy(self, instance):
"""
Destroy VM instance
@@ -628,40 +708,56 @@ class VMOps(object):
"""
rescue_vm_ref = VMHelper.lookup(self._session,
- instance.name + "-rescue")
+ instance.name + "-rescue")
if not rescue_vm_ref:
raise exception.NotFound(_(
"Instance is not in Rescue Mode: %s" % instance.name))
original_vm_ref = self._get_vm_opaque_ref(instance)
- vbd_refs = self._session.get_xenapi().VM.get_VBDs(rescue_vm_ref)
-
instance._rescue = False
- for vbd_ref in vbd_refs:
- _vbd_ref = self._session.get_xenapi().VBD.get_record(vbd_ref)
- if _vbd_ref["userdevice"] == "1":
- VMHelper.unplug_vbd(self._session, vbd_ref)
- VMHelper.destroy_vbd(self._session, vbd_ref)
+ self._destroy_rescue_instance(rescue_vm_ref)
+ self._release_bootlock(original_vm_ref)
+ self._start(instance, original_vm_ref)
- task1 = self._session.call_xenapi("Async.VM.hard_shutdown",
- rescue_vm_ref)
- self._session.wait_for_task(task1, instance.id)
+ def poll_rescued_instances(self, timeout):
+ """Look for expirable rescued instances
+ - forcibly exit rescue mode for any instances that have been
+ in rescue mode for >= the provided timeout
+ """
+ last_ran = self.poll_rescue_last_ran
+ if last_ran:
+ if not utils.is_older_than(last_ran, timeout):
+ # Do not run. Let's bail.
+ return
+ else:
+ # Update the time tracker and proceed.
+ self.poll_rescue_last_ran = utils.utcnow()
+ else:
+ # We need a base time to start tracking.
+ self.poll_rescue_last_ran = utils.utcnow()
+ return
- vdi_refs = VMHelper.lookup_vm_vdis(self._session, rescue_vm_ref)
- for vdi_ref in vdi_refs:
- try:
- task = self._session.call_xenapi('Async.VDI.destroy', vdi_ref)
- self._session.wait_for_task(task, instance.id)
- except self.XenAPI.Failure:
- continue
+ rescue_vms = []
+ for instance in self.list_instances():
+ if instance.endswith("-rescue"):
+ rescue_vms.append(dict(name=instance,
+ vm_ref=VMHelper.lookup(self._session,
+ instance)))
- task2 = self._session.call_xenapi('Async.VM.destroy', rescue_vm_ref)
- self._session.wait_for_task(task2, instance.id)
+ for vm in rescue_vms:
+ rescue_name = vm["name"]
+ rescue_vm_ref = vm["vm_ref"]
- self._release_bootlock(original_vm_ref)
- self._start(instance, original_vm_ref)
+ self._destroy_rescue_instance(rescue_vm_ref)
+
+ original_name = vm["name"].split("-rescue", 1)[0]
+ original_vm_ref = VMHelper.lookup(self._session, original_name)
+
+ self._release_bootlock(original_vm_ref)
+ self._session.call_xenapi("VM.start", original_vm_ref, False,
+ False)
def get_info(self, instance):
"""Return data about VM instance"""
@@ -685,24 +781,17 @@ class VMOps(object):
# TODO: implement this!
return 'http://fakeajaxconsole/fake_url'
- def inject_network_info(self, instance):
- """
- Generate the network info and make calls to place it into the
- xenstore and the xenstore param list
-
- """
- # TODO(tr3buchet) - remove comment in multi-nic
- # I've decided to go ahead and consider multiple IPs and networks
- # at this stage even though they aren't implemented because these will
- # be needed for multi-nic and there was no sense writing it for single
- # network/single IP and then having to turn around and re-write it
- vm_ref = self._get_vm_opaque_ref(instance.id)
- logging.debug(_("injecting network info to xenstore for vm: |%s|"),
- vm_ref)
+ # TODO(tr3buchet) - remove this function after nova multi-nic
+ def _get_network_info(self, instance):
+ """creates network info list for instance"""
admin_context = context.get_admin_context()
- IPs = db.fixed_ip_get_all_by_instance(admin_context, instance['id'])
+ IPs = 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_name(admin_context,
+ instance['instance_type'])
+ network_info = []
for network in networks:
network_IPs = [ip for ip in IPs if ip.network_id == network.id]
@@ -714,67 +803,71 @@ class VMOps(object):
def ip6_dict(ip6):
return {
- "ip": ip6.addressV6,
- "netmask": ip6.netmaskV6,
- "gateway": ip6.gatewayV6,
+ "ip": utils.to_global_ipv6(network['cidr_v6'],
+ instance['mac_address']),
+ "netmask": network['netmask_v6'],
+ "gateway": network['gateway_v6'],
"enabled": "1"}
- mac_id = instance.mac_address.replace(':', '')
- location = 'vm-data/networking/%s' % mac_id
- mapping = {
+ info = {
'label': network['label'],
'gateway': network['gateway'],
'mac': instance.mac_address,
+ 'rxtx_cap': flavor['rxtx_cap'],
'dns': [network['dns']],
- 'ips': [ip_dict(ip) for ip in network_IPs],
- 'ip6s': [ip6_dict(ip) for ip in network_IPs]}
+ 'ips': [ip_dict(ip) for ip in network_IPs]}
+ if network['cidr_v6']:
+ info['ip6s'] = [ip6_dict(ip) for ip in network_IPs]
+ network_info.append((network, info))
+ return network_info
- self.write_to_param_xenstore(vm_ref, {location: mapping})
+ def inject_network_info(self, instance, vm_ref, network_info):
+ """
+ Generate the network info and make calls to place it into the
+ xenstore and the xenstore param list
+ """
+ logging.debug(_("injecting network info to xs for vm: |%s|"), vm_ref)
+ # this function raises if vm_ref is not a vm_opaque_ref
+ self._session.get_xenapi().VM.get_record(vm_ref)
+
+ for (network, info) in network_info:
+ location = 'vm-data/networking/%s' % info['mac'].replace(':', '')
+ self.write_to_param_xenstore(vm_ref, {location: info})
try:
- self.write_to_xenstore(vm_ref, location, mapping['location'])
+ # TODO(tr3buchet): fix function call after refactor
+ #self.write_to_xenstore(vm_ref, location, info)
+ self._make_plugin_call('xenstore.py', 'write_record', instance,
+ location, {'value': json.dumps(info)},
+ vm_ref)
except KeyError:
# catch KeyError for domid if instance isn't running
pass
- return networks
+ def create_vifs(self, vm_ref, network_info):
+ """Creates vifs for an instance"""
+ logging.debug(_("creating vif(s) for vm: |%s|"), vm_ref)
- def create_vifs(self, instance, networks=None):
- """
- Creates vifs for an instance
+ # this function raises if vm_ref is not a vm_opaque_ref
+ self._session.get_xenapi().VM.get_record(vm_ref)
- """
- vm_ref = self._get_vm_opaque_ref(instance.id)
- logging.debug(_("creating vif(s) for vm: |%s|"), vm_ref)
- if networks is None:
- networks = db.network_get_all_by_instance(admin_context,
- instance['id'])
- # TODO(tr3buchet) - remove comment in multi-nic
- # this bit here about creating the vifs will be updated
- # in multi-nic to handle multiple IPs on the same network
- # and multiple networks
- # for now it works as there is only one of each
- for network in networks:
+ for device, (network, info) in enumerate(network_info):
+ mac_address = info['mac']
bridge = network['bridge']
+ rxtx_cap = info.pop('rxtx_cap')
network_ref = \
NetworkHelper.find_network_with_bridge(self._session, bridge)
- if network_ref:
- try:
- device = "1" if instance._rescue else "0"
- except AttributeError:
- device = "0"
+ VMHelper.create_vif(self._session, vm_ref, network_ref,
+ mac_address, device, rxtx_cap)
- VMHelper.create_vif(self._session, vm_ref, network_ref,
- instance.mac_address, device)
-
- def reset_network(self, instance):
- """
- Creates uuid arg to pass to make_agent_call and calls it.
-
- """
+ def reset_network(self, instance, vm_ref):
+ """Creates uuid arg to pass to make_agent_call and calls it."""
args = {'id': str(uuid.uuid4())}
- resp = self._make_agent_call('resetnetwork', instance, '', args)
+ # TODO(tr3buchet): fix function call after refactor
+ #resp = self._make_agent_call('resetnetwork', instance, '', args)
+ resp = self._make_plugin_call('agent', 'resetnetwork', instance, '',
+ args, vm_ref)
def list_from_xenstore(self, vm, path):
"""Runs the xenstore-ls command to get a listing of all records
@@ -815,25 +908,26 @@ class VMOps(object):
"""
self._make_xenstore_call('delete_record', vm, path)
- def _make_xenstore_call(self, method, vm, path, addl_args={}):
+ def _make_xenstore_call(self, method, vm, path, addl_args=None):
"""Handles calls to the xenstore xenapi plugin."""
return self._make_plugin_call('xenstore.py', method=method, vm=vm,
path=path, addl_args=addl_args)
- def _make_agent_call(self, method, vm, path, addl_args={}):
+ def _make_agent_call(self, method, vm, path, addl_args=None):
"""Abstracts out the interaction with the agent xenapi plugin."""
return self._make_plugin_call('agent', method=method, vm=vm,
path=path, addl_args=addl_args)
- def _make_plugin_call(self, plugin, method, vm, path, addl_args={}):
+ def _make_plugin_call(self, plugin, method, vm, path, addl_args=None,
+ vm_ref=None):
"""Abstracts out the process of calling a method of a xenapi plugin.
Any errors raised by the plugin will in turn raise a RuntimeError here.
"""
instance_id = vm.id
- vm_ref = self._get_vm_opaque_ref(vm)
+ vm_ref = vm_ref or self._get_vm_opaque_ref(vm)
vm_rec = self._session.get_xenapi().VM.get_record(vm_ref)
args = {'dom_id': vm_rec['domid'], 'path': path}
- args.update(addl_args)
+ args.update(addl_args or {})
try:
task = self._session.async_call_plugin(plugin, method, args)
ret = self._session.wait_for_task(task, instance_id)
@@ -914,7 +1008,7 @@ class VMOps(object):
"""
vm_ref = self._get_vm_opaque_ref(instance_or_vm)
data = self._session.call_xenapi_request('VM.get_xenstore_data',
- (vm_ref, ))
+ (vm_ref,))
ret = {}
if keys is None:
keys = data.keys()
diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py
index da42a83b6..f20fb29d8 100644
--- a/nova/virt/xenapi_conn.py
+++ b/nova/virt/xenapi_conn.py
@@ -69,6 +69,7 @@ from nova import db
from nova import utils
from nova import flags
from nova import log as logging
+from nova.virt import driver
from nova.virt.xenapi.vmops import VMOps
from nova.virt.xenapi.volumeops import VolumeOps
@@ -141,10 +142,11 @@ def get_connection(_):
return XenAPIConnection(url, username, password)
-class XenAPIConnection(object):
+class XenAPIConnection(driver.ComputeDriver):
"""A connection to XenServer or Xen Cloud Platform"""
def __init__(self, url, user, pw):
+ super(XenAPIConnection, self).__init__()
session = XenAPISession(url, user, pw)
self._vmops = VMOps(session)
self._volumeops = VolumeOps(session)
@@ -160,24 +162,25 @@ class XenAPIConnection(object):
"""List VM instances"""
return self._vmops.list_instances()
+ def list_instances_detail(self):
+ return self._vmops.list_instances_detail()
+
def spawn(self, instance):
"""Create VM instance"""
self._vmops.spawn(instance)
+ def revert_resize(self, instance):
+ """Reverts a resize, powering back on the instance"""
+ self._vmops.revert_resize(instance)
+
def finish_resize(self, instance, disk_info):
"""Completes a resize, turning on the migrated instance"""
- vdi_uuid = self._vmops.attach_disk(instance, disk_info['base_copy'],
- disk_info['cow'])
- self._vmops._spawn_with_disk(instance, vdi_uuid)
+ self._vmops.finish_resize(instance, disk_info)
def snapshot(self, instance, image_id):
""" Create snapshot from a running VM instance """
self._vmops.snapshot(instance, image_id)
- def resize(self, instance, flavor):
- """Resize a VM instance"""
- raise NotImplementedError()
-
def reboot(self, instance):
"""Reboot VM instance"""
self._vmops.reboot(instance)
@@ -225,6 +228,10 @@ class XenAPIConnection(object):
"""Unrescue the specified instance"""
self._vmops.unrescue(instance, callback)
+ def poll_rescued_instances(self, timeout):
+ """Poll for rescued instances"""
+ self._vmops.poll_rescued_instances(timeout)
+
def reset_network(self, instance):
"""reset networking for specified instance"""
self._vmops.reset_network(instance)
diff --git a/nova/volume/driver.py b/nova/volume/driver.py
index 7b4bacdec..779b46755 100644
--- a/nova/volume/driver.py
+++ b/nova/volume/driver.py
@@ -207,8 +207,8 @@ class AOEDriver(VolumeDriver):
(shelf_id,
blade_id) = self.db.volume_get_shelf_and_blade(context,
_volume['id'])
- self._execute("sudo aoe-discover")
- out, err = self._execute("sudo aoe-stat", check_exit_code=False)
+ self._execute('sudo', 'aoe-discover')
+ out, err = self._execute('sudo', 'aoe-stat', check_exit_code=False)
device_path = 'e%(shelf_id)d.%(blade_id)d' % locals()
if out.find(device_path) >= 0:
return "/dev/etherd/%s" % device_path
@@ -224,8 +224,8 @@ class AOEDriver(VolumeDriver):
(shelf_id,
blade_id) = self.db.volume_get_shelf_and_blade(context,
volume_id)
- cmd = "sudo vblade-persist ls --no-header"
- out, _err = self._execute(cmd)
+ cmd = ('sudo', 'vblade-persist', 'ls', '--no-header')
+ out, _err = self._execute(*cmd)
exported = False
for line in out.split('\n'):
param = line.split(' ')
@@ -318,8 +318,8 @@ class ISCSIDriver(VolumeDriver):
iscsi_name = "%s%s" % (FLAGS.iscsi_target_prefix, volume['name'])
volume_path = "/dev/%s/%s" % (FLAGS.volume_group, volume['name'])
self._execute('sudo', 'ietadm', '--op', 'new',
- '--tid=%s --params Name=%s' %
- (iscsi_target, iscsi_name))
+ '--tid=%s' % iscsi_target,
+ '--params', 'Name=%s' % iscsi_name)
self._execute('sudo', 'ietadm', '--op', 'new',
'--tid=%s' % iscsi_target,
'--lun=0', '--params',
@@ -500,7 +500,8 @@ class ISCSIDriver(VolumeDriver):
tid = self.db.volume_get_iscsi_target_num(context, volume_id)
try:
- self._execute("sudo ietadm --op show --tid=%(tid)d" % locals())
+ self._execute('sudo', 'ietadm', '--op', 'show',
+ '--tid=%(tid)d' % locals())
except exception.ProcessExecutionError, e:
# Instances remount read-only in this case.
# /etc/init.d/iscsitarget restart and rebooting nova-volume
@@ -551,7 +552,7 @@ class RBDDriver(VolumeDriver):
def delete_volume(self, volume):
"""Deletes a logical volume."""
self._try_execute('rbd', '--pool', FLAGS.rbd_pool,
- 'rm', voluname['name'])
+ 'rm', volume['name'])
def local_path(self, volume):
"""Returns the path of the rbd volume."""
diff --git a/nova/volume/manager.py b/nova/volume/manager.py
index 9dea35b35..2178389ce 100644
--- a/nova/volume/manager.py
+++ b/nova/volume/manager.py
@@ -64,14 +64,15 @@ flags.DEFINE_boolean('use_local_volumes', True,
'if True, will not discover local volumes')
-class VolumeManager(manager.Manager):
+class VolumeManager(manager.SchedulerDependentManager):
"""Manages attachable block storage devices."""
def __init__(self, volume_driver=None, *args, **kwargs):
"""Load the driver from the one specified in args, or from flags."""
if not volume_driver:
volume_driver = FLAGS.volume_driver
self.driver = utils.import_object(volume_driver)
- super(VolumeManager, self).__init__(*args, **kwargs)
+ super(VolumeManager, self).__init__(service_name='volume',
+ *args, **kwargs)
# NOTE(vish): Implementation specific db handling is done
# by the driver.
self.driver.db = self.db