From 60a1f0c47122cec0601885095ccffbdc13e77f5b Mon Sep 17 00:00:00 2001
From: Thomas Herve
Date: Tue, 16 Jul 2013 11:12:04 +0200
Subject: [PATCH] Handle InstanceType change in Instance.handle_update
Make a resize API call against Nova if the InstanceType of an Instance
resource is change via a resource update.
Implements: blueprint instance-resize-update-stack
Change-Id: Ic4ee82edec842ee756b104a36dfef28bf3f89717
---
heat/engine/resources/instance.py | 51 ++++++++++++++++++------
heat/tests/test_instance.py | 64 +++++++++++++++++++++++++++++++
2 files changed, 103 insertions(+), 12 deletions(-)
diff --git a/heat/engine/resources/instance.py b/heat/engine/resources/instance.py
index ae4f9bfb..c792eed8 100644
--- a/heat/engine/resources/instance.py
+++ b/heat/engine/resources/instance.py
@@ -30,6 +30,7 @@ from heat.engine.resources import volume
from heat.common import exception
from heat.engine.resources.network_interface import NetworkInterface
+from heat.openstack.common.gettextutils import _
from heat.openstack.common import log as logging
from heat.openstack.common import uuidutils
@@ -121,9 +122,8 @@ class Instance(resource.Resource):
'PublicIp': ('Public IP address of the specified '
'instance.')}
- # template keys supported for handle_update, note trailing comma
- # is required for a single item to get a tuple not a string
- update_allowed_keys = ('Metadata',)
+ update_allowed_keys = ('Metadata', 'Properties')
+ update_allowed_properties = ('InstanceType',)
_deferred_server_statuses = ['BUILD',
'HARD_REBOOT',
@@ -286,6 +286,17 @@ class Instance(resource.Resource):
security_groups = None
return security_groups
+ def _get_flavor_id(self, flavor):
+ flavor_id = None
+ flavor_list = self.nova().flavors.list()
+ for o in flavor_list:
+ if o.name == flavor:
+ flavor_id = o.id
+ break
+ if flavor_id is None:
+ raise exception.FlavorMissing(flavor_id=flavor)
+ return flavor_id
+
def handle_create(self):
security_groups = self._get_security_groups()
@@ -302,14 +313,7 @@ class Instance(resource.Resource):
image_id = self._get_image_id(image_name)
- flavor_id = None
- flavor_list = self.nova().flavors.list()
- for o in flavor_list:
- if o.name == flavor:
- flavor_id = o.id
- break
- if flavor_id is None:
- raise exception.FlavorMissing(flavor_id=flavor)
+ flavor_id = self._get_flavor_id(flavor)
tags = {}
if self.properties['Tags']:
@@ -396,7 +400,30 @@ class Instance(resource.Resource):
def handle_update(self, json_snippet, tmpl_diff, prop_diff):
if 'Metadata' in tmpl_diff:
- self.metadata = tmpl_diff.get('Metadata', {})
+ self.metadata = tmpl_diff['Metadata']
+ if 'InstanceType' in prop_diff:
+ flavor = prop_diff['InstanceType']
+ flavor_id = self._get_flavor_id(flavor)
+ server = self.nova().servers.get(self.resource_id)
+ server.resize(flavor_id)
+ scheduler.TaskRunner(self._check_resize, server, flavor)()
+
+ def _check_resize(self, server, flavor):
+ """
+ Verify that the server is properly resized. If that's the case, confirm
+ the resize, if not raise an error.
+ """
+ yield
+ server.get()
+ while server.status == 'RESIZE':
+ yield
+ server.get()
+ if server.status == 'VERIFY_RESIZE':
+ server.confirm_resize()
+ else:
+ raise exception.Error(
+ "Resizing to '%s' failed, status '%s'" % (
+ flavor, server.status))
def metadata_update(self, new_metadata=None):
'''
diff --git a/heat/tests/test_instance.py b/heat/tests/test_instance.py
index 6541633d..d639eae3 100644
--- a/heat/tests/test_instance.py
+++ b/heat/tests/test_instance.py
@@ -252,6 +252,70 @@ class InstancesTest(HeatTestCase):
self.assertEqual(None, instance.update(update_template))
self.assertEqual(instance.metadata, {'test': 123})
+ def test_instance_update_instance_type(self):
+ """
+ Instance.handle_update supports changing the InstanceType, and makes
+ the change making a resize API call against Nova.
+ """
+ return_server = self.fc.servers.list()[1]
+ return_server.id = 1234
+ instance = self._create_test_instance(return_server,
+ 'test_instance_update')
+
+ update_template = copy.deepcopy(instance.t)
+ update_template['Properties']['InstanceType'] = 'm1.small'
+
+ self.m.StubOutWithMock(self.fc.servers, 'get')
+ self.fc.servers.get(1234).AndReturn(return_server)
+
+ def activate_status(server):
+ server.status = 'VERIFY_RESIZE'
+ return_server.get = activate_status.__get__(return_server)
+
+ self.m.StubOutWithMock(self.fc.client, 'post_servers_1234_action')
+ self.fc.client.post_servers_1234_action(
+ body={'resize': {'flavorRef': 2}}).AndReturn((202, None))
+ self.fc.client.post_servers_1234_action(
+ body={'confirmResize': None}).AndReturn((202, None))
+ self.m.ReplayAll()
+
+ self.assertEqual(None, instance.update(update_template))
+ self.assertEqual(instance.state, (instance.UPDATE, instance.COMPLETE))
+ self.m.VerifyAll()
+
+ def test_instance_update_instance_type_failed(self):
+ """
+ If the status after a resize is not VERIFY_RESIZE, it means the resize
+ call failed, so we raise an explicit error.
+ """
+ return_server = self.fc.servers.list()[1]
+ return_server.id = 1234
+ instance = self._create_test_instance(return_server,
+ 'test_instance_update')
+
+ update_template = copy.deepcopy(instance.t)
+ update_template['Properties']['InstanceType'] = 'm1.small'
+
+ self.m.StubOutWithMock(self.fc.servers, 'get')
+ self.fc.servers.get(1234).AndReturn(return_server)
+
+ def activate_status(server):
+ server.status = 'ACTIVE'
+ return_server.get = activate_status.__get__(return_server)
+
+ self.m.StubOutWithMock(self.fc.client, 'post_servers_1234_action')
+ self.fc.client.post_servers_1234_action(
+ body={'resize': {'flavorRef': 2}}).AndReturn((202, None))
+ self.m.ReplayAll()
+
+ error = self.assertRaises(exception.ResourceFailure,
+ instance.update, update_template)
+ self.assertEqual(
+ "Error: Resizing to 'm1.small' failed, status 'ACTIVE'",
+ str(error))
+ self.assertEqual(instance.state, (instance.UPDATE, instance.FAILED))
+ self.m.VerifyAll()
+
def test_instance_update_replace(self):
return_server = self.fc.servers.list()[1]
instance = self._create_test_instance(return_server,
--
2.45.2
|