diff options
| author | Sandy Walsh <sandy.walsh@rackspace.com> | 2010-12-20 20:26:59 +0000 |
|---|---|---|
| committer | Tarmac <> | 2010-12-20 20:26:59 +0000 |
| commit | 85b07de25094b35928f2b67d1be11748e2af6713 (patch) | |
| tree | 5bfcfc627e4ec9fa000a271008469bc69bf2ec4c | |
| parent | 800ecbd713c55d7410d6eb860a439cb87468e7ad (diff) | |
| parent | 8ddae1280da59a0e86e1daf1c8de97248ef6cb13 (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.py | 1 | ||||
| -rw-r--r-- | nova/api/ec2/admin.py | 1 | ||||
| -rw-r--r-- | nova/api/openstack/__init__.py | 13 | ||||
| -rw-r--r-- | nova/api/openstack/backup_schedules.py | 1 | ||||
| -rw-r--r-- | nova/api/openstack/servers.py | 29 | ||||
| -rw-r--r-- | nova/compute/api.py | 18 | ||||
| -rw-r--r-- | nova/compute/manager.py | 41 | ||||
| -rw-r--r-- | nova/db/sqlalchemy/api.py | 4 | ||||
| -rw-r--r-- | nova/exception.py | 3 | ||||
| -rw-r--r-- | nova/tests/api/openstack/test_servers.py | 37 | ||||
| -rw-r--r-- | nova/tests/compute_unittest.py | 8 | ||||
| -rw-r--r-- | nova/twistd.py | 2 | ||||
| -rw-r--r-- | nova/virt/fake.py | 13 | ||||
| -rw-r--r-- | nova/virt/libvirt_conn.py | 9 | ||||
| -rw-r--r-- | nova/virt/xenapi/network_utils.py | 1 | ||||
| -rw-r--r-- | nova/virt/xenapi/vm_utils.py | 1 | ||||
| -rw-r--r-- | nova/virt/xenapi/vmops.py | 27 | ||||
| -rw-r--r-- | nova/virt/xenapi/volumeops.py | 1 | ||||
| -rw-r--r-- | nova/virt/xenapi_conn.py | 10 |
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) |
