Merging trunk
This commit is contained in:
@@ -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'},
|
||||
|
||||
@@ -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."""
|
||||
ctxt = req.environ['nova.context']
|
||||
"""Return all public images in detail"""
|
||||
try:
|
||||
images = self._service.detail(ctxt)
|
||||
images = common.limited(images, req)
|
||||
items = self._service.detail(req.environ['nova.context'])
|
||||
except NotImplementedError:
|
||||
# Emulate detail() using repeated calls to show()
|
||||
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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -293,6 +293,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)
|
||||
|
||||
@@ -317,6 +317,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."""
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -224,6 +224,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))
|
||||
|
||||
@@ -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={},
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -162,6 +162,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.
|
||||
|
||||
@@ -286,6 +286,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)
|
||||
|
||||
@@ -42,7 +42,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}
|
||||
|
||||
|
||||
@@ -331,6 +331,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,
|
||||
|
||||
@@ -239,6 +239,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)
|
||||
|
||||
@@ -155,6 +155,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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user