diff --git a/changelogs/fragments/487-ec2_spot_instance_info-add-new-module.yml b/changelogs/fragments/487-ec2_spot_instance_info-add-new-module.yml new file mode 100644 index 00000000000..6ba3d9767f5 --- /dev/null +++ b/changelogs/fragments/487-ec2_spot_instance_info-add-new-module.yml @@ -0,0 +1,2 @@ +minor_changes: +- ec2_spot_instance_info - Added a new module that describes the specified Spot Instance requests (https://github.com/ansible-collections/amazon.aws/pull/487). diff --git a/meta/runtime.yml b/meta/runtime.yml index 10412c79bff..894f5ca7ecd 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -38,6 +38,7 @@ action_groups: - ec2_snapshot - ec2_snapshot_info - ec2_spot_instance + - ec2_spot_instance_info - ec2_tag - ec2_tag_info - ec2_vol diff --git a/plugins/modules/ec2_spot_instance_info.py b/plugins/modules/ec2_spot_instance_info.py new file mode 100644 index 00000000000..c7c06b0a703 --- /dev/null +++ b/plugins/modules/ec2_spot_instance_info.py @@ -0,0 +1,168 @@ +#!/usr/bin/python +# This file is part of Ansible +# GNU General Public License v3.0+ (see COPYING or https://wwww.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: ec2_spot_instance_info +version_added: 2.0.0 +short_description: Gather information about ec2 spot instance requests +description: + - Describes the specified Spot Instance requests. +author: + - Mandar Vijay Kulkarni (@mandar242) +options: + filters: + description: + - A dict of filters to apply. Each dict item consists of a filter key and a filter value. + - Filter names and values are case sensitive. + - See U(https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeSpotInstanceRequests.html) for possible filters. + required: false + default: {} + type: dict + spot_instance_request_ids: + description: + - One or more Spot Instance request IDs. + required: false + type: list + elements: str + +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: describe the Spot Instance requests based on request IDs + amazon.aws.ec2_spot_instance_info: + spot_instance_request_ids: + - sir-12345678 + +- name: describe the Spot Instance requests and filter results based on instance type + amazon.aws.ec2_spot_instance_info: + spot_instance_request_ids: + - sir-12345678 + - sir-13579246 + - sir-87654321 + filters: + launch.instance-type: t3.medium + +- name: describe the Spot requests filtered using multiple filters + amazon.aws.ec2_spot_instance_info: + filters: + state: active + launch.block-device-mapping.device-name: /dev/sdb + +''' + +RETURN = ''' +spot_request: + description: The gathered information about specified spot instance requests. + returned: when success + type: dict + sample: { + "create_time": "2021-09-01T21:05:57+00:00", + "instance_id": "i-08877936b801ac475", + "instance_interruption_behavior": "terminate", + "launch_specification": { + "ebs_optimized": false, + "image_id": "ami-0443305dabd4be2bc", + "instance_type": "t2.medium", + "key_name": "zuul", + "monitoring": { + "enabled": false + }, + "placement": { + "availability_zone": "us-east-2b" + }, + "security_groups": [ + { + "group_id": "sg-01f9833207d53b937", + "group_name": "default" + } + ], + "subnet_id": "subnet-07d906b8358869bda" + }, + "launched_availability_zone": "us-east-2b", + "product_description": "Linux/UNIX", + "spot_instance_request_id": "sir-c3cp9jsk", + "spot_price": "0.046400", + "state": "active", + "status": { + "code": "fulfilled", + "message": "Your spot request is fulfilled.", + "update_time": "2021-09-01T21:05:59+00:00" + }, + "tags": {}, + "type": "one-time", + "valid_until": "2021-09-08T21:05:57+00:00" + } +''' + + +try: + import botocore +except ImportError: + pass # Handled 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.module_utils.common.dict_transformations import snake_dict_to_camel_dict +from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ansible_dict_to_boto3_filter_list + + +def _describe_spot_instance_requests(connection, **params): + paginator = connection.get_paginator('describe_spot_instance_requests') + return paginator.paginate(**params).build_full_result() + + +def describe_spot_instance_requests(connection, module): + + params = {} + + if module.params.get('filters'): + params['Filters'] = ansible_dict_to_boto3_filter_list(module.params.get('filters')) + if module.params.get('spot_instance_request_ids'): + params['SpotInstanceRequestIds'] = module.params.get('spot_instance_request_ids') + + try: + describe_spot_instance_requests_response = _describe_spot_instance_requests(connection, **params)['SpotInstanceRequests'] + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg='Failed to describe spot instance requests') + + spot_request = [] + for response_list_item in describe_spot_instance_requests_response: + spot_request.append(camel_dict_to_snake_dict(response_list_item)) + + if len(spot_request) == 0: + module.exit_json(msg='No spot requests found for specified options') + + module.exit_json(spot_request=spot_request) + + +def main(): + + argument_spec = dict( + filters=dict(default={}, type='dict'), + spot_instance_request_ids=dict(default=[], type='list', elements='str'), + ) + module = AnsibleAWSModule( + argument_spec=argument_spec, + supports_check_mode=True + ) + try: + connection = module.client('ec2', retry_decorator=AWSRetry.jittered_backoff()) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg='Failed to connect to AWS') + + describe_spot_instance_requests(connection, module) + + +if __name__ == '__main__': + main() diff --git a/tests/integration/targets/ec2_spot_instance/aliases b/tests/integration/targets/ec2_spot_instance/aliases index 4ef4b2067d0..f556641fbed 100644 --- a/tests/integration/targets/ec2_spot_instance/aliases +++ b/tests/integration/targets/ec2_spot_instance/aliases @@ -1 +1,2 @@ cloud/aws +ec2_spot_instance_info \ No newline at end of file diff --git a/tests/integration/targets/ec2_spot_instance/tasks/main.yaml b/tests/integration/targets/ec2_spot_instance/tasks/main.yaml index 6f1515f8aa8..680b3249964 100644 --- a/tests/integration/targets/ec2_spot_instance/tasks/main.yaml +++ b/tests/integration/targets/ec2_spot_instance/tasks/main.yaml @@ -80,6 +80,8 @@ key_name: "{{ resource_prefix }}-keypair" instance_type: "t2.medium" subnet_id: "{{ vpc_subnet_result.subnet.id }}" + tags: + ansible-test: "{{ resource_prefix }}" register: create_result - name: Assert that result has changed and request has been created @@ -90,6 +92,17 @@ - create_result.spot_request.spot_instance_request_id is defined - create_result.spot_request.launch_specification.subnet_id == vpc_subnet_result.subnet.id + - name: Get info about the spot instance request created + ec2_spot_instance_info: + spot_instance_request_ids: + - "{{ create_result.spot_request.spot_instance_request_id }}" + register: spot_instance_info_result + + - name: Assert that the spot request created is open or active + assert: + that: + - spot_instance_info_result.spot_request[0].state in ['open', 'active'] + - name: Create spot request with more complex options ec2_spot_instance: launch_specification: @@ -104,6 +117,7 @@ volume_size: 5 network_interfaces: - associate_public_ip_address: False + subnet_id: "{{ vpc_subnet_result.subnet.id }}" delete_on_termination: True device_index: 0 placement: @@ -117,6 +131,7 @@ snake_case: "hello_world" "Title Case": "Hello World" "lowercase spaced": "hello world" + ansible-test: "{{ resource_prefix }}" register: complex_create_result - assert: @@ -135,7 +150,7 @@ - launch_spec.network_interfaces.0.device_index == 0 - launch_spec.network_interfaces.0.associate_public_ip_address == false - launch_spec.network_interfaces.0.delete_on_termination == true - - spot_request_tags|length == 5 + - spot_request_tags|length == 6 - spot_request_tags['camelCase'] == 'helloWorld' - spot_request_tags['PascalCase'] == 'HelloWorld' - spot_request_tags['snake_case'] == 'hello_world' @@ -145,6 +160,39 @@ launch_spec: '{{ complex_create_result.spot_request.launch_specification }}' spot_request_tags: '{{ complex_create_result.spot_request.tags }}' + - name: Get info about the complex spot instance request created + ec2_spot_instance_info: + spot_instance_request_ids: + - "{{ complex_create_result.spot_request.spot_instance_request_id }}" + register: complex_info_result + + - name: Assert that the complex spot request created is open/active and correct keys are set + assert: + that: + - complex_info_result.spot_request[0].state in ['open', 'active'] + - complex_create_result.spot_request.spot_price == complex_info_result.spot_request[0].spot_price + - create_launch_spec.block_device_mappings[0].ebs.volume_size == info_launch_spec.block_device_mappings[0].ebs.volume_size + - create_launch_spec.block_device_mappings[0].ebs.volume_type == info_launch_spec.block_device_mappings[0].ebs.volume_type + - create_launch_spec.network_interfaces[0].delete_on_termination == info_launch_spec.network_interfaces[0].delete_on_termination + vars: + create_launch_spec: "{{ complex_create_result.spot_request.launch_specification }}" + info_launch_spec: "{{ complex_info_result.spot_request[0].launch_specification }}" + + - name: Get info about the created spot instance requests and filter result based on provided filters + ec2_spot_instance_info: + spot_instance_request_ids: + - '{{ create_result.spot_request.spot_instance_request_id }}' + - '{{ complex_create_result.spot_request.spot_instance_request_id }}' + filters: + tag:ansible-test: "{{ resource_prefix }}" + launch.block-device-mapping.device-name: /dev/sdb + register: spot_instance_info_filter_result + + - name: Assert that the correct spot request was returned in the filtered result + assert: + that: + - spot_instance_info_filter_result.spot_request[0].spot_instance_request_id == complex_create_result.spot_request.spot_instance_request_id + # Assert check mode - name: Create spot instance request (check_mode) ec2_spot_instance: @@ -153,6 +201,8 @@ key_name: "{{ resource_prefix }}-keypair" instance_type: "t2.medium" subnet_id: "{{ vpc_subnet_result.subnet.id }}" + tags: + ansible-test: "{{ resource_prefix }}" check_mode: True register: check_create_result @@ -221,14 +271,20 @@ filters: vpc-id: "{{ vpc_result.vpc.id }}" + - name: get all spot requests created during test + ec2_spot_instance_info: + filters: + tag:ansible-test: "{{ resource_prefix }}" + register: spot_request_list + - name: remove spot instance requests ec2_spot_instance: spot_instance_request_ids: - - '{{ create_result.spot_request.spot_instance_request_id }}' - - '{{ complex_create_result.spot_request.spot_instance_request_id }}' + - '{{ item.spot_instance_request_id }}' state: 'absent' ignore_errors: true retries: 5 + with_items: "{{ spot_request_list.spot_request }}" - name: remove the security group ec2_group: