diff --git a/plugins/modules/cloudwatch_metric_alarm.py b/plugins/modules/cloudwatch_metric_alarm.py index aa0752c0aaa..012a05fceea 100644 --- a/plugins/modules/cloudwatch_metric_alarm.py +++ b/plugins/modules/cloudwatch_metric_alarm.py @@ -51,34 +51,40 @@ aliases: ['metric'] metrics: description: - - An array of MetricDataQuery structures that enable + - An array of MetricDataQuery structures that enable you to create an alarm based on the result of a metric math expression. type: list + required: false elements: dict suboptions: id: description: - - A short name used to tie this object to the results in the response. + - A short name used to tie this object to the results in the response. type: str + required: true metric_stat: - description: The metric to be returned, along with statistics, period, and units. + description: The metric to be returned, along with statistics, period, and units. type: dict + required: false suboptions: metric: description: The metric to return, including the metric name, namespace, and dimensions. type: dict + required: false suboptions: namespace: description: The namespace of the metric. type: str + required: false metric_name: - description: The name of the metric. + description: The name of the metric. type: str required: True dimensions: description: a name/value pair that is part of the identity of a metric. type: list elements: dict + required: false suboptions: name: description: The name of the dimension. @@ -89,7 +95,7 @@ type: str required: True period: - description: The granularity, in seconds, of the returned data points. + description: The granularity, in seconds, of the returned data points. type: int required: True stat: @@ -99,23 +105,29 @@ unit: description: Unit to use when storing the metric. type: str + required: false expression: - description: + description: - This field can contain either a Metrics Insights query, or a metric math expression to be performed on the returned data. type: str + required: false label: - description: A human-readable label for this metric or expression. + description: A human-readable label for this metric or expression. type: str + required: false return_data: description: This option indicates whether to return the timestamps and raw data values of this metric. type: bool + required: false period: - description: The granularity, in seconds, of the returned data points. + description: The granularity, in seconds, of the returned data points. type: int + required: false account_id: description: The ID of the account where the metrics are located, if this is a cross-account alarm. type: str + required: false namespace: description: - Name of the appropriate namespace (C(AWS/EC2), C(System/Linux), etc.), which determines the category it will appear under in CloudWatch. @@ -130,7 +142,8 @@ type: str extended_statistic: description: The percentile statistic for the metric specified in the metric name. - type: string + type: str + required: false comparison: description: - Determines how the threshold value is compared @@ -273,7 +286,7 @@ unit: "Percent" return_data: False alarm_actions: ["action1","action2"] - + - name: Create an alarm to recover a failed instance amazon.aws.cloudwatch_metric_alarm: state: present @@ -296,18 +309,18 @@ from botocore.exceptions import ClientError except ImportError: pass # protected by AnsibleAWSModule - from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule - +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import snake_dict_to_camel_dict def create_metric_alarm(connection, module, params): alarms = connection.describe_alarms(AlarmNames=[params['AlarmName']]) - if not isinstance(params['Dimensions'], list): - fixed_dimensions = [] - for key, value in params['Dimensions'].items(): - fixed_dimensions.append({'Name': key, 'Value': value}) - params['Dimensions'] = fixed_dimensions + if params.get('Dimensions'): + if not isinstance(params['Dimensions'], list): + fixed_dimensions = [] + for key, value in params['Dimensions'].items(): + fixed_dimensions.append({'Name': key, 'Value': value}) + params['Dimensions'] = fixed_dimensions if not alarms['MetricAlarms']: try: @@ -327,7 +340,7 @@ def create_metric_alarm(connection, module, params): for key in ['ActionsEnabled', 'StateValue', 'StateReason', 'StateReasonData', 'StateUpdatedTimestamp', - 'AlarmArn', 'AlarmConfigurationUpdatedTimestamp']: + 'AlarmArn', 'AlarmConfigurationUpdatedTimestamp', 'Metrics']: alarm.pop(key, None) if alarm != params: changed = True @@ -361,6 +374,8 @@ def create_metric_alarm(connection, module, params): insufficient_data_actions=result.get('InsufficientDataActions'), last_updated=result.get('AlarmConfigurationUpdatedTimestamp'), metric=result.get('MetricName'), + metric_name=result.get('MetricName'), + metrics=result.get('Metrics'), namespace=result.get('Namespace'), ok_actions=result.get('OKActions'), period=result.get('Period'), @@ -384,12 +399,25 @@ def delete_metric_alarm(connection, module, params): module.fail_json_aws(e) else: module.exit_json(changed=False) + +def delete_none_values(params): + """Delete None values recursively from params""" + if isinstance(params, dict): + for key, value in list(params.items()): + if isinstance(value, (list, dict, tuple, set)): + params[key] = delete_none_values(value) + elif value is None or key is None: + del params[key] + elif isinstance(params, list): + params = [(delete_none_values(item) for item in params if item is not None)] + + return params def main(): argument_spec = dict( name=dict(required=True, type='str'), - metric=dict(type='str'), + metric_name=dict(type='str', aliases=['metric']), namespace=dict(type='str'), statistic=dict(type='str', choices=['SampleCount', 'Average', 'Sum', 'Minimum', 'Maximum']), comparison=dict(type='str', choices=['LessThanOrEqualToThreshold', 'LessThanThreshold', 'GreaterThanThreshold', @@ -402,28 +430,34 @@ def main(): 'Terabytes/Second', 'Bits/Second', 'Kilobits/Second', 'Megabits/Second', 'Gigabits/Second', 'Terabits/Second', 'Count/Second', 'None']), evaluation_periods=dict(type='int'), + extended_statistic=dict(type='str'), description=dict(type='str'), - dimensions=dict(type='dict', default={}), + dimensions=dict(type='dict'), alarm_actions=dict(type='list', default=[], elements='str'), insufficient_data_actions=dict(type='list', default=[], elements='str'), ok_actions=dict(type='list', default=[], elements='str'), treat_missing_data=dict(type='str', choices=['breaching', 'notBreaching', 'ignore', 'missing'], default='missing'), state=dict(default='present', choices=['present', 'absent']), - metrics=dict(type='list', elements='dict'), + metrics=dict(type='list', elements='dict', default=[]), ) - + mutually_exclusive = [ - ['metric_name', 'metrics', 'dimensions', 'period', 'namespace', 'statistic', 'extended_statistic'], + ['metric_name', 'metrics'], + ['dimensions', 'metrics'], + ['period', 'metrics'], + ['namespace', 'metrics'], + ['statistic', 'metrics'], + ['extended_statistic', 'metrics'], + ['unit', 'metrics'], ['statistic', 'extended_statistic'], ] - + #mutually_exclusive = [] module = AnsibleAWSModule( argument_spec=argument_spec, mutually_exclusive=mutually_exclusive, supports_check_mode=True, ) - state = module.params.get('state') params = dict() @@ -443,7 +477,20 @@ def main(): params['InsufficientDataActions'] = module.params.get('insufficient_data_actions', []) params['OKActions'] = module.params.get('ok_actions', []) params['TreatMissingData'] = module.params.get('treat_missing_data') - params['Metrics'] = module.params.get('metrics') + if module.params.get('metrics'): + params['Metrics'] = [] + for element in module.params.get('metrics'): + params['Metrics'].append(snake_dict_to_camel_dict(element, capitalize_first=True)) + if module.params.get('extended_statistic'): + params['ExtendedStatistic'] = module.params.get('extended_statistic') + + # Remove None value from params + #delete_none_values(params) + for key, value in list(params.items()): + if value is None: + del params[key] + + connection = module.client('cloudwatch') diff --git a/tests/integration/targets/cloudwatch_metric_alarm/tasks/main.yml b/tests/integration/targets/cloudwatch_metric_alarm/tasks/main.yml index 13476e84f97..dffdde04824 100644 --- a/tests/integration/targets/cloudwatch_metric_alarm/tasks/main.yml +++ b/tests/integration/targets/cloudwatch_metric_alarm/tasks/main.yml @@ -374,6 +374,75 @@ - 'ec2_instance_metric_alarm_no_unit.description == alarm_info_no_unit.metric_alarms[0].alarm_description' - 'ec2_instance_metric_alarm_no_unit.treat_missing_data == alarm_info_no_unit.metric_alarms[0].treat_missing_data' + - name: try to remove the alarm + ec2_metric_alarm: + state: absent + name: '{{ alarm_full_name }}' + register: ec2_instance_metric_alarm_deletion + + - name: Verify that the alarm reports deleted/changed + assert: + that: + - ec2_instance_metric_alarm_deletion.changed + + - name: get info on alarms + amazon.aws.cloudwatch_metric_alarm_info: + alarm_names: + - "{{ alarm_full_name }}" + register: alarm_info + + - name: Verify that the alarm was deleted using cli + assert: + that: + - 'alarm_info.metric_alarms | length == 0' + + - name: create ec2 metric alarm with metrics + ec2_metric_alarm: + state: present + name: '{{ alarm_full_name }}' + treat_missing_data: missing + comparison: LessThanOrEqualToThreshold + threshold: 5.0 + evaluation_periods: 3 + description: This will alarm when an instance's cpu usage average is lower than + 5% for 15 minutes + metrics: + - id: cpu + metric_stat: + metric: + dimensions: + - name: "InstanceId" + value: "{{ ec2_instance_results.instances[0].instance_id }}" + metric_name: "CPUUtilization" + namespace: "AWS/EC2" + period: 300 + stat: "Average" + unit: "Percent" + return_data: true + register: ec2_instance_metric_alarm_metrics + + - name: get info on alarms + amazon.aws.cloudwatch_metric_alarm_info: + alarm_names: + - "{{ alarm_full_name }}" + register: alarm_info_metrics + + - name: verify that an alarm was created + assert: + that: + - ec2_instance_metric_alarm_metrics.changed + - ec2_instance_metric_alarm_metrics.alarm_arn + - 'ec2_instance_metric_alarm_metrics.metrics[0].metric_stat.stat == alarm_info_metrics.metric_alarms[0].metrics[0].metric_stat.stat' + - 'ec2_instance_metric_alarm_metrics.metrics[0].metric_stat.metric.namespace == alarm_info_metrics.metric_alarms[0].nmetrics[0].metric_stat.metric.namespace' + - 'ec2_instance_metric_alarm_metrics.metrics[0].metric_stat.metric.metric_name == alarm_info_metrics.metric_alarms[0].nmetrics[0].metric_stat.metric.metric_name' + - 'ec2_instance_metric_alarm_metrics.metrics[0].metric_stat.metric.dimensions[0].name == alarm_info_metrics.metric_alarms[0].nmetrics[0].metric_stat.metric.dimensions[0].name' + - 'ec2_instance_metric_alarm_metrics.metrics[0].metric_stat.metric.dimensions[0].value == alarm_info_metrics.metric_alarms[0].nmetrics[0].metric_stat.metric.dimensions[0].value' + - 'ec2_instance_metric_alarm_metrics.metrics[0].metric_stat.id == alarm_info_metrics.metric_alarms[0].metrics[0].metric_stat.id' + - 'ec2_instance_metric_alarm_metrics.metrics[0].metric_stat.period == alarm_info_metrics.metric_alarms[0].metrics[0].metric_stat.period' + - 'ec2_instance_metric_alarm_metrics.metrics[0].metric_stat.unit == alarm_info_metrics.metric_alarms[0].metrics[0].metric_stat.unit' + - 'ec2_instance_metric_alarm_metrics.metrics[0].metric_stat.return_data == alarm_info_metrics.metric_alarms[0].metrics[0].metric_stat.return_data' + + - name: try to remove the alarm ec2_metric_alarm: state: absent