From: Thomas Herve Date: Tue, 16 Jul 2013 09:12:04 +0000 (+0200) Subject: Handle InstanceType change in Instance.handle_update X-Git-Tag: 2014.1~353 X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=60a1f0c47122cec0601885095ccffbdc13e77f5b;p=openstack-build%2Fheat-build.git 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 --- 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,