From 762605f572ff4dff00b3666f0648761182752c2f Mon Sep 17 00:00:00 2001 From: Steven Hardy Date: Fri, 28 Jun 2013 14:43:04 +0100 Subject: [PATCH] Add resume support to Instance Change-Id: Id02c74243ff5fcd67ec8f6200ee7969b34f2faa1 blueprint: stack-suspend-resume --- heat/engine/resources/instance.py | 33 ++++++++++-- heat/tests/test_instance.py | 86 +++++++++++++++++++++++++++++++ heat/tests/v1_1/fakes.py | 2 + 3 files changed, 118 insertions(+), 3 deletions(-) diff --git a/heat/engine/resources/instance.py b/heat/engine/resources/instance.py index ce4965ec9..6f90944f1 100644 --- a/heat/engine/resources/instance.py +++ b/heat/engine/resources/instance.py @@ -348,16 +348,20 @@ class Instance(resource.Resource): if server is not None: self.resource_id_set(server.id) + return server, scheduler.TaskRunner(self._attach_volumes_task()) + + def _attach_volumes_task(self): attach_tasks = (volume.VolumeAttachTask(self.stack, self.resource_id, volume_id, device) for volume_id, device in self.volumes()) - attach_volumes_task = scheduler.PollingTaskGroup(attach_tasks) - - return server, scheduler.TaskRunner(attach_volumes_task) + return scheduler.PollingTaskGroup(attach_tasks) def check_create_complete(self, cookie): + return self._check_active(cookie) + + def _check_active(self, cookie): server, volume_attach = cookie if not volume_attach.started(): @@ -563,6 +567,29 @@ class Instance(resource.Resource): else: return volumes_runner.step() + def handle_resume(self): + ''' + Resume an instance - note we do not wait for the ACTIVE state, + this is polled for by check_resume_complete in a similar way to the + create logic so we can take advantage of coroutines + ''' + if self.resource_id is None: + raise exception.Error(_('Cannot resume %s, resource_id not set') % + self.name) + + try: + server = self.nova().servers.get(self.resource_id) + except clients.novaclient.exceptions.NotFound: + raise exception.NotFound(_('Failed to find instance %s') % + self.resource_id) + else: + logger.debug("resuming instance %s" % self.resource_id) + server.resume() + return server, scheduler.TaskRunner(self._attach_volumes_task()) + + def check_resume_complete(self, cookie): + return self._check_active(cookie) + def resource_mapping(): return { diff --git a/heat/tests/test_instance.py b/heat/tests/test_instance.py index 8f090d802..617b7dc4e 100644 --- a/heat/tests/test_instance.py +++ b/heat/tests/test_instance.py @@ -308,6 +308,28 @@ class instancesTest(HeatTestCase): self.m.VerifyAll() + def test_instance_status_resume_immediate(self): + return_server = self.fc.servers.list()[1] + instance = self._create_test_instance(return_server, + 'test_instance_resume') + + instance.resource_id = 1234 + self.m.ReplayAll() + + # Override the get_servers_1234 handler status to SUSPENDED + d = {'server': self.fc.client.get_servers_detail()[1]['servers'][0]} + d['server']['status'] = 'ACTIVE' + self.m.StubOutWithMock(self.fc.client, 'get_servers_1234') + get = self.fc.client.get_servers_1234 + get().AndReturn((200, d)) + mox.Replay(get) + instance.state_set(instance.SUSPEND, instance.COMPLETE) + + scheduler.TaskRunner(instance.resume)() + self.assertEqual(instance.state, (instance.RESUME, instance.COMPLETE)) + + self.m.VerifyAll() + def test_instance_status_suspend_wait(self): return_server = self.fc.servers.list()[1] instance = self._create_test_instance(return_server, @@ -336,6 +358,36 @@ class instancesTest(HeatTestCase): self.m.VerifyAll() + def test_instance_status_resume_wait(self): + return_server = self.fc.servers.list()[1] + instance = self._create_test_instance(return_server, + 'test_instance_resume') + + instance.resource_id = 1234 + self.m.ReplayAll() + + # Override the get_servers_1234 handler status to ACTIVE, but + # return the SUSPENDED state first (twice, so we sleep) + d1 = {'server': self.fc.client.get_servers_detail()[1]['servers'][0]} + d2 = copy.deepcopy(d1) + d1['server']['status'] = 'SUSPENDED' + d2['server']['status'] = 'ACTIVE' + self.m.StubOutWithMock(self.fc.client, 'get_servers_1234') + get = self.fc.client.get_servers_1234 + get().AndReturn((200, d1)) + get().AndReturn((200, d1)) + self.m.StubOutWithMock(scheduler.TaskRunner, '_sleep') + scheduler.TaskRunner._sleep(mox.IsA(int)).AndReturn(None) + get().AndReturn((200, d2)) + self.m.ReplayAll() + + instance.state_set(instance.SUSPEND, instance.COMPLETE) + + scheduler.TaskRunner(instance.resume)() + self.assertEqual(instance.state, (instance.RESUME, instance.COMPLETE)) + + self.m.VerifyAll() + def test_instance_suspend_volumes_step(self): return_server = self.fc.servers.list()[1] instance = self._create_test_instance(return_server, @@ -365,6 +417,40 @@ class instancesTest(HeatTestCase): self.m.VerifyAll() + def test_instance_resume_volumes_step(self): + return_server = self.fc.servers.list()[1] + instance = self._create_test_instance(return_server, + 'test_instance_resume') + + instance.resource_id = 1234 + self.m.ReplayAll() + + # Override the get_servers_1234 handler status to ACTIVE + d = {'server': self.fc.client.get_servers_detail()[1]['servers'][0]} + d['server']['status'] = 'ACTIVE' + + # Return a dummy PollingTaskGroup to make check_resume_complete step + def dummy_attach(): + yield + dummy_tg = scheduler.PollingTaskGroup([dummy_attach, dummy_attach]) + self.m.StubOutWithMock(instance, '_attach_volumes_task') + instance._attach_volumes_task().AndReturn(dummy_tg) + + self.m.StubOutWithMock(self.fc.client, 'get_servers_1234') + get = self.fc.client.get_servers_1234 + get().AndReturn((200, d)) + + self.m.StubOutWithMock(scheduler.TaskRunner, '_sleep') + scheduler.TaskRunner._sleep(mox.IsA(int)).AndReturn(None) + self.m.ReplayAll() + + instance.state_set(instance.SUSPEND, instance.COMPLETE) + + scheduler.TaskRunner(instance.resume)() + self.assertEqual(instance.state, (instance.RESUME, instance.COMPLETE)) + + self.m.VerifyAll() + def test_instance_status_build_spawning(self): self._test_instance_status_not_build_active('BUILD(SPAWNING)') diff --git a/heat/tests/v1_1/fakes.py b/heat/tests/v1_1/fakes.py index f18c4b6ba..4aaef6db7 100644 --- a/heat/tests/v1_1/fakes.py +++ b/heat/tests/v1_1/fakes.py @@ -360,6 +360,8 @@ class FakeHTTPClient(base_client.HTTPClient): assert body[action] is None elif action == 'suspend': assert body[action] is None + elif action == 'resume': + assert body[action] is None elif action == 'addFixedIp': assert body[action].keys() == ['networkId'] elif action == 'removeFixedIp':