merge trunk

This commit is contained in:
Cory Wright
2010-12-28 10:50:18 -05:00
16 changed files with 273 additions and 29 deletions
+2
View File
@@ -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'},
+72 -13
View File
@@ -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):
+26 -3
View File
@@ -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()
+18
View File
@@ -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)
+33
View File
@@ -296,6 +296,39 @@ class ComputeManager(manager.Manager):
instance_id,
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."""
+3 -1
View File
@@ -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]
+16 -2
View File
@@ -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))
+30 -2
View File
@@ -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={},
+10 -2
View File
@@ -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()
+2 -1
View File
@@ -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)
+12
View File
@@ -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.
+8
View File
@@ -279,6 +279,14 @@ class LibvirtConnection(object):
def unpause(self, instance, callback):
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)
+5 -1
View File
@@ -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,
+20
View File
@@ -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)
+8
View File
@@ -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)
+8 -4
View File
@@ -21,6 +21,7 @@ function process_option {
-V|--virtual-env) let always_venv=1; let never_venv=0;;
-N|--no-virtual-env) let always_venv=0; let never_venv=1;;
-f|--force) let force=1;;
*) noseargs="$noseargs $1"
esac
}
@@ -29,15 +30,18 @@ with_venv=tools/with_venv.sh
always_venv=0
never_venv=0
force=0
noseargs=
for arg in "$@"; do
process_option $arg
done
NOSETESTS="nosetests -v $noseargs"
if [ $never_venv -eq 1 ]; then
# Just run the test suites in current environment
rm -f nova.sqlite
nosetests -v
$NOSETESTS
exit
fi
@@ -49,7 +53,7 @@ fi
if [ -e ${venv} ]; then
${with_venv} rm -f nova.sqlite
${with_venv} nosetests -v $@
${with_venv} $NOSETESTS
else
if [ $always_venv -eq 1 ]; then
# Automatically install the virtualenv
@@ -62,10 +66,10 @@ else
python tools/install_venv.py
else
rm -f nova.sqlite
nosetests -v
$NOSETESTS
exit
fi
fi
${with_venv} rm -f nova.sqlite
${with_venv} nosetests -v $@
${with_venv} $NOSETESTS
fi