diff options
| author | Sandy Walsh <sandy.walsh@rackspace.com> | 2011-05-05 07:41:26 -0700 |
|---|---|---|
| committer | Sandy Walsh <sandy.walsh@rackspace.com> | 2011-05-05 07:41:26 -0700 |
| commit | 51f59b4e86be15d1fac62d3a48ae72e930cf1301 (patch) | |
| tree | 5a6bf71b165403b500b2fef5f27a871067574eeb | |
| parent | 37954e665b874ac2358921d175b30617f456c007 (diff) | |
| parent | 114a55d0243f79db7ea1ef29830a9428dbf1aa33 (diff) | |
| download | nova-51f59b4e86be15d1fac62d3a48ae72e930cf1301.tar.gz nova-51f59b4e86be15d1fac62d3a48ae72e930cf1301.tar.xz nova-51f59b4e86be15d1fac62d3a48ae72e930cf1301.zip | |
merge with trunk
| -rw-r--r-- | Authors | 2 | ||||
| -rwxr-xr-x | bin/nova-manage | 2 | ||||
| -rw-r--r-- | doc/source/_theme/layout.html | 2 | ||||
| -rw-r--r-- | nova/api/openstack/__init__.py | 9 | ||||
| -rw-r--r-- | nova/api/openstack/common.py | 9 | ||||
| -rw-r--r-- | nova/api/openstack/images.py | 2 | ||||
| -rw-r--r-- | nova/api/openstack/limits.py | 31 | ||||
| -rw-r--r-- | nova/api/openstack/servers.py | 94 | ||||
| -rw-r--r-- | nova/api/openstack/views/images.py | 17 | ||||
| -rw-r--r-- | nova/api/openstack/views/limits.py | 100 | ||||
| -rw-r--r-- | nova/api/openstack/views/servers.py | 8 | ||||
| -rw-r--r-- | nova/compute/api.py | 28 | ||||
| -rw-r--r-- | nova/compute/manager.py | 87 | ||||
| -rw-r--r-- | nova/compute/power_state.py | 21 | ||||
| -rw-r--r-- | nova/exception.py | 14 | ||||
| -rw-r--r-- | nova/network/linux_net.py | 11 | ||||
| -rw-r--r-- | nova/tests/api/openstack/test_images.py | 12 | ||||
| -rw-r--r-- | nova/tests/api/openstack/test_limits.py | 121 | ||||
| -rw-r--r-- | nova/tests/api/openstack/test_servers.py | 224 | ||||
| -rw-r--r-- | nova/tests/test_exception.py | 34 | ||||
| -rw-r--r-- | nova/tests/test_misc.py | 49 | ||||
| -rw-r--r-- | nova/tests/test_virt.py | 26 | ||||
| -rw-r--r-- | nova/virt/libvirt_conn.py | 11 | ||||
| -rw-r--r-- | nova/virt/xenapi/vmops.py | 8 | ||||
| -rwxr-xr-x | run_tests.sh | 1 |
25 files changed, 787 insertions, 136 deletions
@@ -1,3 +1,4 @@ +Alex Meade <alex.meade@rackspace.com> Andy Smith <code@term.ie> Andy Southgate <andy.southgate@citrix.com> Anne Gentle <anne@openstack.org> @@ -45,6 +46,7 @@ Joshua McKenty <jmckenty@gmail.com> Justin Santa Barbara <justin@fathomdb.com> Kei Masumoto <masumotok@nttdata.co.jp> Ken Pepple <ken.pepple@gmail.com> +Kevin Bringard <kbringard@attinteractive.com> Kevin L. Mitchell <kevin.mitchell@rackspace.com> Koji Iida <iida.koji@lab.ntt.co.jp> Lorin Hochstein <lorin@isi.edu> diff --git a/bin/nova-manage b/bin/nova-manage index 3dff16f27..2f6af6e2d 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -151,7 +151,7 @@ class VpnCommands(object): state = 'up' print address, print vpn['host'], - print vpn['ec2_id'], + print ec2utils.id_to_ec2_id(vpn['id']), print vpn['state_description'], print state else: diff --git a/doc/source/_theme/layout.html b/doc/source/_theme/layout.html index 0a37a7943..b28edb364 100644 --- a/doc/source/_theme/layout.html +++ b/doc/source/_theme/layout.html @@ -73,7 +73,7 @@ <script type="text/javascript">$('#searchbox').show(0);</script> <p class="triangle-border right"> - Psst... hey. You're reading the latest content, but it might be out of sync with code. You can read <a href="http://nova.openstack.org/2011.1">Nova 2011.1 docs</a> or <a href="http://docs.openstack.org">all OpenStack docs</a> too. + Psst... hey. You're reading the latest content, but it might be out of sync with code. You can read <a href="http://nova.openstack.org/2011.2">Nova 2011.2 docs</a> or <a href="http://docs.openstack.org">all OpenStack docs</a> too. </p> {%- endif %} diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 5e76a06f7..348b70d5b 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -112,9 +112,6 @@ class APIRouter(wsgi.Router): parent_resource=dict(member_name='server', collection_name='servers')) - _limits = limits.LimitsController() - mapper.resource("limit", "limits", controller=_limits) - super(APIRouter, self).__init__(mapper) @@ -145,6 +142,9 @@ class APIRouterV10(APIRouter): parent_resource=dict(member_name='server', collection_name='servers')) + mapper.resource("limit", "limits", + controller=limits.LimitsControllerV10()) + mapper.resource("ip", "ips", controller=ips.Controller(), collection=dict(public='GET', private='GET'), parent_resource=dict(member_name='server', @@ -178,3 +178,6 @@ class APIRouterV11(APIRouter): mapper.resource("flavor", "flavors", controller=flavors.ControllerV11(), collection={'detail': 'GET'}) + + mapper.resource("limit", "limits", + controller=limits.LimitsControllerV11()) diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 65ed1e143..32cd689ca 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -15,6 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. +import re from urlparse import urlparse import webob @@ -130,10 +131,16 @@ def get_image_id_from_image_hash(image_service, context, 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 + Given: 'http://www.foo.com/bar/123?q=4' + Returns: 123 + + In order to support local hrefs, the href argument can be just an id: + Given: '123' Returns: 123 """ + if re.match(r'\d+$', str(href)): + return int(href) try: return int(urlparse(href).path.split('/')[-1]) except: diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 77baf5947..34d4c27fc 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -127,7 +127,7 @@ class Controller(common.OpenstackController): raise webob.exc.HTTPBadRequest() image = self._compute_service.snapshot(context, server_id, image_name) - return self.get_builder(req).build(image, detail=True) + return dict(image=self.get_builder(req).build(image, detail=True)) def get_builder(self, request): """Indicates that you must use a Controller subclass.""" diff --git a/nova/api/openstack/limits.py b/nova/api/openstack/limits.py index 9877af191..47bc238f1 100644 --- a/nova/api/openstack/limits.py +++ b/nova/api/openstack/limits.py @@ -33,7 +33,7 @@ from webob.dec import wsgify from nova import wsgi from nova.api.openstack import common from nova.api.openstack import faults -from nova.wsgi import Middleware +from nova.api.openstack.views import limits as limits_views # Convenience constants for the limits dictionary passed to Limiter(). @@ -51,8 +51,8 @@ class LimitsController(common.OpenstackController): _serialization_metadata = { "application/xml": { "attributes": { - "limit": ["verb", "URI", "regex", "value", "unit", - "resetTime", "remaining", "name"], + "limit": ["verb", "URI", "uri", "regex", "value", "unit", + "resetTime", "next-available", "remaining", "name"], }, "plurals": { "rate": "limit", @@ -67,12 +67,21 @@ class LimitsController(common.OpenstackController): abs_limits = {} rate_limits = req.environ.get("nova.limits", []) - return { - "limits": { - "rate": rate_limits, - "absolute": abs_limits, - }, - } + builder = self._get_view_builder(req) + return builder.build(rate_limits, abs_limits) + + def _get_view_builder(self, req): + raise NotImplementedError() + + +class LimitsControllerV10(LimitsController): + def _get_view_builder(self, req): + return limits_views.ViewBuilderV10() + + +class LimitsControllerV11(LimitsController): + def _get_view_builder(self, req): + return limits_views.ViewBuilderV11() class Limit(object): @@ -186,7 +195,7 @@ DEFAULT_LIMITS = [ ] -class RateLimitingMiddleware(Middleware): +class RateLimitingMiddleware(wsgi.Middleware): """ Rate-limits requests passing through this middleware. All limit information is stored in memory for this implementation. @@ -200,7 +209,7 @@ class RateLimitingMiddleware(Middleware): @param application: WSGI application to wrap @param limits: List of dictionaries describing limits """ - Middleware.__init__(self, application) + wsgi.Middleware.__init__(self, application) self._limiter = Limiter(limits or DEFAULT_LIMITS) @wsgify(RequestClass=wsgi.Request) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 9b883f06b..3cf78e32c 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -14,28 +14,25 @@ # under the License. import base64 -import hashlib import traceback 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 from nova import quota 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.images 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 from nova.scheduler import api as scheduler_api @@ -320,10 +317,6 @@ class Controller(common.OpenstackController): return faults.Fault(exc.HTTPBadRequest()) return exc.HTTPAccepted() - def _action_rebuild(self, input_dict, req, id): - LOG.debug(_("Rebuild server action is not implemented")) - return faults.Fault(exc.HTTPNotImplemented()) - def _action_resize(self, input_dict, req, id): """ Resizes a given instance to the flavor size requested """ try: @@ -595,9 +588,6 @@ class ControllerV10(Controller): return nova.api.openstack.views.servers.ViewBuilderV10( addresses_builder) - def _get_addresses_view_builder(self, req): - return nova.api.openstack.views.addresses.ViewBuilderV10(req) - def _limit_items(self, items, req): return common.limited(items, req) @@ -609,6 +599,28 @@ class ControllerV10(Controller): except exception.TimeoutException: return exc.HTTPRequestTimeout() + def _action_rebuild(self, info, request, instance_id): + context = request.environ['nova.context'] + instance_id = int(instance_id) + + try: + image_id = info["rebuild"]["imageId"] + except (KeyError, TypeError): + msg = _("Could not parse imageId from request.") + LOG.debug(msg) + return faults.Fault(exc.HTTPBadRequest(explanation=msg)) + + try: + self.compute_api.rebuild(context, instance_id, image_id) + except exception.BuildInProgress: + msg = _("Instance %d is currently being rebuilt.") % instance_id + LOG.debug(msg) + return faults.Fault(exc.HTTPConflict(explanation=msg)) + + response = exc.HTTPAccepted() + response.empty_body = True + return response + class ControllerV11(Controller): def _image_id_from_req_data(self, data): @@ -629,9 +641,6 @@ class ControllerV11(Controller): return nova.api.openstack.views.servers.ViewBuilderV11( addresses_builder, flavor_builder, image_builder, base_url) - def _get_addresses_view_builder(self, req): - return nova.api.openstack.views.addresses.ViewBuilderV11(req) - def _action_change_password(self, input_dict, req, id): context = req.environ['nova.context'] if (not 'changePassword' in input_dict @@ -648,6 +657,63 @@ class ControllerV11(Controller): def _limit_items(self, items, req): return common.limited_by_marker(items, req) + def _validate_metadata(self, metadata): + """Ensure that we can work with the metadata given.""" + try: + metadata.iteritems() + except AttributeError as ex: + msg = _("Unable to parse metadata key/value pairs.") + LOG.debug(msg) + raise faults.Fault(exc.HTTPBadRequest(explanation=msg)) + + def _decode_personalities(self, personalities): + """Decode the Base64-encoded personalities.""" + for personality in personalities: + try: + path = personality["path"] + contents = personality["contents"] + except (KeyError, TypeError): + msg = _("Unable to parse personality path/contents.") + LOG.info(msg) + raise faults.Fault(exc.HTTPBadRequest(explanation=msg)) + + try: + personality["contents"] = base64.b64decode(contents) + except TypeError: + msg = _("Personality content could not be Base64 decoded.") + LOG.info(msg) + raise faults.Fault(exc.HTTPBadRequest(explanation=msg)) + + def _action_rebuild(self, info, request, instance_id): + context = request.environ['nova.context'] + instance_id = int(instance_id) + + try: + image_ref = info["rebuild"]["imageRef"] + except (KeyError, TypeError): + msg = _("Could not parse imageRef from request.") + LOG.debug(msg) + return faults.Fault(exc.HTTPBadRequest(explanation=msg)) + + image_id = common.get_id_from_href(image_ref) + personalities = info["rebuild"].get("personality", []) + metadata = info["rebuild"].get("metadata", {}) + + self._validate_metadata(metadata) + self._decode_personalities(personalities) + + try: + self.compute_api.rebuild(context, instance_id, image_id, metadata, + personalities) + except exception.BuildInProgress: + msg = _("Instance %d is currently being rebuilt.") % instance_id + LOG.debug(msg) + return faults.Fault(exc.HTTPConflict(explanation=msg)) + + response = exc.HTTPAccepted() + response.empty_body = True + return response + def _get_server_admin_password(self, server): """ Determine the admin password for a server on creation """ password = server.get('adminPass') diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index 9dec8a355..2773c9c13 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -46,6 +46,14 @@ class ViewBuilder(object): except KeyError: image['status'] = image['status'].upper() + def _build_server(self, image, instance_id): + """Indicates that you must use a ViewBuilder subclass.""" + raise NotImplementedError + + def generate_server_ref(self, server_id): + """Return an href string pointing to this server.""" + return os.path.join(self._url, "servers", str(server_id)) + def generate_href(self, image_id): """Return an href string pointing to this object.""" return os.path.join(self._url, "images", str(image_id)) @@ -66,7 +74,7 @@ class ViewBuilder(object): if "instance_id" in properties: try: - image["serverId"] = int(properties["instance_id"]) + self._build_server(image, int(properties["instance_id"])) except ValueError: pass @@ -85,12 +93,17 @@ class ViewBuilder(object): class ViewBuilderV10(ViewBuilder): """OpenStack API v1.0 Image Builder""" - pass + + def _build_server(self, image, instance_id): + image["serverId"] = instance_id class ViewBuilderV11(ViewBuilder): """OpenStack API v1.1 Image Builder""" + def _build_server(self, image, instance_id): + image["serverRef"] = self.generate_server_ref(instance_id) + def build(self, image_obj, detail=False): """Return a standardized image structure for display by the API.""" image = ViewBuilder.build(self, image_obj, detail) diff --git a/nova/api/openstack/views/limits.py b/nova/api/openstack/views/limits.py new file mode 100644 index 000000000..552db39ee --- /dev/null +++ b/nova/api/openstack/views/limits.py @@ -0,0 +1,100 @@ +# 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 time + +from nova.api.openstack import common + + +class ViewBuilder(object): + """Openstack API base limits view builder.""" + + def build(self, rate_limits, absolute_limits): + rate_limits = self._build_rate_limits(rate_limits) + absolute_limits = self._build_absolute_limits(absolute_limits) + + output = { + "limits": { + "rate": rate_limits, + "absolute": absolute_limits, + }, + } + + return output + + +class ViewBuilderV10(ViewBuilder): + """Openstack API v1.0 limits view builder.""" + + def _build_rate_limits(self, rate_limits): + return [self._build_rate_limit(r) for r in rate_limits] + + def _build_rate_limit(self, rate_limit): + return { + "verb": rate_limit["verb"], + "URI": rate_limit["URI"], + "regex": rate_limit["regex"], + "value": rate_limit["value"], + "remaining": int(rate_limit["remaining"]), + "unit": rate_limit["unit"], + "resetTime": rate_limit["resetTime"], + } + + def _build_absolute_limits(self, absolute_limit): + return {} + + +class ViewBuilderV11(ViewBuilder): + """Openstack API v1.1 limits view builder.""" + + def _build_rate_limits(self, rate_limits): + limits = [] + for rate_limit in rate_limits: + _rate_limit_key = None + _rate_limit = self._build_rate_limit(rate_limit) + + # check for existing key + for limit in limits: + if limit["uri"] == rate_limit["URI"] and \ + limit["regex"] == limit["regex"]: + _rate_limit_key = limit + break + + # ensure we have a key if we didn't find one + if not _rate_limit_key: + _rate_limit_key = { + "uri": rate_limit["URI"], + "regex": rate_limit["regex"], + "limit": [], + } + limits.append(_rate_limit_key) + + _rate_limit_key["limit"].append(_rate_limit) + + return limits + + def _build_rate_limit(self, rate_limit): + return { + "verb": rate_limit["verb"], + "value": rate_limit["value"], + "remaining": int(rate_limit["remaining"]), + "unit": rate_limit["unit"], + "next-available": rate_limit["resetTime"], + } + + def _build_absolute_limits(self, absolute_limit): + return {} diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index e52bfaea3..0be468edc 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -63,10 +63,12 @@ class ViewBuilder(object): power_state.BLOCKED: 'ACTIVE', power_state.SUSPENDED: 'SUSPENDED', power_state.PAUSED: 'PAUSED', - power_state.SHUTDOWN: 'ACTIVE', - power_state.SHUTOFF: 'ACTIVE', + power_state.SHUTDOWN: 'SHUTDOWN', + power_state.SHUTOFF: 'SHUTOFF', power_state.CRASHED: 'ERROR', - power_state.FAILED: 'ERROR'} + power_state.FAILED: 'ERROR', + power_state.BUILDING: 'BUILD', + } inst_dict = { 'id': int(inst['id']), diff --git a/nova/compute/api.py b/nova/compute/api.py index c85f0f53a..be26d8ca3 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -32,6 +32,7 @@ from nova import rpc from nova import utils from nova import volume from nova.compute import instance_types +from nova.compute import power_state from nova.scheduler import api as scheduler_api from nova.db import base @@ -501,6 +502,33 @@ class API(base.Base): """Reboot the given instance.""" self._cast_compute_message('reboot_instance', context, instance_id) + def rebuild(self, context, instance_id, image_id, metadata=None, + files_to_inject=None): + """Rebuild the given instance with the provided metadata.""" + instance = db.api.instance_get(context, instance_id) + + if instance["state"] == power_state.BUILDING: + msg = _("Instance already building") + raise exception.BuildInProgress(msg) + + metadata = metadata or {} + self._check_metadata_properties_quota(context, metadata) + + files_to_inject = files_to_inject or [] + self._check_injected_file_quota(context, files_to_inject) + + self.db.instance_update(context, instance_id, {"metadata": metadata}) + + rebuild_params = { + "image_id": image_id, + "injected_files": files_to_inject, + } + + self._cast_compute_message('rebuild_instance', + context, + instance_id, + params=rebuild_params) + def revert_resize(self, context, instance_id): """Reverts a resize, deleting the 'new' instance in the process.""" context = context.elevated() diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 61564cb7d..1ff78007b 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -137,17 +137,33 @@ class ComputeManager(manager.SchedulerDependentManager): """Initialization for a standalone compute service.""" self.driver.init_host(host=self.host) - def _update_state(self, context, instance_id): + def _update_state(self, context, instance_id, state=None): """Update the state of an instance from the driver info.""" - # FIXME(ja): include other fields from state? instance_ref = self.db.instance_get(context, instance_id) - try: - info = self.driver.get_info(instance_ref['name']) - state = info['state'] - except exception.NotFound: - state = power_state.FAILED + + if state is None: + try: + info = self.driver.get_info(instance_ref['name']) + except exception.NotFound: + info = None + + if info is not None: + state = info['state'] + else: + state = power_state.FAILED + self.db.instance_set_state(context, instance_id, state) + def _update_launched_at(self, context, instance_id, launched_at=None): + """Update the launched_at parameter of the given instance.""" + data = {'launched_at': launched_at or datetime.datetime.utcnow()} + self.db.instance_update(context, instance_id, data) + + def _update_image_id(self, context, instance_id, image_id): + """Update the image_id for the given instance.""" + data = {'image_id': image_id} + self.db.instance_update(context, instance_id, data) + def get_console_topic(self, context, **kwargs): """Retrieves the console host for a project on this host. @@ -231,24 +247,15 @@ class ComputeManager(manager.SchedulerDependentManager): instance_id) # TODO(vish) check to make sure the availability zone matches - self.db.instance_set_state(context, - instance_id, - power_state.NOSTATE, - 'spawning') + self._update_state(context, instance_id, power_state.BUILDING) try: self.driver.spawn(instance_ref) - now = datetime.datetime.utcnow() - self.db.instance_update(context, - instance_id, - {'launched_at': now}) - 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) + except Exception as ex: # pylint: disable=W0702 + msg = _("Instance '%(instance_id)s' failed to spawn. Is " + "virtualization enabled in the BIOS? Details: " + "%(ex)s") % locals() + LOG.exception(msg) if not FLAGS.stub_network and FLAGS.auto_assign_floating_ip: public_ip = self.network_api.allocate_floating_ip(context) @@ -262,6 +269,8 @@ class ComputeManager(manager.SchedulerDependentManager): floating_ip, fixed_ip, affect_auto_assigned=True) + + self._update_launched_at(context, instance_id) self._update_state(context, instance_id) @exception.wrap_exception @@ -318,6 +327,33 @@ class ComputeManager(manager.SchedulerDependentManager): @exception.wrap_exception @checks_instance_lock + def rebuild_instance(self, context, instance_id, image_id): + """Destroy and re-make this instance. + + A 'rebuild' effectively purges all existing data from the system and + remakes the VM with given 'metadata' and 'personalities'. + + :param context: `nova.RequestContext` object + :param instance_id: Instance identifier (integer) + :param image_id: Image identifier (integer) + """ + context = context.elevated() + + instance_ref = self.db.instance_get(context, instance_id) + LOG.audit(_("Rebuilding instance %s"), instance_id, context=context) + + self._update_state(context, instance_id, power_state.BUILDING) + + self.driver.destroy(instance_ref) + instance_ref.image_id = image_id + self.driver.spawn(instance_ref) + + self._update_image_id(context, instance_id, image_id) + self._update_launched_at(context, instance_id) + self._update_state(context, instance_id) + + @exception.wrap_exception + @checks_instance_lock def reboot_instance(self, context, instance_id): """Reboot an instance on this host.""" context = context.elevated() @@ -1072,8 +1108,7 @@ class ComputeManager(manager.SchedulerDependentManager): if vm_instance is None: # NOTE(justinsb): We have to be very careful here, because a # concurrent operation could be in progress (e.g. a spawn) - if db_state == power_state.NOSTATE: - # Assume that NOSTATE => spawning + if db_state == power_state.BUILDING: # TODO(justinsb): This does mean that if we crash during a # spawn, the machine will never leave the spawning state, # but this is just the way nova is; this function isn't @@ -1104,9 +1139,7 @@ class ComputeManager(manager.SchedulerDependentManager): 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) + self._update_state(context, db_instance['id'], vm_state) # NOTE(justinsb): We no longer auto-remove SHUTOFF instances # It's quite hard to get them back when we do. diff --git a/nova/compute/power_state.py b/nova/compute/power_state.py index ef013b2ef..c468fe6b3 100644 --- a/nova/compute/power_state.py +++ b/nova/compute/power_state.py @@ -30,20 +30,23 @@ SHUTOFF = 0x05 CRASHED = 0x06 SUSPENDED = 0x07 FAILED = 0x08 +BUILDING = 0x09 # 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', - PAUSED: 'paused', - SHUTDOWN: 'shutdown', - SHUTOFF: 'shutdown', - CRASHED: 'crashed', - SUSPENDED: 'suspended', - FAILED: 'failed to spawn'} + NOSTATE: 'pending', + RUNNING: 'running', + BLOCKED: 'blocked', + PAUSED: 'paused', + SHUTDOWN: 'shutdown', + SHUTOFF: 'shutdown', + CRASHED: 'crashed', + SUSPENDED: 'suspended', + FAILED: 'failed to spawn', + BUILDING: 'building', +} def name(code): diff --git a/nova/exception.py b/nova/exception.py index 586ec009b..eeeed7952 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -49,10 +49,18 @@ class Error(Exception): class ApiError(Error): - def __init__(self, message='Unknown', code='ApiError'): - self.message = message + def __init__(self, message='Unknown', code=None): + self.msg = message self.code = code - super(ApiError, self).__init__('%s: %s' % (code, message)) + if code: + outstr = '%s: %s' % (code, message) + else: + outstr = '%s' % message + super(ApiError, self).__init__(outstr) + + +class BuildInProgress(Error): + pass class DBError(Error): diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index ec5579dee..b50a4b4ea 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -56,13 +56,12 @@ flags.DEFINE_string('input_chain', 'INPUT', 'chain to add nova_input to') flags.DEFINE_integer('dhcp_lease_time', 120, 'Lifetime of a DHCP lease') - flags.DEFINE_string('dns_server', None, 'if set, uses specific dns server for dnsmasq') flags.DEFINE_string('dmz_cidr', '10.128.0.0/24', 'dmz range that should be accepted') - - +flags.DEFINE_string('dnsmasq_config_file', "", + 'Override the default dnsmasq settings with this file') binary_name = os.path.basename(inspect.stack()[-1][1]) @@ -407,6 +406,10 @@ def ensure_vlan_forward(public_ip, port, private_ip): "-d %s -p udp " "--dport %s -j DNAT --to %s:1194" % (public_ip, port, private_ip)) + iptables_manager.ipv4['nat'].add_rule("OUTPUT", + "-d %s -p udp " + "--dport %s -j DNAT --to %s:1194" % + (public_ip, port, private_ip)) iptables_manager.apply() @@ -678,7 +681,7 @@ def _dnsmasq_cmd(net): cmd = ['sudo', '-E', 'dnsmasq', '--strict-order', '--bind-interfaces', - '--conf-file=', + '--conf-file=%s' % FLAGS.dnsmasq_config_file, '--domain=%s' % FLAGS.dhcp_domain, '--pid-file=%s' % _dhcp_file(net['bridge'], 'pid'), '--listen-address=%s' % net['gateway'], diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index ae86d0686..e5dd93c3f 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -538,7 +538,8 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): }, { 'id': 127, - 'name': 'killed backup', 'serverId': 42, + 'name': 'killed backup', + 'serverId': 42, 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'FAILED', @@ -584,7 +585,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): { 'id': 124, 'name': 'queued backup', - 'serverId': 42, + 'serverRef': "http://localhost/v1.1/servers/42", 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'QUEUED', @@ -606,7 +607,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): { 'id': 125, 'name': 'saving backup', - 'serverId': 42, + 'serverRef': "http://localhost/v1.1/servers/42", 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'SAVING', @@ -629,7 +630,7 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): { 'id': 126, 'name': 'active backup', - 'serverId': 42, + 'serverRef': "http://localhost/v1.1/servers/42", 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'ACTIVE', @@ -650,7 +651,8 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): }, { 'id': 127, - 'name': 'killed backup', 'serverId': 42, + 'name': 'killed backup', + 'serverRef': "http://localhost/v1.1/servers/42", 'updated': self.NOW_API_FORMAT, 'created': self.NOW_API_FORMAT, 'status': 'FAILED', diff --git a/nova/tests/api/openstack/test_limits.py b/nova/tests/api/openstack/test_limits.py index df367005d..45bd4d501 100644 --- a/nova/tests/api/openstack/test_limits.py +++ b/nova/tests/api/openstack/test_limits.py @@ -28,15 +28,14 @@ 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), + limits.Limit("GET", "/delayed", "^/delayed", 1, limits.PER_MINUTE), + limits.Limit("POST", "*", ".*", 7, limits.PER_MINUTE), + limits.Limit("POST", "/servers", "^/servers", 3, limits.PER_MINUTE), + limits.Limit("PUT", "*", "", 10, limits.PER_MINUTE), + limits.Limit("PUT", "/servers", "^/servers", 5, limits.PER_MINUTE), ] @@ -58,15 +57,15 @@ class BaseLimitTestSuite(unittest.TestCase): return self.time -class LimitsControllerTest(BaseLimitTestSuite): +class LimitsControllerV10Test(BaseLimitTestSuite): """ - Tests for `limits.LimitsController` class. + Tests for `limits.LimitsControllerV10` class. """ def setUp(self): """Run before each test.""" BaseLimitTestSuite.setUp(self) - self.controller = limits.LimitsController() + self.controller = limits.LimitsControllerV10() def _get_index_request(self, accept_header="application/json"): """Helper to set routing arguments.""" @@ -81,8 +80,8 @@ class LimitsControllerTest(BaseLimitTestSuite): def _populate_limits(self, request): """Put limit info into a request.""" _limits = [ - Limit("GET", "*", ".*", 10, 60).display(), - Limit("POST", "*", ".*", 5, 60 * 60).display(), + limits.Limit("GET", "*", ".*", 10, 60).display(), + limits.Limit("POST", "*", ".*", 5, 60 * 60).display(), ] request.environ["nova.limits"] = _limits return request @@ -171,6 +170,100 @@ class LimitsControllerTest(BaseLimitTestSuite): self.assertEqual(expected.toxml(), body.toxml()) +class LimitsControllerV11Test(BaseLimitTestSuite): + """ + Tests for `limits.LimitsControllerV11` class. + """ + + def setUp(self): + """Run before each test.""" + BaseLimitTestSuite.setUp(self) + self.controller = limits.LimitsControllerV11() + + 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 = [ + limits.Limit("GET", "*", ".*", 10, 60).display(), + limits.Limit("POST", "*", ".*", 5, 60 * 60).display(), + limits.Limit("GET", "changes-since*", "changes-since", + 5, 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": ".*", + "uri": "*", + "limit": [ + { + "verb": "GET", + "next-available": 0, + "unit": "MINUTE", + "value": 10, + "remaining": 10, + }, + { + "verb": "POST", + "next-available": 0, + "unit": "HOUR", + "value": 5, + "remaining": 5, + }, + ], + }, + { + "regex": "changes-since", + "uri": "changes-since*", + "limit": [ + { + "verb": "GET", + "next-available": 0, + "unit": "MINUTE", + "value": 5, + "remaining": 5, + }, + ], + }, + + ], + "absolute": {}, + }, + } + body = json.loads(response.body) + self.assertEqual(expected, body) + + class LimitMiddlewareTest(BaseLimitTestSuite): """ Tests for the `limits.RateLimitingMiddleware` class. @@ -185,7 +278,7 @@ class LimitMiddlewareTest(BaseLimitTestSuite): """Prepare middleware for use through fake WSGI app.""" BaseLimitTestSuite.setUp(self) _limits = [ - Limit("GET", "*", ".*", 1, 60), + limits.Limit("GET", "*", ".*", 1, 60), ] self.app = limits.RateLimitingMiddleware(self._empty_app, _limits) @@ -238,7 +331,7 @@ class LimitTest(BaseLimitTestSuite): def test_GET_no_delay(self): """Test a limit handles 1 GET per second.""" - limit = Limit("GET", "*", ".*", 1, 1) + limit = limits.Limit("GET", "*", ".*", 1, 1) delay = limit("GET", "/anything") self.assertEqual(None, delay) self.assertEqual(0, limit.next_request) @@ -246,7 +339,7 @@ class LimitTest(BaseLimitTestSuite): def test_GET_delay(self): """Test two calls to 1 GET per second limit.""" - limit = Limit("GET", "*", ".*", 1, 1) + limit = limits.Limit("GET", "*", ".*", 1, 1) delay = limit("GET", "/anything") self.assertEqual(None, delay) diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 556046e9d..5c643fcef 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -33,6 +33,7 @@ import nova.api.openstack from nova.api.openstack import servers import nova.compute.api from nova.compute import instance_types +from nova.compute import power_state import nova.db.api from nova.db.sqlalchemy.models import Instance from nova.db.sqlalchemy.models import InstanceMetadata @@ -56,6 +57,12 @@ def return_server_with_addresses(private, public): return _return_server +def return_server_with_power_state(power_state): + def _return_server(context, id): + return stub_instance(id, power_state=power_state) + return _return_server + + def return_servers(context, user_id=1): return [stub_instance(i, user_id) for i in xrange(5)] @@ -73,7 +80,7 @@ def instance_address(context, instance_id): def stub_instance(id, user_id=1, private_address=None, public_addresses=None, - host=None): + host=None, power_state=0): metadata = [] metadata.append(InstanceMetadata(key='seq', value=id)) @@ -96,7 +103,7 @@ def stub_instance(id, user_id=1, private_address=None, public_addresses=None, "launch_index": 0, "key_name": "", "key_data": "", - "state": 0, + "state": power_state, "state_description": "", "memory_mb": 0, "vcpus": 0, @@ -613,6 +620,33 @@ class ServersTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 400) + def test_create_instance_v11_local_href(self): + self._setup_for_create_instance() + + imageRef = 'http://localhost/v1.1/images/2' + imageRefLocal = '2' + flavorRef = 'http://localhost/v1.1/flavors/3' + body = { + 'server': { + 'name': 'server_test', + 'imageRef': imageRefLocal, + 'flavorRef': flavorRef, + }, + } + + 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(1, server['id']) + self.assertEqual(flavorRef, server['flavorRef']) + self.assertEqual(imageRef, server['imageRef']) + self.assertEqual(res.status_int, 200) + def test_create_instance_with_admin_pass_v10(self): self._setup_for_create_instance() @@ -1024,15 +1058,175 @@ class ServersTest(test.TestCase): req.body = json.dumps(body) res = req.get_response(fakes.wsgi_app()) - def test_server_rebuild(self): - body = dict(server=dict( - name='server_test', imageId=2, flavorId=2, metadata={}, - personality={})) + def test_server_rebuild_accepted(self): + body = { + "rebuild": { + "imageId": 2, + }, + } + 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()) + self.assertEqual(res.status_int, 202) + self.assertEqual(res.body, "") + + def test_server_rebuild_rejected_when_building(self): + body = { + "rebuild": { + "imageId": 2, + }, + } + + state = power_state.BUILDING + new_return_server = return_server_with_power_state(state) + self.stubs.Set(nova.db.api, 'instance_get', new_return_server) + + 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()) + self.assertEqual(res.status_int, 409) + + def test_server_rebuild_bad_entity(self): + body = { + "rebuild": { + }, + } + + 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()) + self.assertEqual(res.status_int, 400) + + def test_server_rebuild_accepted_minimum_v11(self): + body = { + "rebuild": { + "imageRef": "http://localhost/images/2", + }, + } + + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(body) + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 202) + + def test_server_rebuild_rejected_when_building_v11(self): + body = { + "rebuild": { + "imageRef": "http://localhost/images/2", + }, + } + + state = power_state.BUILDING + new_return_server = return_server_with_power_state(state) + self.stubs.Set(nova.db.api, 'instance_get', new_return_server) + + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(body) + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 409) + + def test_server_rebuild_accepted_with_metadata_v11(self): + body = { + "rebuild": { + "imageRef": "http://localhost/images/2", + "metadata": { + "new": "metadata", + }, + }, + } + + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(body) + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 202) + + def test_server_rebuild_accepted_with_bad_metadata_v11(self): + body = { + "rebuild": { + "imageRef": "http://localhost/images/2", + "metadata": "stack", + }, + } + + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(body) + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + + def test_server_rebuild_bad_entity_v11(self): + body = { + "rebuild": { + "imageId": 2, + }, + } + + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(body) + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + + def test_server_rebuild_bad_personality_v11(self): + body = { + "rebuild": { + "imageRef": "http://localhost/images/2", + "personality": [{ + "path": "/path/to/file", + "contents": "INVALID b64", + }] + }, + } + + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(body) + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 400) + + def test_server_rebuild_personality_v11(self): + body = { + "rebuild": { + "imageRef": "http://localhost/images/2", + "personality": [{ + "path": "/path/to/file", + "contents": base64.b64encode("Test String"), + }] + }, + } + + req = webob.Request.blank('/v1.1/servers/1/action') + req.method = 'POST' + req.content_type = 'application/json' + req.body = json.dumps(body) + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 202) def test_delete_server_instance(self): req = webob.Request.blank('/v1.0/servers/1') @@ -1155,6 +1349,24 @@ class ServersTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 400) + def test_shutdown_status(self): + new_server = return_server_with_power_state(power_state.SHUTDOWN) + self.stubs.Set(nova.db.api, 'instance_get', new_server) + req = webob.Request.blank('/v1.0/servers/1') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + res_dict = json.loads(res.body) + self.assertEqual(res_dict['server']['status'], 'SHUTDOWN') + + def test_shutoff_status(self): + new_server = return_server_with_power_state(power_state.SHUTOFF) + self.stubs.Set(nova.db.api, 'instance_get', new_server) + req = webob.Request.blank('/v1.0/servers/1') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + res_dict = json.loads(res.body) + self.assertEqual(res_dict['server']['status'], 'SHUTOFF') + class TestServerCreateRequestXMLDeserializer(unittest.TestCase): diff --git a/nova/tests/test_exception.py b/nova/tests/test_exception.py new file mode 100644 index 000000000..4d3b9cc73 --- /dev/null +++ b/nova/tests/test_exception.py @@ -0,0 +1,34 @@ +# 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. + +from nova import test +from nova import exception + + +class ApiErrorTestCase(test.TestCase): + def test_return_valid_error(self): + # without 'code' arg + err = exception.ApiError('fake error') + self.assertEqual(err.__str__(), 'fake error') + self.assertEqual(err.code, None) + self.assertEqual(err.msg, 'fake error') + # with 'code' arg + err = exception.ApiError('fake error', 'blah code') + self.assertEqual(err.__str__(), 'blah code: fake error') + self.assertEqual(err.code, 'blah code') + self.assertEqual(err.msg, 'fake error') diff --git a/nova/tests/test_misc.py b/nova/tests/test_misc.py index 4e17e1ce0..cf8f4c05e 100644 --- a/nova/tests/test_misc.py +++ b/nova/tests/test_misc.py @@ -29,11 +29,12 @@ from nova.utils import parse_mailmap, str_dict_replace class ProjectTestCase(test.TestCase): def test_authors_up_to_date(self): topdir = os.path.normpath(os.path.dirname(__file__) + '/../../') - if os.path.exists(os.path.join(topdir, '.bzr')): - contributors = set() - - mailmap = parse_mailmap(os.path.join(topdir, '.mailmap')) + missing = set() + contributors = set() + mailmap = parse_mailmap(os.path.join(topdir, '.mailmap')) + authors_file = open(os.path.join(topdir, 'Authors'), 'r').read() + if os.path.exists(os.path.join(topdir, '.bzr')): import bzrlib.workingtree tree = bzrlib.workingtree.WorkingTree.open(topdir) tree.lock_read() @@ -47,22 +48,36 @@ class ProjectTestCase(test.TestCase): for r in revs: for author in r.get_apparent_authors(): email = author.split(' ')[-1] - contributors.add(str_dict_replace(email, mailmap)) + contributors.add(str_dict_replace(email, + mailmap)) + finally: + tree.unlock() - authors_file = open(os.path.join(topdir, 'Authors'), - 'r').read() + elif os.path.exists(os.path.join(topdir, '.git')): + import git + repo = git.Repo(topdir) + for commit in repo.head.commit.iter_parents(): + email = commit.author.email + if email is None: + email = commit.author.name + if 'nova-core' in email: + continue + if email.split(' ')[-1] == '<>': + email = email.split(' ')[-2] + email = '<' + email + '>' + contributors.add(str_dict_replace(email, mailmap)) - missing = set() - for contributor in contributors: - if contributor == 'nova-core': - continue - if not contributor in authors_file: - missing.add(contributor) + else: + return - self.assertTrue(len(missing) == 0, - '%r not listed in Authors' % missing) - finally: - tree.unlock() + for contributor in contributors: + if contributor == 'nova-core': + continue + if not contributor in authors_file: + missing.add(contributor) + + self.assertTrue(len(missing) == 0, + '%r not listed in Authors' % missing) class LockTestCase(test.TestCase): diff --git a/nova/tests/test_virt.py b/nova/tests/test_virt.py index 462cf5aa0..1311ba361 100644 --- a/nova/tests/test_virt.py +++ b/nova/tests/test_virt.py @@ -44,7 +44,9 @@ def _concurrency(wait, done, target): done.send() -def _create_network_info(count=1): +def _create_network_info(count=1, ipv6=None): + if ipv6 is None: + ipv6 = FLAGS.use_ipv6 fake = 'fake' fake_ip = '0.0.0.0/0' fake_ip_2 = '0.0.0.1/0' @@ -55,8 +57,11 @@ def _create_network_info(count=1): 'cidr': fake_ip, 'cidr_v6': fake_ip} mapping = {'mac': fake, - 'ips': [{'ip': fake_ip}, {'ip': fake_ip}], - 'ip6s': [{'ip': fake_ip}, {'ip': fake_ip_2}, {'ip': fake_ip_3}]} + 'ips': [{'ip': fake_ip}, {'ip': fake_ip}]} + if ipv6: + mapping['ip6s'] = [{'ip': fake_ip}, + {'ip': fake_ip_2}, + {'ip': fake_ip_3}] return [(network, mapping) for x in xrange(0, count)] @@ -641,6 +646,11 @@ class LibvirtConnTestCase(test.TestCase): self.assertTrue(count) + def test_get_host_ip_addr(self): + conn = libvirt_conn.LibvirtConnection(False) + ip = conn.get_host_ip_addr() + self.assertEquals(ip, FLAGS.my_ip) + def tearDown(self): self.manager.delete_project(self.project) self.manager.delete_user(self.user) @@ -825,12 +835,20 @@ class IptablesFirewallTestCase(test.TestCase): "TCP port 80/81 acceptance rule wasn't added") db.instance_destroy(admin_ctxt, instance_ref['id']) - def test_filters_for_instance(self): + def test_filters_for_instance_with_ip_v6(self): + self.flags(use_ipv6=True) network_info = _create_network_info() rulesv4, rulesv6 = self.fw._filters_for_instance("fake", network_info) self.assertEquals(len(rulesv4), 2) self.assertEquals(len(rulesv6), 3) + def test_filters_for_instance_without_ip_v6(self): + self.flags(use_ipv6=False) + network_info = _create_network_info() + rulesv4, rulesv6 = self.fw._filters_for_instance("fake", network_info) + self.assertEquals(len(rulesv4), 2) + self.assertEquals(len(rulesv6), 0) + def multinic_iptables_test(self): ipv4_rules_per_network = 2 ipv6_rules_per_network = 3 diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py index 879534f59..9780c69a6 100644 --- a/nova/virt/libvirt_conn.py +++ b/nova/virt/libvirt_conn.py @@ -735,6 +735,9 @@ class LibvirtConnection(driver.ComputeDriver): subprocess.Popen(cmd, shell=True) return {'token': token, 'host': host, 'port': port} + def get_host_ip_addr(self): + return FLAGS.my_ip + @exception.wrap_exception def get_vnc_console(self, instance): def get_vnc_port_for_instance(instance_name): @@ -2008,10 +2011,12 @@ class IptablesFirewallDriver(FirewallDriver): for ip in mapping['ips']] ipv4_rules = self._create_filter(ips_v4, chain_name) - ips_v6 = [ip['ip'] for (_n, mapping) in network_info - for ip in mapping['ip6s']] + ipv6_rules = [] + if FLAGS.use_ipv6: + ips_v6 = [ip['ip'] for (_n, mapping) in network_info + for ip in mapping['ip6s']] + ipv6_rules = self._create_filter(ips_v6, chain_name) - ipv6_rules = self._create_filter(ips_v6, chain_name) return ipv4_rules, ipv6_rules def _add_filters(self, chain_name, ipv4_rules, ipv6_rules): diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index 808708e8b..30f31517d 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -210,8 +210,6 @@ class VMOps(object): def _wait_for_boot(): try: state = self.get_info(instance_name)['state'] - db.instance_set_state(context.get_admin_context(), - instance['id'], state) if state == power_state.RUNNING: LOG.debug(_('Instance %s: booted'), instance_name) timer.stop() @@ -219,11 +217,7 @@ class VMOps(object): return True except Exception, exc: LOG.warn(exc) - LOG.exception(_('instance %s: failed to boot'), - instance_name) - db.instance_set_state(context.get_admin_context(), - instance['id'], - power_state.SHUTDOWN) + LOG.exception(_('Instance %s: failed to boot'), instance_name) timer.stop() return False diff --git a/run_tests.sh b/run_tests.sh index 610cf1f27..e3a0bd243 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -6,6 +6,7 @@ function usage { echo "" echo " -V, --virtual-env Always use virtualenv. Install automatically if not present" echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment" + echo " -x, --stop Stop running tests after the first error or failure." echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added." echo " -p, --pep8 Just run pep8" echo " -h, --help Print this usage message" |
