diff options
| author | Sandy Walsh <sandy.walsh@rackspace.com> | 2011-09-21 13:17:05 -0700 |
|---|---|---|
| committer | Sandy Walsh <sandy.walsh@rackspace.com> | 2011-09-21 13:17:05 -0700 |
| commit | cd4050422340d27ba6161e2b9c4ccc7f057fb4bb (patch) | |
| tree | 8c4951d69e2625d14eaa28db350d1a1876d6254f /nova | |
| parent | 1051adcfc88c7a9eacef2a7000f43f9e66e1cb47 (diff) | |
| parent | d9752d46554ffa87360bfd740177b40871cfbea6 (diff) | |
trunk merge fixup
Diffstat (limited to 'nova')
| -rw-r--r-- | nova/api/ec2/cloud.py | 1 | ||||
| -rw-r--r-- | nova/api/ec2/ec2utils.py | 2 | ||||
| -rw-r--r-- | nova/api/openstack/common.py | 3 | ||||
| -rw-r--r-- | nova/api/openstack/contrib/deferred_delete.py | 76 | ||||
| -rw-r--r-- | nova/api/openstack/images.py | 2 | ||||
| -rw-r--r-- | nova/api/openstack/servers.py | 13 | ||||
| -rw-r--r-- | nova/api/openstack/views/images.py | 16 | ||||
| -rw-r--r-- | nova/compute/api.py | 85 | ||||
| -rw-r--r-- | nova/compute/manager.py | 64 | ||||
| -rw-r--r-- | nova/compute/task_states.py | 2 | ||||
| -rw-r--r-- | nova/compute/vm_states.py | 1 | ||||
| -rw-r--r-- | nova/flags.py | 4 | ||||
| -rwxr-xr-x | nova/network/linux_net.py | 40 | ||||
| -rw-r--r-- | nova/tests/api/openstack/test_extensions.py | 1 | ||||
| -rw-r--r-- | nova/tests/api/openstack/test_images.py | 102 | ||||
| -rw-r--r-- | nova/tests/integrated/integrated_helpers.py | 8 | ||||
| -rw-r--r-- | nova/tests/integrated/test_servers.py | 146 | ||||
| -rwxr-xr-x | nova/tests/test_linux_net.py | 69 | ||||
| -rw-r--r-- | nova/tests/test_utils.py | 9 | ||||
| -rw-r--r-- | nova/utils.py | 7 | ||||
| -rw-r--r-- | nova/virt/driver.py | 8 | ||||
| -rw-r--r-- | nova/virt/xenapi/vmops.py | 10 | ||||
| -rw-r--r-- | nova/virt/xenapi_conn.py | 8 |
23 files changed, 629 insertions, 48 deletions
diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 23ac30494..68d39042f 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -89,6 +89,7 @@ _STATE_DESCRIPTION_MAP = { vm_states.BUILDING: 'pending', vm_states.REBUILDING: 'pending', vm_states.DELETED: 'terminated', + vm_states.SOFT_DELETE: 'terminated', vm_states.STOPPED: 'stopped', vm_states.MIGRATING: 'migrate', vm_states.RESIZING: 'resize', diff --git a/nova/api/ec2/ec2utils.py b/nova/api/ec2/ec2utils.py index bcdf2ba78..aac8f9a1e 100644 --- a/nova/api/ec2/ec2utils.py +++ b/nova/api/ec2/ec2utils.py @@ -116,7 +116,7 @@ def dict_from_dotted_str(items): args = {} for key, value in items: parts = key.split(".") - key = camelcase_to_underscore(parts[0]) + key = str(camelcase_to_underscore(parts[0])) if isinstance(value, str) or isinstance(value, unicode): # NOTE(vish): Automatically convert strings back # into their respective values diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index ca7848678..3ef9bdee5 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -78,6 +78,9 @@ _STATE_MAP = { vm_states.DELETED: { 'default': 'DELETED', }, + vm_states.SOFT_DELETE: { + 'default': 'DELETED', + }, } diff --git a/nova/api/openstack/contrib/deferred_delete.py b/nova/api/openstack/contrib/deferred_delete.py new file mode 100644 index 000000000..13ee5511e --- /dev/null +++ b/nova/api/openstack/contrib/deferred_delete.py @@ -0,0 +1,76 @@ +# Copyright 2011 Openstack, LLC +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""The deferred instance delete extension.""" + +import webob +from webob import exc + +from nova import compute +from nova import exception +from nova import log as logging +from nova.api.openstack import common +from nova.api.openstack import extensions +from nova.api.openstack import faults +from nova.api.openstack import servers + + +LOG = logging.getLogger("nova.api.contrib.deferred-delete") + + +class Deferred_delete(extensions.ExtensionDescriptor): + def __init__(self): + super(Deferred_delete, self).__init__() + self.compute_api = compute.API() + + def _restore(self, input_dict, req, instance_id): + """Restore a previously deleted instance.""" + + context = req.environ["nova.context"] + self.compute_api.restore(context, instance_id) + return webob.Response(status_int=202) + + def _force_delete(self, input_dict, req, instance_id): + """Force delete of instance before deferred cleanup.""" + + context = req.environ["nova.context"] + self.compute_api.force_delete(context, instance_id) + return webob.Response(status_int=202) + + def get_name(self): + return "DeferredDelete" + + def get_alias(self): + return "os-deferred-delete" + + def get_description(self): + return "Instance deferred delete" + + def get_namespace(self): + return "http://docs.openstack.org/ext/deferred-delete/api/v1.1" + + def get_updated(self): + return "2011-09-01T00:00:00+00:00" + + def get_actions(self): + """Return the actions the extension adds, as required by contract.""" + actions = [ + extensions.ActionExtension("servers", "restore", + self._restore), + extensions.ActionExtension("servers", "forceDelete", + self._force_delete), + ] + + return actions diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 4340cbe3e..d579ae716 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -251,6 +251,8 @@ class ImageXMLSerializer(wsgi.XMLDictSerializer): elem = etree.SubElement(image_elem, '{%s}link' % xmlutil.XMLNS_ATOM) elem.set('rel', link['rel']) + if 'type' in link: + elem.set('type', link['type']) elem.set('href', link['href']) return image_elem diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 3b19e695f..1a4703069 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -47,7 +47,7 @@ FLAGS = flags.FLAGS class ConvertedException(exc.WSGIHTTPException): - def __init__(self, code, title, explaination): + def __init__(self, code, title, explanation): self.code = code self.title = title self.explanation = explanation @@ -192,7 +192,12 @@ class Controller(object): server['server']['adminPass'] = extra_values['password'] return server - @novaclient_exception_converter + def _delete(self, context, id): + if FLAGS.reclaim_instance_interval: + self.compute_api.soft_delete(context, id) + else: + self.compute_api.delete(context, id) + @scheduler_api.redirect_handler def update(self, req, id, body): """Update server then pass on to version-specific controller""" @@ -613,7 +618,7 @@ class ControllerV10(Controller): def delete(self, req, id): """ Destroys a server """ try: - self.compute_api.delete(req.environ['nova.context'], id) + self._delete(req.environ['nova.context'], id) except exception.NotFound: raise exc.HTTPNotFound() return webob.Response(status_int=202) @@ -692,7 +697,7 @@ class ControllerV11(Controller): def delete(self, req, id): """ Destroys a server """ try: - self.compute_api.delete(req.environ['nova.context'], id) + self._delete(req.environ['nova.context'], id) except exception.NotFound: raise exc.HTTPNotFound() diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index 86e8d7f3a..659bfd463 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -18,6 +18,7 @@ import os.path from nova.api.openstack import common +from nova import utils class ViewBuilder(object): @@ -139,6 +140,7 @@ class ViewBuilderV11(ViewBuilder): image = ViewBuilder.build(self, image_obj, detail) href = self.generate_href(image_obj["id"]) bookmark = self.generate_bookmark(image_obj["id"]) + alternate = self.generate_alternate(image_obj["id"]) image["links"] = [ { @@ -149,6 +151,11 @@ class ViewBuilderV11(ViewBuilder): "rel": "bookmark", "href": bookmark, }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": alternate, + }, ] @@ -158,6 +165,13 @@ class ViewBuilderV11(ViewBuilder): return image def generate_bookmark(self, image_id): - """Create an url that refers to a specific flavor id.""" + """Create a URL that refers to a specific flavor id.""" return os.path.join(common.remove_version_from_href(self.base_url), self.project_id, "images", str(image_id)) + + def generate_alternate(self, image_id): + """Create an alternate link for a specific flavor id.""" + glance_url = utils.generate_glance_url() + + return "%s/%s/images/%s" % (glance_url, self.project_id, + str(image_id)) diff --git a/nova/compute/api.py b/nova/compute/api.py index e7afceb0b..130828d7b 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -92,6 +92,19 @@ def _is_able_to_shutdown(instance, instance_id): return True +def _is_queued_delete(instance, instance_id): + vm_state = instance["vm_state"] + task_state = instance["task_state"] + + if vm_state != vm_states.SOFT_DELETE: + LOG.warn(_("Instance %(instance_id)s is not in a 'soft delete' " + "state. It is currently %(vm_state)s. Action aborted.") % + locals()) + return False + + return True + + class API(base.Base): """API for interacting with the compute manager.""" @@ -752,15 +765,85 @@ class API(base.Base): {'instance_id': instance_id, 'action_str': action_str}) raise + @scheduler_api.reroute_compute("soft_delete") + def soft_delete(self, context, instance_id): + """Terminate an instance.""" + LOG.debug(_("Going to try to soft delete %s"), instance_id) + instance = self._get_instance(context, instance_id, 'soft delete') + + if not _is_able_to_shutdown(instance, instance_id): + return + + # NOTE(jerdfelt): The compute daemon handles reclaiming instances + # that are in soft delete. If there is no host assigned, there is + # no daemon to reclaim, so delete it immediately. + host = instance['host'] + if host: + self.update(context, + instance_id, + vm_state=vm_states.SOFT_DELETE, + task_state=task_states.POWERING_OFF, + deleted_at=utils.utcnow()) + + self._cast_compute_message('power_off_instance', context, + instance_id, host) + else: + LOG.warning(_("No host for instance %s, deleting immediately"), + instance_id) + terminate_volumes(self.db, context, instance_id) + self.db.instance_destroy(context, instance_id) + @scheduler_api.reroute_compute("delete") def delete(self, context, instance_id): """Terminate an instance.""" LOG.debug(_("Going to try to terminate %s"), instance_id) - instance = self._get_instance(context, instance_id, 'terminating') + instance = self._get_instance(context, instance_id, 'delete') if not _is_able_to_shutdown(instance, instance_id): return + host = instance['host'] + if host: + self.update(context, + instance_id, + task_state=task_states.DELETING) + + self._cast_compute_message('terminate_instance', context, + instance_id, host) + else: + terminate_volumes(self.db, context, instance_id) + self.db.instance_destroy(context, instance_id) + + @scheduler_api.reroute_compute("restore") + def restore(self, context, instance_id): + """Restore a previously deleted (but not reclaimed) instance.""" + instance = self._get_instance(context, instance_id, 'restore') + + if not _is_queued_delete(instance, instance_id): + return + + self.update(context, + instance_id, + vm_state=vm_states.ACTIVE, + task_state=None, + deleted_at=None) + + host = instance['host'] + if host: + self.update(context, + instance_id, + task_state=task_states.POWERING_ON) + self._cast_compute_message('power_on_instance', context, + instance_id, host) + + @scheduler_api.reroute_compute("force_delete") + def force_delete(self, context, instance_id): + """Force delete a previously deleted (but not reclaimed) instance.""" + instance = self._get_instance(context, instance_id, 'force delete') + + if not _is_queued_delete(instance, instance_id): + return + self.update(context, instance_id, task_state=task_states.DELETING) diff --git a/nova/compute/manager.py b/nova/compute/manager.py index cb5d10f83..d7c23c65d 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -35,12 +35,13 @@ terminating it. """ +import datetime +import functools import os import socket import sys import tempfile import time -import functools from eventlet import greenthread @@ -84,6 +85,8 @@ flags.DEFINE_integer("resize_confirm_window", 0, " Set to 0 to disable.") flags.DEFINE_integer('host_state_interval', 120, 'Interval in seconds for querying the host status') +flags.DEFINE_integer('reclaim_instance_interval', 0, + 'Interval in seconds for reclaiming deleted instances') LOG = logging.getLogger('nova.compute.manager') @@ -175,7 +178,7 @@ class ComputeManager(manager.SchedulerDependentManager): 'nova-compute restart.'), locals()) self.reboot_instance(context, instance['id']) elif drv_state == power_state.RUNNING: - # Hyper-V and VMWareAPI drivers will raise and exception + # Hyper-V and VMWareAPI drivers will raise an exception try: net_info = self._get_instance_nw_info(context, instance) self.driver.ensure_filtering_rules_for_instance(instance, @@ -487,10 +490,8 @@ class ComputeManager(manager.SchedulerDependentManager): if action_str == 'Terminating': terminate_volumes(self.db, context, instance_id) - @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) - @checks_instance_lock - def terminate_instance(self, context, instance_id): - """Terminate an instance on this host.""" + def _delete_instance(self, context, instance_id): + """Delete an instance on this host.""" self._shutdown_instance(context, instance_id, 'Terminating') instance = self.db.instance_get(context.elevated(), instance_id) self._instance_update(context, @@ -508,6 +509,12 @@ class ComputeManager(manager.SchedulerDependentManager): @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) @checks_instance_lock + def terminate_instance(self, context, instance_id): + """Terminate an instance on this host.""" + self._delete_instance(context, instance_id) + + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) + @checks_instance_lock def stop_instance(self, context, instance_id): """Stopping an instance on this host.""" self._shutdown_instance(context, instance_id, 'Stopping') @@ -518,6 +525,30 @@ class ComputeManager(manager.SchedulerDependentManager): @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) @checks_instance_lock + def power_off_instance(self, context, instance_id): + """Power off an instance on this host.""" + instance = self.db.instance_get(context, instance_id) + self.driver.power_off(instance) + current_power_state = self._get_power_state(context, instance) + self._instance_update(context, + instance_id, + power_state=current_power_state, + task_state=None) + + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) + @checks_instance_lock + def power_on_instance(self, context, instance_id): + """Power on an instance on this host.""" + instance = self.db.instance_get(context, instance_id) + self.driver.power_on(instance) + current_power_state = self._get_power_state(context, instance) + self._instance_update(context, + instance_id, + power_state=current_power_state, + task_state=None) + + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) + @checks_instance_lock def rebuild_instance(self, context, instance_id, **kwargs): """Destroy and re-make this instance. @@ -1676,6 +1707,13 @@ class ComputeManager(manager.SchedulerDependentManager): LOG.warning(_("Error during power_state sync: %s"), unicode(ex)) error_list.append(ex) + try: + self._reclaim_queued_deletes(context) + except Exception as ex: + LOG.warning(_("Error during reclamation of queued deletes: %s"), + unicode(ex)) + error_list.append(ex) + return error_list def _report_driver_status(self): @@ -1725,3 +1763,17 @@ class ComputeManager(manager.SchedulerDependentManager): self._instance_update(context, db_instance["id"], power_state=vm_power_state) + + def _reclaim_queued_deletes(self, context): + """Reclaim instances that are queued for deletion.""" + + instances = self.db.instance_get_all_by_host(context, self.host) + + queue_time = datetime.timedelta( + seconds=FLAGS.reclaim_instance_interval) + curtime = utils.utcnow() + for instance in instances: + if instance['vm_state'] == vm_states.SOFT_DELETE and \ + (curtime - instance['deleted_at']) >= queue_time: + LOG.info('Deleting %s' % instance['name']) + self._delete_instance(context, instance['id']) diff --git a/nova/compute/task_states.py b/nova/compute/task_states.py index e3315a542..b52140bf8 100644 --- a/nova/compute/task_states.py +++ b/nova/compute/task_states.py @@ -50,6 +50,8 @@ PAUSING = 'pausing' UNPAUSING = 'unpausing' SUSPENDING = 'suspending' RESUMING = 'resuming' +POWERING_OFF = 'powering-off' +POWERING_ON = 'powering-on' RESCUING = 'rescuing' UNRESCUING = 'unrescuing' diff --git a/nova/compute/vm_states.py b/nova/compute/vm_states.py index 6f16c1f09..f219bf7f4 100644 --- a/nova/compute/vm_states.py +++ b/nova/compute/vm_states.py @@ -32,6 +32,7 @@ SUSPENDED = 'suspended' RESCUED = 'rescued' DELETED = 'deleted' STOPPED = 'stopped' +SOFT_DELETE = 'soft-delete' MIGRATING = 'migrating' RESIZING = 'resizing' diff --git a/nova/flags.py b/nova/flags.py index 971e78807..ffb313cec 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -271,8 +271,10 @@ DEFINE_string('connection_type', 'libvirt', 'libvirt, xenapi or fake') DEFINE_string('aws_access_key_id', 'admin', 'AWS Access ID') DEFINE_string('aws_secret_access_key', 'admin', 'AWS Access Key') # NOTE(sirp): my_ip interpolation doesn't work within nested structures +DEFINE_string('glance_host', _get_my_ip(), 'default glance host') +DEFINE_integer('glance_port', 9292, 'default glance port') DEFINE_list('glance_api_servers', - ['%s:9292' % _get_my_ip()], + ['%s:%d' % (FLAGS.glance_host, FLAGS.glance_port)], 'list of glance api servers available to nova (host:port)') DEFINE_integer('s3_port', 3333, 's3 port') DEFINE_string('s3_host', '$my_ip', 's3 host (for infrastructure)') diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index ad7c5776b..0459b4aeb 100755 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -472,22 +472,30 @@ def initialize_gateway_device(dev, network_ref): # NOTE(vish): The ip for dnsmasq has to be the first address on the # bridge for it to respond to reqests properly - suffix = network_ref['cidr'].rpartition('/')[2] - out, err = _execute('ip', 'addr', 'add', - '%s/%s' % - (network_ref['dhcp_server'], suffix), - 'brd', - network_ref['broadcast'], - 'dev', - dev, - run_as_root=True, - check_exit_code=False) - if err and err != 'RTNETLINK answers: File exists\n': - raise exception.Error('Failed to add ip: %s' % err) - if FLAGS.send_arp_for_ha: - _execute('arping', '-U', network_ref['gateway'], - '-A', '-I', dev, - '-c', 1, run_as_root=True, check_exit_code=False) + full_ip = '%s/%s' % (network_ref['dhcp_server'], + network_ref['cidr'].rpartition('/')[2]) + new_ip_params = [[full_ip, 'brd', network_ref['broadcast']]] + old_ip_params = [] + out, err = _execute('ip', 'addr', 'show', 'dev', dev, + 'scope', 'global', run_as_root=True) + for line in out.split('\n'): + fields = line.split() + if fields and fields[0] == 'inet': + ip_params = fields[1:-1] + old_ip_params.append(ip_params) + if ip_params[0] != full_ip: + new_ip_params.append(ip_params) + if not old_ip_params or old_ip_params[0][0] != full_ip: + for ip_params in old_ip_params: + _execute(*_ip_bridge_cmd('del', ip_params, dev), + run_as_root=True) + for ip_params in new_ip_params: + _execute(*_ip_bridge_cmd('add', ip_params, dev), + run_as_root=True) + if FLAGS.send_arp_for_ha: + _execute('arping', '-U', network_ref['dhcp_server'], + '-A', '-I', dev, + '-c', 1, run_as_root=True, check_exit_code=False) if(FLAGS.use_ipv6): _execute('ip', '-f', 'inet6', 'addr', 'change', network_ref['cidr_v6'], diff --git a/nova/tests/api/openstack/test_extensions.py b/nova/tests/api/openstack/test_extensions.py index 44f4eb055..ca36523e4 100644 --- a/nova/tests/api/openstack/test_extensions.py +++ b/nova/tests/api/openstack/test_extensions.py @@ -86,6 +86,7 @@ class ExtensionControllerTest(test.TestCase): self.flags(osapi_extensions_path=ext_path) self.ext_list = [ "Createserverext", + "DeferredDelete", "FlavorExtraSpecs", "FlavorExtraData", "Floating_ips", diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index e5fd4764a..886efb6ac 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -33,7 +33,9 @@ from nova import context import nova.api.openstack from nova.api.openstack import images from nova.api.openstack import xmlutil +from nova.api.openstack.views import images as images_view from nova import test +from nova import utils from nova.tests.api.openstack import fakes @@ -119,6 +121,7 @@ class ImagesTest(test.TestCase): href = "http://localhost/v1.1/fake/images/124" bookmark = "http://localhost/fake/images/124" + alternate = "%s/fake/images/124" % utils.generate_glance_url() server_href = "http://localhost/v1.1/servers/42" server_bookmark = "http://localhost/servers/42" @@ -152,6 +155,11 @@ class ImagesTest(test.TestCase): { "rel": "bookmark", "href": bookmark, + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": alternate }], }, } @@ -289,6 +297,12 @@ class ImagesTest(test.TestCase): "rel": "bookmark", "href": "http://localhost/fake/images/123", }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/123" % + utils.generate_glance_url() + }, ], }, { @@ -303,6 +317,12 @@ class ImagesTest(test.TestCase): "rel": "bookmark", "href": "http://localhost/fake/images/124", }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/124" % + utils.generate_glance_url() + }, ], }, { @@ -317,6 +337,12 @@ class ImagesTest(test.TestCase): "rel": "bookmark", "href": "http://localhost/fake/images/125", }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/125" % + utils.generate_glance_url() + }, ], }, { @@ -331,6 +357,12 @@ class ImagesTest(test.TestCase): "rel": "bookmark", "href": "http://localhost/fake/images/126", }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/126" % + utils.generate_glance_url() + }, ], }, { @@ -345,6 +377,12 @@ class ImagesTest(test.TestCase): "rel": "bookmark", "href": "http://localhost/fake/images/127", }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/127" % + utils.generate_glance_url() + }, ], }, { @@ -359,6 +397,12 @@ class ImagesTest(test.TestCase): "rel": "bookmark", "href": "http://localhost/fake/images/128", }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/128" % + utils.generate_glance_url() + }, ], }, { @@ -373,6 +417,12 @@ class ImagesTest(test.TestCase): "rel": "bookmark", "href": "http://localhost/fake/images/129", }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/129" % + utils.generate_glance_url() + }, ], }, { @@ -387,6 +437,12 @@ class ImagesTest(test.TestCase): "rel": "bookmark", "href": "http://localhost/fake/images/130", }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/130" % + utils.generate_glance_url() + }, ], }, ] @@ -493,6 +549,11 @@ class ImagesTest(test.TestCase): { "rel": "bookmark", "href": "http://localhost/fake/images/123", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/123" % utils.generate_glance_url() }], }, { @@ -524,6 +585,11 @@ class ImagesTest(test.TestCase): { "rel": "bookmark", "href": "http://localhost/fake/images/124", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/124" % utils.generate_glance_url() }], }, { @@ -555,6 +621,11 @@ class ImagesTest(test.TestCase): { "rel": "bookmark", "href": "http://localhost/fake/images/125", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/125" % utils.generate_glance_url() }], }, { @@ -586,6 +657,11 @@ class ImagesTest(test.TestCase): { "rel": "bookmark", "href": "http://localhost/fake/images/126", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/126" % utils.generate_glance_url() }], }, { @@ -617,6 +693,11 @@ class ImagesTest(test.TestCase): { "rel": "bookmark", "href": "http://localhost/fake/images/127", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/127" % utils.generate_glance_url() }], }, { @@ -648,6 +729,11 @@ class ImagesTest(test.TestCase): { "rel": "bookmark", "href": "http://localhost/fake/images/128", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/128" % utils.generate_glance_url() }], }, { @@ -679,6 +765,11 @@ class ImagesTest(test.TestCase): { "rel": "bookmark", "href": "http://localhost/fake/images/129", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/129" % utils.generate_glance_url() }], }, { @@ -696,6 +787,11 @@ class ImagesTest(test.TestCase): { "rel": "bookmark", "href": "http://localhost/fake/images/130", + }, + { + "rel": "alternate", + "type": "application/vnd.openstack.image", + "href": "%s/fake/images/130" % utils.generate_glance_url() }], }, ] @@ -963,6 +1059,12 @@ class ImagesTest(test.TestCase): response = req.get_response(fakes.wsgi_app()) self.assertEqual(400, response.status_int) + def test_generate_alternate(self): + view = images_view.ViewBuilderV11(1) + generated_url = view.generate_alternate(1) + actual_url = "%s//images/1" % utils.generate_glance_url() + self.assertEqual(generated_url, actual_url) + class ImageXMLSerializationTest(test.TestCase): diff --git a/nova/tests/integrated/integrated_helpers.py b/nova/tests/integrated/integrated_helpers.py index 49de9c854..b53e4cec6 100644 --- a/nova/tests/integrated/integrated_helpers.py +++ b/nova/tests/integrated/integrated_helpers.py @@ -70,10 +70,10 @@ class _IntegratedTestBase(test.TestCase): self.stubs.Set(nova.image, 'get_image_service', fake_get_image_service) # set up services - self.start_service('compute') - self.start_service('volume') - self.start_service('network') - self.start_service('scheduler') + self.compute = self.start_service('compute') + self.volume = self.start_service('volume') + self.network = self.start_service('network') + self.scheduler = self.start_service('scheduler') self._start_api_service() diff --git a/nova/tests/integrated/test_servers.py b/nova/tests/integrated/test_servers.py index 2cf604d06..e9c79aa13 100644 --- a/nova/tests/integrated/test_servers.py +++ b/nova/tests/integrated/test_servers.py @@ -28,17 +28,25 @@ LOG = logging.getLogger('nova.tests.integrated') class ServersTest(integrated_helpers._IntegratedTestBase): - def _wait_for_creation(self, server): - retries = 0 - while server['status'] == 'BUILD': - time.sleep(1) + def _wait_for_state_change(self, server, status): + for i in xrange(0, 50): server = self.api.get_server(server['id']) print server - retries = retries + 1 - if retries > 5: + if server['status'] != status: break + time.sleep(.1) + return server + def _restart_compute_service(self, periodic_interval=None): + """restart compute service. NOTE: fake driver forgets all instances.""" + self.compute.kill() + if periodic_interval: + self.compute = self.start_service( + 'compute', periodic_interval=periodic_interval) + else: + self.compute = self.start_service('compute') + def test_get_servers(self): """Simple check that listing servers works.""" servers = self.api.get_servers() @@ -102,7 +110,7 @@ class ServersTest(integrated_helpers._IntegratedTestBase): server_ids = [server['id'] for server in servers] self.assertTrue(created_server_id in server_ids) - found_server = self._wait_for_creation(found_server) + found_server = self._wait_for_state_change(found_server, 'BUILD') # It should be available... # TODO(justinsb): Mock doesn't yet do this... @@ -114,12 +122,117 @@ class ServersTest(integrated_helpers._IntegratedTestBase): self._delete_server(created_server_id) - def _delete_server(self, server_id): + def test_deferred_delete(self): + """Creates, deletes and waits for server to be reclaimed.""" + self.flags(stub_network=True, reclaim_instance_interval=1) + + # enforce periodic tasks run in short time to avoid wait for 60s. + self._restart_compute_service(periodic_interval=0.3) + + # Create server + server = self._build_minimal_create_server_request() + + created_server = self.api.post_server({'server': server}) + LOG.debug("created_server: %s" % created_server) + self.assertTrue(created_server['id']) + created_server_id = created_server['id'] + + # Wait for it to finish being created + found_server = self._wait_for_state_change(created_server, 'BUILD') + + # It should be available... + self.assertEqual('ACTIVE', found_server['status']) + + # Cannot restore unless instance is deleted + self.api.post_server_action(created_server_id, {'restore': {}}) + + # Check it's still active + found_server = self.api.get_server(created_server_id) + self.assertEqual('ACTIVE', found_server['status']) + + # Cannot forceDelete unless instance is deleted + self.api.post_server_action(created_server_id, {'forceDelete': {}}) + + # Check it's still active + found_server = self.api.get_server(created_server_id) + self.assertEqual('ACTIVE', found_server['status']) + # Delete the server - self.api.delete_server(server_id) + self.api.delete_server(created_server_id) + + # Wait for queued deletion + found_server = self._wait_for_state_change(found_server, 'ACTIVE') + self.assertEqual('DELETED', found_server['status']) + # Wait for real deletion + self._wait_for_deletion(created_server_id) + + def test_deferred_delete_restore(self): + """Creates, deletes and restores a server.""" + self.flags(stub_network=True, reclaim_instance_interval=1) + + # Create server + server = self._build_minimal_create_server_request() + + created_server = self.api.post_server({'server': server}) + LOG.debug("created_server: %s" % created_server) + self.assertTrue(created_server['id']) + created_server_id = created_server['id'] + + # Wait for it to finish being created + found_server = self._wait_for_state_change(created_server, 'BUILD') + + # It should be available... + self.assertEqual('ACTIVE', found_server['status']) + + # Delete the server + self.api.delete_server(created_server_id) + + # Wait for queued deletion + found_server = self._wait_for_state_change(found_server, 'ACTIVE') + self.assertEqual('DELETED', found_server['status']) + + # Restore server + self.api.post_server_action(created_server_id, {'restore': {}}) + + # Wait for server to become active again + found_server = self._wait_for_state_change(found_server, 'DELETED') + self.assertEqual('ACTIVE', found_server['status']) + + def test_deferred_delete_force(self): + """Creates, deletes and force deletes a server.""" + self.flags(stub_network=True, reclaim_instance_interval=1) + + # Create server + server = self._build_minimal_create_server_request() + + created_server = self.api.post_server({'server': server}) + LOG.debug("created_server: %s" % created_server) + self.assertTrue(created_server['id']) + created_server_id = created_server['id'] + + # Wait for it to finish being created + found_server = self._wait_for_state_change(created_server, 'BUILD') + + # It should be available... + self.assertEqual('ACTIVE', found_server['status']) + + # Delete the server + self.api.delete_server(created_server_id) + + # Wait for queued deletion + found_server = self._wait_for_state_change(found_server, 'ACTIVE') + self.assertEqual('DELETED', found_server['status']) + + # Force delete server + self.api.post_server_action(created_server_id, {'forceDelete': {}}) + + # Wait for real deletion + self._wait_for_deletion(created_server_id) + + def _wait_for_deletion(self, server_id): # Wait (briefly) for deletion - for _retries in range(5): + for _retries in range(50): try: found_server = self.api.get_server(server_id) except client.OpenStackApiNotFoundException: @@ -132,11 +245,16 @@ class ServersTest(integrated_helpers._IntegratedTestBase): # TODO(justinsb): Mock doesn't yet do accurate state changes #if found_server['status'] != 'deleting': # break - time.sleep(1) + time.sleep(.1) # Should be gone self.assertFalse(found_server) + def _delete_server(self, server_id): + # Delete the server + self.api.delete_server(server_id) + self._wait_for_deletion(server_id) + def test_create_server_with_metadata(self): """Creates a server with metadata.""" @@ -194,7 +312,7 @@ class ServersTest(integrated_helpers._IntegratedTestBase): self.assertTrue(created_server['id']) created_server_id = created_server['id'] - created_server = self._wait_for_creation(created_server) + created_server = self._wait_for_state_change(created_server, 'BUILD') # rebuild the server with metadata post = {} @@ -228,7 +346,7 @@ class ServersTest(integrated_helpers._IntegratedTestBase): self.assertTrue(created_server['id']) created_server_id = created_server['id'] - created_server = self._wait_for_creation(created_server) + created_server = self._wait_for_state_change(created_server, 'BUILD') # rebuild the server with metadata post = {} @@ -274,7 +392,7 @@ class ServersTest(integrated_helpers._IntegratedTestBase): self.assertTrue(created_server['id']) created_server_id = created_server['id'] - created_server = self._wait_for_creation(created_server) + created_server = self._wait_for_state_change(created_server, 'BUILD') # rebuild the server with metadata post = {} diff --git a/nova/tests/test_linux_net.py b/nova/tests/test_linux_net.py index 99577b88e..940af7b5f 100755 --- a/nova/tests/test_linux_net.py +++ b/nova/tests/test_linux_net.py @@ -345,3 +345,72 @@ class LinuxNetworkTestCase(test.TestCase): expected = ("10.0.0.1,fake_instance00.novalocal,192.168.0.100")
actual = self.driver._host_dhcp(fixed_ips[0])
self.assertEquals(actual, expected)
+
+ def _test_initialize_gateway(self, existing, expected):
+ self.flags(fake_network=False)
+ executes = []
+
+ def fake_execute(*args, **kwargs):
+ executes.append(args)
+ if args[0] == 'ip' and args[1] == 'addr' and args[2] == 'show':
+ return existing, ""
+ self.stubs.Set(utils, 'execute', fake_execute)
+ network = {'dhcp_server': '192.168.1.1',
+ 'cidr': '192.168.1.0/24',
+ 'broadcast': '192.168.1.255',
+ 'cidr_v6': '2001:db8::/64'}
+ self.driver.initialize_gateway_device('eth0', network)
+ self.assertEqual(executes, expected)
+
+ def test_initialize_gateway_moves_wrong_ip(self):
+ existing = ("2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> "
+ " mtu 1500 qdisc pfifo_fast state UNKNOWN qlen 1000\n"
+ " link/ether de:ad:be:ef:be:ef brd ff:ff:ff:ff:ff:ff\n"
+ " inet 192.168.0.1/24 brd 192.168.0.255 scope global eth0\n"
+ " inet6 dead::beef:dead:beef:dead/64 scope link\n"
+ " valid_lft forever preferred_lft forever\n")
+ expected = [
+ ('ip', 'addr', 'show', 'dev', 'eth0', 'scope', 'global'),
+ ('ip', 'addr', 'del', '192.168.0.1/24',
+ 'brd', '192.168.0.255', 'scope', 'global', 'dev', 'eth0'),
+ ('ip', 'addr', 'add', '192.168.1.1/24',
+ 'brd', '192.168.1.255', 'dev', 'eth0'),
+ ('ip', 'addr', 'add', '192.168.0.1/24',
+ 'brd', '192.168.0.255', 'scope', 'global', 'dev', 'eth0'),
+ ('ip', '-f', 'inet6', 'addr', 'change',
+ '2001:db8::/64', 'dev', 'eth0'),
+ ('ip', 'link', 'set', 'dev', 'eth0', 'promisc', 'on'),
+ ]
+ self._test_initialize_gateway(existing, expected)
+
+ def test_initialize_gateway_no_move_right_ip(self):
+ existing = ("2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> "
+ " mtu 1500 qdisc pfifo_fast state UNKNOWN qlen 1000\n"
+ " link/ether de:ad:be:ef:be:ef brd ff:ff:ff:ff:ff:ff\n"
+ " inet 192.168.1.1/24 brd 192.168.1.255 scope global eth0\n"
+ " inet 192.168.0.1/24 brd 192.168.0.255 scope global eth0\n"
+ " inet6 dead::beef:dead:beef:dead/64 scope link\n"
+ " valid_lft forever preferred_lft forever\n")
+ expected = [
+ ('ip', 'addr', 'show', 'dev', 'eth0', 'scope', 'global'),
+ ('ip', '-f', 'inet6', 'addr', 'change',
+ '2001:db8::/64', 'dev', 'eth0'),
+ ('ip', 'link', 'set', 'dev', 'eth0', 'promisc', 'on'),
+ ]
+ self._test_initialize_gateway(existing, expected)
+
+ def test_initialize_gateway_add_if_blank(self):
+ existing = ("2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> "
+ " mtu 1500 qdisc pfifo_fast state UNKNOWN qlen 1000\n"
+ " link/ether de:ad:be:ef:be:ef brd ff:ff:ff:ff:ff:ff\n"
+ " inet6 dead::beef:dead:beef:dead/64 scope link\n"
+ " valid_lft forever preferred_lft forever\n")
+ expected = [
+ ('ip', 'addr', 'show', 'dev', 'eth0', 'scope', 'global'),
+ ('ip', 'addr', 'add', '192.168.1.1/24',
+ 'brd', '192.168.1.255', 'dev', 'eth0'),
+ ('ip', '-f', 'inet6', 'addr', 'change',
+ '2001:db8::/64', 'dev', 'eth0'),
+ ('ip', 'link', 'set', 'dev', 'eth0', 'promisc', 'on'),
+ ]
+ self._test_initialize_gateway(existing, expected)
diff --git a/nova/tests/test_utils.py b/nova/tests/test_utils.py index 1ba794a1a..19a15332d 100644 --- a/nova/tests/test_utils.py +++ b/nova/tests/test_utils.py @@ -20,10 +20,14 @@ import tempfile import nova from nova import exception +from nova import flags from nova import test from nova import utils +FLAGS = flags.FLAGS + + class ExecuteTestCase(test.TestCase): def test_retry_on_failure(self): fd, tmpfilename = tempfile.mkstemp() @@ -291,6 +295,11 @@ class GenericUtilsTestCase(test.TestCase): self.assertFalse(utils.bool_from_str(None)) self.assertFalse(utils.bool_from_str('junk')) + def test_generate_glance_url(self): + generated_url = utils.generate_glance_url() + actual_url = "http://%s:%d" % (FLAGS.glance_host, FLAGS.glance_port) + self.assertEqual(generated_url, actual_url) + class IsUUIDLikeTestCase(test.TestCase): def assertUUIDLike(self, val, expected): diff --git a/nova/utils.py b/nova/utils.py index 81157a4cd..57c93d9d0 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -910,3 +910,10 @@ def convert_to_list_dict(lst, label): if not isinstance(lst, list): lst = [lst] return [{label: x} for x in lst] + + +def generate_glance_url(): + """Generate the URL to glance.""" + # TODO(jk0): This will eventually need to take SSL into consideration + # when supported in glance. + return "http://%s:%d" % (FLAGS.glance_host, FLAGS.glance_port) diff --git a/nova/virt/driver.py b/nova/virt/driver.py index fc47d8d2d..7edb2cf1a 100644 --- a/nova/virt/driver.py +++ b/nova/virt/driver.py @@ -287,6 +287,14 @@ class ComputeDriver(object): # TODO(Vek): Need to pass context in for access to auth_token raise NotImplementedError() + def power_off(self, instance): + """Power off the specified instance.""" + raise NotImplementedError() + + def power_on(self, instance): + """Power on the specified instance""" + raise NotImplementedError() + def update_available_resource(self, ctxt, host): """Updates compute manager resource info on ComputeNode table. diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index 210b8fe65..988007bae 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -1002,6 +1002,16 @@ class VMOps(object): self._release_bootlock(original_vm_ref) self._start(instance, original_vm_ref) + def power_off(self, instance): + """Power off the specified instance.""" + vm_ref = self._get_vm_opaque_ref(instance) + self._shutdown(instance, vm_ref, hard=True) + + def power_on(self, instance): + """Power on the specified instance.""" + vm_ref = self._get_vm_opaque_ref(instance) + self._start(instance, vm_ref) + def poll_rescued_instances(self, timeout): """Look for expirable rescued instances. diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py index 7fc683a9f..79b02891d 100644 --- a/nova/virt/xenapi_conn.py +++ b/nova/virt/xenapi_conn.py @@ -250,6 +250,14 @@ class XenAPIConnection(driver.ComputeDriver): """Unrescue the specified instance""" self._vmops.unrescue(instance, _callback) + def power_off(self, instance): + """Power off the specified instance""" + self._vmops.power_off(instance) + + def power_on(self, instance): + """Power on the specified instance""" + self._vmops.power_on(instance) + def poll_rescued_instances(self, timeout): """Poll for rescued instances""" self._vmops.poll_rescued_instances(timeout) |
