From 91300e40690daee260505de24497be82e7eaf70b Mon Sep 17 00:00:00 2001 From: tengqm Date: Wed, 27 Aug 2014 11:40:22 +0800 Subject: [PATCH] Split scaling policy into separate files A further step to split the AWS and the OS version of auto-scaling group related classes. This patch splits the scaling policy and launch configuration into separate files. This patch is not breaking any existing test cases. But more test cases will be needed for the OS version scaling policy. Implements: partial-blueprint reorg-asg-code Change-Id: I0f888496d92b0efb5d63b567864488ff8a45e4ac --- heat/engine/resources/autoscaling.py | 299 ------------------ heat/engine/resources/aws/__init__.py | 0 heat/engine/resources/aws/launch_config.py | 114 +++++++ heat/engine/resources/aws/scaling_policy.py | 165 ++++++++++ heat/engine/resources/openstack/__init__.py | 0 .../resources/openstack/scaling_policy.py | 167 ++++++++++ heat/tests/test_heat_autoscaling_group.py | 2 +- 7 files changed, 447 insertions(+), 300 deletions(-) create mode 100644 heat/engine/resources/aws/__init__.py create mode 100644 heat/engine/resources/aws/launch_config.py create mode 100644 heat/engine/resources/aws/scaling_policy.py create mode 100644 heat/engine/resources/openstack/__init__.py create mode 100644 heat/engine/resources/openstack/scaling_policy.py diff --git a/heat/engine/resources/autoscaling.py b/heat/engine/resources/autoscaling.py index b2f1b53c3c..d0f5a0d9e8 100644 --- a/heat/engine/resources/autoscaling.py +++ b/heat/engine/resources/autoscaling.py @@ -24,10 +24,8 @@ from heat.engine import environment from heat.engine import function from heat.engine.notification import autoscaling as notification from heat.engine import properties -from heat.engine import resource from heat.engine import rsrc_defn from heat.engine import scheduler -from heat.engine import signal_responder from heat.engine import stack_resource from heat.openstack.common import excutils from heat.openstack.common import log as logging @@ -717,93 +715,6 @@ class AutoScalingGroup(InstanceGroup, cooldown.CooldownMixin): return self._create_template(num_instances) -class LaunchConfiguration(resource.Resource): - - PROPERTIES = ( - IMAGE_ID, INSTANCE_TYPE, KEY_NAME, USER_DATA, SECURITY_GROUPS, - KERNEL_ID, RAM_DISK_ID, BLOCK_DEVICE_MAPPINGS, NOVA_SCHEDULER_HINTS, - ) = ( - 'ImageId', 'InstanceType', 'KeyName', 'UserData', 'SecurityGroups', - 'KernelId', 'RamDiskId', 'BlockDeviceMappings', 'NovaSchedulerHints', - ) - - _NOVA_SCHEDULER_HINT_KEYS = ( - NOVA_SCHEDULER_HINT_KEY, NOVA_SCHEDULER_HINT_VALUE, - ) = ( - 'Key', 'Value', - ) - - properties_schema = { - IMAGE_ID: properties.Schema( - properties.Schema.STRING, - _('Glance image ID or name.'), - required=True, - constraints=[ - constraints.CustomConstraint('glance.image') - ] - ), - INSTANCE_TYPE: properties.Schema( - properties.Schema.STRING, - _('Nova instance type (flavor).'), - required=True - ), - KEY_NAME: properties.Schema( - properties.Schema.STRING, - _('Optional Nova keypair name.'), - constraints=[ - constraints.CustomConstraint("nova.keypair") - ] - ), - USER_DATA: properties.Schema( - properties.Schema.STRING, - _('User data to pass to instance.') - ), - SECURITY_GROUPS: properties.Schema( - properties.Schema.LIST, - _('Security group names to assign.') - ), - KERNEL_ID: properties.Schema( - properties.Schema.STRING, - _('Not Implemented.'), - implemented=False - ), - RAM_DISK_ID: properties.Schema( - properties.Schema.STRING, - _('Not Implemented.'), - implemented=False - ), - BLOCK_DEVICE_MAPPINGS: properties.Schema( - properties.Schema.STRING, - _('Not Implemented.'), - implemented=False - ), - NOVA_SCHEDULER_HINTS: properties.Schema( - properties.Schema.LIST, - _('Scheduler hints to pass to Nova (Heat extension).'), - schema=properties.Schema( - properties.Schema.MAP, - schema={ - NOVA_SCHEDULER_HINT_KEY: properties.Schema( - properties.Schema.STRING, - required=True - ), - NOVA_SCHEDULER_HINT_VALUE: properties.Schema( - properties.Schema.STRING, - required=True - ), - }, - ) - ), - } - - def handle_update(self, json_snippet, tmpl_diff, prop_diff): - if 'Metadata' in tmpl_diff: - raise resource.UpdateReplace(self.name) - - def FnGetRefId(self): - return self.physical_resource_name_or_FnGetRefId() - - class AutoScalingResourceGroup(AutoScalingGroup): """An autoscaling group that can scale arbitrary resources.""" @@ -940,219 +851,9 @@ class AutoScalingResourceGroup(AutoScalingGroup): key=key) -class ScalingPolicy(signal_responder.SignalResponder, cooldown.CooldownMixin): - PROPERTIES = ( - AUTO_SCALING_GROUP_NAME, SCALING_ADJUSTMENT, ADJUSTMENT_TYPE, - COOLDOWN, - ) = ( - 'AutoScalingGroupName', 'ScalingAdjustment', 'AdjustmentType', - 'Cooldown', - ) - - EXACT_CAPACITY, CHANGE_IN_CAPACITY, PERCENT_CHANGE_IN_CAPACITY = ( - 'ExactCapacity', 'ChangeInCapacity', 'PercentChangeInCapacity') - - ATTRIBUTES = ( - ALARM_URL, - ) = ( - 'AlarmUrl', - ) - - properties_schema = { - AUTO_SCALING_GROUP_NAME: properties.Schema( - properties.Schema.STRING, - _('AutoScaling group name to apply policy to.'), - required=True - ), - SCALING_ADJUSTMENT: properties.Schema( - properties.Schema.NUMBER, - _('Size of adjustment.'), - required=True, - update_allowed=True - ), - ADJUSTMENT_TYPE: properties.Schema( - properties.Schema.STRING, - _('Type of adjustment (absolute or percentage).'), - required=True, - constraints=[ - constraints.AllowedValues([CHANGE_IN_CAPACITY, - EXACT_CAPACITY, - PERCENT_CHANGE_IN_CAPACITY]), - ], - update_allowed=True - ), - COOLDOWN: properties.Schema( - properties.Schema.NUMBER, - _('Cooldown period, in seconds.'), - update_allowed=True - ), - } - - attributes_schema = { - ALARM_URL: attributes.Schema( - _("A signed url to handle the alarm. (Heat extension).") - ), - } - - def handle_create(self): - super(ScalingPolicy, self).handle_create() - self.resource_id_set(self._get_user_id()) - - def handle_update(self, json_snippet, tmpl_diff, prop_diff): - """ - If Properties has changed, update self.properties, so we get the new - values during any subsequent adjustment. - """ - if prop_diff: - self.properties = json_snippet.properties(self.properties_schema, - self.context) - - def _get_adjustement_type(self): - return self.properties[self.ADJUSTMENT_TYPE] - - def handle_signal(self, details=None): - if self.action in (self.SUSPEND, self.DELETE): - msg = _('Cannot signal resource during %s') % self.action - raise Exception(msg) - - # ceilometer sends details like this: - # {u'alarm_id': ID, u'previous': u'ok', u'current': u'alarm', - # u'reason': u'...'}) - # in this policy we currently assume that this gets called - # only when there is an alarm. But the template writer can - # put the policy in all the alarm notifiers (nodata, and ok). - # - # our watchrule has upper case states so lower() them all. - if details is None: - alarm_state = 'alarm' - else: - alarm_state = details.get('current', - details.get('state', 'alarm')).lower() - - LOG.info(_('%(name)s Alarm, new state %(state)s') - % {'name': self.name, 'state': alarm_state}) - - if alarm_state != 'alarm': - return - if self._cooldown_inprogress(): - LOG.info(_("%(name)s NOT performing scaling action, " - "cooldown %(cooldown)s") - % {'name': self.name, - 'cooldown': self.properties[self.COOLDOWN]}) - return - - asgn_id = self.properties[self.AUTO_SCALING_GROUP_NAME] - group = self.stack.resource_by_refid(asgn_id) - if group is None: - raise exception.NotFound(_('Alarm %(alarm)s could not find ' - 'scaling group named "%(group)s"') % { - 'alarm': self.name, - 'group': asgn_id}) - - LOG.info(_('%(name)s Alarm, adjusting Group %(group)s with id ' - '%(asgn_id)s by %(filter)s') - % {'name': self.name, 'group': group.name, 'asgn_id': asgn_id, - 'filter': self.properties[self.SCALING_ADJUSTMENT]}) - adjustment_type = self._get_adjustement_type() - group.adjust(self.properties[self.SCALING_ADJUSTMENT], adjustment_type) - - self._cooldown_timestamp("%s : %s" % - (self.properties[self.ADJUSTMENT_TYPE], - self.properties[self.SCALING_ADJUSTMENT])) - - def _resolve_attribute(self, name): - ''' - heat extension: "AlarmUrl" returns the url to post to the policy - when there is an alarm. - ''' - if name == self.ALARM_URL and self.resource_id is not None: - return unicode(self._get_signed_url()) - - def FnGetRefId(self): - if self.resource_id is not None: - return unicode(self._get_signed_url()) - else: - return unicode(self.name) - - -class AutoScalingPolicy(ScalingPolicy): - """A resource to manage scaling of `OS::Heat::AutoScalingGroup`. - - **Note** while it may incidentally support - `AWS::AutoScaling::AutoScalingGroup` for now, please don't use it for that - purpose and use `AWS::AutoScaling::ScalingPolicy` instead. - """ - PROPERTIES = ( - AUTO_SCALING_GROUP_NAME, SCALING_ADJUSTMENT, ADJUSTMENT_TYPE, - COOLDOWN, - ) = ( - 'auto_scaling_group_id', 'scaling_adjustment', 'adjustment_type', - 'cooldown', - ) - - EXACT_CAPACITY, CHANGE_IN_CAPACITY, PERCENT_CHANGE_IN_CAPACITY = ( - 'exact_capacity', 'change_in_capacity', 'percent_change_in_capacity') - - ATTRIBUTES = ( - ALARM_URL, - ) = ( - 'alarm_url', - ) - - properties_schema = { - AUTO_SCALING_GROUP_NAME: properties.Schema( - properties.Schema.STRING, - _('AutoScaling group ID to apply policy to.'), - required=True - ), - SCALING_ADJUSTMENT: properties.Schema( - properties.Schema.NUMBER, - _('Size of adjustment.'), - required=True, - update_allowed=True - ), - ADJUSTMENT_TYPE: properties.Schema( - properties.Schema.STRING, - _('Type of adjustment (absolute or percentage).'), - required=True, - constraints=[ - constraints.AllowedValues([CHANGE_IN_CAPACITY, - EXACT_CAPACITY, - PERCENT_CHANGE_IN_CAPACITY]), - ], - update_allowed=True - ), - COOLDOWN: properties.Schema( - properties.Schema.NUMBER, - _('Cooldown period, in seconds.'), - update_allowed=True - ), - } - - attributes_schema = { - ALARM_URL: attributes.Schema( - _("A signed url to handle the alarm.") - ), - } - - def _get_adjustement_type(self): - adjustment_type = self.properties[self.ADJUSTMENT_TYPE] - return ''.join([t.capitalize() for t in adjustment_type.split('_')]) - - def _resolve_attribute(self, name): - if name == self.ALARM_URL and self.resource_id is not None: - return unicode(self._get_signed_url()) - - def FnGetRefId(self): - return resource.Resource.FnGetRefId(self) - - def resource_mapping(): return { - 'AWS::AutoScaling::LaunchConfiguration': LaunchConfiguration, 'AWS::AutoScaling::AutoScalingGroup': AutoScalingGroup, - 'AWS::AutoScaling::ScalingPolicy': ScalingPolicy, 'OS::Heat::InstanceGroup': InstanceGroup, 'OS::Heat::AutoScalingGroup': AutoScalingResourceGroup, - 'OS::Heat::ScalingPolicy': AutoScalingPolicy, } diff --git a/heat/engine/resources/aws/__init__.py b/heat/engine/resources/aws/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/heat/engine/resources/aws/launch_config.py b/heat/engine/resources/aws/launch_config.py new file mode 100644 index 0000000000..3a1bf0f140 --- /dev/null +++ b/heat/engine/resources/aws/launch_config.py @@ -0,0 +1,114 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from heat.engine import constraints +from heat.engine import properties +from heat.engine import resource + +from heat.openstack.common import log as logging + +LOG = logging.getLogger(__name__) + + +class LaunchConfiguration(resource.Resource): + + PROPERTIES = ( + IMAGE_ID, INSTANCE_TYPE, KEY_NAME, USER_DATA, SECURITY_GROUPS, + KERNEL_ID, RAM_DISK_ID, BLOCK_DEVICE_MAPPINGS, NOVA_SCHEDULER_HINTS, + ) = ( + 'ImageId', 'InstanceType', 'KeyName', 'UserData', 'SecurityGroups', + 'KernelId', 'RamDiskId', 'BlockDeviceMappings', 'NovaSchedulerHints', + ) + + _NOVA_SCHEDULER_HINT_KEYS = ( + NOVA_SCHEDULER_HINT_KEY, NOVA_SCHEDULER_HINT_VALUE, + ) = ( + 'Key', 'Value', + ) + + properties_schema = { + IMAGE_ID: properties.Schema( + properties.Schema.STRING, + _('Glance image ID or name.'), + required=True, + constraints=[ + constraints.CustomConstraint('glance.image') + ] + ), + INSTANCE_TYPE: properties.Schema( + properties.Schema.STRING, + _('Nova instance type (flavor).'), + required=True + ), + KEY_NAME: properties.Schema( + properties.Schema.STRING, + _('Optional Nova keypair name.'), + constraints=[ + constraints.CustomConstraint("nova.keypair") + ] + ), + USER_DATA: properties.Schema( + properties.Schema.STRING, + _('User data to pass to instance.') + ), + SECURITY_GROUPS: properties.Schema( + properties.Schema.LIST, + _('Security group names to assign.') + ), + KERNEL_ID: properties.Schema( + properties.Schema.STRING, + _('Not Implemented.'), + implemented=False + ), + RAM_DISK_ID: properties.Schema( + properties.Schema.STRING, + _('Not Implemented.'), + implemented=False + ), + BLOCK_DEVICE_MAPPINGS: properties.Schema( + properties.Schema.STRING, + _('Not Implemented.'), + implemented=False + ), + NOVA_SCHEDULER_HINTS: properties.Schema( + properties.Schema.LIST, + _('Scheduler hints to pass to Nova (Heat extension).'), + schema=properties.Schema( + properties.Schema.MAP, + schema={ + NOVA_SCHEDULER_HINT_KEY: properties.Schema( + properties.Schema.STRING, + required=True + ), + NOVA_SCHEDULER_HINT_VALUE: properties.Schema( + properties.Schema.STRING, + required=True + ), + }, + ) + ), + } + + def handle_update(self, json_snippet, tmpl_diff, prop_diff): + if 'Metadata' in tmpl_diff: + raise resource.UpdateReplace(self.name) + + def FnGetRefId(self): + return self.physical_resource_name_or_FnGetRefId() + + +def resource_mapping(): + return { + 'AWS::AutoScaling::LaunchConfiguration': LaunchConfiguration, + } diff --git a/heat/engine/resources/aws/scaling_policy.py b/heat/engine/resources/aws/scaling_policy.py new file mode 100644 index 0000000000..40986bb899 --- /dev/null +++ b/heat/engine/resources/aws/scaling_policy.py @@ -0,0 +1,165 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from heat.common import exception + +from heat.engine import attributes +from heat.engine import constraints +from heat.engine import properties +from heat.engine import signal_responder +from heat.openstack.common import log as logging +from heat.scaling import cooldown + +LOG = logging.getLogger(__name__) + + +class AWSScalingPolicy(signal_responder.SignalResponder, + cooldown.CooldownMixin): + PROPERTIES = ( + AUTO_SCALING_GROUP_NAME, SCALING_ADJUSTMENT, ADJUSTMENT_TYPE, + COOLDOWN, + ) = ( + 'AutoScalingGroupName', 'ScalingAdjustment', 'AdjustmentType', + 'Cooldown', + ) + + EXACT_CAPACITY, CHANGE_IN_CAPACITY, PERCENT_CHANGE_IN_CAPACITY = ( + 'ExactCapacity', 'ChangeInCapacity', 'PercentChangeInCapacity') + + ATTRIBUTES = ( + ALARM_URL, + ) = ( + 'AlarmUrl', + ) + + properties_schema = { + AUTO_SCALING_GROUP_NAME: properties.Schema( + properties.Schema.STRING, + _('AutoScaling group name to apply policy to.'), + required=True + ), + SCALING_ADJUSTMENT: properties.Schema( + properties.Schema.NUMBER, + _('Size of adjustment.'), + required=True, + update_allowed=True + ), + ADJUSTMENT_TYPE: properties.Schema( + properties.Schema.STRING, + _('Type of adjustment (absolute or percentage).'), + required=True, + constraints=[ + constraints.AllowedValues([CHANGE_IN_CAPACITY, + EXACT_CAPACITY, + PERCENT_CHANGE_IN_CAPACITY]), + ], + update_allowed=True + ), + COOLDOWN: properties.Schema( + properties.Schema.NUMBER, + _('Cooldown period, in seconds.'), + update_allowed=True + ), + } + + attributes_schema = { + ALARM_URL: attributes.Schema( + _("A signed url to handle the alarm. (Heat extension).") + ), + } + + def handle_create(self): + super(AWSScalingPolicy, self).handle_create() + self.resource_id_set(self._get_user_id()) + + def handle_update(self, json_snippet, tmpl_diff, prop_diff): + """ + If Properties has changed, update self.properties, so we get the new + values during any subsequent adjustment. + """ + if prop_diff: + self.properties = json_snippet.properties(self.properties_schema, + self.context) + + def _get_adjustement_type(self): + return self.properties[self.ADJUSTMENT_TYPE] + + def handle_signal(self, details=None): + if self.action in (self.SUSPEND, self.DELETE): + msg = _('Cannot signal resource during %s') % self.action + raise Exception(msg) + + # ceilometer sends details like this: + # {u'alarm_id': ID, u'previous': u'ok', u'current': u'alarm', + # u'reason': u'...'}) + # in this policy we currently assume that this gets called + # only when there is an alarm. But the template writer can + # put the policy in all the alarm notifiers (nodata, and ok). + # + # our watchrule has upper case states so lower() them all. + if details is None: + alarm_state = 'alarm' + else: + alarm_state = details.get('current', + details.get('state', 'alarm')).lower() + + LOG.info(_('%(name)s Alarm, new state %(state)s') + % {'name': self.name, 'state': alarm_state}) + + if alarm_state != 'alarm': + return + if self._cooldown_inprogress(): + LOG.info(_("%(name)s NOT performing scaling action, " + "cooldown %(cooldown)s") + % {'name': self.name, + 'cooldown': self.properties[self.COOLDOWN]}) + return + + asgn_id = self.properties[self.AUTO_SCALING_GROUP_NAME] + group = self.stack.resource_by_refid(asgn_id) + if group is None: + raise exception.NotFound(_('Alarm %(alarm)s could not find ' + 'scaling group named "%(group)s"') % { + 'alarm': self.name, + 'group': asgn_id}) + + LOG.info(_('%(name)s Alarm, adjusting Group %(group)s with id ' + '%(asgn_id)s by %(filter)s') + % {'name': self.name, 'group': group.name, 'asgn_id': asgn_id, + 'filter': self.properties[self.SCALING_ADJUSTMENT]}) + adjustment_type = self._get_adjustement_type() + group.adjust(self.properties[self.SCALING_ADJUSTMENT], adjustment_type) + + self._cooldown_timestamp("%s : %s" % + (self.properties[self.ADJUSTMENT_TYPE], + self.properties[self.SCALING_ADJUSTMENT])) + + def _resolve_attribute(self, name): + ''' + heat extension: "AlarmUrl" returns the url to post to the policy + when there is an alarm. + ''' + if name == self.ALARM_URL and self.resource_id is not None: + return unicode(self._get_signed_url()) + + def FnGetRefId(self): + if self.resource_id is not None: + return unicode(self._get_signed_url()) + else: + return unicode(self.name) + + +def resource_mapping(): + return { + 'AWS::AutoScaling::ScalingPolicy': AWSScalingPolicy, + } diff --git a/heat/engine/resources/openstack/__init__.py b/heat/engine/resources/openstack/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/heat/engine/resources/openstack/scaling_policy.py b/heat/engine/resources/openstack/scaling_policy.py new file mode 100644 index 0000000000..0d1fed3251 --- /dev/null +++ b/heat/engine/resources/openstack/scaling_policy.py @@ -0,0 +1,167 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from heat.common import exception +from heat.engine import attributes +from heat.engine import constraints +from heat.engine import properties +from heat.engine import resource +from heat.engine import signal_responder +from heat.scaling import cooldown + +from heat.openstack.common import log as logging + +LOG = logging.getLogger(__name__) + + +class AutoScalingPolicy(signal_responder.SignalResponder, + cooldown.CooldownMixin): + """A resource to manage scaling of `OS::Heat::AutoScalingGroup`. + + **Note** while it may incidentally support + `AWS::AutoScaling::AutoScalingGroup` for now, please don't use it for that + purpose and use `AWS::AutoScaling::ScalingPolicy` instead. + """ + PROPERTIES = ( + AUTO_SCALING_GROUP_NAME, SCALING_ADJUSTMENT, ADJUSTMENT_TYPE, + COOLDOWN, + ) = ( + 'auto_scaling_group_id', 'scaling_adjustment', 'adjustment_type', + 'cooldown', + ) + + EXACT_CAPACITY, CHANGE_IN_CAPACITY, PERCENT_CHANGE_IN_CAPACITY = ( + 'exact_capacity', 'change_in_capacity', 'percent_change_in_capacity') + + ATTRIBUTES = ( + ALARM_URL, + ) = ( + 'alarm_url', + ) + + properties_schema = { + # TODO(Qiming): property name should be AUTO_SCALING_GROUP_ID + AUTO_SCALING_GROUP_NAME: properties.Schema( + properties.Schema.STRING, + _('AutoScaling group ID to apply policy to.'), + required=True + ), + SCALING_ADJUSTMENT: properties.Schema( + properties.Schema.NUMBER, + _('Size of adjustment.'), + required=True, + update_allowed=True + ), + ADJUSTMENT_TYPE: properties.Schema( + properties.Schema.STRING, + _('Type of adjustment (absolute or percentage).'), + required=True, + constraints=[ + constraints.AllowedValues([CHANGE_IN_CAPACITY, + EXACT_CAPACITY, + PERCENT_CHANGE_IN_CAPACITY]), + ], + update_allowed=True + ), + COOLDOWN: properties.Schema( + properties.Schema.NUMBER, + _('Cooldown period, in seconds.'), + update_allowed=True + ), + } + + attributes_schema = { + ALARM_URL: attributes.Schema( + _("A signed url to handle the alarm.") + ), + } + + def handle_create(self): + super(AutoScalingPolicy, self).handle_create() + self.resource_id_set(self._get_user_id()) + + def handle_update(self, json_snippet, tmpl_diff, prop_diff): + """ + If Properties has changed, update self.properties, so we get the new + values during any subsequent adjustment. + """ + if prop_diff: + self.properties = json_snippet.properties(self.properties_schema, + self.context) + + def _get_adjustement_type(self): + adjustment_type = self.properties[self.ADJUSTMENT_TYPE] + return ''.join([t.capitalize() for t in adjustment_type.split('_')]) + + def handle_signal(self, details=None): + if self.action in (self.SUSPEND, self.DELETE): + msg = _('Cannot signal resource during %s') % self.action + raise Exception(msg) + + # ceilometer sends details like this: + # {u'alarm_id': ID, u'previous': u'ok', u'current': u'alarm', + # u'reason': u'...'}) + # in this policy we currently assume that this gets called + # only when there is an alarm. But the template writer can + # put the policy in all the alarm notifiers (nodata, and ok). + # + # our watchrule has upper case states so lower() them all. + if details is None: + alarm_state = 'alarm' + else: + alarm_state = details.get('current', + details.get('state', 'alarm')).lower() + + LOG.info(_('Alarm %(name)s, new state %(state)s') + % {'name': self.name, 'state': alarm_state}) + + if alarm_state != 'alarm': + return + if self._cooldown_inprogress(): + LOG.info(_("%(name)s NOT performing scaling action, " + "cooldown %(cooldown)s") + % {'name': self.name, + 'cooldown': self.properties[self.COOLDOWN]}) + return + + asgn_id = self.properties[self.AUTO_SCALING_GROUP_NAME] + group = self.stack.resource_by_refid(asgn_id) + if group is None: + raise exception.NotFound(_('Alarm %(alarm)s could not find ' + 'scaling group named "%(group)s"') % { + 'alarm': self.name, + 'group': asgn_id}) + + LOG.info(_('%(name)s Alarm, adjusting Group %(group)s with id ' + '%(asgn_id)s by %(filter)s') + % {'name': self.name, 'group': group.name, 'asgn_id': asgn_id, + 'filter': self.properties[self.SCALING_ADJUSTMENT]}) + adjustment_type = self._get_adjustement_type() + group.adjust(self.properties[self.SCALING_ADJUSTMENT], adjustment_type) + + self._cooldown_timestamp("%s : %s" % + (self.properties[self.ADJUSTMENT_TYPE], + self.properties[self.SCALING_ADJUSTMENT])) + + def _resolve_attribute(self, name): + if name == self.ALARM_URL and self.resource_id is not None: + return unicode(self._get_signed_url()) + + def FnGetRefId(self): + return resource.Resource.FnGetRefId(self) + + +def resource_mapping(): + return { + 'OS::Heat::ScalingPolicy': AutoScalingPolicy, + } diff --git a/heat/tests/test_heat_autoscaling_group.py b/heat/tests/test_heat_autoscaling_group.py index 0799c92051..eb3c12f9df 100644 --- a/heat/tests/test_heat_autoscaling_group.py +++ b/heat/tests/test_heat_autoscaling_group.py @@ -323,7 +323,7 @@ class HeatScalingGroupWithCFNScalingPolicyTest(HeatTestCase): class ScalingPolicyTest(HeatTestCase): - + # TODO(Qiming): Add more tests to the scaling policy as_template = ''' heat_template_version: 2013-05-23 resources: