summaryrefslogtreecommitdiffstats
path: root/nova
diff options
context:
space:
mode:
authorTushar Patil <tushar.vitthal.patil@gmail.com>2011-07-18 12:19:31 -0700
committerTushar Patil <tushar.vitthal.patil@gmail.com>2011-07-18 12:19:31 -0700
commit6d410105828c4dbfa11df5dd146b3b2591a24409 (patch)
tree458dc5ce08f531df75c23c2b960fa82435daf877 /nova
parentb4fba58f2785936f68a712d1cdd1d5c34f6a7c22 (diff)
parentcf25ab33cb7d6b5e233a767ad96b3c45b1387b5e (diff)
Merged with trunk
Diffstat (limited to 'nova')
-rw-r--r--nova/api/openstack/__init__.py12
-rw-r--r--nova/api/openstack/common.py13
-rw-r--r--nova/api/openstack/ips.py79
-rw-r--r--nova/api/openstack/limits.py108
-rw-r--r--nova/api/openstack/servers.py26
-rw-r--r--nova/api/openstack/views/addresses.py39
-rw-r--r--nova/api/openstack/views/images.py18
-rw-r--r--nova/api/openstack/views/servers.py13
-rw-r--r--nova/db/sqlalchemy/api.py24
-rw-r--r--nova/tests/api/openstack/test_common.py24
-rw-r--r--nova/tests/api/openstack/test_images.py14
-rw-r--r--nova/tests/api/openstack/test_limits.py96
-rw-r--r--nova/tests/api/openstack/test_servers.py195
-rw-r--r--nova/tests/integrated/api/client.py14
-rw-r--r--nova/tests/integrated/test_servers.py19
-rw-r--r--nova/virt/xenapi/vmops.py12
16 files changed, 611 insertions, 95 deletions
diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py
index f24017df0..e87d7c754 100644
--- a/nova/api/openstack/__init__.py
+++ b/nova/api/openstack/__init__.py
@@ -125,6 +125,10 @@ class APIRouter(base_wsgi.Router):
collection={'detail': 'GET'},
member=self.server_members)
+ mapper.resource("ip", "ips", controller=ips.create_resource(version),
+ parent_resource=dict(member_name='server',
+ collection_name='servers'))
+
mapper.resource("image", "images",
controller=images.create_resource(version),
collection={'detail': 'GET'})
@@ -144,9 +148,6 @@ class APIRouterV10(APIRouter):
def _setup_routes(self, mapper):
super(APIRouterV10, self)._setup_routes(mapper, '1.0')
- mapper.resource("image", "images",
- controller=images.create_resource('1.0'),
- collection={'detail': 'GET'})
mapper.resource("shared_ip_group", "shared_ip_groups",
collection={'detail': 'GET'},
@@ -157,11 +158,6 @@ class APIRouterV10(APIRouter):
parent_resource=dict(member_name='server',
collection_name='servers'))
- mapper.resource("ip", "ips", controller=ips.create_resource(),
- collection=dict(public='GET', private='GET'),
- parent_resource=dict(member_name='server',
- collection_name='servers'))
-
class APIRouterV11(APIRouter):
"""Define routes specific to OpenStack API V1.1."""
diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py
index 79969d393..8e12ce0c0 100644
--- a/nova/api/openstack/common.py
+++ b/nova/api/openstack/common.py
@@ -137,15 +137,22 @@ def get_id_from_href(href):
def remove_version_from_href(href):
- """Removes the api version from the href.
+ """Removes the first api version from the href.
Given: 'http://www.nova.com/v1.1/123'
Returns: 'http://www.nova.com/123'
+ Given: 'http://www.nova.com/v1.1'
+ Returns: 'http://www.nova.com'
+
"""
try:
- #matches /v#.#
- new_href = re.sub(r'[/][v][0-9]*.[0-9]*', '', href)
+ #removes the first instance that matches /v#.#/
+ new_href = re.sub(r'[/][v][0-9]+\.[0-9]+[/]', '/', href, count=1)
+
+ #if no version was found, try finding /v#.# at the end of the string
+ if new_href == href:
+ new_href = re.sub(r'[/][v][0-9]+\.[0-9]+$', '', href, count=1)
except:
LOG.debug(_("Error removing version from href: %s") % href)
msg = _('could not parse version from href')
diff --git a/nova/api/openstack/ips.py b/nova/api/openstack/ips.py
index 23e5432d6..1ebfdb831 100644
--- a/nova/api/openstack/ips.py
+++ b/nova/api/openstack/ips.py
@@ -23,6 +23,7 @@ import nova
from nova.api.openstack import faults
import nova.api.openstack.views.addresses
from nova.api.openstack import wsgi
+from nova import db
class Controller(object):
@@ -30,7 +31,6 @@ class Controller(object):
def __init__(self):
self.compute_api = nova.compute.API()
- self.builder = nova.api.openstack.views.addresses.ViewBuilderV10()
def _get_instance(self, req, server_id):
try:
@@ -40,29 +40,78 @@ class Controller(object):
return faults.Fault(exc.HTTPNotFound())
return instance
+ def create(self, req, server_id, body):
+ return faults.Fault(exc.HTTPNotImplemented())
+
+ def delete(self, req, server_id, id):
+ return faults.Fault(exc.HTTPNotImplemented())
+
+
+class ControllerV10(Controller):
+
def index(self, req, server_id):
instance = self._get_instance(req, server_id)
- return {'addresses': self.builder.build(instance)}
+ builder = nova.api.openstack.views.addresses.ViewBuilderV10()
+ return {'addresses': builder.build(instance)}
- def public(self, req, server_id):
+ def show(self, req, server_id, id):
instance = self._get_instance(req, server_id)
- return {'public': self.builder.build_public_parts(instance)}
+ builder = self._get_view_builder(req)
+ if id == 'private':
+ view = builder.build_private_parts(instance)
+ elif id == 'public':
+ view = builder.build_public_parts(instance)
+ else:
+ msg = _("Only private and public networks available")
+ return faults.Fault(exc.HTTPNotFound(explanation=msg))
- def private(self, req, server_id):
- instance = self._get_instance(req, server_id)
- return {'private': self.builder.build_private_parts(instance)}
+ return {id: view}
+
+ def _get_view_builder(self, req):
+ return nova.api.openstack.views.addresses.ViewBuilderV10()
+
+
+class ControllerV11(Controller):
+
+ def index(self, req, server_id):
+ context = req.environ['nova.context']
+ interfaces = self._get_virtual_interfaces(context, server_id)
+ networks = self._get_view_builder(req).build(interfaces)
+ return {'addresses': networks}
def show(self, req, server_id, id):
- return faults.Fault(exc.HTTPNotImplemented())
+ context = req.environ['nova.context']
+ interfaces = self._get_virtual_interfaces(context, server_id)
+ network = self._get_view_builder(req).build_network(interfaces, id)
- def create(self, req, server_id, body):
- return faults.Fault(exc.HTTPNotImplemented())
+ if network is None:
+ msg = _("Instance is not a member of specified network")
+ return faults.Fault(exc.HTTPNotFound(explanation=msg))
- def delete(self, req, server_id, id):
- return faults.Fault(exc.HTTPNotImplemented())
+ return network
+
+ def _get_virtual_interfaces(self, context, server_id):
+ try:
+ return db.api.virtual_interface_get_by_instance(context, server_id)
+ except nova.exception.InstanceNotFound:
+ msg = _("Instance does not exist")
+ raise exc.HTTPNotFound(explanation=msg)
+
+ def _get_view_builder(self, req):
+ return nova.api.openstack.views.addresses.ViewBuilderV11()
+
+
+def create_resource(version):
+ controller = {
+ '1.0': ControllerV10,
+ '1.1': ControllerV11,
+ }[version]()
+ xmlns = {
+ '1.0': wsgi.XMLNS_V10,
+ '1.1': wsgi.XMLNS_V11,
+ }[version]
-def create_resource():
metadata = {
'list_collections': {
'public': {'item_name': 'ip', 'item_key': 'addr'},
@@ -72,8 +121,8 @@ def create_resource():
body_serializers = {
'application/xml': wsgi.XMLDictSerializer(metadata=metadata,
- xmlns=wsgi.XMLNS_V10),
+ xmlns=xmlns),
}
serializer = wsgi.ResponseSerializer(body_serializers)
- return wsgi.Resource(Controller(), serializer=serializer)
+ return wsgi.Resource(controller, serializer=serializer)
diff --git a/nova/api/openstack/limits.py b/nova/api/openstack/limits.py
index d08287f6b..bc76547d8 100644
--- a/nova/api/openstack/limits.py
+++ b/nova/api/openstack/limits.py
@@ -31,8 +31,8 @@ from collections import defaultdict
from webob.dec import wsgify
from nova import quota
+from nova import utils
from nova import wsgi as base_wsgi
-from nova import wsgi
from nova.api.openstack import common
from nova.api.openstack import faults
from nova.api.openstack.views import limits as limits_views
@@ -119,6 +119,8 @@ class Limit(object):
60 * 60 * 24: "DAY",
}
+ UNIT_MAP = dict([(v, k) for k, v in UNITS.items()])
+
def __init__(self, verb, uri, regex, value, unit):
"""
Initialize a new `Limit`.
@@ -224,16 +226,30 @@ class RateLimitingMiddleware(base_wsgi.Middleware):
is stored in memory for this implementation.
"""
- def __init__(self, application, limits=None):
+ def __init__(self, application, limits=None, limiter=None, **kwargs):
"""
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
+ @param limits: String describing limits
+ @param limiter: String identifying class for representing limits
+
+ Other parameters are passed to the constructor for the limiter.
"""
base_wsgi.Middleware.__init__(self, application)
- self._limiter = Limiter(limits or DEFAULT_LIMITS)
+
+ # Select the limiter class
+ if limiter is None:
+ limiter = Limiter
+ else:
+ limiter = utils.import_class(limiter)
+
+ # Parse the limits, if any are provided
+ if limits is not None:
+ limits = limiter.parse_limits(limits)
+
+ self._limiter = limiter(limits or DEFAULT_LIMITS, **kwargs)
@wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
@@ -271,7 +287,7 @@ class Limiter(object):
Rate-limit checking class which handles limits in memory.
"""
- def __init__(self, limits):
+ def __init__(self, limits, **kwargs):
"""
Initialize the new `Limiter`.
@@ -280,6 +296,12 @@ class Limiter(object):
self.limits = copy.deepcopy(limits)
self.levels = defaultdict(lambda: copy.deepcopy(limits))
+ # Pick up any per-user limit information
+ for key, value in kwargs.items():
+ if key.startswith('user:'):
+ username = key[5:]
+ self.levels[username] = self.parse_limits(value)
+
def get_limits(self, username=None):
"""
Return the limits for a given user.
@@ -305,6 +327,66 @@ class Limiter(object):
return None, None
+ # Note: This method gets called before the class is instantiated,
+ # so this must be either a static method or a class method. It is
+ # used to develop a list of limits to feed to the constructor. We
+ # put this in the class so that subclasses can override the
+ # default limit parsing.
+ @staticmethod
+ def parse_limits(limits):
+ """
+ Convert a string into a list of Limit instances. This
+ implementation expects a semicolon-separated sequence of
+ parenthesized groups, where each group contains a
+ comma-separated sequence consisting of HTTP method,
+ user-readable URI, a URI reg-exp, an integer number of
+ requests which can be made, and a unit of measure. Valid
+ values for the latter are "SECOND", "MINUTE", "HOUR", and
+ "DAY".
+
+ @return: List of Limit instances.
+ """
+
+ # Handle empty limit strings
+ limits = limits.strip()
+ if not limits:
+ return []
+
+ # Split up the limits by semicolon
+ result = []
+ for group in limits.split(';'):
+ group = group.strip()
+ if group[:1] != '(' or group[-1:] != ')':
+ raise ValueError("Limit rules must be surrounded by "
+ "parentheses")
+ group = group[1:-1]
+
+ # Extract the Limit arguments
+ args = [a.strip() for a in group.split(',')]
+ if len(args) != 5:
+ raise ValueError("Limit rules must contain the following "
+ "arguments: verb, uri, regex, value, unit")
+
+ # Pull out the arguments
+ verb, uri, regex, value, unit = args
+
+ # Upper-case the verb
+ verb = verb.upper()
+
+ # Convert value--raises ValueError if it's not integer
+ value = int(value)
+
+ # Convert unit
+ unit = unit.upper()
+ if unit not in Limit.UNIT_MAP:
+ raise ValueError("Invalid units specified")
+ unit = Limit.UNIT_MAP[unit]
+
+ # Build a limit
+ result.append(Limit(verb, uri, regex, value, unit))
+
+ return result
+
class WsgiLimiter(object):
"""
@@ -388,3 +470,19 @@ class WsgiLimiterProxy(object):
return None, None
return resp.getheader("X-Wait-Seconds"), resp.read() or None
+
+ # Note: This method gets called before the class is instantiated,
+ # so this must be either a static method or a class method. It is
+ # used to develop a list of limits to feed to the constructor.
+ # This implementation returns an empty list, since all limit
+ # decisions are made by a remote server.
+ @staticmethod
+ def parse_limits(limits):
+ """
+ Ignore a limits string--simply doesn't apply for the limit
+ proxy.
+
+ @return: Empty list.
+ """
+
+ return []
diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py
index 12af44a8d..93f8e832c 100644
--- a/nova/api/openstack/servers.py
+++ b/nova/api/openstack/servers.py
@@ -19,6 +19,7 @@ import traceback
from webob import exc
from nova import compute
+from nova import db
from nova import exception
from nova import flags
from nova import log as logging
@@ -62,7 +63,7 @@ class Controller(object):
return exc.HTTPBadRequest(explanation=str(err))
return servers
- def _get_view_builder(self, req):
+ def _build_view(self, req, instance, is_detail=False):
raise NotImplementedError()
def _limit_items(self, items, req):
@@ -88,8 +89,7 @@ class Controller(object):
fixed_ip=fixed_ip,
recurse_zones=recurse_zones)
limited_list = self._limit_items(instance_list, req)
- builder = self._get_view_builder(req)
- servers = [builder.build(inst, is_detail)['server']
+ servers = [self._build_view(req, inst, is_detail)['server']
for inst in limited_list]
return dict(servers=servers)
@@ -99,8 +99,7 @@ class Controller(object):
try:
instance = self.compute_api.routing_get(
req.environ['nova.context'], id)
- builder = self._get_view_builder(req)
- return builder.build(instance, is_detail=True)
+ return self._build_view(req, instance, is_detail=True)
except exception.NotFound:
return faults.Fault(exc.HTTPNotFound())
@@ -121,8 +120,7 @@ class Controller(object):
for key in ['instance_type', 'image_ref']:
inst[key] = extra_values[key]
- builder = self._get_view_builder(req)
- server = builder.build(inst, is_detail=True)
+ server = self._build_view(req, inst, is_detail=True)
server['server']['adminPass'] = extra_values['password']
return server
@@ -426,10 +424,10 @@ class ControllerV10(Controller):
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 _build_view(self, req, instance, is_detail=False):
+ addresses = nova.api.openstack.views.addresses.ViewBuilderV10()
+ builder = nova.api.openstack.views.servers.ViewBuilderV10(addresses)
+ return builder.build(instance, is_detail=is_detail)
def _limit_items(self, items, req):
return common.limited(items, req)
@@ -498,16 +496,18 @@ class ControllerV11(Controller):
href = data['server']['flavorRef']
return common.get_id_from_href(href)
- def _get_view_builder(self, req):
+ def _build_view(self, req, instance, is_detail=False):
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(
+ builder = nova.api.openstack.views.servers.ViewBuilderV11(
addresses_builder, flavor_builder, image_builder, base_url)
+ return builder.build(instance, is_detail=is_detail)
+
def _action_change_password(self, input_dict, req, id):
context = req.environ['nova.context']
if (not 'changePassword' in input_dict
diff --git a/nova/api/openstack/views/addresses.py b/nova/api/openstack/views/addresses.py
index b59eb4751..a242efa45 100644
--- a/nova/api/openstack/views/addresses.py
+++ b/nova/api/openstack/views/addresses.py
@@ -20,13 +20,14 @@ from nova.api.openstack import common
class ViewBuilder(object):
- ''' Models a server addresses response as a python dictionary.'''
+ """Models a server addresses response as a python dictionary."""
def build(self, inst):
raise NotImplementedError()
class ViewBuilderV10(ViewBuilder):
+
def build(self, inst):
private_ips = self.build_private_parts(inst)
public_ips = self.build_public_parts(inst)
@@ -40,11 +41,31 @@ class ViewBuilderV10(ViewBuilder):
class ViewBuilderV11(ViewBuilder):
- def build(self, inst):
- # TODO(tr3buchet) - this shouldn't be hard coded to 4...
- private_ips = utils.get_from_path(inst, 'fixed_ips/address')
- private_ips = [dict(version=4, addr=a) for a in private_ips]
- public_ips = utils.get_from_path(inst,
- 'fixed_ips/floating_ips/address')
- public_ips = [dict(version=4, addr=a) for a in public_ips]
- return dict(public=public_ips, private=private_ips)
+
+ def build(self, interfaces):
+ networks = {}
+ for interface in interfaces:
+ network_label = interface['network']['label']
+
+ if network_label not in networks:
+ networks[network_label] = []
+
+ networks[network_label].extend(self._extract_ipv4(interface))
+
+ return networks
+
+ def build_network(self, interfaces, network_label):
+ for interface in interfaces:
+ if interface['network']['label'] == network_label:
+ ips = self._extract_ipv4(interface)
+ return {network_label: list(ips)}
+ return None
+
+ def _extract_ipv4(self, interface):
+ for fixed_ip in interface['fixed_ips']:
+ yield self._build_ip_entity(fixed_ip['address'], 4)
+ for floating_ip in fixed_ip.get('floating_ips', []):
+ yield self._build_ip_entity(floating_ip['address'], 4)
+
+ def _build_ip_entity(self, address, version):
+ return {'addr': address, 'version': version}
diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py
index 5c0510377..873ce212a 100644
--- a/nova/api/openstack/views/images.py
+++ b/nova/api/openstack/views/images.py
@@ -121,16 +121,20 @@ class ViewBuilderV11(ViewBuilder):
href = self.generate_href(image_obj["id"])
bookmark = self.generate_bookmark(image_obj["id"])
- image["links"] = [{
- "rel": "self",
- "href": href,
- }]
+ image["links"] = [
+ {
+ "rel": "self",
+ "href": href,
+ },
+ {
+ "rel": "bookmark",
+ "href": bookmark,
+ },
+
+ ]
if detail:
image["metadata"] = image_obj.get("properties", {})
- image["links"].append({"rel": "bookmark",
- "href": bookmark,
- })
return image
diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py
index 67fb6a84e..ab7e8da61 100644
--- a/nova/api/openstack/views/servers.py
+++ b/nova/api/openstack/views/servers.py
@@ -77,7 +77,6 @@ class ViewBuilder(object):
inst_dict = {
'id': inst['id'],
'name': inst['display_name'],
- 'addresses': self.addresses_builder.build(inst),
'status': power_mapping[inst.get('state')]}
ctxt = nova.context.get_admin_context()
@@ -98,10 +97,15 @@ class ViewBuilder(object):
self._build_image(inst_dict, inst)
self._build_flavor(inst_dict, inst)
+ self._build_addresses(inst_dict, inst)
inst_dict['uuid'] = inst['uuid']
return dict(server=inst_dict)
+ def _build_addresses(self, response, inst):
+ """Return the addresses sub-resource of a server."""
+ raise NotImplementedError()
+
def _build_image(self, response, inst):
"""Return the image sub-resource of a server."""
raise NotImplementedError()
@@ -128,6 +132,9 @@ class ViewBuilderV10(ViewBuilder):
if 'instance_type' in dict(inst):
response['flavorId'] = inst['instance_type']['flavorid']
+ def _build_addresses(self, response, inst):
+ response['addresses'] = self.addresses_builder.build(inst)
+
class ViewBuilderV11(ViewBuilder):
"""Model an Openstack API V1.0 server response."""
@@ -151,6 +158,10 @@ class ViewBuilderV11(ViewBuilder):
flavor_ref = self.flavor_builder.generate_href(flavor_id)
response["flavorRef"] = flavor_ref
+ def _build_addresses(self, response, inst):
+ interfaces = inst.get('virtual_interfaces', [])
+ response['addresses'] = self.addresses_builder.build(interfaces)
+
def _build_extra(self, response, inst):
self._build_links(response, inst)
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index d5cfc6099..6558946fd 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -118,8 +118,23 @@ def require_context(f):
return wrapper
+def require_instance_exists(f):
+ """Decorator to require the specified instance to exist.
+
+ Requres the wrapped function to use context and instance_id as
+ their first two arguments.
+ """
+
+ def wrapper(context, instance_id, *args, **kwargs):
+ db.api.instance_get(context, instance_id)
+ return f(context, instance_id, *args, **kwargs)
+ wrapper.__name__ = f.__name__
+ return wrapper
+
+
###################
+
@require_admin_context
def service_destroy(context, service_id):
session = get_session()
@@ -975,6 +990,7 @@ def virtual_interface_get_by_fixed_ip(context, fixed_ip_id):
@require_context
+@require_instance_exists
def virtual_interface_get_by_instance(context, instance_id):
"""Gets all virtual interfaces for instance.
@@ -3194,14 +3210,6 @@ def zone_get_all(context):
####################
-def require_instance_exists(func):
- def new_func(context, instance_id, *args, **kwargs):
- db.api.instance_get(context, instance_id)
- return func(context, instance_id, *args, **kwargs)
- new_func.__name__ = func.__name__
- return new_func
-
-
@require_context
@require_instance_exists
def instance_metadata_get(context, instance_id):
diff --git a/nova/tests/api/openstack/test_common.py b/nova/tests/api/openstack/test_common.py
index 7440bccfb..4c4d03995 100644
--- a/nova/tests/api/openstack/test_common.py
+++ b/nova/tests/api/openstack/test_common.py
@@ -206,12 +206,36 @@ class MiscFunctionsTest(test.TestCase):
actual = common.remove_version_from_href(fixture)
self.assertEqual(actual, expected)
+ def test_remove_version_from_href_3(self):
+ fixture = 'http://www.testsite.com/v10.10'
+ expected = 'http://www.testsite.com'
+ actual = common.remove_version_from_href(fixture)
+ self.assertEqual(actual, expected)
+
+ def test_remove_version_from_href_4(self):
+ fixture = 'http://www.testsite.com/v1.1/images/v10.5'
+ expected = 'http://www.testsite.com/images/v10.5'
+ actual = common.remove_version_from_href(fixture)
+ self.assertEqual(actual, expected)
+
def test_remove_version_from_href_bad_request(self):
fixture = 'http://www.testsite.com/1.1/images'
self.assertRaises(ValueError,
common.remove_version_from_href,
fixture)
+ def test_remove_version_from_href_bad_request_2(self):
+ fixture = 'http://www.testsite.com/v/images'
+ self.assertRaises(ValueError,
+ common.remove_version_from_href,
+ fixture)
+
+ def test_remove_version_from_href_bad_request_3(self):
+ fixture = 'http://www.testsite.com/v1.1images'
+ self.assertRaises(ValueError,
+ common.remove_version_from_href,
+ fixture)
+
def test_get_id_from_href(self):
fixture = 'http://www.testsite.com/dir/45'
actual = common.get_id_from_href(fixture)
diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py
index c1bdd6906..534460d46 100644
--- a/nova/tests/api/openstack/test_images.py
+++ b/nova/tests/api/openstack/test_images.py
@@ -568,10 +568,16 @@ class ImageControllerWithGlanceServiceTest(test.TestCase):
test_image = {
"id": image["id"],
"name": image["name"],
- "links": [{
- "rel": "self",
- "href": href,
- }],
+ "links": [
+ {
+ "rel": "self",
+ "href": href,
+ },
+ {
+ "rel": "bookmark",
+ "href": bookmark,
+ },
+ ],
}
self.assertTrue(test_image in response_list)
diff --git a/nova/tests/api/openstack/test_limits.py b/nova/tests/api/openstack/test_limits.py
index 38c959fae..76363450d 100644
--- a/nova/tests/api/openstack/test_limits.py
+++ b/nova/tests/api/openstack/test_limits.py
@@ -400,6 +400,10 @@ class LimitsControllerV11Test(BaseLimitTestSuite):
self._test_index_absolute_limits_json(expected)
+class TestLimiter(limits.Limiter):
+ pass
+
+
class LimitMiddlewareTest(BaseLimitTestSuite):
"""
Tests for the `limits.RateLimitingMiddleware` class.
@@ -413,10 +417,14 @@ class LimitMiddlewareTest(BaseLimitTestSuite):
def setUp(self):
"""Prepare middleware for use through fake WSGI app."""
BaseLimitTestSuite.setUp(self)
- _limits = [
- limits.Limit("GET", "*", ".*", 1, 60),
- ]
- self.app = limits.RateLimitingMiddleware(self._empty_app, _limits)
+ _limits = '(GET, *, .*, 1, MINUTE)'
+ self.app = limits.RateLimitingMiddleware(self._empty_app, _limits,
+ "%s.TestLimiter" %
+ self.__class__.__module__)
+
+ def test_limit_class(self):
+ """Test that middleware selected correct limiter class."""
+ assert isinstance(self.app._limiter, TestLimiter)
def test_good_request(self):
"""Test successful GET request through middleware."""
@@ -492,6 +500,72 @@ class LimitTest(BaseLimitTestSuite):
self.assertEqual(4, limit.last_request)
+class ParseLimitsTest(BaseLimitTestSuite):
+ """
+ Tests for the default limits parser in the in-memory
+ `limits.Limiter` class.
+ """
+
+ def test_invalid(self):
+ """Test that parse_limits() handles invalid input correctly."""
+ self.assertRaises(ValueError, limits.Limiter.parse_limits,
+ ';;;;;')
+
+ def test_bad_rule(self):
+ """Test that parse_limits() handles bad rules correctly."""
+ self.assertRaises(ValueError, limits.Limiter.parse_limits,
+ 'GET, *, .*, 20, minute')
+
+ def test_missing_arg(self):
+ """Test that parse_limits() handles missing args correctly."""
+ self.assertRaises(ValueError, limits.Limiter.parse_limits,
+ '(GET, *, .*, 20)')
+
+ def test_bad_value(self):
+ """Test that parse_limits() handles bad values correctly."""
+ self.assertRaises(ValueError, limits.Limiter.parse_limits,
+ '(GET, *, .*, foo, minute)')
+
+ def test_bad_unit(self):
+ """Test that parse_limits() handles bad units correctly."""
+ self.assertRaises(ValueError, limits.Limiter.parse_limits,
+ '(GET, *, .*, 20, lightyears)')
+
+ def test_multiple_rules(self):
+ """Test that parse_limits() handles multiple rules correctly."""
+ try:
+ l = limits.Limiter.parse_limits('(get, *, .*, 20, minute);'
+ '(PUT, /foo*, /foo.*, 10, hour);'
+ '(POST, /bar*, /bar.*, 5, second);'
+ '(Say, /derp*, /derp.*, 1, day)')
+ except ValueError, e:
+ assert False, str(e)
+
+ # Make sure the number of returned limits are correct
+ self.assertEqual(len(l), 4)
+
+ # Check all the verbs...
+ expected = ['GET', 'PUT', 'POST', 'SAY']
+ self.assertEqual([t.verb for t in l], expected)
+
+ # ...the URIs...
+ expected = ['*', '/foo*', '/bar*', '/derp*']
+ self.assertEqual([t.uri for t in l], expected)
+
+ # ...the regexes...
+ expected = ['.*', '/foo.*', '/bar.*', '/derp.*']
+ self.assertEqual([t.regex for t in l], expected)
+
+ # ...the values...
+ expected = [20, 10, 5, 1]
+ self.assertEqual([t.value for t in l], expected)
+
+ # ...and the units...
+ expected = [limits.PER_MINUTE, limits.PER_HOUR,
+ limits.PER_SECOND, limits.PER_DAY]
+ self.assertEqual([t.unit for t in l], expected)
+
+
class LimiterTest(BaseLimitTestSuite):
"""
Tests for the in-memory `limits.Limiter` class.
@@ -500,7 +574,8 @@ class LimiterTest(BaseLimitTestSuite):
def setUp(self):
"""Run before each test."""
BaseLimitTestSuite.setUp(self)
- self.limiter = limits.Limiter(TEST_LIMITS)
+ userlimits = {'user:user3': ''}
+ self.limiter = limits.Limiter(TEST_LIMITS, **userlimits)
def _check(self, num, verb, url, username=None):
"""Check and yield results from checks."""
@@ -605,6 +680,12 @@ class LimiterTest(BaseLimitTestSuite):
results = list(self._check(10, "PUT", "/anything"))
self.assertEqual(expected, results)
+ def test_user_limit(self):
+ """
+ Test user-specific limits.
+ """
+ self.assertEqual(self.limiter.levels['user3'], [])
+
def test_multiple_users(self):
"""
Tests involving multiple users.
@@ -619,6 +700,11 @@ class LimiterTest(BaseLimitTestSuite):
results = list(self._check(15, "PUT", "/anything", "user2"))
self.assertEqual(expected, results)
+ # User3
+ expected = [None] * 20
+ results = list(self._check(20, "PUT", "/anything", "user3"))
+ self.assertEqual(expected, results)
+
self.time += 1.0
# User1 again
diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py
index 702dcb7a8..b2f1a8ef0 100644
--- a/nova/tests/api/openstack/test_servers.py
+++ b/nova/tests/api/openstack/test_servers.py
@@ -65,6 +65,18 @@ def return_server_by_uuid(context, uuid):
return stub_instance(id, uuid=uuid)
+def return_virtual_interface_by_instance(interfaces):
+ def _return_virtual_interface_by_instance(context, instance_id):
+ return interfaces
+ return _return_virtual_interface_by_instance
+
+
+def return_virtual_interface_instance_nonexistant(interfaces):
+ def _return_virtual_interface_by_instance(context, instance_id):
+ raise exception.InstanceNotFound(instance_id=instance_id)
+ return _return_virtual_interface_by_instance
+
+
def return_server_with_addresses(private, public):
def _return_server(context, id):
return stub_instance(id, private_address=private,
@@ -72,6 +84,12 @@ def return_server_with_addresses(private, public):
return _return_server
+def return_server_with_interfaces(interfaces):
+ def _return_server(context, id):
+ return stub_instance(id, interfaces=interfaces)
+ return _return_server
+
+
def return_server_with_power_state(power_state):
def _return_server(context, id):
return stub_instance(id, power_state=power_state)
@@ -124,10 +142,13 @@ def instance_addresses(context, instance_id):
def stub_instance(id, user_id=1, private_address=None, public_addresses=None,
host=None, power_state=0, reservation_id="",
- uuid=FAKE_UUID):
+ uuid=FAKE_UUID, interfaces=None):
metadata = []
metadata.append(InstanceMetadata(key='seq', value=id))
+ if interfaces is None:
+ interfaces = []
+
inst_type = instance_types.get_instance_type_by_flavor_id(1)
if public_addresses is None:
@@ -171,7 +192,8 @@ def stub_instance(id, user_id=1, private_address=None, public_addresses=None,
"display_description": "",
"locked": False,
"metadata": metadata,
- "uuid": uuid}
+ "uuid": uuid,
+ "virtual_interfaces": interfaces}
instance["fixed_ips"] = {
"address": private_address,
@@ -411,23 +433,152 @@ class ServersTest(test.TestCase):
self.assertEquals(ip.getAttribute('addr'), private)
def test_get_server_by_id_with_addresses_v1_1(self):
- private = "192.168.0.3"
- public = ["1.2.3.4"]
- new_return_server = return_server_with_addresses(private, public)
+ interfaces = [
+ {
+ 'network': {'label': 'network_1'},
+ 'fixed_ips': [
+ {'address': '192.168.0.3'},
+ {'address': '192.168.0.4'},
+ ],
+ },
+ {
+ 'network': {'label': 'network_2'},
+ 'fixed_ips': [
+ {'address': '172.19.0.1'},
+ {'address': '172.19.0.2'},
+ ],
+ },
+ ]
+ new_return_server = return_server_with_interfaces(interfaces)
self.stubs.Set(nova.db.api, 'instance_get', new_return_server)
+
req = webob.Request.blank('/v1.1/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']['name'], 'server1')
addresses = res_dict['server']['addresses']
- # RM(4047): Figure otu what is up with the 1.1 api and multi-nic
- #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})
+ expected = {
+ 'network_1': [
+ {'addr': '192.168.0.3', 'version': 4},
+ {'addr': '192.168.0.4', 'version': 4},
+ ],
+ 'network_2': [
+ {'addr': '172.19.0.1', 'version': 4},
+ {'addr': '172.19.0.2', 'version': 4},
+ ],
+ }
+
+ self.assertEqual(addresses, expected)
+
+ def test_get_server_addresses_v1_1(self):
+ interfaces = [
+ {
+ 'network': {'label': 'network_1'},
+ 'fixed_ips': [
+ {'address': '192.168.0.3'},
+ {'address': '192.168.0.4'},
+ ],
+ },
+ {
+ 'network': {'label': 'network_2'},
+ 'fixed_ips': [
+ {
+ 'address': '172.19.0.1',
+ 'floating_ips': [
+ {'address': '1.2.3.4'},
+ ],
+ },
+ {'address': '172.19.0.2'},
+ ],
+ },
+ ]
+
+ _return_vifs = return_virtual_interface_by_instance(interfaces)
+ self.stubs.Set(nova.db.api,
+ 'virtual_interface_get_by_instance',
+ _return_vifs)
+
+ req = webob.Request.blank('/v1.1/servers/1/ips')
+ res = req.get_response(fakes.wsgi_app())
+ res_dict = json.loads(res.body)
+
+ expected = {
+ 'addresses': {
+ 'network_1': [
+ {'version': 4, 'addr': '192.168.0.3'},
+ {'version': 4, 'addr': '192.168.0.4'},
+ ],
+ 'network_2': [
+ {'version': 4, 'addr': '172.19.0.1'},
+ {'version': 4, 'addr': '1.2.3.4'},
+ {'version': 4, 'addr': '172.19.0.2'},
+ ],
+ },
+ }
+
+ self.assertEqual(res_dict, expected)
+
+ def test_get_server_addresses_single_network_v1_1(self):
+ interfaces = [
+ {
+ 'network': {'label': 'network_1'},
+ 'fixed_ips': [
+ {'address': '192.168.0.3'},
+ {'address': '192.168.0.4'},
+ ],
+ },
+ {
+ 'network': {'label': 'network_2'},
+ 'fixed_ips': [
+ {
+ 'address': '172.19.0.1',
+ 'floating_ips': [
+ {'address': '1.2.3.4'},
+ ],
+ },
+ {'address': '172.19.0.2'},
+ ],
+ },
+ ]
+ _return_vifs = return_virtual_interface_by_instance(interfaces)
+ self.stubs.Set(nova.db.api,
+ 'virtual_interface_get_by_instance',
+ _return_vifs)
+
+ req = webob.Request.blank('/v1.1/servers/1/ips/network_2')
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
+ res_dict = json.loads(res.body)
+ expected = {
+ 'network_2': [
+ {'version': 4, 'addr': '172.19.0.1'},
+ {'version': 4, 'addr': '1.2.3.4'},
+ {'version': 4, 'addr': '172.19.0.2'},
+ ],
+ }
+ self.assertEqual(res_dict, expected)
+
+ def test_get_server_addresses_nonexistant_network_v1_1(self):
+ _return_vifs = return_virtual_interface_by_instance([])
+ self.stubs.Set(nova.db.api,
+ 'virtual_interface_get_by_instance',
+ _return_vifs)
+
+ req = webob.Request.blank('/v1.1/servers/1/ips/network_0')
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 404)
+
+ def test_get_server_addresses_nonexistant_server_v1_1(self):
+ _return_vifs = return_virtual_interface_instance_nonexistant([])
+ self.stubs.Set(nova.db.api,
+ 'virtual_interface_get_by_instance',
+ _return_vifs)
+
+ req = webob.Request.blank('/v1.1/servers/600/ips')
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 404)
def test_get_server_list(self):
req = webob.Request.blank('/v1.0/servers')
@@ -794,13 +945,13 @@ class ServersTest(test.TestCase):
res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 200)
server = json.loads(res.body)['server']
self.assertEqual(16, len(server['adminPass']))
self.assertEqual('server_test', server['name'])
self.assertEqual(1, server['id'])
self.assertEqual(flavor_ref, server['flavorRef'])
self.assertEqual(image_href, server['imageRef'])
- self.assertEqual(res.status_int, 200)
def test_create_instance_v1_1_bad_href(self):
self._setup_for_create_instance()
@@ -931,7 +1082,7 @@ class ServersTest(test.TestCase):
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 400)
- def test_update_no_body(self):
+ def test_update_server_no_body(self):
req = webob.Request.blank('/v1.0/servers/1')
req.method = 'PUT'
res = req.get_response(fakes.wsgi_app())
@@ -997,6 +1148,21 @@ class ServersTest(test.TestCase):
self.assertEqual(mock_method.instance_id, '1')
self.assertEqual(mock_method.password, 'bacon')
+ def test_update_server_no_body_v1_1(self):
+ req = webob.Request.blank('/v1.0/servers/1')
+ req.method = 'PUT'
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 400)
+
+ def test_update_server_name_v1_1(self):
+ req = webob.Request.blank('/v1.1/servers/1')
+ req.method = 'PUT'
+ req.content_type = 'application/json'
+ req.body = json.dumps({'server': {'name': 'new-name'}})
+ res = req.get_response(fakes.wsgi_app())
+ self.assertEqual(res.status_int, 204)
+ self.assertEqual(res.body, '')
+
def test_update_server_adminPass_ignored_v1_1(self):
inst_dict = dict(name='server_test', adminPass='bacon')
self.body = json.dumps(dict(server=inst_dict))
@@ -1015,6 +1181,7 @@ class ServersTest(test.TestCase):
req.body = self.body
res = req.get_response(fakes.wsgi_app())
self.assertEqual(res.status_int, 204)
+ self.assertEqual(res.body, '')
def test_create_backup_schedules(self):
req = webob.Request.blank('/v1.0/servers/1/backup_schedule')
diff --git a/nova/tests/integrated/api/client.py b/nova/tests/integrated/api/client.py
index 59cc3b564..035a35aab 100644
--- a/nova/tests/integrated/api/client.py
+++ b/nova/tests/integrated/api/client.py
@@ -172,6 +172,17 @@ class TestOpenStackClient(object):
response = self.api_request(relative_uri, **kwargs)
return self._decode_json(response)
+ def api_put(self, relative_uri, body, **kwargs):
+ kwargs['method'] = 'PUT'
+ if body:
+ headers = kwargs.setdefault('headers', {})
+ headers['Content-Type'] = 'application/json'
+ kwargs['body'] = json.dumps(body)
+
+ kwargs.setdefault('check_response_status', [200, 202, 204])
+ response = self.api_request(relative_uri, **kwargs)
+ return self._decode_json(response)
+
def api_delete(self, relative_uri, **kwargs):
kwargs['method'] = 'DELETE'
kwargs.setdefault('check_response_status', [200, 202, 204])
@@ -187,6 +198,9 @@ class TestOpenStackClient(object):
def post_server(self, server):
return self.api_post('/servers', server)['server']
+ def put_server(self, server_id, server):
+ return self.api_put('/servers/%s' % server_id, server)
+
def post_server_action(self, server_id, data):
return self.api_post('/servers/%s/action' % server_id, data)
diff --git a/nova/tests/integrated/test_servers.py b/nova/tests/integrated/test_servers.py
index 0c8d6c313..33ffa7cf1 100644
--- a/nova/tests/integrated/test_servers.py
+++ b/nova/tests/integrated/test_servers.py
@@ -501,6 +501,25 @@ class ServersTest(integrated_helpers._IntegratedTestBase):
# Cleanup
self._delete_server(created_server_id)
+ def test_rename_server(self):
+ """Test building and renaming a server."""
+
+ # Create a server
+ server = self._build_minimal_create_server_request()
+ created_server = self.api.post_server({'server': server})
+ LOG.debug("created_server: %s" % created_server)
+ server_id = created_server['id']
+ self.assertTrue(server_id)
+
+ # Rename the server to 'new-name'
+ self.api.put_server(server_id, {'server': {'name': 'new-name'}})
+
+ # Check the name of the server
+ created_server = self.api.get_server(server_id)
+ self.assertEqual(created_server['name'], 'new-name')
+
+ # Cleanup
+ self._delete_server(server_id)
if __name__ == "__main__":
unittest.main()
diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py
index 56718f8e8..c332c27b0 100644
--- a/nova/virt/xenapi/vmops.py
+++ b/nova/virt/xenapi/vmops.py
@@ -597,7 +597,9 @@ class VMOps(object):
# No response from the agent
return
resp_dict = json.loads(resp)
- return resp_dict['message']
+ # Some old versions of the Windows agent have a trailing \\r\\n
+ # (ie CRLF escaped) for some reason. Strip that off.
+ return resp_dict['message'].replace('\\r\\n', '')
if timeout:
vm_ref = self._get_vm_opaque_ref(instance)
@@ -662,9 +664,13 @@ class VMOps(object):
# There was some sort of error; the message will contain
# a description of the error.
raise RuntimeError(resp_dict['message'])
- agent_pub = int(resp_dict['message'])
+ # Some old versions of the Windows agent have a trailing \\r\\n
+ # (ie CRLF escaped) for some reason. Strip that off.
+ agent_pub = int(resp_dict['message'].replace('\\r\\n', ''))
dh.compute_shared(agent_pub)
- enc_pass = dh.encrypt(new_pass)
+ # Some old versions of Linux and Windows agent expect trailing \n
+ # on password to work correctly.
+ enc_pass = dh.encrypt(new_pass + '\n')
# Send the encrypted password
password_transaction_id = str(uuid.uuid4())
password_args = {'id': password_transaction_id, 'enc_pass': enc_pass}