From 41011d335ba89dda7c8b565a1c4024b2c5f6ee3d Mon Sep 17 00:00:00 2001 From: Alina Buzachis Date: Fri, 28 Apr 2023 11:38:08 +0200 Subject: [PATCH] Add metrics and extended_statistic keys to cloudwatch module (#1133) (#1485) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [manual backport stable-5] Add metrics and extended_statistic keys to cloudwatch module (#1133) Add metrics and extended_statistic keys to cloudwatch module Signed-off-by: GomathiselviS gomathiselvi@gmail.com To support https://issues.redhat.com/browse/ACA-638 , a new key metric ( a list of dicts) is added to the cloudwatch module SUMMARY ISSUE TYPE Feature Pull Request COMPONENT NAME cloudwatch.py ADDITIONAL INFORMATION Reviewed-by: Bikouo Aubin Reviewed-by: Gonéri Le Bouder goneri@lebouder.net Reviewed-by: Mike Graves mgraves@redhat.com Reviewed-by: GomathiselviS Reviewed-by: Alina Buzachis SUMMARY ISSUE TYPE Bugfix Pull Request Docs Pull Request Feature Pull Request New Module Pull Request COMPONENT NAME ADDITIONAL INFORMATION --- .../fragments/1133-add_metrics_cloudwatch.yml | 3 + plugins/modules/cloudwatch_metric_alarm.py | 165 ++++++++++++++++-- .../cloudwatch_metric_alarm/tasks/main.yml | 106 +++++++++++ 3 files changed, 261 insertions(+), 13 deletions(-) create mode 100644 changelogs/fragments/1133-add_metrics_cloudwatch.yml diff --git a/changelogs/fragments/1133-add_metrics_cloudwatch.yml b/changelogs/fragments/1133-add_metrics_cloudwatch.yml new file mode 100644 index 00000000000..912f186727d --- /dev/null +++ b/changelogs/fragments/1133-add_metrics_cloudwatch.yml @@ -0,0 +1,3 @@ +--- +minor_changes: +- cloudwatch - Add metrics and extended_statistic keys to cloudwatch module (https://github.com/ansible-collections/amazon.aws/pull/1133). diff --git a/plugins/modules/cloudwatch_metric_alarm.py b/plugins/modules/cloudwatch_metric_alarm.py index fc69f71f2ff..f2e3056c695 100644 --- a/plugins/modules/cloudwatch_metric_alarm.py +++ b/plugins/modules/cloudwatch_metric_alarm.py @@ -42,12 +42,93 @@ - Unique name for the alarm. required: true type: str - metric: + metric_name: description: - Name of the monitored metric (e.g. C(CPUUtilization)). - Metric must already exist. required: false type: str + aliases: ['metric'] + metrics: + description: + - 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 + version_added: "5.1.0" + elements: dict + suboptions: + id: + description: + - 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. + 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. + 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. + type: str + required: True + value: + description: The value of the dimension. + type: str + required: True + period: + description: The granularity, in seconds, of the returned data points. + type: int + required: True + stat: + description: The statistic to return. It can include any CloudWatch statistic or extended statistic. + type: str + required: True + unit: + description: Unit to use when storing the metric. + type: str + required: false + expression: + 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. + 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. + 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. @@ -60,6 +141,11 @@ required: false choices: ['SampleCount','Average','Sum','Minimum','Maximum'] type: str + extended_statistic: + description: The percentile statistic for the metric specified in the metric name. + type: str + required: false + version_added: "5.1.0" comparison: description: - Determines how the threshold value is compared @@ -175,7 +261,7 @@ state: present region: ap-southeast-2 name: "cpu-low" - metric: "CPUUtilization" + metric_name: "CPUUtilization" namespace: "AWS/EC2" statistic: Average comparison: "LessThanOrEqualToThreshold" @@ -187,6 +273,26 @@ dimensions: {'InstanceId':'i-XXX'} alarm_actions: ["action1","action2"] + - name: create alarm with metrics + amazon.aws.cloudwatch_metric_alarm: + state: present + region: ap-southeast-2 + name: "cpu-low" + metrics: + - id: 'CPU' + metric_stat: + metric: + dimensions: + name: "InstanceId" + value: "i-xx" + metric_name: "CPUUtilization" + namespace: "AWS/EC2" + period: "300" + stat: "Average" + 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 @@ -209,18 +315,19 @@ from botocore.exceptions import ClientError except ImportError: pass # protected by AnsibleAWSModule - from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule +from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict +from ansible.module_utils.common.dict_transformations 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: @@ -262,6 +369,11 @@ def create_metric_alarm(connection, module, params): result = {} if alarms['MetricAlarms']: + if alarms['MetricAlarms'][0].get('Metrics'): + metric_list = [] + for metric_element in alarms['MetricAlarms'][0]['Metrics']: + metric_list.append(camel_dict_to_snake_dict(metric_element)) + alarms['MetricAlarms'][0]['Metrics'] = metric_list result = alarms['MetricAlarms'][0] module.exit_json(changed=changed, @@ -276,6 +388,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'), @@ -304,7 +418,7 @@ def delete_metric_alarm(connection, module, 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', @@ -317,22 +431,39 @@ 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', default=[]), ) - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) + mutually_exclusive = [ + ['metric_name', 'metrics'], + ['dimensions', 'metrics'], + ['period', 'metrics'], + ['namespace', 'metrics'], + ['statistic', 'metrics'], + ['extended_statistic', 'metrics'], + ['unit', 'metrics'], + ['statistic', 'extended_statistic'], + ] + + module = AnsibleAWSModule( + argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True, + ) state = module.params.get('state') params = dict() params['AlarmName'] = module.params.get('name') - params['MetricName'] = module.params.get('metric') + params['MetricName'] = module.params.get('metric_name') params['Namespace'] = module.params.get('namespace') params['Statistic'] = module.params.get('statistic') params['ComparisonOperator'] = module.params.get('comparison') @@ -347,6 +478,14 @@ 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') + if module.params.get('metrics'): + params['Metrics'] = snake_dict_to_camel_dict(module.params['metrics'], capitalize_first=True) + if module.params.get('extended_statistic'): + params['ExtendedStatistic'] = module.params.get('extended_statistic') + + 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..d3f522c97ed 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].metrics[0].metric_stat.metric.namespace' + - 'ec2_instance_metric_alarm_metrics.metrics[0].metric_stat.metric.metric_name == alarm_info_metrics.metric_alarms[0].metrics[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].metrics[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].metrics[0].metric_stat.metric.dimensions[0].value' + - 'ec2_instance_metric_alarm_metrics.metrics[0].id == alarm_info_metrics.metric_alarms[0].metrics[0].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].return_data == alarm_info_metrics.metric_alarms[0].metrics[0].return_data' + + - name: try to remove the alarm ec2_metric_alarm: state: absent @@ -396,6 +465,43 @@ that: - 'alarm_info_no_unit.metric_alarms | length == 0' + - name: create ec2 metric alarm by providing mutually exclusive values + ec2_metric_alarm: + dimensions: + InstanceId: '{{ ec2_instance_results.instances[0].instance_id }}' + state: present + name: '{{ alarm_full_name }}' + metric: CPUUtilization + namespace: AWS/EC2 + treat_missing_data: missing + statistic: Average + comparison: LessThanOrEqualToThreshold + threshold: 5.0 + period: 300 + 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_mutually_exclusive + ignore_errors: true + + - assert: + that: + - ec2_instance_metric_mutually_exclusive.failed + - '"parameters are mutually exclusive" in ec2_instance_metric_mutually_exclusive.msg' + always: - name: try to delete the alarm ec2_metric_alarm: