diff options
author | Cory Wright <cory.wright@rackspace.com> | 2010-12-28 10:50:18 -0500 |
---|---|---|
committer | Cory Wright <cory.wright@rackspace.com> | 2010-12-28 10:50:18 -0500 |
commit | 3a3987d3acaa23a90131ab550e659f2611fed63a (patch) | |
tree | 8269abe1ec96b4c93ca46900a4e58ef9b37b1ea9 /nova | |
parent | 002bbfa7a648a1117e14713eab3ee3ee4b2b6d8e (diff) | |
parent | 675ca7c5f38af0fa1150936e881482aa20fdaa45 (diff) | |
download | nova-3a3987d3acaa23a90131ab550e659f2611fed63a.tar.gz nova-3a3987d3acaa23a90131ab550e659f2611fed63a.tar.xz nova-3a3987d3acaa23a90131ab550e659f2611fed63a.zip |
merge trunk
Diffstat (limited to 'nova')
-rw-r--r-- | nova/api/openstack/__init__.py | 2 | ||||
-rw-r--r-- | nova/api/openstack/images.py | 85 | ||||
-rw-r--r-- | nova/api/openstack/servers.py | 29 | ||||
-rw-r--r-- | nova/compute/api.py | 18 | ||||
-rw-r--r-- | nova/compute/manager.py | 33 | ||||
-rw-r--r-- | nova/compute/power_state.py | 4 | ||||
-rw-r--r-- | nova/tests/api/openstack/test_images.py | 18 | ||||
-rw-r--r-- | nova/tests/api/openstack/test_servers.py | 32 | ||||
-rw-r--r-- | nova/tests/test_compute.py | 12 | ||||
-rw-r--r-- | nova/utils.py | 3 | ||||
-rw-r--r-- | nova/virt/fake.py | 12 | ||||
-rw-r--r-- | nova/virt/libvirt_conn.py | 8 | ||||
-rw-r--r-- | nova/virt/xenapi/vm_utils.py | 6 | ||||
-rw-r--r-- | nova/virt/xenapi/vmops.py | 20 | ||||
-rw-r--r-- | nova/virt/xenapi_conn.py | 8 |
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) |