From 0d7c068d97f8fa36671c5d9d70f6f0ef063f581b Mon Sep 17 00:00:00 2001 From: Mandar Kulkarni Date: Thu, 24 Mar 2022 09:54:00 -0700 Subject: [PATCH] ec2_asg_instance_refresh and ec2_asg_instance_refresh_info modules (#973) ec2_asg_instance_refresh and ec2_asg_instance_refresh_info modules SUMMARY Reviving original PR that adds Autoscaling instance refresh API support as the author has yet not updated PR based on review feedback. Issue: #135 PR being revived: #795 Fixes #135 ISSUE TYPE New Module Pull Request COMPONENT NAME ec2_asg_instance_refresh ec2_asg_instance_refreshes_info ADDITIONAL INFORMATION More about the feature: https://aws.amazon.com/blogs/compute/introducing-instance-refresh-for-ec2-auto-scaling/ Boto3 documentation: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/autoscaling.html#AutoScaling.Client.start_instance_refresh Reviewed-by: Alina Buzachis Reviewed-by: Mandar Kulkarni Reviewed-by: Mark Woolley Reviewed-by: Jill R Reviewed-by: Joseph Torcasso (cherry picked from commit 301501391aa56d8b36d427bc8f8f7e9a6f513eef) --- meta/runtime.yml | 2 + plugins/modules/ec2_asg_instance_refresh.py | 267 +++++++++ .../modules/ec2_asg_instance_refresh_info.py | 219 +++++++ .../targets/ec2_asg_instance_refresh/aliases | 2 + .../defaults/main.yml | 5 + .../ec2_asg_instance_refresh/tasks/main.yml | 533 ++++++++++++++++++ .../tasks/refresh_and_cancel_three_times.yml | 29 + .../ec2_asg_instance_refresh/vars/main.yml | 0 8 files changed, 1057 insertions(+) create mode 100644 plugins/modules/ec2_asg_instance_refresh.py create mode 100644 plugins/modules/ec2_asg_instance_refresh_info.py create mode 100644 tests/integration/targets/ec2_asg_instance_refresh/aliases create mode 100644 tests/integration/targets/ec2_asg_instance_refresh/defaults/main.yml create mode 100644 tests/integration/targets/ec2_asg_instance_refresh/tasks/main.yml create mode 100644 tests/integration/targets/ec2_asg_instance_refresh/tasks/refresh_and_cancel_three_times.yml create mode 100644 tests/integration/targets/ec2_asg_instance_refresh/vars/main.yml diff --git a/meta/runtime.yml b/meta/runtime.yml index ab5bb4e0bb8..2d5d3cd4dfe 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -66,6 +66,8 @@ action_groups: - ec2_asg - ec2_asg_info - ec2_asg_scheduled_action + - ec2_asg_instance_refresh + - ec2_asg_instance_refresh_info - ec2_asg_lifecycle_hook - ec2_customer_gateway - ec2_customer_gateway_info diff --git a/plugins/modules/ec2_asg_instance_refresh.py b/plugins/modules/ec2_asg_instance_refresh.py new file mode 100644 index 00000000000..faa61fa74cb --- /dev/null +++ b/plugins/modules/ec2_asg_instance_refresh.py @@ -0,0 +1,267 @@ +#!/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 + + +DOCUMENTATION = ''' +--- +module: ec2_asg_instance_refresh +version_added: 3.2.0 +short_description: Start or cancel an EC2 Auto Scaling Group (ASG) instance refresh in AWS +description: + - Start or cancel an EC2 Auto Scaling Group instance refresh in AWS. + - Can be used with M(community.aws.ec2_asg_instance_refresh_info) to track the subsequent progress. +author: "Dan Khersonsky (@danquixote)" +options: + state: + description: + - Desired state of the ASG. + type: str + required: true + choices: [ 'started', 'cancelled' ] + name: + description: + - The name of the auto scaling group you are searching for. + type: str + required: true + strategy: + description: + - The strategy to use for the instance refresh. The only valid value is C(Rolling). + - A rolling update is an update that is applied to all instances in an Auto Scaling group until all instances have been updated. + - A rolling update can fail due to failed health checks or if instances are on standby or are protected from scale in. + - If the rolling update process fails, any instances that were already replaced are not rolled back to their previous configuration. + type: str + default: 'Rolling' + preferences: + description: + - Set of preferences associated with the instance refresh request. + - If not provided, the default values are used. + - For I(min_healthy_percentage), the default value is C(90). + - For I(instance_warmup), the default is to use the value specified for the health check grace period for the Auto Scaling group. + - Can not be specified when I(state) is set to 'cancelled'. + required: false + suboptions: + min_healthy_percentage: + description: + - Total percent of capacity in ASG that must remain healthy during instance refresh to allow operation to continue. + - It is rounded up to the nearest integer. + type: int + default: 90 + instance_warmup: + description: + - The number of seconds until a newly launched instance is configured and ready to use. + - During this time, Amazon EC2 Auto Scaling does not immediately move on to the next replacement. + - The default is to use the value for the health check grace period defined for the group. + type: int + type: dict +extends_documentation_fragment: +- amazon.aws.aws +- amazon.aws.ec2 + +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +- name: Start a refresh + community.aws.ec2_asg_instance_refresh: + name: some-asg + state: started + +- name: Cancel a refresh + community.aws.ec2_asg_instance_refresh: + name: some-asg + state: cancelled + +- name: Start a refresh and pass preferences + community.aws.ec2_asg_instance_refresh: + name: some-asg + state: started + preferences: + min_healthy_percentage: 91 + instance_warmup: 60 + +''' + +RETURN = ''' +--- +instance_refresh_id: + description: instance refresh id + returned: success + type: str + sample: "08b91cf7-8fa6-48af-b6a6-d227f40f1b9b" +auto_scaling_group_name: + description: Name of autoscaling group + returned: success + type: str + sample: "public-webapp-production-1" +status: + description: + - The current state of the group when DeleteAutoScalingGroup is in progress. + - The following are the possible statuses + - Pending -- The request was created, but the operation has not started. + - InProgress -- The operation is in progress. + - Successful -- The operation completed successfully. + - Failed -- The operation failed to complete. You can troubleshoot using the status reason and the scaling activities. + - Cancelling -- + - An ongoing operation is being cancelled. + - Cancellation does not roll back any replacements that have already been completed, + - but it prevents new replacements from being started. + - Cancelled -- The operation is cancelled. + returned: success + type: str + sample: "Pending" +start_time: + description: The date and time this ASG was created, in ISO 8601 format. + returned: success + type: str + sample: "2015-11-25T00:05:36.309Z" +end_time: + description: The date and time this ASG was created, in ISO 8601 format. + returned: success + type: str + sample: "2015-11-25T00:05:36.309Z" +percentage_complete: + description: the % of completeness + returned: success + type: int + sample: 100 +instances_to_update: + description: num. of instance to update + returned: success + type: int + sample: 5 +''' + +try: + from botocore.exceptions import BotoCoreError, ClientError +except ImportError: + pass # caught by AnsibleAWSModule + + +from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import camel_dict_to_snake_dict +from ansible_collections.amazon.aws.plugins.module_utils.core import scrub_none_parameters +from ansible.module_utils.common.dict_transformations import snake_dict_to_camel_dict + + +def start_or_cancel_instance_refresh(conn, module): + """ + Args: + conn (boto3.AutoScaling.Client): Valid Boto3 ASG client. + module: AnsibleAWSModule object + + Returns: + { + "instance_refreshes": [ + { + 'auto_scaling_group_name': 'ansible-test-hermes-63642726-asg', + 'instance_refresh_id': '6507a3e5-4950-4503-8978-e9f2636efc09', + 'instances_to_update': 1, + 'percentage_complete': 0, + "preferences": { + "instance_warmup": 60, + "min_healthy_percentage": 90, + "skip_matching": false + }, + 'start_time': '2021-02-04T03:39:40+00:00', + 'status': 'Cancelling', + 'status_reason': 'Replacing instances before cancelling.', + } + ] + } + """ + + asg_state = module.params.get('state') + asg_name = module.params.get('name') + preferences = module.params.get('preferences') + + args = {} + args['AutoScalingGroupName'] = asg_name + if asg_state == 'started': + args['Strategy'] = module.params.get('strategy') + if preferences: + if asg_state == 'cancelled': + module.fail_json(msg='can not pass preferences dict when canceling a refresh') + _prefs = scrub_none_parameters(preferences) + args['Preferences'] = snake_dict_to_camel_dict(_prefs, capitalize_first=True) + cmd_invocations = { + 'cancelled': conn.cancel_instance_refresh, + 'started': conn.start_instance_refresh, + } + try: + if module.check_mode: + if asg_state == 'started': + ongoing_refresh = conn.describe_instance_refreshes(AutoScalingGroupName=asg_name).get('InstanceRefreshes', '[]') + if ongoing_refresh: + module.exit_json(changed=False, msg='In check_mode - Instance Refresh is already in progress, can not start new instance refresh.') + else: + module.exit_json(changed=True, msg='Would have started instance refresh if not in check mode.') + elif asg_state == 'cancelled': + ongoing_refresh = conn.describe_instance_refreshes(AutoScalingGroupName=asg_name).get('InstanceRefreshes', '[]')[0] + if ongoing_refresh.get('Status', '') in ['Cancelling', 'Cancelled']: + module.exit_json(changed=False, msg='In check_mode - Instance Refresh already cancelled or is pending cancellation.') + elif not ongoing_refresh: + module.exit_json(chaned=False, msg='In check_mode - No active referesh found, nothing to cancel.') + else: + module.exit_json(changed=True, msg='Would have cancelled instance refresh if not in check mode.') + result = cmd_invocations[asg_state](aws_retry=True, **args) + instance_refreshes = conn.describe_instance_refreshes(AutoScalingGroupName=asg_name, InstanceRefreshIds=[result['InstanceRefreshId']]) + result = dict( + instance_refreshes=camel_dict_to_snake_dict(instance_refreshes['InstanceRefreshes'][0]) + ) + return module.exit_json(**result) + except (BotoCoreError, ClientError) as e: + module.fail_json_aws( + e, + msg='Failed to {0} InstanceRefresh'.format( + asg_state.replace('ed', '') + ) + ) + + +def main(): + + argument_spec = dict( + state=dict( + type='str', + required=True, + choices=['started', 'cancelled'], + ), + name=dict(required=True), + strategy=dict( + type='str', + default='Rolling', + required=False + ), + preferences=dict( + type='dict', + required=False, + options=dict( + min_healthy_percentage=dict(type='int', default=90), + instance_warmup=dict(type='int'), + ) + ), + ) + + module = AnsibleAWSModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + autoscaling = module.client( + 'autoscaling', + retry_decorator=AWSRetry.jittered_backoff( + retries=10, + catch_extra_error_codes=['InstanceRefreshInProgress'] + ) + ) + + start_or_cancel_instance_refresh(autoscaling, module) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ec2_asg_instance_refresh_info.py b/plugins/modules/ec2_asg_instance_refresh_info.py new file mode 100644 index 00000000000..d4a12380098 --- /dev/null +++ b/plugins/modules/ec2_asg_instance_refresh_info.py @@ -0,0 +1,219 @@ +#!/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 + + +DOCUMENTATION = ''' +--- +module: ec2_asg_instance_refresh_info +version_added: 3.2.0 +short_description: Gather information about ec2 Auto Scaling Group (ASG) Instance Refreshes in AWS +description: + - Describes one or more instance refreshes. + - You can determine the status of a request by looking at the I(status) parameter. +author: "Dan Khersonsky (@danquixote)" +options: + name: + description: + - The name of the Auto Scaling group. + type: str + required: true + ids: + description: + - One or more instance refresh IDs. + type: list + elements: str + default: [] + next_token: + description: + - The token for the next set of items to return. (You received this token from a previous call.) + type: str + max_records: + description: + - The maximum number of items to return with this call. The default value is 50 and the maximum value is 100. + type: int + required: false +extends_documentation_fragment: +- amazon.aws.aws +- amazon.aws.ec2 + +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +- name: Find an refresh by ASG name + community.aws.ec2_asg_instance_refresh_info: + name: somename-asg + +- name: Find an refresh by ASG name and one or more refresh-IDs + community.aws.ec2_asg_instance_refresh_info: + name: somename-asg + ids: ['some-id-123'] + register: asgs + +- name: Find an refresh by ASG name and set max_records + community.aws.ec2_asg_instance_refresh_info: + name: somename-asg + max_records: 4 + register: asgs + +- name: Find an refresh by ASG name and NextToken, if received from a previous call + community.aws.ec2_asg_instance_refresh_info: + name: somename-asg + next_token: 'some-token-123' + register: asgs +''' + +RETURN = ''' +--- +instance_refresh_id: + description: instance refresh id + returned: success + type: str + sample: "08b91cf7-8fa6-48af-b6a6-d227f40f1b9b" +auto_scaling_group_name: + description: Name of autoscaling group + returned: success + type: str + sample: "public-webapp-production-1" +status: + description: + - The current state of the group when DeleteAutoScalingGroup is in progress. + - The following are the possible statuses + - Pending -- The request was created, but the operation has not started. + - InProgress -- The operation is in progress. + - Successful -- The operation completed successfully. + - Failed -- The operation failed to complete. You can troubleshoot using the status reason and the scaling activities. + - Cancelling -- + - An ongoing operation is being cancelled. + - Cancellation does not roll back any replacements that have already been completed, + - but it prevents new replacements from being started. + - Cancelled -- The operation is cancelled. + returned: success + type: str + sample: "Pending" +start_time: + description: The date and time this ASG was created, in ISO 8601 format. + returned: success + type: str + sample: "2015-11-25T00:05:36.309Z" +end_time: + description: The date and time this ASG was created, in ISO 8601 format. + returned: success + type: str + sample: "2015-11-25T00:05:36.309Z" +percentage_complete: + description: the % of completeness + returned: success + type: int + sample: 100 +instances_to_update: + description: num. of instance to update + returned: success + type: int + sample: 5 +''' + +try: + from botocore.exceptions import BotoCoreError, ClientError +except ImportError: + pass # caught by AnsibleAWSModule + +from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import camel_dict_to_snake_dict + + +def find_asg_instance_refreshes(conn, module): + """ + Args: + conn (boto3.AutoScaling.Client): Valid Boto3 ASG client. + module: AnsibleAWSModule object + + Returns: + { + "instance_refreshes": [ + { + 'auto_scaling_group_name': 'ansible-test-hermes-63642726-asg', + 'instance_refresh_id': '6507a3e5-4950-4503-8978-e9f2636efc09', + 'instances_to_update': 1, + 'percentage_complete': 0, + "preferences": { + "instance_warmup": 60, + "min_healthy_percentage": 90, + "skip_matching": false + }, + 'start_time': '2021-02-04T03:39:40+00:00', + 'status': 'Cancelled', + 'status_reason': 'Cancelled due to user request.', + } + ], + 'next_token': 'string' + } + """ + + asg_name = module.params.get('name') + asg_ids = module.params.get('ids') + asg_next_token = module.params.get('next_token') + asg_max_records = module.params.get('max_records') + + args = {} + args['AutoScalingGroupName'] = asg_name + if asg_ids: + args['InstanceRefreshIds'] = asg_ids + if asg_next_token: + args['NextToken'] = asg_next_token + if asg_max_records: + args['MaxRecords'] = asg_max_records + + try: + instance_refreshes_result = {} + response = conn.describe_instance_refreshes(**args) + if 'InstanceRefreshes' in response: + instance_refreshes_dict = dict( + instance_refreshes=response['InstanceRefreshes'], next_token=response.get('next_token', '')) + instance_refreshes_result = camel_dict_to_snake_dict( + instance_refreshes_dict) + + while 'NextToken' in response: + args['NextToken'] = response['NextToken'] + response = conn.describe_instance_refreshes(**args) + if 'InstanceRefreshes' in response: + instance_refreshes_dict = camel_dict_to_snake_dict(dict( + instance_refreshes=response['InstanceRefreshes'], next_token=response.get('next_token', ''))) + instance_refreshes_result.update(instance_refreshes_dict) + + return module.exit_json(**instance_refreshes_result) + except (BotoCoreError, ClientError) as e: + module.fail_json_aws(e, msg='Failed to describe InstanceRefreshes') + + +def main(): + + argument_spec = dict( + name=dict(required=True, type='str'), + ids=dict(required=False, default=[], elements='str', type='list'), + next_token=dict(required=False, default=None, type='str', no_log=True), + max_records=dict(required=False, type='int'), + ) + + module = AnsibleAWSModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + autoscaling = module.client( + 'autoscaling', + retry_decorator=AWSRetry.jittered_backoff(retries=10) + ) + find_asg_instance_refreshes(autoscaling, module) + + +if __name__ == '__main__': + main() diff --git a/tests/integration/targets/ec2_asg_instance_refresh/aliases b/tests/integration/targets/ec2_asg_instance_refresh/aliases new file mode 100644 index 00000000000..29bc7f5288f --- /dev/null +++ b/tests/integration/targets/ec2_asg_instance_refresh/aliases @@ -0,0 +1,2 @@ +cloud/aws +ec2_asg_instance_refresh_info diff --git a/tests/integration/targets/ec2_asg_instance_refresh/defaults/main.yml b/tests/integration/targets/ec2_asg_instance_refresh/defaults/main.yml new file mode 100644 index 00000000000..529b8133b96 --- /dev/null +++ b/tests/integration/targets/ec2_asg_instance_refresh/defaults/main.yml @@ -0,0 +1,5 @@ +--- +# defaults file for ec2_asg +vpc_seed: '{{ tiny_prefix }}' +ec2_ami_name: 'amzn2-ami-hvm-2.*-x86_64-gp2' +subnet_a_cidr: '10.{{ 256 | random(seed=vpc_seed) }}.32.0/24' diff --git a/tests/integration/targets/ec2_asg_instance_refresh/tasks/main.yml b/tests/integration/targets/ec2_asg_instance_refresh/tasks/main.yml new file mode 100644 index 00000000000..a6be21b42de --- /dev/null +++ b/tests/integration/targets/ec2_asg_instance_refresh/tasks/main.yml @@ -0,0 +1,533 @@ +--- +- name: setup credentials and region + 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 }}" + + collections: + - amazon.aws + + block: + + #NOTE: entire ASG setup is 'borrowed' from ec2_asg + - 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: load balancer name has to be less than 32 characters + set_fact: + load_balancer_name: "{{ item }}-lb" + loop: "{{ resource_prefix | regex_findall('.{8}$') }}" + + # Set up the testing dependencies: VPC, subnet, security group, and two launch configurations + - name: Create VPC for use in testing + ec2_vpc_net: + name: "{{ resource_prefix }}-vpc" + cidr_block: '{{ subnet_a_cidr }}' + 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 subnet for use in testing + ec2_vpc_subnet: + state: present + vpc_id: "{{ testing_vpc.vpc.id }}" + cidr: '{{ subnet_a_cidr }}' + az: "{{ aws_region }}a" + resource_tags: + Name: "{{ resource_prefix }}-subnet" + register: testing_subnet + + - 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.subnet.id }}" + + - name: create a security group with the vpc created in the ec2_setup + 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 + + - name: ensure launch configs exist + ec2_lc: + name: "{{ item }}" + assign_public_ip: true + image_id: "{{ ec2_ami_image }}" + user_data: | + package_upgrade: true + package_update: true + packages: + - httpd + runcmd: + - "service httpd start" + security_groups: "{{ sg.group_id }}" + instance_type: t3.micro + loop: + - "{{ resource_prefix }}-lc" + - "{{ resource_prefix }}-lc-2" + + - name: launch asg and do not wait for instances to be deemed healthy (no ELB) + ec2_asg: + name: "{{ resource_prefix }}-asg" + launch_config_name: "{{ resource_prefix }}-lc" + desired_capacity: 1 + min_size: 1 + max_size: 1 + vpc_zone_identifier: "{{ testing_subnet.subnet.id }}" + wait_for_instances: no + state: present + register: output + + - assert: + that: + - "output.viable_instances == 0" + + # ============================================================ + + - name: test invalid cancelation - V1 - (pre-refresh) + ec2_asg_instance_refresh: + name: "{{ resource_prefix }}-asg" + state: "cancelled" + ignore_errors: yes + register: result + + - assert: + that: + - "'An error occurred (ActiveInstanceRefreshNotFound) when calling the CancelInstanceRefresh operation: No in progress or pending Instance Refresh found for Auto Scaling group {{ resource_prefix }}-asg' in result.msg" + + - name: test starting a refresh with a valid ASG name - check_mode + ec2_asg_instance_refresh: + name: "{{ resource_prefix }}-asg" + state: "started" + check_mode: true + register: output + + - assert: + that: + - output is not failed + - output is changed + - '"autoscaling:StartInstanceRefresh" not in output.resource_actions' + + - name: test starting a refresh with a valid ASG name + ec2_asg_instance_refresh: + name: "{{ resource_prefix }}-asg" + state: "started" + register: output + + - assert: + that: + - "'instance_refresh_id' in output.instance_refreshes" + + - name: test starting a refresh with a valid ASG name - Idempotent + ec2_asg_instance_refresh: + name: "{{ resource_prefix }}-asg" + state: "started" + ignore_errors: true + register: output + + - assert: + that: + - output is not changed + - '"Failed to start InstanceRefresh: An error occurred (InstanceRefreshInProgress) when calling the StartInstanceRefresh operation: An Instance Refresh is already in progress and blocks the execution of this Instance Refresh." in output.msg' + + - name: test starting a refresh with a valid ASG name - Idempotent (check_mode) + ec2_asg_instance_refresh: + name: "{{ resource_prefix }}-asg" + state: "started" + ignore_errors: true + check_mode: true + register: output + + - assert: + that: + - output is not changed + - output is not failed + - '"In check_mode - Instance Refresh is already in progress, can not start new instance refresh." in output.msg' + + - name: test starting a refresh with a nonexistent ASG name + ec2_asg_instance_refresh: + name: "nonexistentname-asg" + state: "started" + ignore_errors: yes + register: result + + - assert: + that: + - "'Failed to start InstanceRefresh: An error occurred (ValidationError) when calling the StartInstanceRefresh operation: AutoScalingGroup name not found' in result.msg" + + - name: test canceling a refresh with an ASG name - check_mode + ec2_asg_instance_refresh: + name: "{{ resource_prefix }}-asg" + state: "cancelled" + check_mode: true + register: output + + - assert: + that: + - output is not failed + - output is changed + - '"autoscaling:CancelInstanceRefresh" not in output.resource_actions' + + - name: test canceling a refresh with an ASG name + ec2_asg_instance_refresh: + name: "{{ resource_prefix }}-asg" + state: "cancelled" + register: output + + - assert: + that: + - "'instance_refresh_id' in output.instance_refreshes" + + - name: test canceling a refresh with a ASG name - Idempotent + ec2_asg_instance_refresh: + name: "{{ resource_prefix }}-asg" + state: "cancelled" + ignore_errors: yes + register: output + + - assert: + that: + - output is not changed + + - name: test cancelling a refresh with a valid ASG name - Idempotent (check_mode) + ec2_asg_instance_refresh: + name: "{{ resource_prefix }}-asg" + state: "cancelled" + ignore_errors: true + check_mode: true + register: output + + - assert: + that: + - output is not changed + - output is not failed + + - name: test starting a refresh with an ASG name and preferences dict + ec2_asg_instance_refresh: + name: "{{ resource_prefix }}-asg" + state: "started" + preferences: + min_healthy_percentage: 10 + instance_warmup: 10 + retries: 5 + register: output + until: output is not failed + + - assert: + that: + - "'instance_refresh_id' in output.instance_refreshes" + + - name: re-test canceling a refresh with an ASG name + ec2_asg_instance_refresh: + name: "{{ resource_prefix }}-asg" + state: "cancelled" + register: output + + - assert: + that: + - "'instance_refresh_id' in output.instance_refreshes" + + - name: test valid start - V1 - (with preferences missing instance_warmup) + ec2_asg_instance_refresh: + name: "{{ resource_prefix }}-asg" + state: "started" + preferences: + min_healthy_percentage: 10 + ignore_errors: yes + retries: 5 + register: output + until: output is not failed + + - assert: + that: + - "'instance_refresh_id' in output.instance_refreshes" + + - name: re-test canceling a refresh with an ASG name + ec2_asg_instance_refresh: + name: "{{ resource_prefix }}-asg" + state: "cancelled" + register: output + + - assert: + that: + - "'instance_refresh_id' in output.instance_refreshes" + + - name: test valid start - V2 - (with preferences missing min_healthy_percentage) + ec2_asg_instance_refresh: + name: "{{ resource_prefix }}-asg" + state: "started" + preferences: + instance_warmup: 10 + retries: 5 + register: output + until: output is not failed + ignore_errors: yes + + - assert: + that: + - "'instance_refresh_id' in output.instance_refreshes" + + - name: test invalid cancelation - V2 - (with preferences) + ec2_asg_instance_refresh: + name: "{{ resource_prefix }}-asg" + state: "cancelled" + preferences: + min_healthy_percentage: 10 + instance_warmup: 10 + ignore_errors: yes + register: result + + - assert: + that: + - "'can not pass preferences dict when canceling a refresh' in result.msg" + + # ============================================================ + + - name: run setup with refresh_and_cancel_three_times.yml + include_tasks: refresh_and_cancel_three_times.yml + loop: "{{ query('sequence', 'start=1 end=3') }}" + + - name: test getting info for an ASG name + ec2_asg_instance_refresh_info: + name: "{{ resource_prefix }}-asg" + region: "{{ aws_region }}" + ignore_errors: yes + register: output + + - assert: + that: + - output | community.general.json_query(inst_refresh_id_json_query) | unique | length == 7 + vars: + inst_refresh_id_json_query: instance_refreshes[].instance_refresh_id + + - name: test using fake refresh ID + ec2_asg_instance_refresh_info: + name: "{{ resource_prefix }}-asg" + ids: ['0e367f58-blabla-bla-bla-ca870dc5dbfe'] + ignore_errors: yes + register: output + + - assert: + that: + - "{{ output.instance_refreshes|length }} == 0" + + - name: test using a real refresh ID + ec2_asg_instance_refresh_info: + name: "{{ resource_prefix }}-asg" + ids: [ '{{ refreshout.instance_refreshes.instance_refresh_id }}' ] + ignore_errors: yes + register: output + + - assert: + that: + - "{{ output.instance_refreshes |length }} == 1" + + - name: test getting info for an ASG name which doesn't exist + ec2_asg_instance_refresh_info: + name: n0n3x1stentname27b + ignore_errors: yes + register: output + + - assert: + that: + - "'Failed to describe InstanceRefreshes: An error occurred (ValidationError) when calling the DescribeInstanceRefreshes operation: AutoScalingGroup name not found - AutoScalingGroup n0n3x1stentname27b not found' == output.msg" + + - name: assert that the correct number of records are returned + ec2_asg_instance_refresh_info: + name: "{{ resource_prefix }}-asg" + ignore_errors: yes + register: output + + - assert: + that: + - "{{ output.instance_refreshes|length }} == 7" + + - name: assert that valid message with fake-token is returned + ec2_asg_instance_refresh_info: + name: "{{ resource_prefix }}-asg" + next_token: "fake-token-123" + ignore_errors: yes + register: output + + - assert: + that: + - '"Failed to describe InstanceRefreshes: An error occurred (InvalidNextToken) when calling the DescribeInstanceRefreshes operation: The token ''********'' is invalid." == output.msg' + + - name: assert that max records=1 returns no more than one record + ec2_asg_instance_refresh_info: + name: "{{ resource_prefix }}-asg" + max_records: 1 + ignore_errors: yes + register: output + + - assert: + that: + - "{{ output.instance_refreshes|length }} < 2" + + - name: assert that valid message with real-token is returned + ec2_asg_instance_refresh_info: + name: "{{ resource_prefix }}-asg" + next_token: "{{ output.next_token }}" + ignore_errors: yes + register: output + + - assert: + that: + - "{{ output.instance_refreshes|length }} == 7" + + - name: test using both real nextToken and max_records=1 + ec2_asg_instance_refresh_info: + name: "{{ resource_prefix }}-asg" + max_records: 1 + next_token: "{{ output.next_token }}" + ignore_errors: yes + register: output + + - assert: + that: + - "{{ output.instance_refreshes|length }} == 1" + + always: + + - name: kill asg + ec2_asg: + name: "{{ resource_prefix }}-asg" + state: absent + register: removed + until: removed is not failed + ignore_errors: yes + retries: 10 + # Remove the testing dependencies + + - name: remove the load balancer + ec2_elb_lb: + name: "{{ load_balancer_name }}" + state: absent + security_group_ids: + - "{{ sg.group_id }}" + subnets: "{{ testing_subnet.subnet.id }}" + wait: yes + connection_draining_timeout: 60 + listeners: + - protocol: http + load_balancer_port: 80 + instance_port: 80 + health_check: + ping_protocol: tcp + ping_port: 80 + ping_path: "/" + response_timeout: 5 + interval: 10 + unhealthy_threshold: 4 + healthy_threshold: 2 + register: removed + until: removed is not failed + ignore_errors: yes + retries: 10 + + - name: remove launch configs + ec2_lc: + name: "{{ resource_prefix }}-lc" + state: absent + register: removed + until: removed is not failed + ignore_errors: yes + retries: 10 + loop: + - "{{ resource_prefix }}-lc" + - "{{ resource_prefix }}-lc-2" + + - name: delete launch template + ec2_launch_template: + name: "{{ resource_prefix }}-lt" + state: absent + register: del_lt + retries: 10 + until: del_lt is not failed + ignore_errors: true + + - 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.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 the subnet + ec2_vpc_subnet: + state: absent + vpc_id: "{{ testing_vpc.vpc.id }}" + cidr: '{{ subnet_a_cidr }}' + 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: '{{ subnet_a_cidr }}' + state: absent + register: removed + until: removed is not failed + ignore_errors: yes + retries: 10 diff --git a/tests/integration/targets/ec2_asg_instance_refresh/tasks/refresh_and_cancel_three_times.yml b/tests/integration/targets/ec2_asg_instance_refresh/tasks/refresh_and_cancel_three_times.yml new file mode 100644 index 00000000000..2d84828de34 --- /dev/null +++ b/tests/integration/targets/ec2_asg_instance_refresh/tasks/refresh_and_cancel_three_times.yml @@ -0,0 +1,29 @@ +--- + +- name: try to cancel pre-loop + ec2_asg_instance_refresh: + name: "{{ resource_prefix }}-asg" + state: "cancelled" + ignore_errors: yes + +- name: test starting a refresh with an ASG name + ec2_asg_instance_refresh: + name: "{{ resource_prefix }}-asg" + state: "started" + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + region: "{{ aws_region }}" + ignore_errors: no + retries: 10 + delay: 5 + register: refreshout + until: refreshout is not failed + +- name: test cancelling a refresh with an ASG name + ec2_asg_instance_refresh: + name: "{{ resource_prefix }}-asg" + state: "cancelled" + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + region: "{{ aws_region }}" + ignore_errors: yes diff --git a/tests/integration/targets/ec2_asg_instance_refresh/vars/main.yml b/tests/integration/targets/ec2_asg_instance_refresh/vars/main.yml new file mode 100644 index 00000000000..e69de29bb2d