#!/usr/bin/python # Copyright: Ansible Project # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) from __future__ import absolute_import, division, print_function __metaclass__ = type ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['stableinterface'], 'supported_by': 'community'} DOCUMENTATION = """ module: ec2_metric_alarm short_description: "Create/update or delete AWS Cloudwatch 'metric alarms'" description: - Can create or delete AWS metric alarms. - Metrics you wish to alarm on must already exist. version_added: "1.6" author: "Zacharie Eakin (@Zeekin)" options: state: description: - register or deregister the alarm required: true choices: ['present', 'absent'] name: description: - Unique name for the alarm required: true metric: description: - Name of the monitored metric (e.g. CPUUtilization) - Metric must already exist required: false namespace: description: - Name of the appropriate namespace ('AWS/EC2', 'System/Linux', etc.), which determines the category it will appear under in cloudwatch required: false statistic: description: - Operation applied to the metric - Works in conjunction with period and evaluation_periods to determine the comparison value required: false choices: ['SampleCount','Average','Sum','Minimum','Maximum'] comparison: description: - Determines how the threshold value is compared required: false choices: ['<=','<','>','>='] threshold: description: - Sets the min/max bound for triggering the alarm required: false period: description: - The time (in seconds) between metric evaluations required: false evaluation_periods: description: - The number of times in which the metric is evaluated before final calculation required: false unit: description: - The threshold's unit of measurement required: false choices: - 'Seconds' - 'Microseconds' - 'Milliseconds' - 'Bytes' - 'Kilobytes' - 'Megabytes' - 'Gigabytes' - 'Terabytes' - 'Bits' - 'Kilobits' - 'Megabits' - 'Gigabits' - 'Terabits' - 'Percent' - 'Count' - 'Bytes/Second' - 'Kilobytes/Second' - 'Megabytes/Second' - 'Gigabytes/Second' - 'Terabytes/Second' - 'Bits/Second' - 'Kilobits/Second' - 'Megabits/Second' - 'Gigabits/Second' - 'Terabits/Second' - 'Count/Second' - 'None' description: description: - A longer description of the alarm required: false dimensions: description: - Describes to what the alarm is applied required: false alarm_actions: description: - A list of the names action(s) taken when the alarm is in the 'alarm' status, denoted as Amazon Resource Name(s) required: false insufficient_data_actions: description: - A list of the names of action(s) to take when the alarm is in the 'insufficient_data' status required: false ok_actions: description: - A list of the names of action(s) to take when the alarm is in the 'ok' status, denoted as Amazon Resource Name(s) required: false extends_documentation_fragment: - aws - ec2 """ EXAMPLES = ''' - name: create alarm ec2_metric_alarm: state: present region: ap-southeast-2 name: "cpu-low" metric: "CPUUtilization" namespace: "AWS/EC2" statistic: Average comparison: "<=" threshold: 5.0 period: 300 evaluation_periods: 3 unit: "Percent" description: "This will alarm when a bamboo slave's cpu usage average is lower than 5% for 15 minutes " dimensions: {'InstanceId':'i-XXX'} alarm_actions: ["action1","action2"] - name: Create an alarm to recover a failed instance ec2_metric_alarm: state: present region: us-west-1 name: "recover-instance" metric: "StatusCheckFailed_System" namespace: "AWS/EC2" statistic: "Minimum" comparison: ">=" threshold: 1.0 period: 60 evaluation_periods: 2 unit: "Seconds" description: "This will recover an instance when it fails" dimensions: {"InstanceId":'i-XXX'} alarm_actions: ["arn:aws:automate:us-west-1:ec2:recover"] ''' try: import boto.ec2.cloudwatch from boto.ec2.cloudwatch import MetricAlarm from boto.exception import BotoServerError, NoAuthHandlerFound except ImportError: pass # Taken care of by ec2.HAS_BOTO from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.ec2 import (AnsibleAWSError, HAS_BOTO, connect_to_aws, ec2_argument_spec, get_aws_connection_info) def create_metric_alarm(connection, module): name = module.params.get('name') metric = module.params.get('metric') namespace = module.params.get('namespace') statistic = module.params.get('statistic') comparison = module.params.get('comparison') threshold = module.params.get('threshold') period = module.params.get('period') evaluation_periods = module.params.get('evaluation_periods') unit = module.params.get('unit') description = module.params.get('description') dimensions = module.params.get('dimensions') alarm_actions = module.params.get('alarm_actions') insufficient_data_actions = module.params.get('insufficient_data_actions') ok_actions = module.params.get('ok_actions') alarms = connection.describe_alarms(alarm_names=[name]) if not alarms: alm = MetricAlarm( name=name, metric=metric, namespace=namespace, statistic=statistic, comparison=comparison, threshold=threshold, period=period, evaluation_periods=evaluation_periods, unit=unit, description=description, dimensions=dimensions, alarm_actions=alarm_actions, insufficient_data_actions=insufficient_data_actions, ok_actions=ok_actions ) try: connection.create_alarm(alm) changed = True alarms = connection.describe_alarms(alarm_names=[name]) except BotoServerError as e: module.fail_json(msg=str(e)) else: alarm = alarms[0] changed = False for attr in ('comparison', 'metric', 'namespace', 'statistic', 'threshold', 'period', 'evaluation_periods', 'unit', 'description'): if getattr(alarm, attr) != module.params.get(attr): changed = True setattr(alarm, attr, module.params.get(attr)) # this is to deal with a current bug where you cannot assign '<=>' to the comparator when modifying an existing alarm comparison = alarm.comparison comparisons = {'<=': 'LessThanOrEqualToThreshold', '<': 'LessThanThreshold', '>=': 'GreaterThanOrEqualToThreshold', '>': 'GreaterThanThreshold'} alarm.comparison = comparisons[comparison] dim1 = module.params.get('dimensions') dim2 = alarm.dimensions for keys in dim1: if not isinstance(dim1[keys], list): dim1[keys] = [dim1[keys]] if keys not in dim2 or dim1[keys] != dim2[keys]: changed = True setattr(alarm, 'dimensions', dim1) for attr in ('alarm_actions', 'insufficient_data_actions', 'ok_actions'): action = module.params.get(attr) or [] # Boto and/or ansible may provide same elements in lists but in different order. # Compare on sets since they do not need any order. if set(getattr(alarm, attr)) != set(action): changed = True setattr(alarm, attr, module.params.get(attr)) try: if changed: connection.create_alarm(alarm) except BotoServerError as e: module.fail_json(msg=str(e)) result = alarms[0] module.exit_json(changed=changed, name=result.name, actions_enabled=result.actions_enabled, alarm_actions=result.alarm_actions, alarm_arn=result.alarm_arn, comparison=result.comparison, description=result.description, dimensions=result.dimensions, evaluation_periods=result.evaluation_periods, insufficient_data_actions=result.insufficient_data_actions, last_updated=result.last_updated, metric=result.metric, namespace=result.namespace, ok_actions=result.ok_actions, period=result.period, state_reason=result.state_reason, state_value=result.state_value, statistic=result.statistic, threshold=result.threshold, unit=result.unit) def delete_metric_alarm(connection, module): name = module.params.get('name') alarms = connection.describe_alarms(alarm_names=[name]) if alarms: try: connection.delete_alarms([name]) module.exit_json(changed=True) except BotoServerError as e: module.fail_json(msg=str(e)) else: module.exit_json(changed=False) def main(): argument_spec = ec2_argument_spec() argument_spec.update( dict( name=dict(required=True, type='str'), metric=dict(type='str'), namespace=dict(type='str'), statistic=dict(type='str', choices=['SampleCount', 'Average', 'Sum', 'Minimum', 'Maximum']), comparison=dict(type='str', choices=['<=', '<', '>', '>=']), threshold=dict(type='float'), period=dict(type='int'), unit=dict(type='str', choices=['Seconds', 'Microseconds', 'Milliseconds', 'Bytes', 'Kilobytes', 'Megabytes', 'Gigabytes', 'Terabytes', 'Bits', 'Kilobits', 'Megabits', 'Gigabits', 'Terabits', 'Percent', 'Count', 'Bytes/Second', 'Kilobytes/Second', 'Megabytes/Second', 'Gigabytes/Second', 'Terabytes/Second', 'Bits/Second', 'Kilobits/Second', 'Megabits/Second', 'Gigabits/Second', 'Terabits/Second', 'Count/Second', 'None']), evaluation_periods=dict(type='int'), description=dict(type='str'), dimensions=dict(type='dict', default={}), alarm_actions=dict(type='list'), insufficient_data_actions=dict(type='list'), ok_actions=dict(type='list'), state=dict(default='present', choices=['present', 'absent']), ) ) module = AnsibleModule(argument_spec=argument_spec) if not HAS_BOTO: module.fail_json(msg='boto required for this module') state = module.params.get('state') region, ec2_url, aws_connect_params = get_aws_connection_info(module) if region: try: connection = connect_to_aws(boto.ec2.cloudwatch, region, **aws_connect_params) except (NoAuthHandlerFound, AnsibleAWSError) as e: module.fail_json(msg=str(e)) else: module.fail_json(msg="region must be specified") if state == 'present': create_metric_alarm(connection, module) elif state == 'absent': delete_metric_alarm(connection, module) if __name__ == '__main__': main()