diff options
| author | Tushar Patil <tushar.vitthal.patil@gmail.com> | 2011-07-18 12:19:31 -0700 |
|---|---|---|
| committer | Tushar Patil <tushar.vitthal.patil@gmail.com> | 2011-07-18 12:19:31 -0700 |
| commit | 6d410105828c4dbfa11df5dd146b3b2591a24409 (patch) | |
| tree | 458dc5ce08f531df75c23c2b960fa82435daf877 | |
| parent | b4fba58f2785936f68a712d1cdd1d5c34f6a7c22 (diff) | |
| parent | cf25ab33cb7d6b5e233a767ad96b3c45b1387b5e (diff) | |
Merged with trunk
| -rw-r--r-- | Authors | 1 | ||||
| -rw-r--r-- | doc/source/devref/index.rst | 7 | ||||
| -rw-r--r-- | doc/source/devref/multinic.rst | 4 | ||||
| -rw-r--r-- | nova/api/openstack/__init__.py | 12 | ||||
| -rw-r--r-- | nova/api/openstack/common.py | 13 | ||||
| -rw-r--r-- | nova/api/openstack/ips.py | 79 | ||||
| -rw-r--r-- | nova/api/openstack/limits.py | 108 | ||||
| -rw-r--r-- | nova/api/openstack/servers.py | 26 | ||||
| -rw-r--r-- | nova/api/openstack/views/addresses.py | 39 | ||||
| -rw-r--r-- | nova/api/openstack/views/images.py | 18 | ||||
| -rw-r--r-- | nova/api/openstack/views/servers.py | 13 | ||||
| -rw-r--r-- | nova/db/sqlalchemy/api.py | 24 | ||||
| -rw-r--r-- | nova/tests/api/openstack/test_common.py | 24 | ||||
| -rw-r--r-- | nova/tests/api/openstack/test_images.py | 14 | ||||
| -rw-r--r-- | nova/tests/api/openstack/test_limits.py | 96 | ||||
| -rw-r--r-- | nova/tests/api/openstack/test_servers.py | 195 | ||||
| -rw-r--r-- | nova/tests/integrated/api/client.py | 14 | ||||
| -rw-r--r-- | nova/tests/integrated/test_servers.py | 19 | ||||
| -rw-r--r-- | nova/virt/xenapi/vmops.py | 12 | ||||
| -rwxr-xr-x | plugins/xenserver/xenapi/etc/xapi.d/plugins/agent | 2 |
20 files changed, 620 insertions, 100 deletions
@@ -87,6 +87,7 @@ Sandy Walsh <sandy.walsh@rackspace.com> Sateesh Chodapuneedi <sateesh.chodapuneedi@citrix.com> Scott Moser <smoser@ubuntu.com> Soren Hansen <soren.hansen@rackspace.com> +Stephanie Reese <reese.sm@gmail.com> Thierry Carrez <thierry@openstack.org> Todd Willey <todd@ansolabs.com> Trey Morris <trey.morris@rackspace.com> diff --git a/doc/source/devref/index.rst b/doc/source/devref/index.rst index 0a5a7a4d6..859d4e331 100644 --- a/doc/source/devref/index.rst +++ b/doc/source/devref/index.rst @@ -30,13 +30,16 @@ Programming HowTos and Tutorials addmethod.openstackapi -Programming Concepts --------------------- +Background Concepts for Nova +---------------------------- .. toctree:: :maxdepth: 3 + distributed_scheduler + multinic zone rabbit + API Reference ------------- diff --git a/doc/source/devref/multinic.rst b/doc/source/devref/multinic.rst index b3a82d341..43830258f 100644 --- a/doc/source/devref/multinic.rst +++ b/doc/source/devref/multinic.rst @@ -29,11 +29,11 @@ FlatDHCP Manager .. image:: /images/multinic_dhcp.png -FlatDHCP manager builds on the the Flat manager adding dnsmask (DNS and DHCP) and radvd (Router Advertisement) servers on the bridge for that network. The services run on the host that is assigned to that nework. The FlatDHCP manager will create its bridge as specified when the network was created on the network-host when the network host starts up or when a new network gets allocated to that host. Compute nodes will also create the bridges as necessary and connect instance VIFs to them. +FlatDHCP manager builds on the the Flat manager adding dnsmask (DNS and DHCP) and radvd (Router Advertisement) servers on the bridge for that network. The services run on the host that is assigned to that network. The FlatDHCP manager will create its bridge as specified when the network was created on the network-host when the network host starts up or when a new network gets allocated to that host. Compute nodes will also create the bridges as necessary and connect instance VIFs to them. VLAN Manager ------------ .. image:: /images/multinic_vlan.png -The VLAN manager sets up forwarding to/from a cloudpipe instance in addition to providing dnsmask (DNS and DHCP) and radvd (Router Advertisement) services for each network. The manager will create its bridge as specified when the network was created on the network-host when the network host starts up or when a new network gets allocated to that host. Compute nodes will also create the bridges as necessary and conenct instance VIFs to them. +The VLAN manager sets up forwarding to/from a cloudpipe instance in addition to providing dnsmask (DNS and DHCP) and radvd (Router Advertisement) services for each network. The manager will create its bridge as specified when the network was created on the network-host when the network host starts up or when a new network gets allocated to that host. Compute nodes will also create the bridges as necessary and connect instance VIFs to them. 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} diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent index b8a1b936a..68d7e7bff 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/agent @@ -127,7 +127,7 @@ def inject_file(self, arg_dict): been disabled, and raise a NotImplemented error if that is the case. """ b64_path = arg_dict["b64_path"] - b64_file = arg_dict["b64_file"] + b64_file = arg_dict["b64_contents"] request_id = arg_dict["id"] if self._agent_has_method("file_inject"): # New version of the agent. Agent should receive a 'value' |
