From 493ec588abcd715211818b5832e6997e75f58c42 Mon Sep 17 00:00:00 2001 From: Tad Merchant Date: Thu, 12 Dec 2019 16:09:09 -0500 Subject: [PATCH] Ec2 metric alarm boto3 and treat missing data (#62669) * Converted ec2_metric_alarm to boto3. Added treat_missing_data option. * Handle potentially non-existent alarm keys in ec2_metric_alarm module * Add treat missing data to ec2_metric_alarms wth some tests Continues the work of #23407 * Clean up ec2_metric_alarm main test playbook * fix test suite and sanity checks * more fixes for sanity tests * fixes to ec2_metric_alarms requested in code review * import ClientError from botocore, catch generic ClientError * more fixes from review drops extra dict in argument spec and set_facts for aws access * Fix pep8 blank line issue * switch to fail_json_aws, add idempotency test * fix under indented continuation * remove unsupported alias * Add group to ec2_metric_alarm aliases * Put alarm prefix before resource prefix to match aws-terminator pr 63 * Add type for treat_missing_data --- .../modules/cloud/amazon/ec2_metric_alarm.py | 369 ++++++++++-------- .../targets/ec2_metric_alarm/aliases | 2 + .../ec2_metric_alarm/defaults/main.yml | 6 + .../targets/ec2_metric_alarm/meta/main.yml | 3 + .../ec2_metric_alarm/tasks/env_cleanup.yml | 94 +++++ .../ec2_metric_alarm/tasks/env_setup.yml | 62 +++ .../targets/ec2_metric_alarm/tasks/main.yml | 228 +++++++++++ .../targets/ec2_metric_alarm/vars/main.yml | 1 + 8 files changed, 603 insertions(+), 162 deletions(-) create mode 100644 test/integration/targets/ec2_metric_alarm/aliases create mode 100644 test/integration/targets/ec2_metric_alarm/defaults/main.yml create mode 100644 test/integration/targets/ec2_metric_alarm/meta/main.yml create mode 100644 test/integration/targets/ec2_metric_alarm/tasks/env_cleanup.yml create mode 100644 test/integration/targets/ec2_metric_alarm/tasks/env_setup.yml create mode 100644 test/integration/targets/ec2_metric_alarm/tasks/main.yml create mode 100644 test/integration/targets/ec2_metric_alarm/vars/main.yml diff --git a/lib/ansible/modules/cloud/amazon/ec2_metric_alarm.py b/lib/ansible/modules/cloud/amazon/ec2_metric_alarm.py index 42f1416c81..b221ae49e3 100644 --- a/lib/ansible/modules/cloud/amazon/ec2_metric_alarm.py +++ b/lib/ansible/modules/cloud/amazon/ec2_metric_alarm.py @@ -1,6 +1,18 @@ #!/usr/bin/python -# Copyright: Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . from __future__ import absolute_import, division, print_function __metaclass__ = type @@ -51,10 +63,19 @@ options: type: str comparison: description: - - Determines how the threshold value is compared. + - Determines how the threshold value is compared + - Symbolic comparison operators have been deprecated, and will be removed in 2.14 required: false - choices: ['<=','<','>','>='] type: str + choices: + - 'GreaterThanOrEqualToThreshold' + - 'GreaterThanThreshold' + - 'LessThanThreshold' + - 'LessThanOrEqualToThreshold' + - '<=' + - '<' + - '>=' + - '>' threshold: description: - Sets the min/max bound for triggering the alarm. @@ -76,33 +97,33 @@ options: required: false 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' + - '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. @@ -133,6 +154,18 @@ options: required: false type: list elements: str + treat_missing_data: + description: + - Sets how the alarm handles missing data points. + required: false + type: str + choices: + - 'breaching' + - 'notBreaching' + - 'ignore' + - 'missing' + default: 'missing' + version_added: "2.10" extends_documentation_fragment: - aws - ec2 @@ -147,7 +180,7 @@ EXAMPLES = ''' metric: "CPUUtilization" namespace: "AWS/EC2" statistic: Average - comparison: "<=" + comparison: "LessThanOrEqualToThreshold" threshold: 5.0 period: 300 evaluation_periods: 3 @@ -175,17 +208,12 @@ EXAMPLES = ''' ''' -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.aws.core import AnsibleAWSModule -import traceback -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) +try: + from botocore.exceptions import ClientError +except ImportError: + pass # protected by AnsibleAWSModule def create_metric_alarm(connection, module): @@ -204,156 +232,173 @@ def create_metric_alarm(connection, module): alarm_actions = module.params.get('alarm_actions') insufficient_data_actions = module.params.get('insufficient_data_actions') ok_actions = module.params.get('ok_actions') + treat_missing_data = module.params.get('treat_missing_data') - alarms = None - try: - alarms = connection.describe_alarms(alarm_names=[name]) - except BotoServerError as e: - module.fail_json(msg="Failed to describe alarm %s: %s" % (name, str(e)), exception=traceback.format_exc()) + warnings = [] - if not alarms: + alarms = connection.describe_alarms(AlarmNames=[name]) - 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 - ) + comparisons = {'<=': 'LessThanOrEqualToThreshold', + '<': 'LessThanThreshold', + '>=': 'GreaterThanOrEqualToThreshold', + '>': 'GreaterThanThreshold'} + if comparison in ('<=', '<', '>', '>='): + module.deprecate('Using the <=, <, > and >= operators for comparison has been deprecated. Please use LessThanOrEqualToThreshold, ' + 'LessThanThreshold, GreaterThanThreshold or GreaterThanOrEqualToThreshold instead.', version="2.14") + comparison = comparisons[comparison] + + if not isinstance(dimensions, list): + fixed_dimensions = [] + for key, value in dimensions.items(): + fixed_dimensions.append({'Name': key, 'Value': value}) + dimensions = fixed_dimensions + + if not alarms['MetricAlarms']: try: - connection.create_alarm(alm) + connection.put_metric_alarm(AlarmName=name, + MetricName=metric, + Namespace=namespace, + Statistic=statistic, + ComparisonOperator=comparison, + Threshold=threshold, + Period=period, + EvaluationPeriods=evaluation_periods, + Unit=unit, + AlarmDescription=description, + Dimensions=dimensions, + AlarmActions=alarm_actions, + InsufficientDataActions=insufficient_data_actions, + OKActions=ok_actions, + TreatMissingData=treat_missing_data) changed = True - alarms = connection.describe_alarms(alarm_names=[name]) - except BotoServerError as e: - module.fail_json(msg="Failed to create alarm %s: %s" % (name, str(e)), exception=traceback.format_exc()) + alarms = connection.describe_alarms(AlarmNames=[name]) + except ClientError as e: + module.fail_json_aws(e) else: - alarm = alarms[0] changed = False + alarm = alarms['MetricAlarms'][0] - for attr in ('comparison', 'metric', 'namespace', 'statistic', 'threshold', 'period', 'evaluation_periods', 'unit', 'description'): - if getattr(alarm, attr) != module.params.get(attr): + # Workaround for alarms created before TreatMissingData was introduced + if 'TreatMissingData' not in alarm.keys(): + alarm['TreatMissingData'] = 'missing' + + for key, value in {'MetricName': metric, + 'Namespace': namespace, + 'Statistic': statistic, + 'ComparisonOperator': comparison, + 'Threshold': threshold, + 'Period': period, + 'EvaluationPeriods': evaluation_periods, + 'Unit': unit, + 'AlarmDescription': description, + 'Dimensions': dimensions, + 'TreatMissingData': treat_missing_data}.items(): + try: + if alarm[key] != value: + changed = True + except KeyError: + if value is not None: + changed = True + + alarm[key] = value + + for key, value in {'AlarmActions': alarm_actions, + 'InsufficientDataActions': insufficient_data_actions, + 'OKActions': ok_actions}.items(): + action = value or [] + if alarm[key] != action: 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)) + alarm[key] = value 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) + connection.put_metric_alarm(AlarmName=alarm['AlarmName'], + MetricName=alarm['MetricName'], + Namespace=alarm['Namespace'], + Statistic=alarm['Statistic'], + ComparisonOperator=alarm['ComparisonOperator'], + Threshold=alarm['Threshold'], + Period=alarm['Period'], + EvaluationPeriods=alarm['EvaluationPeriods'], + Unit=alarm['Unit'], + AlarmDescription=alarm['AlarmDescription'], + Dimensions=alarm['Dimensions'], + AlarmActions=alarm['AlarmActions'], + InsufficientDataActions=alarm['InsufficientDataActions'], + OKActions=alarm['OKActions'], + TreatMissingData=alarm['TreatMissingData']) + except ClientError as e: + module.fail_json_aws(e) + + result = alarms['MetricAlarms'][0] + module.exit_json(changed=changed, warnings=warnings, + name=result['AlarmName'], + actions_enabled=result['ActionsEnabled'], + alarm_actions=result['AlarmActions'], + alarm_arn=result['AlarmArn'], + comparison=result['ComparisonOperator'], + description=result['AlarmDescription'], + dimensions=result['Dimensions'], + evaluation_periods=result['EvaluationPeriods'], + insufficient_data_actions=result['InsufficientDataActions'], + last_updated=result['AlarmConfigurationUpdatedTimestamp'], + metric=result['MetricName'], + namespace=result['Namespace'], + ok_actions=result['OKActions'], + period=result['Period'], + state_reason=result['StateReason'], + state_value=result['StateValue'], + statistic=result['Statistic'], + threshold=result['Threshold'], + treat_missing_data=result['TreatMissingData'], + unit=result['Unit']) def delete_metric_alarm(connection, module): name = module.params.get('name') + alarms = connection.describe_alarms(AlarmNames=[name]) - alarms = None - try: - alarms = connection.describe_alarms(alarm_names=[name]) - except BotoServerError as e: - module.fail_json(msg="Failed to describe alarm %s: %s" % (name, str(e)), exception=traceback.format_exc()) - - if alarms: + if alarms['MetricAlarms']: try: - connection.delete_alarms([name]) + connection.delete_alarms(AlarmNames=[name]) module.exit_json(changed=True) - except BotoServerError as e: - module.fail_json(msg=str(e)) + except (ClientError) as e: + module.fail_json_aws(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']), - ) + argument_spec = 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=['LessThanOrEqualToThreshold', 'LessThanThreshold', 'GreaterThanThreshold', + 'GreaterThanOrEqualToThreshold', '<=', '<', '>', '>=']), + 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', default=[]), + insufficient_data_actions=dict(type='list', default=[]), + ok_actions=dict(type='list', default=[]), + treat_missing_data=dict(type='str', choices=['breaching', 'notBreaching', 'ignore', 'missing'], default='missing'), + 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') + module = AnsibleAWSModule(argument_spec=argument_spec) 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") + connection = module.client('cloudwatch') if state == 'present': create_metric_alarm(connection, module) diff --git a/test/integration/targets/ec2_metric_alarm/aliases b/test/integration/targets/ec2_metric_alarm/aliases new file mode 100644 index 0000000000..a112c3d1bb --- /dev/null +++ b/test/integration/targets/ec2_metric_alarm/aliases @@ -0,0 +1,2 @@ +cloud/aws +shippable/aws/group1 diff --git a/test/integration/targets/ec2_metric_alarm/defaults/main.yml b/test/integration/targets/ec2_metric_alarm/defaults/main.yml new file mode 100644 index 0000000000..4d80b5d6e0 --- /dev/null +++ b/test/integration/targets/ec2_metric_alarm/defaults/main.yml @@ -0,0 +1,6 @@ +--- +# defaults file for ec2_instance +ec2_instance_name: '{{ resource_prefix }}-node' +ec2_instance_owner: 'integration-run-{{ resource_prefix }}' +ec2_ami_name: "amzn-ami-hvm*" +alarm_prefix: "ansible-test" diff --git a/test/integration/targets/ec2_metric_alarm/meta/main.yml b/test/integration/targets/ec2_metric_alarm/meta/main.yml new file mode 100644 index 0000000000..1f64f1169a --- /dev/null +++ b/test/integration/targets/ec2_metric_alarm/meta/main.yml @@ -0,0 +1,3 @@ +dependencies: + - prepare_tests + - setup_ec2 diff --git a/test/integration/targets/ec2_metric_alarm/tasks/env_cleanup.yml b/test/integration/targets/ec2_metric_alarm/tasks/env_cleanup.yml new file mode 100644 index 0000000000..e90ddc6450 --- /dev/null +++ b/test/integration/targets/ec2_metric_alarm/tasks/env_cleanup.yml @@ -0,0 +1,94 @@ +- name: remove any instances in the test VPC + ec2_instance: + filters: + vpc_id: "{{ testing_vpc.vpc.id }}" + state: absent + register: removed + until: removed is not failed + ignore_errors: yes + retries: 10 + +- name: remove ENIs + ec2_eni_info: + filters: + vpc-id: "{{ testing_vpc.vpc.id }}" + register: enis + +- name: delete all ENIs + ec2_eni: + eni_id: "{{ item.id }}" + state: absent + until: removed is not failed + with_items: "{{ enis.network_interfaces }}" + ignore_errors: yes + retries: 10 + +- name: remove the security group + ec2_group: + name: "{{ resource_prefix }}-sg" + description: a security group for ansible tests + vpc_id: "{{ testing_vpc.vpc.id }}" + state: absent + register: removed + until: removed is not failed + ignore_errors: yes + retries: 10 + +- name: remove routing rules + ec2_vpc_route_table: + state: absent + vpc_id: "{{ testing_vpc.vpc.id }}" + tags: + created: "{{ resource_prefix }}-route" + routes: + - dest: 0.0.0.0/0 + gateway_id: "{{ igw.gateway_id }}" + subnets: + - "{{ testing_subnet_a.subnet.id }}" + - "{{ testing_subnet_b.subnet.id }}" + register: removed + until: removed is not failed + ignore_errors: yes + retries: 10 + +- name: remove internet gateway + ec2_vpc_igw: + vpc_id: "{{ testing_vpc.vpc.id }}" + state: absent + register: removed + until: removed is not failed + ignore_errors: yes + retries: 10 + +- name: remove subnet A + ec2_vpc_subnet: + state: absent + vpc_id: "{{ testing_vpc.vpc.id }}" + cidr: 10.22.32.0/24 + register: removed + until: removed is not failed + ignore_errors: yes + retries: 10 + +- name: remove subnet B + ec2_vpc_subnet: + state: absent + vpc_id: "{{ testing_vpc.vpc.id }}" + cidr: 10.22.33.0/24 + register: removed + until: removed is not failed + ignore_errors: yes + retries: 10 + +- name: remove the VPC + ec2_vpc_net: + name: "{{ resource_prefix }}-vpc" + cidr_block: 10.22.32.0/23 + state: absent + tags: + Name: Ansible Testing VPC + tenancy: default + register: removed + until: removed is not failed + ignore_errors: yes + retries: 10 diff --git a/test/integration/targets/ec2_metric_alarm/tasks/env_setup.yml b/test/integration/targets/ec2_metric_alarm/tasks/env_setup.yml new file mode 100644 index 0000000000..80b49dbcf7 --- /dev/null +++ b/test/integration/targets/ec2_metric_alarm/tasks/env_setup.yml @@ -0,0 +1,62 @@ +- name: Create VPC for use in testing + ec2_vpc_net: + name: "{{ resource_prefix }}-vpc" + cidr_block: 10.22.32.0/23 + tags: + Name: Ansible ec2_instance Testing VPC + tenancy: default + register: testing_vpc + +- name: Create internet gateway for use in testing + ec2_vpc_igw: + vpc_id: "{{ testing_vpc.vpc.id }}" + state: present + register: igw + +- name: Create default subnet in zone A + ec2_vpc_subnet: + state: present + vpc_id: "{{ testing_vpc.vpc.id }}" + cidr: 10.22.32.0/24 + az: "{{ aws_region }}a" + resource_tags: + Name: "{{ resource_prefix }}-subnet-a" + register: testing_subnet_a + +- name: Create secondary subnet in zone B + ec2_vpc_subnet: + state: present + vpc_id: "{{ testing_vpc.vpc.id }}" + cidr: 10.22.33.0/24 + az: "{{ aws_region }}b" + resource_tags: + Name: "{{ resource_prefix }}-subnet-b" + register: testing_subnet_b + +- name: create routing rules + ec2_vpc_route_table: + vpc_id: "{{ testing_vpc.vpc.id }}" + tags: + created: "{{ resource_prefix }}-route" + routes: + - dest: 0.0.0.0/0 + gateway_id: "{{ igw.gateway_id }}" + subnets: + - "{{ testing_subnet_a.subnet.id }}" + - "{{ testing_subnet_b.subnet.id }}" + +- name: create a security group with the vpc + ec2_group: + name: "{{ resource_prefix }}-sg" + description: a security group for ansible tests + vpc_id: "{{ testing_vpc.vpc.id }}" + rules: + - proto: tcp + from_port: 22 + to_port: 22 + cidr_ip: 0.0.0.0/0 + - proto: tcp + from_port: 80 + to_port: 80 + cidr_ip: 0.0.0.0/0 + register: sg diff --git a/test/integration/targets/ec2_metric_alarm/tasks/main.yml b/test/integration/targets/ec2_metric_alarm/tasks/main.yml new file mode 100644 index 0000000000..f3f645cb2a --- /dev/null +++ b/test/integration/targets/ec2_metric_alarm/tasks/main.yml @@ -0,0 +1,228 @@ +- name: run ec2_metric_alarm tests + module_defaults: + group/aws: + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + security_token: "{{ security_token | default(omit) }}" + region: "{{ aws_region }}" + block: + - set_fact: + alarm_full_name: "{{ alarm_prefix }}-{{ resource_prefix }}-cpu-low" + + # until there's a module to get info about alarms, awscli is needed + - name: install awscli + pip: + state: present + name: awscli + + - name: set up environment for testing. + include_tasks: env_setup.yml + + - name: get info on alarms + command: aws cloudwatch describe-alarms --alarm-names {{ alarm_full_name }} + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + AWS_SESSION_TOKEN: "{{ security_token | default('') }}" + AWS_DEFAULT_REGION: "{{ aws_region }}" + register: alarm_info_query + + - name: Find AMI to use + ec2_ami_info: + owners: 'amazon' + filters: + name: '{{ ec2_ami_name }}' + register: ec2_amis + - set_fact: + ec2_ami_image: '{{ ec2_amis.images[0].image_id }}' + + - name: Make instance in a default subnet of the VPC + ec2_instance: + name: "{{ resource_prefix }}-test-default-vpc" + image_id: "{{ec2_ami_image }}" + tags: + TestId: "{{ resource_prefix }}" + security_groups: "{{ sg.group_id }}" + instance_type: t2.micro + wait: true + register: ec2_instance_results + + - name: create ec2 metric alarm on ec2 instance + 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: "<=" + threshold: 5.0 + period: 300 + evaluation_periods: 3 + unit: "Percent" + description: "This will alarm when an instance's cpu usage average is lower than 5% for 15 minutes " + register: ec2_instance_metric_alarm + + - name: get info on alarms + command: aws cloudwatch describe-alarms --alarm-names {{ alarm_full_name }} + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + AWS_SESSION_TOKEN: "{{ security_token | default('') }}" + AWS_DEFAULT_REGION: "{{ aws_region }}" + register: alarm_info_query + + - name: convert it to an object + set_fact: + alarm_info: "{{ alarm_info_query.stdout |from_json }}" + + - name: "verify that an alarm was created" + assert: + that: + - 'ec2_instance_metric_alarm.changed' + - 'ec2_instance_metric_alarm.alarm_arn' + - 'ec2_instance_metric_alarm.statistic == alarm_info["MetricAlarms"][0].Statistic' + - 'ec2_instance_metric_alarm.name == alarm_info["MetricAlarms"][0].AlarmName' + - 'ec2_instance_metric_alarm.metric== alarm_info["MetricAlarms"][0].MetricName' + - 'ec2_instance_metric_alarm.namespace == alarm_info["MetricAlarms"][0].Namespace' + - 'ec2_instance_metric_alarm.comparison == alarm_info["MetricAlarms"][0].ComparisonOperator' + - 'ec2_instance_metric_alarm.comparison == alarm_info["MetricAlarms"][0].ComparisonOperator' + - 'ec2_instance_metric_alarm.threshold == alarm_info["MetricAlarms"][0].Threshold' + - 'ec2_instance_metric_alarm.period == alarm_info["MetricAlarms"][0].Period' + - 'ec2_instance_metric_alarm.unit == alarm_info["MetricAlarms"][0].Unit' + - 'ec2_instance_metric_alarm.evaluation_periods == alarm_info["MetricAlarms"][0].EvaluationPeriods' + - 'ec2_instance_metric_alarm.description == alarm_info["MetricAlarms"][0].AlarmDescription' + - 'ec2_instance_metric_alarm.treat_missing_data == alarm_info["MetricAlarms"][0].TreatMissingData' + + - name: create ec2 metric alarm on ec2 instance (idempotent) + 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: "<=" + threshold: 5.0 + period: 300 + evaluation_periods: 3 + unit: "Percent" + description: "This will alarm when an instance's cpu usage average is lower than 5% for 15 minutes " + register: ec2_instance_metric_alarm_idempotent + + - name: get info on alarms + command: aws cloudwatch describe-alarms --alarm-names {{ alarm_full_name }} + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + AWS_SESSION_TOKEN: "{{ security_token | default('') }}" + AWS_DEFAULT_REGION: "{{ aws_region }}" + register: alarm_info_query_idempotent + + - name: convert it to an object + set_fact: + alarm_info_idempotent: "{{ alarm_info_query_idempotent.stdout |from_json }}" + + - name: "Verify alarm does not register as changed after update" + assert: + that: + - not ec2_instance_metric_alarm_idempotent.changed + + - name: "Verify alarm did not change after updating" + assert: + that: + - "alarm_info['MetricAlarms'][0]['{{item}}'] == alarm_info_idempotent['MetricAlarms'][0]['{{ item }}']" + with_items: + - AlarmArn + - Statistic + - AlarmName + - MetricName + - Namespace + - ComparisonOperator + - Threshold + - Period + - Unit + - EvaluationPeriods + - AlarmDescription + - TreatMissingData + + - name: update alarm + ec2_metric_alarm: + dimensions: + InstanceId: "{{ ec2_instance_results.instances[0].instance_id }}" + state: present + name: "{{ alarm_full_name }}" + metric: "CPUUtilization" + namespace: "AWS/EC2" + statistic: Average + comparison: "<=" + threshold: 5.0 + period: 60 + evaluation_periods: 3 + unit: "Percent" + description: "This will alarm when an instance's cpu usage average is lower than 5% for 3 minutes " + register: ec2_instance_metric_alarm_update + + - name: "verify that alarm registers as updated" + assert: + that: + - 'ec2_instance_metric_alarm.changed' + + - name: "verify that properties were changed" + assert: + that: + - 'ec2_instance_metric_alarm_update.changed' + - 'ec2_instance_metric_alarm_update.period == 60' #Period should be 60, not matching old value + - 'ec2_instance_metric_alarm_update.alarm_arn == ec2_instance_metric_alarm.alarm_arn' + - 'ec2_instance_metric_alarm_update.statistic == alarm_info["MetricAlarms"][0].Statistic' + - 'ec2_instance_metric_alarm_update.name == alarm_info["MetricAlarms"][0].AlarmName' + - 'ec2_instance_metric_alarm_update.metric== alarm_info["MetricAlarms"][0].MetricName' + - 'ec2_instance_metric_alarm_update.namespace == alarm_info["MetricAlarms"][0].Namespace' + - 'ec2_instance_metric_alarm_update.statistic == alarm_info["MetricAlarms"][0].Statistic' + - 'ec2_instance_metric_alarm_update.comparison == alarm_info["MetricAlarms"][0].ComparisonOperator' + - 'ec2_instance_metric_alarm_update.threshold == alarm_info["MetricAlarms"][0].Threshold' + - 'ec2_instance_metric_alarm_update.unit == alarm_info["MetricAlarms"][0].Unit' + - 'ec2_instance_metric_alarm_update.evaluation_periods == alarm_info["MetricAlarms"][0].EvaluationPeriods' + - 'ec2_instance_metric_alarm_update.treat_missing_data == alarm_info["MetricAlarms"][0].TreatMissingData' + + - 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 + command: aws cloudwatch describe-alarms --alarm-names {{ alarm_full_name }} + environment: + AWS_ACCESS_KEY_ID: "{{ aws_access_key }}" + AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}" + AWS_SESSION_TOKEN: "{{ security_token | default('') }}" + AWS_DEFAULT_REGION: "{{ aws_region }}" + register: alarm_info_query + + - name: convert it to an object + set_fact: + alarm_info: "{{ alarm_info_query.stdout |from_json }}" + + - name: Verify that the alarm was deleted using cli + assert: + that: + - 'alarm_info["MetricAlarms"]|length == 0' + always: + - name: try to stop the ec2 instance + ec2_instance: + instance_ids: "{{ ec2_instance_results.instances[0].instance_id }}" + state: terminated + ignore_errors: yes + + - include_tasks: env_cleanup.yml diff --git a/test/integration/targets/ec2_metric_alarm/vars/main.yml b/test/integration/targets/ec2_metric_alarm/vars/main.yml new file mode 100644 index 0000000000..ed97d539c0 --- /dev/null +++ b/test/integration/targets/ec2_metric_alarm/vars/main.yml @@ -0,0 +1 @@ +---