]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Handle InstanceType change in Instance.handle_update
authorThomas Herve <th@rackspace.com>
Tue, 16 Jul 2013 09:12:04 +0000 (11:12 +0200)
committerThomas Herve <th@rackspace.com>
Thu, 18 Jul 2013 16:29:28 +0000 (18:29 +0200)
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
heat/tests/test_instance.py

index ae4f9bfb93ef6fcca2a685b7c09b00d78f26c9e8..c792eed8a487bbe5db70b8e379807d312b24239d 100644 (file)
@@ -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):
         '''
index 6541633d0dd8f0da268f674d588060da429d44bf..d639eae3ff24ac9e5c906cba0ab64da6e9b02c91 100644 (file)
@@ -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,