diff --git a/heat/api/cloudwatch/watch.py b/heat/api/cloudwatch/watch.py index ca8dc13fd..8082b70a1 100644 --- a/heat/api/cloudwatch/watch.py +++ b/heat/api/cloudwatch/watch.py @@ -283,15 +283,6 @@ class WatchController(object): else: dimensions.append(dimension) - # We expect an AlarmName dimension as currently the engine - # implementation requires metric data to be associated - # with an alarm. When this is fixed, we can simply - # parse the user-defined dimensions and add the list to - # the metric data - if not watch_name: - logger.error("Request does not contain AlarmName dimension!") - return exception.HeatMissingParameterError("AlarmName dimension") - # Extract the required data from the metric_data # and format dict to pass to engine data = {'Namespace': namespace, diff --git a/heat/engine/resources/autoscaling.py b/heat/engine/resources/autoscaling.py index f1ae64ca9..5a8c48f07 100644 --- a/heat/engine/resources/autoscaling.py +++ b/heat/engine/resources/autoscaling.py @@ -135,11 +135,22 @@ class InstanceGroup(resource.Resource): instance_definition = self.stack.t['Resources'][conf] # honour the Tags property in the InstanceGroup and AutoScalingGroup - tags = self.properties.data.get('Tags', []) - instance_definition['Properties']['Tags'] = tags - + instance_definition['Properties']['Tags'] = self._tags() return GroupedInstance(name, instance_definition, self.stack) + def _tags(self): + """ + Make sure that we add a tag that Ceilometer can pick up. + These need to be prepended with 'metering.'. + """ + tags = self.properties.get('Tags') or [] + for t in tags: + if t['Key'].startswith('metering.'): + # the user has added one, don't add another. + return tags + return tags + [{'Key': 'metering.groupname', + 'Value': self.FnGetRefId()}] + def _instances(self): ''' Convert the stored instance list into a list of GroupedInstance objects @@ -323,7 +334,6 @@ class AutoScalingGroup(InstanceGroup, CooldownMixin): 'Cooldown', 'DesiredCapacity',) def handle_create(self): - if self.properties['DesiredCapacity']: num_to_create = int(self.properties['DesiredCapacity']) else: @@ -406,6 +416,17 @@ class AutoScalingGroup(InstanceGroup, CooldownMixin): return result + def _tags(self): + """Add Identifing Tags to all servers in the group. + + This is so the Dimensions received from cfn-push-stats all include + the groupname and stack id. + Note: the group name must match what is returned from FnGetRefId + """ + autoscaling_tag = [{'Key': 'AutoScalingGroupName', + 'Value': self.FnGetRefId()}] + return super(AutoScalingGroup, self)._tags() + autoscaling_tag + def FnGetRefId(self): return unicode(self.name) diff --git a/heat/engine/service.py b/heat/engine/service.py index 0c438eef9..cf36e6aa7 100644 --- a/heat/engine/service.py +++ b/heat/engine/service.py @@ -662,9 +662,24 @@ class EngineService(service.Service): This could be used by CloudWatch and WaitConditions and treat HA service events like any other CloudWatch. ''' - rule = watchrule.WatchRule.load(cnxt, watch_name) - rule.create_watch_data(stats_data) - logger.debug('new watch:%s data:%s' % (watch_name, str(stats_data))) + def get_matching_watches(): + if watch_name: + yield watchrule.WatchRule.load(cnxt, watch_name) + else: + for wr in db_api.watch_rule_get_all(cnxt): + if watchrule.rule_can_use_sample(wr, stats_data): + yield watchrule.WatchRule.load(cnxt, watch=wr) + + rule_run = False + for rule in get_matching_watches(): + rule.create_watch_data(stats_data) + rule_run = True + + if not rule_run: + if watch_name is None: + watch_name = 'Unknown' + raise exception.WatchRuleNotFound(watch_name=watch_name) + return stats_data @request_context diff --git a/heat/engine/watchrule.py b/heat/engine/watchrule.py index 7f6d30fd8..3f2f82de6 100644 --- a/heat/engine/watchrule.py +++ b/heat/engine/watchrule.py @@ -309,3 +309,32 @@ class WatchRule(object): logger.warning("Unable to override state %s for watch %s" % (self.state, self.name)) return actions + + +def rule_can_use_sample(wr, stats_data): + def match_dimesions(rule, data): + for k, v in iter(rule.items()): + if k not in data: + return False + elif v != data[k]: + return False + return True + + if wr.state == WatchRule.SUSPENDED: + return False + if wr.rule['MetricName'] not in stats_data: + return False + + rule_dims = dict((d['Name'], d['Value']) + for d in wr.rule.get('Dimensions', [])) + + for k, v in iter(stats_data.items()): + if k == 'Namespace': + continue + if k == wr.rule['MetricName']: + data_dims = v.get('Dimensions', {}) + if isinstance(data_dims, list): + data_dims = data_dims[0] + if match_dimesions(rule_dims, data_dims): + return True + return False diff --git a/heat/tests/test_server_tags.py b/heat/tests/test_server_tags.py index ef59889a3..091d53f98 100644 --- a/heat/tests/test_server_tags.py +++ b/heat/tests/test_server_tags.py @@ -168,6 +168,7 @@ class ServerTagsTest(HeatTestCase): def test_group_tags(self): tags = [{'Key': 'Food', 'Value': 'yum'}] metadata = dict((tm['Key'], tm['Value']) for tm in tags) + metadata['metering.groupname'] = 'WebServer' group = self._setup_test_group(intags=tags, nova_tags=metadata) self.m.ReplayAll() scheduler.TaskRunner(group.create)() diff --git a/heat/tests/test_watch.py b/heat/tests/test_watch.py index b3238f077..fe1acad64 100644 --- a/heat/tests/test_watch.py +++ b/heat/tests/test_watch.py @@ -644,6 +644,137 @@ class WatchRuleTest(HeatTestCase): dbwr = db_api.watch_rule_get_by_name(self.ctx, 'create_data_test') self.assertEqual(dbwr.watch_data, []) + @utils.wr_delete_after + def test_create_watch_data_match(self): + rule = {u'EvaluationPeriods': u'1', + u'AlarmDescription': u'test alarm', + u'Period': u'300', + u'ComparisonOperator': u'GreaterThanThreshold', + u'Statistic': u'SampleCount', + u'Threshold': u'2', + u'Dimensions': [{u'Name': 'AutoScalingGroupName', + u'Value': 'group_x'}], + u'MetricName': u'CreateDataMetric'} + self.wr = watchrule.WatchRule(context=self.ctx, + watch_name='create_data_test', + stack_id=self.stack_id, rule=rule) + self.wr.store() + + data = {u'CreateDataMetric': {"Unit": "Counter", + "Value": "1", + "Dimensions": [{u'AutoScalingGroupName': + u'group_x'}]}} + self.assertTrue(watchrule.rule_can_use_sample(self.wr, data)) + + @utils.wr_delete_after + def test_create_watch_data_match_2(self): + rule = {u'EvaluationPeriods': u'1', + u'AlarmDescription': u'test alarm', + u'Period': u'300', + u'ComparisonOperator': u'GreaterThanThreshold', + u'Statistic': u'SampleCount', + u'Threshold': u'2', + u'Dimensions': [{u'Name': 'AutoScalingGroupName', + u'Value': 'group_x'}], + u'MetricName': u'CreateDataMetric'} + self.wr = watchrule.WatchRule(context=self.ctx, + watch_name='create_data_test', + stack_id=self.stack_id, rule=rule) + self.wr.store() + + data = {u'not_interesting': {"Unit": "Counter", + "Value": "1", + "Dimensions": [ + {u'AutoScalingGroupName': + u'group_x'}]}, + u'CreateDataMetric': {"Unit": "Counter", + "Value": "1", + "Dimensions": [ + {u'AutoScalingGroupName': + u'group_x'}]}} + self.assertTrue(watchrule.rule_can_use_sample(self.wr, data)) + + def test_create_watch_data_match_3(self): + rule = {u'EvaluationPeriods': u'1', + u'AlarmDescription': u'test alarm', + u'Period': u'300', + u'ComparisonOperator': u'GreaterThanThreshold', + u'Statistic': u'SampleCount', + u'Threshold': u'2', + u'Dimensions': [{u'Name': 'AutoScalingGroupName', + u'Value': 'group_x'}], + u'MetricName': u'CreateDataMetric'} + self.wr = watchrule.WatchRule(context=self.ctx, + watch_name='create_data_test', + stack_id=self.stack_id, rule=rule) + self.wr.store() + + data = {u'CreateDataMetric': {"Unit": "Counter", + "Value": "1", + "Dimensions": [ + {u'AutoScalingGroupName': + u'not_this'}]}, + u'CreateDataMetric': {"Unit": "Counter", + "Value": "1", + "Dimensions": [ + {u'AutoScalingGroupName': + u'group_x'}]}} + self.assertTrue(watchrule.rule_can_use_sample(self.wr, data)) + + def test_create_watch_data_not_match_metric(self): + rule = {u'EvaluationPeriods': u'1', + u'AlarmDescription': u'test alarm', + u'Period': u'300', + u'ComparisonOperator': u'GreaterThanThreshold', + u'Statistic': u'SampleCount', + u'Threshold': u'2', + u'Dimensions': [{u'Name': 'AutoScalingGroupName', + u'Value': 'group_x'}], + u'MetricName': u'CreateDataMetric'} + self.wr = watchrule.WatchRule(context=self.ctx, + watch_name='create_data_test', + stack_id=self.stack_id, rule=rule) + self.wr.store() + + data = {u'not_this': {"Unit": "Counter", + "Value": "1", + "Dimensions": [ + {u'AutoScalingGroupName': + u'group_x'}]}, + u'nor_this': {"Unit": "Counter", + "Value": "1", + "Dimensions": [ + {u'AutoScalingGroupName': + u'group_x'}]}} + self.assertFalse(watchrule.rule_can_use_sample(self.wr, data)) + + def test_create_watch_data_not_match_dimensions(self): + rule = {u'EvaluationPeriods': u'1', + u'AlarmDescription': u'test alarm', + u'Period': u'300', + u'ComparisonOperator': u'GreaterThanThreshold', + u'Statistic': u'SampleCount', + u'Threshold': u'2', + u'Dimensions': [{u'Name': 'AutoScalingGroupName', + u'Value': 'group_x'}], + u'MetricName': u'CreateDataMetric'} + self.wr = watchrule.WatchRule(context=self.ctx, + watch_name='create_data_test', + stack_id=self.stack_id, rule=rule) + self.wr.store() + + data = {u'CreateDataMetric': {"Unit": "Counter", + "Value": "1", + "Dimensions": [ + {u'AutoScalingGroupName': + u'not_this'}]}, + u'CreateDataMetric': {"Unit": "Counter", + "Value": "1", + "Dimensions": [ + {u'wrong_key': + u'group_x'}]}} + self.assertFalse(watchrule.rule_can_use_sample(self.wr, data)) + def test_destroy(self): rule = {'EvaluationPeriods': '1', 'MetricName': 'test_metric',