From 7dc655b461987bde9f73efd9c592a656f5dc12af Mon Sep 17 00:00:00 2001 From: Ethan Lynn Date: Thu, 10 Dec 2015 23:46:06 +0800 Subject: [PATCH] Add handle_update for senlin cluster Add handle_update for senlin cluster resource. blueprint senlin-resources Change-Id: I829e1b5cd44ac7ff234659004e218f007c5af491 --- .../resources/openstack/senlin/cluster.py | 74 ++++++++++++++++++- heat/tests/openstack/senlin/test_cluster.py | 65 ++++++++++++++++ 2 files changed, 138 insertions(+), 1 deletion(-) diff --git a/heat/engine/resources/openstack/senlin/cluster.py b/heat/engine/resources/openstack/senlin/cluster.py index 30babe315..2c00aa58c 100644 --- a/heat/engine/resources/openstack/senlin/cluster.py +++ b/heat/engine/resources/openstack/senlin/cluster.py @@ -12,6 +12,8 @@ # License for the specific language governing permissions and limitations # under the License. +import six + from heat.common import exception from heat.common.i18n import _ from heat.engine import attributes @@ -57,11 +59,18 @@ class Cluster(resource.Resource): 'CREATING', 'DELETING', 'UPDATING' ) + _ACTION_STATUS = ( + ACTION_SUCCEEDED, ACTION_FAILED, + ) = ( + 'SUCCEEDED', 'FAILED', + ) + properties_schema = { PROFILE: properties.Schema( properties.Schema.STRING, _('The name or id of the Senlin profile.'), required=True, + update_allowed=True, constraints=[ constraints.CustomConstraint('senlin.profile') ] @@ -70,16 +79,19 @@ class Cluster(resource.Resource): properties.Schema.STRING, _('Name of the cluster. By default, physical resource name ' 'is used.'), + update_allowed=True, ), DESIRED_CAPACITY: properties.Schema( properties.Schema.INTEGER, _('Desired initial number of resources in cluster.'), - default=0 + default=0, + update_allowed=True, ), MIN_SIZE: properties.Schema( properties.Schema.INTEGER, _('Minimum number of resources in the cluster.'), default=0, + update_allowed=True, constraints=[ constraints.Range(min=0) ] @@ -89,6 +101,7 @@ class Cluster(resource.Resource): _('Maximum number of resources in the cluster. ' '-1 means unlimited.'), default=-1, + update_allowed=True, constraints=[ constraints.Range(min=-1) ] @@ -96,10 +109,12 @@ class Cluster(resource.Resource): METADATA: properties.Schema( properties.Schema.MAP, _('Metadata key-values defined for cluster.'), + update_allowed=True, ), TIMEOUT: properties.Schema( properties.Schema.INTEGER, _('The number of seconds to wait for the cluster actions.'), + update_allowed=True, constraints=[ constraints.Range(min=0) ] @@ -177,6 +192,63 @@ class Cluster(resource.Resource): return True return False + def handle_update(self, json_snippet, tmpl_diff, prop_diff): + UPDATE_PROPS = (self.NAME, self.METADATA, self.TIMEOUT, self.PROFILE) + RESIZE_PROPS = (self.MIN_SIZE, self.MAX_SIZE, self.DESIRED_CAPACITY) + updaters = {} + if prop_diff: + if any(p in prop_diff for p in UPDATE_PROPS): + params = dict((k, v) for k, v in six.iteritems(prop_diff) + if k in UPDATE_PROPS) + if self.PROFILE in prop_diff: + params.pop(self.PROFILE) + params['profile_id'] = prop_diff[self.PROFILE] + updaters['cluster_update'] = { + 'params': params, + 'start': False, + } + if any(p in prop_diff for p in RESIZE_PROPS): + params = dict((k, v) for k, v in six.iteritems(prop_diff) + if k in RESIZE_PROPS) + if self.DESIRED_CAPACITY in prop_diff: + params.pop(self.DESIRED_CAPACITY) + params['adjustment_type'] = 'EXACT_CAPACITY' + params['number'] = prop_diff.pop(self.DESIRED_CAPACITY) + updaters['cluster_resize'] = { + 'params': params, + 'start': False, + } + return updaters + + def check_update_complete(self, updaters): + def start_action(action, params): + if action == 'cluster_resize': + resp = self.client().cluster_resize(self.resource_id, + **params) + return resp['action'] + elif action == 'cluster_update': + resp = self.client().update_cluster(self.resource_id, + **params) + return resp.location.split('/')[-1] + + if not updaters: + return True + for k in six.iterkeys(updaters): + if not updaters[k]['start']: + action_id = start_action(k, updaters[k]['params']) + updaters[k]['action'] = action_id + updaters[k]['start'] = True + else: + action = self.client().get_action(updaters[k]['action']) + if action.status == self.ACTION_SUCCEEDED: + del updaters[k] + elif action.status == self.ACTION_FAILED: + raise exception.ResourceInError( + status_reason=action.status_reason, + resource_status=self.FAILED, + ) + return False + def validate(self): min_size = self.properties[self.MIN_SIZE] max_size = self.properties[self.MAX_SIZE] diff --git a/heat/tests/openstack/senlin/test_cluster.py b/heat/tests/openstack/senlin/test_cluster.py index 40ef3bfdc..8476bdbc8 100644 --- a/heat/tests/openstack/senlin/test_cluster.py +++ b/heat/tests/openstack/senlin/test_cluster.py @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import copy import mock from oslo_config import cfg import six @@ -22,6 +23,7 @@ from heat.common import template_format from heat.engine.clients.os import senlin from heat.engine.resources.openstack.senlin import cluster as sc from heat.engine import scheduler +from heat.engine import template from heat.tests import common from heat.tests import utils from senlinclient.common import exc @@ -149,6 +151,69 @@ class SenlinClusterTest(common.HeatTestCase): expected = 'Error: resources.senlin-cluster: oops' self.assertEqual(expected, six.text_type(ex)) + def test_cluster_update_profile(self): + cluster = self._create_cluster(self.t) + new_t = copy.deepcopy(self.t) + props = new_t['resources']['senlin-cluster']['properties'] + props['profile'] = 'new_profile' + props['name'] = 'new_name' + rsrc_defns = template.Template(new_t).resource_definitions(self.stack) + new_cluster = rsrc_defns['senlin-cluster'] + self.senlin_mock.update_cluster.return_value = mock.Mock( + location='/actions/fake-action') + self.senlin_mock.get_action.return_value = mock.Mock( + status='SUCCEEDED') + scheduler.TaskRunner(cluster.update, new_cluster)() + self.assertEqual((cluster.UPDATE, cluster.COMPLETE), cluster.state) + cluster_update_kwargs = { + 'profile_id': 'new_profile', + 'name': 'new_name' + } + self.senlin_mock.update_cluster.assert_called_once_with( + cluster.resource_id, **cluster_update_kwargs) + self.senlin_mock.get_action.assert_called_once_with( + 'fake-action') + + def test_cluster_update_desire_capacity(self): + cluster = self._create_cluster(self.t) + new_t = copy.deepcopy(self.t) + props = new_t['resources']['senlin-cluster']['properties'] + props['desired_capacity'] = 10 + rsrc_defns = template.Template(new_t).resource_definitions(self.stack) + new_cluster = rsrc_defns['senlin-cluster'] + self.senlin_mock.cluster_resize.return_value = { + 'action': 'fake-action'} + self.senlin_mock.get_action.return_value = mock.Mock( + status='SUCCEEDED') + scheduler.TaskRunner(cluster.update, new_cluster)() + self.assertEqual((cluster.UPDATE, cluster.COMPLETE), cluster.state) + cluster_resize_kwargs = { + 'adjustment_type': 'EXACT_CAPACITY', + 'number': 10 + } + self.senlin_mock.cluster_resize.assert_called_once_with( + cluster.resource_id, **cluster_resize_kwargs) + self.senlin_mock.get_action.assert_called_once_with( + 'fake-action') + + def test_cluster_update_failed(self): + cluster = self._create_cluster(self.t) + new_t = copy.deepcopy(self.t) + props = new_t['resources']['senlin-cluster']['properties'] + props['desired_capacity'] = 3 + rsrc_defns = template.Template(new_t).resource_definitions(self.stack) + update_snippet = rsrc_defns['senlin-cluster'] + self.senlin_mock.cluster_resize.return_value = { + 'action': 'fake-action'} + self.senlin_mock.get_action.return_value = mock.Mock( + status='FAILED', status_reason='Unknown') + exc = self.assertRaises( + exception.ResourceFailure, + scheduler.TaskRunner(cluster.update, update_snippet)) + self.assertEqual('ResourceInError: resources.senlin-cluster: ' + 'Went to status FAILED due to "Unknown"', + six.text_type(exc)) + def test_cluster_resolve_attribute(self): excepted_show = { 'id': 'some_id',