diff --git a/changelogs/fragments/1402-ec2_spot_instance-ability-to-terminate-instances.yml b/changelogs/fragments/1402-ec2_spot_instance-ability-to-terminate-instances.yml new file mode 100644 index 00000000000..b69c9e3c810 --- /dev/null +++ b/changelogs/fragments/1402-ec2_spot_instance-ability-to-terminate-instances.yml @@ -0,0 +1,2 @@ +minor_changes: +- ec2_spot_instance - add parameter ``terminate_instances`` to support terminate instances associated with spot requests. (https://github.com/ansible-collections/amazon.aws/pull/1402). diff --git a/plugins/modules/ec2_spot_instance.py b/plugins/modules/ec2_spot_instance.py index 99d70bb120f..a5d8f2ca8a9 100644 --- a/plugins/modules/ec2_spot_instance.py +++ b/plugins/modules/ec2_spot_instance.py @@ -282,6 +282,13 @@ - List of strings with IDs of spot requests to be cancelled type: list elements: str + terminate_instances: + description: + - Boolean value to set whether or not to terminate instances associated to spot request. + - Can be used only when I(state=absent). + default: False + type: bool + version_added: 5.4.0 extends_documentation_fragment: - amazon.aws.aws - amazon.aws.ec2 @@ -526,6 +533,11 @@ def cancel_spot_instance_requests(module, connection): msg='Would have cancelled Spot request {0}'.format(spot_instance_request_ids)) connection.cancel_spot_instance_requests(aws_retry=True, SpotInstanceRequestIds=module.params.get('spot_instance_request_ids')) + + if module.params.get("terminate_instances") is True: + associated_instances = [request["InstanceId"] for request in requests_exist["SpotInstanceRequests"]] + terminate_associated_instances(connection, module, associated_instances) + module.exit_json(changed=changed, msg='Cancelled Spot request {0}'.format(module.params.get('spot_instance_request_ids'))) else: module.exit_json(changed=changed, msg='Spot request not found or already cancelled') @@ -533,6 +545,13 @@ def cancel_spot_instance_requests(module, connection): module.fail_json_aws(e, msg='Error while cancelling the spot instance request') +def terminate_associated_instances(connection, module, instance_ids): + try: + connection.terminate_instances(aws_retry=True, InstanceIds=instance_ids) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json(e, msg="Unable to terminate instances") + + def main(): network_interface_options = dict( associate_public_ip_address=dict(type='bool'), @@ -604,16 +623,21 @@ def main(): tags=dict(type='dict'), # valid_from=dict(type='datetime', default=datetime.datetime.now()), # valid_until=dict(type='datetime', default=(datetime.datetime.now() + datetime.timedelta(minutes=60)) - spot_instance_request_ids=dict(type='list', elements='str'), + spot_instance_request_ids=dict(type="list", elements="str"), + terminate_instances=dict(type="bool", default="False"), ) + module = AnsibleAWSModule( argument_spec=argument_spec, supports_check_mode=True ) - connection = module.client('ec2', retry_decorator=AWSRetry.jittered_backoff()) + state = module.params["state"] - state = module.params['state'] + if module.params.get("terminate_instances") and state != "absent": + module.fail_json("terminate_instances can only be used when state is absent.") + + connection = module.client('ec2', retry_decorator=AWSRetry.jittered_backoff()) if state == 'present': request_spot_instances(module, connection) diff --git a/tests/integration/targets/ec2_spot_instance/tasks/main.yaml b/tests/integration/targets/ec2_spot_instance/tasks/main.yaml index f9fd9d63a88..1e98ad890fd 100644 --- a/tests/integration/targets/ec2_spot_instance/tasks/main.yaml +++ b/tests/integration/targets/ec2_spot_instance/tasks/main.yaml @@ -64,6 +64,9 @@ # ============================================================ + - name: Run tests for termianting associated instances + import_tasks: terminate_associated_instances.yml + # Assert that spot instance request is created - name: Create simple spot instance request ec2_spot_instance: @@ -253,7 +256,6 @@ - fake_cancel_result is not changed - '"Spot request not found or already cancelled" in fake_cancel_result.msg' - always: # ============================================================ diff --git a/tests/integration/targets/ec2_spot_instance/tasks/terminate_associated_instances.yml b/tests/integration/targets/ec2_spot_instance/tasks/terminate_associated_instances.yml new file mode 100644 index 00000000000..92864baafa4 --- /dev/null +++ b/tests/integration/targets/ec2_spot_instance/tasks/terminate_associated_instances.yml @@ -0,0 +1,109 @@ +--- +- block: + + # Spot instance request creation + - name: Simple Spot Request Creation + amazon.aws.ec2_spot_instance: + launch_specification: + image_id: "{{ ec2_ami_id }}" + key_name: "{{ resource_prefix }}-keypair" + instance_type: "t2.micro" + subnet_id: "{{ vpc_subnet_result.subnet.id }}" + tags: + ansible-test: "{{ resource_prefix }}" + register: create_result + + # Get instance ID of associated spot instance request + - name: Get info about the spot instance request created + amazon.aws.ec2_spot_instance_info: + spot_instance_request_ids: + - "{{ create_result.spot_request.spot_instance_request_id }}" + register: spot_instance_info_result + retries: 5 + until: spot_instance_info_result.spot_request[0].instance_id is defined + + - name: Pause to allow instance launch + pause: + seconds: 60 + + - name: Get instance ID of the instance associated with above spot instance request + set_fact: + instance_id_1: "{{ spot_instance_info_result.spot_request[0].instance_id }}" + + - name: Check state of instance - BEFORE request cancellation + amazon.aws.ec2_instance_info: + instance_ids: ["{{ instance_id_1 }}"] + register: instance_info_result + + # Cancel spot instance request + - name: Spot Request Termination + amazon.aws.ec2_spot_instance: + spot_instance_request_ids: + - '{{ create_result.spot_request.spot_instance_request_id }}' + state: absent + + # Verify that instance is not terminated and still running + - name: Check state of instance - AFTER request cancellation + amazon.aws.ec2_instance_info: + instance_ids: ["{{ instance_id_1 }}"] + register: instance_info_result + + - assert: + that: instance_info_result.instances[0].state.name == 'running' + +#========================================================================== + + # Spot instance request creation + - name: Simple Spot Request Creation + amazon.aws.ec2_spot_instance: + launch_specification: + image_id: "{{ ec2_ami_id }}" + key_name: "{{ resource_prefix }}-keypair" + instance_type: "t2.micro" + subnet_id: "{{ vpc_subnet_result.subnet.id }}" + tags: + ansible-test: "{{ resource_prefix }}" + register: create_result + + # Get instance ID of associated spot instance request + - name: Get info about the spot instance request created + amazon.aws.ec2_spot_instance_info: + spot_instance_request_ids: + - "{{ create_result.spot_request.spot_instance_request_id }}" + register: spot_instance_info_result + retries: 5 + until: spot_instance_info_result.spot_request[0].instance_id is defined + + - name: Pause to allow instance launch + pause: + seconds: 60 + + - name: Get instance ID of the instance associated with above spot instance request + set_fact: + instance_id_2: "{{ spot_instance_info_result.spot_request[0].instance_id }}" + + - name: Check state of instance - BEFORE request cancellation + amazon.aws.ec2_instance_info: + instance_ids: ["{{ instance_id_2 }}"] + register: instance_info_result + + # Cancel spot instance request + - name: Spot Request Termination + amazon.aws.ec2_spot_instance: + spot_instance_request_ids: + - '{{ create_result.spot_request.spot_instance_request_id }}' + state: absent + terminate_instances: true + + - name: wait for instance to terminate + pause: + seconds: 60 + + # Verify that instance is terminated or shutting-down + - name: Check state of instance - AFTER request cancellation + amazon.aws.ec2_instance_info: + instance_ids: ["{{ instance_id_2 }}"] + register: instance_info_result + + - assert: + that: instance_info_result.instances[0].state.name in ['terminated', 'shutting-down']