summaryrefslogtreecommitdiffstats
path: root/nova
diff options
context:
space:
mode:
authorSandy Walsh <sandy.walsh@rackspace.com>2011-09-21 13:17:05 -0700
committerSandy Walsh <sandy.walsh@rackspace.com>2011-09-21 13:17:05 -0700
commitcd4050422340d27ba6161e2b9c4ccc7f057fb4bb (patch)
tree8c4951d69e2625d14eaa28db350d1a1876d6254f /nova
parent1051adcfc88c7a9eacef2a7000f43f9e66e1cb47 (diff)
parentd9752d46554ffa87360bfd740177b40871cfbea6 (diff)
trunk merge fixup
Diffstat (limited to 'nova')
-rw-r--r--nova/api/ec2/cloud.py1
-rw-r--r--nova/api/ec2/ec2utils.py2
-rw-r--r--nova/api/openstack/common.py3
-rw-r--r--nova/api/openstack/contrib/deferred_delete.py76
-rw-r--r--nova/api/openstack/images.py2
-rw-r--r--nova/api/openstack/servers.py13
-rw-r--r--nova/api/openstack/views/images.py16
-rw-r--r--nova/compute/api.py85
-rw-r--r--nova/compute/manager.py64
-rw-r--r--nova/compute/task_states.py2
-rw-r--r--nova/compute/vm_states.py1
-rw-r--r--nova/flags.py4
-rwxr-xr-xnova/network/linux_net.py40
-rw-r--r--nova/tests/api/openstack/test_extensions.py1
-rw-r--r--nova/tests/api/openstack/test_images.py102
-rw-r--r--nova/tests/integrated/integrated_helpers.py8
-rw-r--r--nova/tests/integrated/test_servers.py146
-rwxr-xr-xnova/tests/test_linux_net.py69
-rw-r--r--nova/tests/test_utils.py9
-rw-r--r--nova/utils.py7
-rw-r--r--nova/virt/driver.py8
-rw-r--r--nova/virt/xenapi/vmops.py10
-rw-r--r--nova/virt/xenapi_conn.py8
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)