diff --git a/README.md b/README.md index 4b281c37f66..676d987142b 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,11 @@ PEP440 is the schema used to describe the versions of Ansible. ## Python version compatibility -This collection depends on the AWS SDK for Python (Boto3 and Botocore). As AWS has [ceased supporting Python 2.6](https://aws.amazon.com/blogs/developer/deprecation-of-python-2-6-and-python-3-3-in-botocore-boto3-and-the-aws-cli/), this collection requires Python 2.7 or greater. +As the AWS SDK for Python (Boto3 and Botocore) has [ceased supporting Python 2.7](https://aws.amazon.com/blogs/developer/announcing-end-of-support-for-python-2-7-in-aws-sdk-for-python-and-aws-cli-v1/), this collection requires Python 3.6 or greater. + +Starting with the 2.0.0 releases of amazon.aws and community.aws, it is generally the collection's policy to support the versions of `botocore` and `boto3` that were released 12 months prior to the most recent major collection release, following semantic versioning (for example, 2.0.0, 3.0.0). + +Version 2.0.0 of this collection supports `boto3 >= 1.13.0` and `botocore >= 1.16.0` ## Included content diff --git a/changelogs/fragments/290-lint-cleanup.yml b/changelogs/fragments/290-lint-cleanup.yml new file mode 100644 index 00000000000..36ab84b6df3 --- /dev/null +++ b/changelogs/fragments/290-lint-cleanup.yml @@ -0,0 +1,2 @@ +breaking_changes: +- module_utils.core - The boto3 switch has been removed from the region parameter (https://github.com/ansible-collections/amazon.aws/pull/287). diff --git a/changelogs/fragments/297-scrub_none_parameters-descend-default.yml b/changelogs/fragments/297-scrub_none_parameters-descend-default.yml new file mode 100644 index 00000000000..8874e379b04 --- /dev/null +++ b/changelogs/fragments/297-scrub_none_parameters-descend-default.yml @@ -0,0 +1,2 @@ +breaking_changes: +- module_utils/core - updated the ``scrub_none_parameters`` function so that ``descend_into_lists`` is set to ``True`` by default (https://github.com/ansible-collections/amazon.aws/pull/297). diff --git a/changelogs/fragments/298-python3.6.yml b/changelogs/fragments/298-python3.6.yml new file mode 100644 index 00000000000..600414b3412 --- /dev/null +++ b/changelogs/fragments/298-python3.6.yml @@ -0,0 +1,2 @@ +major_changes: +- amazon.aws collection - Due to the AWS SDKs announcing the end of support for Python less than 3.6 (https://boto3.amazonaws.com/v1/documentation/api/1.17.64/guide/migrationpy3.html) this collection now requires Python 3.6+ (https://github.com/ansible-collections/amazon.aws/pull/298). diff --git a/changelogs/fragments/318-s3-upload-acl.yml b/changelogs/fragments/318-s3-upload-acl.yml new file mode 100644 index 00000000000..19326ceb315 --- /dev/null +++ b/changelogs/fragments/318-s3-upload-acl.yml @@ -0,0 +1,2 @@ +bugfixes: +- aws_s3 - Fix upload permission when an S3 bucket ACL policy requires a particular canned ACL (https://github.com/ansible-collections/amazon.aws/pull/318) diff --git a/changelogs/fragments/361-drop-community.general-support-for-integration.tests.yml b/changelogs/fragments/361-drop-community.general-support-for-integration.tests.yml new file mode 100644 index 00000000000..be7557ed5cf --- /dev/null +++ b/changelogs/fragments/361-drop-community.general-support-for-integration.tests.yml @@ -0,0 +1,2 @@ +minor_changes: +- integration tests - remove dependency with collection ``community.general`` (https://github.com/ansible-collections/amazon.aws/pull/361). diff --git a/changelogs/fragments/migrate_ec2_instance.yml b/changelogs/fragments/migrate_ec2_instance.yml new file mode 100644 index 00000000000..988822f7d08 --- /dev/null +++ b/changelogs/fragments/migrate_ec2_instance.yml @@ -0,0 +1,3 @@ +major_changes: + - ec2_instance - The module has been migrated from the ``community.aws`` collection. Playbooks using the Fully Qualified Collection Name for this module should be updated to use ``amazon.aws.ec2_instance``. + - ec2_instance_info - The module has been migrated from the ``community.aws`` collection. Playbooks using the Fully Qualified Collection Name for this module should be updated to use ``amazon.aws.ec2_instance_info``. diff --git a/meta/runtime.yml b/meta/runtime.yml index 6b938e41c4c..df3b4370954 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -33,6 +33,8 @@ action_groups: - ec2_eni_info - ec2_group - ec2_group_info + - ec2_instance + - ec2_instance_info - ec2_key - ec2_snapshot - ec2_snapshot_info diff --git a/plugins/doc_fragments/aws.py b/plugins/doc_fragments/aws.py index 9eec9a8b3bd..ce7a6eab2fd 100644 --- a/plugins/doc_fragments/aws.py +++ b/plugins/doc_fragments/aws.py @@ -53,17 +53,17 @@ class ModuleDocFragment(object): aws_ca_bundle: description: - "The location of a CA Bundle to use when validating SSL certificates." - - "Only used for boto3 based modules." + - "Not used by boto 2 based modules." - "Note: The CA Bundle is read 'module' side and may need to be explicitly copied from the controller if not run locally." type: path validate_certs: description: - - When set to "no", SSL certificates will not be validated for boto versions >= 2.6.0. + - When set to "no", SSL certificates will not be validated for + communication with the AWS APIs. type: bool default: yes profile: description: - - Uses a boto profile. Only works with boto >= 2.24.0. - Using I(profile) will override I(aws_access_key), I(aws_secret_key) and I(security_token) and support for passing them at the same time as I(profile) has been deprecated. - I(aws_access_key), I(aws_secret_key) and I(security_token) will be made mutually exclusive with I(profile) after 2022-06-01. @@ -76,8 +76,9 @@ class ModuleDocFragment(object): - Only the 'user_agent' key is used for boto modules. See U(http://boto.cloudhackers.com/en/latest/boto_config_tut.html#boto) for more boto configuration. type: dict requirements: - - python >= 2.6 - - boto + - python >= 3.6 + - boto3 >= 1.13.0 + - botocore >= 1.16.0 notes: - If parameters are not set within the module, the following environment variables can be used in decreasing order of precedence diff --git a/plugins/lookup/aws_account_attribute.py b/plugins/lookup/aws_account_attribute.py index e1ba8f23ddf..9b79aa26861 100644 --- a/plugins/lookup/aws_account_attribute.py +++ b/plugins/lookup/aws_account_attribute.py @@ -8,8 +8,9 @@ author: - Sloane Hertel requirements: + - python >= 3.6 - boto3 - - botocore + - botocore >= 1.16.0 extends_documentation_fragment: - amazon.aws.aws_credentials - amazon.aws.aws_region diff --git a/plugins/lookup/aws_secret.py b/plugins/lookup/aws_secret.py index ef7a8f9a909..5e8b2602c00 100644 --- a/plugins/lookup/aws_secret.py +++ b/plugins/lookup/aws_secret.py @@ -9,8 +9,9 @@ author: - Aaron Smith requirements: + - python >= 3.6 - boto3 - - botocore>=1.10.0 + - botocore >= 1.16.0 extends_documentation_fragment: - amazon.aws.aws_credentials - amazon.aws.aws_region diff --git a/plugins/lookup/aws_ssm.py b/plugins/lookup/aws_ssm.py index c9de00d4389..0a4646c4478 100644 --- a/plugins/lookup/aws_ssm.py +++ b/plugins/lookup/aws_ssm.py @@ -14,8 +14,9 @@ - Marat Bakeev - Michael De La Rue requirements: + - python >= 3.6 - boto3 - - botocore + - botocore >= 1.16.0 short_description: Get the value for a SSM parameter or all parameters under a path. description: - Get the value for an Amazon Simple Systems Manager parameter or a hierarchy of parameters. diff --git a/plugins/module_utils/core.py b/plugins/module_utils/core.py index 7e72843d62a..35fc24df98b 100644 --- a/plugins/module_utils/core.py +++ b/plugins/module_utils/core.py @@ -190,8 +190,8 @@ def resource(self, service): region=region, endpoint=ec2_url, **aws_connect_kwargs) @property - def region(self, boto3=True): - return get_aws_region(self, boto3) + def region(self): + return get_aws_region(self, True) def fail_json_aws(self, exception, msg=None, **kwargs): """call fail_json with processed exception @@ -360,7 +360,7 @@ def get_boto3_client_method_parameters(client, method_name, required=False): return parameters -def scrub_none_parameters(parameters, descend_into_lists=False): +def scrub_none_parameters(parameters, descend_into_lists=True): """ Iterate over a dictionary removing any keys that have a None value diff --git a/plugins/modules/aws_az_info.py b/plugins/modules/aws_az_info.py index 42f1232345c..1aef86f5cea 100644 --- a/plugins/modules/aws_az_info.py +++ b/plugins/modules/aws_az_info.py @@ -29,8 +29,6 @@ extends_documentation_fragment: - amazon.aws.aws - amazon.aws.ec2 - -requirements: [botocore, boto3] ''' EXAMPLES = ''' diff --git a/plugins/modules/aws_caller_info.py b/plugins/modules/aws_caller_info.py index 91880fdba1e..a66e7c6b9c7 100644 --- a/plugins/modules/aws_caller_info.py +++ b/plugins/modules/aws_caller_info.py @@ -20,7 +20,6 @@ - Ed Costello (@orthanc) - Stijn Dubrul (@sdubrul) -requirements: [ 'botocore', 'boto3' ] extends_documentation_fragment: - amazon.aws.aws - amazon.aws.ec2 diff --git a/plugins/modules/aws_s3.py b/plugins/modules/aws_s3.py index 71b74acbd1e..1476aa013b1 100644 --- a/plugins/modules/aws_s3.py +++ b/plugins/modules/aws_s3.py @@ -78,7 +78,8 @@ - This option lets the user set the canned permissions on the object/bucket that are created. The permissions that can be set are C(private), C(public-read), C(public-read-write), C(authenticated-read) for a bucket or C(private), C(public-read), C(public-read-write), C(aws-exec-read), C(authenticated-read), C(bucket-owner-read), - C(bucket-owner-full-control) for an object. Multiple permissions can be specified as a list. + C(bucket-owner-full-control) for an object. Multiple permissions can be specified as a list; although only the first one + will be used during the initial upload of the file default: ['private'] type: list elements: str @@ -117,7 +118,6 @@ dualstack: description: - Enables Amazon S3 Dual-Stack Endpoints, allowing S3 communications using both IPv4 and IPv6. - - Requires at least botocore version 1.4.45. type: bool default: false rgw: @@ -156,6 +156,7 @@ description: - KMS key id to use when encrypting objects using I(encrypting=aws:kms). Ignored if I(encryption) is not C(aws:kms). type: str +<<<<<<< HEAD copy_src: description: - The source details of the object to copy. @@ -178,6 +179,8 @@ description: - version ID of the source object. requirements: [ "boto3", "botocore" ] +======= +>>>>>>> dff146be9cb89b0e4857a721dd02103d084cb0fc author: - "Lester Wade (@lwade)" - "Sloane Hertel (@s-hertel)" @@ -563,6 +566,13 @@ def upload_s3file(module, s3, bucket, obj, expiry, metadata, encrypt, headers, s else: extra['Metadata'][option] = metadata[option] + if module.params.get('permission'): + permissions = module.params['permission'] + if isinstance(permissions, str): + extra['ACL'] = permissions + elif isinstance(permissions, list): + extra['ACL'] = permissions[0] + if 'ContentType' not in extra: content_type = None if src is not None: diff --git a/plugins/modules/cloudformation.py b/plugins/modules/cloudformation.py index 76fb55f19b7..2d3e0453f44 100644 --- a/plugins/modules/cloudformation.py +++ b/plugins/modules/cloudformation.py @@ -128,7 +128,7 @@ type: str termination_protection: description: - - Enable or disable termination protection on the stack. Only works with botocore >= 1.7.18. + - Enable or disable termination protection on the stack. type: bool template_body: description: @@ -174,8 +174,6 @@ extends_documentation_fragment: - amazon.aws.aws - amazon.aws.ec2 - -requirements: [ boto3, botocore>=1.5.45 ] ''' EXAMPLES = ''' @@ -344,10 +342,15 @@ from ansible.module_utils._text import to_native from ..module_utils.core import AnsibleAWSModule +from ..module_utils.core import is_boto3_error_message from ..module_utils.ec2 import AWSRetry from ..module_utils.ec2 import ansible_dict_to_boto3_tag_list from ..module_utils.ec2 import boto_exception +# Set a default, mostly for our integration tests. This will be overridden in +# the main() loop to match the parameters we're passed +retry_decorator = AWSRetry.jittered_backoff() + def get_stack_events(cfn, stack_name, events_limit, token_filter=None): '''This event data was never correct, it worked as a side effect. So the v2.3 format is different.''' @@ -361,17 +364,16 @@ def get_stack_events(cfn, stack_name, events_limit, token_filter=None): PaginationConfig={'MaxItems': events_limit} ) if token_filter is not None: - events = list(pg.search( + events = list(retry_decorator(pg.search)( "StackEvents[?ClientRequestToken == '{0}']".format(token_filter) )) else: events = list(pg.search("StackEvents[*]")) - except (botocore.exceptions.ValidationError, botocore.exceptions.ClientError) as err: + except is_boto3_error_message('does not exist'): + ret['log'].append('Stack does not exist.') + return ret + except (botocore.exceptions.ValidationError, botocore.exceptions.ClientError) as err: # pylint: disable=duplicate-except error_msg = boto_exception(err) - if 'does not exist' in error_msg: - # missing stack, don't bail. - ret['log'].append('Stack does not exist.') - return ret ret['log'].append('Unknown error: ' + str(error_msg)) return ret @@ -406,9 +408,9 @@ def create_stack(module, stack_params, cfn, events_limit): module.fail_json(msg="termination_protection parameter requires botocore >= 1.7.18") try: - response = cfn.create_stack(**stack_params) + response = cfn.create_stack(aws_retry=True, **stack_params) # Use stack ID to follow stack state in case of on_create_failure = DELETE - result = stack_operation(cfn, response['StackId'], 'CREATE', events_limit, stack_params.get('ClientRequestToken', None)) + result = stack_operation(module, cfn, response['StackId'], 'CREATE', events_limit, stack_params.get('ClientRequestToken', None)) except Exception as err: module.fail_json_aws(err, msg="Failed to create stack {0}".format(stack_params.get('StackName'))) if not result: @@ -417,7 +419,7 @@ def create_stack(module, stack_params, cfn, events_limit): def list_changesets(cfn, stack_name): - res = cfn.list_change_sets(StackName=stack_name) + res = cfn.list_change_sets(aws_retry=True, StackName=stack_name) return [cs['ChangeSetName'] for cs in res['Summaries']] @@ -440,18 +442,18 @@ def create_changeset(module, stack_params, cfn, events_limit): warning = 'WARNING: %d pending changeset(s) exist(s) for this stack!' % len(pending_changesets) result = dict(changed=False, output='ChangeSet %s already exists.' % changeset_name, warnings=[warning]) else: - cs = cfn.create_change_set(**stack_params) + cs = cfn.create_change_set(aws_retry=True, **stack_params) # Make sure we don't enter an infinite loop time_end = time.time() + 600 while time.time() < time_end: try: - newcs = cfn.describe_change_set(ChangeSetName=cs['Id']) + newcs = cfn.describe_change_set(aws_retry=True, ChangeSetName=cs['Id']) except botocore.exceptions.BotoCoreError as err: module.fail_json_aws(err) if newcs['Status'] == 'CREATE_PENDING' or newcs['Status'] == 'CREATE_IN_PROGRESS': time.sleep(1) elif newcs['Status'] == 'FAILED' and "The submitted information didn't contain changes" in newcs['StatusReason']: - cfn.delete_change_set(ChangeSetName=cs['Id']) + cfn.delete_change_set(aws_retry=True, ChangeSetName=cs['Id']) result = dict(changed=False, output='The created Change Set did not contain any changes to this stack and was deleted.') # a failed change set does not trigger any stack events so we just want to @@ -461,17 +463,15 @@ def create_changeset(module, stack_params, cfn, events_limit): break # Lets not hog the cpu/spam the AWS API time.sleep(1) - result = stack_operation(cfn, stack_params['StackName'], 'CREATE_CHANGESET', events_limit) + result = stack_operation(module, cfn, stack_params['StackName'], 'CREATE_CHANGESET', events_limit) result['change_set_id'] = cs['Id'] result['warnings'] = ['Created changeset named %s for stack %s' % (changeset_name, stack_params['StackName']), 'You can execute it using: aws cloudformation execute-change-set --change-set-name %s' % cs['Id'], 'NOTE that dependencies on this stack might fail due to pending changes!'] + except is_boto3_error_message('No updates are to be performed.'): + result = dict(changed=False, output='Stack is already up-to-date.') except Exception as err: - error_msg = boto_exception(err) - if 'No updates are to be performed.' in error_msg: - result = dict(changed=False, output='Stack is already up-to-date.') - else: - module.fail_json_aws(err, msg='Failed to create change set') + module.fail_json_aws(err, msg='Failed to create change set') if not result: module.fail_json(msg="empty result") @@ -489,14 +489,12 @@ def update_stack(module, stack_params, cfn, events_limit): # AWS will tell us if the stack template and parameters are the same and # don't need to be updated. try: - cfn.update_stack(**stack_params) - result = stack_operation(cfn, stack_params['StackName'], 'UPDATE', events_limit, stack_params.get('ClientRequestToken', None)) + cfn.update_stack(aws_retry=True, **stack_params) + result = stack_operation(module, cfn, stack_params['StackName'], 'UPDATE', events_limit, stack_params.get('ClientRequestToken', None)) + except is_boto3_error_message('No updates are to be performed.'): + result = dict(changed=False, output='Stack is already up-to-date.') except Exception as err: - error_msg = boto_exception(err) - if 'No updates are to be performed.' in error_msg: - result = dict(changed=False, output='Stack is already up-to-date.') - else: - module.fail_json_aws(err, msg="Failed to update stack {0}".format(stack_params.get('StackName'))) + module.fail_json_aws(err, msg="Failed to update stack {0}".format(stack_params.get('StackName'))) if not result: module.fail_json(msg="empty result") return result @@ -506,11 +504,12 @@ def update_termination_protection(module, cfn, stack_name, desired_termination_p '''updates termination protection of a stack''' if not boto_supports_termination_protection(cfn): module.fail_json(msg="termination_protection parameter requires botocore >= 1.7.18") - stack = get_stack_facts(cfn, stack_name) + stack = get_stack_facts(module, cfn, stack_name) if stack: if stack['EnableTerminationProtection'] is not desired_termination_protection_state: try: cfn.update_termination_protection( + aws_retry=True, EnableTerminationProtection=desired_termination_protection_state, StackName=stack_name) except botocore.exceptions.ClientError as e: @@ -522,12 +521,12 @@ def boto_supports_termination_protection(cfn): return hasattr(cfn, "update_termination_protection") -def stack_operation(cfn, stack_name, operation, events_limit, op_token=None): +def stack_operation(module, cfn, stack_name, operation, events_limit, op_token=None): '''gets the status of a stack while it is created/updated/deleted''' existed = [] while True: try: - stack = get_stack_facts(cfn, stack_name) + stack = get_stack_facts(module, cfn, stack_name, raise_errors=True) existed.append('yes') except Exception: # If the stack previously existed, and now can't be found then it's @@ -591,9 +590,9 @@ def check_mode_changeset(module, stack_params, cfn): stack_params.pop('ClientRequestToken', None) try: - change_set = cfn.create_change_set(**stack_params) + change_set = cfn.create_change_set(aws_retry=True, **stack_params) for i in range(60): # total time 5 min - description = cfn.describe_change_set(ChangeSetName=change_set['Id']) + description = cfn.describe_change_set(aws_retry=True, ChangeSetName=change_set['Id']) if description['Status'] in ('CREATE_COMPLETE', 'FAILED'): break time.sleep(5) @@ -601,7 +600,7 @@ def check_mode_changeset(module, stack_params, cfn): # if the changeset doesn't finish in 5 mins, this `else` will trigger and fail module.fail_json(msg="Failed to create change set %s" % stack_params['ChangeSetName']) - cfn.delete_change_set(ChangeSetName=change_set['Id']) + cfn.delete_change_set(aws_retry=True, ChangeSetName=change_set['Id']) reason = description.get('StatusReason') @@ -613,18 +612,16 @@ def check_mode_changeset(module, stack_params, cfn): module.fail_json_aws(err) -def get_stack_facts(cfn, stack_name): +def get_stack_facts(module, cfn, stack_name, raise_errors=False): try: - stack_response = cfn.describe_stacks(StackName=stack_name) + stack_response = cfn.describe_stacks(aws_retry=True, StackName=stack_name) stack_info = stack_response['Stacks'][0] - except (botocore.exceptions.ValidationError, botocore.exceptions.ClientError) as err: - error_msg = boto_exception(err) - if 'does not exist' in error_msg: - # missing stack, don't bail. - return None - - # other error, bail. - raise err + except is_boto3_error_message('does not exist'): + return None + except (botocore.exceptions.ValidationError, botocore.exceptions.ClientError) as err: # pylint: disable=duplicate-except + if raise_errors: + raise err + module.fail_json_aws(err, msg="Failed to describe stack") if stack_response and stack_response.get('Stacks', None): stacks = stack_response['Stacks'] @@ -735,27 +732,16 @@ def main(): result = {} - cfn = module.client('cloudformation') - # Wrap the cloudformation client methods that this module uses with # automatic backoff / retry for throttling error codes - backoff_wrapper = AWSRetry.jittered_backoff( + retry_decorator = AWSRetry.jittered_backoff( retries=module.params.get('backoff_retries'), delay=module.params.get('backoff_delay'), max_delay=module.params.get('backoff_max_delay') ) - cfn.describe_stack_events = backoff_wrapper(cfn.describe_stack_events) - cfn.create_stack = backoff_wrapper(cfn.create_stack) - cfn.list_change_sets = backoff_wrapper(cfn.list_change_sets) - cfn.create_change_set = backoff_wrapper(cfn.create_change_set) - cfn.update_stack = backoff_wrapper(cfn.update_stack) - cfn.describe_stacks = backoff_wrapper(cfn.describe_stacks) - cfn.list_stack_resources = backoff_wrapper(cfn.list_stack_resources) - cfn.delete_stack = backoff_wrapper(cfn.delete_stack) - if boto_supports_termination_protection(cfn): - cfn.update_termination_protection = backoff_wrapper(cfn.update_termination_protection) - - stack_info = get_stack_facts(cfn, stack_params['StackName']) + cfn = module.client('cloudformation', retry_decorator=retry_decorator) + + stack_info = get_stack_facts(module, cfn, stack_params['StackName']) if module.check_mode: if state == 'absent' and stack_info: @@ -780,7 +766,7 @@ def main(): # format the stack output - stack = get_stack_facts(cfn, stack_params['StackName']) + stack = get_stack_facts(module, cfn, stack_params['StackName']) if stack is not None: if result.get('stack_outputs') is None: # always define stack_outputs, but it may be empty @@ -788,7 +774,7 @@ def main(): for output in stack.get('Outputs', []): result['stack_outputs'][output['OutputKey']] = output['OutputValue'] stack_resources = [] - reslist = cfn.list_stack_resources(StackName=stack_params['StackName']) + reslist = cfn.list_stack_resources(aws_retry=True, StackName=stack_params['StackName']) for res in reslist.get('StackResourceSummaries', []): stack_resources.append({ "logical_resource_id": res['LogicalResourceId'], @@ -806,15 +792,15 @@ def main(): # so must describe the stack first try: - stack = get_stack_facts(cfn, stack_params['StackName']) + stack = get_stack_facts(module, cfn, stack_params['StackName']) if not stack: result = {'changed': False, 'output': 'Stack not found.'} else: if stack_params.get('RoleARN') is None: - cfn.delete_stack(StackName=stack_params['StackName']) + cfn.delete_stack(aws_retry=True, StackName=stack_params['StackName']) else: - cfn.delete_stack(StackName=stack_params['StackName'], RoleARN=stack_params['RoleARN']) - result = stack_operation(cfn, stack_params['StackName'], 'DELETE', module.params.get('events_limit'), + cfn.delete_stack(aws_retry=True, StackName=stack_params['StackName'], RoleARN=stack_params['RoleARN']) + result = stack_operation(module, cfn, stack_params['StackName'], 'DELETE', module.params.get('events_limit'), stack_params.get('ClientRequestToken', None)) except Exception as err: module.fail_json_aws(err) diff --git a/plugins/modules/cloudformation_info.py b/plugins/modules/cloudformation_info.py index 0c34e8b1d18..492fb23bc8d 100644 --- a/plugins/modules/cloudformation_info.py +++ b/plugins/modules/cloudformation_info.py @@ -15,9 +15,6 @@ - Gets information about an AWS CloudFormation stack. - This module was called C(amazon.aws.cloudformation_facts) before Ansible 2.9, returning C(ansible_facts). Note that the M(amazon.aws.cloudformation_info) module no longer returns C(ansible_facts)! -requirements: - - boto3 >= 1.0.0 - - python >= 2.6 author: - Justin Menga (@jmenga) - Kevin Coming (@waffie1) diff --git a/plugins/modules/ec2.py b/plugins/modules/ec2.py index 990a7e69be5..ab0b7ea0ab0 100644 --- a/plugins/modules/ec2.py +++ b/plugins/modules/ec2.py @@ -257,6 +257,9 @@ extends_documentation_fragment: - amazon.aws.aws - amazon.aws.ec2 +requirements: +- python >= 2.6 +- boto ''' @@ -577,6 +580,297 @@ ''' +RETURN = r''' +changed: + description: If the EC2 instance has changed. + type: bool + returned: always + sample: true +instances: + description: The instances. + type: list + returned: always + contains: + ami_launch_index: + description: The AMI launch index, which can be used to find this instance in the launch group. + type: int + returned: always + sample: 0 + architecture: + description: The architecture of the image. + type: str + returned: always + sample: "x86_64" + block_device_mapping: + description: Any block device mapping entries for the instance. + type: dict + returned: always + sample: { + "/dev/xvda": { + "delete_on_termination": true, + "status": "attached", + "volume_id": "vol-06d364586f5550b62" + } + } + dns_name: + description: The public DNS name assigned to the instance. + type: str + returned: always + sample: "ec2-203-0-113-1.z-2.compute-1.amazonaws.com" + ebs_optimized: + description: Indicates whether the instance is optimized for Amazon EBS I/O. + type: bool + returned: always + sample: false + groups: + description: One or more security groups. + type: dict + returned: always + sample: { + "sg-0c6562ab3d435619f": "ansible-test--88312190_setup" + } + hypervisor: + description: The hypervisor type of the instance. + type: str + returned: always + sample: "xen" + image_id: + description: The ID of the AMI used to launch the instance. + type: str + returned: always + sample: "ami-0d5eff06f840b45e9" + instance_id: + description: The ID of the instance. + type: str + returned: always + sample: "i-0250719204c428be1" + instance_type: + description: The instance type. + type: str + returned: always + sample: "t2.micro" + kernel: + description: The kernel associated with this instance, if applicable. + type: str + returned: always + sample: "" + key_name: + description: The name of the key pair, if this instance was launched with an associated key pair. + type: str + returned: always + sample: "ansible-test-88312190_setup" + launch_time: + description: The time the instance was launched. + type: str + returned: always + sample: "2021-05-09T19:30:26.000Z" + placement: + description: The location where the instance launched, if applicable. + type: dict + returned: always + sample: { + "availability_zone": "us-east-1a", + "group_name": "", + "tenancy": "default" + } + private_dns_name: + description: The private DNS hostname name assigned to the instance. + type: str + returned: always + sample: "ip-10-176-1-249.ec2.internal" + private_ip: + description: The private IPv4 address assigned to the instance. + type: str + returned: always + sample: "10.176.1.249" + public_dns_name: + description: The public DNS name assigned to the instance. + type: str + returned: always + sample: "ec2-203-0-113-1.z-2.compute-1.amazonaws.com" + public_ip: + description: The public IPv4 address, or the Carrier IP address assigned to the instance, if applicable. + type: str + returned: always + sample: "203.0.113.1" + ramdisk: + description: The RAM disk associated with this instance, if applicable. + type: str + returned: always + sample: "" + root_device_name: + description: The device name of the root device volume. + type: str + returned: always + sample: "/dev/xvda" + root_device_type: + description: The root device type used by the AMI. + type: str + returned: always + sample: "ebs" + state: + description: The current state of the instance. + type: dict + returned: always + sample: { + "code": 80, + "name": "stopped" + } + tags: + description: Any tags assigned to the instance. + type: dict + returned: always + sample: { + "ResourcePrefix": "ansible-test-88312190-integration_tests" + } + tenancy: + description: The tenancy of the instance (if the instance is running in a VPC). + type: str + returned: always + sample: "default" + virtualization_type: + description: The virtualization type of the instance. + type: str + returned: always + sample: "hvm" + monitoring: + description: The monitoring for the instance. + type: dict + returned: always + sample: { + "state": "disabled" + } + capacity_reservation_specification: + description: Information about the Capacity Reservation targeting option. + type: dict + returned: always + sample: { + "capacity_reservation_preference": "open" + } + client_token: + description: The idempotency token you provided when you launched the instance, if applicable. + type: str + returned: always + sample: "" + cpu_options: + description: The CPU options for the instance. + type: dict + returned: always + sample: { + "core_count": 1, + "threads_per_core": 1 + } + ena_support: + description: Specifies whether enhanced networking with ENA is enabled. + type: bool + returned: always + sample: true + enclave_options: + description: Indicates whether the instance is enabled for AWS Nitro Enclaves. + type: dict + returned: always + sample: { + "enabled": false + } + hibernation_options: + description: Indicates whether the instance is enabled for hibernation. + type: dict + returned: always + sample: { + "configured": false + } + network_interfaces: + description: The network interfaces for the instance. + type: list + returned: always + sample: [ + { + "attachment": { + "attach_time": "2021-05-09T19:30:57+00:00", + "attachment_id": "eni-attach-07341f2560be6c8fc", + "delete_on_termination": true, + "device_index": 0, + "network_card_index": 0, + "status": "attached" + }, + "description": "", + "groups": [ + { + "group_id": "sg-0c6562ab3d435619f", + "group_name": "ansible-test-88312190_setup" + } + ], + "interface_type": "interface", + "ipv6_addresses": [], + "mac_address": "0e:0e:36:60:67:cf", + "network_interface_id": "eni-061dee20eba3b445a", + "owner_id": "721066863947", + "private_dns_name": "ip-10-176-1-178.ec2.internal", + "private_ip_address": "10.176.1.178", + "private_ip_addresses": [ + { + "primary": true, + "private_dns_name": "ip-10-176-1-178.ec2.internal", + "private_ip_address": "10.176.1.178" + } + ], + "source_dest_check": true, + "status": "in-use", + "subnet_id": "subnet-069d3e2eab081955d", + "vpc_id": "vpc-0b6879b6ca2e9be2b" + } + ] + vpc_id: + description: The ID of the VPC in which the instance is running. + type: str + returned: always + sample: "vpc-0b6879b6ca2e9be2b" + subnet_id: + description: The ID of the subnet in which the instance is running. + type: str + returned: always + sample: "subnet-069d3e2eab081955d" + state_transition_reason: + description: The reason for the most recent state transition. This might be an empty string. + type: str + returned: always + sample: "User initiated (2021-05-09 19:31:28 GMT)" + state_reason: + description: The reason for the most recent state transition. + type: dict + returned: always + sample: { + "code": "Client.UserInitiatedShutdown", + "message": "Client.UserInitiatedShutdown: User initiated shutdown" + } + security_groups: + description: The security groups for the instance. + type: list + returned: always + sample: [ + { + "group_id": "sg-0c6562ab3d435619f", + "group_name": "ansible-test-alinas-mbp-88312190_setup" + } + ] + source_dest_check: + description: Indicates whether source/destination checking is enabled. + type: bool + returned: always + sample: true + metadata: + description: The metadata options for the instance. + type: dict + returned: always + sample: { + "http_endpoint": "enabled", + "http_put_response_hop_limit": 1, + "http_tokens": "optional", + "state": "applied" + } +''' + + import time import datetime from ast import literal_eval diff --git a/plugins/modules/ec2_ami_info.py b/plugins/modules/ec2_ami_info.py index 11c1bb6e687..3dd8f71e69c 100644 --- a/plugins/modules/ec2_ami_info.py +++ b/plugins/modules/ec2_ami_info.py @@ -16,7 +16,6 @@ - This module was called C(amazon.aws.ec2_ami_facts) before Ansible 2.9. The usage did not change. author: - Prasad Katti (@prasadkatti) -requirements: [ boto3 ] options: image_ids: description: One or more image IDs. diff --git a/plugins/modules/ec2_elb_lb.py b/plugins/modules/ec2_elb_lb.py index 9a005b29faf..1c3c8d34fbb 100644 --- a/plugins/modules/ec2_elb_lb.py +++ b/plugins/modules/ec2_elb_lb.py @@ -132,6 +132,10 @@ - amazon.aws.aws - amazon.aws.ec2 +requirements: +- python >= 2.6 +- boto + ''' EXAMPLES = """ diff --git a/plugins/modules/ec2_eni_info.py b/plugins/modules/ec2_eni_info.py index 17a5fff38ea..9ed67bcabfa 100644 --- a/plugins/modules/ec2_eni_info.py +++ b/plugins/modules/ec2_eni_info.py @@ -15,7 +15,6 @@ - Gather information about ec2 ENI interfaces in AWS. - This module was called C(ec2_eni_facts) before Ansible 2.9. The usage did not change. author: "Rob White (@wimnat)" -requirements: [ boto3 ] options: eni_id: description: diff --git a/plugins/modules/ec2_group.py b/plugins/modules/ec2_group.py index 8bbb112a313..7683ecb1a83 100644 --- a/plugins/modules/ec2_group.py +++ b/plugins/modules/ec2_group.py @@ -12,7 +12,6 @@ module: ec2_group version_added: 1.0.0 author: "Andrew de Quincey (@adq)" -requirements: [ boto3 ] short_description: maintain an ec2 VPC security group. description: - Maintains ec2 security groups. diff --git a/plugins/modules/ec2_group_info.py b/plugins/modules/ec2_group_info.py index 228b82d9923..63d9e7ecfca 100644 --- a/plugins/modules/ec2_group_info.py +++ b/plugins/modules/ec2_group_info.py @@ -14,7 +14,6 @@ description: - Gather information about ec2 security groups in AWS. - This module was called C(amazon.aws.ec2_group_facts) before Ansible 2.9. The usage did not change. -requirements: [ boto3 ] author: - Henrique Rodrigues (@Sodki) options: diff --git a/plugins/modules/ec2_instance.py b/plugins/modules/ec2_instance.py new file mode 100644 index 00000000000..81841761a53 --- /dev/null +++ b/plugins/modules/ec2_instance.py @@ -0,0 +1,1830 @@ +#!/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 = r''' +--- +module: ec2_instance +version_added: 1.0.0 +short_description: Create & manage EC2 instances +description: + - Create and manage AWS EC2 instances. + - > + Note: This module does not support creating + L(EC2 Spot instances,https://aws.amazon.com/ec2/spot/). The M(amazon.aws.ec2) module + can create and manage spot instances. +author: + - Ryan Scott Brown (@ryansb) +requirements: [ "boto3", "botocore" ] +options: + instance_ids: + description: + - If you specify one or more instance IDs, only instances that have the specified IDs are returned. + type: list + elements: str + state: + description: + - Goal state for the instances. + choices: [present, terminated, running, started, stopped, restarted, rebooted, absent] + default: present + type: str + wait: + description: + - Whether or not to wait for the desired state (use wait_timeout to customize this). + default: true + type: bool + wait_timeout: + description: + - How long to wait (in seconds) for the instance to finish booting/terminating. + default: 600 + type: int + instance_type: + description: + - Instance type to use for the instance, see U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-types.html) + Only required when instance is not already present. + default: t2.micro + type: str + user_data: + description: + - Opaque blob of data which is made available to the ec2 instance + type: str + tower_callback: + description: + - Preconfigured user-data to enable an instance to perform a Tower callback (Linux only). + - Mutually exclusive with I(user_data). + - For Windows instances, to enable remote access via Ansible set I(tower_callback.windows) to true, and optionally set an admin password. + - If using 'windows' and 'set_password', callback to Tower will not be performed but the instance will be ready to receive winrm connections from Ansible. + type: dict + suboptions: + tower_address: + description: + - IP address or DNS name of Tower server. Must be accessible via this address from the VPC that this instance will be launched in. + type: str + job_template_id: + description: + - Either the integer ID of the Tower Job Template, or the name (name supported only for Tower 3.2+). + type: str + host_config_key: + description: + - Host configuration secret key generated by the Tower job template. + type: str + tags: + description: + - A hash/dictionary of tags to add to the new instance or to add/remove from an existing one. + type: dict + purge_tags: + description: + - Delete any tags not specified in the task that are on the instance. + This means you have to specify all the desired tags on each task affecting an instance. + default: false + type: bool + image: + description: + - An image to use for the instance. The M(amazon.aws.ec2_ami_info) module may be used to retrieve images. + One of I(image) or I(image_id) are required when instance is not already present. + type: dict + suboptions: + id: + description: + - The AMI ID. + type: str + ramdisk: + description: + - Overrides the AMI's default ramdisk ID. + type: str + kernel: + description: + - a string AKI to override the AMI kernel. + image_id: + description: + - I(ami) ID to use for the instance. One of I(image) or I(image_id) are required when instance is not already present. + - This is an alias for I(image.id). + type: str + security_groups: + description: + - A list of security group IDs or names (strings). Mutually exclusive with I(security_group). + type: list + elements: str + security_group: + description: + - A security group ID or name. Mutually exclusive with I(security_groups). + type: str + name: + description: + - The Name tag for the instance. + type: str + vpc_subnet_id: + description: + - The subnet ID in which to launch the instance (VPC) + If none is provided, M(amazon.aws.ec2_instance) will chose the default zone of the default VPC. + aliases: ['subnet_id'] + type: str + network: + description: + - Either a dictionary containing the key 'interfaces' corresponding to a list of network interface IDs or + containing specifications for a single network interface. + - Use the M(amazon.aws.ec2_eni) module to create ENIs with special settings. + type: dict + suboptions: + interfaces: + description: + - a list of ENI IDs (strings) or a list of objects containing the key I(id). + type: list + assign_public_ip: + description: + - when true assigns a public IP address to the interface + type: bool + private_ip_address: + description: + - an IPv4 address to assign to the interface + type: str + ipv6_addresses: + description: + - a list of IPv6 addresses to assign to the network interface + type: list + source_dest_check: + description: + - controls whether source/destination checking is enabled on the interface + type: bool + description: + description: + - a description for the network interface + type: str + private_ip_addresses: + description: + - a list of IPv4 addresses to assign to the network interface + type: list + subnet_id: + description: + - the subnet to connect the network interface to + type: str + delete_on_termination: + description: + - Delete the interface when the instance it is attached to is + terminated. + type: bool + device_index: + description: + - The index of the interface to modify + type: int + groups: + description: + - a list of security group IDs to attach to the interface + type: list + volumes: + description: + - A list of block device mappings, by default this will always use the AMI root device so the volumes option is primarily for adding more storage. + - A mapping contains the (optional) keys device_name, virtual_name, ebs.volume_type, ebs.volume_size, ebs.kms_key_id, + ebs.iops, and ebs.delete_on_termination. + - For more information about each parameter, see U(https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_BlockDeviceMapping.html). + type: list + elements: dict + launch_template: + description: + - The EC2 launch template to base instance configuration on. + type: dict + suboptions: + id: + description: + - the ID of the launch template (optional if name is specified). + type: str + name: + description: + - the pretty name of the launch template (optional if id is specified). + type: str + version: + description: + - the specific version of the launch template to use. If unspecified, the template default is chosen. + key_name: + description: + - Name of the SSH access key to assign to the instance - must exist in the region the instance is created. + type: str + availability_zone: + description: + - Specify an availability zone to use the default subnet it. Useful if not specifying the I(vpc_subnet_id) parameter. + - If no subnet, ENI, or availability zone is provided, the default subnet in the default VPC will be used in the first AZ (alphabetically sorted). + type: str + instance_initiated_shutdown_behavior: + description: + - Whether to stop or terminate an instance upon shutdown. + choices: ['stop', 'terminate'] + type: str + tenancy: + description: + - What type of tenancy to allow an instance to use. Default is shared tenancy. Dedicated tenancy will incur additional charges. + choices: ['dedicated', 'default'] + type: str + termination_protection: + description: + - Whether to enable termination protection. + This module will not terminate an instance with termination protection active, it must be turned off first. + type: bool + cpu_credit_specification: + description: + - For T series instances, choose whether to allow increased charges to buy CPU credits if the default pool is depleted. + - Choose I(unlimited) to enable buying additional CPU credits. + choices: ['unlimited', 'standard'] + type: str + cpu_options: + description: + - Reduce the number of vCPU exposed to the instance. + - Those parameters can only be set at instance launch. The two suboptions threads_per_core and core_count are mandatory. + - See U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-optimize-cpu.html) for combinations available. + - Requires botocore >= 1.10.16 + type: dict + suboptions: + threads_per_core: + description: + - Select the number of threads per core to enable. Disable or Enable Intel HT. + choices: [1, 2] + required: true + type: int + core_count: + description: + - Set the number of core to enable. + required: true + type: int + detailed_monitoring: + description: + - Whether to allow detailed cloudwatch metrics to be collected, enabling more detailed alerting. + type: bool + ebs_optimized: + description: + - Whether instance is should use optimized EBS volumes, see U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSOptimized.html). + type: bool + filters: + description: + - A dict of filters to apply when deciding whether existing instances match and should be altered. Each dict item + consists of a filter key and a filter value. See + U(https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html). + for possible filters. Filter names and values are case sensitive. + - By default, instances are filtered for counting by their "Name" tag, base AMI, state (running, by default), and + subnet ID. Any queryable filter can be used. Good candidates are specific tags, SSH keys, or security groups. + type: dict + instance_role: + description: + - The ARN or name of an EC2-enabled instance role to be used. If a name is not provided in arn format + then the ListInstanceProfiles permission must also be granted. + U(https://docs.aws.amazon.com/IAM/latest/APIReference/API_ListInstanceProfiles.html) If no full ARN is provided, + the role with a matching name will be used from the active AWS account. + type: str + placement_group: + description: + - The placement group that needs to be assigned to the instance + type: 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: Terminate every running instance in a region. Use with EXTREME caution. + amazon.aws.ec2_instance: + state: absent + filters: + instance-state-name: running + +- name: restart a particular instance by its ID + amazon.aws.ec2_instance: + state: restarted + instance_ids: + - i-12345678 + +- name: start an instance with a public IP address + amazon.aws.ec2_instance: + name: "public-compute-instance" + key_name: "prod-ssh-key" + vpc_subnet_id: subnet-5ca1ab1e + instance_type: c5.large + security_group: default + network: + assign_public_ip: true + image_id: ami-123456 + tags: + Environment: Testing + +- name: start an instance and Add EBS + amazon.aws.ec2_instance: + name: "public-withebs-instance" + vpc_subnet_id: subnet-5ca1ab1e + instance_type: t2.micro + key_name: "prod-ssh-key" + security_group: default + volumes: + - device_name: /dev/sda1 + ebs: + volume_size: 16 + delete_on_termination: true + +- name: start an instance with a cpu_options + amazon.aws.ec2_instance: + name: "public-cpuoption-instance" + vpc_subnet_id: subnet-5ca1ab1e + tags: + Environment: Testing + instance_type: c4.large + volumes: + - device_name: /dev/sda1 + ebs: + delete_on_termination: true + cpu_options: + core_count: 1 + threads_per_core: 1 + +- name: start an instance and have it begin a Tower callback on boot + amazon.aws.ec2_instance: + name: "tower-callback-test" + key_name: "prod-ssh-key" + vpc_subnet_id: subnet-5ca1ab1e + security_group: default + tower_callback: + # IP or hostname of tower server + tower_address: 1.2.3.4 + job_template_id: 876 + host_config_key: '[secret config key goes here]' + network: + assign_public_ip: true + image_id: ami-123456 + cpu_credit_specification: unlimited + tags: + SomeThing: "A value" + +- name: start an instance with ENI (An existing ENI ID is required) + amazon.aws.ec2_instance: + name: "public-eni-instance" + key_name: "prod-ssh-key" + vpc_subnet_id: subnet-5ca1ab1e + network: + interfaces: + - id: "eni-12345" + tags: + Env: "eni_on" + volumes: + - device_name: /dev/sda1 + ebs: + delete_on_termination: true + instance_type: t2.micro + image_id: ami-123456 + +- name: add second ENI interface + amazon.aws.ec2_instance: + name: "public-eni-instance" + network: + interfaces: + - id: "eni-12345" + - id: "eni-67890" + image_id: ami-123456 + tags: + Env: "eni_on" + instance_type: t2.micro +''' + +RETURN = ''' +instances: + description: a list of ec2 instances + returned: when wait == true + type: complex + contains: + ami_launch_index: + description: The AMI launch index, which can be used to find this instance in the launch group. + returned: always + type: int + sample: 0 + architecture: + description: The architecture of the image + returned: always + type: str + sample: x86_64 + block_device_mappings: + description: Any block device mapping entries for the instance. + returned: always + type: complex + contains: + device_name: + description: The device name exposed to the instance (for example, /dev/sdh or xvdh). + returned: always + type: str + sample: /dev/sdh + ebs: + description: Parameters used to automatically set up EBS volumes when the instance is launched. + returned: always + type: complex + contains: + attach_time: + description: The time stamp when the attachment initiated. + returned: always + type: str + sample: "2017-03-23T22:51:24+00:00" + delete_on_termination: + description: Indicates whether the volume is deleted on instance termination. + returned: always + type: bool + sample: true + status: + description: The attachment state. + returned: always + type: str + sample: attached + volume_id: + description: The ID of the EBS volume + returned: always + type: str + sample: vol-12345678 + client_token: + description: The idempotency token you provided when you launched the instance, if applicable. + returned: always + type: str + sample: mytoken + ebs_optimized: + description: Indicates whether the instance is optimized for EBS I/O. + returned: always + type: bool + sample: false + hypervisor: + description: The hypervisor type of the instance. + returned: always + type: str + sample: xen + iam_instance_profile: + description: The IAM instance profile associated with the instance, if applicable. + returned: always + type: complex + contains: + arn: + description: The Amazon Resource Name (ARN) of the instance profile. + returned: always + type: str + sample: "arn:aws:iam::000012345678:instance-profile/myprofile" + id: + description: The ID of the instance profile + returned: always + type: str + sample: JFJ397FDG400FG9FD1N + image_id: + description: The ID of the AMI used to launch the instance. + returned: always + type: str + sample: ami-0011223344 + instance_id: + description: The ID of the instance. + returned: always + type: str + sample: i-012345678 + instance_type: + description: The instance type size of the running instance. + returned: always + type: str + sample: t2.micro + key_name: + description: The name of the key pair, if this instance was launched with an associated key pair. + returned: always + type: str + sample: my-key + launch_time: + description: The time the instance was launched. + returned: always + type: str + sample: "2017-03-23T22:51:24+00:00" + monitoring: + description: The monitoring for the instance. + returned: always + type: complex + contains: + state: + description: Indicates whether detailed monitoring is enabled. Otherwise, basic monitoring is enabled. + returned: always + type: str + sample: disabled + network_interfaces: + description: One or more network interfaces for the instance. + returned: always + type: complex + contains: + association: + description: The association information for an Elastic IPv4 associated with the network interface. + returned: always + type: complex + contains: + ip_owner_id: + description: The ID of the owner of the Elastic IP address. + returned: always + type: str + sample: amazon + public_dns_name: + description: The public DNS name. + returned: always + type: str + sample: "" + public_ip: + description: The public IP address or Elastic IP address bound to the network interface. + returned: always + type: str + sample: 1.2.3.4 + attachment: + description: The network interface attachment. + returned: always + type: complex + contains: + attach_time: + description: The time stamp when the attachment initiated. + returned: always + type: str + sample: "2017-03-23T22:51:24+00:00" + attachment_id: + description: The ID of the network interface attachment. + returned: always + type: str + sample: eni-attach-3aff3f + delete_on_termination: + description: Indicates whether the network interface is deleted when the instance is terminated. + returned: always + type: bool + sample: true + device_index: + description: The index of the device on the instance for the network interface attachment. + returned: always + type: int + sample: 0 + status: + description: The attachment state. + returned: always + type: str + sample: attached + description: + description: The description. + returned: always + type: str + sample: My interface + groups: + description: One or more security groups. + returned: always + type: list + elements: dict + contains: + group_id: + description: The ID of the security group. + returned: always + type: str + sample: sg-abcdef12 + group_name: + description: The name of the security group. + returned: always + type: str + sample: mygroup + ipv6_addresses: + description: One or more IPv6 addresses associated with the network interface. + returned: always + type: list + elements: dict + contains: + ipv6_address: + description: The IPv6 address. + returned: always + type: str + sample: "2001:0db8:85a3:0000:0000:8a2e:0370:7334" + mac_address: + description: The MAC address. + returned: always + type: str + sample: "00:11:22:33:44:55" + network_interface_id: + description: The ID of the network interface. + returned: always + type: str + sample: eni-01234567 + owner_id: + description: The AWS account ID of the owner of the network interface. + returned: always + type: str + sample: 01234567890 + private_ip_address: + description: The IPv4 address of the network interface within the subnet. + returned: always + type: str + sample: 10.0.0.1 + private_ip_addresses: + description: The private IPv4 addresses associated with the network interface. + returned: always + type: list + elements: dict + contains: + association: + description: The association information for an Elastic IP address (IPv4) associated with the network interface. + returned: always + type: complex + contains: + ip_owner_id: + description: The ID of the owner of the Elastic IP address. + returned: always + type: str + sample: amazon + public_dns_name: + description: The public DNS name. + returned: always + type: str + sample: "" + public_ip: + description: The public IP address or Elastic IP address bound to the network interface. + returned: always + type: str + sample: 1.2.3.4 + primary: + description: Indicates whether this IPv4 address is the primary private IP address of the network interface. + returned: always + type: bool + sample: true + private_ip_address: + description: The private IPv4 address of the network interface. + returned: always + type: str + sample: 10.0.0.1 + source_dest_check: + description: Indicates whether source/destination checking is enabled. + returned: always + type: bool + sample: true + status: + description: The status of the network interface. + returned: always + type: str + sample: in-use + subnet_id: + description: The ID of the subnet for the network interface. + returned: always + type: str + sample: subnet-0123456 + vpc_id: + description: The ID of the VPC for the network interface. + returned: always + type: str + sample: vpc-0123456 + placement: + description: The location where the instance launched, if applicable. + returned: always + type: complex + contains: + availability_zone: + description: The Availability Zone of the instance. + returned: always + type: str + sample: ap-southeast-2a + group_name: + description: The name of the placement group the instance is in (for cluster compute instances). + returned: always + type: str + sample: "" + tenancy: + description: The tenancy of the instance (if the instance is running in a VPC). + returned: always + type: str + sample: default + private_dns_name: + description: The private DNS name. + returned: always + type: str + sample: ip-10-0-0-1.ap-southeast-2.compute.internal + private_ip_address: + description: The IPv4 address of the network interface within the subnet. + returned: always + type: str + sample: 10.0.0.1 + product_codes: + description: One or more product codes. + returned: always + type: list + elements: dict + contains: + product_code_id: + description: The product code. + returned: always + type: str + sample: aw0evgkw8ef3n2498gndfgasdfsd5cce + product_code_type: + description: The type of product code. + returned: always + type: str + sample: marketplace + public_dns_name: + description: The public DNS name assigned to the instance. + returned: always + type: str + sample: + public_ip_address: + description: The public IPv4 address assigned to the instance + returned: always + type: str + sample: 52.0.0.1 + root_device_name: + description: The device name of the root device + returned: always + type: str + sample: /dev/sda1 + root_device_type: + description: The type of root device used by the AMI. + returned: always + type: str + sample: ebs + security_groups: + description: One or more security groups for the instance. + returned: always + type: list + elements: dict + contains: + group_id: + description: The ID of the security group. + returned: always + type: str + sample: sg-0123456 + group_name: + description: The name of the security group. + returned: always + type: str + sample: my-security-group + network.source_dest_check: + description: Indicates whether source/destination checking is enabled. + returned: always + type: bool + sample: true + state: + description: The current state of the instance. + returned: always + type: complex + contains: + code: + description: The low byte represents the state. + returned: always + type: int + sample: 16 + name: + description: The name of the state. + returned: always + type: str + sample: running + state_transition_reason: + description: The reason for the most recent state transition. + returned: always + type: str + sample: + subnet_id: + description: The ID of the subnet in which the instance is running. + returned: always + type: str + sample: subnet-00abcdef + tags: + description: Any tags assigned to the instance. + returned: always + type: dict + sample: + virtualization_type: + description: The type of virtualization of the AMI. + returned: always + type: str + sample: hvm + vpc_id: + description: The ID of the VPC the instance is in. + returned: always + type: dict + sample: vpc-0011223344 +''' + +from collections import namedtuple +import re +import string +import textwrap +import time +import uuid + +try: + import botocore +except ImportError: + pass # caught by AnsibleAWSModule + +from ansible.module_utils._text import to_native +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 +from ansible.module_utils.six import string_types +from ansible.module_utils.six.moves.urllib import parse as urlparse + +from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule +from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_code +from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_message +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ansible_dict_to_boto3_filter_list +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import ansible_dict_to_boto3_tag_list +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import boto3_tag_list_to_ansible_dict +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import compare_aws_tags +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import get_ec2_security_group_ids_from_names + +module = None + + +def tower_callback_script(tower_conf, windows=False, passwd=None): + script_url = 'https://raw.githubusercontent.com/ansible/ansible/devel/examples/scripts/ConfigureRemotingForAnsible.ps1' + if windows and passwd is not None: + script_tpl = """ + $admin = [adsi]("WinNT://./administrator, user") + $admin.PSBase.Invoke("SetPassword", "{PASS}") + Invoke-Expression ((New-Object System.Net.Webclient).DownloadString('{SCRIPT}')) + + """ + return to_native(textwrap.dedent(script_tpl).format(PASS=passwd, SCRIPT=script_url)) + elif windows and passwd is None: + script_tpl = """ + $admin = [adsi]("WinNT://./administrator, user") + Invoke-Expression ((New-Object System.Net.Webclient).DownloadString('{SCRIPT}')) + + """ + return to_native(textwrap.dedent(script_tpl).format(PASS=passwd, SCRIPT=script_url)) + elif not windows: + for p in ['tower_address', 'job_template_id', 'host_config_key']: + if p not in tower_conf: + module.fail_json(msg="Incomplete tower_callback configuration. tower_callback.{0} not set.".format(p)) + + if isinstance(tower_conf['job_template_id'], string_types): + tower_conf['job_template_id'] = urlparse.quote(tower_conf['job_template_id']) + tpl = string.Template(textwrap.dedent("""#!/bin/bash + set -x + + retry_attempts=10 + attempt=0 + while [[ $attempt -lt $retry_attempts ]] + do + status_code=`curl --max-time 10 -v -k -s -i \ + --data "host_config_key=${host_config_key}" \ + 'https://${tower_address}/api/v2/job_templates/${template_id}/callback/' \ + | head -n 1 \ + | awk '{print $2}'` + if [[ $status_code == 404 ]] + then + status_code=`curl --max-time 10 -v -k -s -i \ + --data "host_config_key=${host_config_key}" \ + 'https://${tower_address}/api/v1/job_templates/${template_id}/callback/' \ + | head -n 1 \ + | awk '{print $2}'` + # fall back to using V1 API for Tower 3.1 and below, since v2 API will always 404 + fi + if [[ $status_code == 201 ]] + then + exit 0 + fi + attempt=$(( attempt + 1 )) + echo "$${status_code} received... retrying in 1 minute. (Attempt $${attempt})" + sleep 60 + done + exit 1 + """)) + return tpl.safe_substitute(tower_address=tower_conf['tower_address'], + template_id=tower_conf['job_template_id'], + host_config_key=tower_conf['host_config_key']) + raise NotImplementedError("Only windows with remote-prep or non-windows with tower job callback supported so far.") + + +def manage_tags(match, new_tags, purge_tags, ec2): + changed = False + old_tags = boto3_tag_list_to_ansible_dict(match.get('Tags', {})) + tags_to_set, tags_to_delete = compare_aws_tags( + old_tags, new_tags, + purge_tags=purge_tags, + ) + if module.check_mode: + return bool(tags_to_delete or tags_to_set) + try: + if tags_to_set: + ec2.create_tags( + aws_retry=True, + Resources=[match['InstanceId']], + Tags=ansible_dict_to_boto3_tag_list(tags_to_set)) + changed |= True + if tags_to_delete: + delete_with_current_values = dict((k, old_tags.get(k)) for k in tags_to_delete) + ec2.delete_tags( + aws_retry=True, + Resources=[match['InstanceId']], + Tags=ansible_dict_to_boto3_tag_list(delete_with_current_values)) + changed |= True + except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: + module.fail_json_aws(e, msg="Could not update tags for instance {0}".format(match['InstanceId'])) + return changed + + +def build_volume_spec(params): + volumes = params.get('volumes') or [] + for volume in volumes: + if 'ebs' in volume: + for int_value in ['volume_size', 'iops']: + if int_value in volume['ebs']: + volume['ebs'][int_value] = int(volume['ebs'][int_value]) + return [snake_dict_to_camel_dict(v, capitalize_first=True) for v in volumes] + + +def add_or_update_instance_profile(instance, desired_profile_name, ec2): + instance_profile_setting = instance.get('IamInstanceProfile') + if instance_profile_setting and desired_profile_name: + if desired_profile_name in (instance_profile_setting.get('Name'), instance_profile_setting.get('Arn')): + # great, the profile we asked for is what's there + return False + else: + desired_arn = determine_iam_role(desired_profile_name) + if instance_profile_setting.get('Arn') == desired_arn: + return False + + # update association + try: + association = ec2.describe_iam_instance_profile_associations( + aws_retry=True, + Filters=[{'Name': 'instance-id', 'Values': [instance['InstanceId']]}]) + except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: + # check for InvalidAssociationID.NotFound + module.fail_json_aws(e, "Could not find instance profile association") + try: + resp = ec2.replace_iam_instance_profile_association( + aws_retry=True, + AssociationId=association['IamInstanceProfileAssociations'][0]['AssociationId'], + IamInstanceProfile={'Arn': determine_iam_role(desired_profile_name)} + ) + return True + except botocore.exceptions.ClientError as e: + module.fail_json_aws(e, "Could not associate instance profile") + + if not instance_profile_setting and desired_profile_name: + # create association + try: + resp = ec2.associate_iam_instance_profile( + aws_retry=True, + IamInstanceProfile={'Arn': determine_iam_role(desired_profile_name)}, + InstanceId=instance['InstanceId'] + ) + return True + except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: + module.fail_json_aws(e, "Could not associate new instance profile") + + return False + + +def build_network_spec(params, ec2): + """ + Returns list of interfaces [complex] + Interface type: { + 'AssociatePublicIpAddress': True|False, + 'DeleteOnTermination': True|False, + 'Description': 'string', + 'DeviceIndex': 123, + 'Groups': [ + 'string', + ], + 'Ipv6AddressCount': 123, + 'Ipv6Addresses': [ + { + 'Ipv6Address': 'string' + }, + ], + 'NetworkInterfaceId': 'string', + 'PrivateIpAddress': 'string', + 'PrivateIpAddresses': [ + { + 'Primary': True|False, + 'PrivateIpAddress': 'string' + }, + ], + 'SecondaryPrivateIpAddressCount': 123, + 'SubnetId': 'string' + }, + """ + + interfaces = [] + network = params.get('network') or {} + if not network.get('interfaces'): + # they only specified one interface + spec = { + 'DeviceIndex': 0, + } + if network.get('assign_public_ip') is not None: + spec['AssociatePublicIpAddress'] = network['assign_public_ip'] + + if params.get('vpc_subnet_id'): + spec['SubnetId'] = params['vpc_subnet_id'] + else: + default_vpc = get_default_vpc(ec2) + if default_vpc is None: + raise module.fail_json( + msg="No default subnet could be found - you must include a VPC subnet ID (vpc_subnet_id parameter) to create an instance") + else: + sub = get_default_subnet(ec2, default_vpc) + spec['SubnetId'] = sub['SubnetId'] + + if network.get('private_ip_address'): + spec['PrivateIpAddress'] = network['private_ip_address'] + + if params.get('security_group') or params.get('security_groups'): + groups = discover_security_groups( + group=params.get('security_group'), + groups=params.get('security_groups'), + subnet_id=spec['SubnetId'], + ec2=ec2 + ) + spec['Groups'] = groups + if network.get('description') is not None: + spec['Description'] = network['description'] + # TODO more special snowflake network things + + return [spec] + + # handle list of `network.interfaces` options + for idx, interface_params in enumerate(network.get('interfaces', [])): + spec = { + 'DeviceIndex': idx, + } + + if isinstance(interface_params, string_types): + # naive case where user gave + # network_interfaces: [eni-1234, eni-4567, ....] + # put into normal data structure so we don't dupe code + interface_params = {'id': interface_params} + + if interface_params.get('id') is not None: + # if an ID is provided, we don't want to set any other parameters. + spec['NetworkInterfaceId'] = interface_params['id'] + interfaces.append(spec) + continue + + spec['DeleteOnTermination'] = interface_params.get('delete_on_termination', True) + + if interface_params.get('ipv6_addresses'): + spec['Ipv6Addresses'] = [{'Ipv6Address': a} for a in interface_params.get('ipv6_addresses', [])] + + if interface_params.get('private_ip_address'): + spec['PrivateIpAddress'] = interface_params.get('private_ip_address') + + if interface_params.get('description'): + spec['Description'] = interface_params.get('description') + + if interface_params.get('subnet_id', params.get('vpc_subnet_id')): + spec['SubnetId'] = interface_params.get('subnet_id', params.get('vpc_subnet_id')) + elif not spec.get('SubnetId') and not interface_params['id']: + # TODO grab a subnet from default VPC + raise ValueError('Failed to assign subnet to interface {0}'.format(interface_params)) + + interfaces.append(spec) + return interfaces + + +def warn_if_public_ip_assignment_changed(instance): + # This is a non-modifiable attribute. + assign_public_ip = (module.params.get('network') or {}).get('assign_public_ip') + if assign_public_ip is None: + return + + # Check that public ip assignment is the same and warn if not + public_dns_name = instance.get('PublicDnsName') + if (public_dns_name and not assign_public_ip) or (assign_public_ip and not public_dns_name): + module.warn( + "Unable to modify public ip assignment to {0} for instance {1}. " + "Whether or not to assign a public IP is determined during instance creation.".format( + assign_public_ip, instance['InstanceId'])) + + +def warn_if_cpu_options_changed(instance): + # This is a non-modifiable attribute. + cpu_options = module.params.get('cpu_options') + if cpu_options is None: + return + + # Check that the CpuOptions set are the same and warn if not + core_count_curr = instance['CpuOptions'].get('CoreCount') + core_count = cpu_options.get('core_count') + threads_per_core_curr = instance['CpuOptions'].get('ThreadsPerCore') + threads_per_core = cpu_options.get('threads_per_core') + if core_count_curr != core_count: + module.warn( + "Unable to modify core_count from {0} to {1}. " + "Assigning a number of core is determinted during instance creation".format( + core_count_curr, core_count)) + + if threads_per_core_curr != threads_per_core: + module.warn( + "Unable to modify threads_per_core from {0} to {1}. " + "Assigning a number of threads per core is determined during instance creation.".format( + threads_per_core_curr, threads_per_core)) + + +def discover_security_groups(group, groups, parent_vpc_id=None, subnet_id=None, ec2=None): + + if subnet_id is not None: + try: + sub = ec2.describe_subnets(aws_retry=True, SubnetIds=[subnet_id]) + except is_boto3_error_code('InvalidGroup.NotFound'): + module.fail_json( + "Could not find subnet {0} to associate security groups. Please check the vpc_subnet_id and security_groups parameters.".format( + subnet_id + ) + ) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, msg="Error while searching for subnet {0} parent VPC.".format(subnet_id)) + parent_vpc_id = sub['Subnets'][0]['VpcId'] + + if group: + return get_ec2_security_group_ids_from_names(group, ec2, vpc_id=parent_vpc_id) + if groups: + return get_ec2_security_group_ids_from_names(groups, ec2, vpc_id=parent_vpc_id) + return [] + + +def build_top_level_options(params): + spec = {} + if params.get('image_id'): + spec['ImageId'] = params['image_id'] + elif isinstance(params.get('image'), dict): + image = params.get('image', {}) + spec['ImageId'] = image.get('id') + if 'ramdisk' in image: + spec['RamdiskId'] = image['ramdisk'] + if 'kernel' in image: + spec['KernelId'] = image['kernel'] + if not spec.get('ImageId') and not params.get('launch_template'): + module.fail_json(msg="You must include an image_id or image.id parameter to create an instance, or use a launch_template.") + + if params.get('key_name') is not None: + spec['KeyName'] = params.get('key_name') + if params.get('user_data') is not None: + spec['UserData'] = to_native(params.get('user_data')) + elif params.get('tower_callback') is not None: + spec['UserData'] = tower_callback_script( + tower_conf=params.get('tower_callback'), + windows=params.get('tower_callback').get('windows', False), + passwd=params.get('tower_callback').get('set_password'), + ) + + if params.get('launch_template') is not None: + spec['LaunchTemplate'] = {} + if not params.get('launch_template').get('id') or params.get('launch_template').get('name'): + module.fail_json(msg="Could not create instance with launch template. Either launch_template.name or launch_template.id parameters are required") + + if params.get('launch_template').get('id') is not None: + spec['LaunchTemplate']['LaunchTemplateId'] = params.get('launch_template').get('id') + if params.get('launch_template').get('name') is not None: + spec['LaunchTemplate']['LaunchTemplateName'] = params.get('launch_template').get('name') + if params.get('launch_template').get('version') is not None: + spec['LaunchTemplate']['Version'] = to_native(params.get('launch_template').get('version')) + + if params.get('detailed_monitoring', False): + spec['Monitoring'] = {'Enabled': True} + if params.get('cpu_credit_specification') is not None: + spec['CreditSpecification'] = {'CpuCredits': params.get('cpu_credit_specification')} + if params.get('tenancy') is not None: + spec['Placement'] = {'Tenancy': params.get('tenancy')} + if params.get('placement_group'): + if 'Placement' in spec: + spec['Placement']['GroupName'] = str(params.get('placement_group')) + else: + spec.setdefault('Placement', {'GroupName': str(params.get('placement_group'))}) + if params.get('ebs_optimized') is not None: + spec['EbsOptimized'] = params.get('ebs_optimized') + if params.get('instance_initiated_shutdown_behavior'): + spec['InstanceInitiatedShutdownBehavior'] = params.get('instance_initiated_shutdown_behavior') + if params.get('termination_protection') is not None: + spec['DisableApiTermination'] = params.get('termination_protection') + if params.get('cpu_options') is not None: + spec['CpuOptions'] = {} + spec['CpuOptions']['ThreadsPerCore'] = params.get('cpu_options').get('threads_per_core') + spec['CpuOptions']['CoreCount'] = params.get('cpu_options').get('core_count') + return spec + + +def build_instance_tags(params, propagate_tags_to_volumes=True): + tags = params.get('tags', {}) + if params.get('name') is not None: + if tags is None: + tags = {} + tags['Name'] = params.get('name') + return [ + { + 'ResourceType': 'volume', + 'Tags': ansible_dict_to_boto3_tag_list(tags), + }, + { + 'ResourceType': 'instance', + 'Tags': ansible_dict_to_boto3_tag_list(tags), + }, + ] + + +def build_run_instance_spec(params, ec2): + + spec = dict( + ClientToken=uuid.uuid4().hex, + MaxCount=1, + MinCount=1, + ) + # network parameters + spec['NetworkInterfaces'] = build_network_spec(params, ec2) + spec['BlockDeviceMappings'] = build_volume_spec(params) + spec.update(**build_top_level_options(params)) + spec['TagSpecifications'] = build_instance_tags(params) + + # IAM profile + if params.get('instance_role'): + spec['IamInstanceProfile'] = dict(Arn=determine_iam_role(params.get('instance_role'))) + + spec['InstanceType'] = params['instance_type'] + return spec + + +def await_instances(ids, state='OK'): + if not module.params.get('wait', True): + # the user asked not to wait for anything + return + + if module.check_mode: + # In check mode, there is no change even if you wait. + return + + state_opts = { + 'OK': 'instance_status_ok', + 'STOPPED': 'instance_stopped', + 'TERMINATED': 'instance_terminated', + 'EXISTS': 'instance_exists', + 'RUNNING': 'instance_running', + } + if state not in state_opts: + module.fail_json(msg="Cannot wait for state {0}, invalid state".format(state)) + waiter = module.client('ec2', retry_decorator=AWSRetry.jittered_backoff()).get_waiter(state_opts[state]) + try: + waiter.wait( + InstanceIds=ids, + WaiterConfig={ + 'Delay': 15, + 'MaxAttempts': module.params.get('wait_timeout', 600) // 15, + } + ) + except botocore.exceptions.WaiterConfigError as e: + module.fail_json(msg="{0}. Error waiting for instances {1} to reach state {2}".format( + to_native(e), ', '.join(ids), state)) + except botocore.exceptions.WaiterError as e: + module.warn("Instances {0} took too long to reach state {1}. {2}".format( + ', '.join(ids), state, to_native(e))) + + +def diff_instance_and_params(instance, params, ec2, skip=None): + """boto3 instance obj, module params""" + + if skip is None: + skip = [] + + changes_to_apply = [] + id_ = instance['InstanceId'] + + ParamMapper = namedtuple('ParamMapper', ['param_key', 'instance_key', 'attribute_name', 'add_value']) + + def value_wrapper(v): + return {'Value': v} + + param_mappings = [ + ParamMapper('ebs_optimized', 'EbsOptimized', 'ebsOptimized', value_wrapper), + ParamMapper('termination_protection', 'DisableApiTermination', 'disableApiTermination', value_wrapper), + # user data is an immutable property + # ParamMapper('user_data', 'UserData', 'userData', value_wrapper), + ] + + for mapping in param_mappings: + if params.get(mapping.param_key) is None: + continue + if mapping.instance_key in skip: + continue + + try: + value = ec2.describe_instance_attribute(aws_retry=True, Attribute=mapping.attribute_name, InstanceId=id_) + except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: + module.fail_json_aws(e, msg="Could not describe attribute {0} for instance {1}".format(mapping.attribute_name, id_)) + if value[mapping.instance_key]['Value'] != params.get(mapping.param_key): + arguments = dict( + InstanceId=instance['InstanceId'], + # Attribute=mapping.attribute_name, + ) + arguments[mapping.instance_key] = mapping.add_value(params.get(mapping.param_key)) + changes_to_apply.append(arguments) + + if params.get('security_group') or params.get('security_groups'): + try: + value = ec2.describe_instance_attribute(aws_retry=True, Attribute="groupSet", InstanceId=id_) + except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: + module.fail_json_aws(e, msg="Could not describe attribute groupSet for instance {0}".format(id_)) + # managing security groups + if params.get('vpc_subnet_id'): + subnet_id = params.get('vpc_subnet_id') + else: + default_vpc = get_default_vpc(ec2) + if default_vpc is None: + module.fail_json( + msg="No default subnet could be found - you must include a VPC subnet ID (vpc_subnet_id parameter) to modify security groups.") + else: + sub = get_default_subnet(ec2, default_vpc) + subnet_id = sub['SubnetId'] + + groups = discover_security_groups( + group=params.get('security_group'), + groups=params.get('security_groups'), + subnet_id=subnet_id, + ec2=ec2 + ) + expected_groups = groups + instance_groups = [g['GroupId'] for g in value['Groups']] + if set(instance_groups) != set(expected_groups): + changes_to_apply.append(dict( + Groups=expected_groups, + InstanceId=instance['InstanceId'] + )) + + if (params.get('network') or {}).get('source_dest_check') is not None: + # network.source_dest_check is nested, so needs to be treated separately + check = bool(params.get('network').get('source_dest_check')) + if instance['SourceDestCheck'] != check: + changes_to_apply.append(dict( + InstanceId=instance['InstanceId'], + SourceDestCheck={'Value': check}, + )) + + return changes_to_apply + + +def change_network_attachments(instance, params, ec2): + if (params.get('network') or {}).get('interfaces') is not None: + new_ids = [] + for inty in params.get('network').get('interfaces'): + if isinstance(inty, dict) and 'id' in inty: + new_ids.append(inty['id']) + elif isinstance(inty, string_types): + new_ids.append(inty) + # network.interfaces can create the need to attach new interfaces + old_ids = [inty['NetworkInterfaceId'] for inty in instance['NetworkInterfaces']] + to_attach = set(new_ids) - set(old_ids) + for eni_id in to_attach: + try: + ec2.attach_network_interface( + aws_retry=True, + DeviceIndex=new_ids.index(eni_id), + InstanceId=instance['InstanceId'], + NetworkInterfaceId=eni_id, + ) + except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: + module.fail_json_aws(e, msg="Could not attach interface {0} to instance {1}".format(eni_id, instance['InstanceId'])) + return bool(len(to_attach)) + return False + + +@AWSRetry.jittered_backoff() +def find_instances(ec2, ids=None, filters=None): + paginator = ec2.get_paginator('describe_instances') + if ids: + params = dict(InstanceIds=ids) + elif filters is None: + module.fail_json(msg="No filters provided when they were required") + else: + for key in list(filters.keys()): + if not key.startswith("tag:"): + filters[key.replace("_", "-")] = filters.pop(key) + params = dict(Filters=ansible_dict_to_boto3_filter_list(filters)) + + try: + results = _describe_instances(ec2, **params) + except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: + module.fail_json_aws(e, msg="Could not describe instances") + return list(results) + + +@AWSRetry.jittered_backoff() +def _describe_instances(ec2, **params): + paginator = ec2.get_paginator('describe_instances') + return paginator.paginate(**params).search('Reservations[].Instances[]') + + +def get_default_vpc(ec2): + try: + vpcs = ec2.describe_vpcs( + aws_retry=True, + Filters=ansible_dict_to_boto3_filter_list({'isDefault': 'true'})) + except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: + module.fail_json_aws(e, msg="Could not describe default VPC") + if len(vpcs.get('Vpcs', [])): + return vpcs.get('Vpcs')[0] + return None + + +def get_default_subnet(ec2, vpc, availability_zone=None): + try: + subnets = ec2.describe_subnets( + aws_retry=True, + Filters=ansible_dict_to_boto3_filter_list({ + 'vpc-id': vpc['VpcId'], + 'state': 'available', + 'default-for-az': 'true', + }) + ) + except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: + module.fail_json_aws(e, msg="Could not describe default subnets for VPC {0}".format(vpc['VpcId'])) + if len(subnets.get('Subnets', [])): + if availability_zone is not None: + subs_by_az = dict((subnet['AvailabilityZone'], subnet) for subnet in subnets.get('Subnets')) + if availability_zone in subs_by_az: + return subs_by_az[availability_zone] + + # to have a deterministic sorting order, we sort by AZ so we'll always pick the `a` subnet first + # there can only be one default-for-az subnet per AZ, so the AZ key is always unique in this list + by_az = sorted(subnets.get('Subnets'), key=lambda s: s['AvailabilityZone']) + return by_az[0] + return None + + +def ensure_instance_state(state, ec2): + if state in ('running', 'started'): + changed, failed, instances, failure_reason = change_instance_state(filters=module.params.get('filters'), desired_state='RUNNING', ec2=ec2) + + if failed: + module.fail_json( + msg="Unable to start instances: {0}".format(failure_reason), + reboot_success=list(changed), + reboot_failed=failed) + + module.exit_json( + msg='Instances started', + reboot_success=list(changed), + changed=bool(len(changed)), + reboot_failed=[], + instances=[pretty_instance(i) for i in instances], + ) + elif state in ('restarted', 'rebooted'): + changed, failed, instances, failure_reason = change_instance_state( + filters=module.params.get('filters'), + desired_state='STOPPED', + ec2=ec2) + changed, failed, instances, failure_reason = change_instance_state( + filters=module.params.get('filters'), + desired_state='RUNNING', + ec2=ec2) + + if failed: + module.fail_json( + msg="Unable to restart instances: {0}".format(failure_reason), + reboot_success=list(changed), + reboot_failed=failed) + + module.exit_json( + msg='Instances restarted', + reboot_success=list(changed), + changed=bool(len(changed)), + reboot_failed=[], + instances=[pretty_instance(i) for i in instances], + ) + elif state in ('stopped',): + changed, failed, instances, failure_reason = change_instance_state( + filters=module.params.get('filters'), + desired_state='STOPPED', + ec2=ec2) + + if failed: + module.fail_json( + msg="Unable to stop instances: {0}".format(failure_reason), + stop_success=list(changed), + stop_failed=failed) + + module.exit_json( + msg='Instances stopped', + stop_success=list(changed), + changed=bool(len(changed)), + stop_failed=[], + instances=[pretty_instance(i) for i in instances], + ) + elif state in ('absent', 'terminated'): + terminated, terminate_failed, instances, failure_reason = change_instance_state( + filters=module.params.get('filters'), + desired_state='TERMINATED', + ec2=ec2) + + if terminate_failed: + module.fail_json( + msg="Unable to terminate instances: {0}".format(failure_reason), + terminate_success=list(terminated), + terminate_failed=terminate_failed) + module.exit_json( + msg='Instances terminated', + terminate_success=list(terminated), + changed=bool(len(terminated)), + terminate_failed=[], + instances=[pretty_instance(i) for i in instances], + ) + + +def change_instance_state(filters, desired_state, ec2): + """Takes STOPPED/RUNNING/TERMINATED""" + + changed = set() + instances = find_instances(ec2, filters=filters) + to_change = set(i['InstanceId'] for i in instances if i['State']['Name'].upper() != desired_state) + unchanged = set() + failure_reason = "" + + # TODO: better check_moding in here https://github.com/ansible-collections/community.aws/issues/16 + for inst in instances: + try: + if desired_state == 'TERMINATED': + if module.check_mode: + changed.add(inst['InstanceId']) + continue + + # TODO use a client-token to prevent double-sends of these start/stop/terminate commands + # https://docs.aws.amazon.com/AWSEC2/latest/APIReference/Run_Instance_Idempotency.html + resp = ec2.terminate_instances(aws_retry=True, InstanceIds=[inst['InstanceId']]) + [changed.add(i['InstanceId']) for i in resp['TerminatingInstances']] + if desired_state == 'STOPPED': + if inst['State']['Name'] in ('stopping', 'stopped'): + unchanged.add(inst['InstanceId']) + continue + + if module.check_mode: + changed.add(inst['InstanceId']) + continue + + resp = ec2.stop_instances(aws_retry=True, InstanceIds=[inst['InstanceId']]) + [changed.add(i['InstanceId']) for i in resp['StoppingInstances']] + if desired_state == 'RUNNING': + if module.check_mode: + changed.add(inst['InstanceId']) + continue + + resp = ec2.start_instances(aws_retry=True, InstanceIds=[inst['InstanceId']]) + [changed.add(i['InstanceId']) for i in resp['StartingInstances']] + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + try: + failure_reason = to_native(e.message) + except AttributeError: + failure_reason = to_native(e) + + if changed: + await_instances(ids=list(changed) + list(unchanged), state=desired_state) + + change_failed = list(to_change - changed) + + if instances: + instances = find_instances(ec2, ids=list(i['InstanceId'] for i in instances)) + return changed, change_failed, instances, failure_reason + + +def pretty_instance(i): + instance = camel_dict_to_snake_dict(i, ignore_list=['Tags']) + instance['tags'] = boto3_tag_list_to_ansible_dict(i.get('Tags', {})) + return instance + + +def determine_iam_role(name_or_arn): + if re.match(r'^arn:aws:iam::\d+:instance-profile/[\w+=/,.@-]+$', name_or_arn): + return name_or_arn + iam = module.client('iam', retry_decorator=AWSRetry.jittered_backoff()) + try: + role = iam.get_instance_profile(InstanceProfileName=name_or_arn, aws_retry=True) + return role['InstanceProfile']['Arn'] + except is_boto3_error_code('NoSuchEntity') as e: + module.fail_json_aws(e, msg="Could not find instance_role {0}".format(name_or_arn)) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, msg="An error occurred while searching for instance_role {0}. Please try supplying the full ARN.".format(name_or_arn)) + + +def handle_existing(existing_matches, changed, ec2, state): + if state in ('running', 'started') and [i for i in existing_matches if i['State']['Name'] != 'running']: + ins_changed, failed, instances, failure_reason = change_instance_state(filters=module.params.get('filters'), desired_state='RUNNING', ec2=ec2) + if failed: + module.fail_json(msg="Couldn't start instances: {0}. Failure reason: {1}".format(instances, failure_reason)) + module.exit_json( + changed=bool(len(ins_changed)) or changed, + instances=[pretty_instance(i) for i in instances], + instance_ids=[i['InstanceId'] for i in instances], + ) + changes = diff_instance_and_params(existing_matches[0], module.params, ec2) + for c in changes: + if not module.check_mode: + try: + ec2.modify_instance_attribute(aws_retry=True, **c) + except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: + module.fail_json_aws(e, msg="Could not apply change {0} to existing instance.".format(str(c))) + changed |= bool(changes) + changed |= add_or_update_instance_profile(existing_matches[0], module.params.get('instance_role'), ec2) + changed |= change_network_attachments(existing_matches[0], module.params, ec2) + altered = find_instances(ec2, ids=[i['InstanceId'] for i in existing_matches]) + module.exit_json( + changed=bool(len(changes)) or changed, + instances=[pretty_instance(i) for i in altered], + instance_ids=[i['InstanceId'] for i in altered], + changes=changes, + ) + + +def ensure_present(existing_matches, changed, ec2, state): + if len(existing_matches): + try: + handle_existing(existing_matches, changed, ec2, state) + except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: + module.fail_json_aws( + e, msg="Failed to handle existing instances {0}".format(', '.join([i['InstanceId'] for i in existing_matches])), + # instances=[pretty_instance(i) for i in existing_matches], + # instance_ids=[i['InstanceId'] for i in existing_matches], + ) + try: + instance_spec = build_run_instance_spec(module.params, ec2) + # If check mode is enabled,suspend 'ensure function'. + if module.check_mode: + module.exit_json( + changed=True, + spec=instance_spec, + ) + instance_response = run_instances(ec2, **instance_spec) + instances = instance_response['Instances'] + instance_ids = [i['InstanceId'] for i in instances] + + for ins in instances: + # Wait for instances to exist (don't check state) + try: + AWSRetry.jittered_backoff( + catch_extra_error_codes=['InvalidInstanceID.NotFound'], + )( + ec2.describe_instance_status + )( + InstanceIds=[ins['InstanceId']], + IncludeAllInstances=True, + ) + except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: + module.fail_json_aws(e, msg="Failed to fetch status of new EC2 instance") + changes = diff_instance_and_params(ins, module.params, ec2, skip=['UserData', 'EbsOptimized']) + for c in changes: + try: + ec2.modify_instance_attribute(aws_retry=True, **c) + except botocore.exceptions.ClientError as e: + module.fail_json_aws(e, msg="Could not apply change {0} to new instance.".format(str(c))) + + if not module.params.get('wait'): + module.exit_json( + changed=True, + instance_ids=instance_ids, + spec=instance_spec, + ) + await_instances(instance_ids) + instances = find_instances(ec2, ids=instance_ids) + + module.exit_json( + changed=True, + instances=[pretty_instance(i) for i in instances], + instance_ids=instance_ids, + spec=instance_spec, + ) + except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: + module.fail_json_aws(e, msg="Failed to create new EC2 instance") + + +def run_instances(ec2, **instance_spec): + try: + return ec2.run_instances(**instance_spec) + except is_boto3_error_message('Invalid IAM Instance Profile ARN'): + # If the instance profile has just been created, it takes some time to be visible by ec2 + # So we wait 10 second and retry the run_instances + time.sleep(10) + return ec2.run_instances(**instance_spec) + + +def main(): + global module + argument_spec = dict( + state=dict(default='present', choices=['present', 'started', 'running', 'stopped', 'restarted', 'rebooted', 'terminated', 'absent']), + wait=dict(default=True, type='bool'), + wait_timeout=dict(default=600, type='int'), + # count=dict(default=1, type='int'), + image=dict(type='dict'), + image_id=dict(type='str'), + instance_type=dict(default='t2.micro', type='str'), + user_data=dict(type='str'), + tower_callback=dict(type='dict'), + ebs_optimized=dict(type='bool'), + vpc_subnet_id=dict(type='str', aliases=['subnet_id']), + availability_zone=dict(type='str'), + security_groups=dict(default=[], type='list', elements='str'), + security_group=dict(type='str'), + instance_role=dict(type='str'), + name=dict(type='str'), + tags=dict(type='dict'), + purge_tags=dict(type='bool', default=False), + filters=dict(type='dict', default=None), + launch_template=dict(type='dict'), + key_name=dict(type='str'), + cpu_credit_specification=dict(type='str', choices=['standard', 'unlimited']), + cpu_options=dict(type='dict', options=dict( + core_count=dict(type='int', required=True), + threads_per_core=dict(type='int', choices=[1, 2], required=True) + )), + tenancy=dict(type='str', choices=['dedicated', 'default']), + placement_group=dict(type='str'), + instance_initiated_shutdown_behavior=dict(type='str', choices=['stop', 'terminate']), + termination_protection=dict(type='bool'), + detailed_monitoring=dict(type='bool'), + instance_ids=dict(default=[], type='list', elements='str'), + network=dict(default=None, type='dict'), + volumes=dict(default=None, type='list', elements='dict'), + ) + # running/present are synonyms + # as are terminated/absent + module = AnsibleAWSModule( + argument_spec=argument_spec, + mutually_exclusive=[ + ['security_groups', 'security_group'], + ['availability_zone', 'vpc_subnet_id'], + ['tower_callback', 'user_data'], + ['image_id', 'image'], + ], + supports_check_mode=True + ) + + if module.params.get('network'): + if module.params.get('network').get('interfaces'): + if module.params.get('security_group'): + module.fail_json(msg="Parameter network.interfaces can't be used with security_group") + if module.params.get('security_groups'): + module.fail_json(msg="Parameter network.interfaces can't be used with security_groups") + + state = module.params.get('state') + ec2 = module.client('ec2', retry_decorator=AWSRetry.jittered_backoff()) + if module.params.get('filters') is None: + filters = { + # all states except shutting-down and terminated + 'instance-state-name': ['pending', 'running', 'stopping', 'stopped'] + } + if state == 'stopped': + # only need to change instances that aren't already stopped + filters['instance-state-name'] = ['stopping', 'pending', 'running'] + + if isinstance(module.params.get('instance_ids'), string_types): + filters['instance-id'] = [module.params.get('instance_ids')] + elif isinstance(module.params.get('instance_ids'), list) and len(module.params.get('instance_ids')): + filters['instance-id'] = module.params.get('instance_ids') + else: + if not module.params.get('vpc_subnet_id'): + if module.params.get('network'): + # grab AZ from one of the ENIs + ints = module.params.get('network').get('interfaces') + if ints: + filters['network-interface.network-interface-id'] = [] + for i in ints: + if isinstance(i, dict): + i = i['id'] + filters['network-interface.network-interface-id'].append(i) + else: + sub = get_default_subnet(ec2, get_default_vpc(ec2), availability_zone=module.params.get('availability_zone')) + filters['subnet-id'] = sub['SubnetId'] + else: + filters['subnet-id'] = [module.params.get('vpc_subnet_id')] + + if module.params.get('name'): + filters['tag:Name'] = [module.params.get('name')] + + if module.params.get('image_id'): + filters['image-id'] = [module.params.get('image_id')] + elif (module.params.get('image') or {}).get('id'): + filters['image-id'] = [module.params.get('image', {}).get('id')] + + module.params['filters'] = filters + + if module.params.get('cpu_options') and not module.botocore_at_least('1.10.16'): + module.fail_json(msg="cpu_options is only supported with botocore >= 1.10.16") + + existing_matches = find_instances(ec2, filters=module.params.get('filters')) + changed = False + + if state not in ('terminated', 'absent') and existing_matches: + for match in existing_matches: + warn_if_public_ip_assignment_changed(match) + warn_if_cpu_options_changed(match) + tags = module.params.get('tags') or {} + name = module.params.get('name') + if name: + tags['Name'] = name + changed |= manage_tags(match, tags, module.params.get('purge_tags', False), ec2) + + if state in ('present', 'running', 'started'): + ensure_present(existing_matches=existing_matches, changed=changed, ec2=ec2, state=state) + elif state in ('restarted', 'rebooted', 'stopped', 'absent', 'terminated'): + if existing_matches: + ensure_instance_state(state, ec2) + else: + module.exit_json( + msg='No matching instances found', + changed=False, + instances=[], + ) + else: + module.fail_json(msg="We don't handle the state {0}".format(state)) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ec2_instance_facts.py b/plugins/modules/ec2_instance_facts.py new file mode 120000 index 00000000000..7010fdcb95f --- /dev/null +++ b/plugins/modules/ec2_instance_facts.py @@ -0,0 +1 @@ +ec2_instance_info.py \ No newline at end of file diff --git a/plugins/modules/ec2_instance_info.py b/plugins/modules/ec2_instance_info.py new file mode 100644 index 00000000000..ff6fca00075 --- /dev/null +++ b/plugins/modules/ec2_instance_info.py @@ -0,0 +1,591 @@ +#!/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 = r''' +--- +module: ec2_instance_info +version_added: 1.0.0 +short_description: Gather information about ec2 instances in AWS +description: + - Gather information about ec2 instances in AWS + - This module was called C(ec2_instance_facts) before Ansible 2.9. The usage did not change. +author: + - Michael Schuett (@michaeljs1990) + - Rob White (@wimnat) +requirements: [ "boto3", "botocore" ] +options: + instance_ids: + description: + - If you specify one or more instance IDs, only instances that have the specified IDs are returned. + required: false + type: list + elements: str + filters: + description: + - A dict of filters to apply. Each dict item consists of a filter key and a filter value. See + U(https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html) for possible filters. Filter + names and values are case sensitive. + required: false + default: {} + type: dict + minimum_uptime: + description: + - Minimum running uptime in minutes of instances. For example if I(uptime) is C(60) return all instances that have run more than 60 minutes. + required: false + aliases: ['uptime'] + type: int + + +extends_documentation_fragment: +- amazon.aws.aws +- amazon.aws.ec2 + +''' + +EXAMPLES = r''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +- name: Gather information about all instances + amazon.aws.ec2_instance_info: + +- name: Gather information about all instances in AZ ap-southeast-2a + amazon.aws.ec2_instance_info: + filters: + availability-zone: ap-southeast-2a + +- name: Gather information about a particular instance using ID + amazon.aws.ec2_instance_info: + instance_ids: + - i-12345678 + +- name: Gather information about any instance with a tag key Name and value Example + amazon.aws.ec2_instance_info: + filters: + "tag:Name": Example + +- name: Gather information about any instance in states "shutting-down", "stopping", "stopped" + amazon.aws.ec2_instance_info: + filters: + instance-state-name: [ "shutting-down", "stopping", "stopped" ] + +- name: Gather information about any instance with Name beginning with RHEL and an uptime of at least 60 minutes + amazon.aws.ec2_instance_info: + region: "{{ ec2_region }}" + uptime: 60 + filters: + "tag:Name": "RHEL-*" + instance-state-name: [ "running"] + register: ec2_node_info + +''' + +RETURN = r''' +instances: + description: a list of ec2 instances + returned: always + type: complex + contains: + ami_launch_index: + description: The AMI launch index, which can be used to find this instance in the launch group. + returned: always + type: int + sample: 0 + architecture: + description: The architecture of the image + returned: always + type: str + sample: x86_64 + block_device_mappings: + description: Any block device mapping entries for the instance. + returned: always + type: complex + contains: + device_name: + description: The device name exposed to the instance (for example, /dev/sdh or xvdh). + returned: always + type: str + sample: /dev/sdh + ebs: + description: Parameters used to automatically set up EBS volumes when the instance is launched. + returned: always + type: complex + contains: + attach_time: + description: The time stamp when the attachment initiated. + returned: always + type: str + sample: "2017-03-23T22:51:24+00:00" + delete_on_termination: + description: Indicates whether the volume is deleted on instance termination. + returned: always + type: bool + sample: true + status: + description: The attachment state. + returned: always + type: str + sample: attached + volume_id: + description: The ID of the EBS volume + returned: always + type: str + sample: vol-12345678 + cpu_options: + description: The CPU options set for the instance. + returned: always if botocore version >= 1.10.16 + type: complex + contains: + core_count: + description: The number of CPU cores for the instance. + returned: always + type: int + sample: 1 + threads_per_core: + description: The number of threads per CPU core. On supported instance, a value of 1 means Intel Hyper-Threading Technology is disabled. + returned: always + type: int + sample: 1 + client_token: + description: The idempotency token you provided when you launched the instance, if applicable. + returned: always + type: str + sample: mytoken + ebs_optimized: + description: Indicates whether the instance is optimized for EBS I/O. + returned: always + type: bool + sample: false + hypervisor: + description: The hypervisor type of the instance. + returned: always + type: str + sample: xen + iam_instance_profile: + description: The IAM instance profile associated with the instance, if applicable. + returned: always + type: complex + contains: + arn: + description: The Amazon Resource Name (ARN) of the instance profile. + returned: always + type: str + sample: "arn:aws:iam::000012345678:instance-profile/myprofile" + id: + description: The ID of the instance profile + returned: always + type: str + sample: JFJ397FDG400FG9FD1N + image_id: + description: The ID of the AMI used to launch the instance. + returned: always + type: str + sample: ami-0011223344 + instance_id: + description: The ID of the instance. + returned: always + type: str + sample: i-012345678 + instance_type: + description: The instance type size of the running instance. + returned: always + type: str + sample: t2.micro + key_name: + description: The name of the key pair, if this instance was launched with an associated key pair. + returned: always + type: str + sample: my-key + launch_time: + description: The time the instance was launched. + returned: always + type: str + sample: "2017-03-23T22:51:24+00:00" + monitoring: + description: The monitoring for the instance. + returned: always + type: complex + contains: + state: + description: Indicates whether detailed monitoring is enabled. Otherwise, basic monitoring is enabled. + returned: always + type: str + sample: disabled + network_interfaces: + description: One or more network interfaces for the instance. + returned: always + type: complex + contains: + association: + description: The association information for an Elastic IPv4 associated with the network interface. + returned: always + type: complex + contains: + ip_owner_id: + description: The ID of the owner of the Elastic IP address. + returned: always + type: str + sample: amazon + public_dns_name: + description: The public DNS name. + returned: always + type: str + sample: "" + public_ip: + description: The public IP address or Elastic IP address bound to the network interface. + returned: always + type: str + sample: 1.2.3.4 + attachment: + description: The network interface attachment. + returned: always + type: complex + contains: + attach_time: + description: The time stamp when the attachment initiated. + returned: always + type: str + sample: "2017-03-23T22:51:24+00:00" + attachment_id: + description: The ID of the network interface attachment. + returned: always + type: str + sample: eni-attach-3aff3f + delete_on_termination: + description: Indicates whether the network interface is deleted when the instance is terminated. + returned: always + type: bool + sample: true + device_index: + description: The index of the device on the instance for the network interface attachment. + returned: always + type: int + sample: 0 + status: + description: The attachment state. + returned: always + type: str + sample: attached + description: + description: The description. + returned: always + type: str + sample: My interface + groups: + description: One or more security groups. + returned: always + type: list + elements: dict + contains: + group_id: + description: The ID of the security group. + returned: always + type: str + sample: sg-abcdef12 + group_name: + description: The name of the security group. + returned: always + type: str + sample: mygroup + ipv6_addresses: + description: One or more IPv6 addresses associated with the network interface. + returned: always + type: list + elements: dict + contains: + ipv6_address: + description: The IPv6 address. + returned: always + type: str + sample: "2001:0db8:85a3:0000:0000:8a2e:0370:7334" + mac_address: + description: The MAC address. + returned: always + type: str + sample: "00:11:22:33:44:55" + network_interface_id: + description: The ID of the network interface. + returned: always + type: str + sample: eni-01234567 + owner_id: + description: The AWS account ID of the owner of the network interface. + returned: always + type: str + sample: 01234567890 + private_ip_address: + description: The IPv4 address of the network interface within the subnet. + returned: always + type: str + sample: 10.0.0.1 + private_ip_addresses: + description: The private IPv4 addresses associated with the network interface. + returned: always + type: list + elements: dict + contains: + association: + description: The association information for an Elastic IP address (IPv4) associated with the network interface. + returned: always + type: complex + contains: + ip_owner_id: + description: The ID of the owner of the Elastic IP address. + returned: always + type: str + sample: amazon + public_dns_name: + description: The public DNS name. + returned: always + type: str + sample: "" + public_ip: + description: The public IP address or Elastic IP address bound to the network interface. + returned: always + type: str + sample: 1.2.3.4 + primary: + description: Indicates whether this IPv4 address is the primary private IP address of the network interface. + returned: always + type: bool + sample: true + private_ip_address: + description: The private IPv4 address of the network interface. + returned: always + type: str + sample: 10.0.0.1 + source_dest_check: + description: Indicates whether source/destination checking is enabled. + returned: always + type: bool + sample: true + status: + description: The status of the network interface. + returned: always + type: str + sample: in-use + subnet_id: + description: The ID of the subnet for the network interface. + returned: always + type: str + sample: subnet-0123456 + vpc_id: + description: The ID of the VPC for the network interface. + returned: always + type: str + sample: vpc-0123456 + placement: + description: The location where the instance launched, if applicable. + returned: always + type: complex + contains: + availability_zone: + description: The Availability Zone of the instance. + returned: always + type: str + sample: ap-southeast-2a + group_name: + description: The name of the placement group the instance is in (for cluster compute instances). + returned: always + type: str + sample: "" + tenancy: + description: The tenancy of the instance (if the instance is running in a VPC). + returned: always + type: str + sample: default + private_dns_name: + description: The private DNS name. + returned: always + type: str + sample: ip-10-0-0-1.ap-southeast-2.compute.internal + private_ip_address: + description: The IPv4 address of the network interface within the subnet. + returned: always + type: str + sample: 10.0.0.1 + product_codes: + description: One or more product codes. + returned: always + type: list + elements: dict + contains: + product_code_id: + description: The product code. + returned: always + type: str + sample: aw0evgkw8ef3n2498gndfgasdfsd5cce + product_code_type: + description: The type of product code. + returned: always + type: str + sample: marketplace + public_dns_name: + description: The public DNS name assigned to the instance. + returned: always + type: str + sample: + public_ip_address: + description: The public IPv4 address assigned to the instance + returned: always + type: str + sample: 52.0.0.1 + root_device_name: + description: The device name of the root device + returned: always + type: str + sample: /dev/sda1 + root_device_type: + description: The type of root device used by the AMI. + returned: always + type: str + sample: ebs + security_groups: + description: One or more security groups for the instance. + returned: always + type: list + elements: dict + contains: + group_id: + description: The ID of the security group. + returned: always + type: str + sample: sg-0123456 + group_name: + description: The name of the security group. + returned: always + type: str + sample: my-security-group + source_dest_check: + description: Indicates whether source/destination checking is enabled. + returned: always + type: bool + sample: true + state: + description: The current state of the instance. + returned: always + type: complex + contains: + code: + description: The low byte represents the state. + returned: always + type: int + sample: 16 + name: + description: The name of the state. + returned: always + type: str + sample: running + state_transition_reason: + description: The reason for the most recent state transition. + returned: always + type: str + sample: + subnet_id: + description: The ID of the subnet in which the instance is running. + returned: always + type: str + sample: subnet-00abcdef + tags: + description: Any tags assigned to the instance. + returned: always + type: dict + sample: + virtualization_type: + description: The type of virtualization of the AMI. + returned: always + type: str + sample: hvm + vpc_id: + description: The ID of the VPC the instance is in. + returned: always + type: dict + sample: vpc-0011223344 +''' + +import datetime + +try: + import botocore +except ImportError: + pass # caught by AnsibleAWSModule + +from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict + +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 ansible_dict_to_boto3_filter_list +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import boto3_tag_list_to_ansible_dict + + +@AWSRetry.jittered_backoff() +def _describe_instances(connection, **params): + paginator = connection.get_paginator('describe_instances') + return paginator.paginate(**params).build_full_result() + + +def list_ec2_instances(connection, module): + + instance_ids = module.params.get("instance_ids") + uptime = module.params.get('minimum_uptime') + filters = ansible_dict_to_boto3_filter_list(module.params.get("filters")) + + try: + reservations = _describe_instances(connection, InstanceIds=instance_ids, Filters=filters) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Failed to list ec2 instances") + + instances = [] + + if uptime: + timedelta = int(uptime) if uptime else 0 + oldest_launch_time = datetime.datetime.utcnow() - datetime.timedelta(minutes=timedelta) + # Get instances from reservations + for reservation in reservations['Reservations']: + instances += [instance for instance in reservation['Instances'] if instance['LaunchTime'].replace(tzinfo=None) < oldest_launch_time] + else: + for reservation in reservations['Reservations']: + instances = instances + reservation['Instances'] + + # Turn the boto3 result in to ansible_friendly_snaked_names + snaked_instances = [camel_dict_to_snake_dict(instance) for instance in instances] + + # Turn the boto3 result in to ansible friendly tag dictionary + for instance in snaked_instances: + instance['tags'] = boto3_tag_list_to_ansible_dict(instance.get('tags', []), 'key', 'value') + + module.exit_json(instances=snaked_instances) + + +def main(): + + argument_spec = dict( + minimum_uptime=dict(required=False, type='int', default=None, aliases=['uptime']), + instance_ids=dict(default=[], type='list', elements='str'), + filters=dict(default={}, type='dict') + ) + + module = AnsibleAWSModule( + argument_spec=argument_spec, + mutually_exclusive=[ + ['instance_ids', 'filters'] + ], + supports_check_mode=True, + ) + if module._name == 'ec2_instance_facts': + module.deprecate("The 'ec2_instance_facts' module has been renamed to 'ec2_instance_info'", date='2021-12-01', collection_name='amazon.aws') + + try: + connection = module.client('ec2') + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg='Failed to connect to AWS') + + list_ec2_instances(connection, module) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/ec2_key.py b/plugins/modules/ec2_key.py index 1a861c11635..e9e8660b7cf 100644 --- a/plugins/modules/ec2_key.py +++ b/plugins/modules/ec2_key.py @@ -52,7 +52,6 @@ - amazon.aws.aws - amazon.aws.ec2 -requirements: [ boto3 ] author: - "Vincent Viallet (@zbal)" - "Prasad Katti (@prasadkatti)" diff --git a/plugins/modules/ec2_snapshot_info.py b/plugins/modules/ec2_snapshot_info.py index de949553411..087d7c1ca2c 100644 --- a/plugins/modules/ec2_snapshot_info.py +++ b/plugins/modules/ec2_snapshot_info.py @@ -14,7 +14,6 @@ description: - Gather information about ec2 volume snapshots in AWS. - This module was called C(ec2_snapshot_facts) before Ansible 2.9. The usage did not change. -requirements: [ boto3 ] author: - "Rob White (@wimnat)" - Aubin Bikouo (@abikouo) diff --git a/plugins/modules/ec2_tag.py b/plugins/modules/ec2_tag.py index f04d1102ff6..b74c21573e8 100644 --- a/plugins/modules/ec2_tag.py +++ b/plugins/modules/ec2_tag.py @@ -15,7 +15,6 @@ - Creates, modifies and removes tags for any EC2 resource. - Resources are referenced by their resource id (for example, an instance being i-XXXXXXX, a VPC being vpc-XXXXXXX). - This module is designed to be used with complex args (tags), see the examples. -requirements: [ "boto3", "botocore" ] options: resource: description: diff --git a/plugins/modules/ec2_tag_info.py b/plugins/modules/ec2_tag_info.py index cf326fd20a5..e29a2952125 100644 --- a/plugins/modules/ec2_tag_info.py +++ b/plugins/modules/ec2_tag_info.py @@ -15,7 +15,6 @@ - Lists tags for any EC2 resource. - Resources are referenced by their resource id (e.g. an instance being i-XXXXXXX, a vpc being vpc-XXXXXX). - Resource tags can be managed using the M(amazon.aws.ec2_tag) module. -requirements: [ "boto3", "botocore" ] options: resource: description: diff --git a/plugins/modules/ec2_vol_info.py b/plugins/modules/ec2_vol_info.py index fb6a658790b..ba20d45ee4f 100644 --- a/plugins/modules/ec2_vol_info.py +++ b/plugins/modules/ec2_vol_info.py @@ -14,7 +14,6 @@ description: - Gather information about ec2 volumes in AWS. - This module was called C(ec2_vol_facts) before Ansible 2.9. The usage did not change. -requirements: [ boto3 ] author: "Rob White (@wimnat)" options: filters: diff --git a/plugins/modules/ec2_vpc_dhcp_option.py b/plugins/modules/ec2_vpc_dhcp_option.py index d2c02efb284..ac3e4a16bf9 100644 --- a/plugins/modules/ec2_vpc_dhcp_option.py +++ b/plugins/modules/ec2_vpc_dhcp_option.py @@ -104,9 +104,6 @@ extends_documentation_fragment: - amazon.aws.aws - amazon.aws.ec2 - -requirements: - - boto ''' RETURN = """ diff --git a/plugins/modules/ec2_vpc_dhcp_option_info.py b/plugins/modules/ec2_vpc_dhcp_option_info.py index a33dff30bd8..d3be0bd53f2 100644 --- a/plugins/modules/ec2_vpc_dhcp_option_info.py +++ b/plugins/modules/ec2_vpc_dhcp_option_info.py @@ -14,7 +14,6 @@ description: - Gather information about dhcp options sets in AWS. - This module was called C(ec2_vpc_dhcp_option_facts) before Ansible 2.9. The usage did not change. -requirements: [ boto3 ] author: "Nick Aslanidis (@naslanidis)" options: filters: diff --git a/plugins/modules/ec2_vpc_net.py b/plugins/modules/ec2_vpc_net.py index 9e76e4ba60c..555f51389a9 100644 --- a/plugins/modules/ec2_vpc_net.py +++ b/plugins/modules/ec2_vpc_net.py @@ -78,9 +78,6 @@ duplicate VPCs created. type: bool default: false -requirements: - - boto3 - - botocore extends_documentation_fragment: - amazon.aws.aws - amazon.aws.ec2 diff --git a/plugins/modules/ec2_vpc_net_info.py b/plugins/modules/ec2_vpc_net_info.py index 62a9b1eecf3..a28df3e0e17 100644 --- a/plugins/modules/ec2_vpc_net_info.py +++ b/plugins/modules/ec2_vpc_net_info.py @@ -15,9 +15,6 @@ - Gather information about ec2 VPCs in AWS - This module was called C(ec2_vpc_net_facts) before Ansible 2.9. The usage did not change. author: "Rob White (@wimnat)" -requirements: - - boto3 - - botocore options: vpc_ids: description: diff --git a/plugins/modules/ec2_vpc_subnet.py b/plugins/modules/ec2_vpc_subnet.py index 2fe34f6ff35..5ac0c6eebfa 100644 --- a/plugins/modules/ec2_vpc_subnet.py +++ b/plugins/modules/ec2_vpc_subnet.py @@ -16,7 +16,6 @@ author: - Robert Estelle (@erydo) - Brad Davidson (@brandond) -requirements: [ boto3 ] options: az: description: diff --git a/plugins/modules/ec2_vpc_subnet_info.py b/plugins/modules/ec2_vpc_subnet_info.py index 316d532e8ff..e1a85fc858f 100644 --- a/plugins/modules/ec2_vpc_subnet_info.py +++ b/plugins/modules/ec2_vpc_subnet_info.py @@ -15,9 +15,6 @@ - Gather information about ec2 VPC subnets in AWS - This module was called C(ec2_vpc_subnet_facts) before Ansible 2.9. The usage did not change. author: "Rob White (@wimnat)" -requirements: - - boto3 - - botocore options: subnet_ids: description: diff --git a/plugins/modules/s3_bucket.py b/plugins/modules/s3_bucket.py index 35206c0b59c..82950fbe6ba 100644 --- a/plugins/modules/s3_bucket.py +++ b/plugins/modules/s3_bucket.py @@ -24,7 +24,6 @@ short_description: Manage S3 buckets in AWS, DigitalOcean, Ceph, Walrus, FakeS3 and StorageGRID description: - Manage S3 buckets in AWS, DigitalOcean, Ceph, Walrus, FakeS3 and StorageGRID. -requirements: [ boto3 ] author: - Rob White (@wimnat) - Aubin Bikouo (@abikouo) diff --git a/requirements.txt b/requirements.txt index 5c4c76b86f0..0d58b96112d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ boto>=2.49.0 -botocore>=1.12.249 -boto3>=1.9.249 +botocore>=1.16.0 +boto3>=1.13.0 diff --git a/tests/integration/targets/aws_s3/defaults/main.yml b/tests/integration/targets/aws_s3/defaults/main.yml index eb7dd2d3712..67d026de087 100644 --- a/tests/integration/targets/aws_s3/defaults/main.yml +++ b/tests/integration/targets/aws_s3/defaults/main.yml @@ -1,3 +1,4 @@ --- # defaults file for s3 -bucket_name: '{{resource_prefix}}' +bucket_name: '{{ resource_prefix }}' +bucket_name_acl: '{{ bucket_name }}-with-acl' diff --git a/tests/integration/targets/aws_s3/tasks/main.yml b/tests/integration/targets/aws_s3/tasks/main.yml index e47ad29e52c..23cb377456f 100644 --- a/tests/integration/targets/aws_s3/tasks/main.yml +++ b/tests/integration/targets/aws_s3/tasks/main.yml @@ -8,6 +8,14 @@ region: "{{ aws_region }}" block: + - name: get ARN of calling user + aws_caller_info: + register: aws_caller_info + + - name: register account id + set_fact: + aws_account: "{{ aws_caller_info.account }}" + - name: Create temporary directory tempfile: state: directory @@ -537,6 +545,47 @@ - result is not changed when: ansible_system == 'Linux' or ansible_distribution == 'MacOSX' + - name: make a bucket with the bucket-owner-full-control ACL + s3_bucket: + name: "{{ bucket_name_acl }}" + state: present + policy: "{{ lookup('template', 'policy.json.j2') }}" + register: bucket_with_policy + + - assert: + that: + - bucket_with_policy is changed + + - name: fail to upload the file to the bucket with an ACL + aws_s3: + bucket: "{{ bucket_name_acl }}" + mode: put + src: "{{ tmpdir.path }}/upload.txt" + object: file-with-permissions.txt + permission: private + ignore_nonexistent_bucket: True + register: upload_private + ignore_errors: True + + # XXX Doesn't fail... + # - assert: + # that: + # - upload_private is failed + + - name: upload the file to the bucket with an ACL + aws_s3: + bucket: "{{ bucket_name_acl }}" + mode: put + src: "{{ tmpdir.path }}/upload.txt" + object: file-with-permissions.txt + permission: bucket-owner-full-control + ignore_nonexistent_bucket: True + register: upload_owner + + - assert: + that: + - upload_owner is changed + - name: create an object from static content aws_s3: bucket: "{{ bucket_name }}" @@ -653,3 +702,4 @@ with_items: - "{{ bucket_name }}" - "{{ bucket_name | hash('md5') + '.bucket' }}" + - "{{ bucket_name_acl }}" diff --git a/tests/integration/targets/aws_s3/templates/policy.json.j2 b/tests/integration/targets/aws_s3/templates/policy.json.j2 new file mode 100644 index 00000000000..4af2e0713b1 --- /dev/null +++ b/tests/integration/targets/aws_s3/templates/policy.json.j2 @@ -0,0 +1,21 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "Only allow writes to my bucket with bucket owner full control", + "Effect": "Allow", + "Principal": { "AWS":"{{ aws_account }}" }, + "Action": [ + "s3:PutObject" + ], + "Resource": [ + "arn:aws:s3:::{{ bucket_name_acl }}/*" + ], + "Condition": { + "StringEquals": { + "s3:x-amz-acl": "bucket-owner-full-control" + } + } + } + ] +} diff --git a/tests/integration/targets/ec2_eni/tasks/test_deletion.yaml b/tests/integration/targets/ec2_eni/tasks/test_deletion.yaml index aecb625eb32..b6dffc3481e 100644 --- a/tests/integration/targets/ec2_eni/tasks/test_deletion.yaml +++ b/tests/integration/targets/ec2_eni/tasks/test_deletion.yaml @@ -16,7 +16,7 @@ - result.changed - result.interface is undefined - '"network_interfaces" in eni_info' - - eni_id_1 not in ( eni_info | community.general.json_query("network_interfaces[].id") | list ) + - eni_id_1 not in ( eni_info.network_interfaces | selectattr('id') | map(attribute='id') | list ) - name: test removing the network interface by ID is idempotent ec2_eni: @@ -53,7 +53,7 @@ - result.changed - result.interface is undefined - '"network_interfaces" in eni_info' - - eni_id_2 not in ( eni_info | community.general.json_query("network_interfaces[].id") | list ) + - eni_id_2 not in ( eni_info.network_interfaces | selectattr('id') | map(attribute='id') | list ) - name: test removing the network interface by name is idempotent ec2_eni: @@ -88,5 +88,5 @@ assert: that: - '"network_interfaces" in eni_info' - - eni_id_1 not in ( eni_info | community.general.json_query("network_interfaces[].id") | list ) - - eni_id_2 not in ( eni_info | community.general.json_query("network_interfaces[].id") | list ) + - eni_id_1 not in ( eni_info.network_interfaces | selectattr('id') | map(attribute='id') | list ) + - eni_id_2 not in ( eni_info.network_interfaces | selectattr('id') | map(attribute='id') | list ) diff --git a/tests/integration/targets/ec2_eni/tasks/test_eni_basic_creation.yaml b/tests/integration/targets/ec2_eni/tasks/test_eni_basic_creation.yaml index b18af2dc9b3..b6f61c9365e 100644 --- a/tests/integration/targets/ec2_eni/tasks/test_eni_basic_creation.yaml +++ b/tests/integration/targets/ec2_eni/tasks/test_eni_basic_creation.yaml @@ -61,7 +61,7 @@ - _interface_0.private_ip_address == ip_1 - '"private_ip_addresses" in _interface_0' - _interface_0.private_ip_addresses | length == 1 - - ip_1 in ( eni_info | community.general.json_query("network_interfaces[].private_ip_addresses[].private_ip_address") | list ) + - ip_1 in ( eni_info.network_interfaces | map(attribute='private_ip_addresses') | flatten | map(attribute='private_ip_address') | list ) - '"requester_id" in _interface_0' - _interface_0.requester_id is string - '"requester_managed" in _interface_0' @@ -156,7 +156,7 @@ - _interface_0.private_ip_address == ip_5 - '"private_ip_addresses" in _interface_0' - _interface_0.private_ip_addresses | length == 1 - - ip_5 in ( eni_info | community.general.json_query("network_interfaces[].private_ip_addresses[].private_ip_address") | list ) + - ip_5 in ( eni_info.network_interfaces | map(attribute='private_ip_addresses') | flatten | map(attribute='private_ip_address') | list ) - '"requester_id" in _interface_0' - _interface_0.requester_id is string - '"requester_managed" in _interface_0' @@ -181,8 +181,8 @@ that: - '"network_interfaces" in eni_info' - eni_info.network_interfaces | length >= 2 - - eni_id_1 in ( eni_info | community.general.json_query("network_interfaces[].id") | list ) - - eni_id_2 in ( eni_info | community.general.json_query("network_interfaces[].id") | list ) + - eni_id_1 in ( eni_info.network_interfaces | selectattr('id') | map(attribute='id') | list ) + - eni_id_2 in ( eni_info.network_interfaces | selectattr('id') | map(attribute='id') | list ) # ============================================================ # Run some VPC filter based tests of ec2_eni_info @@ -199,8 +199,8 @@ that: - '"network_interfaces" in eni_info' - eni_info.network_interfaces | length == 2 - - eni_id_1 in ( eni_info | community.general.json_query("network_interfaces[].id") | list ) - - eni_id_2 in ( eni_info | community.general.json_query("network_interfaces[].id") | list ) + - eni_id_1 in ( eni_info.network_interfaces | selectattr('id') | map(attribute='id') | list ) + - eni_id_2 in ( eni_info.network_interfaces | selectattr('id') | map(attribute='id') | list ) - name: Fetch ENI info with VPC filters - VPC ec2_eni_info: @@ -213,7 +213,7 @@ that: - '"network_interfaces" in eni_info' - eni_info.network_interfaces | length == 4 - - eni_id_1 in ( eni_info | community.general.json_query("network_interfaces[].id") | list ) - - eni_id_2 in ( eni_info | community.general.json_query("network_interfaces[].id") | list ) - - ec2_ips[0] in ( eni_info | community.general.json_query("network_interfaces[].private_ip_addresses[].private_ip_address") | list ) - - ec2_ips[1] in ( eni_info | community.general.json_query("network_interfaces[].private_ip_addresses[].private_ip_address") | list ) + - eni_id_1 in ( eni_info.network_interfaces | selectattr('id') | map(attribute='id') | list ) + - eni_id_2 in ( eni_info.network_interfaces | selectattr('id') | map(attribute='id') | list ) + - ec2_ips[0] in ( eni_info.network_interfaces | map(attribute='private_ip_addresses') | flatten | map(attribute='private_ip_address') | list ) + - ec2_ips[1] in ( eni_info.network_interfaces | map(attribute='private_ip_addresses') | flatten | map(attribute='private_ip_address') | list ) diff --git a/tests/integration/targets/ec2_eni/tasks/test_ipaddress_assign.yaml b/tests/integration/targets/ec2_eni/tasks/test_ipaddress_assign.yaml index a0a3696e9b5..1e67227cb4f 100644 --- a/tests/integration/targets/ec2_eni/tasks/test_ipaddress_assign.yaml +++ b/tests/integration/targets/ec2_eni/tasks/test_ipaddress_assign.yaml @@ -18,7 +18,7 @@ - result.interface.id == eni_id_1 - result.interface.private_ip_addresses | length == 3 - _interface_0.private_ip_addresses | length == 3 - - ip_1 in ( eni_info | community.general.json_query("network_interfaces[].private_ip_addresses[].private_ip_address") | list ) + - ip_1 in ( eni_info.network_interfaces | map(attribute='private_ip_addresses') | flatten | map(attribute='private_ip_address') | list ) vars: _interface_0: '{{ eni_info.network_interfaces[0] }}' @@ -40,7 +40,7 @@ - result.interface.id == eni_id_1 - result.interface.private_ip_addresses | length == 3 - _interface_0.private_ip_addresses | length == 3 - - ip_1 in ( eni_info | community.general.json_query("network_interfaces[].private_ip_addresses[].private_ip_address") | list ) + - ip_1 in ( eni_info.network_interfaces | map(attribute='private_ip_addresses') | flatten | map(attribute='private_ip_address') | list ) vars: _interface_0: '{{ eni_info.network_interfaces[0] }}' @@ -64,7 +64,7 @@ - result.interface.id == eni_id_1 - result.interface.private_ip_addresses | length == 3 - _interface_0.private_ip_addresses | length == 3 - - ip_1 in ( eni_info | community.general.json_query("network_interfaces[].private_ip_addresses[].private_ip_address") | list ) + - ip_1 in ( eni_info.network_interfaces | map(attribute='private_ip_addresses') | flatten | map(attribute='private_ip_address') | list ) vars: _interface_0: '{{ eni_info.network_interfaces[0] }}' @@ -106,7 +106,7 @@ - new_secondary_ip in _private_ips vars: _interface_0: '{{ eni_info.network_interfaces[0] }}' - _private_ips: '{{ eni_info | community.general.json_query("network_interfaces[].private_ip_addresses[].private_ip_address") | list }}' + _private_ips: "{{ eni_info.network_interfaces | map(attribute='private_ip_addresses') | flatten | map(attribute='private_ip_address') | list }}" # ============================================================ - name: remove secondary address @@ -128,7 +128,7 @@ - result.interface.id == eni_id_1 - result.interface.private_ip_addresses | length == 1 - _interface_0.private_ip_addresses | length == 1 - - ip_1 in ( eni_info | community.general.json_query("network_interfaces[].private_ip_addresses[].private_ip_address") | list ) + - ip_1 in ( eni_info.network_interfaces | map(attribute='private_ip_addresses') | flatten | map(attribute='private_ip_address') | list ) vars: _interface_0: '{{ eni_info.network_interfaces[0] }}' @@ -152,7 +152,7 @@ - result.interface.private_ip_addresses | length == 1 - result.interface.private_ip_addresses | length == 1 - _interface_0.private_ip_addresses | length == 1 - - ip_1 in ( eni_info | community.general.json_query("network_interfaces[].private_ip_addresses[].private_ip_address") | list ) + - ip_1 in ( eni_info.network_interfaces | map(attribute='private_ip_addresses') | flatten | map(attribute='private_ip_address') | list ) vars: _interface_0: '{{ eni_info.network_interfaces[0] }}' @@ -177,8 +177,8 @@ - result.interface.id == eni_id_2 - result.interface.private_ip_addresses | length == 2 - _interface_0.private_ip_addresses | length == 2 - - ip_5 in ( eni_info | community.general.json_query("network_interfaces[].private_ip_addresses[].private_ip_address") | list ) - - ip_4 in ( eni_info | community.general.json_query("network_interfaces[].private_ip_addresses[].private_ip_address") | list ) + - ip_5 in ( eni_info.network_interfaces | map(attribute='private_ip_addresses') | flatten | map(attribute='private_ip_address') | list ) + - ip_4 in ( eni_info.network_interfaces | map(attribute='private_ip_addresses') | flatten | map(attribute='private_ip_address') | list ) vars: _interface_0: '{{ eni_info.network_interfaces[0] }}' @@ -262,6 +262,6 @@ that: - result.changed - _interface_0.private_ip_addresses | length == 1 - - ip_1 in ( eni_info | community.general.json_query("network_interfaces[].private_ip_addresses[].private_ip_address") | list ) + - ip_1 in ( eni_info.network_interfaces | map(attribute='private_ip_addresses') | flatten | map(attribute='private_ip_address') | list ) vars: _interface_0: '{{ eni_info.network_interfaces[0] }}' diff --git a/tests/integration/targets/ec2_eni/tasks/test_modifying_delete_on_termination.yaml b/tests/integration/targets/ec2_eni/tasks/test_modifying_delete_on_termination.yaml index 8e8bd0596d4..54240b4d2a2 100644 --- a/tests/integration/targets/ec2_eni/tasks/test_modifying_delete_on_termination.yaml +++ b/tests/integration/targets/ec2_eni/tasks/test_modifying_delete_on_termination.yaml @@ -149,8 +149,8 @@ - not result.changed - '"network_interfaces" in eni_info' - eni_info.network_interfaces | length >= 1 - - eni_id_1 not in ( eni_info | community.general.json_query("network_interfaces[].id") | list ) - - eni_id_2 in ( eni_info | community.general.json_query("network_interfaces[].id") | list ) + - eni_id_1 not in ( eni_info.network_interfaces | selectattr('id') | map(attribute='id') | list ) + - eni_id_2 in ( eni_info.network_interfaces | selectattr('id') | map(attribute='id') | list ) # ============================================================ diff --git a/tests/integration/targets/ec2_instance/aliases b/tests/integration/targets/ec2_instance/aliases new file mode 100644 index 00000000000..6a794c03bc1 --- /dev/null +++ b/tests/integration/targets/ec2_instance/aliases @@ -0,0 +1,2 @@ +cloud/aws +ec2_instance_info diff --git a/tests/integration/targets/ec2_instance/inventory b/tests/integration/targets/ec2_instance/inventory new file mode 100644 index 00000000000..a49c076d2f2 --- /dev/null +++ b/tests/integration/targets/ec2_instance/inventory @@ -0,0 +1,18 @@ +[tests] +# Sorted fastest to slowest +version_fail_wrapper +ebs_optimized +block_devices +cpu_options +default_vpc_tests +external_resource_attach +instance_no_wait +iam_instance_role +termination_protection_wrapper +tags_and_vpc_settings +checkmode_tests +security_group + +[all:vars] +ansible_connection=local +ansible_python_interpreter="{{ ansible_playbook_python }}" diff --git a/tests/integration/targets/ec2_instance/main.yml b/tests/integration/targets/ec2_instance/main.yml new file mode 100644 index 00000000000..7695f7bcb92 --- /dev/null +++ b/tests/integration/targets/ec2_instance/main.yml @@ -0,0 +1,43 @@ +--- +# Beware: most of our tests here are run in parallel. +# To add new tests you'll need to add a new host to the inventory and a matching +# '{{ inventory_hostname }}'.yml file in roles/ec2_instance/tasks/ + + +# Prepare the VPC and figure out which AMI to use +- hosts: all + gather_facts: no + tasks: + - 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 }}" + vars: + # We can't just use "run_once" because the facts don't propagate when + # running an 'include' that was run_once + setup_run_once: yes + block: + - include_role: + name: 'ec2_instance' + tasks_from: find_ami.yml + - include_role: + name: 'ec2_instance' + tasks_from: env_setup.yml + rescue: + - include_role: + name: 'ec2_instance' + tasks_from: env_cleanup.yml + run_once: yes + - fail: + msg: 'Environment preparation failed' + run_once: yes + +# VPC should get cleaned up once all hosts have run +- hosts: all + gather_facts: no + strategy: free + #serial: 10 + roles: + - ec2_instance diff --git a/tests/integration/targets/ec2_instance/meta/main.yml b/tests/integration/targets/ec2_instance/meta/main.yml new file mode 100644 index 00000000000..38b31be0728 --- /dev/null +++ b/tests/integration/targets/ec2_instance/meta/main.yml @@ -0,0 +1,4 @@ +dependencies: + - prepare_tests + - setup_ec2 + - setup_remote_tmp_dir diff --git a/tests/integration/targets/ec2_instance/roles/ec2_instance/defaults/main.yml b/tests/integration/targets/ec2_instance/roles/ec2_instance/defaults/main.yml new file mode 100644 index 00000000000..5dc70554e02 --- /dev/null +++ b/tests/integration/targets/ec2_instance/roles/ec2_instance/defaults/main.yml @@ -0,0 +1,16 @@ +--- +# defaults file for ec2_instance +ec2_instance_owner: 'integration-run-{{ resource_prefix }}' +ec2_instance_type: 't3.micro' +ec2_instance_tag_TestId: '{{ resource_prefix }}-{{ inventory_hostname }}' +ec2_ami_name: 'amzn2-ami-hvm-2.*-x86_64-gp2' + +vpc_name: '{{ resource_prefix }}-vpc' +vpc_seed: '{{ resource_prefix }}' +vpc_cidr: '10.{{ 256 | random(seed=vpc_seed) }}.0.0/16' +subnet_a_cidr: '10.{{ 256 | random(seed=vpc_seed) }}.32.0/24' +subnet_a_startswith: '10.{{ 256 | random(seed=vpc_seed) }}.32.' +subnet_b_cidr: '10.{{ 256 | random(seed=vpc_seed) }}.33.0/24' +subnet_b_startswith: '10.{{ 256 | random(seed=vpc_seed) }}.33.' +first_iam_role: "ansible-test-sts-{{ resource_prefix | hash('md5') }}-test-policy" +second_iam_role: "ansible-test-sts-{{ resource_prefix | hash('md5') }}-test-policy-2" diff --git a/tests/integration/targets/ec2_instance/roles/ec2_instance/files/assume-role-policy.json b/tests/integration/targets/ec2_instance/roles/ec2_instance/files/assume-role-policy.json new file mode 100644 index 00000000000..72413abdd38 --- /dev/null +++ b/tests/integration/targets/ec2_instance/roles/ec2_instance/files/assume-role-policy.json @@ -0,0 +1,13 @@ +{ + "Version": "2008-10-17", + "Statement": [ + { + "Sid": "", + "Effect": "Allow", + "Principal": { + "Service": "ec2.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] +} diff --git a/tests/integration/targets/ec2_instance/roles/ec2_instance/meta/main.yml b/tests/integration/targets/ec2_instance/roles/ec2_instance/meta/main.yml new file mode 100644 index 00000000000..77589cc2b48 --- /dev/null +++ b/tests/integration/targets/ec2_instance/roles/ec2_instance/meta/main.yml @@ -0,0 +1,5 @@ +dependencies: + - prepare_tests + - setup_ec2 +collections: + - amazon.aws diff --git a/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/block_devices.yml b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/block_devices.yml new file mode 100644 index 00000000000..0a8ab63f08b --- /dev/null +++ b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/block_devices.yml @@ -0,0 +1,82 @@ +- block: + - name: "New instance with an extra block device" + ec2_instance: + state: present + name: "{{ resource_prefix }}-test-ebs-vols" + image_id: "{{ ec2_ami_image }}" + vpc_subnet_id: "{{ testing_subnet_b.subnet.id }}" + volumes: + - device_name: /dev/sdb + ebs: + volume_size: 20 + delete_on_termination: true + volume_type: standard + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + instance_type: "{{ ec2_instance_type }}" + wait: true + register: block_device_instances + + - name: "Gather instance info" + ec2_instance_info: + filters: + "tag:Name": "{{ resource_prefix }}-test-ebs-vols" + register: block_device_instances_info + + - assert: + that: + - block_device_instances is not failed + - block_device_instances is changed + - block_device_instances_info.instances[0].block_device_mappings[0] + - block_device_instances_info.instances[0].block_device_mappings[1] + - block_device_instances_info.instances[0].block_device_mappings[1].device_name == '/dev/sdb' + + - name: "New instance with an extra block device (check mode)" + ec2_instance: + state: present + name: "{{ resource_prefix }}-test-ebs-vols-checkmode" + image_id: "{{ ec2_ami_image }}" + vpc_subnet_id: "{{ testing_subnet_b.subnet.id }}" + volumes: + - device_name: /dev/sdb + ebs: + volume_size: 20 + delete_on_termination: true + volume_type: standard + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + instance_type: "{{ ec2_instance_type }}" + check_mode: yes + + - name: "fact presented ec2 instance" + ec2_instance_info: + filters: + "tag:Name": "{{ resource_prefix }}-test-ebs-vols" + "instance-state-name": "running" + register: presented_instance_fact + + - name: "fact checkmode ec2 instance" + ec2_instance_info: + filters: + "tag:Name": "{{ resource_prefix }}-test-ebs-vols-checkmode" + register: checkmode_instance_fact + + - name: "Confirm whether the check mode is working normally." + assert: + that: + - "{{ presented_instance_fact.instances | length }} > 0" + - "{{ checkmode_instance_fact.instances | length }} == 0" + + - name: "Terminate instances" + ec2_instance: + state: absent + instance_ids: "{{ block_device_instances.instance_ids }}" + + always: + - name: "Terminate block_devices instances" + ec2_instance: + state: absent + filters: + "tag:TestId": "{{ ec2_instance_tag_TestId }}" + wait: yes + ignore_errors: yes diff --git a/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/checkmode_tests.yml b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/checkmode_tests.yml new file mode 100644 index 00000000000..e13ad44063b --- /dev/null +++ b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/checkmode_tests.yml @@ -0,0 +1,201 @@ +- block: + - name: "Make basic instance" + ec2_instance: + state: present + name: "{{ resource_prefix }}-checkmode-comparison" + image_id: "{{ ec2_ami_image }}" + security_groups: "{{ sg.group_id }}" + instance_type: "{{ ec2_instance_type }}" + vpc_subnet_id: "{{ testing_subnet_a.subnet.id }}" + wait: false + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + TestTag: "Some Value" + register: basic_instance + + - name: "Make basic instance (check mode)" + ec2_instance: + state: present + name: "{{ resource_prefix }}-checkmode-comparison-checkmode" + image_id: "{{ ec2_ami_image }}" + security_groups: "{{ sg.group_id }}" + instance_type: "{{ ec2_instance_type }}" + vpc_subnet_id: "{{ testing_subnet_b.subnet.id }}" + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + TestTag: "Some Value" + check_mode: yes + + - name: "fact presented ec2 instance" + ec2_instance_info: + filters: + "tag:Name": "{{ resource_prefix }}-checkmode-comparison" + register: presented_instance_fact + + - name: "fact checkmode ec2 instance" + ec2_instance_info: + filters: + "tag:Name": "{{ resource_prefix }}-checkmode-comparison-checkmode" + register: checkmode_instance_fact + + - name: "Confirm whether the check mode is working normally." + assert: + that: + - "{{ presented_instance_fact.instances | length }} > 0" + - "{{ checkmode_instance_fact.instances | length }} == 0" + + - name: "Stop instance (check mode)" + ec2_instance: + state: stopped + name: "{{ resource_prefix }}-checkmode-comparison" + vpc_subnet_id: "{{ testing_subnet_a.subnet.id }}" + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + TestTag: "Some Value" + check_mode: yes + + - name: "fact ec2 instance" + ec2_instance_info: + filters: + "tag:Name": "{{ resource_prefix }}-checkmode-comparison" + register: confirm_checkmode_stopinstance_fact + + - name: "Verify that it was not stopped." + assert: + that: + - '"{{ confirm_checkmode_stopinstance_fact.instances[0].state.name }}" != "stopped"' + + - name: "Stop instance." + ec2_instance: + state: stopped + name: "{{ resource_prefix }}-checkmode-comparison" + vpc_subnet_id: "{{ testing_subnet_a.subnet.id }}" + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + TestTag: "Some Value" + register: instance_stop + until: not instance_stop.failed + retries: 10 + + - name: "fact stopped ec2 instance" + ec2_instance_info: + filters: + "tag:Name": "{{ resource_prefix }}-checkmode-comparison" + register: confirm_stopinstance_fact + + - name: "Verify that it was stopped." + assert: + that: + - '"{{ confirm_stopinstance_fact.instances[0].state.name }}" in ["stopped", "stopping"]' + + - name: "Running instance in check mode." + ec2_instance: + state: running + name: "{{ resource_prefix }}-checkmode-comparison" + vpc_subnet_id: "{{ testing_subnet_a.subnet.id }}" + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + TestTag: "Some Value" + check_mode: yes + + - name: "fact ec2 instance" + ec2_instance_info: + filters: + "tag:Name": "{{ resource_prefix }}-checkmode-comparison" + register: confirm_checkmode_runninginstance_fact + + - name: "Verify that it was not running." + assert: + that: + - '"{{ confirm_checkmode_runninginstance_fact.instances[0].state.name }}" != "running"' + + - name: "Running instance." + ec2_instance: + state: running + name: "{{ resource_prefix }}-checkmode-comparison" + vpc_subnet_id: "{{ testing_subnet_a.subnet.id }}" + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + TestTag: "Some Value" + + - name: "fact ec2 instance." + ec2_instance_info: + filters: + "tag:Name": "{{ resource_prefix }}-checkmode-comparison" + register: confirm_runninginstance_fact + + - name: "Verify that it was running." + assert: + that: + - '"{{ confirm_runninginstance_fact.instances[0].state.name }}" == "running"' + + - name: "Tag instance." + ec2_instance: + state: running + name: "{{ resource_prefix }}-checkmode-comparison" + vpc_subnet_id: "{{ testing_subnet_a.subnet.id }}" + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + TestTag: "Some Other Value" + check_mode: yes + + - name: "fact ec2 instance." + ec2_instance_info: + filters: + "tag:Name": "{{ resource_prefix }}-checkmode-comparison" + register: confirm_not_tagged + + - name: "Verify that it hasn't been re-tagged." + assert: + that: + - '"{{ confirm_not_tagged.instances[0].tags.TestTag }}" == "Some Value"' + + - name: "Terminate instance in check mode." + ec2_instance: + state: absent + name: "{{ resource_prefix }}-checkmode-comparison" + vpc_subnet_id: "{{ testing_subnet_a.subnet.id }}" + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + TestTag: "Some Value" + check_mode: yes + + - name: "fact ec2 instance" + ec2_instance_info: + filters: + "tag:Name": "{{ resource_prefix }}-checkmode-comparison" + register: confirm_checkmode_terminatedinstance_fact + + - name: "Verify that it was not terminated," + assert: + that: + - '"{{ confirm_checkmode_terminatedinstance_fact.instances[0].state.name }}" != "terminated"' + + - name: "Terminate instance." + ec2_instance: + state: absent + name: "{{ resource_prefix }}-checkmode-comparison" + vpc_subnet_id: "{{ testing_subnet_a.subnet.id }}" + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + TestTag: "Some Value" + + - name: "fact ec2 instance" + ec2_instance_info: + filters: + "tag:Name": "{{ resource_prefix }}-checkmode-comparison" + register: confirm_terminatedinstance_fact + + - name: "Verify that it was terminated," + assert: + that: + - '"{{ confirm_terminatedinstance_fact.instances[0].state.name }}" == "terminated"' + + always: + - name: "Terminate checkmode instances" + ec2_instance: + state: absent + filters: + "tag:TestId": "{{ ec2_instance_tag_TestId }}" + wait: yes + ignore_errors: yes diff --git a/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/cpu_options.yml b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/cpu_options.yml new file mode 100644 index 00000000000..947011f75e1 --- /dev/null +++ b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/cpu_options.yml @@ -0,0 +1,86 @@ +- block: + - name: "create t3.nano instance with cpu_options" + ec2_instance: + state: present + name: "{{ resource_prefix }}-test-t3nano-1-threads-per-core" + image_id: "{{ ec2_ami_image }}" + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + vpc_subnet_id: "{{ testing_subnet_a.subnet.id }}" + instance_type: t3.nano + cpu_options: + core_count: 1 + threads_per_core: 1 + wait: false + register: instance_creation + + - name: "instance with cpu_options created with the right options" + assert: + that: + - instance_creation is success + - instance_creation is changed + + - name: "modify cpu_options on existing instance (warning displayed)" + ec2_instance: + state: present + name: "{{ resource_prefix }}-test-t3nano-1-threads-per-core" + image_id: "{{ ec2_ami_image }}" + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + vpc_subnet_id: "{{ testing_subnet_a.subnet.id }}" + instance_type: t3.nano + cpu_options: + core_count: 1 + threads_per_core: 2 + wait: false + register: cpu_options_update + ignore_errors: yes + + - name: "fact presented ec2 instance" + ec2_instance_info: + filters: + "tag:Name": "{{ resource_prefix }}-test-t3nano-1-threads-per-core" + register: presented_instance_fact + + - name: "modify cpu_options has no effect on existing instance" + assert: + that: + - cpu_options_update is success + - cpu_options_update is not changed + - "{{ presented_instance_fact.instances | length }} > 0" + - "'{{ presented_instance_fact.instances.0.state.name }}' in ['running','pending']" + - "{{ presented_instance_fact.instances.0.cpu_options.core_count }} == 1" + - "{{ presented_instance_fact.instances.0.cpu_options.threads_per_core }} == 1" + + - name: "create t3.nano instance with cpu_options(check mode)" + ec2_instance: + name: "{{ resource_prefix }}-test-t3nano-1-threads-per-core-checkmode" + image_id: "{{ ec2_ami_image }}" + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + vpc_subnet_id: "{{ testing_subnet_a.subnet.id }}" + instance_type: t3.nano + cpu_options: + core_count: 1 + threads_per_core: 1 + check_mode: yes + + - name: "fact checkmode ec2 instance" + ec2_instance_info: + filters: + "tag:Name": "{{ resource_prefix }}-test-t3nano-1-threads-per-core-checkmode" + register: checkmode_instance_fact + + - name: "Confirm existence of instance id." + assert: + that: + - "{{ checkmode_instance_fact.instances | length }} == 0" + + always: + - name: "Terminate cpu_options instances" + ec2_instance: + state: absent + filters: + "tag:TestId": "{{ ec2_instance_tag_TestId }}" + wait: yes + ignore_errors: yes diff --git a/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/default_vpc_tests.yml b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/default_vpc_tests.yml new file mode 100644 index 00000000000..a69dfe9f866 --- /dev/null +++ b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/default_vpc_tests.yml @@ -0,0 +1,57 @@ +- block: + - name: "Make instance in a default subnet of the VPC" + ec2_instance: + state: present + name: "{{ resource_prefix }}-test-default-vpc" + image_id: "{{ ec2_ami_image }}" + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + security_group: "default" + instance_type: "{{ ec2_instance_type }}" + wait: false + register: in_default_vpc + + - name: "Make instance in a default subnet of the VPC(check mode)" + ec2_instance: + state: present + name: "{{ resource_prefix }}-test-default-vpc-checkmode" + image_id: "{{ ec2_ami_image }}" + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + security_group: "default" + instance_type: "{{ ec2_instance_type }}" + check_mode: yes + + - name: "fact presented ec2 instance" + ec2_instance_info: + filters: + "tag:Name": "{{ resource_prefix }}-test-default-vpc" + register: presented_instance_fact + + - name: "fact checkmode ec2 instance" + ec2_instance_info: + filters: + "tag:Name": "{{ resource_prefix }}-test-default-vpc-checkmode" + register: checkmode_instance_fact + + - name: "Confirm whether the check mode is working normally." + assert: + that: + - "{{ presented_instance_fact.instances | length }} > 0" + - "{{ checkmode_instance_fact.instances | length }} == 0" + + - name: "Terminate instances" + ec2_instance: + state: absent + instance_ids: "{{ in_default_vpc.instance_ids }}" + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + + always: + - name: "Terminate vpc_tests instances" + ec2_instance: + state: absent + filters: + "tag:TestId": "{{ ec2_instance_tag_TestId }}" + wait: yes + ignore_errors: yes diff --git a/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/ebs_optimized.yml b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/ebs_optimized.yml new file mode 100644 index 00000000000..5bfdc086e76 --- /dev/null +++ b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/ebs_optimized.yml @@ -0,0 +1,41 @@ +- block: + - name: "Make EBS optimized instance in the testing subnet of the test VPC" + ec2_instance: + state: present + name: "{{ resource_prefix }}-test-ebs-optimized-instance-in-vpc" + image_id: "{{ ec2_ami_image }}" + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + security_groups: "{{ sg.group_id }}" + vpc_subnet_id: "{{ testing_subnet_b.subnet.id }}" + ebs_optimized: true + instance_type: t3.nano + wait: false + register: ebs_opt_in_vpc + + - name: "Get ec2 instance info" + ec2_instance_info: + filters: + "tag:Name": "{{ resource_prefix }}-test-ebs-optimized-instance-in-vpc" + register: ebs_opt_instance_info + + - name: "Assert instance is ebs_optimized" + assert: + that: + - "{{ ebs_opt_instance_info.instances.0.ebs_optimized }}" + + - name: "Terminate instances" + ec2_instance: + state: absent + instance_ids: "{{ ebs_opt_in_vpc.instance_ids }}" + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + + always: + - name: "Terminate ebs_optimzed instances" + ec2_instance: + state: absent + filters: + "tag:TestId": "{{ ec2_instance_tag_TestId }}" + wait: yes + ignore_errors: yes diff --git a/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/env_cleanup.yml b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/env_cleanup.yml new file mode 100644 index 00000000000..07c7f72bd8e --- /dev/null +++ b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/env_cleanup.yml @@ -0,0 +1,104 @@ +- name: "remove Instances" + ec2_instance: + state: absent + filters: + vpc-id: "{{ testing_vpc.vpc.id }}" + wait: yes + 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: + state: absent + eni_id: "{{ item.id }}" + until: removed is not failed + with_items: "{{ enis.network_interfaces }}" + ignore_errors: yes + retries: 10 + +- name: "remove the security group" + ec2_group: + state: absent + name: "{{ resource_prefix }}-sg" + description: a security group for ansible tests + vpc_id: "{{ testing_vpc.vpc.id }}" + register: removed + until: removed is not failed + ignore_errors: yes + retries: 10 + +- name: "remove the second security group" + ec2_group: + name: "{{ resource_prefix }}-sg-2" + 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: + state: absent + vpc_id: "{{ testing_vpc.vpc.id }}" + 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: "{{ subnet_a_cidr }}" + 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: "{{ subnet_b_cidr }}" + register: removed + until: removed is not failed + ignore_errors: yes + retries: 10 + +- name: "remove the VPC" + ec2_vpc_net: + state: absent + name: "{{ vpc_name }}" + cidr_block: "{{ vpc_cidr }}" + tags: + Name: Ansible Testing VPC + tenancy: default + register: removed + until: removed is not failed + ignore_errors: yes + retries: 10 diff --git a/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/env_setup.yml b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/env_setup.yml new file mode 100644 index 00000000000..7c99f807177 --- /dev/null +++ b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/env_setup.yml @@ -0,0 +1,95 @@ +- run_once: '{{ setup_run_once | default("no") | bool }}' + block: + - name: "fetch AZ availability" + aws_az_info: + register: az_info + - name: "Assert that we have multiple AZs available to us" + assert: + that: az_info.availability_zones | length >= 2 + + - name: "pick AZs" + set_fact: + subnet_a_az: '{{ az_info.availability_zones[0].zone_name }}' + subnet_b_az: '{{ az_info.availability_zones[1].zone_name }}' + + - name: "Create VPC for use in testing" + ec2_vpc_net: + state: present + name: "{{ vpc_name }}" + cidr_block: "{{ vpc_cidr }}" + tags: + Name: Ansible ec2_instance Testing VPC + tenancy: default + register: testing_vpc + + - name: "Create internet gateway for use in testing" + ec2_vpc_igw: + state: present + vpc_id: "{{ testing_vpc.vpc.id }}" + register: igw + + - name: "Create default subnet in zone A" + ec2_vpc_subnet: + state: present + vpc_id: "{{ testing_vpc.vpc.id }}" + cidr: "{{ subnet_a_cidr }}" + az: "{{ subnet_a_az }}" + 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: "{{ subnet_b_cidr }}" + az: "{{ subnet_b_az }}" + resource_tags: + Name: "{{ resource_prefix }}-subnet-b" + register: testing_subnet_b + + - name: "create routing rules" + ec2_vpc_route_table: + state: present + 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: + state: present + 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: "create secondary security group with the vpc" + ec2_group: + name: "{{ resource_prefix }}-sg-2" + description: a secondary 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: sg2 diff --git a/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/external_resource_attach.yml b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/external_resource_attach.yml new file mode 100644 index 00000000000..2625977f416 --- /dev/null +++ b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/external_resource_attach.yml @@ -0,0 +1,129 @@ +- block: + # Make custom ENIs and attach via the `network` parameter + - ec2_eni: + state: present + delete_on_termination: true + subnet_id: "{{ testing_subnet_b.subnet.id }}" + security_groups: + - "{{ sg.group_id }}" + register: eni_a + + - ec2_eni: + state: present + delete_on_termination: true + subnet_id: "{{ testing_subnet_b.subnet.id }}" + security_groups: + - "{{ sg.group_id }}" + register: eni_b + + - ec2_eni: + state: present + delete_on_termination: true + subnet_id: "{{ testing_subnet_b.subnet.id }}" + security_groups: + - "{{ sg.group_id }}" + register: eni_c + + - ec2_key: + name: "{{ resource_prefix }}_test_key" + + - name: "Make instance in the testing subnet created in the test VPC" + ec2_instance: + state: present + name: "{{ resource_prefix }}-test-eni-vpc" + key_name: "{{ resource_prefix }}_test_key" + network: + interfaces: + - id: "{{ eni_a.interface.id }}" + image_id: "{{ ec2_ami_image }}" + availability_zone: '{{ subnet_b_az }}' + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + instance_type: "{{ ec2_instance_type }}" + wait: false + register: in_test_vpc + + - name: "Gather {{ resource_prefix }}-test-eni-vpc info" + ec2_instance_info: + filters: + "tag:Name": '{{ resource_prefix }}-test-eni-vpc' + register: in_test_vpc_instance + + - assert: + that: + - 'in_test_vpc_instance.instances.0.key_name == "{{ resource_prefix }}_test_key"' + - '(in_test_vpc_instance.instances.0.network_interfaces | length) == 1' + + - name: "Add a second interface" + ec2_instance: + state: present + name: "{{ resource_prefix }}-test-eni-vpc" + network: + interfaces: + - id: "{{ eni_a.interface.id }}" + - id: "{{ eni_b.interface.id }}" + image_id: "{{ ec2_ami_image }}" + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + instance_type: "{{ ec2_instance_type }}" + wait: false + register: add_interface + until: add_interface is not failed + ignore_errors: yes + retries: 10 + + - name: "Make instance in the testing subnet created in the test VPC(check mode)" + ec2_instance: + state: present + name: "{{ resource_prefix }}-test-eni-vpc-checkmode" + key_name: "{{ resource_prefix }}_test_key" + network: + interfaces: + - id: "{{ eni_c.interface.id }}" + image_id: "{{ ec2_ami_image }}" + availability_zone: '{{ subnet_b_az }}' + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + instance_type: "{{ ec2_instance_type }}" + check_mode: yes + + - name: "fact presented ec2 instance" + ec2_instance_info: + filters: + "tag:Name": "{{ resource_prefix }}-test-eni-vpc" + register: presented_instance_fact + + - name: "fact checkmode ec2 instance" + ec2_instance_info: + filters: + "tag:Name": "{{ resource_prefix }}-test-eni-vpc-checkmode" + register: checkmode_instance_fact + + - name: "Confirm existence of instance id." + assert: + that: + - "{{ presented_instance_fact.instances | length }} > 0" + - "{{ checkmode_instance_fact.instances | length }} == 0" + + always: + - name: "Terminate external_resource_attach instances" + ec2_instance: + state: absent + filters: + "tag:TestId": "{{ ec2_instance_tag_TestId }}" + wait: yes + ignore_errors: yes + + - ec2_key: + state: absent + name: "{{ resource_prefix }}_test_key" + ignore_errors: yes + + - ec2_eni: + state: absent + eni_id: '{{ item.interface.id }}' + ignore_errors: yes + with_items: + - '{{ eni_a }}' + - '{{ eni_b }}' + - '{{ eni_c }}' diff --git a/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/find_ami.yml b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/find_ami.yml new file mode 100644 index 00000000000..5c0e61f84c6 --- /dev/null +++ b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/find_ami.yml @@ -0,0 +1,15 @@ +- run_once: '{{ setup_run_once | default("no") | bool }}' + block: + - name: "Find AMI to use" + run_once: yes + ec2_ami_info: + owners: 'amazon' + filters: + name: '{{ ec2_ami_name }}' + register: ec2_amis + - name: "Set fact with latest AMI" + run_once: yes + vars: + latest_ami: '{{ ec2_amis.images | sort(attribute="creation_date") | last }}' + set_fact: + ec2_ami_image: '{{ latest_ami.image_id }}' diff --git a/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/iam_instance_role.yml b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/iam_instance_role.yml new file mode 100644 index 00000000000..f2da199e02b --- /dev/null +++ b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/iam_instance_role.yml @@ -0,0 +1,127 @@ +- block: + - name: "Create IAM role for test" + iam_role: + state: present + name: '{{ first_iam_role }}' + assume_role_policy_document: "{{ lookup('file','assume-role-policy.json') }}" + create_instance_profile: yes + managed_policy: + - AmazonEC2ContainerServiceRole + register: iam_role + + - name: "Create second IAM role for test" + iam_role: + state: present + name: '{{ second_iam_role }}' + assume_role_policy_document: "{{ lookup('file','assume-role-policy.json') }}" + create_instance_profile: yes + managed_policy: + - AmazonEC2ContainerServiceRole + register: iam_role_2 + + - name: "wait 10 seconds for roles to become available" + wait_for: + timeout: 10 + delegate_to: localhost + + - name: "Make instance with an instance_role" + ec2_instance: + state: present + name: "{{ resource_prefix }}-test-instance-role" + image_id: "{{ ec2_ami_image }}" + security_groups: "{{ sg.group_id }}" + instance_type: "{{ ec2_instance_type }}" + instance_role: "ansible-test-sts-{{ resource_prefix }}-test-policy" + vpc_subnet_id: "{{ testing_subnet_a.subnet.id }}" + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + register: instance_with_role + + - assert: + that: + - 'instance_with_role.instances[0].iam_instance_profile.arn == iam_role.arn.replace(":role/", ":instance-profile/")' + + - name: "Make instance with an instance_role(check mode)" + ec2_instance: + state: present + name: "{{ resource_prefix }}-test-instance-role-checkmode" + image_id: "{{ ec2_ami_image }}" + security_groups: "{{ sg.group_id }}" + instance_type: "{{ ec2_instance_type }}" + instance_role: "{{ iam_role.arn.replace(':role/', ':instance-profile/') }}" + vpc_subnet_id: "{{ testing_subnet_a.subnet.id }}" + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + check_mode: yes + + - name: "fact presented ec2 instance" + ec2_instance_info: + filters: + "tag:Name": "{{ resource_prefix }}-test-instance-role" + register: presented_instance_fact + + - name: "fact checkmode ec2 instance" + ec2_instance_info: + filters: + "tag:Name": "{{ resource_prefix }}-test-instance-role-checkmode" + register: checkmode_instance_fact + + - name: "Confirm whether the check mode is working normally." + assert: + that: + - "{{ presented_instance_fact.instances | length }} > 0" + - "{{ checkmode_instance_fact.instances | length }} == 0" + + - name: "Update instance with new instance_role" + ec2_instance: + state: present + name: "{{ resource_prefix }}-test-instance-role" + image_id: "{{ ec2_ami_image }}" + security_groups: "{{ sg.group_id }}" + instance_type: "{{ ec2_instance_type }}" + instance_role: "{{ iam_role_2.arn.replace(':role/', ':instance-profile/') }}" + vpc_subnet_id: "{{ testing_subnet_a.subnet.id }}" + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + register: instance_with_updated_role + + - name: "wait 10 seconds for role update to complete" + wait_for: + timeout: 10 + delegate_to: localhost + + - name: "fact checkmode ec2 instance" + ec2_instance_info: + filters: + "tag:Name": "{{ resource_prefix }}-test-instance-role" + register: updates_instance_info + + - assert: + that: + - 'updates_instance_info.instances[0].iam_instance_profile.arn == iam_role_2.arn.replace(":role/", ":instance-profile/")' + - 'updates_instance_info.instances[0].instance_id == instance_with_role.instances[0].instance_id' + + always: + - name: "Terminate iam_instance_role instances" + ec2_instance: + state: absent + filters: + "tag:TestId": "{{ ec2_instance_tag_TestId }}" + wait: yes + ignore_errors: yes + + - name: "Delete IAM role for test" + iam_role: + state: absent + name: "{{ item }}" + assume_role_policy_document: "{{ lookup('file','assume-role-policy.json') }}" + create_instance_profile: yes + managed_policy: + - AmazonEC2ContainerServiceRole + loop: + - '{{ first_iam_role }}' + - '{{ second_iam_role }}' + register: removed + until: removed is not failed + ignore_errors: yes + retries: 10 diff --git a/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/instance_no_wait.yml b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/instance_no_wait.yml new file mode 100644 index 00000000000..418d7ef3e82 --- /dev/null +++ b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/instance_no_wait.yml @@ -0,0 +1,68 @@ +- block: + - name: "New instance and don't wait for it to complete" + ec2_instance: + state: present + name: "{{ resource_prefix }}-test-no-wait" + image_id: "{{ ec2_ami_image }}" + vpc_subnet_id: "{{ testing_subnet_b.subnet.id }}" + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + wait: false + instance_type: "{{ ec2_instance_type }}" + register: in_test_vpc + + - assert: + that: + - in_test_vpc is not failed + - in_test_vpc is changed + - in_test_vpc.instances is not defined + - in_test_vpc.instance_ids is defined + - in_test_vpc.instance_ids | length > 0 + + - name: "New instance and don't wait for it to complete ( check mode )" + ec2_instance: + state: present + name: "{{ resource_prefix }}-test-no-wait-checkmode" + image_id: "{{ ec2_ami_image }}" + vpc_subnet_id: "{{ testing_subnet_b.subnet.id }}" + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + wait: false + instance_type: "{{ ec2_instance_type }}" + check_mode: yes + + - name: "Facts for ec2 test instance" + ec2_instance_info: + filters: + "tag:Name": "{{ resource_prefix }}-test-no-wait" + register: real_instance_fact + until: real_instance_fact.instances | length > 0 + retries: 10 + + - name: "Facts for checkmode ec2 test instance" + ec2_instance_info: + filters: + "tag:Name": "{{ resource_prefix }}-test-no-wait-checkmode" + register: checkmode_instance_fact + + - name: "Confirm whether the check mode is working normally." + assert: + that: + - "{{ real_instance_fact.instances | length }} > 0" + - "{{ checkmode_instance_fact.instances | length }} == 0" + + - name: "Terminate instances" + ec2_instance: + state: absent + instance_ids: "{{ in_test_vpc.instance_ids }}" + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + + always: + - name: "Terminate instance_no_wait instances" + ec2_instance: + state: absent + filters: + "tag:TestId": "{{ ec2_instance_tag_TestId }}" + wait: yes + ignore_errors: yes diff --git a/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/main.yml b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/main.yml new file mode 100644 index 00000000000..5f06153db1a --- /dev/null +++ b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/main.yml @@ -0,0 +1,55 @@ +--- +# Beware: most of our tests here are run in parallel. +# To add new tests you'll need to add a new host to the inventory and a matching +# '{{ inventory_hostname }}'.yml file in roles/ec2_instance/tasks/ +# +# Please make sure you tag your instances with +# tags: +# "tag:TestId": "{{ ec2_instance_tag_TestId }}" +# And delete them based off that tag at the end of your specific set of tests +# +# ############################################################################### +# +# A Note about ec2 environment variable name preference: +# - EC2_URL -> AWS_URL +# - EC2_ACCESS_KEY -> AWS_ACCESS_KEY_ID -> AWS_ACCESS_KEY +# - EC2_SECRET_KEY -> AWS_SECRET_ACCESS_KEY -> AWX_SECRET_KEY +# - EC2_REGION -> AWS_REGION +# + +- name: "Wrap up all tests and setup AWS credentials" + 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 }}" + aws_config: + retries: + # Unfortunately AWSRetry doesn't support paginators and boto3's paginators + # don't support any configuration of the delay between retries. + max_attempts: 20 + collections: + - community.aws + block: + - debug: + msg: "{{ inventory_hostname }} start: {{ lookup('pipe','date') }}" + - include_tasks: '{{ inventory_hostname }}.yml' + - debug: + msg: "{{ inventory_hostname }} finish: {{ lookup('pipe','date') }}" + + always: + - set_fact: + _role_complete: True + - vars: + completed_hosts: '{{ ansible_play_hosts_all | map("extract", hostvars, "_role_complete") | list | select("defined") | list | length }}' + hosts_in_play: '{{ ansible_play_hosts_all | length }}' + debug: + msg: "{{ completed_hosts }} of {{ hosts_in_play }} complete" + - include_tasks: env_cleanup.yml + vars: + completed_hosts: '{{ ansible_play_hosts_all | map("extract", hostvars, "_role_complete") | list | select("defined") | list | length }}' + hosts_in_play: '{{ ansible_play_hosts_all | length }}' + when: + - aws_cleanup + - completed_hosts == hosts_in_play diff --git a/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/security_group.yml b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/security_group.yml new file mode 100644 index 00000000000..c0e52a5f386 --- /dev/null +++ b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/security_group.yml @@ -0,0 +1,81 @@ +- block: + - name: "New instance with 2 security groups" + ec2_instance: + name: "{{ resource_prefix }}-test-security-groups" + image_id: "{{ ec2_ami_image }}" + vpc_subnet_id: "{{ testing_subnet_a.subnet.id }}" + tags: + TestId: "{{ resource_prefix }}" + instance_type: t2.micro + wait: false + security_groups: + - "{{ sg.group_id }}" + - "{{ sg2.group_id }}" + register: security_groups_test + + - name: "Recreate same instance with 2 security groups ( Idempotency )" + ec2_instance: + name: "{{ resource_prefix }}-test-security-groups" + image_id: "{{ ec2_ami_image }}" + vpc_subnet_id: "{{ testing_subnet_a.subnet.id }}" + tags: + TestId: "{{ resource_prefix }}" + instance_type: t2.micro + wait: false + security_groups: + - "{{ sg.group_id }}" + - "{{ sg2.group_id }}" + register: security_groups_test_idempotency + + - name: "Gather ec2 facts to check SGs have been added" + ec2_instance_info: + filters: + "tag:Name": "{{ resource_prefix }}-test-security-groups" + "instance-state-name": "running" + register: dual_sg_instance_facts + until: dual_sg_instance_facts.instances | length > 0 + retries: 10 + + - name: "Remove secondary security group from instance" + ec2_instance: + name: "{{ resource_prefix }}-test-security-groups" + image_id: "{{ ec2_ami_image }}" + vpc_subnet_id: "{{ testing_subnet_a.subnet.id }}" + tags: + TestId: "{{ resource_prefix }}" + instance_type: t2.micro + security_groups: + - "{{ sg.group_id }}" + register: remove_secondary_security_group + + - name: "Gather ec2 facts to check seconday SG has been removed" + ec2_instance_info: + filters: + "tag:Name": "{{ resource_prefix }}-test-security-groups" + "instance-state-name": "running" + register: single_sg_instance_facts + until: single_sg_instance_facts.instances | length > 0 + retries: 10 + + - name: "Add secondary security group to instance" + ec2_instance: + name: "{{ resource_prefix }}-test-security-groups" + image_id: "{{ ec2_ami_image }}" + vpc_subnet_id: "{{ testing_subnet_a.subnet.id }}" + tags: + TestId: "{{ resource_prefix }}" + instance_type: t2.micro + security_groups: + - "{{ sg.group_id }}" + - "{{ sg2.group_id }}" + register: add_secondary_security_group + + - assert: + that: + - security_groups_test is not failed + - security_groups_test is changed + - security_groups_test_idempotency is not changed + - remove_secondary_security_group is changed + - single_sg_instance_facts.instances.0.security_groups | length == 1 + - dual_sg_instance_facts.instances.0.security_groups | length == 2 + - add_secondary_security_group is changed diff --git a/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/tags_and_vpc_settings.yml b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/tags_and_vpc_settings.yml new file mode 100644 index 00000000000..d38b53f76fb --- /dev/null +++ b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/tags_and_vpc_settings.yml @@ -0,0 +1,158 @@ +- block: + - name: "Make instance in the testing subnet created in the test VPC" + ec2_instance: + state: present + name: "{{ resource_prefix }}-test-basic-vpc-create" + image_id: "{{ ec2_ami_image }}" + user_data: | + #cloud-config + package_upgrade: true + package_update: true + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + Something: else + security_groups: "{{ sg.group_id }}" + network: + source_dest_check: false + vpc_subnet_id: "{{ testing_subnet_b.subnet.id }}" + instance_type: "{{ ec2_instance_type }}" + wait: false + register: in_test_vpc + + - name: "Make instance in the testing subnet created in the test VPC(check mode)" + ec2_instance: + state: present + name: "{{ resource_prefix }}-test-basic-vpc-create-checkmode" + image_id: "{{ ec2_ami_image }}" + user_data: | + #cloud-config + package_upgrade: true + package_update: true + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + Something: else + security_groups: "{{ sg.group_id }}" + network: + source_dest_check: false + vpc_subnet_id: "{{ testing_subnet_b.subnet.id }}" + instance_type: "{{ ec2_instance_type }}" + check_mode: yes + + - name: "Try to re-make the instance, hopefully this shows changed=False" + ec2_instance: + state: present + name: "{{ resource_prefix }}-test-basic-vpc-create" + image_id: "{{ ec2_ami_image }}" + user_data: | + #cloud-config + package_upgrade: true + package_update: true + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + Something: else + security_groups: "{{ sg.group_id }}" + vpc_subnet_id: "{{ testing_subnet_b.subnet.id }}" + instance_type: "{{ ec2_instance_type }}" + register: remake_in_test_vpc + - name: "Remaking the same instance resulted in no changes" + assert: + that: not remake_in_test_vpc.changed + - name: "check that instance IDs match anyway" + assert: + that: 'remake_in_test_vpc.instance_ids[0] == in_test_vpc.instance_ids[0]' + - name: "check that source_dest_check was set to false" + assert: + that: 'not remake_in_test_vpc.instances[0].source_dest_check' + + - name: "fact presented ec2 instance" + ec2_instance_info: + filters: + "tag:Name": "{{ resource_prefix }}-test-basic-vpc-create" + register: presented_instance_fact + + - name: "fact checkmode ec2 instance" + ec2_instance_info: + filters: + "tag:Name": "{{ resource_prefix }}-test-basic-vpc-create-checkmode" + register: checkmode_instance_fact + + - name: "Confirm whether the check mode is working normally." + assert: + that: + - "{{ presented_instance_fact.instances | length }} > 0" + - "{{ checkmode_instance_fact.instances | length }} == 0" + + - name: "Alter it by adding tags" + ec2_instance: + state: present + name: "{{ resource_prefix }}-test-basic-vpc-create" + image_id: "{{ ec2_ami_image }}" + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + Another: thing + security_groups: "{{ sg.group_id }}" + vpc_subnet_id: "{{ testing_subnet_b.subnet.id }}" + instance_type: "{{ ec2_instance_type }}" + register: add_another_tag + + - ec2_instance_info: + instance_ids: "{{ add_another_tag.instance_ids }}" + register: check_tags + - name: "Remaking the same instance resulted in no changes" + assert: + that: + - check_tags.instances[0].tags.Another == 'thing' + - check_tags.instances[0].tags.Something == 'else' + + - name: "Purge a tag" + ec2_instance: + state: present + name: "{{ resource_prefix }}-test-basic-vpc-create" + image_id: "{{ ec2_ami_image }}" + purge_tags: true + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + Another: thing + security_groups: "{{ sg.group_id }}" + vpc_subnet_id: "{{ testing_subnet_b.subnet.id }}" + instance_type: "{{ ec2_instance_type }}" + + - ec2_instance_info: + instance_ids: "{{ add_another_tag.instance_ids }}" + register: check_tags + + - name: "Remaking the same instance resulted in no changes" + assert: + that: + - "'Something' not in check_tags.instances[0].tags" + + - name: "check that subnet-default public IP rule was followed" + assert: + that: + - check_tags.instances[0].public_dns_name == "" + - check_tags.instances[0].private_ip_address.startswith(subnet_b_startswith) + - check_tags.instances[0].subnet_id == testing_subnet_b.subnet.id + - name: "check that tags were applied" + assert: + that: + - check_tags.instances[0].tags.Name.startswith(resource_prefix) + - "'{{ check_tags.instances[0].state.name }}' in ['pending', 'running']" + + - name: "Terminate instance" + ec2_instance: + state: absent + filters: + "tag:TestId": "{{ ec2_instance_tag_TestId }}" + wait: false + register: result + - assert: + that: result.changed + + always: + - name: "Terminate tags_and_vpc_settings instances" + ec2_instance: + state: absent + filters: + "tag:TestId": "{{ ec2_instance_tag_TestId }}" + wait: yes + ignore_errors: yes diff --git a/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/termination_protection.yml b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/termination_protection.yml new file mode 100644 index 00000000000..bcbef1bfd84 --- /dev/null +++ b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/termination_protection.yml @@ -0,0 +1,261 @@ +- block: + - name: Create instance with termination protection (check mode) + ec2_instance: + name: "{{ resource_prefix }}-termination-protection" + image_id: "{{ ec2_ami_image }}" + tags: + TestId: "{{ resource_prefix }}" + security_groups: "{{ sg.group_id }}" + vpc_subnet_id: "{{ testing_subnet_b.subnet.id }}" + termination_protection: true + instance_type: "{{ ec2_instance_type }}" + state: running + wait: yes + check_mode: yes + register: create_instance_check_mode_results + + - name: Check the returned value for the earlier task + assert: + that: + - "{{ create_instance_check_mode_results.changed }}" + - "{{ create_instance_check_mode_results.spec.DisableApiTermination }}" + + - name: Create instance with termination protection + ec2_instance: + name: "{{ resource_prefix }}-termination-protection" + image_id: "{{ ec2_ami_image }}" + tags: + TestId: "{{ resource_prefix }}" + security_groups: "{{ sg.group_id }}" + vpc_subnet_id: "{{ testing_subnet_b.subnet.id }}" + termination_protection: true + instance_type: "{{ ec2_instance_type }}" + state: running + wait: yes + register: create_instance_results + + - set_fact: + instance_id: '{{ create_instance_results.instances[0].instance_id }}' + + - name: Check return values of the create instance task + assert: + that: + - "{{ create_instance_results.instances | length }} > 0" + - "'{{ create_instance_results.instances.0.state.name }}' == 'running'" + - "'{{ create_instance_results.spec.DisableApiTermination }}'" + + - name: Get info on termination protection + command: '{{ aws_cli }} ec2 describe-instance-attribute --attribute disableApiTermination --instance-id {{ instance_id }}' + 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: instance_termination_check + + - name: convert it to an object + set_fact: + instance_termination_status: "{{ instance_termination_check.stdout | from_json }}" + + - name: Assert termination protection status did not change in check_mode + assert: + that: + - instance_termination_status.DisableApiTermination.Value == true + + - name: Create instance with termination protection (check mode) (idempotent) + ec2_instance: + name: "{{ resource_prefix }}-termination-protection" + image_id: "{{ ec2_ami_image }}" + tags: + TestId: "{{ resource_prefix }}" + security_groups: "{{ sg.group_id }}" + vpc_subnet_id: "{{ testing_subnet_b.subnet.id }}" + termination_protection: true + instance_type: "{{ ec2_instance_type }}" + state: running + wait: yes + check_mode: yes + register: create_instance_check_mode_results + + - name: Check the returned value for the earlier task + assert: + that: + - "{{ not create_instance_check_mode_results.changed }}" + + - name: Create instance with termination protection (idempotent) + ec2_instance: + name: "{{ resource_prefix }}-termination-protection" + image_id: "{{ ec2_ami_image }}" + tags: + TestId: "{{ resource_prefix }}" + security_groups: "{{ sg.group_id }}" + vpc_subnet_id: "{{ testing_subnet_b.subnet.id }}" + termination_protection: true + instance_type: "{{ ec2_instance_type }}" + state: running + wait: yes + register: create_instance_results + + - name: Check return values of the create instance task + assert: + that: + - "{{ not create_instance_results.changed }}" + - "{{ create_instance_results.instances | length }} > 0" + + - name: Try to terminate the instance (expected to fail) + ec2_instance: + filters: + tag:Name: "{{ resource_prefix }}-termination-protection" + state: absent + failed_when: "'Unable to terminate instances' not in terminate_instance_results.msg" + register: terminate_instance_results + + - name: Set termination protection to false (check_mode) + ec2_instance: + name: "{{ resource_prefix }}-termination-protection" + image_id: "{{ ec2_ami_image }}" + tags: + TestId: "{{ resource_prefix }}" + termination_protection: false + instance_type: "{{ ec2_instance_type }}" + vpc_subnet_id: "{{ testing_subnet_b.subnet.id }}" + check_mode: True + register: set_termination_protectioncheck_mode_results + + - name: Check return value + assert: + that: + - "{{ set_termination_protectioncheck_mode_results.changed }}" + + - name: Get info on termination protection + command: '{{ aws_cli }} ec2 describe-instance-attribute --attribute disableApiTermination --instance-id {{ instance_id }}' + 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: instance_termination_check + + - name: convert it to an object + set_fact: + instance_termination_status: "{{ instance_termination_check.stdout | from_json }}" + + - assert: + that: + - instance_termination_status.DisableApiTermination.Value == true + + - name: Set termination protection to false + ec2_instance: + name: "{{ resource_prefix }}-termination-protection" + image_id: "{{ ec2_ami_image }}" + tags: + TestId: "{{ resource_prefix }}" + termination_protection: false + instance_type: "{{ ec2_instance_type }}" + vpc_subnet_id: "{{ testing_subnet_b.subnet.id }}" + register: set_termination_protection_results + + - name: Check return value + assert: + that: + - set_termination_protection_results.changed + + - name: Get info on termination protection + command: '{{ aws_cli }} ec2 describe-instance-attribute --attribute disableApiTermination --instance-id {{ instance_id }}' + 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: instance_termination_check + + - name: convert it to an object + set_fact: + instance_termination_status: "{{ instance_termination_check.stdout | from_json }}" + + - assert: + that: + - instance_termination_status.DisableApiTermination.Value == false + + - name: Set termination protection to false (idempotent) + ec2_instance: + name: "{{ resource_prefix }}-termination-protection" + image_id: "{{ ec2_ami_image }}" + tags: + TestId: "{{ resource_prefix }}" + termination_protection: false + instance_type: "{{ ec2_instance_type }}" + vpc_subnet_id: "{{ testing_subnet_b.subnet.id }}" + register: set_termination_protection_results + + - name: Check return value + assert: + that: + - "{{ not set_termination_protection_results.changed }}" + + - name: Set termination protection to true + ec2_instance: + name: "{{ resource_prefix }}-termination-protection" + image_id: "{{ ec2_ami_image }}" + tags: + TestId: "{{ resource_prefix }}" + termination_protection: true + instance_type: "{{ ec2_instance_type }}" + vpc_subnet_id: "{{ testing_subnet_b.subnet.id }}" + register: set_termination_protection_results + + - name: Check return value + assert: + that: + - "{{ set_termination_protection_results.changed }}" + - "{{ set_termination_protection_results.changes[0].DisableApiTermination.Value }}" + + - name: Set termination protection to true (idempotent) + ec2_instance: + name: "{{ resource_prefix }}-termination-protection" + image_id: "{{ ec2_ami_image }}" + tags: + TestId: "{{ resource_prefix }}" + termination_protection: true + instance_type: "{{ ec2_instance_type }}" + vpc_subnet_id: "{{ testing_subnet_b.subnet.id }}" + register: set_termination_protection_results + + - name: Check return value + assert: + that: + - "{{ not set_termination_protection_results.changed }}" + + - name: Set termination protection to false (so we can terminate instance) + ec2_instance: + name: "{{ resource_prefix }}-termination-protection" + image_id: "{{ ec2_ami_image }}" + tags: + TestId: "{{ resource_prefix }}" + termination_protection: false + instance_type: "{{ ec2_instance_type }}" + vpc_subnet_id: "{{ testing_subnet_b.subnet.id }}" + register: set_termination_protection_results + + - name: Terminate the instance + ec2_instance: + filters: + tag:TestId: "{{ resource_prefix }}" + state: absent + + always: + + - name: Set termination protection to false (so we can terminate instance) (cleanup) + ec2_instance: + filters: + tag:TestId: "{{ resource_prefix }}" + termination_protection: false + ignore_errors: yes + + - name: Terminate instance + ec2_instance: + filters: + tag:TestId: "{{ resource_prefix }}" + state: absent + wait: false + ignore_errors: yes diff --git a/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/termination_protection_wrapper.yml b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/termination_protection_wrapper.yml new file mode 100644 index 00000000000..41a00882bd1 --- /dev/null +++ b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/termination_protection_wrapper.yml @@ -0,0 +1,32 @@ +--- +- include_role: + name: 'setup_remote_tmp_dir' + +- set_fact: + virtualenv: "{{ remote_tmp_dir }}/virtualenv" + virtualenv_command: "{{ ansible_python_interpreter }} -m virtualenv" + +- set_fact: + virtualenv_interpreter: "{{ virtualenv }}/bin/python" + aws_cli: "{{ virtualenv }}/bin/aws" + +- pip: + name: "virtualenv" + +- pip: + name: + - awscli<=1.18.159 + - botocore<1.19.0,>=1.13.3 + - boto3 >= 1.9.250, <= 1.15.18 + - coverage<5 + virtualenv: "{{ virtualenv }}" + virtualenv_command: "{{ virtualenv_command }}" + virtualenv_site_packages: no + +- include_tasks: termination_protection.yml + vars: + ansible_python_interpreter: "{{ virtualenv_interpreter }}" + +- file: + state: absent + path: "{{ virtualenv }}" diff --git a/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/uptime.yml b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/uptime.yml new file mode 100644 index 00000000000..6f6c5fe0d49 --- /dev/null +++ b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/uptime.yml @@ -0,0 +1,66 @@ +--- +- block: + - name: "create t3.nano instance" + ec2_instance: + name: "{{ resource_prefix }}-test-uptime" + region: "{{ ec2_region }}" + image_id: "{{ ec2_ami_image }}" + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + vpc_subnet_id: "{{ testing_subnet_a.subnet.id }}" + instance_type: t3.nano + wait: yes + + - name: "check ec2 instance" + ec2_instance_info: + filters: + "tag:Name": "{{ resource_prefix }}-test-uptime" + instance-state-name: [ "running"] + register: instance_facts + + - name: "Confirm existence of instance id." + assert: + that: + - "{{ instance_facts.instances | length }} == 1" + + - name: "check using uptime 100 hours - should find nothing" + ec2_instance_info: + region: "{{ ec2_region }}" + uptime: 6000 + filters: + instance-state-name: [ "running"] + "tag:Name": "{{ resource_prefix }}-test-uptime" + register: instance_facts + + - name: "Confirm there is no running instance" + assert: + that: + - "{{ instance_facts.instances | length }} == 0" + + - name: Sleep for 61 seconds and continue with play + wait_for: + timeout: 61 + delegate_to: localhost + + - name: "check using uptime 1 minute" + ec2_instance_info: + region: "{{ ec2_region }}" + uptime: 1 + filters: + instance-state-name: [ "running"] + "tag:Name": "{{ resource_prefix }}-test-uptime" + register: instance_facts + + - name: "Confirm there is one running instance" + assert: + that: + - "{{ instance_facts.instances | length }} == 1" + + always: + - name: "Terminate instances" + ec2_instance: + state: absent + filters: + "tag:TestId": "{{ ec2_instance_tag_TestId }}" + wait: yes + ignore_errors: yes diff --git a/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/version_fail.yml b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/version_fail.yml new file mode 100644 index 00000000000..67370ebe37c --- /dev/null +++ b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/version_fail.yml @@ -0,0 +1,29 @@ +- block: + - name: "create t3.nano with cpu options (fails gracefully)" + ec2_instance: + state: present + name: "ansible-test-{{ resource_prefix | regex_search('([0-9]+)$') }}-ec2" + image_id: "{{ ec2_ami_image }}" + instance_type: "t3.nano" + cpu_options: + core_count: 1 + threads_per_core: 1 + tags: + TestId: "{{ ec2_instance_tag_TestId }}" + register: ec2_instance_cpu_options_creation + ignore_errors: yes + + - name: "check that graceful error message is returned when creation with cpu_options and old botocore" + assert: + that: + - ec2_instance_cpu_options_creation.failed + - 'ec2_instance_cpu_options_creation.msg == "cpu_options is only supported with botocore >= 1.10.16"' + + always: + - name: "Terminate version_fail instances" + ec2_instance: + state: absent + filters: + "tag:TestId": "{{ ec2_instance_tag_TestId }}" + wait: yes + ignore_errors: yes diff --git a/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/version_fail_wrapper.yml b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/version_fail_wrapper.yml new file mode 100644 index 00000000000..4513ae71119 --- /dev/null +++ b/tests/integration/targets/ec2_instance/roles/ec2_instance/tasks/version_fail_wrapper.yml @@ -0,0 +1,30 @@ +--- +- include_role: + name: 'setup_remote_tmp_dir' + +- set_fact: + virtualenv: "{{ remote_tmp_dir }}/virtualenv" + virtualenv_command: "{{ ansible_python_interpreter }} -m virtualenv" + +- set_fact: + virtualenv_interpreter: "{{ virtualenv }}/bin/python" + +- pip: + name: "virtualenv" + +- pip: + name: + - 'botocore<1.10.16' + - boto3 + - coverage<5 + virtualenv: "{{ virtualenv }}" + virtualenv_command: "{{ virtualenv_command }}" + virtualenv_site_packages: no + +- include_tasks: version_fail.yml + vars: + ansible_python_interpreter: "{{ virtualenv_interpreter }}" + +- file: + state: absent + path: "{{ virtualenv }}" diff --git a/tests/integration/targets/ec2_instance/runme.sh b/tests/integration/targets/ec2_instance/runme.sh new file mode 100755 index 00000000000..aa324772bbe --- /dev/null +++ b/tests/integration/targets/ec2_instance/runme.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +# +# Beware: most of our tests here are run in parallel. +# To add new tests you'll need to add a new host to the inventory and a matching +# '{{ inventory_hostname }}'.yml file in roles/ec2_instance/tasks/ + + +set -eux + +export ANSIBLE_ROLES_PATH=../ + +ansible-playbook main.yml -i inventory "$@" diff --git a/tests/integration/targets/ec2_snapshot/tasks/main.yml b/tests/integration/targets/ec2_snapshot/tasks/main.yml index fd00216bc92..94a4c9ab20d 100644 --- a/tests/integration/targets/ec2_snapshot/tasks/main.yml +++ b/tests/integration/targets/ec2_snapshot/tasks/main.yml @@ -165,7 +165,7 @@ that: - result is changed - info_result.snapshots| length == 2 - - '"{{ result.snapshot_id }}" in "{{ info_result| community.general.json_query("snapshots[].snapshot_id") }}"' + - result.snapshot_id in ( info_result.snapshots | map(attribute='snapshot_id') | list ) # JR: Check mode not supported # - name: Take snapshot with a tag (check mode) @@ -296,7 +296,7 @@ - assert: that: - info_result.snapshots| length == 7 - - '"{{ tagged_snapshot_id }}" not in "{{ info_result| community.general.json_query("snapshots[].snapshot_id") }}"' + - tagged_snapshot_id not in ( info_result.snapshots | map(attribute='snapshot_id') | list ) - name: Delete snapshots ec2_snapshot: diff --git a/tests/integration/targets/ec2_vpc_dhcp_option/tasks/main.yml b/tests/integration/targets/ec2_vpc_dhcp_option/tasks/main.yml index ee780170387..5441e4f7f9b 100644 --- a/tests/integration/targets/ec2_vpc_dhcp_option/tasks/main.yml +++ b/tests/integration/targets/ec2_vpc_dhcp_option/tasks/main.yml @@ -14,9 +14,6 @@ security_token: "{{ security_token | default('') }}" region: "{{ aws_region }}" - collections: - - community.general - block: # DHCP option set can be attached to multiple VPCs, we don't want to use any that @@ -26,7 +23,7 @@ register: result - set_fact: - preexisting_option_sets: "{{ result | community.general.json_query('dhcp_options[*].dhcp_options_id') | list }}" + preexisting_option_sets: "{{ result.dhcp_options | map(attribute='dhcp_options_id') | list }}" - name: create a VPC with a default DHCP option set to test inheritance and delete_old ec2_vpc_net: @@ -183,8 +180,8 @@ - dhcp_options.new_options['domain-name'] == ['{{ aws_domain_name }}'] - dhcp_options.new_options['domain-name-servers'] == ['AmazonProvidedDNS'] # We return the list of dicts that boto gives us, in addition to the user-friendly config dict - - dhcp_options_config['ntp-servers'] | community.general.json_query('[*].value') | list | sort == ['10.0.0.2', '10.0.1.2'] - - dhcp_options_config['netbios-name-servers'] | community.general.json_query('[*].value') | list | sort == ['10.0.0.1', '10.0.1.1'] + - dhcp_options_config['ntp-servers'] | map(attribute='value') | list | sort == ['10.0.0.2', '10.0.1.2'] + - dhcp_options_config['netbios-name-servers'] | map(attribute='value') | list | sort == ['10.0.0.1', '10.0.1.1'] - dhcp_options_config['netbios-node-type'][0]['value'] == '2' - dhcp_options_config['domain-name'][0]['value'] == '{{ aws_domain_name }}' - dhcp_options_config['domain-name-servers'][0]['value'] == 'AmazonProvidedDNS' @@ -206,8 +203,8 @@ - new_config.keys() | list | sort == ['domain-name', 'domain-name-servers', 'netbios-name-servers', 'netbios-node-type', 'ntp-servers'] - new_config['domain-name'][0]['value'] == '{{ aws_domain_name }}' - new_config['domain-name-servers'][0]['value'] == 'AmazonProvidedDNS' - - new_config['ntp-servers'] | community.general.json_query('[*].value') | list | sort == ['10.0.0.2', '10.0.1.2'] - - new_config['netbios-name-servers'] | community.general.json_query('[*].value') | list | sort == ['10.0.0.1', '10.0.1.1'] + - new_config['ntp-servers'] | map(attribute='value') | list | sort == ['10.0.0.2', '10.0.1.2'] + - new_config['netbios-name-servers'] | map(attribute='value') | list | sort == ['10.0.0.1', '10.0.1.1'] - new_config['netbios-node-type'][0]['value'] == '2' # We return the list of dicts that boto gives us, in addition to the user-friendly config dict - new_dhcp_options.dhcp_config[0]['ntp-servers'] | sort == ['10.0.0.2', '10.0.1.2'] @@ -305,8 +302,8 @@ - assert: that: - new_config.keys() | list | sort == ['netbios-name-servers', 'netbios-node-type', 'ntp-servers'] - - new_config['ntp-servers'] | community.general.json_query('[*].value') | list | sort == ['10.0.0.2', '10.0.1.2'] - - new_config['netbios-name-servers'] | community.general.json_query('[*].value') | list | sort == ['10.0.0.1', '10.0.1.1'] + - new_config['ntp-servers'] | map(attribute='value') | list | sort == ['10.0.0.2', '10.0.1.2'] + - new_config['netbios-name-servers'] | map(attribute='value') | list | sort == ['10.0.0.1', '10.0.1.1'] - new_config['netbios-node-type'][0]['value'] == '2' - name: disassociate the new DHCP option set so it can be deleted @@ -384,8 +381,8 @@ that: - new_config.keys() | list | sort == ['domain-name', 'domain-name-servers', 'netbios-name-servers', 'netbios-node-type', 'ntp-servers'] - new_config['domain-name'][0]['value'] == '{{ aws_domain_name }}' - - new_config['ntp-servers'] | community.general.json_query('[*].value') | list | sort == ['10.0.0.2', '10.0.1.2'] - - new_config['netbios-name-servers'] | community.general.json_query('[*].value') | list | sort == ['10.0.0.1', '10.0.1.1'] + - new_config['ntp-servers'] | map(attribute='value') | list | sort == ['10.0.0.2', '10.0.1.2'] + - new_config['netbios-name-servers'] | map(attribute='value') | list | sort == ['10.0.0.1', '10.0.1.1'] - new_config['netbios-node-type'][0]['value'] == '1' - name: verify the original set was deleted @@ -533,8 +530,8 @@ - dhcp_options.new_options['netbios-name-servers'] | sort == ['10.0.0.1', '10.0.1.1'] - original_dhcp_options_id != dhcp_options.dhcp_options_id # We return the list of dicts that boto gives us, in addition to the user-friendly config dict - - dhcp_options_config['ntp-servers'] | community.general.json_query('[*].value') | list | sort == ['10.0.0.2', '10.0.1.2'] - - dhcp_options_config['netbios-name-servers'] | community.general.json_query('[*].value') | list | sort == ['10.0.0.1', '10.0.1.1'] + - dhcp_options_config['ntp-servers'] | map(attribute='value') | list | sort == ['10.0.0.2', '10.0.1.2'] + - dhcp_options_config['netbios-name-servers'] | map(attribute='value') | list | sort == ['10.0.0.1', '10.0.1.1'] - dhcp_options.dhcp_options.tags.keys() | length == 2 - dhcp_options.dhcp_options.tags['CreatedBy'] == 'ansible-test' - dhcp_options.dhcp_options.tags['Collection'] == 'amazon.aws' diff --git a/tests/integration/targets/ec2_vpc_net/tasks/main.yml b/tests/integration/targets/ec2_vpc_net/tasks/main.yml index 728667ac32e..19fcd65ae65 100644 --- a/tests/integration/targets/ec2_vpc_net/tasks/main.yml +++ b/tests/integration/targets/ec2_vpc_net/tasks/main.yml @@ -174,7 +174,7 @@ - name: Test that our new VPC shows up in the results assert: that: - - vpc_1 in ( vpc_info | community.general.json_query("vpcs[].vpc_id") | list ) + - vpc_1 in ( vpc_info.vpcs | map(attribute="vpc_id") | list ) - name: VPC info (Simple tag filter) ec2_vpc_net_info: @@ -789,17 +789,17 @@ # - result.vpc.id == vpc_1 # - vpc_info.vpcs | length == 1 # - vpc_info.vpcs[0].cidr_block == vpc_cidr - # - vpc_cidr in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - # - vpc_cidr_a not in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - # - vpc_cidr_b not in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) + # - vpc_cidr in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) + # - vpc_cidr_a not in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) + # - vpc_cidr_b not in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) # - vpc_info.vpcs[0].cidr_block_association_set | length == 1 # - vpc_info.vpcs[0].cidr_block_association_set[0].association_id.startswith("vpc-cidr-assoc-") # - vpc_info.vpcs[0].cidr_block_association_set[1].association_id.startswith("vpc-cidr-assoc-") # - vpc_info.vpcs[0].cidr_block_association_set[0].cidr_block_state.state in ["associated", "associating"] # - vpc_info.vpcs[0].cidr_block_association_set[1].cidr_block_state.state in ["associated", "associating"] - # - vpc_cidr in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - # - vpc_cidr_a not in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - # - vpc_cidr_b not in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) + # - vpc_cidr in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) + # - vpc_cidr_a not in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) + # - vpc_cidr_b not in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) - name: modify CIDR ec2_vpc_net: @@ -828,17 +828,17 @@ - result.vpc.cidr_block_association_set[1].association_id.startswith("vpc-cidr-assoc-") - result.vpc.cidr_block_association_set[0].cidr_block_state.state in ["associated", "associating"] - result.vpc.cidr_block_association_set[1].cidr_block_state.state in ["associated", "associating"] - - vpc_cidr in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b not in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) + - vpc_cidr in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_a in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_b not in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) - vpc_info.vpcs[0].cidr_block_association_set | length == 2 - vpc_info.vpcs[0].cidr_block_association_set[0].association_id.startswith("vpc-cidr-assoc-") - vpc_info.vpcs[0].cidr_block_association_set[1].association_id.startswith("vpc-cidr-assoc-") - vpc_info.vpcs[0].cidr_block_association_set[0].cidr_block_state.state in ["associated", "associating"] - vpc_info.vpcs[0].cidr_block_association_set[1].cidr_block_state.state in ["associated", "associating"] - - vpc_cidr in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b not in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) + - vpc_cidr in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_a in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_b not in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) - name: modify CIDR (no change) ec2_vpc_net: @@ -867,17 +867,17 @@ - result.vpc.cidr_block_association_set[1].association_id.startswith("vpc-cidr-assoc-") - result.vpc.cidr_block_association_set[0].cidr_block_state.state in ["associated", "associating"] - result.vpc.cidr_block_association_set[1].cidr_block_state.state in ["associated", "associating"] - - vpc_cidr in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b not in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) + - vpc_cidr in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_a in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_b not in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) - vpc_info.vpcs[0].cidr_block_association_set | length == 2 - vpc_info.vpcs[0].cidr_block_association_set[0].association_id.startswith("vpc-cidr-assoc-") - vpc_info.vpcs[0].cidr_block_association_set[1].association_id.startswith("vpc-cidr-assoc-") - vpc_info.vpcs[0].cidr_block_association_set[0].cidr_block_state.state in ["associated", "associating"] - vpc_info.vpcs[0].cidr_block_association_set[1].cidr_block_state.state in ["associated", "associating"] - - vpc_cidr in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b not in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) + - vpc_cidr in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_a in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_b not in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) # #62678 #- name: modify CIDR - no purge (check mode) @@ -901,17 +901,17 @@ # - result is changed # - vpc_info.vpcs | length == 1 # - vpc_info.vpcs[0].cidr_block == vpc_cidr - # - vpc_cidr in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - # - vpc_cidr_a in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - # - vpc_cidr_b not in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) + # - vpc_cidr in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) + # - vpc_cidr_a in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) + # - vpc_cidr_b not in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) # - vpc_info.vpcs[0].cidr_block_association_set | length == 2 # - vpc_info.vpcs[0].cidr_block_association_set[0].association_id.startswith("vpc-cidr-assoc-") # - vpc_info.vpcs[0].cidr_block_association_set[1].association_id.startswith("vpc-cidr-assoc-") # - vpc_info.vpcs[0].cidr_block_association_set[0].cidr_block_state.state in ["associated", "associating"] # - vpc_info.vpcs[0].cidr_block_association_set[1].cidr_block_state.state in ["associated", "associating"] - # - vpc_cidr in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - # - vpc_cidr_a in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - # - vpc_cidr_b not in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) + # - vpc_cidr in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) + # - vpc_cidr_a in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) + # - vpc_cidr_b not in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) - name: modify CIDR - no purge ec2_vpc_net: @@ -942,9 +942,9 @@ - result.vpc.cidr_block_association_set[0].cidr_block_state.state in ["associated", "associating"] - result.vpc.cidr_block_association_set[1].cidr_block_state.state in ["associated", "associating"] - result.vpc.cidr_block_association_set[2].cidr_block_state.state in ["associated", "associating"] - - vpc_cidr in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) + - vpc_cidr in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_a in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_b in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) - vpc_info.vpcs[0].cidr_block_association_set | length == 3 - vpc_info.vpcs[0].cidr_block_association_set[0].association_id.startswith("vpc-cidr-assoc-") - vpc_info.vpcs[0].cidr_block_association_set[1].association_id.startswith("vpc-cidr-assoc-") @@ -952,9 +952,9 @@ - vpc_info.vpcs[0].cidr_block_association_set[0].cidr_block_state.state in ["associated", "associating"] - vpc_info.vpcs[0].cidr_block_association_set[1].cidr_block_state.state in ["associated", "associating"] - vpc_info.vpcs[0].cidr_block_association_set[2].cidr_block_state.state in ["associated", "associating"] - - vpc_cidr in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) + - vpc_cidr in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_a in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_b in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) - name: modify CIDR - no purge (no change) ec2_vpc_net: @@ -984,9 +984,9 @@ - result.vpc.cidr_block_association_set[0].cidr_block_state.state in ["associated", "associating"] - result.vpc.cidr_block_association_set[1].cidr_block_state.state in ["associated", "associating"] - result.vpc.cidr_block_association_set[2].cidr_block_state.state in ["associated", "associating"] - - vpc_cidr in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) + - vpc_cidr in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_a in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_b in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) - vpc_info.vpcs[0].cidr_block_association_set | length == 3 - vpc_info.vpcs[0].cidr_block_association_set[0].association_id.startswith("vpc-cidr-assoc-") - vpc_info.vpcs[0].cidr_block_association_set[1].association_id.startswith("vpc-cidr-assoc-") @@ -994,9 +994,9 @@ - vpc_info.vpcs[0].cidr_block_association_set[0].cidr_block_state.state in ["associated", "associating"] - vpc_info.vpcs[0].cidr_block_association_set[1].cidr_block_state.state in ["associated", "associating"] - vpc_info.vpcs[0].cidr_block_association_set[2].cidr_block_state.state in ["associated", "associating"] - - vpc_cidr in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) + - vpc_cidr in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_a in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_b in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) - name: modify CIDR - no purge (no change - list all - check mode) ec2_vpc_net: @@ -1027,9 +1027,9 @@ - result.vpc.cidr_block_association_set[0].cidr_block_state.state in ["associated", "associating"] - result.vpc.cidr_block_association_set[1].cidr_block_state.state in ["associated", "associating"] - result.vpc.cidr_block_association_set[2].cidr_block_state.state in ["associated", "associating"] - - vpc_cidr in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) + - vpc_cidr in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_a in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_b in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) - vpc_info.vpcs[0].cidr_block_association_set | length == 3 - vpc_info.vpcs[0].cidr_block_association_set[0].association_id.startswith("vpc-cidr-assoc-") - vpc_info.vpcs[0].cidr_block_association_set[1].association_id.startswith("vpc-cidr-assoc-") @@ -1037,9 +1037,9 @@ - vpc_info.vpcs[0].cidr_block_association_set[0].cidr_block_state.state in ["associated", "associating"] - vpc_info.vpcs[0].cidr_block_association_set[1].cidr_block_state.state in ["associated", "associating"] - vpc_info.vpcs[0].cidr_block_association_set[2].cidr_block_state.state in ["associated", "associating"] - - vpc_cidr in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) + - vpc_cidr in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_a in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_b in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) - name: modify CIDR - no purge (no change - list all) ec2_vpc_net: @@ -1070,9 +1070,9 @@ - result.vpc.cidr_block_association_set[0].cidr_block_state.state in ["associated", "associating"] - result.vpc.cidr_block_association_set[1].cidr_block_state.state in ["associated", "associating"] - result.vpc.cidr_block_association_set[2].cidr_block_state.state in ["associated", "associating"] - - vpc_cidr in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) + - vpc_cidr in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_a in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_b in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) - vpc_info.vpcs[0].cidr_block_association_set | length == 3 - vpc_info.vpcs[0].cidr_block_association_set[0].association_id.startswith("vpc-cidr-assoc-") - vpc_info.vpcs[0].cidr_block_association_set[1].association_id.startswith("vpc-cidr-assoc-") @@ -1080,9 +1080,9 @@ - vpc_info.vpcs[0].cidr_block_association_set[0].cidr_block_state.state in ["associated", "associating"] - vpc_info.vpcs[0].cidr_block_association_set[1].cidr_block_state.state in ["associated", "associating"] - vpc_info.vpcs[0].cidr_block_association_set[2].cidr_block_state.state in ["associated", "associating"] - - vpc_cidr in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) + - vpc_cidr in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_a in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_b in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) - name: modify CIDR - no purge (no change - different order - check mode) ec2_vpc_net: @@ -1113,9 +1113,9 @@ - result.vpc.cidr_block_association_set[0].cidr_block_state.state in ["associated", "associating"] - result.vpc.cidr_block_association_set[1].cidr_block_state.state in ["associated", "associating"] - result.vpc.cidr_block_association_set[2].cidr_block_state.state in ["associated", "associating"] - - vpc_cidr in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) + - vpc_cidr in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_a in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_b in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) - vpc_info.vpcs[0].cidr_block_association_set | length == 3 - vpc_info.vpcs[0].cidr_block_association_set[0].association_id.startswith("vpc-cidr-assoc-") - vpc_info.vpcs[0].cidr_block_association_set[1].association_id.startswith("vpc-cidr-assoc-") @@ -1123,9 +1123,9 @@ - vpc_info.vpcs[0].cidr_block_association_set[0].cidr_block_state.state in ["associated", "associating"] - vpc_info.vpcs[0].cidr_block_association_set[1].cidr_block_state.state in ["associated", "associating"] - vpc_info.vpcs[0].cidr_block_association_set[2].cidr_block_state.state in ["associated", "associating"] - - vpc_cidr in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) + - vpc_cidr in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_a in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_b in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) - name: modify CIDR - no purge (no change - different order) ec2_vpc_net: @@ -1156,9 +1156,9 @@ - result.vpc.cidr_block_association_set[0].cidr_block_state.state in ["associated", "associating"] - result.vpc.cidr_block_association_set[1].cidr_block_state.state in ["associated", "associating"] - result.vpc.cidr_block_association_set[2].cidr_block_state.state in ["associated", "associating"] - - vpc_cidr in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b in (result.vpc | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) + - vpc_cidr in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_a in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_b in (result.vpc.cidr_block_association_set | map(attribute="cidr_block") | list) - vpc_info.vpcs[0].cidr_block_association_set | length == 3 - vpc_info.vpcs[0].cidr_block_association_set[0].association_id.startswith("vpc-cidr-assoc-") - vpc_info.vpcs[0].cidr_block_association_set[1].association_id.startswith("vpc-cidr-assoc-") @@ -1166,9 +1166,9 @@ - vpc_info.vpcs[0].cidr_block_association_set[0].cidr_block_state.state in ["associated", "associating"] - vpc_info.vpcs[0].cidr_block_association_set[1].cidr_block_state.state in ["associated", "associating"] - vpc_info.vpcs[0].cidr_block_association_set[2].cidr_block_state.state in ["associated", "associating"] - - vpc_cidr in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) + - vpc_cidr in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_a in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) + - vpc_cidr_b in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) # #62678 #- name: modify CIDR - purge (check mode) @@ -1200,9 +1200,9 @@ # - vpc_info.vpcs[0].cidr_block_association_set[0].cidr_block_state.state in ["associated", "associating"] # - vpc_info.vpcs[0].cidr_block_association_set[1].cidr_block_state.state in ["associated", "associating"] # - vpc_info.vpcs[0].cidr_block_association_set[2].cidr_block_state.state in ["associated", "associating"] - # - vpc_cidr in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - # - vpc_cidr_a in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) - # - vpc_cidr_b in (vpc_info.vpcs[0] | community.general.json_query("cidr_block_association_set[*].cidr_block") | list) + # - vpc_cidr in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) + # - vpc_cidr_a in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) + # - vpc_cidr_b in (vpc_info.vpcs[0].cidr_block_association_set | map(attribute="cidr_block") | list) - name: modify CIDR - purge ec2_vpc_net: @@ -1219,8 +1219,6 @@ register: vpc_info - name: assert the CIDRs changed - vars: - cidr_query: 'cidr_block_association_set[?cidr_block_state.state == `associated`].cidr_block' assert: that: - result is successful @@ -1229,14 +1227,14 @@ - vpc_info.vpcs | length == 1 - result.vpc.cidr_block == vpc_cidr - vpc_info.vpcs[0].cidr_block == vpc_cidr - - result.vpc | community.general.json_query(cidr_query) | list | length == 2 - - vpc_cidr in (result.vpc | community.general.json_query(cidr_query) | list) - - vpc_cidr_a not in (result.vpc | community.general.json_query(cidr_query) | list) - - vpc_cidr_b in (result.vpc | community.general.json_query(cidr_query) | list) - - vpc_info.vpcs[0] | community.general.json_query(cidr_query) | list | length == 2 - - vpc_cidr in (vpc_info.vpcs[0] | community.general.json_query(cidr_query) | list) - - vpc_cidr_a not in (vpc_info.vpcs[0] | community.general.json_query(cidr_query) | list) - - vpc_cidr_b in (vpc_info.vpcs[0] | community.general.json_query(cidr_query) | list) + - result.vpc.cidr_block_association_set | selectattr('cidr_block_state.state', 'equalto', 'associated') | map(attribute='cidr_block') | list | length == 2 + - vpc_cidr in (result.vpc.cidr_block_association_set | selectattr('cidr_block_state.state', 'equalto', 'associated') | map(attribute='cidr_block') | list) + - vpc_cidr_a not in (result.vpc.cidr_block_association_set | selectattr('cidr_block_state.state', 'equalto', 'associated') | map(attribute='cidr_block')) + - vpc_cidr_b in (result.vpc.cidr_block_association_set | selectattr('cidr_block_state.state', 'equalto', 'associated') | map(attribute='cidr_block')) + - vpc_info.vpcs[0].cidr_block_association_set | selectattr('cidr_block_state.state', 'equalto', 'associated') | map(attribute='cidr_block') | list | length == 2 + - vpc_cidr in (vpc_info.vpcs[0].cidr_block_association_set | selectattr('cidr_block_state.state', 'equalto', 'associated') | map(attribute='cidr_block') | list) + - vpc_cidr_a not in (vpc_info.vpcs[0].cidr_block_association_set | selectattr('cidr_block_state.state', 'equalto', 'associated') | map(attribute='cidr_block') | list) + - vpc_cidr_b in (vpc_info.vpcs[0].cidr_block_association_set | selectattr('cidr_block_state.state', 'equalto', 'associated') | map(attribute='cidr_block') | list) - name: modify CIDR - purge (no change) ec2_vpc_net: @@ -1253,8 +1251,6 @@ register: vpc_info - name: assert the CIDRs didn't change - vars: - cidr_query: 'cidr_block_association_set[?cidr_block_state.state == `associated`].cidr_block' assert: that: - result is successful @@ -1263,14 +1259,14 @@ - vpc_info.vpcs | length == 1 - result.vpc.cidr_block == vpc_cidr - vpc_info.vpcs[0].cidr_block == vpc_cidr - - result.vpc | community.general.json_query(cidr_query) | list | length == 2 - - vpc_cidr in (result.vpc | community.general.json_query(cidr_query) | list) - - vpc_cidr_a not in (result.vpc | community.general.json_query(cidr_query) | list) - - vpc_cidr_b in (result.vpc | community.general.json_query(cidr_query) | list) - - vpc_info.vpcs[0] | community.general.json_query(cidr_query) | list | length == 2 - - vpc_cidr in (vpc_info.vpcs[0] | community.general.json_query(cidr_query) | list) - - vpc_cidr_a not in (vpc_info.vpcs[0] | community.general.json_query(cidr_query) | list) - - vpc_cidr_b in (vpc_info.vpcs[0] | community.general.json_query(cidr_query) | list) + - result.vpc.cidr_block_association_set | selectattr('cidr_block_state.state', 'equalto', 'associated') | map(attribute='cidr_block') | list | length == 2 + - vpc_cidr in (result.vpc.cidr_block_association_set | selectattr('cidr_block_state.state', 'equalto', 'associated') | map(attribute='cidr_block') | list) + - vpc_cidr_a not in (result.vpc.cidr_block_association_set | selectattr('cidr_block_state.state', 'equalto', 'associated') | map(attribute='cidr_block') | list) + - vpc_cidr_b in (result.vpc.cidr_block_association_set | selectattr('cidr_block_state.state', 'equalto', 'associated') | map(attribute='cidr_block') | list) + - vpc_info.vpcs[0].cidr_block_association_set | selectattr('cidr_block_state.state', 'equalto', 'associated') | map(attribute='cidr_block') | list | length == 2 + - vpc_cidr in (vpc_info.vpcs[0].cidr_block_association_set | selectattr('cidr_block_state.state', 'equalto', 'associated') | map(attribute='cidr_block') | list) + - vpc_cidr_a not in (vpc_info.vpcs[0].cidr_block_association_set | selectattr('cidr_block_state.state', 'equalto', 'associated') | map(attribute='cidr_block') | list) + - vpc_cidr_b in (vpc_info.vpcs[0].cidr_block_association_set | selectattr('cidr_block_state.state', 'equalto', 'associated') | map(attribute='cidr_block') | list) # ============================================================ diff --git a/tests/integration/targets/inventory_aws_ec2/playbooks/populate_cache.yml b/tests/integration/targets/inventory_aws_ec2/playbooks/populate_cache.yml index 64e8da4c749..1308fab93a7 100644 --- a/tests/integration/targets/inventory_aws_ec2/playbooks/populate_cache.yml +++ b/tests/integration/targets/inventory_aws_ec2/playbooks/populate_cache.yml @@ -3,8 +3,6 @@ connection: local gather_facts: no environment: "{{ ansible_test.environment }}" - collections: - - community.general tasks: - module_defaults: diff --git a/tests/integration/targets/inventory_aws_ec2/templates/inventory_with_constructed.yml.j2 b/tests/integration/targets/inventory_aws_ec2/templates/inventory_with_constructed.yml.j2 index c0ebcbfcb47..a33f03e21c7 100644 --- a/tests/integration/targets/inventory_aws_ec2/templates/inventory_with_constructed.yml.j2 +++ b/tests/integration/targets/inventory_aws_ec2/templates/inventory_with_constructed.yml.j2 @@ -10,7 +10,7 @@ filters: tag:Name: - '{{ resource_prefix }}' keyed_groups: -- key: 'security_groups|community.general.json_query("[].group_id")' +- key: 'security_groups|map(attribute="group_id")' prefix: security_groups - key: tags prefix: tag diff --git a/tests/integration/targets/inventory_aws_rds/templates/inventory_with_constructed.j2 b/tests/integration/targets/inventory_aws_rds/templates/inventory_with_constructed.j2 index a0636a971bd..c5603ef874a 100644 --- a/tests/integration/targets/inventory_aws_rds/templates/inventory_with_constructed.j2 +++ b/tests/integration/targets/inventory_aws_rds/templates/inventory_with_constructed.j2 @@ -7,7 +7,7 @@ aws_security_token: '{{ security_token }}' regions: - '{{ aws_region }}' keyed_groups: - - key: 'db_parameter_groups|community.general.json_query("[].db_parameter_group_name")' + - key: 'db_parameter_groups|map(attribute="db_parameter_group_name")' prefix: rds_parameter_group - key: tags prefix: tag diff --git a/tests/requirements.yml b/tests/requirements.yml index 3e967b199d4..77938b9e2dc 100644 --- a/tests/requirements.yml +++ b/tests/requirements.yml @@ -1,4 +1,4 @@ integration_tests_dependencies: - ansible.windows -- community.general +- ansible.netcommon # ipv6 filter unit_tests_dependencies: [] diff --git a/tests/sanity/ignore-2.11.txt b/tests/sanity/ignore-2.11.txt index 82cef71c1ff..31a4d4c9c6d 100644 --- a/tests/sanity/ignore-2.11.txt +++ b/tests/sanity/ignore-2.11.txt @@ -2,4 +2,3 @@ plugins/modules/ec2_tag.py validate-modules:parameter-state-invalid-choice # De plugins/modules/ec2_vol.py validate-modules:parameter-state-invalid-choice # Deprecated choice that can't be removed until 2022 plugins/module_utils/compat/_ipaddress.py no-assert # Vendored library plugins/module_utils/compat/_ipaddress.py no-unicode-literals # Vendored library -plugins/module_utils/core.py pylint:property-with-parameters # Breaking change required to fix - https://github.com/ansible-collections/amazon.aws/pull/290 diff --git a/tests/sanity/ignore-2.12.txt b/tests/sanity/ignore-2.12.txt index 82cef71c1ff..31a4d4c9c6d 100644 --- a/tests/sanity/ignore-2.12.txt +++ b/tests/sanity/ignore-2.12.txt @@ -2,4 +2,3 @@ plugins/modules/ec2_tag.py validate-modules:parameter-state-invalid-choice # De plugins/modules/ec2_vol.py validate-modules:parameter-state-invalid-choice # Deprecated choice that can't be removed until 2022 plugins/module_utils/compat/_ipaddress.py no-assert # Vendored library plugins/module_utils/compat/_ipaddress.py no-unicode-literals # Vendored library -plugins/module_utils/core.py pylint:property-with-parameters # Breaking change required to fix - https://github.com/ansible-collections/amazon.aws/pull/290 diff --git a/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt index 7815aaa28dd..d9d68e5a3d2 100644 --- a/tests/sanity/ignore-2.9.txt +++ b/tests/sanity/ignore-2.9.txt @@ -16,3 +16,4 @@ plugins/modules/ec2_vpc_subnet_info.py pylint:ansible-deprecated-no-version # W plugins/module_utils/compat/_ipaddress.py no-assert # Vendored library plugins/module_utils/compat/_ipaddress.py no-unicode-literals # Vendored library plugins/module_utils/ec2.py pylint:ansible-deprecated-no-version # We use dates for deprecations, Ansible 2.9 only supports this for compatability +plugins/modules/ec2_instance_info.py pylint:ansible-deprecated-no-version # We use dates for deprecations, Ansible 2.9 only supports this for compatability diff --git a/tests/unit/module_utils/core/test_scrub_none_parameters.py b/tests/unit/module_utils/core/test_scrub_none_parameters.py index a1a1b491788..8c1faf42832 100644 --- a/tests/unit/module_utils/core/test_scrub_none_parameters.py +++ b/tests/unit/module_utils/core/test_scrub_none_parameters.py @@ -83,6 +83,6 @@ @pytest.mark.parametrize("input_params, output_params_no_descend, output_params_descend", scrub_none_test_data) def test_scrub_none_parameters(input_params, output_params_no_descend, output_params_descend): - assert scrub_none_parameters(input_params) == output_params_no_descend + assert scrub_none_parameters(input_params) == output_params_descend assert scrub_none_parameters(input_params, descend_into_lists=False) == output_params_no_descend assert scrub_none_parameters(input_params, descend_into_lists=True) == output_params_descend diff --git a/tests/unit/plugins/modules/test_cloudformation.py b/tests/unit/plugins/modules/test_cloudformation.py index 6ee1fcf95d5..3b0e7c9fb5e 100644 --- a/tests/unit/plugins/modules/test_cloudformation.py +++ b/tests/unit/plugins/modules/test_cloudformation.py @@ -12,6 +12,9 @@ # Magic... from ansible_collections.amazon.aws.tests.unit.utils.amazon_placebo_fixtures import maybe_sleep, placeboify # pylint: disable=unused-import +import ansible_collections.amazon.aws.plugins.module_utils.core as aws_core +import ansible_collections.amazon.aws.plugins.module_utils.ec2 as aws_ec2 + from ansible_collections.amazon.aws.plugins.module_utils.ec2 import boto_exception from ansible_collections.amazon.aws.plugins.modules import cloudformation as cfn_module @@ -78,8 +81,15 @@ def exit_json(self, *args, **kwargs): raise Exception('EXIT') -def test_invalid_template_json(placeboify): +def _create_wrapped_client(placeboify): connection = placeboify.client('cloudformation') + retry_decorator = aws_ec2.AWSRetry.jittered_backoff() + wrapped_conn = aws_core._RetryingBotoClientWrapper(connection, retry_decorator) + return wrapped_conn + + +def test_invalid_template_json(placeboify): + connection = _create_wrapped_client(placeboify) params = { 'StackName': 'ansible-test-wrong-json', 'TemplateBody': bad_json_tpl, @@ -94,7 +104,7 @@ def test_invalid_template_json(placeboify): def test_client_request_token_s3_stack(maybe_sleep, placeboify): - connection = placeboify.client('cloudformation') + connection = _create_wrapped_client(placeboify) params = { 'StackName': 'ansible-test-client-request-token-yaml', 'TemplateBody': basic_yaml_tpl, @@ -111,7 +121,7 @@ def test_client_request_token_s3_stack(maybe_sleep, placeboify): def test_basic_s3_stack(maybe_sleep, placeboify): - connection = placeboify.client('cloudformation') + connection = _create_wrapped_client(placeboify) params = { 'StackName': 'ansible-test-basic-yaml', 'TemplateBody': basic_yaml_tpl @@ -127,15 +137,19 @@ def test_basic_s3_stack(maybe_sleep, placeboify): def test_delete_nonexistent_stack(maybe_sleep, placeboify): - connection = placeboify.client('cloudformation') - result = cfn_module.stack_operation(connection, 'ansible-test-nonexist', 'DELETE', default_events_limit) + connection = _create_wrapped_client(placeboify) + # module is only used if we threw an unexpected error + module = None + result = cfn_module.stack_operation(module, connection, 'ansible-test-nonexist', 'DELETE', default_events_limit) assert result['changed'] assert 'Stack does not exist.' in result['log'] def test_get_nonexistent_stack(placeboify): - connection = placeboify.client('cloudformation') - assert cfn_module.get_stack_facts(connection, 'ansible-test-nonexist') is None + connection = _create_wrapped_client(placeboify) + # module is only used if we threw an unexpected error + module = None + assert cfn_module.get_stack_facts(module, connection, 'ansible-test-nonexist') is None def test_missing_template_body(): @@ -159,7 +173,7 @@ def test_on_create_failure_delete(maybe_sleep, placeboify): on_create_failure='DELETE', disable_rollback=False, ) - connection = placeboify.client('cloudformation') + connection = _create_wrapped_client(placeboify) params = { 'StackName': 'ansible-test-on-create-failure-delete', 'TemplateBody': failing_yaml_tpl @@ -178,7 +192,7 @@ def test_on_create_failure_rollback(maybe_sleep, placeboify): on_create_failure='ROLLBACK', disable_rollback=False, ) - connection = placeboify.client('cloudformation') + connection = _create_wrapped_client(placeboify) params = { 'StackName': 'ansible-test-on-create-failure-rollback', 'TemplateBody': failing_yaml_tpl @@ -198,7 +212,7 @@ def test_on_create_failure_do_nothing(maybe_sleep, placeboify): on_create_failure='DO_NOTHING', disable_rollback=False, ) - connection = placeboify.client('cloudformation') + connection = _create_wrapped_client(placeboify) params = { 'StackName': 'ansible-test-on-create-failure-do-nothing', 'TemplateBody': failing_yaml_tpl diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt index 917ee278d67..063eab0c1ed 100644 --- a/tests/unit/requirements.txt +++ b/tests/unit/requirements.txt @@ -1,2 +1,2 @@ -boto3 +boto3>=1.13.0 placebo