summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSandy Walsh <sandy.walsh@rackspace.com>2010-12-20 20:26:59 +0000
committerTarmac <>2010-12-20 20:26:59 +0000
commit85b07de25094b35928f2b67d1be11748e2af6713 (patch)
tree5bfcfc627e4ec9fa000a271008469bc69bf2ec4c
parent800ecbd713c55d7410d6eb860a439cb87468e7ad (diff)
parent8ddae1280da59a0e86e1daf1c8de97248ef6cb13 (diff)
Adds support for Pause and Unpause of xenserver instances.
Pause freezes the vm, keeping resources in-memory. I used this version of python cloudservers to test with: https://github.com/SandyWalsh/python-cloudservers url's are http://a.b.c.d/v1.0/servers/#######/pause and http://a.b.c.d/v1.0/servers/#######/unpause Note: no support for libvirt.
-rw-r--r--nova/adminclient.py1
-rw-r--r--nova/api/ec2/admin.py1
-rw-r--r--nova/api/openstack/__init__.py13
-rw-r--r--nova/api/openstack/backup_schedules.py1
-rw-r--r--nova/api/openstack/servers.py29
-rw-r--r--nova/compute/api.py18
-rw-r--r--nova/compute/manager.py41
-rw-r--r--nova/db/sqlalchemy/api.py4
-rw-r--r--nova/exception.py3
-rw-r--r--nova/tests/api/openstack/test_servers.py37
-rw-r--r--nova/tests/compute_unittest.py8
-rw-r--r--nova/twistd.py2
-rw-r--r--nova/virt/fake.py13
-rw-r--r--nova/virt/libvirt_conn.py9
-rw-r--r--nova/virt/xenapi/network_utils.py1
-rw-r--r--nova/virt/xenapi/vm_utils.py1
-rw-r--r--nova/virt/xenapi/vmops.py27
-rw-r--r--nova/virt/xenapi/volumeops.py1
-rw-r--r--nova/virt/xenapi_conn.py10
19 files changed, 213 insertions, 7 deletions
diff --git a/nova/adminclient.py b/nova/adminclient.py
index 5a62cce7d..6ae9f0c0f 100644
--- a/nova/adminclient.py
+++ b/nova/adminclient.py
@@ -194,6 +194,7 @@ class HostInfo(object):
class NovaAdminClient(object):
+
def __init__(
self,
clc_url=DEFAULT_CLC_URL,
diff --git a/nova/api/ec2/admin.py b/nova/api/ec2/admin.py
index 1c6ab688d..fac01369e 100644
--- a/nova/api/ec2/admin.py
+++ b/nova/api/ec2/admin.py
@@ -168,6 +168,7 @@ class AdminController(object):
# FIXME(vish): these host commands don't work yet, perhaps some of the
# required data can be retrieved from service objects?
+
def describe_hosts(self, _context, **_kwargs):
"""Returns status info for all nodes. Includes:
* Disk Space
diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py
index b9ecbd9b8..210df8d24 100644
--- a/nova/api/openstack/__init__.py
+++ b/nova/api/openstack/__init__.py
@@ -170,9 +170,16 @@ class APIRouter(wsgi.Router):
def __init__(self):
mapper = routes.Mapper()
+
+ server_members = {'action': 'POST'}
+ if FLAGS.allow_admin_api:
+ logging.debug("Including admin operations in API.")
+ server_members['pause'] = 'POST'
+ server_members['unpause'] = 'POST'
+
mapper.resource("server", "servers", controller=servers.Controller(),
collection={'detail': 'GET'},
- member={'action': 'POST'})
+ member=server_members)
mapper.resource("backup_schedule", "backup_schedules",
controller=backup_schedules.Controller(),
@@ -186,10 +193,6 @@ class APIRouter(wsgi.Router):
mapper.resource("sharedipgroup", "sharedipgroups",
controller=sharedipgroups.Controller())
- if FLAGS.allow_admin_api:
- logging.debug("Including admin operations in API.")
- # TODO: Place routes for admin operations here.
-
super(APIRouter, self).__init__(mapper)
diff --git a/nova/api/openstack/backup_schedules.py b/nova/api/openstack/backup_schedules.py
index 3ed691d7b..fc70b5c6c 100644
--- a/nova/api/openstack/backup_schedules.py
+++ b/nova/api/openstack/backup_schedules.py
@@ -24,6 +24,7 @@ import nova.image.service
class Controller(wsgi.Controller):
+
def __init__(self):
pass
diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py
index 7704f48f1..5c3322f7c 100644
--- a/nova/api/openstack/servers.py
+++ b/nova/api/openstack/servers.py
@@ -15,6 +15,9 @@
# License for the specific language governing permissions and limitations
# under the License.
+import logging
+import traceback
+
from webob import exc
from nova import exception
@@ -27,6 +30,10 @@ from nova.compute import power_state
import nova.api.openstack
+LOG = logging.getLogger('server')
+LOG.setLevel(logging.DEBUG)
+
+
def _entity_list(entities):
""" Coerces a list of servers into proper dictionary format """
return dict(servers=entities)
@@ -166,3 +173,25 @@ class Controller(wsgi.Controller):
except:
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
+
+ def pause(self, req, id):
+ """ Permit Admins to Pause the server. """
+ ctxt = req.environ['nova.context']
+ try:
+ self.compute_api.pause(ctxt, id)
+ except:
+ readable = traceback.format_exc()
+ logging.error("Compute.api::pause %s", readable)
+ return faults.Fault(exc.HTTPUnprocessableEntity())
+ return exc.HTTPAccepted()
+
+ def unpause(self, req, id):
+ """ Permit Admins to Unpause the server. """
+ ctxt = req.environ['nova.context']
+ try:
+ self.compute_api.unpause(ctxt, id)
+ except:
+ readable = traceback.format_exc()
+ logging.error("Compute.api::unpause %s", readable)
+ return faults.Fault(exc.HTTPUnprocessableEntity())
+ return exc.HTTPAccepted()
diff --git a/nova/compute/api.py b/nova/compute/api.py
index 8e0efa4cc..7420c40d2 100644
--- a/nova/compute/api.py
+++ b/nova/compute/api.py
@@ -275,6 +275,24 @@ class ComputeAPI(base.Base):
{"method": "reboot_instance",
"args": {"instance_id": instance['id']}})
+ def pause(self, context, instance_id):
+ """Pause the given instance."""
+ 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": "pause_instance",
+ "args": {"instance_id": instance['id']}})
+
+ def unpause(self, context, instance_id):
+ """Unpause the given instance."""
+ 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": "unpause_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 7eb60e262..a84af6bb9 100644
--- a/nova/compute/manager.py
+++ b/nova/compute/manager.py
@@ -186,6 +186,47 @@ class ComputeManager(manager.Manager):
self.driver.unrescue(instance_ref)
self._update_state(context, instance_id)
+ @staticmethod
+ def _update_state_callback(self, context, instance_id, result):
+ """Update instance state when async task completes."""
+ self._update_state(context, instance_id)
+
+ @exception.wrap_exception
+ def pause_instance(self, context, instance_id):
+ """Pause an instance on this server."""
+ context = context.elevated()
+ instance_ref = self.db.instance_get(context, instance_id)
+
+ logging.debug('instance %s: pausing',
+ instance_ref['internal_id'])
+ self.db.instance_set_state(context,
+ instance_id,
+ power_state.NOSTATE,
+ 'pausing')
+ self.driver.pause(instance_ref,
+ lambda result: self._update_state_callback(self,
+ context,
+ instance_id,
+ result))
+
+ @exception.wrap_exception
+ def unpause_instance(self, context, instance_id):
+ """Unpause a paused instance on this server."""
+ context = context.elevated()
+ instance_ref = self.db.instance_get(context, instance_id)
+
+ logging.debug('instance %s: unpausing',
+ instance_ref['internal_id'])
+ self.db.instance_set_state(context,
+ instance_id,
+ power_state.NOSTATE,
+ 'unpausing')
+ self.driver.unpause(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."""
diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py
index 55036d1d1..935063609 100644
--- a/nova/db/sqlalchemy/api.py
+++ b/nova/db/sqlalchemy/api.py
@@ -528,6 +528,8 @@ def fixed_ip_update(context, address, values):
#TODO(gundlach): instance_create and volume_create are nearly identical
#and should be refactored. I expect there are other copy-and-paste
#functions between the two of them as well.
+
+
@require_context
def instance_create(context, values):
"""Create a new Instance record in the database.
@@ -913,6 +915,8 @@ def network_get(context, network_id, session=None):
# NOTE(vish): pylint complains because of the long method name, but
# it fits with the names of the rest of the methods
# pylint: disable-msg=C0103
+
+
@require_admin_context
def network_get_associated_fixed_ips(context, network_id):
session = get_session()
diff --git a/nova/exception.py b/nova/exception.py
index 6d6c37338..9af4017ba 100644
--- a/nova/exception.py
+++ b/nova/exception.py
@@ -27,6 +27,7 @@ import traceback
class ProcessExecutionError(IOError):
+
def __init__(self, stdout=None, stderr=None, exit_code=None, cmd=None,
description=None):
if description is None:
@@ -39,11 +40,13 @@ class ProcessExecutionError(IOError):
class Error(Exception):
+
def __init__(self, message=None):
super(Error, self).__init__(message)
class ApiError(Error):
+
def __init__(self, message='Unknown', code='Unknown'):
self.message = message
self.code = code
diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py
index 8444b6fce..3820f5f27 100644
--- a/nova/tests/api/openstack/test_servers.py
+++ b/nova/tests/api/openstack/test_servers.py
@@ -56,11 +56,16 @@ def instance_address(context, instance_id):
def stub_instance(id, user_id=1):
- return Instance(id=id + 123456, state=0, image_id=10, user_id=user_id,
+ return Instance(id=int(id) + 123456, state=0, image_id=10, user_id=user_id,
display_name='server%s' % id, internal_id=id)
+def fake_compute_api(cls, req, id):
+ return True
+
+
class ServersTest(unittest.TestCase):
+
def setUp(self):
self.stubs = stubout.StubOutForTesting()
fakes.FakeAuthManager.auth_data = {}
@@ -82,9 +87,15 @@ class ServersTest(unittest.TestCase):
instance_address)
self.stubs.Set(nova.db.api, 'instance_get_floating_address',
instance_address)
+ self.stubs.Set(nova.compute.api.ComputeAPI, 'pause',
+ fake_compute_api)
+ self.stubs.Set(nova.compute.api.ComputeAPI, 'unpause',
+ fake_compute_api)
+ self.allow_admin = FLAGS.allow_admin_api
def tearDown(self):
self.stubs.UnsetAll()
+ FLAGS.allow_admin_api = self.allow_admin
def test_get_server_by_id(self):
req = webob.Request.blank('/v1.0/servers/1')
@@ -211,6 +222,30 @@ class ServersTest(unittest.TestCase):
self.assertEqual(s['imageId'], 10)
i += 1
+ def test_server_pause(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/pause')
+ 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_unpause(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/unpause')
+ 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/compute_unittest.py b/nova/tests/compute_unittest.py
index c6353d357..187ca31de 100644
--- a/nova/tests/compute_unittest.py
+++ b/nova/tests/compute_unittest.py
@@ -127,6 +127,14 @@ class ComputeTestCase(test.TestCase):
self.assert_(instance_ref['launched_at'] < terminate)
self.assert_(instance_ref['deleted_at'] > terminate)
+ def test_pause(self):
+ """Ensure instance can be paused"""
+ instance_id = self._create_instance()
+ self.compute.run_instance(self.context, instance_id)
+ self.compute.pause_instance(self.context, instance_id)
+ self.compute.unpause_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/twistd.py b/nova/twistd.py
index cb5648ce6..e6c3101f1 100644
--- a/nova/twistd.py
+++ b/nova/twistd.py
@@ -43,7 +43,7 @@ else:
FLAGS = flags.FLAGS
-flags.DEFINE_string('logdir', None, 'directory to keep log files in '
+flags.DEFINE_string('logdir', None, 'directory to keep log files in '
'(will be prepended to $logfile)')
diff --git a/nova/virt/fake.py b/nova/virt/fake.py
index 77bc926c2..55c6dcef9 100644
--- a/nova/virt/fake.py
+++ b/nova/virt/fake.py
@@ -130,6 +130,18 @@ class FakeConnection(object):
"""
pass
+ def pause(self, instance, callback):
+ """
+ Pause the specified instance.
+ """
+ pass
+
+ def unpause(self, instance, callback):
+ """
+ Unpause the specified instance.
+ """
+ pass
+
def destroy(self, instance):
"""
Destroy (shutdown and delete) the specified instance.
@@ -243,5 +255,6 @@ class FakeConnection(object):
class FakeInstance(object):
+
def __init__(self):
self._state = power_state.NOSTATE
diff --git a/nova/virt/libvirt_conn.py b/nova/virt/libvirt_conn.py
index 5a8c71850..14ec0c877 100644
--- a/nova/virt/libvirt_conn.py
+++ b/nova/virt/libvirt_conn.py
@@ -118,6 +118,7 @@ def get_connection(read_only):
class LibvirtConnection(object):
+
def __init__(self, read_only):
(self.libvirt_uri,
template_file,
@@ -291,6 +292,14 @@ class LibvirtConnection(object):
return timer.start(interval=0.5, now=True)
@exception.wrap_exception
+ def pause(self, instance, callback):
+ raise exception.APIError("pause not supported for libvirt.")
+
+ @exception.wrap_exception
+ def unpause(self, instance, callback):
+ raise exception.APIError("unpause not supported for libvirt.")
+
+ @exception.wrap_exception
def rescue(self, instance):
self.destroy(instance, False)
diff --git a/nova/virt/xenapi/network_utils.py b/nova/virt/xenapi/network_utils.py
index 012954394..ce2c68ce0 100644
--- a/nova/virt/xenapi/network_utils.py
+++ b/nova/virt/xenapi/network_utils.py
@@ -25,6 +25,7 @@ class NetworkHelper():
"""
The class that wraps the helper methods together.
"""
+
def __init__(self):
return
diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py
index 2f5d78e75..017a6eab0 100644
--- a/nova/virt/xenapi/vm_utils.py
+++ b/nova/virt/xenapi/vm_utils.py
@@ -47,6 +47,7 @@ class VMHelper():
"""
The class that wraps the helper methods together.
"""
+
def __init__(self):
return
diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py
index 3034df9e1..a18eacf07 100644
--- a/nova/virt/xenapi/vmops.py
+++ b/nova/virt/xenapi/vmops.py
@@ -34,6 +34,7 @@ class VMOps(object):
"""
Management class for VM-related tasks
"""
+
def __init__(self, session):
global XenAPI
if XenAPI is None:
@@ -117,6 +118,32 @@ class VMOps(object):
except XenAPI.Failure, exc:
logging.warn(exc)
+ def _wait_with_callback(self, task, callback):
+ ret = None
+ try:
+ ret = self._session.wait_for_task(task)
+ except XenAPI.Failure, exc:
+ logging.warn(exc)
+ callback(ret)
+
+ def pause(self, instance, callback):
+ """ Pause VM instance """
+ instance_name = instance.name
+ vm = VMHelper.lookup(self._session, instance_name)
+ if vm is None:
+ raise Exception('instance not present %s' % instance_name)
+ task = self._session.call_xenapi('Async.VM.pause', vm)
+ self._wait_with_callback(task, callback)
+
+ def unpause(self, instance, callback):
+ """ Unpause VM instance """
+ instance_name = instance.name
+ vm = VMHelper.lookup(self._session, instance_name)
+ if vm is None:
+ raise Exception('instance not present %s' % instance_name)
+ task = self._session.call_xenapi('Async.VM.unpause', vm)
+ self._wait_with_callback(task, callback)
+
def get_info(self, instance_id):
""" Return data about VM instance """
vm = VMHelper.lookup_blocking(self._session, instance_id)
diff --git a/nova/virt/xenapi/volumeops.py b/nova/virt/xenapi/volumeops.py
index a4c7a3861..1943ccab0 100644
--- a/nova/virt/xenapi/volumeops.py
+++ b/nova/virt/xenapi/volumeops.py
@@ -20,6 +20,7 @@ Management class for Storage-related functions (attach, detach, etc).
class VolumeOps(object):
+
def __init__(self, session):
self._session = session
diff --git a/nova/virt/xenapi_conn.py b/nova/virt/xenapi_conn.py
index 6beb08f5e..21ed2cd65 100644
--- a/nova/virt/xenapi_conn.py
+++ b/nova/virt/xenapi_conn.py
@@ -102,6 +102,7 @@ def get_connection(_):
class XenAPIConnection(object):
""" A connection to XenServer or Xen Cloud Platform """
+
def __init__(self, url, user, pw):
session = XenAPISession(url, user, pw)
self._vmops = VMOps(session)
@@ -123,6 +124,14 @@ class XenAPIConnection(object):
""" Destroy VM instance """
self._vmops.destroy(instance)
+ def pause(self, instance, callback):
+ """ Pause VM instance """
+ self._vmops.pause(instance, callback)
+
+ def unpause(self, instance, callback):
+ """ Unpause paused VM instance """
+ self._vmops.unpause(instance, callback)
+
def get_info(self, instance_id):
""" Return data about VM instance """
return self._vmops.get_info(instance_id)
@@ -148,6 +157,7 @@ class XenAPIConnection(object):
class XenAPISession(object):
""" The session to invoke XenAPI SDK calls """
+
def __init__(self, url, user, pw):
self._session = XenAPI.Session(url)
self._session.login_with_password(user, pw)