summaryrefslogtreecommitdiffstats
path: root/nova
diff options
context:
space:
mode:
authorCory Wright <cory.wright@rackspace.com>2010-12-28 10:50:18 -0500
committerCory Wright <cory.wright@rackspace.com>2010-12-28 10:50:18 -0500
commit3a3987d3acaa23a90131ab550e659f2611fed63a (patch)
tree8269abe1ec96b4c93ca46900a4e58ef9b37b1ea9 /nova
parent002bbfa7a648a1117e14713eab3ee3ee4b2b6d8e (diff)
parent675ca7c5f38af0fa1150936e881482aa20fdaa45 (diff)
downloadnova-3a3987d3acaa23a90131ab550e659f2611fed63a.tar.gz
nova-3a3987d3acaa23a90131ab550e659f2611fed63a.tar.xz
nova-3a3987d3acaa23a90131ab550e659f2611fed63a.zip
merge trunk
Diffstat (limited to 'nova')
-rw-r--r--nova/api/openstack/__init__.py2
-rw-r--r--nova/api/openstack/images.py85
-rw-r--r--nova/api/openstack/servers.py29
-rw-r--r--nova/compute/api.py18
-rw-r--r--nova/compute/manager.py33
-rw-r--r--nova/compute/power_state.py4
-rw-r--r--nova/tests/api/openstack/test_images.py18
-rw-r--r--nova/tests/api/openstack/test_servers.py32
-rw-r--r--nova/tests/test_compute.py12
-rw-r--r--nova/utils.py3
-rw-r--r--nova/virt/fake.py12
-rw-r--r--nova/virt/libvirt_conn.py8
-rw-r--r--nova/virt/xenapi/vm_utils.py6
-rw-r--r--nova/virt/xenapi/vmops.py20
-rw-r--r--nova/virt/xenapi_conn.py8
15 files changed, 265 insertions, 25 deletions
diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py
index c49399f28..bebcdc18c 100644
--- a/nova/api/openstack/__init__.py
+++ b/nova/api/openstack/__init__.py
@@ -93,6 +93,8 @@ class APIRouter(wsgi.Router):
logging.debug("Including admin operations in API.")
server_members['pause'] = 'POST'
server_members['unpause'] = 'POST'
+ server_members['suspend'] = 'POST'
+ server_members['resume'] = 'POST'
mapper.resource("server", "servers", controller=servers.Controller(),
collection={'detail': 'GET'},
diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py
index d3312aba8..ba35fbc78 100644
--- a/nova/api/openstack/images.py
+++ b/nova/api/openstack/images.py
@@ -30,6 +30,65 @@ from nova.api.openstack import faults
FLAGS = flags.FLAGS
+def _translate_keys(item):
+ """
+ Maps key names to Rackspace-like attributes for return
+ also pares down attributes to those we want
+ item is a dict
+
+ Note: should be removed when the set of keys expected by the api
+ and the set of keys returned by the image service are equivalent
+
+ """
+ # TODO(tr3buchet): this map is specific to s3 object store,
+ # replace with a list of keys for _filter_keys later
+ mapped_keys = {'status': 'imageState',
+ 'id': 'imageId',
+ 'name': 'imageLocation'}
+
+ mapped_item = {}
+ # TODO(tr3buchet):
+ # this chunk of code works with s3 and the local image service/glance
+ # when we switch to glance/local image service it can be replaced with
+ # a call to _filter_keys, and mapped_keys can be changed to a list
+ try:
+ for k, v in mapped_keys.iteritems():
+ # map s3 fields
+ mapped_item[k] = item[v]
+ except KeyError:
+ # return only the fields api expects
+ mapped_item = _filter_keys(item, mapped_keys.keys())
+
+ return mapped_item
+
+
+def _translate_status(item):
+ """
+ Translates status of image to match current Rackspace api bindings
+ item is a dict
+
+ Note: should be removed when the set of statuses expected by the api
+ and the set of statuses returned by the image service are equivalent
+
+ """
+ status_mapping = {
+ 'pending': 'queued',
+ 'decrypting': 'preparing',
+ 'untarring': 'saving',
+ 'available': 'active'}
+ item['status'] = status_mapping[item['status']]
+ return item
+
+
+def _filter_keys(item, keys):
+ """
+ Filters all model attributes except for keys
+ item is a dict
+
+ """
+ return dict((k, v) for k, v in item.iteritems() if k in keys)
+
+
class Controller(wsgi.Controller):
_serialization_metadata = {
@@ -42,25 +101,25 @@ class Controller(wsgi.Controller):
self._service = utils.import_object(FLAGS.image_service)
def index(self, req):
- """Return all public images in brief."""
- return dict(images=[dict(id=img['id'], name=img['name'])
- for img in self.detail(req)['images']])
+ """Return all public images in brief"""
+ items = self._service.index(req.environ['nova.context'])
+ items = common.limited(items, req)
+ items = [_filter_keys(item, ('id', 'name')) for item in items]
+ return dict(images=items)
def detail(self, req):
- """Return all public images in detail."""
+ """Return all public images in detail"""
try:
- images = self._service.detail(req.environ['nova.context'])
- images = common.limited(images, req)
+ items = self._service.detail(req.environ['nova.context'])
except NotImplementedError:
- # Emulate detail() using repeated calls to show()
- ctxt = req.environ['nova.context']
- images = self._service.index(ctxt)
- images = common.limited(images, req)
- images = [self._service.show(ctxt, i['id']) for i in images]
- return dict(images=images)
+ items = self._service.index(req.environ['nova.context'])
+ items = common.limited(items, req)
+ items = [_translate_keys(item) for item in items]
+ items = [_translate_status(item) for item in items]
+ return dict(images=items)
def show(self, req, id):
- """Return data about the given image id."""
+ """Return data about the given image id"""
return dict(image=self._service.show(req.environ['nova.context'], id))
def delete(self, req, id):
diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py
index 8d60e2cab..10c397384 100644
--- a/nova/api/openstack/servers.py
+++ b/nova/api/openstack/servers.py
@@ -46,7 +46,8 @@ def _entity_detail(inst):
power_state.NOSTATE: 'build',
power_state.RUNNING: 'active',
power_state.BLOCKED: 'active',
- power_state.PAUSED: 'suspended',
+ power_state.SUSPENDED: 'suspended',
+ power_state.PAUSED: 'error',
power_state.SHUTDOWN: 'active',
power_state.SHUTOFF: 'active',
power_state.CRASHED: 'error'}
@@ -182,7 +183,7 @@ class Controller(wsgi.Controller):
self.compute_api.pause(ctxt, id)
except:
readable = traceback.format_exc()
- logging.error("Compute.api::pause %s", readable)
+ logging.error(_("Compute.api::pause %s"), readable)
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
@@ -193,6 +194,28 @@ class Controller(wsgi.Controller):
self.compute_api.unpause(ctxt, id)
except:
readable = traceback.format_exc()
- logging.error("Compute.api::unpause %s", readable)
+ logging.error(_("Compute.api::unpause %s"), readable)
+ return faults.Fault(exc.HTTPUnprocessableEntity())
+ return exc.HTTPAccepted()
+
+ def suspend(self, req, id):
+ """permit admins to suspend the server"""
+ context = req.environ['nova.context']
+ try:
+ self.compute_api.suspend(context, id)
+ except:
+ readable = traceback.format_exc()
+ logging.error(_("compute.api::suspend %s"), readable)
+ return faults.Fault(exc.HTTPUnprocessableEntity())
+ return exc.HTTPAccepted()
+
+ def resume(self, req, id):
+ """permit admins to resume the server from suspend"""
+ context = req.environ['nova.context']
+ try:
+ self.compute_api.resume(context, id)
+ except:
+ readable = traceback.format_exc()
+ logging.error(_("compute.api::resume %s"), readable)
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
diff --git a/nova/compute/api.py b/nova/compute/api.py
index a3245940e..a47703461 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -284,6 +284,24 @@ class ComputeAPI(base.Base):
{"method": "unpause_instance",
"args": {"instance_id": instance['id']}})
+ def suspend(self, context, instance_id):
+ """suspend the instance with instance_id"""
+ instance = self.db.instance_get_by_internal_id(context, instance_id)
+ host = instance['host']
+ rpc.cast(context,
+ self.db.queue_get_for(context, FLAGS.compute_topic, host),
+ {"method": "suspend_instance",
+ "args": {"instance_id": instance['id']}})
+
+ def resume(self, context, instance_id):
+ """resume the instance with instance_id"""
+ instance = self.db.instance_get_by_internal_id(context, instance_id)
+ host = instance['host']
+ rpc.cast(context,
+ self.db.queue_get_for(context, FLAGS.compute_topic, host),
+ {"method": "resume_instance",
+ "args": {"instance_id": instance['id']}})
+
def rescue(self, context, instance_id):
"""Rescue the given instance."""
instance = self.db.instance_get_by_internal_id(context, instance_id)
diff --git a/nova/compute/manager.py b/nova/compute/manager.py
index ff8202cca..70b175e7c 100644
--- a/nova/compute/manager.py
+++ b/nova/compute/manager.py
@@ -297,6 +297,39 @@ class ComputeManager(manager.Manager):
result))
@exception.wrap_exception
+ def suspend_instance(self, context, instance_id):
+ """suspend the instance with instance_id"""
+ context = context.elevated()
+ instance_ref = self.db.instance_get(context, instance_id)
+
+ logging.debug(_('instance %s: suspending'),
+ instance_ref['internal_id'])
+ self.db.instance_set_state(context, instance_id,
+ power_state.NOSTATE,
+ 'suspending')
+ self.driver.suspend(instance_ref,
+ lambda result: self._update_state_callback(self,
+ context,
+ instance_id,
+ result))
+
+ @exception.wrap_exception
+ def resume_instance(self, context, instance_id):
+ """resume the suspended instance with instance_id"""
+ context = context.elevated()
+ instance_ref = self.db.instance_get(context, instance_id)
+
+ logging.debug(_('instance %s: resuming'), instance_ref['internal_id'])
+ self.db.instance_set_state(context, instance_id,
+ power_state.NOSTATE,
+ 'resuming')
+ self.driver.resume(instance_ref,
+ lambda result: self._update_state_callback(self,
+ context,
+ instance_id,
+ result))
+
+ @exception.wrap_exception
def get_console_output(self, context, instance_id):
"""Send the console output for an instance."""
context = context.elevated()
diff --git a/nova/compute/power_state.py b/nova/compute/power_state.py
index cefdf2d9e..37039d2ec 100644
--- a/nova/compute/power_state.py
+++ b/nova/compute/power_state.py
@@ -26,6 +26,7 @@ PAUSED = 0x03
SHUTDOWN = 0x04
SHUTOFF = 0x05
CRASHED = 0x06
+SUSPENDED = 0x07
def name(code):
@@ -36,5 +37,6 @@ def name(code):
PAUSED: 'paused',
SHUTDOWN: 'shutdown',
SHUTOFF: 'shutdown',
- CRASHED: 'crashed'}
+ CRASHED: 'crashed',
+ SUSPENDED: 'suspended'}
return d[code]
diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py
index f610cbf9c..1b4031217 100644
--- a/nova/tests/api/openstack/test_images.py
+++ b/nova/tests/api/openstack/test_images.py
@@ -223,6 +223,20 @@ class ImageControllerWithGlanceServiceTest(unittest.TestCase):
res = req.get_response(nova.api.API('os'))
res_dict = json.loads(res.body)
+ def _is_equivalent_subset(x, y):
+ if set(x) <= set(y):
+ for k, v in x.iteritems():
+ if x[k] != y[k]:
+ if x[k] == 'active' and y[k] == 'available':
+ continue
+ return False
+ return True
+ return False
+
for image in res_dict['images']:
- self.assertEquals(1, self.IMAGE_FIXTURES.count(image),
- "image %s not in fixtures!" % str(image))
+ for image_fixture in self.IMAGE_FIXTURES:
+ if _is_equivalent_subset(image, image_fixture):
+ break
+ else:
+ self.assertEquals(1, 2, "image %s not in fixtures!" %
+ str(image))
diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py
index 3820f5f27..5d23db588 100644
--- a/nova/tests/api/openstack/test_servers.py
+++ b/nova/tests/api/openstack/test_servers.py
@@ -88,9 +88,13 @@ class ServersTest(unittest.TestCase):
self.stubs.Set(nova.db.api, 'instance_get_floating_address',
instance_address)
self.stubs.Set(nova.compute.api.ComputeAPI, 'pause',
- fake_compute_api)
+ fake_compute_api)
self.stubs.Set(nova.compute.api.ComputeAPI, 'unpause',
- fake_compute_api)
+ fake_compute_api)
+ self.stubs.Set(nova.compute.api.ComputeAPI, 'suspend',
+ fake_compute_api)
+ self.stubs.Set(nova.compute.api.ComputeAPI, 'resume',
+ fake_compute_api)
self.allow_admin = FLAGS.allow_admin_api
def tearDown(self):
@@ -246,6 +250,30 @@ class ServersTest(unittest.TestCase):
res = req.get_response(nova.api.API('os'))
self.assertEqual(res.status_int, 202)
+ def test_server_suspend(self):
+ FLAGS.allow_admin_api = True
+ body = dict(server=dict(
+ name='server_test', imageId=2, flavorId=2, metadata={},
+ personality={}))
+ req = webob.Request.blank('/v1.0/servers/1/suspend')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = json.dumps(body)
+ res = req.get_response(nova.api.API('os'))
+ self.assertEqual(res.status_int, 202)
+
+ def test_server_resume(self):
+ FLAGS.allow_admin_api = True
+ body = dict(server=dict(
+ name='server_test', imageId=2, flavorId=2, metadata={},
+ personality={}))
+ req = webob.Request.blank('/v1.0/servers/1/resume')
+ req.method = 'POST'
+ req.content_type = 'application/json'
+ req.body = json.dumps(body)
+ res = req.get_response(nova.api.API('os'))
+ self.assertEqual(res.status_int, 202)
+
def test_server_reboot(self):
body = dict(server=dict(
name='server_test', imageId=2, flavorId=2, metadata={},
diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py
index 348bb3351..bcb8a1526 100644
--- a/nova/tests/test_compute.py
+++ b/nova/tests/test_compute.py
@@ -101,13 +101,13 @@ class ComputeTestCase(test.TestCase):
self.compute.run_instance(self.context, instance_id)
instances = db.instance_get_all(context.get_admin_context())
- logging.info("Running instances: %s", instances)
+ logging.info(_("Running instances: %s"), instances)
self.assertEqual(len(instances), 1)
self.compute.terminate_instance(self.context, instance_id)
instances = db.instance_get_all(context.get_admin_context())
- logging.info("After terminating instances: %s", instances)
+ logging.info(_("After terminating instances: %s"), instances)
self.assertEqual(len(instances), 0)
def test_run_terminate_timestamps(self):
@@ -136,6 +136,14 @@ class ComputeTestCase(test.TestCase):
self.compute.unpause_instance(self.context, instance_id)
self.compute.terminate_instance(self.context, instance_id)
+ def test_suspend(self):
+ """ensure instance can be suspended"""
+ instance_id = self._create_instance()
+ self.compute.run_instance(self.context, instance_id)
+ self.compute.suspend_instance(self.context, instance_id)
+ self.compute.resume_instance(self.context, instance_id)
+ self.compute.terminate_instance(self.context, instance_id)
+
def test_reboot(self):
"""Ensure instance can be rebooted"""
instance_id = self._create_instance()
diff --git a/nova/utils.py b/nova/utils.py
index b9045a50c..15112faa2 100644
--- a/nova/utils.py
+++ b/nova/utils.py
@@ -48,7 +48,8 @@ def import_class(import_str):
try:
__import__(mod_str)
return getattr(sys.modules[mod_str], class_str)
- except (ImportError, ValueError, AttributeError):
+ except (ImportError, ValueError, AttributeError), exc:
+ logging.debug(_('Inner Exception: %s'), exc)
raise exception.NotFound(_('Class %s cannot be found') % class_str)
diff --git a/nova/virt/fake.py b/nova/virt/fake.py
index 238acf798..706888b0d 100644
--- a/nova/virt/fake.py
+++ b/nova/virt/fake.py
@@ -148,6 +148,18 @@ class FakeConnection(object):
"""
pass
+ def suspend(self, instance, callback):
+ """
+ suspend the specified instance
+ """
+ pass
+
+ def resume(self, instance, callback):
+ """
+ resume the specified instance
+ """
+ pass
+
def destroy(self, instance):
"""
Destroy (shutdown and delete) the specified instance.
diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py
index 67b9fc47e..65cf65098 100644
--- a/nova/virt/libvirt_conn.py
+++ b/nova/virt/libvirt_conn.py
@@ -280,6 +280,14 @@ class LibvirtConnection(object):
raise exception.APIError("unpause not supported for libvirt.")
@exception.wrap_exception
+ def suspend(self, instance, callback):
+ raise exception.APIError("suspend not supported for libvirt")
+
+ @exception.wrap_exception
+ def resume(self, instance, callback):
+ raise exception.APIError("resume not supported for libvirt")
+
+ @exception.wrap_exception
def rescue(self, instance):
self.destroy(instance, False)
diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py
index 2f02f4677..47fb6db53 100644
--- a/nova/virt/xenapi/vm_utils.py
+++ b/nova/virt/xenapi/vm_utils.py
@@ -39,7 +39,7 @@ XENAPI_POWER_STATE = {
'Halted': power_state.SHUTDOWN,
'Running': power_state.RUNNING,
'Paused': power_state.PAUSED,
- 'Suspended': power_state.SHUTDOWN, # FIXME
+ 'Suspended': power_state.SUSPENDED,
'Crashed': power_state.CRASHED}
@@ -283,6 +283,10 @@ class VMHelper(HelperBase):
@classmethod
def compile_info(cls, record):
"""Fill record with VM status information"""
+ logging.info(_("(VM_UTILS) xenserver vm state -> |%s|"),
+ record['power_state'])
+ logging.info(_("(VM_UTILS) xenapi power_state -> |%s|"),
+ XENAPI_POWER_STATE[record['power_state']])
return {'state': XENAPI_POWER_STATE[record['power_state']],
'max_mem': long(record['memory_static_max']) >> 10,
'mem': long(record['memory_dynamic_max']) >> 10,
diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py
index b1de73641..ba502ffa2 100644
--- a/nova/virt/xenapi/vmops.py
+++ b/nova/virt/xenapi/vmops.py
@@ -188,6 +188,26 @@ class VMOps(object):
task = self._session.call_xenapi('Async.VM.unpause', vm)
self._wait_with_callback(instance.id, task, callback)
+ def suspend(self, instance, callback):
+ """suspend the specified instance"""
+ instance_name = instance.name
+ vm = VMHelper.lookup(self._session, instance_name)
+ if vm is None:
+ raise Exception(_("suspend: instance not present %s") %
+ instance_name)
+ task = self._session.call_xenapi('Async.VM.suspend', vm)
+ self._wait_with_callback(task, callback)
+
+ def resume(self, instance, callback):
+ """resume the specified instance"""
+ instance_name = instance.name
+ vm = VMHelper.lookup(self._session, instance_name)
+ if vm is None:
+ raise Exception(_("resume: instance not present %s") %
+ instance_name)
+ task = self._session.call_xenapi('Async.VM.resume', vm, False, True)
+ self._wait_with_callback(task, callback)
+
def get_info(self, instance_id):
"""Return data about VM instance"""
vm = VMHelper.lookup(self._session, instance_id)
diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py
index 11c66c974..7f03d6c2b 100644
--- a/nova/virt/xenapi_conn.py
+++ b/nova/virt/xenapi_conn.py
@@ -147,6 +147,14 @@ class XenAPIConnection(object):
"""Unpause paused VM instance"""
self._vmops.unpause(instance, callback)
+ def suspend(self, instance, callback):
+ """suspend the specified instance"""
+ self._vmops.suspend(instance, callback)
+
+ def resume(self, instance, callback):
+ """resume the specified instance"""
+ self._vmops.resume(instance, callback)
+
def get_info(self, instance_id):
"""Return data about VM instance"""
return self._vmops.get_info(instance_id)