diff options
author | Ansible Core Team <info@ansible.com> | 2020-03-09 09:40:31 +0000 |
---|---|---|
committer | Matt Martz <matt@sivel.net> | 2020-03-23 11:14:21 -0500 |
commit | 6d910034a0a12f93b323e88b809c66afde6e713a (patch) | |
tree | 99aac704166cb859c6e366a9521b644fb5b6247c | |
parent | 8f7e9f73ce69a413cd8d650b50635f55674492d1 (diff) | |
download | ansible-6d910034a0a12f93b323e88b809c66afde6e713a.tar.gz |
Migrated to ansible.amazon
190 files changed, 0 insertions, 31279 deletions
diff --git a/lib/ansible/module_utils/aws/__init__.py b/lib/ansible/module_utils/aws/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 --- a/lib/ansible/module_utils/aws/__init__.py +++ /dev/null diff --git a/lib/ansible/module_utils/aws/acm.py b/lib/ansible/module_utils/aws/acm.py deleted file mode 100644 index a2ac4505bf..0000000000 --- a/lib/ansible/module_utils/aws/acm.py +++ /dev/null @@ -1,212 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2019 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -# -# This module is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This software is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this software. If not, see <http://www.gnu.org/licenses/>. -# -# Author: -# - Matthew Davis <Matthew.Davis.2@team.telstra.com> -# on behalf of Telstra Corporation Limited -# -# Common functionality to be used by the modules: -# - acm - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -""" -Common Amazon Certificate Manager facts shared between modules -""" -import traceback -from ansible.module_utils.ec2 import get_aws_connection_info, boto3_conn -from ansible.module_utils.ec2 import camel_dict_to_snake_dict, AWSRetry, HAS_BOTO3, boto3_tag_list_to_ansible_dict, ansible_dict_to_boto3_tag_list -from ansible.module_utils._text import to_bytes - - -try: - import botocore - from botocore.exceptions import BotoCoreError, ClientError -except ImportError: - pass # caught by imported HAS_BOTO3 - - -class ACMServiceManager(object): - """Handles ACM Facts Services""" - - def __init__(self, module): - self.module = module - - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - self.client = module.client('acm') - - @AWSRetry.backoff(tries=5, delay=5, backoff=2.0, catch_extra_error_codes=['RequestInProgressException']) - def delete_certificate_with_backoff(self, client, arn): - client.delete_certificate(CertificateArn=arn) - - def delete_certificate(self, client, module, arn): - module.debug("Attempting to delete certificate %s" % arn) - try: - self.delete_certificate_with_backoff(client, arn) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Couldn't delete certificate %s" % arn) - module.debug("Successfully deleted certificate %s" % arn) - - @AWSRetry.backoff(tries=5, delay=5, backoff=2.0, catch_extra_error_codes=['RequestInProgressException']) - def list_certificates_with_backoff(self, client, statuses=None): - paginator = client.get_paginator('list_certificates') - kwargs = dict() - if statuses: - kwargs['CertificateStatuses'] = statuses - return paginator.paginate(**kwargs).build_full_result()['CertificateSummaryList'] - - @AWSRetry.backoff(tries=5, delay=5, backoff=2.0, catch_extra_error_codes=['ResourceNotFoundException', 'RequestInProgressException']) - def get_certificate_with_backoff(self, client, certificate_arn): - response = client.get_certificate(CertificateArn=certificate_arn) - # strip out response metadata - return {'Certificate': response['Certificate'], - 'CertificateChain': response['CertificateChain']} - - @AWSRetry.backoff(tries=5, delay=5, backoff=2.0, catch_extra_error_codes=['ResourceNotFoundException', 'RequestInProgressException']) - def describe_certificate_with_backoff(self, client, certificate_arn): - return client.describe_certificate(CertificateArn=certificate_arn)['Certificate'] - - @AWSRetry.backoff(tries=5, delay=5, backoff=2.0, catch_extra_error_codes=['ResourceNotFoundException', 'RequestInProgressException']) - def list_certificate_tags_with_backoff(self, client, certificate_arn): - return client.list_tags_for_certificate(CertificateArn=certificate_arn)['Tags'] - - # Returns a list of certificates - # if domain_name is specified, returns only certificates with that domain - # if an ARN is specified, returns only that certificate - # only_tags is a dict, e.g. {'key':'value'}. If specified this function will return - # only certificates which contain all those tags (key exists, value matches). - def get_certificates(self, client, module, domain_name=None, statuses=None, arn=None, only_tags=None): - try: - all_certificates = self.list_certificates_with_backoff(client=client, statuses=statuses) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Couldn't obtain certificates") - if domain_name: - certificates = [cert for cert in all_certificates - if cert['DomainName'] == domain_name] - else: - certificates = all_certificates - - if arn: - # still return a list, not just one item - certificates = [c for c in certificates if c['CertificateArn'] == arn] - - results = [] - for certificate in certificates: - try: - cert_data = self.describe_certificate_with_backoff(client, certificate['CertificateArn']) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Couldn't obtain certificate metadata for domain %s" % certificate['DomainName']) - - # in some states, ACM resources do not have a corresponding cert - if cert_data['Status'] not in ['PENDING_VALIDATION', 'VALIDATION_TIMED_OUT', 'FAILED']: - try: - cert_data.update(self.get_certificate_with_backoff(client, certificate['CertificateArn'])) - except (BotoCoreError, ClientError, KeyError) as e: - module.fail_json_aws(e, msg="Couldn't obtain certificate data for domain %s" % certificate['DomainName']) - cert_data = camel_dict_to_snake_dict(cert_data) - try: - tags = self.list_certificate_tags_with_backoff(client, certificate['CertificateArn']) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Couldn't obtain tags for domain %s" % certificate['DomainName']) - - cert_data['tags'] = boto3_tag_list_to_ansible_dict(tags) - results.append(cert_data) - - if only_tags: - for tag_key in only_tags: - try: - results = [c for c in results if ('tags' in c) and (tag_key in c['tags']) and (c['tags'][tag_key] == only_tags[tag_key])] - except (TypeError, AttributeError) as e: - for c in results: - if 'tags' not in c: - module.debug("cert is %s" % str(c)) - module.fail_json(msg="ACM tag filtering err", exception=e) - - return results - - # returns the domain name of a certificate (encoded in the public cert) - # for a given ARN - # A cert with that ARN must already exist - def get_domain_of_cert(self, client, module, arn): - if arn is None: - module.fail(msg="Internal error with ACM domain fetching, no certificate ARN specified") - try: - cert_data = self.describe_certificate_with_backoff(client=client, certificate_arn=arn) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Couldn't obtain certificate data for arn %s" % arn) - return cert_data['DomainName'] - - @AWSRetry.backoff(tries=5, delay=5, backoff=2.0) - def import_certificate_with_backoff(self, client, certificate, private_key, certificate_chain, arn): - if certificate_chain: - if arn: - ret = client.import_certificate(Certificate=to_bytes(certificate), - PrivateKey=to_bytes(private_key), - CertificateChain=to_bytes(certificate_chain), - CertificateArn=arn) - else: - ret = client.import_certificate(Certificate=to_bytes(certificate), - PrivateKey=to_bytes(private_key), - CertificateChain=to_bytes(certificate_chain)) - else: - if arn: - ret = client.import_certificate(Certificate=to_bytes(certificate), - PrivateKey=to_bytes(private_key), - CertificateArn=arn) - else: - ret = client.import_certificate(Certificate=to_bytes(certificate), - PrivateKey=to_bytes(private_key)) - return ret['CertificateArn'] - - # Tags are a normal Ansible style dict - # {'Key':'Value'} - @AWSRetry.backoff(tries=5, delay=5, backoff=2.0, catch_extra_error_codes=['ResourceNotFoundException', 'RequestInProgressException']) - def tag_certificate_with_backoff(self, client, arn, tags): - aws_tags = ansible_dict_to_boto3_tag_list(tags) - client.add_tags_to_certificate(CertificateArn=arn, Tags=aws_tags) - - def import_certificate(self, client, module, certificate, private_key, arn=None, certificate_chain=None, tags=None): - - original_arn = arn - - # upload cert - try: - arn = self.import_certificate_with_backoff(client, certificate, private_key, certificate_chain, arn) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Couldn't upload new certificate") - - if original_arn and (arn != original_arn): - # I'm not sure whether the API guarentees that the ARN will not change - # I'm failing just in case. - # If I'm wrong, I'll catch it in the integration tests. - module.fail_json(msg="ARN changed with ACM update, from %s to %s" % (original_arn, arn)) - - # tag that cert - try: - self.tag_certificate_with_backoff(client, arn, tags) - except (BotoCoreError, ClientError) as e: - module.debug("Attempting to delete the cert we just created, arn=%s" % arn) - try: - self.delete_certificate_with_backoff(client, arn) - except Exception as f: - module.warn("Certificate %s exists, and is not tagged. So Ansible will not see it on the next run.") - module.fail_json_aws(e, msg="Couldn't tag certificate %s, couldn't delete it either" % arn) - module.fail_json_aws(e, msg="Couldn't tag certificate %s" % arn) - - return arn diff --git a/lib/ansible/module_utils/aws/batch.py b/lib/ansible/module_utils/aws/batch.py deleted file mode 100644 index 3c92798ddb..0000000000 --- a/lib/ansible/module_utils/aws/batch.py +++ /dev/null @@ -1,103 +0,0 @@ -# Copyright (c) 2017 Ansible Project -# -# This code is part of Ansible, but is an independent component. -# This particular file snippet, and this file snippet only, is BSD licensed. -# Modules you write using this snippet, which is embedded dynamically by Ansible -# still belong to the author of the module, and may assign their own license -# to the complete work. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# -""" -This module adds shared support for Batch modules. -""" - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -from ansible.module_utils.ec2 import get_aws_connection_info, boto3_conn, snake_dict_to_camel_dict - -try: - from botocore.exceptions import ClientError -except ImportError: - pass # Handled by HAS_BOTO3 - - -class AWSConnection(object): - """ - Create the connection object and client objects as required. - """ - - def __init__(self, ansible_obj, resources, boto3=True): - - ansible_obj.deprecate("The 'ansible.module_utils.aws.batch.AWSConnection' class is deprecated please use 'AnsibleAWSModule.client()'", - version='2.14') - - self.region, self.endpoint, aws_connect_kwargs = get_aws_connection_info(ansible_obj, boto3=boto3) - - self.resource_client = dict() - if not resources: - resources = ['batch'] - - resources.append('iam') - - for resource in resources: - aws_connect_kwargs.update(dict(region=self.region, - endpoint=self.endpoint, - conn_type='client', - resource=resource - )) - self.resource_client[resource] = boto3_conn(ansible_obj, **aws_connect_kwargs) - - # if region is not provided, then get default profile/session region - if not self.region: - self.region = self.resource_client['batch'].meta.region_name - - # set account ID - try: - self.account_id = self.resource_client['iam'].get_user()['User']['Arn'].split(':')[4] - except (ClientError, ValueError, KeyError, IndexError): - self.account_id = '' - - def client(self, resource='batch'): - return self.resource_client[resource] - - -def cc(key): - """ - Changes python key into Camel case equivalent. For example, 'compute_environment_name' becomes - 'computeEnvironmentName'. - - :param key: - :return: - """ - components = key.split('_') - return components[0] + "".join([token.capitalize() for token in components[1:]]) - - -def set_api_params(module, module_params): - """ - Sets module parameters to those expected by the boto3 API. - :param module: - :param module_params: - :return: - """ - api_params = dict((k, v) for k, v in dict(module.params).items() if k in module_params and v is not None) - return snake_dict_to_camel_dict(api_params) diff --git a/lib/ansible/module_utils/aws/cloudfront_facts.py b/lib/ansible/module_utils/aws/cloudfront_facts.py deleted file mode 100644 index 780f4026a1..0000000000 --- a/lib/ansible/module_utils/aws/cloudfront_facts.py +++ /dev/null @@ -1,235 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2017 Willem van Ketwich -# -# This module is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This software is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this software. If not, see <http://www.gnu.org/licenses/>. -# -# Author: -# - Willem van Ketwich <willem@vanketwich.com.au> -# -# Common functionality to be used by the modules: -# - cloudfront_distribution -# - cloudfront_invalidation -# - cloudfront_origin_access_identity -""" -Common cloudfront facts shared between modules -""" - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -from ansible.module_utils.ec2 import get_aws_connection_info, boto3_conn -from ansible.module_utils.ec2 import boto3_tag_list_to_ansible_dict, camel_dict_to_snake_dict - -try: - import botocore -except ImportError: - pass - - -class CloudFrontFactsServiceManager(object): - """Handles CloudFront Facts Services""" - - def __init__(self, module): - self.module = module - - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - self.client = boto3_conn(module, conn_type='client', - resource='cloudfront', region=region, - endpoint=ec2_url, **aws_connect_kwargs) - - def get_distribution(self, distribution_id): - try: - return self.client.get_distribution(Id=distribution_id) - except botocore.exceptions.ClientError as e: - self.module.fail_json_aws(e, msg="Error describing distribution") - - def get_distribution_config(self, distribution_id): - try: - return self.client.get_distribution_config(Id=distribution_id) - except botocore.exceptions.ClientError as e: - self.module.fail_json_aws(e, msg="Error describing distribution configuration") - - def get_origin_access_identity(self, origin_access_identity_id): - try: - return self.client.get_cloud_front_origin_access_identity(Id=origin_access_identity_id) - except botocore.exceptions.ClientError as e: - self.module.fail_json_aws(e, msg="Error describing origin access identity") - - def get_origin_access_identity_config(self, origin_access_identity_id): - try: - return self.client.get_cloud_front_origin_access_identity_config(Id=origin_access_identity_id) - except botocore.exceptions.ClientError as e: - self.module.fail_json_aws(e, msg="Error describing origin access identity configuration") - - def get_invalidation(self, distribution_id, invalidation_id): - try: - return self.client.get_invalidation(DistributionId=distribution_id, Id=invalidation_id) - except botocore.exceptions.ClientError as e: - self.module.fail_json_aws(e, msg="Error describing invalidation") - - def get_streaming_distribution(self, distribution_id): - try: - return self.client.get_streaming_distribution(Id=distribution_id) - except botocore.exceptions.ClientError as e: - self.module.fail_json_aws(e, msg="Error describing streaming distribution") - - def get_streaming_distribution_config(self, distribution_id): - try: - return self.client.get_streaming_distribution_config(Id=distribution_id) - except botocore.exceptions.ClientError as e: - self.module.fail_json_aws(e, msg="Error describing streaming distribution") - - def list_origin_access_identities(self): - try: - paginator = self.client.get_paginator('list_cloud_front_origin_access_identities') - result = paginator.paginate().build_full_result().get('CloudFrontOriginAccessIdentityList', {}) - return result.get('Items', []) - except botocore.exceptions.ClientError as e: - self.module.fail_json_aws(e, msg="Error listing cloud front origin access identities") - - def list_distributions(self, keyed=True): - try: - paginator = self.client.get_paginator('list_distributions') - result = paginator.paginate().build_full_result().get('DistributionList', {}) - distribution_list = result.get('Items', []) - if not keyed: - return distribution_list - return self.keyed_list_helper(distribution_list) - except botocore.exceptions.ClientError as e: - self.module.fail_json_aws(e, msg="Error listing distributions") - - def list_distributions_by_web_acl_id(self, web_acl_id): - try: - result = self.client.list_distributions_by_web_acl_id(WebAclId=web_acl_id) - distribution_list = result.get('DistributionList', {}).get('Items', []) - return self.keyed_list_helper(distribution_list) - except botocore.exceptions.ClientError as e: - self.module.fail_json_aws(e, msg="Error listing distributions by web acl id") - - def list_invalidations(self, distribution_id): - try: - paginator = self.client.get_paginator('list_invalidations') - result = paginator.paginate(DistributionId=distribution_id).build_full_result() - return result.get('InvalidationList', {}).get('Items', []) - except botocore.exceptions.ClientError as e: - self.module.fail_json_aws(e, msg="Error listing invalidations") - - def list_streaming_distributions(self, keyed=True): - try: - paginator = self.client.get_paginator('list_streaming_distributions') - result = paginator.paginate().build_full_result() - streaming_distribution_list = result.get('StreamingDistributionList', {}).get('Items', []) - if not keyed: - return streaming_distribution_list - return self.keyed_list_helper(streaming_distribution_list) - except botocore.exceptions.ClientError as e: - self.module.fail_json_aws(e, msg="Error listing streaming distributions") - - def summary(self): - summary_dict = {} - summary_dict.update(self.summary_get_distribution_list(False)) - summary_dict.update(self.summary_get_distribution_list(True)) - summary_dict.update(self.summary_get_origin_access_identity_list()) - return summary_dict - - def summary_get_origin_access_identity_list(self): - try: - origin_access_identity_list = {'origin_access_identities': []} - origin_access_identities = self.list_origin_access_identities() - for origin_access_identity in origin_access_identities: - oai_id = origin_access_identity['Id'] - oai_full_response = self.get_origin_access_identity(oai_id) - oai_summary = {'Id': oai_id, 'ETag': oai_full_response['ETag']} - origin_access_identity_list['origin_access_identities'].append(oai_summary) - return origin_access_identity_list - except botocore.exceptions.ClientError as e: - self.module.fail_json_aws(e, msg="Error generating summary of origin access identities") - - def summary_get_distribution_list(self, streaming=False): - try: - list_name = 'streaming_distributions' if streaming else 'distributions' - key_list = ['Id', 'ARN', 'Status', 'LastModifiedTime', 'DomainName', 'Comment', 'PriceClass', 'Enabled'] - distribution_list = {list_name: []} - distributions = self.list_streaming_distributions(False) if streaming else self.list_distributions(False) - for dist in distributions: - temp_distribution = {} - for key_name in key_list: - temp_distribution[key_name] = dist[key_name] - temp_distribution['Aliases'] = [alias for alias in dist['Aliases'].get('Items', [])] - temp_distribution['ETag'] = self.get_etag_from_distribution_id(dist['Id'], streaming) - if not streaming: - temp_distribution['WebACLId'] = dist['WebACLId'] - invalidation_ids = self.get_list_of_invalidation_ids_from_distribution_id(dist['Id']) - if invalidation_ids: - temp_distribution['Invalidations'] = invalidation_ids - resource_tags = self.client.list_tags_for_resource(Resource=dist['ARN']) - temp_distribution['Tags'] = boto3_tag_list_to_ansible_dict(resource_tags['Tags'].get('Items', [])) - distribution_list[list_name].append(temp_distribution) - return distribution_list - except botocore.exceptions.ClientError as e: - self.module.fail_json_aws(e, msg="Error generating summary of distributions") - except Exception as e: - self.module.fail_json_aws(e, msg="Error generating summary of distributions") - - def get_etag_from_distribution_id(self, distribution_id, streaming): - distribution = {} - if not streaming: - distribution = self.get_distribution(distribution_id) - else: - distribution = self.get_streaming_distribution(distribution_id) - return distribution['ETag'] - - def get_list_of_invalidation_ids_from_distribution_id(self, distribution_id): - try: - invalidation_ids = [] - invalidations = self.list_invalidations(distribution_id) - for invalidation in invalidations: - invalidation_ids.append(invalidation['Id']) - return invalidation_ids - except botocore.exceptions.ClientError as e: - self.module.fail_json_aws(e, msg="Error getting list of invalidation ids") - - def get_distribution_id_from_domain_name(self, domain_name): - try: - distribution_id = "" - distributions = self.list_distributions(False) - distributions += self.list_streaming_distributions(False) - for dist in distributions: - if 'Items' in dist['Aliases']: - for alias in dist['Aliases']['Items']: - if str(alias).lower() == domain_name.lower(): - distribution_id = dist['Id'] - break - return distribution_id - except botocore.exceptions.ClientError as e: - self.module.fail_json_aws(e, msg="Error getting distribution id from domain name") - - def get_aliases_from_distribution_id(self, distribution_id): - try: - distribution = self.get_distribution(distribution_id) - return distribution['DistributionConfig']['Aliases'].get('Items', []) - except botocore.exceptions.ClientError as e: - self.module.fail_json_aws(e, msg="Error getting list of aliases from distribution_id") - - def keyed_list_helper(self, list_to_key): - keyed_list = dict() - for item in list_to_key: - distribution_id = item['Id'] - if 'Items' in item['Aliases']: - aliases = item['Aliases']['Items'] - for alias in aliases: - keyed_list.update({alias: item}) - keyed_list.update({distribution_id: item}) - return keyed_list diff --git a/lib/ansible/module_utils/aws/core.py b/lib/ansible/module_utils/aws/core.py deleted file mode 100644 index c4527b6deb..0000000000 --- a/lib/ansible/module_utils/aws/core.py +++ /dev/null @@ -1,335 +0,0 @@ -# -# Copyright 2017 Michael De La Rue | Ansible -# -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see <http://www.gnu.org/licenses/>. - -"""This module adds shared support for generic Amazon AWS modules - -**This code is not yet ready for use in user modules. As of 2017** -**and through to 2018, the interface is likely to change** -**aggressively as the exact correct interface for ansible AWS modules** -**is identified. In particular, until this notice goes away or is** -**changed, methods may disappear from the interface. Please don't** -**publish modules using this except directly to the main Ansible** -**development repository.** - -In order to use this module, include it as part of a custom -module as shown below. - - from ansible.module_utils.aws import AnsibleAWSModule - module = AnsibleAWSModule(argument_spec=dictionary, supports_check_mode=boolean - mutually_exclusive=list1, required_together=list2) - -The 'AnsibleAWSModule' module provides similar, but more restricted, -interfaces to the normal Ansible module. It also includes the -additional methods for connecting to AWS using the standard module arguments - - m.resource('lambda') # - get an AWS connection as a boto3 resource. - -or - - m.client('sts') # - get an AWS connection as a boto3 client. - -To make use of AWSRetry easier, it can now be wrapped around any call from a -module-created client. To add retries to a client, create a client: - - m.client('ec2', retry_decorator=AWSRetry.jittered_backoff(retries=10)) - -Any calls from that client can be made to use the decorator passed at call-time -using the `aws_retry` argument. By default, no retries are used. - - ec2 = m.client('ec2', retry_decorator=AWSRetry.jittered_backoff(retries=10)) - ec2.describe_instances(InstanceIds=['i-123456789'], aws_retry=True) - -The call will be retried the specified number of times, so the calling functions -don't need to be wrapped in the backoff decorator. -""" - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import re -import logging -import traceback -from functools import wraps -from distutils.version import LooseVersion - -try: - from cStringIO import StringIO -except ImportError: - # Python 3 - from io import StringIO - -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils._text import to_native -from ansible.module_utils.ec2 import HAS_BOTO3, camel_dict_to_snake_dict, ec2_argument_spec, boto3_conn -from ansible.module_utils.ec2 import get_aws_connection_info, get_aws_region - -# We will also export HAS_BOTO3 so end user modules can use it. -__all__ = ('AnsibleAWSModule', 'HAS_BOTO3', 'is_boto3_error_code') - - -class AnsibleAWSModule(object): - """An ansible module class for AWS modules - - AnsibleAWSModule provides an a class for building modules which - connect to Amazon Web Services. The interface is currently more - restricted than the basic module class with the aim that later the - basic module class can be reduced. If you find that any key - feature is missing please contact the author/Ansible AWS team - (available on #ansible-aws on IRC) to request the additional - features needed. - """ - default_settings = { - "default_args": True, - "check_boto3": True, - "auto_retry": True, - "module_class": AnsibleModule - } - - def __init__(self, **kwargs): - local_settings = {} - for key in AnsibleAWSModule.default_settings: - try: - local_settings[key] = kwargs.pop(key) - except KeyError: - local_settings[key] = AnsibleAWSModule.default_settings[key] - self.settings = local_settings - - if local_settings["default_args"]: - # ec2_argument_spec contains the region so we use that; there's a patch coming which - # will add it to aws_argument_spec so if that's accepted then later we should change - # over - argument_spec_full = ec2_argument_spec() - try: - argument_spec_full.update(kwargs["argument_spec"]) - except (TypeError, NameError): - pass - kwargs["argument_spec"] = argument_spec_full - - self._module = AnsibleAWSModule.default_settings["module_class"](**kwargs) - - if local_settings["check_boto3"] and not HAS_BOTO3: - self._module.fail_json( - msg=missing_required_lib('botocore or boto3')) - - self.check_mode = self._module.check_mode - self._diff = self._module._diff - self._name = self._module._name - - self._botocore_endpoint_log_stream = StringIO() - self.logger = None - if self.params.get('debug_botocore_endpoint_logs'): - self.logger = logging.getLogger('botocore.endpoint') - self.logger.setLevel(logging.DEBUG) - self.logger.addHandler(logging.StreamHandler(self._botocore_endpoint_log_stream)) - - @property - def params(self): - return self._module.params - - def _get_resource_action_list(self): - actions = [] - for ln in self._botocore_endpoint_log_stream.getvalue().split('\n'): - ln = ln.strip() - if not ln: - continue - found_operational_request = re.search(r"OperationModel\(name=.*?\)", ln) - if found_operational_request: - operation_request = found_operational_request.group(0)[20:-1] - resource = re.search(r"https://.*?\.", ln).group(0)[8:-1] - actions.append("{0}:{1}".format(resource, operation_request)) - return list(set(actions)) - - def exit_json(self, *args, **kwargs): - if self.params.get('debug_botocore_endpoint_logs'): - kwargs['resource_actions'] = self._get_resource_action_list() - return self._module.exit_json(*args, **kwargs) - - def fail_json(self, *args, **kwargs): - if self.params.get('debug_botocore_endpoint_logs'): - kwargs['resource_actions'] = self._get_resource_action_list() - return self._module.fail_json(*args, **kwargs) - - def debug(self, *args, **kwargs): - return self._module.debug(*args, **kwargs) - - def warn(self, *args, **kwargs): - return self._module.warn(*args, **kwargs) - - def deprecate(self, *args, **kwargs): - return self._module.deprecate(*args, **kwargs) - - def boolean(self, *args, **kwargs): - return self._module.boolean(*args, **kwargs) - - def md5(self, *args, **kwargs): - return self._module.md5(*args, **kwargs) - - def client(self, service, retry_decorator=None): - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(self, boto3=True) - conn = boto3_conn(self, conn_type='client', resource=service, - region=region, endpoint=ec2_url, **aws_connect_kwargs) - return conn if retry_decorator is None else _RetryingBotoClientWrapper(conn, retry_decorator) - - def resource(self, service): - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(self, boto3=True) - return boto3_conn(self, conn_type='resource', resource=service, - region=region, endpoint=ec2_url, **aws_connect_kwargs) - - @property - def region(self, boto3=True): - return get_aws_region(self, boto3) - - def fail_json_aws(self, exception, msg=None): - """call fail_json with processed exception - - function for converting exceptions thrown by AWS SDK modules, - botocore, boto3 and boto, into nice error messages. - """ - last_traceback = traceback.format_exc() - - # to_native is trusted to handle exceptions that str() could - # convert to text. - try: - except_msg = to_native(exception.message) - except AttributeError: - except_msg = to_native(exception) - - if msg is not None: - message = '{0}: {1}'.format(msg, except_msg) - else: - message = except_msg - - try: - response = exception.response - except AttributeError: - response = None - - failure = dict( - msg=message, - exception=last_traceback, - **self._gather_versions() - ) - - if response is not None: - failure.update(**camel_dict_to_snake_dict(response)) - - self.fail_json(**failure) - - def _gather_versions(self): - """Gather AWS SDK (boto3 and botocore) dependency versions - - Returns {'boto3_version': str, 'botocore_version': str} - Returns {} if neither are installed - """ - if not HAS_BOTO3: - return {} - import boto3 - import botocore - return dict(boto3_version=boto3.__version__, - botocore_version=botocore.__version__) - - def boto3_at_least(self, desired): - """Check if the available boto3 version is greater than or equal to a desired version. - - Usage: - if module.params.get('assign_ipv6_address') and not module.boto3_at_least('1.4.4'): - # conditionally fail on old boto3 versions if a specific feature is not supported - module.fail_json(msg="Boto3 can't deal with EC2 IPv6 addresses before version 1.4.4.") - """ - existing = self._gather_versions() - return LooseVersion(existing['boto3_version']) >= LooseVersion(desired) - - def botocore_at_least(self, desired): - """Check if the available botocore version is greater than or equal to a desired version. - - Usage: - if not module.botocore_at_least('1.2.3'): - module.fail_json(msg='The Serverless Elastic Load Compute Service is not in botocore before v1.2.3') - if not module.botocore_at_least('1.5.3'): - module.warn('Botocore did not include waiters for Service X before 1.5.3. ' - 'To wait until Service X resources are fully available, update botocore.') - """ - existing = self._gather_versions() - return LooseVersion(existing['botocore_version']) >= LooseVersion(desired) - - -class _RetryingBotoClientWrapper(object): - __never_wait = ( - 'get_paginator', 'can_paginate', - 'get_waiter', 'generate_presigned_url', - ) - - def __init__(self, client, retry): - self.client = client - self.retry = retry - - def _create_optional_retry_wrapper_function(self, unwrapped): - retrying_wrapper = self.retry(unwrapped) - - @wraps(unwrapped) - def deciding_wrapper(aws_retry=False, *args, **kwargs): - if aws_retry: - return retrying_wrapper(*args, **kwargs) - else: - return unwrapped(*args, **kwargs) - return deciding_wrapper - - def __getattr__(self, name): - unwrapped = getattr(self.client, name) - if name in self.__never_wait: - return unwrapped - elif callable(unwrapped): - wrapped = self._create_optional_retry_wrapper_function(unwrapped) - setattr(self, name, wrapped) - return wrapped - else: - return unwrapped - - -def is_boto3_error_code(code, e=None): - """Check if the botocore exception is raised by a specific error code. - - Returns ClientError if the error code matches, a dummy exception if it does not have an error code or does not match - - Example: - try: - ec2.describe_instances(InstanceIds=['potato']) - except is_boto3_error_code('InvalidInstanceID.Malformed'): - # handle the error for that code case - except botocore.exceptions.ClientError as e: - # handle the generic error case for all other codes - """ - from botocore.exceptions import ClientError - if e is None: - import sys - dummy, e, dummy = sys.exc_info() - if isinstance(e, ClientError) and e.response['Error']['Code'] == code: - return ClientError - return type('NeverEverRaisedException', (Exception,), {}) - - -def get_boto3_client_method_parameters(client, method_name, required=False): - op = client.meta.method_to_api_mapping.get(method_name) - input_shape = client._service_model.operation_model(op).input_shape - if not input_shape: - parameters = [] - elif required: - parameters = list(input_shape.required_members) - else: - parameters = list(input_shape.members.keys()) - return parameters diff --git a/lib/ansible/module_utils/aws/direct_connect.py b/lib/ansible/module_utils/aws/direct_connect.py deleted file mode 100644 index 6419739095..0000000000 --- a/lib/ansible/module_utils/aws/direct_connect.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright (c) 2017 Ansible Project -# -# This code is part of Ansible, but is an independent component. -# This particular file snippet, and this file snippet only, is BSD licensed. -# Modules you write using this snippet, which is embedded dynamically by Ansible -# still belong to the author of the module, and may assign their own license -# to the complete work. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# -""" -This module adds shared support for Direct Connect modules. -""" - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import traceback -try: - import botocore -except ImportError: - pass -from ansible.module_utils.ec2 import camel_dict_to_snake_dict - - -class DirectConnectError(Exception): - def __init__(self, msg, last_traceback=None, exception=None): - self.msg = msg - self.last_traceback = last_traceback - self.exception = exception - - -def delete_connection(client, connection_id): - try: - client.delete_connection(connectionId=connection_id) - except botocore.exceptions.ClientError as e: - raise DirectConnectError(msg="Failed to delete DirectConnection {0}.".format(connection_id), - last_traceback=traceback.format_exc(), - exception=e) - - -def associate_connection_and_lag(client, connection_id, lag_id): - try: - client.associate_connection_with_lag(connectionId=connection_id, - lagId=lag_id) - except botocore.exceptions.ClientError as e: - raise DirectConnectError(msg="Failed to associate Direct Connect connection {0}" - " with link aggregation group {1}.".format(connection_id, lag_id), - last_traceback=traceback.format_exc(), - exception=e) - - -def disassociate_connection_and_lag(client, connection_id, lag_id): - try: - client.disassociate_connection_from_lag(connectionId=connection_id, - lagId=lag_id) - except botocore.exceptions.ClientError as e: - raise DirectConnectError(msg="Failed to disassociate Direct Connect connection {0}" - " from link aggregation group {1}.".format(connection_id, lag_id), - last_traceback=traceback.format_exc(), - exception=e) - - -def delete_virtual_interface(client, virtual_interface): - try: - client.delete_virtual_interface(virtualInterfaceId=virtual_interface) - except botocore.exceptions.ClientError as e: - raise DirectConnectError(msg="Could not delete virtual interface {0}".format(virtual_interface), - last_traceback=traceback.format_exc(), - exception=e) diff --git a/lib/ansible/module_utils/aws/elb_utils.py b/lib/ansible/module_utils/aws/elb_utils.py deleted file mode 100644 index 4027d3309a..0000000000 --- a/lib/ansible/module_utils/aws/elb_utils.py +++ /dev/null @@ -1,112 +0,0 @@ -# Copyright (c) 2017 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 - -from ansible.module_utils.ec2 import AWSRetry - -# Non-ansible imports -try: - from botocore.exceptions import BotoCoreError, ClientError -except ImportError: - pass - - -def get_elb(connection, module, elb_name): - """ - Get an ELB based on name. If not found, return None. - - :param connection: AWS boto3 elbv2 connection - :param module: Ansible module - :param elb_name: Name of load balancer to get - :return: boto3 ELB dict or None if not found - """ - try: - return _get_elb(connection, module, elb_name) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e) - - -@AWSRetry.jittered_backoff() -def _get_elb(connection, module, elb_name): - """ - Get an ELB based on name using AWSRetry. If not found, return None. - - :param connection: AWS boto3 elbv2 connection - :param module: Ansible module - :param elb_name: Name of load balancer to get - :return: boto3 ELB dict or None if not found - """ - - try: - load_balancer_paginator = connection.get_paginator('describe_load_balancers') - return (load_balancer_paginator.paginate(Names=[elb_name]).build_full_result())['LoadBalancers'][0] - except (BotoCoreError, ClientError) as e: - if e.response['Error']['Code'] == 'LoadBalancerNotFound': - return None - else: - raise e - - -def get_elb_listener(connection, module, elb_arn, listener_port): - """ - Get an ELB listener based on the port provided. If not found, return None. - - :param connection: AWS boto3 elbv2 connection - :param module: Ansible module - :param elb_arn: ARN of the ELB to look at - :param listener_port: Port of the listener to look for - :return: boto3 ELB listener dict or None if not found - """ - - try: - listener_paginator = connection.get_paginator('describe_listeners') - listeners = (AWSRetry.jittered_backoff()(listener_paginator.paginate)(LoadBalancerArn=elb_arn).build_full_result())['Listeners'] - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e) - - l = None - - for listener in listeners: - if listener['Port'] == listener_port: - l = listener - break - - return l - - -def get_elb_listener_rules(connection, module, listener_arn): - """ - Get rules for a particular ELB listener using the listener ARN. - - :param connection: AWS boto3 elbv2 connection - :param module: Ansible module - :param listener_arn: ARN of the ELB listener - :return: boto3 ELB rules list - """ - - try: - return AWSRetry.jittered_backoff()(connection.describe_rules)(ListenerArn=listener_arn)['Rules'] - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e) - - -def convert_tg_name_to_arn(connection, module, tg_name): - """ - Get ARN of a target group using the target group's name - - :param connection: AWS boto3 elbv2 connection - :param module: Ansible module - :param tg_name: Name of the target group - :return: target group ARN string - """ - - try: - response = AWSRetry.jittered_backoff()(connection.describe_target_groups)(Names=[tg_name]) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e) - - tg_arn = response['TargetGroups'][0]['TargetGroupArn'] - - return tg_arn diff --git a/lib/ansible/module_utils/aws/elbv2.py b/lib/ansible/module_utils/aws/elbv2.py deleted file mode 100644 index 0b68bde3a4..0000000000 --- a/lib/ansible/module_utils/aws/elbv2.py +++ /dev/null @@ -1,891 +0,0 @@ -# Copyright (c) 2017 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 - -# Ansible imports -from ansible.module_utils.ec2 import camel_dict_to_snake_dict, get_ec2_security_group_ids_from_names, \ - ansible_dict_to_boto3_tag_list, boto3_tag_list_to_ansible_dict, compare_policies as compare_dicts, \ - AWSRetry -from ansible.module_utils.aws.elb_utils import get_elb, get_elb_listener, convert_tg_name_to_arn - -# Non-ansible imports -try: - from botocore.exceptions import BotoCoreError, ClientError -except ImportError: - pass -import traceback -from copy import deepcopy - - -class ElasticLoadBalancerV2(object): - - def __init__(self, connection, module): - - self.connection = connection - self.module = module - self.changed = False - self.new_load_balancer = False - self.scheme = module.params.get("scheme") - self.name = module.params.get("name") - self.subnet_mappings = module.params.get("subnet_mappings") - self.subnets = module.params.get("subnets") - self.deletion_protection = module.params.get("deletion_protection") - self.wait = module.params.get("wait") - - if module.params.get("tags") is not None: - self.tags = ansible_dict_to_boto3_tag_list(module.params.get("tags")) - else: - self.tags = None - self.purge_tags = module.params.get("purge_tags") - - self.elb = get_elb(connection, module, self.name) - if self.elb is not None: - self.elb_attributes = self.get_elb_attributes() - self.elb['tags'] = self.get_elb_tags() - else: - self.elb_attributes = None - - def wait_for_status(self, elb_arn): - """ - Wait for load balancer to reach 'active' status - - :param elb_arn: The load balancer ARN - :return: - """ - - try: - waiter = self.connection.get_waiter('load_balancer_available') - waiter.wait(LoadBalancerArns=[elb_arn]) - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e) - - def get_elb_attributes(self): - """ - Get load balancer attributes - - :return: - """ - - try: - attr_list = AWSRetry.jittered_backoff()( - self.connection.describe_load_balancer_attributes - )(LoadBalancerArn=self.elb['LoadBalancerArn'])['Attributes'] - - elb_attributes = boto3_tag_list_to_ansible_dict(attr_list) - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e) - - # Replace '.' with '_' in attribute key names to make it more Ansibley - return dict((k.replace('.', '_'), v) for k, v in elb_attributes.items()) - - def update_elb_attributes(self): - """ - Update the elb_attributes parameter - :return: - """ - self.elb_attributes = self.get_elb_attributes() - - def get_elb_tags(self): - """ - Get load balancer tags - - :return: - """ - - try: - return AWSRetry.jittered_backoff()( - self.connection.describe_tags - )(ResourceArns=[self.elb['LoadBalancerArn']])['TagDescriptions'][0]['Tags'] - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e) - - def delete_tags(self, tags_to_delete): - """ - Delete elb tags - - :return: - """ - - try: - AWSRetry.jittered_backoff()( - self.connection.remove_tags - )(ResourceArns=[self.elb['LoadBalancerArn']], TagKeys=tags_to_delete) - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e) - - self.changed = True - - def modify_tags(self): - """ - Modify elb tags - - :return: - """ - - try: - AWSRetry.jittered_backoff()( - self.connection.add_tags - )(ResourceArns=[self.elb['LoadBalancerArn']], Tags=self.tags) - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e) - - self.changed = True - - def delete(self): - """ - Delete elb - :return: - """ - - try: - AWSRetry.jittered_backoff()( - self.connection.delete_load_balancer - )(LoadBalancerArn=self.elb['LoadBalancerArn']) - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e) - - self.changed = True - - def compare_subnets(self): - """ - Compare user subnets with current ELB subnets - - :return: bool True if they match otherwise False - """ - - subnet_mapping_id_list = [] - subnet_mappings = [] - - # Check if we're dealing with subnets or subnet_mappings - if self.subnets is not None: - # Convert subnets to subnet_mappings format for comparison - for subnet in self.subnets: - subnet_mappings.append({'SubnetId': subnet}) - - if self.subnet_mappings is not None: - # Use this directly since we're comparing as a mapping - subnet_mappings = self.subnet_mappings - - # Build a subnet_mapping style struture of what's currently - # on the load balancer - for subnet in self.elb['AvailabilityZones']: - this_mapping = {'SubnetId': subnet['SubnetId']} - for address in subnet.get('LoadBalancerAddresses', []): - if 'AllocationId' in address: - this_mapping['AllocationId'] = address['AllocationId'] - break - - subnet_mapping_id_list.append(this_mapping) - - return set(frozenset(mapping.items()) for mapping in subnet_mapping_id_list) == set(frozenset(mapping.items()) for mapping in subnet_mappings) - - def modify_subnets(self): - """ - Modify elb subnets to match module parameters - :return: - """ - - try: - AWSRetry.jittered_backoff()( - self.connection.set_subnets - )(LoadBalancerArn=self.elb['LoadBalancerArn'], Subnets=self.subnets) - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e) - - self.changed = True - - def update(self): - """ - Update the elb from AWS - :return: - """ - - self.elb = get_elb(self.connection, self.module, self.module.params.get("name")) - self.elb['tags'] = self.get_elb_tags() - - -class ApplicationLoadBalancer(ElasticLoadBalancerV2): - - def __init__(self, connection, connection_ec2, module): - """ - - :param connection: boto3 connection - :param module: Ansible module - """ - super(ApplicationLoadBalancer, self).__init__(connection, module) - - self.connection_ec2 = connection_ec2 - - # Ansible module parameters specific to ALBs - self.type = 'application' - if module.params.get('security_groups') is not None: - try: - self.security_groups = AWSRetry.jittered_backoff()( - get_ec2_security_group_ids_from_names - )(module.params.get('security_groups'), self.connection_ec2, boto3=True) - except ValueError as e: - self.module.fail_json(msg=str(e), exception=traceback.format_exc()) - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e) - else: - self.security_groups = module.params.get('security_groups') - self.access_logs_enabled = module.params.get("access_logs_enabled") - self.access_logs_s3_bucket = module.params.get("access_logs_s3_bucket") - self.access_logs_s3_prefix = module.params.get("access_logs_s3_prefix") - self.idle_timeout = module.params.get("idle_timeout") - self.http2 = module.params.get("http2") - - if self.elb is not None and self.elb['Type'] != 'application': - self.module.fail_json(msg="The load balancer type you are trying to manage is not application. Try elb_network_lb module instead.") - - def create_elb(self): - """ - Create a load balancer - :return: - """ - - # Required parameters - params = dict() - params['Name'] = self.name - params['Type'] = self.type - - # Other parameters - if self.subnets is not None: - params['Subnets'] = self.subnets - if self.subnet_mappings is not None: - params['SubnetMappings'] = self.subnet_mappings - if self.security_groups is not None: - params['SecurityGroups'] = self.security_groups - params['Scheme'] = self.scheme - if self.tags: - params['Tags'] = self.tags - - try: - self.elb = AWSRetry.jittered_backoff()(self.connection.create_load_balancer)(**params)['LoadBalancers'][0] - self.changed = True - self.new_load_balancer = True - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e) - - if self.wait: - self.wait_for_status(self.elb['LoadBalancerArn']) - - def modify_elb_attributes(self): - """ - Update Application ELB attributes if required - - :return: - """ - - update_attributes = [] - - if self.access_logs_enabled is not None and str(self.access_logs_enabled).lower() != self.elb_attributes['access_logs_s3_enabled']: - update_attributes.append({'Key': 'access_logs.s3.enabled', 'Value': str(self.access_logs_enabled).lower()}) - if self.access_logs_s3_bucket is not None and self.access_logs_s3_bucket != self.elb_attributes['access_logs_s3_bucket']: - update_attributes.append({'Key': 'access_logs.s3.bucket', 'Value': self.access_logs_s3_bucket}) - if self.access_logs_s3_prefix is not None and self.access_logs_s3_prefix != self.elb_attributes['access_logs_s3_prefix']: - update_attributes.append({'Key': 'access_logs.s3.prefix', 'Value': self.access_logs_s3_prefix}) - if self.deletion_protection is not None and str(self.deletion_protection).lower() != self.elb_attributes['deletion_protection_enabled']: - update_attributes.append({'Key': 'deletion_protection.enabled', 'Value': str(self.deletion_protection).lower()}) - if self.idle_timeout is not None and str(self.idle_timeout) != self.elb_attributes['idle_timeout_timeout_seconds']: - update_attributes.append({'Key': 'idle_timeout.timeout_seconds', 'Value': str(self.idle_timeout)}) - if self.http2 is not None and str(self.http2).lower() != self.elb_attributes['routing_http2_enabled']: - update_attributes.append({'Key': 'routing.http2.enabled', 'Value': str(self.http2).lower()}) - - if update_attributes: - try: - AWSRetry.jittered_backoff()( - self.connection.modify_load_balancer_attributes - )(LoadBalancerArn=self.elb['LoadBalancerArn'], Attributes=update_attributes) - self.changed = True - except (BotoCoreError, ClientError) as e: - # Something went wrong setting attributes. If this ELB was created during this task, delete it to leave a consistent state - if self.new_load_balancer: - AWSRetry.jittered_backoff()(self.connection.delete_load_balancer)(LoadBalancerArn=self.elb['LoadBalancerArn']) - self.module.fail_json_aws(e) - - def compare_security_groups(self): - """ - Compare user security groups with current ELB security groups - - :return: bool True if they match otherwise False - """ - - if set(self.elb['SecurityGroups']) != set(self.security_groups): - return False - else: - return True - - def modify_security_groups(self): - """ - Modify elb security groups to match module parameters - :return: - """ - - try: - AWSRetry.jittered_backoff()( - self.connection.set_security_groups - )(LoadBalancerArn=self.elb['LoadBalancerArn'], SecurityGroups=self.security_groups) - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e) - - self.changed = True - - -class NetworkLoadBalancer(ElasticLoadBalancerV2): - - def __init__(self, connection, connection_ec2, module): - - """ - - :param connection: boto3 connection - :param module: Ansible module - """ - super(NetworkLoadBalancer, self).__init__(connection, module) - - self.connection_ec2 = connection_ec2 - - # Ansible module parameters specific to NLBs - self.type = 'network' - self.cross_zone_load_balancing = module.params.get('cross_zone_load_balancing') - - if self.elb is not None and self.elb['Type'] != 'network': - self.module.fail_json(msg="The load balancer type you are trying to manage is not network. Try elb_application_lb module instead.") - - def create_elb(self): - """ - Create a load balancer - :return: - """ - - # Required parameters - params = dict() - params['Name'] = self.name - params['Type'] = self.type - - # Other parameters - if self.subnets is not None: - params['Subnets'] = self.subnets - if self.subnet_mappings is not None: - params['SubnetMappings'] = self.subnet_mappings - params['Scheme'] = self.scheme - if self.tags: - params['Tags'] = self.tags - - try: - self.elb = AWSRetry.jittered_backoff()(self.connection.create_load_balancer)(**params)['LoadBalancers'][0] - self.changed = True - self.new_load_balancer = True - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e) - - if self.wait: - self.wait_for_status(self.elb['LoadBalancerArn']) - - def modify_elb_attributes(self): - """ - Update Network ELB attributes if required - - :return: - """ - - update_attributes = [] - - if self.cross_zone_load_balancing is not None and str(self.cross_zone_load_balancing).lower() != \ - self.elb_attributes['load_balancing_cross_zone_enabled']: - update_attributes.append({'Key': 'load_balancing.cross_zone.enabled', 'Value': str(self.cross_zone_load_balancing).lower()}) - if self.deletion_protection is not None and str(self.deletion_protection).lower() != self.elb_attributes['deletion_protection_enabled']: - update_attributes.append({'Key': 'deletion_protection.enabled', 'Value': str(self.deletion_protection).lower()}) - - if update_attributes: - try: - AWSRetry.jittered_backoff()( - self.connection.modify_load_balancer_attributes - )(LoadBalancerArn=self.elb['LoadBalancerArn'], Attributes=update_attributes) - self.changed = True - except (BotoCoreError, ClientError) as e: - # Something went wrong setting attributes. If this ELB was created during this task, delete it to leave a consistent state - if self.new_load_balancer: - AWSRetry.jittered_backoff()(self.connection.delete_load_balancer)(LoadBalancerArn=self.elb['LoadBalancerArn']) - self.module.fail_json_aws(e) - - def modify_subnets(self): - """ - Modify elb subnets to match module parameters (unsupported for NLB) - :return: - """ - - self.module.fail_json(msg='Modifying subnets and elastic IPs is not supported for Network Load Balancer') - - -class ELBListeners(object): - - def __init__(self, connection, module, elb_arn): - - self.connection = connection - self.module = module - self.elb_arn = elb_arn - listeners = module.params.get("listeners") - if listeners is not None: - # Remove suboption argspec defaults of None from each listener - listeners = [dict((x, listener_dict[x]) for x in listener_dict if listener_dict[x] is not None) for listener_dict in listeners] - self.listeners = self._ensure_listeners_default_action_has_arn(listeners) - self.current_listeners = self._get_elb_listeners() - self.purge_listeners = module.params.get("purge_listeners") - self.changed = False - - def update(self): - """ - Update the listeners for the ELB - - :return: - """ - self.current_listeners = self._get_elb_listeners() - - def _get_elb_listeners(self): - """ - Get ELB listeners - - :return: - """ - - try: - listener_paginator = self.connection.get_paginator('describe_listeners') - return (AWSRetry.jittered_backoff()(listener_paginator.paginate)(LoadBalancerArn=self.elb_arn).build_full_result())['Listeners'] - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e) - - def _ensure_listeners_default_action_has_arn(self, listeners): - """ - If a listener DefaultAction has been passed with a Target Group Name instead of ARN, lookup the ARN and - replace the name. - - :param listeners: a list of listener dicts - :return: the same list of dicts ensuring that each listener DefaultActions dict has TargetGroupArn key. If a TargetGroupName key exists, it is removed. - """ - - if not listeners: - listeners = [] - - fixed_listeners = [] - for listener in listeners: - fixed_actions = [] - for action in listener['DefaultActions']: - if 'TargetGroupName' in action: - action['TargetGroupArn'] = convert_tg_name_to_arn(self.connection, - self.module, - action['TargetGroupName']) - del action['TargetGroupName'] - fixed_actions.append(action) - listener['DefaultActions'] = fixed_actions - fixed_listeners.append(listener) - - return fixed_listeners - - def compare_listeners(self): - """ - - :return: - """ - listeners_to_modify = [] - listeners_to_delete = [] - listeners_to_add = deepcopy(self.listeners) - - # Check each current listener port to see if it's been passed to the module - for current_listener in self.current_listeners: - current_listener_passed_to_module = False - for new_listener in self.listeners[:]: - new_listener['Port'] = int(new_listener['Port']) - if current_listener['Port'] == new_listener['Port']: - current_listener_passed_to_module = True - # Remove what we match so that what is left can be marked as 'to be added' - listeners_to_add.remove(new_listener) - modified_listener = self._compare_listener(current_listener, new_listener) - if modified_listener: - modified_listener['Port'] = current_listener['Port'] - modified_listener['ListenerArn'] = current_listener['ListenerArn'] - listeners_to_modify.append(modified_listener) - break - - # If the current listener was not matched against passed listeners and purge is True, mark for removal - if not current_listener_passed_to_module and self.purge_listeners: - listeners_to_delete.append(current_listener['ListenerArn']) - - return listeners_to_add, listeners_to_modify, listeners_to_delete - - def _compare_listener(self, current_listener, new_listener): - """ - Compare two listeners. - - :param current_listener: - :param new_listener: - :return: - """ - - modified_listener = {} - - # Port - if current_listener['Port'] != new_listener['Port']: - modified_listener['Port'] = new_listener['Port'] - - # Protocol - if current_listener['Protocol'] != new_listener['Protocol']: - modified_listener['Protocol'] = new_listener['Protocol'] - - # If Protocol is HTTPS, check additional attributes - if current_listener['Protocol'] == 'HTTPS' and new_listener['Protocol'] == 'HTTPS': - # Cert - if current_listener['SslPolicy'] != new_listener['SslPolicy']: - modified_listener['SslPolicy'] = new_listener['SslPolicy'] - if current_listener['Certificates'][0]['CertificateArn'] != new_listener['Certificates'][0]['CertificateArn']: - modified_listener['Certificates'] = [] - modified_listener['Certificates'].append({}) - modified_listener['Certificates'][0]['CertificateArn'] = new_listener['Certificates'][0]['CertificateArn'] - elif current_listener['Protocol'] != 'HTTPS' and new_listener['Protocol'] == 'HTTPS': - modified_listener['SslPolicy'] = new_listener['SslPolicy'] - modified_listener['Certificates'] = [] - modified_listener['Certificates'].append({}) - modified_listener['Certificates'][0]['CertificateArn'] = new_listener['Certificates'][0]['CertificateArn'] - - # Default action - - # Check proper rule format on current listener - if len(current_listener['DefaultActions']) > 1: - for action in current_listener['DefaultActions']: - if 'Order' not in action: - self.module.fail_json(msg="'Order' key not found in actions. " - "installed version of botocore does not support " - "multiple actions, please upgrade botocore to version " - "1.10.30 or higher") - - # If the lengths of the actions are the same, we'll have to verify that the - # contents of those actions are the same - if len(current_listener['DefaultActions']) == len(new_listener['DefaultActions']): - # if actions have just one element, compare the contents and then update if - # they're different - if len(current_listener['DefaultActions']) == 1 and len(new_listener['DefaultActions']) == 1: - if current_listener['DefaultActions'] != new_listener['DefaultActions']: - modified_listener['DefaultActions'] = new_listener['DefaultActions'] - # if actions have multiple elements, we'll have to order them first before comparing. - # multiple actions will have an 'Order' key for this purpose - else: - current_actions_sorted = sorted(current_listener['DefaultActions'], key=lambda x: x['Order']) - new_actions_sorted = sorted(new_listener['DefaultActions'], key=lambda x: x['Order']) - - # the AWS api won't return the client secret, so we'll have to remove it - # or the module will always see the new and current actions as different - # and try to apply the same config - new_actions_sorted_no_secret = [] - for action in new_actions_sorted: - # the secret is currently only defined in the oidc config - if action['Type'] == 'authenticate-oidc': - action['AuthenticateOidcConfig'].pop('ClientSecret') - new_actions_sorted_no_secret.append(action) - else: - new_actions_sorted_no_secret.append(action) - - if current_actions_sorted != new_actions_sorted_no_secret: - modified_listener['DefaultActions'] = new_listener['DefaultActions'] - # If the action lengths are different, then replace with the new actions - else: - modified_listener['DefaultActions'] = new_listener['DefaultActions'] - - if modified_listener: - return modified_listener - else: - return None - - -class ELBListener(object): - - def __init__(self, connection, module, listener, elb_arn): - """ - - :param connection: - :param module: - :param listener: - :param elb_arn: - """ - - self.connection = connection - self.module = module - self.listener = listener - self.elb_arn = elb_arn - - def add(self): - - try: - # Rules is not a valid parameter for create_listener - if 'Rules' in self.listener: - self.listener.pop('Rules') - AWSRetry.jittered_backoff()(self.connection.create_listener)(LoadBalancerArn=self.elb_arn, **self.listener) - except (BotoCoreError, ClientError) as e: - if '"Order", must be one of: Type, TargetGroupArn' in str(e): - self.module.fail_json(msg="installed version of botocore does not support " - "multiple actions, please upgrade botocore to version " - "1.10.30 or higher") - else: - self.module.fail_json_aws(e) - - def modify(self): - - try: - # Rules is not a valid parameter for modify_listener - if 'Rules' in self.listener: - self.listener.pop('Rules') - AWSRetry.jittered_backoff()(self.connection.modify_listener)(**self.listener) - except (BotoCoreError, ClientError) as e: - if '"Order", must be one of: Type, TargetGroupArn' in str(e): - self.module.fail_json(msg="installed version of botocore does not support " - "multiple actions, please upgrade botocore to version " - "1.10.30 or higher") - else: - self.module.fail_json_aws(e) - - def delete(self): - - try: - AWSRetry.jittered_backoff()(self.connection.delete_listener)(ListenerArn=self.listener) - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e) - - -class ELBListenerRules(object): - - def __init__(self, connection, module, elb_arn, listener_rules, listener_port): - - self.connection = connection - self.module = module - self.elb_arn = elb_arn - self.rules = self._ensure_rules_action_has_arn(listener_rules) - self.changed = False - - # Get listener based on port so we can use ARN - self.current_listener = get_elb_listener(connection, module, elb_arn, listener_port) - self.listener_arn = self.current_listener['ListenerArn'] - self.rules_to_add = deepcopy(self.rules) - self.rules_to_modify = [] - self.rules_to_delete = [] - - # If the listener exists (i.e. has an ARN) get rules for the listener - if 'ListenerArn' in self.current_listener: - self.current_rules = self._get_elb_listener_rules() - else: - self.current_rules = [] - - def _ensure_rules_action_has_arn(self, rules): - """ - If a rule Action has been passed with a Target Group Name instead of ARN, lookup the ARN and - replace the name. - - :param rules: a list of rule dicts - :return: the same list of dicts ensuring that each rule Actions dict has TargetGroupArn key. If a TargetGroupName key exists, it is removed. - """ - - fixed_rules = [] - for rule in rules: - fixed_actions = [] - for action in rule['Actions']: - if 'TargetGroupName' in action: - action['TargetGroupArn'] = convert_tg_name_to_arn(self.connection, self.module, action['TargetGroupName']) - del action['TargetGroupName'] - fixed_actions.append(action) - rule['Actions'] = fixed_actions - fixed_rules.append(rule) - - return fixed_rules - - def _get_elb_listener_rules(self): - - try: - return AWSRetry.jittered_backoff()(self.connection.describe_rules)(ListenerArn=self.current_listener['ListenerArn'])['Rules'] - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e) - - def _compare_condition(self, current_conditions, condition): - """ - - :param current_conditions: - :param condition: - :return: - """ - - condition_found = False - - for current_condition in current_conditions: - if current_condition.get('SourceIpConfig'): - if (current_condition['Field'] == condition['Field'] and - current_condition['SourceIpConfig']['Values'][0] == condition['SourceIpConfig']['Values'][0]): - condition_found = True - break - elif current_condition['Field'] == condition['Field'] and sorted(current_condition['Values']) == sorted(condition['Values']): - condition_found = True - break - - return condition_found - - def _compare_rule(self, current_rule, new_rule): - """ - - :return: - """ - - modified_rule = {} - - # Priority - if int(current_rule['Priority']) != int(new_rule['Priority']): - modified_rule['Priority'] = new_rule['Priority'] - - # Actions - - # Check proper rule format on current listener - if len(current_rule['Actions']) > 1: - for action in current_rule['Actions']: - if 'Order' not in action: - self.module.fail_json(msg="'Order' key not found in actions. " - "installed version of botocore does not support " - "multiple actions, please upgrade botocore to version " - "1.10.30 or higher") - - # If the lengths of the actions are the same, we'll have to verify that the - # contents of those actions are the same - if len(current_rule['Actions']) == len(new_rule['Actions']): - # if actions have just one element, compare the contents and then update if - # they're different - if len(current_rule['Actions']) == 1 and len(new_rule['Actions']) == 1: - if current_rule['Actions'] != new_rule['Actions']: - modified_rule['Actions'] = new_rule['Actions'] - # if actions have multiple elements, we'll have to order them first before comparing. - # multiple actions will have an 'Order' key for this purpose - else: - current_actions_sorted = sorted(current_rule['Actions'], key=lambda x: x['Order']) - new_actions_sorted = sorted(new_rule['Actions'], key=lambda x: x['Order']) - - # the AWS api won't return the client secret, so we'll have to remove it - # or the module will always see the new and current actions as different - # and try to apply the same config - new_actions_sorted_no_secret = [] - for action in new_actions_sorted: - # the secret is currently only defined in the oidc config - if action['Type'] == 'authenticate-oidc': - action['AuthenticateOidcConfig'].pop('ClientSecret') - new_actions_sorted_no_secret.append(action) - else: - new_actions_sorted_no_secret.append(action) - - if current_actions_sorted != new_actions_sorted_no_secret: - modified_rule['Actions'] = new_rule['Actions'] - # If the action lengths are different, then replace with the new actions - else: - modified_rule['Actions'] = new_rule['Actions'] - - # Conditions - modified_conditions = [] - for condition in new_rule['Conditions']: - if not self._compare_condition(current_rule['Conditions'], condition): - modified_conditions.append(condition) - - if modified_conditions: - modified_rule['Conditions'] = modified_conditions - - return modified_rule - - def compare_rules(self): - """ - - :return: - """ - - rules_to_modify = [] - rules_to_delete = [] - rules_to_add = deepcopy(self.rules) - - for current_rule in self.current_rules: - current_rule_passed_to_module = False - for new_rule in self.rules[:]: - if current_rule['Priority'] == str(new_rule['Priority']): - current_rule_passed_to_module = True - # Remove what we match so that what is left can be marked as 'to be added' - rules_to_add.remove(new_rule) - modified_rule = self._compare_rule(current_rule, new_rule) - if modified_rule: - modified_rule['Priority'] = int(current_rule['Priority']) - modified_rule['RuleArn'] = current_rule['RuleArn'] - modified_rule['Actions'] = new_rule['Actions'] - modified_rule['Conditions'] = new_rule['Conditions'] - rules_to_modify.append(modified_rule) - break - - # If the current rule was not matched against passed rules, mark for removal - if not current_rule_passed_to_module and not current_rule['IsDefault']: - rules_to_delete.append(current_rule['RuleArn']) - - return rules_to_add, rules_to_modify, rules_to_delete - - -class ELBListenerRule(object): - - def __init__(self, connection, module, rule, listener_arn): - - self.connection = connection - self.module = module - self.rule = rule - self.listener_arn = listener_arn - self.changed = False - - def create(self): - """ - Create a listener rule - - :return: - """ - - try: - self.rule['ListenerArn'] = self.listener_arn - self.rule['Priority'] = int(self.rule['Priority']) - AWSRetry.jittered_backoff()(self.connection.create_rule)(**self.rule) - except (BotoCoreError, ClientError) as e: - if '"Order", must be one of: Type, TargetGroupArn' in str(e): - self.module.fail_json(msg="installed version of botocore does not support " - "multiple actions, please upgrade botocore to version " - "1.10.30 or higher") - else: - self.module.fail_json_aws(e) - - self.changed = True - - def modify(self): - """ - Modify a listener rule - - :return: - """ - - try: - del self.rule['Priority'] - AWSRetry.jittered_backoff()(self.connection.modify_rule)(**self.rule) - except (BotoCoreError, ClientError) as e: - if '"Order", must be one of: Type, TargetGroupArn' in str(e): - self.module.fail_json(msg="installed version of botocore does not support " - "multiple actions, please upgrade botocore to version " - "1.10.30 or higher") - else: - self.module.fail_json_aws(e) - - self.changed = True - - def delete(self): - """ - Delete a listener rule - - :return: - """ - - try: - AWSRetry.jittered_backoff()(self.connection.delete_rule)(RuleArn=self.rule['RuleArn']) - except (BotoCoreError, ClientError) as e: - self.module.fail_json_aws(e) - - self.changed = True diff --git a/lib/ansible/module_utils/aws/iam.py b/lib/ansible/module_utils/aws/iam.py deleted file mode 100644 index f05999aa37..0000000000 --- a/lib/ansible/module_utils/aws/iam.py +++ /dev/null @@ -1,49 +0,0 @@ -# Copyright (c) 2017 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 - -import traceback - -try: - from botocore.exceptions import ClientError, NoCredentialsError -except ImportError: - pass # caught by HAS_BOTO3 - -from ansible.module_utils._text import to_native - - -def get_aws_account_id(module): - """ Given AnsibleAWSModule instance, get the active AWS account ID - - get_account_id tries too find out the account that we are working - on. It's not guaranteed that this will be easy so we try in - several different ways. Giving either IAM or STS privilages to - the account should be enough to permit this. - """ - account_id = None - try: - sts_client = module.client('sts') - account_id = sts_client.get_caller_identity().get('Account') - # non-STS sessions may also get NoCredentialsError from this STS call, so - # we must catch that too and try the IAM version - except (ClientError, NoCredentialsError): - try: - iam_client = module.client('iam') - account_id = iam_client.get_user()['User']['Arn'].split(':')[4] - except ClientError as e: - if (e.response['Error']['Code'] == 'AccessDenied'): - except_msg = to_native(e) - # don't match on `arn:aws` because of China region `arn:aws-cn` and similar - account_id = except_msg.search(r"arn:\w+:iam::([0-9]{12,32}):\w+/").group(1) - if account_id is None: - module.fail_json_aws(e, msg="Could not get AWS account information") - except Exception as e: - module.fail_json( - msg="Failed to get AWS account information, Try allowing sts:GetCallerIdentity or iam:GetUser permissions.", - exception=traceback.format_exc() - ) - if not account_id: - module.fail_json(msg="Failed while determining AWS account ID. Try allowing sts:GetCallerIdentity or iam:GetUser permissions.") - return to_native(account_id) diff --git a/lib/ansible/module_utils/aws/rds.py b/lib/ansible/module_utils/aws/rds.py deleted file mode 100644 index a0f6cb9ffe..0000000000 --- a/lib/ansible/module_utils/aws/rds.py +++ /dev/null @@ -1,232 +0,0 @@ -# Copyright: (c) 2018, 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 - -from ansible.module_utils._text import to_text -from ansible.module_utils.aws.waiters import get_waiter -from ansible.module_utils.common.dict_transformations import snake_dict_to_camel_dict -from ansible.module_utils.ec2 import compare_aws_tags, AWSRetry, ansible_dict_to_boto3_tag_list, boto3_tag_list_to_ansible_dict - -try: - from botocore.exceptions import BotoCoreError, ClientError, WaiterError -except ImportError: - pass - -from collections import namedtuple -from time import sleep - - -Boto3ClientMethod = namedtuple('Boto3ClientMethod', ['name', 'waiter', 'operation_description', 'cluster', 'instance']) -# Whitelist boto3 client methods for cluster and instance resources -cluster_method_names = [ - 'create_db_cluster', 'restore_db_cluster_from_db_snapshot', 'restore_db_cluster_from_s3', - 'restore_db_cluster_to_point_in_time', 'modify_db_cluster', 'delete_db_cluster', 'add_tags_to_resource', - 'remove_tags_from_resource', 'list_tags_for_resource', 'promote_read_replica_db_cluster' -] -instance_method_names = [ - 'create_db_instance', 'restore_db_instance_to_point_in_time', 'restore_db_instance_from_s3', - 'restore_db_instance_from_db_snapshot', 'create_db_instance_read_replica', 'modify_db_instance', - 'delete_db_instance', 'add_tags_to_resource', 'remove_tags_from_resource', 'list_tags_for_resource', - 'promote_read_replica', 'stop_db_instance', 'start_db_instance', 'reboot_db_instance' -] - - -def get_rds_method_attribute(method_name, module): - readable_op = method_name.replace('_', ' ').replace('db', 'DB') - if method_name in cluster_method_names and 'new_db_cluster_identifier' in module.params: - cluster = True - instance = False - if method_name == 'delete_db_cluster': - waiter = 'cluster_deleted' - else: - waiter = 'cluster_available' - elif method_name in instance_method_names and 'new_db_instance_identifier' in module.params: - cluster = False - instance = True - if method_name == 'delete_db_instance': - waiter = 'db_instance_deleted' - elif method_name == 'stop_db_instance': - waiter = 'db_instance_stopped' - else: - waiter = 'db_instance_available' - else: - raise NotImplementedError("method {0} hasn't been added to the list of accepted methods to use a waiter in module_utils/aws/rds.py".format(method_name)) - - return Boto3ClientMethod(name=method_name, waiter=waiter, operation_description=readable_op, cluster=cluster, instance=instance) - - -def get_final_identifier(method_name, module): - apply_immediately = module.params['apply_immediately'] - if get_rds_method_attribute(method_name, module).cluster: - identifier = module.params['db_cluster_identifier'] - updated_identifier = module.params['new_db_cluster_identifier'] - elif get_rds_method_attribute(method_name, module).instance: - identifier = module.params['db_instance_identifier'] - updated_identifier = module.params['new_db_instance_identifier'] - else: - raise NotImplementedError("method {0} hasn't been added to the list of accepted methods in module_utils/aws/rds.py".format(method_name)) - if not module.check_mode and updated_identifier and apply_immediately: - identifier = updated_identifier - return identifier - - -def handle_errors(module, exception, method_name, parameters): - - if not isinstance(exception, ClientError): - module.fail_json_aws(exception, msg="Unexpected failure for method {0} with parameters {1}".format(method_name, parameters)) - - changed = True - error_code = exception.response['Error']['Code'] - if method_name == 'modify_db_instance' and error_code == 'InvalidParameterCombination': - if 'No modifications were requested' in to_text(exception): - changed = False - elif 'ModifyDbCluster API' in to_text(exception): - module.fail_json_aws(exception, msg='It appears you are trying to modify attributes that are managed at the cluster level. Please see rds_cluster') - else: - module.fail_json_aws(exception, msg='Unable to {0}'.format(get_rds_method_attribute(method_name, module).operation_description)) - elif method_name == 'promote_read_replica' and error_code == 'InvalidDBInstanceState': - if 'DB Instance is not a read replica' in to_text(exception): - changed = False - else: - module.fail_json_aws(exception, msg='Unable to {0}'.format(get_rds_method_attribute(method_name, module).operation_description)) - elif method_name == 'create_db_instance' and exception.response['Error']['Code'] == 'InvalidParameterValue': - accepted_engines = [ - 'aurora', 'aurora-mysql', 'aurora-postgresql', 'mariadb', 'mysql', 'oracle-ee', 'oracle-se', - 'oracle-se1', 'oracle-se2', 'postgres', 'sqlserver-ee', 'sqlserver-ex', 'sqlserver-se', 'sqlserver-web' - ] - if parameters.get('Engine') not in accepted_engines: - module.fail_json_aws(exception, msg='DB engine {0} should be one of {1}'.format(parameters.get('Engine'), accepted_engines)) - else: - module.fail_json_aws(exception, msg='Unable to {0}'.format(get_rds_method_attribute(method_name, module).operation_description)) - else: - module.fail_json_aws(exception, msg='Unable to {0}'.format(get_rds_method_attribute(method_name, module).operation_description)) - - return changed - - -def call_method(client, module, method_name, parameters): - result = {} - changed = True - if not module.check_mode: - wait = module.params['wait'] - # TODO: stabilize by adding get_rds_method_attribute(method_name).extra_retry_codes - method = getattr(client, method_name) - try: - if method_name == 'modify_db_instance': - # check if instance is in an available state first, if possible - if wait: - wait_for_status(client, module, module.params['db_instance_identifier'], method_name) - result = AWSRetry.jittered_backoff(catch_extra_error_codes=['InvalidDBInstanceState'])(method)(**parameters) - else: - result = AWSRetry.jittered_backoff()(method)(**parameters) - except (BotoCoreError, ClientError) as e: - changed = handle_errors(module, e, method_name, parameters) - - if wait and changed: - identifier = get_final_identifier(method_name, module) - wait_for_status(client, module, identifier, method_name) - return result, changed - - -def wait_for_instance_status(client, module, db_instance_id, waiter_name): - def wait(client, db_instance_id, waiter_name, extra_retry_codes): - retry = AWSRetry.jittered_backoff(catch_extra_error_codes=extra_retry_codes) - try: - waiter = client.get_waiter(waiter_name) - except ValueError: - # using a waiter in ansible.module_utils.aws.waiters - waiter = get_waiter(client, waiter_name) - waiter.wait(WaiterConfig={'Delay': 60, 'MaxAttempts': 60}, DBInstanceIdentifier=db_instance_id) - - waiter_expected_status = { - 'db_instance_deleted': 'deleted', - 'db_instance_stopped': 'stopped', - } - expected_status = waiter_expected_status.get(waiter_name, 'available') - if expected_status == 'available': - extra_retry_codes = ['DBInstanceNotFound'] - else: - extra_retry_codes = [] - for attempt_to_wait in range(0, 10): - try: - wait(client, db_instance_id, waiter_name, extra_retry_codes) - break - except WaiterError as e: - # Instance may be renamed and AWSRetry doesn't handle WaiterError - if e.last_response.get('Error', {}).get('Code') == 'DBInstanceNotFound': - sleep(10) - continue - module.fail_json_aws(e, msg='Error while waiting for DB instance {0} to be {1}'.format(db_instance_id, expected_status)) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Unexpected error while waiting for DB instance {0} to be {1}'.format( - db_instance_id, expected_status) - ) - - -def wait_for_cluster_status(client, module, db_cluster_id, waiter_name): - try: - waiter = get_waiter(client, waiter_name).wait(DBClusterIdentifier=db_cluster_id) - except WaiterError as e: - if waiter_name == 'cluster_deleted': - msg = "Failed to wait for DB cluster {0} to be deleted".format(db_cluster_id) - else: - msg = "Failed to wait for DB cluster {0} to be available".format(db_cluster_id) - module.fail_json_aws(e, msg=msg) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Failed with an unexpected error while waiting for the DB cluster {0}".format(db_cluster_id)) - - -def wait_for_status(client, module, identifier, method_name): - waiter_name = get_rds_method_attribute(method_name, module).waiter - if get_rds_method_attribute(method_name, module).cluster: - wait_for_cluster_status(client, module, identifier, waiter_name) - elif get_rds_method_attribute(method_name, module).instance: - wait_for_instance_status(client, module, identifier, waiter_name) - else: - raise NotImplementedError("method {0} hasn't been added to the whitelist of handled methods".format(method_name)) - - -def get_tags(client, module, cluster_arn): - try: - return boto3_tag_list_to_ansible_dict( - client.list_tags_for_resource(ResourceName=cluster_arn)['TagList'] - ) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to describe tags") - - -def arg_spec_to_rds_params(options_dict): - tags = options_dict.pop('tags') - has_processor_features = False - if 'processor_features' in options_dict: - has_processor_features = True - processor_features = options_dict.pop('processor_features') - camel_options = snake_dict_to_camel_dict(options_dict, capitalize_first=True) - for key in list(camel_options.keys()): - for old, new in (('Db', 'DB'), ('Iam', 'IAM'), ('Az', 'AZ')): - if old in key: - camel_options[key.replace(old, new)] = camel_options.pop(key) - camel_options['Tags'] = tags - if has_processor_features: - camel_options['ProcessorFeatures'] = processor_features - return camel_options - - -def ensure_tags(client, module, resource_arn, existing_tags, tags, purge_tags): - if tags is None: - return False - tags_to_add, tags_to_remove = compare_aws_tags(existing_tags, tags, purge_tags) - changed = bool(tags_to_add or tags_to_remove) - if tags_to_add: - call_method( - client, module, method_name='add_tags_to_resource', - parameters={'ResourceName': resource_arn, 'Tags': ansible_dict_to_boto3_tag_list(tags_to_add)} - ) - if tags_to_remove: - call_method( - client, module, method_name='remove_tags_from_resource', - parameters={'ResourceName': resource_arn, 'TagKeys': tags_to_remove} - ) - return changed diff --git a/lib/ansible/module_utils/aws/s3.py b/lib/ansible/module_utils/aws/s3.py deleted file mode 100644 index 2185869d49..0000000000 --- a/lib/ansible/module_utils/aws/s3.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright (c) 2018 Red Hat, Inc. -# 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 - -try: - from botocore.exceptions import BotoCoreError, ClientError -except ImportError: - pass # Handled by the calling module - -HAS_MD5 = True -try: - from hashlib import md5 -except ImportError: - try: - from md5 import md5 - except ImportError: - HAS_MD5 = False - - -def calculate_etag(module, filename, etag, s3, bucket, obj, version=None): - if not HAS_MD5: - return None - - if '-' in etag: - # Multi-part ETag; a hash of the hashes of each part. - parts = int(etag[1:-1].split('-')[1]) - digests = [] - - s3_kwargs = dict( - Bucket=bucket, - Key=obj, - ) - if version: - s3_kwargs['VersionId'] = version - - with open(filename, 'rb') as f: - for part_num in range(1, parts + 1): - s3_kwargs['PartNumber'] = part_num - try: - head = s3.head_object(**s3_kwargs) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Failed to get head object") - digests.append(md5(f.read(int(head['ContentLength'])))) - - digest_squared = md5(b''.join(m.digest() for m in digests)) - return '"{0}-{1}"'.format(digest_squared.hexdigest(), len(digests)) - else: # Compute the MD5 sum normally - return '"{0}"'.format(module.md5(filename)) diff --git a/lib/ansible/module_utils/aws/urls.py b/lib/ansible/module_utils/aws/urls.py deleted file mode 100644 index f4db08a064..0000000000 --- a/lib/ansible/module_utils/aws/urls.py +++ /dev/null @@ -1,210 +0,0 @@ -# Copyright: (c) 2018, Aaron Haaf <aabonh@gmail.com> -# 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 - -import datetime -import hashlib -import hmac -import operator - -from ansible.module_utils.urls import open_url -from ansible.module_utils.ec2 import boto3_conn, get_aws_connection_info, HAS_BOTO3 -from ansible.module_utils.six.moves.urllib.parse import urlencode - -try: - from boto3 import session -except ImportError: - pass - - -def hexdigest(s): - """ - Returns the sha256 hexdigest of a string after encoding. - """ - - return hashlib.sha256(s.encode("utf-8")).hexdigest() - - -def format_querystring(params=None): - """ - Returns properly url-encoded query string from the provided params dict. - - It's specially sorted for cannonical requests - """ - - if not params: - return "" - - # Query string values must be URL-encoded (space=%20). The parameters must be sorted by name. - return urlencode(sorted(params.items(), operator.itemgetter(0))) - - -# Key derivation functions. See: -# http://docs.aws.amazon.com/general/latest/gr/signature-v4-examples.html#signature-v4-examples-python -def sign(key, msg): - ''' - Return digest for key applied to msg - ''' - - return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest() - - -def get_signature_key(key, dateStamp, regionName, serviceName): - ''' - Returns signature key for AWS resource - ''' - - kDate = sign(("AWS4" + key).encode("utf-8"), dateStamp) - kRegion = sign(kDate, regionName) - kService = sign(kRegion, serviceName) - kSigning = sign(kService, "aws4_request") - return kSigning - - -def get_aws_credentials_object(module): - ''' - Returns aws_access_key_id, aws_secret_access_key, session_token for a module. - ''' - - if not HAS_BOTO3: - module.fail_json("get_aws_credentials_object requires boto3") - - dummy, dummy, boto_params = get_aws_connection_info(module, boto3=True) - s = session.Session(**boto_params) - - return s.get_credentials() - - -# Reference: https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html -def signed_request( - module=None, - method="GET", service=None, host=None, uri=None, - query=None, body="", headers=None, - session_in_header=True, session_in_query=False -): - """Generate a SigV4 request to an AWS resource for a module - - This is used if you wish to authenticate with AWS credentials to a secure endpoint like an elastisearch domain. - - Returns :class:`HTTPResponse` object. - - Example: - result = signed_request( - module=this, - service="es", - host="search-recipes1-xxxxxxxxx.us-west-2.es.amazonaws.com", - ) - - :kwarg host: endpoint to talk to - :kwarg service: AWS id of service (like `ec2` or `es`) - :kwarg module: An AnsibleAWSModule to gather connection info from - - :kwarg body: (optional) Payload to send - :kwarg method: (optional) HTTP verb to use - :kwarg query: (optional) dict of query params to handle - :kwarg uri: (optional) Resource path without query parameters - - :kwarg session_in_header: (optional) Add the session token to the headers - :kwarg session_in_query: (optional) Add the session token to the query parameters - - :returns: HTTPResponse - """ - - if not HAS_BOTO3: - module.fail_json("A sigv4 signed_request requires boto3") - - # "Constants" - - t = datetime.datetime.utcnow() - amz_date = t.strftime("%Y%m%dT%H%M%SZ") - datestamp = t.strftime("%Y%m%d") # Date w/o time, used in credential scope - algorithm = "AWS4-HMAC-SHA256" - - # AWS stuff - - region, dummy, dummy = get_aws_connection_info(module, boto3=True) - credentials = get_aws_credentials_object(module) - access_key = credentials.access_key - secret_key = credentials.secret_key - session_token = credentials.token - - if not access_key: - module.fail_json(msg="aws_access_key_id is missing") - if not secret_key: - module.fail_json(msg="aws_secret_access_key is missing") - - credential_scope = "/".join([datestamp, region, service, "aws4_request"]) - - # Argument Defaults - - uri = uri or "/" - query_string = format_querystring(query) if query else "" - - headers = headers or dict() - query = query or dict() - - headers.update({ - "host": host, - "x-amz-date": amz_date, - }) - - # Handle adding of session_token if present - if session_token: - if session_in_header: - headers["X-Amz-Security-Token"] = session_token - if session_in_query: - query["X-Amz-Security-Token"] = session_token - - if method == "GET": - body = "" - - # Derived data - - body_hash = hexdigest(body) - signed_headers = ";".join(sorted(headers.keys())) - - # Setup Cannonical request to generate auth token - - cannonical_headers = "\n".join([ - key.lower().strip() + ":" + value for key, value in headers.items() - ]) + "\n" # Note additional trailing newline - - cannonical_request = "\n".join([ - method, - uri, - query_string, - cannonical_headers, - signed_headers, - body_hash, - ]) - - string_to_sign = "\n".join([algorithm, amz_date, credential_scope, hexdigest(cannonical_request)]) - - # Sign the Cannonical request - - signing_key = get_signature_key(secret_key, datestamp, region, service) - signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest() - - # Make auth header with that info - - authorization_header = "{0} Credential={1}/{2}, SignedHeaders={3}, Signature={4}".format( - algorithm, access_key, credential_scope, signed_headers, signature - ) - - # PERFORM THE REQUEST! - - url = "https://" + host + uri - - if query_string != "": - url = url + "?" + query_string - - final_headers = { - "x-amz-date": amz_date, - "Authorization": authorization_header, - } - - final_headers.update(headers) - - return open_url(url, method=method, data=body, headers=final_headers) diff --git a/lib/ansible/module_utils/aws/waf.py b/lib/ansible/module_utils/aws/waf.py deleted file mode 100644 index d4a17efcbd..0000000000 --- a/lib/ansible/module_utils/aws/waf.py +++ /dev/null @@ -1,222 +0,0 @@ -# Copyright (c) 2017 Will Thames -# -# This code is part of Ansible, but is an independent component. -# This particular file snippet, and this file snippet only, is BSD licensed. -# Modules you write using this snippet, which is embedded dynamically by Ansible -# still belong to the author of the module, and may assign their own license -# to the complete work. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# -""" -This module adds shared support for Web Application Firewall modules -""" - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -from ansible.module_utils.ec2 import camel_dict_to_snake_dict, AWSRetry -from ansible.module_utils.aws.waiters import get_waiter - -try: - import botocore -except ImportError: - pass # caught by imported HAS_BOTO3 - - -MATCH_LOOKUP = { - 'byte': { - 'method': 'byte_match_set', - 'conditionset': 'ByteMatchSet', - 'conditiontuple': 'ByteMatchTuple', - 'type': 'ByteMatch' - }, - 'geo': { - 'method': 'geo_match_set', - 'conditionset': 'GeoMatchSet', - 'conditiontuple': 'GeoMatchConstraint', - 'type': 'GeoMatch' - }, - 'ip': { - 'method': 'ip_set', - 'conditionset': 'IPSet', - 'conditiontuple': 'IPSetDescriptor', - 'type': 'IPMatch' - }, - 'regex': { - 'method': 'regex_match_set', - 'conditionset': 'RegexMatchSet', - 'conditiontuple': 'RegexMatchTuple', - 'type': 'RegexMatch' - }, - 'size': { - 'method': 'size_constraint_set', - 'conditionset': 'SizeConstraintSet', - 'conditiontuple': 'SizeConstraint', - 'type': 'SizeConstraint' - }, - 'sql': { - 'method': 'sql_injection_match_set', - 'conditionset': 'SqlInjectionMatchSet', - 'conditiontuple': 'SqlInjectionMatchTuple', - 'type': 'SqlInjectionMatch', - }, - 'xss': { - 'method': 'xss_match_set', - 'conditionset': 'XssMatchSet', - 'conditiontuple': 'XssMatchTuple', - 'type': 'XssMatch' - }, -} - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def get_rule_with_backoff(client, rule_id): - return client.get_rule(RuleId=rule_id)['Rule'] - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def get_byte_match_set_with_backoff(client, byte_match_set_id): - return client.get_byte_match_set(ByteMatchSetId=byte_match_set_id)['ByteMatchSet'] - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def get_ip_set_with_backoff(client, ip_set_id): - return client.get_ip_set(IPSetId=ip_set_id)['IPSet'] - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def get_size_constraint_set_with_backoff(client, size_constraint_set_id): - return client.get_size_constraint_set(SizeConstraintSetId=size_constraint_set_id)['SizeConstraintSet'] - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def get_sql_injection_match_set_with_backoff(client, sql_injection_match_set_id): - return client.get_sql_injection_match_set(SqlInjectionMatchSetId=sql_injection_match_set_id)['SqlInjectionMatchSet'] - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def get_xss_match_set_with_backoff(client, xss_match_set_id): - return client.get_xss_match_set(XssMatchSetId=xss_match_set_id)['XssMatchSet'] - - -def get_rule(client, module, rule_id): - try: - rule = get_rule_with_backoff(client, rule_id) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't obtain waf rule") - - match_sets = { - 'ByteMatch': get_byte_match_set_with_backoff, - 'IPMatch': get_ip_set_with_backoff, - 'SizeConstraint': get_size_constraint_set_with_backoff, - 'SqlInjectionMatch': get_sql_injection_match_set_with_backoff, - 'XssMatch': get_xss_match_set_with_backoff - } - if 'Predicates' in rule: - for predicate in rule['Predicates']: - if predicate['Type'] in match_sets: - predicate.update(match_sets[predicate['Type']](client, predicate['DataId'])) - # replaced by Id from the relevant MatchSet - del(predicate['DataId']) - return rule - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def get_web_acl_with_backoff(client, web_acl_id): - return client.get_web_acl(WebACLId=web_acl_id)['WebACL'] - - -def get_web_acl(client, module, web_acl_id): - try: - web_acl = get_web_acl_with_backoff(client, web_acl_id) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't obtain web acl") - - if web_acl: - try: - for rule in web_acl['Rules']: - rule.update(get_rule(client, module, rule['RuleId'])) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't obtain web acl rule") - return camel_dict_to_snake_dict(web_acl) - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def list_rules_with_backoff(client): - paginator = client.get_paginator('list_rules') - return paginator.paginate().build_full_result()['Rules'] - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def list_regional_rules_with_backoff(client): - resp = client.list_rules() - rules = [] - while resp: - rules += resp['Rules'] - resp = client.list_rules(NextMarker=resp['NextMarker']) if 'NextMarker' in resp else None - return rules - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def list_web_acls_with_backoff(client): - paginator = client.get_paginator('list_web_acls') - return paginator.paginate().build_full_result()['WebACLs'] - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def list_regional_web_acls_with_backoff(client): - resp = client.list_web_acls() - acls = [] - while resp: - acls += resp['WebACLs'] - resp = client.list_web_acls(NextMarker=resp['NextMarker']) if 'NextMarker' in resp else None - return acls - - -def list_web_acls(client, module): - try: - if client.__class__.__name__ == 'WAF': - return list_web_acls_with_backoff(client) - elif client.__class__.__name__ == 'WAFRegional': - return list_regional_web_acls_with_backoff(client) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't obtain web acls") - - -def get_change_token(client, module): - try: - token = client.get_change_token() - return token['ChangeToken'] - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't obtain change token") - - -@AWSRetry.backoff(tries=10, delay=2, backoff=2.0, catch_extra_error_codes=['WAFStaleDataException']) -def run_func_with_change_token_backoff(client, module, params, func, wait=False): - params['ChangeToken'] = get_change_token(client, module) - result = func(**params) - if wait: - get_waiter( - client, 'change_token_in_sync', - ).wait( - ChangeToken=result['ChangeToken'] - ) - return result diff --git a/lib/ansible/module_utils/aws/waiters.py b/lib/ansible/module_utils/aws/waiters.py deleted file mode 100644 index 25db598bcb..0000000000 --- a/lib/ansible/module_utils/aws/waiters.py +++ /dev/null @@ -1,405 +0,0 @@ -# Copyright: (c) 2018, 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 - -try: - import botocore.waiter as core_waiter -except ImportError: - pass # caught by HAS_BOTO3 - - -ec2_data = { - "version": 2, - "waiters": { - "InternetGatewayExists": { - "delay": 5, - "maxAttempts": 40, - "operation": "DescribeInternetGateways", - "acceptors": [ - { - "matcher": "path", - "expected": True, - "argument": "length(InternetGateways) > `0`", - "state": "success" - }, - { - "matcher": "error", - "expected": "InvalidInternetGatewayID.NotFound", - "state": "retry" - }, - ] - }, - "RouteTableExists": { - "delay": 5, - "maxAttempts": 40, - "operation": "DescribeRouteTables", - "acceptors": [ - { - "matcher": "path", - "expected": True, - "argument": "length(RouteTables[]) > `0`", - "state": "success" - }, - { - "matcher": "error", - "expected": "InvalidRouteTableID.NotFound", - "state": "retry" - }, - ] - }, - "SecurityGroupExists": { - "delay": 5, - "maxAttempts": 40, - "operation": "DescribeSecurityGroups", - "acceptors": [ - { - "matcher": "path", - "expected": True, - "argument": "length(SecurityGroups[]) > `0`", - "state": "success" - }, - { - "matcher": "error", - "expected": "InvalidGroup.NotFound", - "state": "retry" - }, - ] - }, - "SubnetExists": { - "delay": 5, - "maxAttempts": 40, - "operation": "DescribeSubnets", - "acceptors": [ - { - "matcher": "path", - "expected": True, - "argument": "length(Subnets[]) > `0`", - "state": "success" - }, - { - "matcher": "error", - "expected": "InvalidSubnetID.NotFound", - "state": "retry" - }, - ] - }, - "SubnetHasMapPublic": { - "delay": 5, - "maxAttempts": 40, - "operation": "DescribeSubnets", - "acceptors": [ - { - "matcher": "pathAll", - "expected": True, - "argument": "Subnets[].MapPublicIpOnLaunch", - "state": "success" - }, - ] - }, - "SubnetNoMapPublic": { - "delay": 5, - "maxAttempts": 40, - "operation": "DescribeSubnets", - "acceptors": [ - { - "matcher": "pathAll", - "expected": False, - "argument": "Subnets[].MapPublicIpOnLaunch", - "state": "success" - }, - ] - }, - "SubnetHasAssignIpv6": { - "delay": 5, - "maxAttempts": 40, - "operation": "DescribeSubnets", - "acceptors": [ - { - "matcher": "pathAll", - "expected": True, - "argument": "Subnets[].AssignIpv6AddressOnCreation", - "state": "success" - }, - ] - }, - "SubnetNoAssignIpv6": { - "delay": 5, - "maxAttempts": 40, - "operation": "DescribeSubnets", - "acceptors": [ - { - "matcher": "pathAll", - "expected": False, - "argument": "Subnets[].AssignIpv6AddressOnCreation", - "state": "success" - }, - ] - }, - "SubnetDeleted": { - "delay": 5, - "maxAttempts": 40, - "operation": "DescribeSubnets", - "acceptors": [ - { - "matcher": "path", - "expected": True, - "argument": "length(Subnets[]) > `0`", - "state": "retry" - }, - { - "matcher": "error", - "expected": "InvalidSubnetID.NotFound", - "state": "success" - }, - ] - }, - "VpnGatewayExists": { - "delay": 5, - "maxAttempts": 40, - "operation": "DescribeVpnGateways", - "acceptors": [ - { - "matcher": "path", - "expected": True, - "argument": "length(VpnGateways[]) > `0`", - "state": "success" - }, - { - "matcher": "error", - "expected": "InvalidVpnGatewayID.NotFound", - "state": "retry" - }, - ] - }, - "VpnGatewayDetached": { - "delay": 5, - "maxAttempts": 40, - "operation": "DescribeVpnGateways", - "acceptors": [ - { - "matcher": "path", - "expected": True, - "argument": "VpnGateways[0].State == 'available'", - "state": "success" - }, - ] - }, - } -} - - -waf_data = { - "version": 2, - "waiters": { - "ChangeTokenInSync": { - "delay": 20, - "maxAttempts": 60, - "operation": "GetChangeTokenStatus", - "acceptors": [ - { - "matcher": "path", - "expected": True, - "argument": "ChangeTokenStatus == 'INSYNC'", - "state": "success" - }, - { - "matcher": "error", - "expected": "WAFInternalErrorException", - "state": "retry" - } - ] - } - } -} - -eks_data = { - "version": 2, - "waiters": { - "ClusterActive": { - "delay": 20, - "maxAttempts": 60, - "operation": "DescribeCluster", - "acceptors": [ - { - "state": "success", - "matcher": "path", - "argument": "cluster.status", - "expected": "ACTIVE" - }, - { - "state": "retry", - "matcher": "error", - "expected": "ResourceNotFoundException" - } - ] - }, - "ClusterDeleted": { - "delay": 20, - "maxAttempts": 60, - "operation": "DescribeCluster", - "acceptors": [ - { - "state": "retry", - "matcher": "path", - "argument": "cluster.status != 'DELETED'", - "expected": True - }, - { - "state": "success", - "matcher": "error", - "expected": "ResourceNotFoundException" - } - ] - } - } -} - - -rds_data = { - "version": 2, - "waiters": { - "DBInstanceStopped": { - "delay": 20, - "maxAttempts": 60, - "operation": "DescribeDBInstances", - "acceptors": [ - { - "state": "success", - "matcher": "pathAll", - "argument": "DBInstances[].DBInstanceStatus", - "expected": "stopped" - }, - ] - } - } -} - - -def ec2_model(name): - ec2_models = core_waiter.WaiterModel(waiter_config=ec2_data) - return ec2_models.get_waiter(name) - - -def waf_model(name): - waf_models = core_waiter.WaiterModel(waiter_config=waf_data) - return waf_models.get_waiter(name) - - -def eks_model(name): - eks_models = core_waiter.WaiterModel(waiter_config=eks_data) - return eks_models.get_waiter(name) - - -def rds_model(name): - rds_models = core_waiter.WaiterModel(waiter_config=rds_data) - return rds_models.get_waiter(name) - - -waiters_by_name = { - ('EC2', 'internet_gateway_exists'): lambda ec2: core_waiter.Waiter( - 'internet_gateway_exists', - ec2_model('InternetGatewayExists'), - core_waiter.NormalizedOperationMethod( - ec2.describe_internet_gateways - )), - ('EC2', 'route_table_exists'): lambda ec2: core_waiter.Waiter( - 'route_table_exists', - ec2_model('RouteTableExists'), - core_waiter.NormalizedOperationMethod( - ec2.describe_route_tables - )), - ('EC2', 'security_group_exists'): lambda ec2: core_waiter.Waiter( - 'security_group_exists', - ec2_model('SecurityGroupExists'), - core_waiter.NormalizedOperationMethod( - ec2.describe_security_groups - )), - ('EC2', 'subnet_exists'): lambda ec2: core_waiter.Waiter( - 'subnet_exists', - ec2_model('SubnetExists'), - core_waiter.NormalizedOperationMethod( - ec2.describe_subnets - )), - ('EC2', 'subnet_has_map_public'): lambda ec2: core_waiter.Waiter( - 'subnet_has_map_public', - ec2_model('SubnetHasMapPublic'), - core_waiter.NormalizedOperationMethod( - ec2.describe_subnets - )), - ('EC2', 'subnet_no_map_public'): lambda ec2: core_waiter.Waiter( - 'subnet_no_map_public', - ec2_model('SubnetNoMapPublic'), - core_waiter.NormalizedOperationMethod( - ec2.describe_subnets - )), - ('EC2', 'subnet_has_assign_ipv6'): lambda ec2: core_waiter.Waiter( - 'subnet_has_assign_ipv6', - ec2_model('SubnetHasAssignIpv6'), - core_waiter.NormalizedOperationMethod( - ec2.describe_subnets - )), - ('EC2', 'subnet_no_assign_ipv6'): lambda ec2: core_waiter.Waiter( - 'subnet_no_assign_ipv6', - ec2_model('SubnetNoAssignIpv6'), - core_waiter.NormalizedOperationMethod( - ec2.describe_subnets - )), - ('EC2', 'subnet_deleted'): lambda ec2: core_waiter.Waiter( - 'subnet_deleted', - ec2_model('SubnetDeleted'), - core_waiter.NormalizedOperationMethod( - ec2.describe_subnets - )), - ('EC2', 'vpn_gateway_exists'): lambda ec2: core_waiter.Waiter( - 'vpn_gateway_exists', - ec2_model('VpnGatewayExists'), - core_waiter.NormalizedOperationMethod( - ec2.describe_vpn_gateways - )), - ('EC2', 'vpn_gateway_detached'): lambda ec2: core_waiter.Waiter( - 'vpn_gateway_detached', - ec2_model('VpnGatewayDetached'), - core_waiter.NormalizedOperationMethod( - ec2.describe_vpn_gateways - )), - ('WAF', 'change_token_in_sync'): lambda waf: core_waiter.Waiter( - 'change_token_in_sync', - waf_model('ChangeTokenInSync'), - core_waiter.NormalizedOperationMethod( - waf.get_change_token_status - )), - ('WAFRegional', 'change_token_in_sync'): lambda waf: core_waiter.Waiter( - 'change_token_in_sync', - waf_model('ChangeTokenInSync'), - core_waiter.NormalizedOperationMethod( - waf.get_change_token_status - )), - ('EKS', 'cluster_active'): lambda eks: core_waiter.Waiter( - 'cluster_active', - eks_model('ClusterActive'), - core_waiter.NormalizedOperationMethod( - eks.describe_cluster - )), - ('EKS', 'cluster_deleted'): lambda eks: core_waiter.Waiter( - 'cluster_deleted', - eks_model('ClusterDeleted'), - core_waiter.NormalizedOperationMethod( - eks.describe_cluster - )), - ('RDS', 'db_instance_stopped'): lambda rds: core_waiter.Waiter( - 'db_instance_stopped', - rds_model('DBInstanceStopped'), - core_waiter.NormalizedOperationMethod( - rds.describe_db_instances - )), -} - - -def get_waiter(client, waiter_name): - try: - return waiters_by_name[(client.__class__.__name__, waiter_name)](client) - except KeyError: - raise NotImplementedError("Waiter {0} could not be found for client {1}. Available waiters: {2}".format( - waiter_name, type(client), ', '.join(repr(k) for k in waiters_by_name.keys()))) diff --git a/lib/ansible/module_utils/ec2.py b/lib/ansible/module_utils/ec2.py deleted file mode 100644 index 5599ee7ea3..0000000000 --- a/lib/ansible/module_utils/ec2.py +++ /dev/null @@ -1,758 +0,0 @@ -# This code is part of Ansible, but is an independent component. -# This particular file snippet, and this file snippet only, is BSD licensed. -# Modules you write using this snippet, which is embedded dynamically by Ansible -# still belong to the author of the module, and may assign their own license -# to the complete work. -# -# Copyright (c), Michael DeHaan <michael.dehaan@gmail.com>, 2012-2013 -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without modification, -# are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import os -import re -import sys -import traceback - -from ansible.module_utils.ansible_release import __version__ -from ansible.module_utils.basic import missing_required_lib, env_fallback -from ansible.module_utils._text import to_native, to_text -from ansible.module_utils.cloud import CloudRetry -from ansible.module_utils.six import string_types, binary_type, text_type -from ansible.module_utils.common.dict_transformations import ( - camel_dict_to_snake_dict, snake_dict_to_camel_dict, - _camel_to_snake, _snake_to_camel, -) - -BOTO_IMP_ERR = None -try: - import boto - import boto.ec2 # boto does weird import stuff - HAS_BOTO = True -except ImportError: - BOTO_IMP_ERR = traceback.format_exc() - HAS_BOTO = False - -BOTO3_IMP_ERR = None -try: - import boto3 - import botocore - HAS_BOTO3 = True -except Exception: - BOTO3_IMP_ERR = traceback.format_exc() - HAS_BOTO3 = False - -try: - # Although this is to allow Python 3 the ability to use the custom comparison as a key, Python 2.7 also - # uses this (and it works as expected). Python 2.6 will trigger the ImportError. - from functools import cmp_to_key - PY3_COMPARISON = True -except ImportError: - PY3_COMPARISON = False - - -class AnsibleAWSError(Exception): - pass - - -def _botocore_exception_maybe(): - """ - Allow for boto3 not being installed when using these utils by wrapping - botocore.exceptions instead of assigning from it directly. - """ - if HAS_BOTO3: - return botocore.exceptions.ClientError - return type(None) - - -class AWSRetry(CloudRetry): - base_class = _botocore_exception_maybe() - - @staticmethod - def status_code_from_exception(error): - return error.response['Error']['Code'] - - @staticmethod - def found(response_code, catch_extra_error_codes=None): - # This list of failures is based on this API Reference - # http://docs.aws.amazon.com/AWSEC2/latest/APIReference/errors-overview.html - # - # TooManyRequestsException comes from inside botocore when it - # does retrys, unfortunately however it does not try long - # enough to allow some services such as API Gateway to - # complete configuration. At the moment of writing there is a - # botocore/boto3 bug open to fix this. - # - # https://github.com/boto/boto3/issues/876 (and linked PRs etc) - retry_on = [ - 'RequestLimitExceeded', 'Unavailable', 'ServiceUnavailable', - 'InternalFailure', 'InternalError', 'TooManyRequestsException', - 'Throttling' - ] - if catch_extra_error_codes: - retry_on.extend(catch_extra_error_codes) - - return response_code in retry_on - - -def boto3_conn(module, conn_type=None, resource=None, region=None, endpoint=None, **params): - try: - return _boto3_conn(conn_type=conn_type, resource=resource, region=region, endpoint=endpoint, **params) - except ValueError as e: - module.fail_json(msg="Couldn't connect to AWS: %s" % to_native(e)) - except (botocore.exceptions.ProfileNotFound, botocore.exceptions.PartialCredentialsError, - botocore.exceptions.NoCredentialsError, botocore.exceptions.ConfigParseError) as e: - module.fail_json(msg=to_native(e)) - except botocore.exceptions.NoRegionError as e: - module.fail_json(msg="The %s module requires a region and none was found in configuration, " - "environment variables or module parameters" % module._name) - - -def _boto3_conn(conn_type=None, resource=None, region=None, endpoint=None, **params): - profile = params.pop('profile_name', None) - - if conn_type not in ['both', 'resource', 'client']: - raise ValueError('There is an issue in the calling code. You ' - 'must specify either both, resource, or client to ' - 'the conn_type parameter in the boto3_conn function ' - 'call') - - config = botocore.config.Config( - user_agent_extra='Ansible/{0}'.format(__version__), - ) - - if params.get('config') is not None: - config = config.merge(params.pop('config')) - if params.get('aws_config') is not None: - config = config.merge(params.pop('aws_config')) - - session = boto3.session.Session( - profile_name=profile, - ) - - if conn_type == 'resource': - return session.resource(resource, config=config, region_name=region, endpoint_url=endpoint, **params) - elif conn_type == 'client': - return session.client(resource, config=config, region_name=region, endpoint_url=endpoint, **params) - else: - client = session.client(resource, region_name=region, endpoint_url=endpoint, **params) - resource = session.resource(resource, region_name=region, endpoint_url=endpoint, **params) - return client, resource - - -boto3_inventory_conn = _boto3_conn - - -def boto_exception(err): - """ - Extracts the error message from a boto exception. - - :param err: Exception from boto - :return: Error message - """ - if hasattr(err, 'error_message'): - error = err.error_message - elif hasattr(err, 'message'): - error = str(err.message) + ' ' + str(err) + ' - ' + str(type(err)) - else: - error = '%s: %s' % (Exception, err) - - return error - - -def aws_common_argument_spec(): - return dict( - debug_botocore_endpoint_logs=dict(fallback=(env_fallback, ['ANSIBLE_DEBUG_BOTOCORE_LOGS']), default=False, type='bool'), - ec2_url=dict(), - aws_secret_key=dict(aliases=['ec2_secret_key', 'secret_key'], no_log=True), - aws_access_key=dict(aliases=['ec2_access_key', 'access_key']), - validate_certs=dict(default=True, type='bool'), - security_token=dict(aliases=['access_token'], no_log=True), - profile=dict(), - aws_config=dict(type='dict'), - ) - - -def ec2_argument_spec(): - spec = aws_common_argument_spec() - spec.update( - dict( - region=dict(aliases=['aws_region', 'ec2_region']), - ) - ) - return spec - - -def get_aws_region(module, boto3=False): - region = module.params.get('region') - - if region: - return region - - if 'AWS_REGION' in os.environ: - return os.environ['AWS_REGION'] - if 'AWS_DEFAULT_REGION' in os.environ: - return os.environ['AWS_DEFAULT_REGION'] - if 'EC2_REGION' in os.environ: - return os.environ['EC2_REGION'] - - if not boto3: - if not HAS_BOTO: - module.fail_json(msg=missing_required_lib('boto'), exception=BOTO_IMP_ERR) - # boto.config.get returns None if config not found - region = boto.config.get('Boto', 'aws_region') - if region: - return region - return boto.config.get('Boto', 'ec2_region') - - if not HAS_BOTO3: - module.fail_json(msg=missing_required_lib('boto3'), exception=BOTO3_IMP_ERR) - - # here we don't need to make an additional call, will default to 'us-east-1' if the below evaluates to None. - try: - profile_name = module.params.get('profile') - return botocore.session.Session(profile=profile_name).get_config_variable('region') - except botocore.exceptions.ProfileNotFound as e: - return None - - -def get_aws_connection_info(module, boto3=False): - - # Check module args for credentials, then check environment vars - # access_key - - ec2_url = module.params.get('ec2_url') - access_key = module.params.get('aws_access_key') - secret_key = module.params.get('aws_secret_key') - security_token = module.params.get('security_token') - region = get_aws_region(module, boto3) - profile_name = module.params.get('profile') - validate_certs = module.params.get('validate_certs') - config = module.params.get('aws_config') - - if not ec2_url: - if 'AWS_URL' in os.environ: - ec2_url = os.environ['AWS_URL'] - elif 'EC2_URL' in os.environ: - ec2_url = os.environ['EC2_URL'] - - if not access_key: - if os.environ.get('AWS_ACCESS_KEY_ID'): - access_key = os.environ['AWS_ACCESS_KEY_ID'] - elif os.environ.get('AWS_ACCESS_KEY'): - access_key = os.environ['AWS_ACCESS_KEY'] - elif os.environ.get('EC2_ACCESS_KEY'): - access_key = os.environ['EC2_ACCESS_KEY'] - elif HAS_BOTO and boto.config.get('Credentials', 'aws_access_key_id'): - access_key = boto.config.get('Credentials', 'aws_access_key_id') - elif HAS_BOTO and boto.config.get('default', 'aws_access_key_id'): - access_key = boto.config.get('default', 'aws_access_key_id') - else: - # in case access_key came in as empty string - access_key = None - - if not secret_key: - if os.environ.get('AWS_SECRET_ACCESS_KEY'): - secret_key = os.environ['AWS_SECRET_ACCESS_KEY'] - elif os.environ.get('AWS_SECRET_KEY'): - secret_key = os.environ['AWS_SECRET_KEY'] - elif os.environ.get('EC2_SECRET_KEY'): - secret_key = os.environ['EC2_SECRET_KEY'] - elif HAS_BOTO and boto.config.get('Credentials', 'aws_secret_access_key'): - secret_key = boto.config.get('Credentials', 'aws_secret_access_key') - elif HAS_BOTO and boto.config.get('default', 'aws_secret_access_key'): - secret_key = boto.config.get('default', 'aws_secret_access_key') - else: - # in case secret_key came in as empty string - secret_key = None - - if not security_token: - if os.environ.get('AWS_SECURITY_TOKEN'): - security_token = os.environ['AWS_SECURITY_TOKEN'] - elif os.environ.get('AWS_SESSION_TOKEN'): - security_token = os.environ['AWS_SESSION_TOKEN'] - elif os.environ.get('EC2_SECURITY_TOKEN'): - security_token = os.environ['EC2_SECURITY_TOKEN'] - elif HAS_BOTO and boto.config.get('Credentials', 'aws_security_token'): - security_token = boto.config.get('Credentials', 'aws_security_token') - elif HAS_BOTO and boto.config.get('default', 'aws_security_token'): - security_token = boto.config.get('default', 'aws_security_token') - else: - # in case secret_token came in as empty string - security_token = None - - if HAS_BOTO3 and boto3: - boto_params = dict(aws_access_key_id=access_key, - aws_secret_access_key=secret_key, - aws_session_token=security_token) - boto_params['verify'] = validate_certs - - if profile_name: - boto_params = dict(aws_access_key_id=None, aws_secret_access_key=None, aws_session_token=None) - boto_params['profile_name'] = profile_name - - else: - boto_params = dict(aws_access_key_id=access_key, - aws_secret_access_key=secret_key, - security_token=security_token) - - # only set profile_name if passed as an argument - if profile_name: - boto_params['profile_name'] = profile_name - - boto_params['validate_certs'] = validate_certs - - if config is not None: - if HAS_BOTO3 and boto3: - boto_params['aws_config'] = botocore.config.Config(**config) - elif HAS_BOTO and not boto3: - if 'user_agent' in config: - sys.modules["boto.connection"].UserAgent = config['user_agent'] - - for param, value in boto_params.items(): - if isinstance(value, binary_type): - boto_params[param] = text_type(value, 'utf-8', 'strict') - - return region, ec2_url, boto_params - - -def get_ec2_creds(module): - ''' for compatibility mode with old modules that don't/can't yet - use ec2_connect method ''' - region, ec2_url, boto_params = get_aws_connection_info(module) - return ec2_url, boto_params['aws_access_key_id'], boto_params['aws_secret_access_key'], region - - -def boto_fix_security_token_in_profile(conn, profile_name): - ''' monkey patch for boto issue boto/boto#2100 ''' - profile = 'profile ' + profile_name - if boto.config.has_option(profile, 'aws_security_token'): - conn.provider.set_security_token(boto.config.get(profile, 'aws_security_token')) - return conn - - -def connect_to_aws(aws_module, region, **params): - try: - conn = aws_module.connect_to_region(region, **params) - except(boto.provider.ProfileNotFoundError): - raise AnsibleAWSError("Profile given for AWS was not found. Please fix and retry.") - if not conn: - if region not in [aws_module_region.name for aws_module_region in aws_module.regions()]: - raise AnsibleAWSError("Region %s does not seem to be available for aws module %s. If the region definitely exists, you may need to upgrade " - "boto or extend with endpoints_path" % (region, aws_module.__name__)) - else: - raise AnsibleAWSError("Unknown problem connecting to region %s for aws module %s." % (region, aws_module.__name__)) - if params.get('profile_name'): - conn = boto_fix_security_token_in_profile(conn, params['profile_name']) - return conn - - -def ec2_connect(module): - - """ Return an ec2 connection""" - - region, ec2_url, boto_params = get_aws_connection_info(module) - - # If we have a region specified, connect to its endpoint. - if region: - try: - ec2 = connect_to_aws(boto.ec2, region, **boto_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError, boto.provider.ProfileNotFoundError) as e: - module.fail_json(msg=str(e)) - # Otherwise, no region so we fallback to the old connection method - elif ec2_url: - try: - ec2 = boto.connect_ec2_endpoint(ec2_url, **boto_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError, boto.provider.ProfileNotFoundError) as e: - module.fail_json(msg=str(e)) - else: - module.fail_json(msg="Either region or ec2_url must be specified") - - return ec2 - - -def ansible_dict_to_boto3_filter_list(filters_dict): - - """ Convert an Ansible dict of filters to list of dicts that boto3 can use - Args: - filters_dict (dict): Dict of AWS filters. - Basic Usage: - >>> filters = {'some-aws-id': 'i-01234567'} - >>> ansible_dict_to_boto3_filter_list(filters) - { - 'some-aws-id': 'i-01234567' - } - Returns: - List: List of AWS filters and their values - [ - { - 'Name': 'some-aws-id', - 'Values': [ - 'i-01234567', - ] - } - ] - """ - - filters_list = [] - for k, v in filters_dict.items(): - filter_dict = {'Name': k} - if isinstance(v, string_types): - filter_dict['Values'] = [v] - else: - filter_dict['Values'] = v - - filters_list.append(filter_dict) - - return filters_list - - -def boto3_tag_list_to_ansible_dict(tags_list, tag_name_key_name=None, tag_value_key_name=None): - - """ Convert a boto3 list of resource tags to a flat dict of key:value pairs - Args: - tags_list (list): List of dicts representing AWS tags. - tag_name_key_name (str): Value to use as the key for all tag keys (useful because boto3 doesn't always use "Key") - tag_value_key_name (str): Value to use as the key for all tag values (useful because boto3 doesn't always use "Value") - Basic Usage: - >>> tags_list = [{'Key': 'MyTagKey', 'Value': 'MyTagValue'}] - >>> boto3_tag_list_to_ansible_dict(tags_list) - [ - { - 'Key': 'MyTagKey', - 'Value': 'MyTagValue' - } - ] - Returns: - Dict: Dict of key:value pairs representing AWS tags - { - 'MyTagKey': 'MyTagValue', - } - """ - - if tag_name_key_name and tag_value_key_name: - tag_candidates = {tag_name_key_name: tag_value_key_name} - else: - tag_candidates = {'key': 'value', 'Key': 'Value'} - - if not tags_list: - return {} - for k, v in tag_candidates.items(): - if k in tags_list[0] and v in tags_list[0]: - return dict((tag[k], tag[v]) for tag in tags_list) - raise ValueError("Couldn't find tag key (candidates %s) in tag list %s" % (str(tag_candidates), str(tags_list))) - - -def ansible_dict_to_boto3_tag_list(tags_dict, tag_name_key_name='Key', tag_value_key_name='Value'): - - """ Convert a flat dict of key:value pairs representing AWS resource tags to a boto3 list of dicts - Args: - tags_dict (dict): Dict representing AWS resource tags. - tag_name_key_name (str): Value to use as the key for all tag keys (useful because boto3 doesn't always use "Key") - tag_value_key_name (str): Value to use as the key for all tag values (useful because boto3 doesn't always use "Value") - Basic Usage: - >>> tags_dict = {'MyTagKey': 'MyTagValue'} - >>> ansible_dict_to_boto3_tag_list(tags_dict) - { - 'MyTagKey': 'MyTagValue' - } - Returns: - List: List of dicts containing tag keys and values - [ - { - 'Key': 'MyTagKey', - 'Value': 'MyTagValue' - } - ] - """ - - tags_list = [] - for k, v in tags_dict.items(): - tags_list.append({tag_name_key_name: k, tag_value_key_name: to_native(v)}) - - return tags_list - - -def get_ec2_security_group_ids_from_names(sec_group_list, ec2_connection, vpc_id=None, boto3=True): - - """ Return list of security group IDs from security group names. Note that security group names are not unique - across VPCs. If a name exists across multiple VPCs and no VPC ID is supplied, all matching IDs will be returned. This - will probably lead to a boto exception if you attempt to assign both IDs to a resource so ensure you wrap the call in - a try block - """ - - def get_sg_name(sg, boto3): - - if boto3: - return sg['GroupName'] - else: - return sg.name - - def get_sg_id(sg, boto3): - - if boto3: - return sg['GroupId'] - else: - return sg.id - - sec_group_id_list = [] - - if isinstance(sec_group_list, string_types): - sec_group_list = [sec_group_list] - - # Get all security groups - if boto3: - if vpc_id: - filters = [ - { - 'Name': 'vpc-id', - 'Values': [ - vpc_id, - ] - } - ] - all_sec_groups = ec2_connection.describe_security_groups(Filters=filters)['SecurityGroups'] - else: - all_sec_groups = ec2_connection.describe_security_groups()['SecurityGroups'] - else: - if vpc_id: - filters = {'vpc-id': vpc_id} - all_sec_groups = ec2_connection.get_all_security_groups(filters=filters) - else: - all_sec_groups = ec2_connection.get_all_security_groups() - - unmatched = set(sec_group_list).difference(str(get_sg_name(all_sg, boto3)) for all_sg in all_sec_groups) - sec_group_name_list = list(set(sec_group_list) - set(unmatched)) - - if len(unmatched) > 0: - # If we have unmatched names that look like an ID, assume they are - import re - sec_group_id_list[:] = [sg for sg in unmatched if re.match('sg-[a-fA-F0-9]+$', sg)] - still_unmatched = [sg for sg in unmatched if not re.match('sg-[a-fA-F0-9]+$', sg)] - if len(still_unmatched) > 0: - raise ValueError("The following group names are not valid: %s" % ', '.join(still_unmatched)) - - sec_group_id_list += [str(get_sg_id(all_sg, boto3)) for all_sg in all_sec_groups if str(get_sg_name(all_sg, boto3)) in sec_group_name_list] - - return sec_group_id_list - - -def _hashable_policy(policy, policy_list): - """ - Takes a policy and returns a list, the contents of which are all hashable and sorted. - Example input policy: - {'Version': '2012-10-17', - 'Statement': [{'Action': 's3:PutObjectAcl', - 'Sid': 'AddCannedAcl2', - 'Resource': 'arn:aws:s3:::test_policy/*', - 'Effect': 'Allow', - 'Principal': {'AWS': ['arn:aws:iam::XXXXXXXXXXXX:user/username1', 'arn:aws:iam::XXXXXXXXXXXX:user/username2']} - }]} - Returned value: - [('Statement', ((('Action', (u's3:PutObjectAcl',)), - ('Effect', (u'Allow',)), - ('Principal', ('AWS', ((u'arn:aws:iam::XXXXXXXXXXXX:user/username1',), (u'arn:aws:iam::XXXXXXXXXXXX:user/username2',)))), - ('Resource', (u'arn:aws:s3:::test_policy/*',)), ('Sid', (u'AddCannedAcl2',)))), - ('Version', (u'2012-10-17',)))] - - """ - # Amazon will automatically convert bool and int to strings for us - if isinstance(policy, bool): - return tuple([str(policy).lower()]) - elif isinstance(policy, int): - return tuple([str(policy)]) - - if isinstance(policy, list): - for each in policy: - tupleified = _hashable_policy(each, []) - if isinstance(tupleified, list): - tupleified = tuple(tupleified) - policy_list.append(tupleified) - elif isinstance(policy, string_types) or isinstance(policy, binary_type): - policy = to_text(policy) - # convert root account ARNs to just account IDs - if policy.startswith('arn:aws:iam::') and policy.endswith(':root'): - policy = policy.split(':')[4] - return [policy] - elif isinstance(policy, dict): - sorted_keys = list(policy.keys()) - sorted_keys.sort() - for key in sorted_keys: - tupleified = _hashable_policy(policy[key], []) - if isinstance(tupleified, list): - tupleified = tuple(tupleified) - policy_list.append((key, tupleified)) - - # ensure we aren't returning deeply nested structures of length 1 - if len(policy_list) == 1 and isinstance(policy_list[0], tuple): - policy_list = policy_list[0] - if isinstance(policy_list, list): - if PY3_COMPARISON: - policy_list.sort(key=cmp_to_key(py3cmp)) - else: - policy_list.sort() - return policy_list - - -def py3cmp(a, b): - """ Python 2 can sort lists of mixed types. Strings < tuples. Without this function this fails on Python 3.""" - try: - if a > b: - return 1 - elif a < b: - return -1 - else: - return 0 - except TypeError as e: - # check to see if they're tuple-string - # always say strings are less than tuples (to maintain compatibility with python2) - str_ind = to_text(e).find('str') - tup_ind = to_text(e).find('tuple') - if -1 not in (str_ind, tup_ind): - if str_ind < tup_ind: - return -1 - elif tup_ind < str_ind: - return 1 - raise - - -def compare_policies(current_policy, new_policy): - """ Compares the existing policy and the updated policy - Returns True if there is a difference between policies. - """ - return set(_hashable_policy(new_policy, [])) != set(_hashable_policy(current_policy, [])) - - -def sort_json_policy_dict(policy_dict): - - """ Sort any lists in an IAM JSON policy so that comparison of two policies with identical values but - different orders will return true - Args: - policy_dict (dict): Dict representing IAM JSON policy. - Basic Usage: - >>> my_iam_policy = {'Principle': {'AWS':["31","7","14","101"]} - >>> sort_json_policy_dict(my_iam_policy) - Returns: - Dict: Will return a copy of the policy as a Dict but any List will be sorted - { - 'Principle': { - 'AWS': [ '7', '14', '31', '101' ] - } - } - """ - - def value_is_list(my_list): - - checked_list = [] - for item in my_list: - if isinstance(item, dict): - checked_list.append(sort_json_policy_dict(item)) - elif isinstance(item, list): - checked_list.append(value_is_list(item)) - else: - checked_list.append(item) - - # Sort list. If it's a list of dictionaries, sort by tuple of key-value - # pairs, since Python 3 doesn't allow comparisons such as `<` between dictionaries. - checked_list.sort(key=lambda x: sorted(x.items()) if isinstance(x, dict) else x) - return checked_list - - ordered_policy_dict = {} - for key, value in policy_dict.items(): - if isinstance(value, dict): - ordered_policy_dict[key] = sort_json_policy_dict(value) - elif isinstance(value, list): - ordered_policy_dict[key] = value_is_list(value) - else: - ordered_policy_dict[key] = value - - return ordered_policy_dict - - -def map_complex_type(complex_type, type_map): - """ - Allows to cast elements within a dictionary to a specific type - Example of usage: - - DEPLOYMENT_CONFIGURATION_TYPE_MAP = { - 'maximum_percent': 'int', - 'minimum_healthy_percent': 'int' - } - - deployment_configuration = map_complex_type(module.params['deployment_configuration'], - DEPLOYMENT_CONFIGURATION_TYPE_MAP) - - This ensures all keys within the root element are casted and valid integers - """ - - if complex_type is None: - return - new_type = type(complex_type)() - if isinstance(complex_type, dict): - for key in complex_type: - if key in type_map: - if isinstance(type_map[key], list): - new_type[key] = map_complex_type( - complex_type[key], - type_map[key][0]) - else: - new_type[key] = map_complex_type( - complex_type[key], - type_map[key]) - else: - return complex_type - elif isinstance(complex_type, list): - for i in range(len(complex_type)): - new_type.append(map_complex_type( - complex_type[i], - type_map)) - elif type_map: - return globals()['__builtins__'][type_map](complex_type) - return new_type - - -def compare_aws_tags(current_tags_dict, new_tags_dict, purge_tags=True): - """ - Compare two dicts of AWS tags. Dicts are expected to of been created using 'boto3_tag_list_to_ansible_dict' helper function. - Two dicts are returned - the first is tags to be set, the second is any tags to remove. Since the AWS APIs differ - these may not be able to be used out of the box. - - :param current_tags_dict: - :param new_tags_dict: - :param purge_tags: - :return: tag_key_value_pairs_to_set: a dict of key value pairs that need to be set in AWS. If all tags are identical this dict will be empty - :return: tag_keys_to_unset: a list of key names (type str) that need to be unset in AWS. If no tags need to be unset this list will be empty - """ - - tag_key_value_pairs_to_set = {} - tag_keys_to_unset = [] - - for key in current_tags_dict.keys(): - if key not in new_tags_dict and purge_tags: - tag_keys_to_unset.append(key) - - for key in set(new_tags_dict.keys()) - set(tag_keys_to_unset): - if to_text(new_tags_dict[key]) != current_tags_dict.get(key): - tag_key_value_pairs_to_set[key] = new_tags_dict[key] - - return tag_key_value_pairs_to_set, tag_keys_to_unset diff --git a/lib/ansible/modules/cloud/amazon/_aws_az_facts.py b/lib/ansible/modules/cloud/amazon/_aws_az_facts.py deleted file mode 120000 index 62447531e0..0000000000 --- a/lib/ansible/modules/cloud/amazon/_aws_az_facts.py +++ /dev/null @@ -1 +0,0 @@ -aws_az_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_aws_caller_facts.py b/lib/ansible/modules/cloud/amazon/_aws_caller_facts.py deleted file mode 120000 index 2f8c5b8177..0000000000 --- a/lib/ansible/modules/cloud/amazon/_aws_caller_facts.py +++ /dev/null @@ -1 +0,0 @@ -aws_caller_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_cloudformation_facts.py b/lib/ansible/modules/cloud/amazon/_cloudformation_facts.py deleted file mode 120000 index 19e1b826c7..0000000000 --- a/lib/ansible/modules/cloud/amazon/_cloudformation_facts.py +++ /dev/null @@ -1 +0,0 @@ -cloudformation_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_ec2_ami_facts.py b/lib/ansible/modules/cloud/amazon/_ec2_ami_facts.py deleted file mode 120000 index ba1561bf24..0000000000 --- a/lib/ansible/modules/cloud/amazon/_ec2_ami_facts.py +++ /dev/null @@ -1 +0,0 @@ -ec2_ami_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_ec2_eni_facts.py b/lib/ansible/modules/cloud/amazon/_ec2_eni_facts.py deleted file mode 120000 index fe99939153..0000000000 --- a/lib/ansible/modules/cloud/amazon/_ec2_eni_facts.py +++ /dev/null @@ -1 +0,0 @@ -ec2_eni_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_ec2_group_facts.py b/lib/ansible/modules/cloud/amazon/_ec2_group_facts.py deleted file mode 120000 index 2961fa7568..0000000000 --- a/lib/ansible/modules/cloud/amazon/_ec2_group_facts.py +++ /dev/null @@ -1 +0,0 @@ -ec2_group_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_ec2_snapshot_facts.py b/lib/ansible/modules/cloud/amazon/_ec2_snapshot_facts.py deleted file mode 120000 index eaaf612d83..0000000000 --- a/lib/ansible/modules/cloud/amazon/_ec2_snapshot_facts.py +++ /dev/null @@ -1 +0,0 @@ -ec2_snapshot_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_ec2_vol_facts.py b/lib/ansible/modules/cloud/amazon/_ec2_vol_facts.py deleted file mode 120000 index aa871abac3..0000000000 --- a/lib/ansible/modules/cloud/amazon/_ec2_vol_facts.py +++ /dev/null @@ -1 +0,0 @@ -ec2_vol_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_ec2_vpc_dhcp_option_facts.py b/lib/ansible/modules/cloud/amazon/_ec2_vpc_dhcp_option_facts.py deleted file mode 120000 index d143c2eded..0000000000 --- a/lib/ansible/modules/cloud/amazon/_ec2_vpc_dhcp_option_facts.py +++ /dev/null @@ -1 +0,0 @@ -ec2_vpc_dhcp_option_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_ec2_vpc_net_facts.py b/lib/ansible/modules/cloud/amazon/_ec2_vpc_net_facts.py deleted file mode 120000 index 19f6e9d373..0000000000 --- a/lib/ansible/modules/cloud/amazon/_ec2_vpc_net_facts.py +++ /dev/null @@ -1 +0,0 @@ -ec2_vpc_net_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/_ec2_vpc_subnet_facts.py b/lib/ansible/modules/cloud/amazon/_ec2_vpc_subnet_facts.py deleted file mode 120000 index 47d72315d2..0000000000 --- a/lib/ansible/modules/cloud/amazon/_ec2_vpc_subnet_facts.py +++ /dev/null @@ -1 +0,0 @@ -ec2_vpc_subnet_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/cloud/amazon/aws_az_info.py b/lib/ansible/modules/cloud/amazon/aws_az_info.py deleted file mode 100644 index eccbf4d7d4..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_az_info.py +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 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 - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'supported_by': 'community', - 'status': ['preview'] -} - -DOCUMENTATION = ''' -module: aws_az_info -short_description: Gather information about availability zones in AWS. -description: - - Gather information about availability zones in AWS. - - This module was called C(aws_az_facts) before Ansible 2.9. The usage did not change. -version_added: '2.5' -author: 'Henrique Rodrigues (@Sodki)' -options: - 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_DescribeAvailabilityZones.html) for - possible filters. Filter names and values are case sensitive. You can also use underscores - instead of dashes (-) in the filter keys, which will take precedence in case of conflict. - required: false - default: {} - type: dict -extends_documentation_fragment: - - aws - - ec2 -requirements: [botocore, boto3] -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Gather information about all availability zones -- aws_az_info: - -# Gather information about a single availability zone -- aws_az_info: - filters: - zone-name: eu-west-1a -''' - -RETURN = ''' -availability_zones: - returned: on success - description: > - Availability zones that match the provided filters. Each element consists of a dict with all the information - related to that available zone. - type: list - sample: "[ - { - 'messages': [], - 'region_name': 'us-west-1', - 'state': 'available', - 'zone_name': 'us-west-1b' - }, - { - 'messages': [], - 'region_name': 'us-west-1', - 'state': 'available', - 'zone_name': 'us-west-1c' - } - ]" -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import AWSRetry, ansible_dict_to_boto3_filter_list, camel_dict_to_snake_dict - -try: - from botocore.exceptions import ClientError, BotoCoreError -except ImportError: - pass # Handled by AnsibleAWSModule - - -def main(): - argument_spec = dict( - filters=dict(default={}, type='dict') - ) - - module = AnsibleAWSModule(argument_spec=argument_spec) - if module._name == 'aws_az_facts': - module.deprecate("The 'aws_az_facts' module has been renamed to 'aws_az_info'", version='2.14') - - connection = module.client('ec2', retry_decorator=AWSRetry.jittered_backoff()) - - # Replace filter key underscores with dashes, for compatibility - sanitized_filters = dict((k.replace('_', '-'), v) for k, v in module.params.get('filters').items()) - - try: - availability_zones = connection.describe_availability_zones( - Filters=ansible_dict_to_boto3_filter_list(sanitized_filters) - ) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to describe availability zones.") - - # Turn the boto3 result into ansible_friendly_snaked_names - snaked_availability_zones = [camel_dict_to_snake_dict(az) for az in availability_zones['AvailabilityZones']] - - module.exit_json(availability_zones=snaked_availability_zones) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_caller_info.py b/lib/ansible/modules/cloud/amazon/aws_caller_info.py deleted file mode 100644 index b3e145d0c1..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_caller_info.py +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 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 - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community' -} - -DOCUMENTATION = ''' ---- -module: aws_caller_info -short_description: Get information about the user and account being used to make AWS calls. -description: - - This module returns information about the account and user / role from which the AWS access tokens originate. - - The primary use of this is to get the account id for templating into ARNs or similar to avoid needing to specify this information in inventory. - - This module was called C(aws_caller_facts) before Ansible 2.9. The usage did not change. -version_added: "2.6" - -author: - - Ed Costello (@orthanc) - - Stijn Dubrul (@sdubrul) - -requirements: [ 'botocore', 'boto3' ] -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -- name: Get the current caller identity information - aws_caller_info: - register: caller_info -''' - -RETURN = ''' -account: - description: The account id the access credentials are associated with. - returned: success - type: str - sample: "123456789012" -account_alias: - description: The account alias the access credentials are associated with. - returned: when caller has the iam:ListAccountAliases permission - type: str - sample: "acme-production" -arn: - description: The arn identifying the user the credentials are associated with. - returned: success - type: str - sample: arn:aws:sts::123456789012:federated-user/my-federated-user-name -user_id: - description: | - The user id the access credentials are associated with. Note that this may not correspond to - anything you can look up in the case of roles or federated identities. - returned: success - type: str - sample: 123456789012:my-federated-user-name -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import camel_dict_to_snake_dict - -try: - from botocore.exceptions import BotoCoreError, ClientError -except ImportError: - pass # caught by AnsibleAWSModule - - -def main(): - module = AnsibleAWSModule( - argument_spec={}, - supports_check_mode=True, - ) - if module._name == 'aws_caller_facts': - module.deprecate("The 'aws_caller_facts' module has been renamed to 'aws_caller_info'", version='2.13') - - client = module.client('sts') - - try: - caller_info = client.get_caller_identity() - caller_info.pop('ResponseMetadata', None) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to retrieve caller identity') - - iam_client = module.client('iam') - - try: - # Although a list is returned by list_account_aliases AWS supports maximum one alias per account. - # If an alias is defined it will be returned otherwise a blank string is filled in as account_alias. - # see https://docs.aws.amazon.com/cli/latest/reference/iam/list-account-aliases.html#output - response = iam_client.list_account_aliases() - if response and response['AccountAliases']: - caller_info['account_alias'] = response['AccountAliases'][0] - else: - caller_info['account_alias'] = '' - except (BotoCoreError, ClientError) as e: - # The iam:ListAccountAliases permission is required for this operation to succeed. - # Lacking this permission is handled gracefully by not returning the account_alias. - pass - - module.exit_json( - changed=False, - **camel_dict_to_snake_dict(caller_info)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/aws_s3.py b/lib/ansible/modules/cloud/amazon/aws_s3.py deleted file mode 100644 index 54874f05ce..0000000000 --- a/lib/ansible/modules/cloud/amazon/aws_s3.py +++ /dev/null @@ -1,925 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# 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 - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'core'} - - -DOCUMENTATION = ''' ---- -module: aws_s3 -short_description: manage objects in S3. -description: - - This module allows the user to manage S3 buckets and the objects within them. Includes support for creating and - deleting both objects and buckets, retrieving objects as files or strings and generating download links. - This module has a dependency on boto3 and botocore. -notes: - - In 2.4, this module has been renamed from C(s3) into M(aws_s3). -version_added: "1.1" -options: - bucket: - description: - - Bucket name. - required: true - type: str - dest: - description: - - The destination file path when downloading an object/key with a GET operation. - version_added: "1.3" - type: path - encrypt: - description: - - When set for PUT mode, asks for server-side encryption. - default: true - version_added: "2.0" - type: bool - encryption_mode: - description: - - What encryption mode to use if I(encrypt=true). - default: AES256 - choices: - - AES256 - - aws:kms - version_added: "2.7" - type: str - expiry: - description: - - Time limit (in seconds) for the URL generated and returned by S3/Walrus when performing a I(mode=put) or I(mode=geturl) operation. - default: 600 - aliases: ['expiration'] - type: int - headers: - description: - - Custom headers for PUT operation, as a dictionary of 'key=value' and 'key=value,key=value'. - version_added: "2.0" - type: dict - marker: - description: - - Specifies the key to start with when using list mode. Object keys are returned in alphabetical order, starting with key after the marker in order. - version_added: "2.0" - type: str - max_keys: - description: - - Max number of results to return in list mode, set this if you want to retrieve fewer than the default 1000 keys. - default: 1000 - version_added: "2.0" - type: int - metadata: - description: - - Metadata for PUT operation, as a dictionary of 'key=value' and 'key=value,key=value'. - version_added: "1.6" - type: dict - mode: - description: - - Switches the module behaviour between put (upload), get (download), geturl (return download url, Ansible 1.3+), - getstr (download object as string (1.3+)), list (list keys, Ansible 2.0+), create (bucket), delete (bucket), - and delobj (delete object, Ansible 2.0+). - required: true - choices: ['get', 'put', 'delete', 'create', 'geturl', 'getstr', 'delobj', 'list'] - type: str - object: - description: - - Keyname of the object inside the bucket. Can be used to create "virtual directories", see examples. - type: str - permission: - description: - - 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. - default: ['private'] - version_added: "2.0" - type: list - elements: str - prefix: - description: - - Limits the response to keys that begin with the specified prefix for list mode. - default: "" - version_added: "2.0" - type: str - version: - description: - - Version ID of the object inside the bucket. Can be used to get a specific version of a file if versioning is enabled in the target bucket. - version_added: "2.0" - type: str - overwrite: - description: - - Force overwrite either locally on the filesystem or remotely with the object/key. Used with PUT and GET operations. - Boolean or one of [always, never, different], true is equal to 'always' and false is equal to 'never', new in 2.0. - When this is set to 'different', the md5 sum of the local file is compared with the 'ETag' of the object/key in S3. - The ETag may or may not be an MD5 digest of the object data. See the ETag response header here - U(https://docs.aws.amazon.com/AmazonS3/latest/API/RESTCommonResponseHeaders.html) - default: 'always' - aliases: ['force'] - version_added: "1.2" - type: str - retries: - description: - - On recoverable failure, how many times to retry before actually failing. - default: 0 - version_added: "2.0" - type: int - aliases: ['retry'] - s3_url: - description: - - S3 URL endpoint for usage with Ceph, Eucalyptus and fakes3 etc. Otherwise assumes AWS. - aliases: [ S3_URL ] - type: str - 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 - version_added: "2.7" - rgw: - description: - - Enable Ceph RGW S3 support. This option requires an explicit url via I(s3_url). - default: false - version_added: "2.2" - type: bool - src: - description: - - The source file path when performing a PUT operation. - version_added: "1.3" - type: str - ignore_nonexistent_bucket: - description: - - "Overrides initial bucket lookups in case bucket or iam policies are restrictive. Example: a user may have the - GetObject permission but no other permissions. In this case using the option mode: get will fail without specifying - I(ignore_nonexistent_bucket=true)." - version_added: "2.3" - type: bool - encryption_kms_key_id: - description: - - KMS key id to use when encrypting objects using I(encrypting=aws:kms). Ignored if I(encryption) is not C(aws:kms) - version_added: "2.7" - type: str -requirements: [ "boto3", "botocore" ] -author: - - "Lester Wade (@lwade)" - - "Sloane Hertel (@s-hertel)" -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -- name: Simple PUT operation - aws_s3: - bucket: mybucket - object: /my/desired/key.txt - src: /usr/local/myfile.txt - mode: put - -- name: Simple PUT operation in Ceph RGW S3 - aws_s3: - bucket: mybucket - object: /my/desired/key.txt - src: /usr/local/myfile.txt - mode: put - rgw: true - s3_url: "http://localhost:8000" - -- name: Simple GET operation - aws_s3: - bucket: mybucket - object: /my/desired/key.txt - dest: /usr/local/myfile.txt - mode: get - -- name: Get a specific version of an object. - aws_s3: - bucket: mybucket - object: /my/desired/key.txt - version: 48c9ee5131af7a716edc22df9772aa6f - dest: /usr/local/myfile.txt - mode: get - -- name: PUT/upload with metadata - aws_s3: - bucket: mybucket - object: /my/desired/key.txt - src: /usr/local/myfile.txt - mode: put - metadata: 'Content-Encoding=gzip,Cache-Control=no-cache' - -- name: PUT/upload with custom headers - aws_s3: - bucket: mybucket - object: /my/desired/key.txt - src: /usr/local/myfile.txt - mode: put - headers: 'x-amz-grant-full-control=emailAddress=owner@example.com' - -- name: List keys simple - aws_s3: - bucket: mybucket - mode: list - -- name: List keys all options - aws_s3: - bucket: mybucket - mode: list - prefix: /my/desired/ - marker: /my/desired/0023.txt - max_keys: 472 - -- name: Create an empty bucket - aws_s3: - bucket: mybucket - mode: create - permission: public-read - -- name: Create a bucket with key as directory, in the EU region - aws_s3: - bucket: mybucket - object: /my/directory/path - mode: create - region: eu-west-1 - -- name: Delete a bucket and all contents - aws_s3: - bucket: mybucket - mode: delete - -- name: GET an object but don't download if the file checksums match. New in 2.0 - aws_s3: - bucket: mybucket - object: /my/desired/key.txt - dest: /usr/local/myfile.txt - mode: get - overwrite: different - -- name: Delete an object from a bucket - aws_s3: - bucket: mybucket - object: /my/desired/key.txt - mode: delobj -''' - -RETURN = ''' -msg: - description: Message indicating the status of the operation. - returned: always - type: str - sample: PUT operation complete -url: - description: URL of the object. - returned: (for put and geturl operations) - type: str - sample: https://my-bucket.s3.amazonaws.com/my-key.txt?AWSAccessKeyId=<access-key>&Expires=1506888865&Signature=<signature> -expiry: - description: Number of seconds the presigned url is valid for. - returned: (for geturl operation) - type: int - sample: 600 -contents: - description: Contents of the object as string. - returned: (for getstr operation) - type: str - sample: "Hello, world!" -s3_keys: - description: List of object keys. - returned: (for list operation) - type: list - elements: str - sample: - - prefix1/ - - prefix1/key1 - - prefix1/key2 -''' - -import mimetypes -import os -from ansible.module_utils.six.moves.urllib.parse import urlparse -from ssl import SSLError -from ansible.module_utils.basic import to_text, to_native -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.aws.s3 import calculate_etag, HAS_MD5 -from ansible.module_utils.ec2 import get_aws_connection_info, boto3_conn - -try: - import botocore -except ImportError: - pass # will be detected by imported AnsibleAWSModule - -IGNORE_S3_DROP_IN_EXCEPTIONS = ['XNotImplemented', 'NotImplemented'] - - -class Sigv4Required(Exception): - pass - - -def key_check(module, s3, bucket, obj, version=None, validate=True): - exists = True - try: - if version: - s3.head_object(Bucket=bucket, Key=obj, VersionId=version) - else: - s3.head_object(Bucket=bucket, Key=obj) - except botocore.exceptions.ClientError as e: - # if a client error is thrown, check if it's a 404 error - # if it's a 404 error, then the object does not exist - error_code = int(e.response['Error']['Code']) - if error_code == 404: - exists = False - elif error_code == 403 and validate is False: - pass - else: - module.fail_json_aws(e, msg="Failed while looking up object (during key check) %s." % obj) - except botocore.exceptions.BotoCoreError as e: - module.fail_json_aws(e, msg="Failed while looking up object (during key check) %s." % obj) - return exists - - -def etag_compare(module, local_file, s3, bucket, obj, version=None): - s3_etag = get_etag(s3, bucket, obj, version=version) - local_etag = calculate_etag(module, local_file, s3_etag, s3, bucket, obj, version) - - return s3_etag == local_etag - - -def get_etag(s3, bucket, obj, version=None): - if version: - key_check = s3.head_object(Bucket=bucket, Key=obj, VersionId=version) - else: - key_check = s3.head_object(Bucket=bucket, Key=obj) - if not key_check: - return None - return key_check['ETag'] - - -def bucket_check(module, s3, bucket, validate=True): - exists = True - try: - s3.head_bucket(Bucket=bucket) - except botocore.exceptions.ClientError as e: - # If a client error is thrown, then check that it was a 404 error. - # If it was a 404 error, then the bucket does not exist. - error_code = int(e.response['Error']['Code']) - if error_code == 404: - exists = False - elif error_code == 403 and validate is False: - pass - else: - module.fail_json_aws(e, msg="Failed while looking up bucket (during bucket_check) %s." % bucket) - except botocore.exceptions.EndpointConnectionError as e: - module.fail_json_aws(e, msg="Invalid endpoint provided") - except botocore.exceptions.BotoCoreError as e: - module.fail_json_aws(e, msg="Failed while looking up bucket (during bucket_check) %s." % bucket) - return exists - - -def create_bucket(module, s3, bucket, location=None): - if module.check_mode: - module.exit_json(msg="CREATE operation skipped - running in check mode", changed=True) - configuration = {} - if location not in ('us-east-1', None): - configuration['LocationConstraint'] = location - try: - if len(configuration) > 0: - s3.create_bucket(Bucket=bucket, CreateBucketConfiguration=configuration) - else: - s3.create_bucket(Bucket=bucket) - if module.params.get('permission'): - # Wait for the bucket to exist before setting ACLs - s3.get_waiter('bucket_exists').wait(Bucket=bucket) - for acl in module.params.get('permission'): - s3.put_bucket_acl(ACL=acl, Bucket=bucket) - except botocore.exceptions.ClientError as e: - if e.response['Error']['Code'] in IGNORE_S3_DROP_IN_EXCEPTIONS: - module.warn("PutBucketAcl is not implemented by your storage provider. Set the permission parameters to the empty list to avoid this warning") - else: - module.fail_json_aws(e, msg="Failed while creating bucket or setting acl (check that you have CreateBucket and PutBucketAcl permission).") - except botocore.exceptions.BotoCoreError as e: - module.fail_json_aws(e, msg="Failed while creating bucket or setting acl (check that you have CreateBucket and PutBucketAcl permission).") - - if bucket: - return True - - -def paginated_list(s3, **pagination_params): - pg = s3.get_paginator('list_objects_v2') - for page in pg.paginate(**pagination_params): - yield [data['Key'] for data in page.get('Contents', [])] - - -def paginated_versioned_list_with_fallback(s3, **pagination_params): - try: - versioned_pg = s3.get_paginator('list_object_versions') - for page in versioned_pg.paginate(**pagination_params): - delete_markers = [{'Key': data['Key'], 'VersionId': data['VersionId']} for data in page.get('DeleteMarkers', [])] - current_objects = [{'Key': data['Key'], 'VersionId': data['VersionId']} for data in page.get('Versions', [])] - yield delete_markers + current_objects - except botocore.exceptions.ClientError as e: - if to_text(e.response['Error']['Code']) in IGNORE_S3_DROP_IN_EXCEPTIONS + ['AccessDenied']: - for page in paginated_list(s3, **pagination_params): - yield [{'Key': data['Key']} for data in page] - - -def list_keys(module, s3, bucket, prefix, marker, max_keys): - pagination_params = {'Bucket': bucket} - for param_name, param_value in (('Prefix', prefix), ('StartAfter', marker), ('MaxKeys', max_keys)): - pagination_params[param_name] = param_value - try: - keys = sum(paginated_list(s3, **pagination_params), []) - module.exit_json(msg="LIST operation complete", s3_keys=keys) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed while listing the keys in the bucket {0}".format(bucket)) - - -def delete_bucket(module, s3, bucket): - if module.check_mode: - module.exit_json(msg="DELETE operation skipped - running in check mode", changed=True) - try: - exists = bucket_check(module, s3, bucket) - if exists is False: - return False - # if there are contents then we need to delete them before we can delete the bucket - for keys in paginated_versioned_list_with_fallback(s3, Bucket=bucket): - if keys: - s3.delete_objects(Bucket=bucket, Delete={'Objects': keys}) - s3.delete_bucket(Bucket=bucket) - return True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed while deleting bucket %s." % bucket) - - -def delete_key(module, s3, bucket, obj): - if module.check_mode: - module.exit_json(msg="DELETE operation skipped - running in check mode", changed=True) - try: - s3.delete_object(Bucket=bucket, Key=obj) - module.exit_json(msg="Object deleted from bucket %s." % (bucket), changed=True) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed while trying to delete %s." % obj) - - -def create_dirkey(module, s3, bucket, obj, encrypt): - if module.check_mode: - module.exit_json(msg="PUT operation skipped - running in check mode", changed=True) - try: - params = {'Bucket': bucket, 'Key': obj, 'Body': b''} - if encrypt: - params['ServerSideEncryption'] = module.params['encryption_mode'] - if module.params['encryption_kms_key_id'] and module.params['encryption_mode'] == 'aws:kms': - params['SSEKMSKeyId'] = module.params['encryption_kms_key_id'] - - s3.put_object(**params) - for acl in module.params.get('permission'): - s3.put_object_acl(ACL=acl, Bucket=bucket, Key=obj) - except botocore.exceptions.ClientError as e: - if e.response['Error']['Code'] in IGNORE_S3_DROP_IN_EXCEPTIONS: - module.warn("PutObjectAcl is not implemented by your storage provider. Set the permissions parameters to the empty list to avoid this warning") - else: - module.fail_json_aws(e, msg="Failed while creating object %s." % obj) - except botocore.exceptions.BotoCoreError as e: - module.fail_json_aws(e, msg="Failed while creating object %s." % obj) - module.exit_json(msg="Virtual directory %s created in bucket %s" % (obj, bucket), changed=True) - - -def path_check(path): - if os.path.exists(path): - return True - else: - return False - - -def option_in_extra_args(option): - temp_option = option.replace('-', '').lower() - - allowed_extra_args = {'acl': 'ACL', 'cachecontrol': 'CacheControl', 'contentdisposition': 'ContentDisposition', - 'contentencoding': 'ContentEncoding', 'contentlanguage': 'ContentLanguage', - 'contenttype': 'ContentType', 'expires': 'Expires', 'grantfullcontrol': 'GrantFullControl', - 'grantread': 'GrantRead', 'grantreadacp': 'GrantReadACP', 'grantwriteacp': 'GrantWriteACP', - 'metadata': 'Metadata', 'requestpayer': 'RequestPayer', 'serversideencryption': 'ServerSideEncryption', - 'storageclass': 'StorageClass', 'ssecustomeralgorithm': 'SSECustomerAlgorithm', 'ssecustomerkey': 'SSECustomerKey', - 'ssecustomerkeymd5': 'SSECustomerKeyMD5', 'ssekmskeyid': 'SSEKMSKeyId', 'websiteredirectlocation': 'WebsiteRedirectLocation'} - - if temp_option in allowed_extra_args: - return allowed_extra_args[temp_option] - - -def upload_s3file(module, s3, bucket, obj, src, expiry, metadata, encrypt, headers): - if module.check_mode: - module.exit_json(msg="PUT operation skipped - running in check mode", changed=True) - try: - extra = {} - if encrypt: - extra['ServerSideEncryption'] = module.params['encryption_mode'] - if module.params['encryption_kms_key_id'] and module.params['encryption_mode'] == 'aws:kms': - extra['SSEKMSKeyId'] = module.params['encryption_kms_key_id'] - if metadata: - extra['Metadata'] = {} - - # determine object metadata and extra arguments - for option in metadata: - extra_args_option = option_in_extra_args(option) - if extra_args_option is not None: - extra[extra_args_option] = metadata[option] - else: - extra['Metadata'][option] = metadata[option] - - if 'ContentType' not in extra: - content_type = mimetypes.guess_type(src)[0] - if content_type is None: - # s3 default content type - content_type = 'binary/octet-stream' - extra['ContentType'] = content_type - - s3.upload_file(Filename=src, Bucket=bucket, Key=obj, ExtraArgs=extra) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Unable to complete PUT operation.") - try: - for acl in module.params.get('permission'): - s3.put_object_acl(ACL=acl, Bucket=bucket, Key=obj) - except botocore.exceptions.ClientError as e: - if e.response['Error']['Code'] in IGNORE_S3_DROP_IN_EXCEPTIONS: - module.warn("PutObjectAcl is not implemented by your storage provider. Set the permission parameters to the empty list to avoid this warning") - else: - module.fail_json_aws(e, msg="Unable to set object ACL") - except botocore.exceptions.BotoCoreError as e: - module.fail_json_aws(e, msg="Unable to set object ACL") - try: - url = s3.generate_presigned_url(ClientMethod='put_object', - Params={'Bucket': bucket, 'Key': obj}, - ExpiresIn=expiry) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Unable to generate presigned URL") - module.exit_json(msg="PUT operation complete", url=url, changed=True) - - -def download_s3file(module, s3, bucket, obj, dest, retries, version=None): - if module.check_mode: - module.exit_json(msg="GET operation skipped - running in check mode", changed=True) - # retries is the number of loops; range/xrange needs to be one - # more to get that count of loops. - try: - if version: - key = s3.get_object(Bucket=bucket, Key=obj, VersionId=version) - else: - key = s3.get_object(Bucket=bucket, Key=obj) - except botocore.exceptions.ClientError as e: - if e.response['Error']['Code'] == 'InvalidArgument' and 'require AWS Signature Version 4' in to_text(e): - raise Sigv4Required() - elif e.response['Error']['Code'] not in ("403", "404"): - # AccessDenied errors may be triggered if 1) file does not exist or 2) file exists but - # user does not have the s3:GetObject permission. 404 errors are handled by download_file(). - module.fail_json_aws(e, msg="Could not find the key %s." % obj) - except botocore.exceptions.BotoCoreError as e: - module.fail_json_aws(e, msg="Could not find the key %s." % obj) - - optional_kwargs = {'ExtraArgs': {'VersionId': version}} if version else {} - for x in range(0, retries + 1): - try: - s3.download_file(bucket, obj, dest, **optional_kwargs) - module.exit_json(msg="GET operation complete", changed=True) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - # actually fail on last pass through the loop. - if x >= retries: - module.fail_json_aws(e, msg="Failed while downloading %s." % obj) - # otherwise, try again, this may be a transient timeout. - except SSLError as e: # will ClientError catch SSLError? - # actually fail on last pass through the loop. - if x >= retries: - module.fail_json_aws(e, msg="s3 download failed") - # otherwise, try again, this may be a transient timeout. - - -def download_s3str(module, s3, bucket, obj, version=None, validate=True): - if module.check_mode: - module.exit_json(msg="GET operation skipped - running in check mode", changed=True) - try: - if version: - contents = to_native(s3.get_object(Bucket=bucket, Key=obj, VersionId=version)["Body"].read()) - else: - contents = to_native(s3.get_object(Bucket=bucket, Key=obj)["Body"].read()) - module.exit_json(msg="GET operation complete", contents=contents, changed=True) - except botocore.exceptions.ClientError as e: - if e.response['Error']['Code'] == 'InvalidArgument' and 'require AWS Signature Version 4' in to_text(e): - raise Sigv4Required() - else: - module.fail_json_aws(e, msg="Failed while getting contents of object %s as a string." % obj) - except botocore.exceptions.BotoCoreError as e: - module.fail_json_aws(e, msg="Failed while getting contents of object %s as a string." % obj) - - -def get_download_url(module, s3, bucket, obj, expiry, changed=True): - try: - url = s3.generate_presigned_url(ClientMethod='get_object', - Params={'Bucket': bucket, 'Key': obj}, - ExpiresIn=expiry) - module.exit_json(msg="Download url:", url=url, expiry=expiry, changed=changed) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed while getting download url.") - - -def is_fakes3(s3_url): - """ Return True if s3_url has scheme fakes3:// """ - if s3_url is not None: - return urlparse(s3_url).scheme in ('fakes3', 'fakes3s') - else: - return False - - -def get_s3_connection(module, aws_connect_kwargs, location, rgw, s3_url, sig_4=False): - if s3_url and rgw: # TODO - test this - rgw = urlparse(s3_url) - params = dict(module=module, conn_type='client', resource='s3', use_ssl=rgw.scheme == 'https', region=location, endpoint=s3_url, **aws_connect_kwargs) - elif is_fakes3(s3_url): - fakes3 = urlparse(s3_url) - port = fakes3.port - if fakes3.scheme == 'fakes3s': - protocol = "https" - if port is None: - port = 443 - else: - protocol = "http" - if port is None: - port = 80 - params = dict(module=module, conn_type='client', resource='s3', region=location, - endpoint="%s://%s:%s" % (protocol, fakes3.hostname, to_text(port)), - use_ssl=fakes3.scheme == 'fakes3s', **aws_connect_kwargs) - else: - params = dict(module=module, conn_type='client', resource='s3', region=location, endpoint=s3_url, **aws_connect_kwargs) - if module.params['mode'] == 'put' and module.params['encryption_mode'] == 'aws:kms': - params['config'] = botocore.client.Config(signature_version='s3v4') - elif module.params['mode'] in ('get', 'getstr') and sig_4: - params['config'] = botocore.client.Config(signature_version='s3v4') - if module.params['dualstack']: - dualconf = botocore.client.Config(s3={'use_dualstack_endpoint': True}) - if 'config' in params: - params['config'] = params['config'].merge(dualconf) - else: - params['config'] = dualconf - return boto3_conn(**params) - - -def main(): - argument_spec = dict( - bucket=dict(required=True), - dest=dict(default=None, type='path'), - encrypt=dict(default=True, type='bool'), - encryption_mode=dict(choices=['AES256', 'aws:kms'], default='AES256'), - expiry=dict(default=600, type='int', aliases=['expiration']), - headers=dict(type='dict'), - marker=dict(default=""), - max_keys=dict(default=1000, type='int'), - metadata=dict(type='dict'), - mode=dict(choices=['get', 'put', 'delete', 'create', 'geturl', 'getstr', 'delobj', 'list'], required=True), - object=dict(), - permission=dict(type='list', default=['private']), - version=dict(default=None), - overwrite=dict(aliases=['force'], default='always'), - prefix=dict(default=""), - retries=dict(aliases=['retry'], type='int', default=0), - s3_url=dict(aliases=['S3_URL']), - dualstack=dict(default='no', type='bool'), - rgw=dict(default='no', type='bool'), - src=dict(), - ignore_nonexistent_bucket=dict(default=False, type='bool'), - encryption_kms_key_id=dict() - ) - module = AnsibleAWSModule( - argument_spec=argument_spec, - supports_check_mode=True, - required_if=[['mode', 'put', ['src', 'object']], - ['mode', 'get', ['dest', 'object']], - ['mode', 'getstr', ['object']], - ['mode', 'geturl', ['object']]], - ) - - bucket = module.params.get('bucket') - encrypt = module.params.get('encrypt') - expiry = module.params.get('expiry') - dest = module.params.get('dest', '') - headers = module.params.get('headers') - marker = module.params.get('marker') - max_keys = module.params.get('max_keys') - metadata = module.params.get('metadata') - mode = module.params.get('mode') - obj = module.params.get('object') - version = module.params.get('version') - overwrite = module.params.get('overwrite') - prefix = module.params.get('prefix') - retries = module.params.get('retries') - s3_url = module.params.get('s3_url') - dualstack = module.params.get('dualstack') - rgw = module.params.get('rgw') - src = module.params.get('src') - ignore_nonexistent_bucket = module.params.get('ignore_nonexistent_bucket') - - object_canned_acl = ["private", "public-read", "public-read-write", "aws-exec-read", "authenticated-read", "bucket-owner-read", "bucket-owner-full-control"] - bucket_canned_acl = ["private", "public-read", "public-read-write", "authenticated-read"] - - if overwrite not in ['always', 'never', 'different']: - if module.boolean(overwrite): - overwrite = 'always' - else: - overwrite = 'never' - - if overwrite == 'different' and not HAS_MD5: - module.fail_json(msg='overwrite=different is unavailable: ETag calculation requires MD5 support') - - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - - if region in ('us-east-1', '', None): - # default to US Standard region - location = 'us-east-1' - else: - # Boto uses symbolic names for locations but region strings will - # actually work fine for everything except us-east-1 (US Standard) - location = region - - if module.params.get('object'): - obj = module.params['object'] - # If there is a top level object, do nothing - if the object starts with / - # remove the leading character to maintain compatibility with Ansible versions < 2.4 - if obj.startswith('/'): - obj = obj[1:] - - # Bucket deletion does not require obj. Prevents ambiguity with delobj. - if obj and mode == "delete": - module.fail_json(msg='Parameter obj cannot be used with mode=delete') - - # allow eucarc environment variables to be used if ansible vars aren't set - if not s3_url and 'S3_URL' in os.environ: - s3_url = os.environ['S3_URL'] - - if dualstack and s3_url is not None and 'amazonaws.com' not in s3_url: - module.fail_json(msg='dualstack only applies to AWS S3') - - if dualstack and not module.botocore_at_least('1.4.45'): - module.fail_json(msg='dualstack requires botocore >= 1.4.45') - - # rgw requires an explicit url - if rgw and not s3_url: - module.fail_json(msg='rgw flavour requires s3_url') - - # Look at s3_url and tweak connection settings - # if connecting to RGW, Walrus or fakes3 - if s3_url: - for key in ['validate_certs', 'security_token', 'profile_name']: - aws_connect_kwargs.pop(key, None) - s3 = get_s3_connection(module, aws_connect_kwargs, location, rgw, s3_url) - - validate = not ignore_nonexistent_bucket - - # separate types of ACLs - bucket_acl = [acl for acl in module.params.get('permission') if acl in bucket_canned_acl] - object_acl = [acl for acl in module.params.get('permission') if acl in object_canned_acl] - error_acl = [acl for acl in module.params.get('permission') if acl not in bucket_canned_acl and acl not in object_canned_acl] - if error_acl: - module.fail_json(msg='Unknown permission specified: %s' % error_acl) - - # First, we check to see if the bucket exists, we get "bucket" returned. - bucketrtn = bucket_check(module, s3, bucket, validate=validate) - - if validate and mode not in ('create', 'put', 'delete') and not bucketrtn: - module.fail_json(msg="Source bucket cannot be found.") - - if mode == 'get': - keyrtn = key_check(module, s3, bucket, obj, version=version, validate=validate) - if keyrtn is False: - if version: - module.fail_json(msg="Key %s with version id %s does not exist." % (obj, version)) - else: - module.fail_json(msg="Key %s does not exist." % obj) - - if path_check(dest) and overwrite != 'always': - if overwrite == 'never': - module.exit_json(msg="Local object already exists and overwrite is disabled.", changed=False) - if etag_compare(module, dest, s3, bucket, obj, version=version): - module.exit_json(msg="Local and remote object are identical, ignoring. Use overwrite=always parameter to force.", changed=False) - - try: - download_s3file(module, s3, bucket, obj, dest, retries, version=version) - except Sigv4Required: - s3 = get_s3_connection(module, aws_connect_kwargs, location, rgw, s3_url, sig_4=True) - download_s3file(module, s3, bucket, obj, dest, retries, version=version) - - if mode == 'put': - - # if putting an object in a bucket yet to be created, acls for the bucket and/or the object may be specified - # these were separated into the variables bucket_acl and object_acl above - - if not path_check(src): - module.fail_json(msg="Local object for PUT does not exist") - - if bucketrtn: - keyrtn = key_check(module, s3, bucket, obj, version=version, validate=validate) - else: - # If the bucket doesn't exist we should create it. - # only use valid bucket acls for create_bucket function - module.params['permission'] = bucket_acl - create_bucket(module, s3, bucket, location) - - if keyrtn and overwrite != 'always': - if overwrite == 'never' or etag_compare(module, src, s3, bucket, obj): - # Return the download URL for the existing object - get_download_url(module, s3, bucket, obj, expiry, changed=False) - - # only use valid object acls for the upload_s3file function - module.params['permission'] = object_acl - upload_s3file(module, s3, bucket, obj, src, expiry, metadata, encrypt, headers) - - # Delete an object from a bucket, not the entire bucket - if mode == 'delobj': - if obj is None: - module.fail_json(msg="object parameter is required") - if bucket: - deletertn = delete_key(module, s3, bucket, obj) - if deletertn is True: - module.exit_json(msg="Object deleted from bucket %s." % bucket, changed=True) - else: - module.fail_json(msg="Bucket parameter is required.") - - # Delete an entire bucket, including all objects in the bucket - if mode == 'delete': - if bucket: - deletertn = delete_bucket(module, s3, bucket) - if deletertn is True: - module.exit_json(msg="Bucket %s and all keys have been deleted." % bucket, changed=True) - else: - module.fail_json(msg="Bucket parameter is required.") - - # Support for listing a set of keys - if mode == 'list': - exists = bucket_check(module, s3, bucket) - - # If the bucket does not exist then bail out - if not exists: - module.fail_json(msg="Target bucket (%s) cannot be found" % bucket) - - list_keys(module, s3, bucket, prefix, marker, max_keys) - - # Need to research how to create directories without "populating" a key, so this should just do bucket creation for now. - # WE SHOULD ENABLE SOME WAY OF CREATING AN EMPTY KEY TO CREATE "DIRECTORY" STRUCTURE, AWS CONSOLE DOES THIS. - if mode == 'create': - - # if both creating a bucket and putting an object in it, acls for the bucket and/or the object may be specified - # these were separated above into the variables bucket_acl and object_acl - - if bucket and not obj: - if bucketrtn: - module.exit_json(msg="Bucket already exists.", changed=False) - else: - # only use valid bucket acls when creating the bucket - module.params['permission'] = bucket_acl - module.exit_json(msg="Bucket created successfully", changed=create_bucket(module, s3, bucket, location)) - if bucket and obj: - if obj.endswith('/'): - dirobj = obj - else: - dirobj = obj + "/" - if bucketrtn: - if key_check(module, s3, bucket, dirobj): - module.exit_json(msg="Bucket %s and key %s already exists." % (bucket, obj), changed=False) - else: - # setting valid object acls for the create_dirkey function - module.params['permission'] = object_acl - create_dirkey(module, s3, bucket, dirobj, encrypt) - else: - # only use valid bucket acls for the create_bucket function - module.params['permission'] = bucket_acl - created = create_bucket(module, s3, bucket, location) - # only use valid object acls for the create_dirkey function - module.params['permission'] = object_acl - create_dirkey(module, s3, bucket, dirobj, encrypt) - - # Support for grabbing the time-expired URL for an object in S3/Walrus. - if mode == 'geturl': - if not bucket and not obj: - module.fail_json(msg="Bucket and Object parameters must be set") - - keyrtn = key_check(module, s3, bucket, obj, version=version, validate=validate) - if keyrtn: - get_download_url(module, s3, bucket, obj, expiry) - else: - module.fail_json(msg="Key %s does not exist." % obj) - - if mode == 'getstr': - if bucket and obj: - keyrtn = key_check(module, s3, bucket, obj, version=version, validate=validate) - if keyrtn: - try: - download_s3str(module, s3, bucket, obj, version=version) - except Sigv4Required: - s3 = get_s3_connection(module, aws_connect_kwargs, location, rgw, s3_url, sig_4=True) - download_s3str(module, s3, bucket, obj, version=version) - elif version is not None: - module.fail_json(msg="Key %s with version id %s does not exist." % (obj, version)) - else: - module.fail_json(msg="Key %s does not exist." % obj) - - module.exit_json(failed=False) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/cloudformation.py b/lib/ansible/modules/cloud/amazon/cloudformation.py deleted file mode 100644 index cd03146501..0000000000 --- a/lib/ansible/modules/cloud/amazon/cloudformation.py +++ /dev/null @@ -1,837 +0,0 @@ -#!/usr/bin/python - -# Copyright: (c) 2017, 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 - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'core'} - - -DOCUMENTATION = ''' ---- -module: cloudformation -short_description: Create or delete an AWS CloudFormation stack -description: - - Launches or updates an AWS CloudFormation stack and waits for it complete. -notes: - - CloudFormation features change often, and this module tries to keep up. That means your botocore version should be fresh. - The version listed in the requirements is the oldest version that works with the module as a whole. - Some features may require recent versions, and we do not pinpoint a minimum version for each feature. - Instead of relying on the minimum version, keep botocore up to date. AWS is always releasing features and fixing bugs. -version_added: "1.1" -options: - stack_name: - description: - - Name of the CloudFormation stack. - required: true - type: str - disable_rollback: - description: - - If a stacks fails to form, rollback will remove the stack. - default: false - type: bool - on_create_failure: - description: - - Action to take upon failure of stack creation. Incompatible with the I(disable_rollback) option. - choices: - - DO_NOTHING - - ROLLBACK - - DELETE - version_added: "2.8" - type: str - create_timeout: - description: - - The amount of time (in minutes) that can pass before the stack status becomes CREATE_FAILED - version_added: "2.6" - type: int - template_parameters: - description: - - A list of hashes of all the template variables for the stack. The value can be a string or a dict. - - Dict can be used to set additional template parameter attributes like UsePreviousValue (see example). - default: {} - type: dict - state: - description: - - If I(state=present), stack will be created. - - If I(state=present) and if stack exists and template has changed, it will be updated. - - If I(state=absent), stack will be removed. - default: present - choices: [ present, absent ] - type: str - template: - description: - - The local path of the CloudFormation template. - - This must be the full path to the file, relative to the working directory. If using roles this may look - like C(roles/cloudformation/files/cloudformation-example.json). - - If I(state=present) and the stack does not exist yet, either I(template), I(template_body) or I(template_url) - must be specified (but only one of them). - - If I(state=present), the stack does exist, and neither I(template), - I(template_body) nor I(template_url) are specified, the previous template will be reused. - type: path - notification_arns: - description: - - A comma separated list of Simple Notification Service (SNS) topic ARNs to publish stack related events. - version_added: "2.0" - type: str - stack_policy: - description: - - The path of the CloudFormation stack policy. A policy cannot be removed once placed, but it can be modified. - for instance, allow all updates U(https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/protect-stack-resources.html#d0e9051) - version_added: "1.9" - type: str - tags: - description: - - Dictionary of tags to associate with stack and its resources during stack creation. - - Can be updated later, updating tags removes previous entries. - version_added: "1.4" - type: dict - template_url: - description: - - Location of file containing the template body. The URL must point to a template (max size 307,200 bytes) located in an - S3 bucket in the same region as the stack. - - If I(state=present) and the stack does not exist yet, either I(template), I(template_body) or I(template_url) - must be specified (but only one of them). - - If I(state=present), the stack does exist, and neither I(template), I(template_body) nor I(template_url) are specified, - the previous template will be reused. - version_added: "2.0" - type: str - create_changeset: - description: - - "If stack already exists create a changeset instead of directly applying changes. See the AWS Change Sets docs - U(https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-changesets.html)." - - "WARNING: if the stack does not exist, it will be created without changeset. If I(state=absent), the stack will be - deleted immediately with no changeset." - type: bool - default: false - version_added: "2.4" - changeset_name: - description: - - Name given to the changeset when creating a changeset. - - Only used when I(create_changeset=true). - - By default a name prefixed with Ansible-STACKNAME is generated based on input parameters. - See the AWS Change Sets docs for more information - U(https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-changesets.html) - version_added: "2.4" - type: str - template_format: - description: - - This parameter is ignored since Ansible 2.3 and will be removed in Ansible 2.14. - - Templates are now passed raw to CloudFormation regardless of format. - version_added: "2.0" - type: str - role_arn: - description: - - The role that AWS CloudFormation assumes to create the stack. See the AWS CloudFormation Service Role - docs U(https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-servicerole.html) - version_added: "2.3" - type: str - termination_protection: - description: - - Enable or disable termination protection on the stack. Only works with botocore >= 1.7.18. - type: bool - version_added: "2.5" - template_body: - description: - - Template body. Use this to pass in the actual body of the CloudFormation template. - - If I(state=present) and the stack does not exist yet, either I(template), I(template_body) or I(template_url) - must be specified (but only one of them). - - If I(state=present), the stack does exist, and neither I(template), I(template_body) nor I(template_url) - are specified, the previous template will be reused. - version_added: "2.5" - type: str - events_limit: - description: - - Maximum number of CloudFormation events to fetch from a stack when creating or updating it. - default: 200 - version_added: "2.7" - type: int - backoff_delay: - description: - - Number of seconds to wait for the next retry. - default: 3 - version_added: "2.8" - type: int - required: False - backoff_max_delay: - description: - - Maximum amount of time to wait between retries. - default: 30 - version_added: "2.8" - type: int - required: False - backoff_retries: - description: - - Number of times to retry operation. - - AWS API throttling mechanism fails CloudFormation module so we have to retry a couple of times. - default: 10 - version_added: "2.8" - type: int - required: False - capabilities: - description: - - Specify capabilities that stack template contains. - - Valid values are C(CAPABILITY_IAM), C(CAPABILITY_NAMED_IAM) and C(CAPABILITY_AUTO_EXPAND). - type: list - elements: str - version_added: "2.8" - default: [ CAPABILITY_IAM, CAPABILITY_NAMED_IAM ] - -author: "James S. Martin (@jsmartin)" -extends_documentation_fragment: -- aws -- ec2 -requirements: [ boto3, botocore>=1.5.45 ] -''' - -EXAMPLES = ''' -- name: create a cloudformation stack - cloudformation: - stack_name: "ansible-cloudformation" - state: "present" - region: "us-east-1" - disable_rollback: true - template: "files/cloudformation-example.json" - template_parameters: - KeyName: "jmartin" - DiskType: "ephemeral" - InstanceType: "m1.small" - ClusterSize: 3 - tags: - Stack: "ansible-cloudformation" - -# Basic role example -- name: create a stack, specify role that cloudformation assumes - cloudformation: - stack_name: "ansible-cloudformation" - state: "present" - region: "us-east-1" - disable_rollback: true - template: "roles/cloudformation/files/cloudformation-example.json" - role_arn: 'arn:aws:iam::123456789012:role/cloudformation-iam-role' - -- name: delete a stack - cloudformation: - stack_name: "ansible-cloudformation-old" - state: "absent" - -# Create a stack, pass in template from a URL, disable rollback if stack creation fails, -# pass in some parameters to the template, provide tags for resources created -- name: create a stack, pass in the template via an URL - cloudformation: - stack_name: "ansible-cloudformation" - state: present - region: us-east-1 - disable_rollback: true - template_url: https://s3.amazonaws.com/my-bucket/cloudformation.template - template_parameters: - KeyName: jmartin - DiskType: ephemeral - InstanceType: m1.small - ClusterSize: 3 - tags: - Stack: ansible-cloudformation - -# Create a stack, passing in template body using lookup of Jinja2 template, disable rollback if stack creation fails, -# pass in some parameters to the template, provide tags for resources created -- name: create a stack, pass in the template body via lookup template - cloudformation: - stack_name: "ansible-cloudformation" - state: present - region: us-east-1 - disable_rollback: true - template_body: "{{ lookup('template', 'cloudformation.j2') }}" - template_parameters: - KeyName: jmartin - DiskType: ephemeral - InstanceType: m1.small - ClusterSize: 3 - tags: - Stack: ansible-cloudformation - -# Pass a template parameter which uses CloudFormation's UsePreviousValue attribute -# When use_previous_value is set to True, the given value will be ignored and -# CloudFormation will use the value from a previously submitted template. -# If use_previous_value is set to False (default) the given value is used. -- cloudformation: - stack_name: "ansible-cloudformation" - state: "present" - region: "us-east-1" - template: "files/cloudformation-example.json" - template_parameters: - DBSnapshotIdentifier: - use_previous_value: True - value: arn:aws:rds:es-east-1:000000000000:snapshot:rds:my-db-snapshot - DBName: - use_previous_value: True - tags: - Stack: "ansible-cloudformation" - -# Enable termination protection on a stack. -# If the stack already exists, this will update its termination protection -- name: enable termination protection during stack creation - cloudformation: - stack_name: my_stack - state: present - template_url: https://s3.amazonaws.com/my-bucket/cloudformation.template - termination_protection: yes - -# Configure TimeoutInMinutes before the stack status becomes CREATE_FAILED -# In this case, if disable_rollback is not set or is set to false, the stack will be rolled back. -- name: enable termination protection during stack creation - cloudformation: - stack_name: my_stack - state: present - template_url: https://s3.amazonaws.com/my-bucket/cloudformation.template - create_timeout: 5 - -# Configure rollback behaviour on the unsuccessful creation of a stack allowing -# CloudFormation to clean up, or do nothing in the event of an unsuccessful -# deployment -# In this case, if on_create_failure is set to "DELETE", it will clean up the stack if -# it fails to create -- name: create stack which will delete on creation failure - cloudformation: - stack_name: my_stack - state: present - template_url: https://s3.amazonaws.com/my-bucket/cloudformation.template - on_create_failure: DELETE -''' - -RETURN = ''' -events: - type: list - description: Most recent events in CloudFormation's event log. This may be from a previous run in some cases. - returned: always - sample: ["StackEvent AWS::CloudFormation::Stack stackname UPDATE_COMPLETE", "StackEvent AWS::CloudFormation::Stack stackname UPDATE_COMPLETE_CLEANUP_IN_PROGRESS"] -log: - description: Debugging logs. Useful when modifying or finding an error. - returned: always - type: list - sample: ["updating stack"] -change_set_id: - description: The ID of the stack change set if one was created - returned: I(state=present) and I(create_changeset=true) - type: str - sample: "arn:aws:cloudformation:us-east-1:012345678901:changeSet/Ansible-StackName-f4496805bd1b2be824d1e315c6884247ede41eb0" -stack_resources: - description: AWS stack resources and their status. List of dictionaries, one dict per resource. - returned: state == present - type: list - sample: [ - { - "last_updated_time": "2016-10-11T19:40:14.979000+00:00", - "logical_resource_id": "CFTestSg", - "physical_resource_id": "cloudformation2-CFTestSg-16UQ4CYQ57O9F", - "resource_type": "AWS::EC2::SecurityGroup", - "status": "UPDATE_COMPLETE", - "status_reason": null - } - ] -stack_outputs: - type: dict - description: A key:value dictionary of all the stack outputs currently defined. If there are no stack outputs, it is an empty dictionary. - returned: state == present - sample: {"MySg": "AnsibleModuleTestYAML-CFTestSg-C8UVS567B6NS"} -''' # NOQA - -import json -import time -import uuid -import traceback -from hashlib import sha1 - -try: - import boto3 - import botocore - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - -from ansible.module_utils.ec2 import ansible_dict_to_boto3_tag_list, AWSRetry, boto3_conn, boto_exception, ec2_argument_spec, get_aws_connection_info -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_bytes, to_native - - -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.''' - ret = {'events': [], 'log': []} - - try: - pg = cfn.get_paginator( - 'describe_stack_events' - ).paginate( - StackName=stack_name, - PaginationConfig={'MaxItems': events_limit} - ) - if token_filter is not None: - events = list(pg.search( - "StackEvents[?ClientRequestToken == '{0}']".format(token_filter) - )) - else: - events = list(pg.search("StackEvents[*]")) - 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. - ret['log'].append('Stack does not exist.') - return ret - ret['log'].append('Unknown error: ' + str(error_msg)) - return ret - - for e in events: - eventline = 'StackEvent {ResourceType} {LogicalResourceId} {ResourceStatus}'.format(**e) - ret['events'].append(eventline) - - if e['ResourceStatus'].endswith('FAILED'): - failline = '{ResourceType} {LogicalResourceId} {ResourceStatus}: {ResourceStatusReason}'.format(**e) - ret['log'].append(failline) - - return ret - - -def create_stack(module, stack_params, cfn, events_limit): - if 'TemplateBody' not in stack_params and 'TemplateURL' not in stack_params: - module.fail_json(msg="Either 'template', 'template_body' or 'template_url' is required when the stack does not exist.") - - # 'DisableRollback', 'TimeoutInMinutes', 'EnableTerminationProtection' and - # 'OnFailure' only apply on creation, not update. - if module.params.get('on_create_failure') is not None: - stack_params['OnFailure'] = module.params['on_create_failure'] - else: - stack_params['DisableRollback'] = module.params['disable_rollback'] - - if module.params.get('create_timeout') is not None: - stack_params['TimeoutInMinutes'] = module.params['create_timeout'] - if module.params.get('termination_protection') is not None: - if boto_supports_termination_protection(cfn): - stack_params['EnableTerminationProtection'] = bool(module.params.get('termination_protection')) - else: - module.fail_json(msg="termination_protection parameter requires botocore >= 1.7.18") - - try: - response = cfn.create_stack(**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)) - except Exception as err: - error_msg = boto_exception(err) - module.fail_json(msg="Failed to create stack {0}: {1}.".format(stack_params.get('StackName'), error_msg), exception=traceback.format_exc()) - if not result: - module.fail_json(msg="empty result") - return result - - -def list_changesets(cfn, stack_name): - res = cfn.list_change_sets(StackName=stack_name) - return [cs['ChangeSetName'] for cs in res['Summaries']] - - -def create_changeset(module, stack_params, cfn, events_limit): - if 'TemplateBody' not in stack_params and 'TemplateURL' not in stack_params: - module.fail_json(msg="Either 'template' or 'template_url' is required.") - if module.params['changeset_name'] is not None: - stack_params['ChangeSetName'] = module.params['changeset_name'] - - # changesets don't accept ClientRequestToken parameters - stack_params.pop('ClientRequestToken', None) - - try: - changeset_name = build_changeset_name(stack_params) - stack_params['ChangeSetName'] = changeset_name - - # Determine if this changeset already exists - pending_changesets = list_changesets(cfn, stack_params['StackName']) - if changeset_name in pending_changesets: - 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) - # 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']) - except botocore.exceptions.BotoCoreError as err: - error_msg = boto_exception(err) - module.fail_json(msg=error_msg) - 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']) - 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 - # skip any further processing of result and just return it directly - return result - else: - 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['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 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(msg="Failed to create change set: {0}".format(error_msg), exception=traceback.format_exc()) - - if not result: - module.fail_json(msg="empty result") - return result - - -def update_stack(module, stack_params, cfn, events_limit): - if 'TemplateBody' not in stack_params and 'TemplateURL' not in stack_params: - stack_params['UsePreviousTemplate'] = True - - # if the state is present and the stack already exists, we try to update it. - # 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)) - 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(msg="Failed to update stack {0}: {1}".format(stack_params.get('StackName'), error_msg), exception=traceback.format_exc()) - if not result: - module.fail_json(msg="empty result") - return result - - -def update_termination_protection(module, cfn, stack_name, desired_termination_protection_state): - '''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) - if stack: - if stack['EnableTerminationProtection'] is not desired_termination_protection_state: - try: - cfn.update_termination_protection( - EnableTerminationProtection=desired_termination_protection_state, - StackName=stack_name) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=boto_exception(e), exception=traceback.format_exc()) - - -def boto_supports_termination_protection(cfn): - '''termination protection was added in botocore 1.7.18''' - return hasattr(cfn, "update_termination_protection") - - -def stack_operation(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) - existed.append('yes') - except Exception: - # If the stack previously existed, and now can't be found then it's - # been deleted successfully. - if 'yes' in existed or operation == 'DELETE': # stacks may delete fast, look in a few ways. - ret = get_stack_events(cfn, stack_name, events_limit, op_token) - ret.update({'changed': True, 'output': 'Stack Deleted'}) - return ret - else: - return {'changed': True, 'failed': True, 'output': 'Stack Not Found', 'exception': traceback.format_exc()} - ret = get_stack_events(cfn, stack_name, events_limit, op_token) - if not stack: - if 'yes' in existed or operation == 'DELETE': # stacks may delete fast, look in a few ways. - ret = get_stack_events(cfn, stack_name, events_limit, op_token) - ret.update({'changed': True, 'output': 'Stack Deleted'}) - return ret - else: - ret.update({'changed': False, 'failed': True, 'output': 'Stack not found.'}) - return ret - # it covers ROLLBACK_COMPLETE and UPDATE_ROLLBACK_COMPLETE - # Possible states: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html#w1ab2c15c17c21c13 - elif stack['StackStatus'].endswith('ROLLBACK_COMPLETE') and operation != 'CREATE_CHANGESET': - ret.update({'changed': True, 'failed': True, 'output': 'Problem with %s. Rollback complete' % operation}) - return ret - elif stack['StackStatus'] == 'DELETE_COMPLETE' and operation == 'CREATE': - ret.update({'changed': True, 'failed': True, 'output': 'Stack create failed. Delete complete.'}) - return ret - # note the ordering of ROLLBACK_COMPLETE, DELETE_COMPLETE, and COMPLETE, because otherwise COMPLETE will match all cases. - elif stack['StackStatus'].endswith('_COMPLETE'): - ret.update({'changed': True, 'output': 'Stack %s complete' % operation}) - return ret - elif stack['StackStatus'].endswith('_ROLLBACK_FAILED'): - ret.update({'changed': True, 'failed': True, 'output': 'Stack %s rollback failed' % operation}) - return ret - # note the ordering of ROLLBACK_FAILED and FAILED, because otherwise FAILED will match both cases. - elif stack['StackStatus'].endswith('_FAILED'): - ret.update({'changed': True, 'failed': True, 'output': 'Stack %s failed' % operation}) - return ret - else: - # this can loop forever :/ - time.sleep(5) - return {'failed': True, 'output': 'Failed for unknown reasons.'} - - -def build_changeset_name(stack_params): - if 'ChangeSetName' in stack_params: - return stack_params['ChangeSetName'] - - json_params = json.dumps(stack_params, sort_keys=True) - - return 'Ansible-{0}-{1}'.format( - stack_params['StackName'], - sha1(to_bytes(json_params, errors='surrogate_or_strict')).hexdigest() - ) - - -def check_mode_changeset(module, stack_params, cfn): - """Create a change set, describe it and delete it before returning check mode outputs.""" - stack_params['ChangeSetName'] = build_changeset_name(stack_params) - # changesets don't accept ClientRequestToken parameters - stack_params.pop('ClientRequestToken', None) - - try: - change_set = cfn.create_change_set(**stack_params) - for i in range(60): # total time 5 min - description = cfn.describe_change_set(ChangeSetName=change_set['Id']) - if description['Status'] in ('CREATE_COMPLETE', 'FAILED'): - break - time.sleep(5) - else: - # 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']) - - reason = description.get('StatusReason') - - if description['Status'] == 'FAILED' and "didn't contain changes" in description['StatusReason']: - return {'changed': False, 'msg': reason, 'meta': description['StatusReason']} - return {'changed': True, 'msg': reason, 'meta': description['Changes']} - - except (botocore.exceptions.ValidationError, botocore.exceptions.ClientError) as err: - error_msg = boto_exception(err) - module.fail_json(msg=error_msg, exception=traceback.format_exc()) - - -def get_stack_facts(cfn, stack_name): - try: - stack_response = cfn.describe_stacks(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 - - if stack_response and stack_response.get('Stacks', None): - stacks = stack_response['Stacks'] - if len(stacks): - stack_info = stacks[0] - - return stack_info - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - stack_name=dict(required=True), - template_parameters=dict(required=False, type='dict', default={}), - state=dict(default='present', choices=['present', 'absent']), - template=dict(default=None, required=False, type='path'), - notification_arns=dict(default=None, required=False), - stack_policy=dict(default=None, required=False), - disable_rollback=dict(default=False, type='bool'), - on_create_failure=dict(default=None, required=False, choices=['DO_NOTHING', 'ROLLBACK', 'DELETE']), - create_timeout=dict(default=None, type='int'), - template_url=dict(default=None, required=False), - template_body=dict(default=None, required=False), - template_format=dict(removed_in_version='2.14'), - create_changeset=dict(default=False, type='bool'), - changeset_name=dict(default=None, required=False), - role_arn=dict(default=None, required=False), - tags=dict(default=None, type='dict'), - termination_protection=dict(default=None, type='bool'), - events_limit=dict(default=200, type='int'), - backoff_retries=dict(type='int', default=10, required=False), - backoff_delay=dict(type='int', default=3, required=False), - backoff_max_delay=dict(type='int', default=30, required=False), - capabilities=dict(type='list', default=['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM']) - ) - ) - - module = AnsibleModule( - argument_spec=argument_spec, - mutually_exclusive=[['template_url', 'template', 'template_body'], - ['disable_rollback', 'on_create_failure']], - supports_check_mode=True - ) - if not HAS_BOTO3: - module.fail_json(msg='boto3 and botocore are required for this module') - - invalid_capabilities = [] - user_capabilities = module.params.get('capabilities') - for user_cap in user_capabilities: - if user_cap not in ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND']: - invalid_capabilities.append(user_cap) - - if invalid_capabilities: - module.fail_json(msg="Specified capabilities are invalid : %r," - " please check documentation for valid capabilities" % invalid_capabilities) - - # collect the parameters that are passed to boto3. Keeps us from having so many scalars floating around. - stack_params = { - 'Capabilities': user_capabilities, - 'ClientRequestToken': to_native(uuid.uuid4()), - } - state = module.params['state'] - stack_params['StackName'] = module.params['stack_name'] - - if module.params['template'] is not None: - with open(module.params['template'], 'r') as template_fh: - stack_params['TemplateBody'] = template_fh.read() - elif module.params['template_body'] is not None: - stack_params['TemplateBody'] = module.params['template_body'] - elif module.params['template_url'] is not None: - stack_params['TemplateURL'] = module.params['template_url'] - - if module.params.get('notification_arns'): - stack_params['NotificationARNs'] = module.params['notification_arns'].split(',') - else: - stack_params['NotificationARNs'] = [] - - # can't check the policy when verifying. - if module.params['stack_policy'] is not None and not module.check_mode and not module.params['create_changeset']: - with open(module.params['stack_policy'], 'r') as stack_policy_fh: - stack_params['StackPolicyBody'] = stack_policy_fh.read() - - template_parameters = module.params['template_parameters'] - - stack_params['Parameters'] = [] - for k, v in template_parameters.items(): - if isinstance(v, dict): - # set parameter based on a dict to allow additional CFN Parameter Attributes - param = dict(ParameterKey=k) - - if 'value' in v: - param['ParameterValue'] = str(v['value']) - - if 'use_previous_value' in v and bool(v['use_previous_value']): - param['UsePreviousValue'] = True - param.pop('ParameterValue', None) - - stack_params['Parameters'].append(param) - else: - # allow default k/v configuration to set a template parameter - stack_params['Parameters'].append({'ParameterKey': k, 'ParameterValue': str(v)}) - - if isinstance(module.params.get('tags'), dict): - stack_params['Tags'] = ansible_dict_to_boto3_tag_list(module.params['tags']) - - if module.params.get('role_arn'): - stack_params['RoleARN'] = module.params['role_arn'] - - result = {} - - try: - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - cfn = boto3_conn(module, conn_type='client', resource='cloudformation', region=region, endpoint=ec2_url, **aws_connect_kwargs) - except botocore.exceptions.NoCredentialsError as e: - module.fail_json(msg=boto_exception(e)) - - # Wrap the cloudformation client methods that this module uses with - # automatic backoff / retry for throttling error codes - backoff_wrapper = 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']) - - if module.check_mode: - if state == 'absent' and stack_info: - module.exit_json(changed=True, msg='Stack would be deleted', meta=[]) - elif state == 'absent' and not stack_info: - module.exit_json(changed=False, msg='Stack doesn\'t exist', meta=[]) - elif state == 'present' and not stack_info: - module.exit_json(changed=True, msg='New stack would be created', meta=[]) - else: - module.exit_json(**check_mode_changeset(module, stack_params, cfn)) - - if state == 'present': - if not stack_info: - result = create_stack(module, stack_params, cfn, module.params.get('events_limit')) - elif module.params.get('create_changeset'): - result = create_changeset(module, stack_params, cfn, module.params.get('events_limit')) - else: - if module.params.get('termination_protection') is not None: - update_termination_protection(module, cfn, stack_params['StackName'], - bool(module.params.get('termination_protection'))) - result = update_stack(module, stack_params, cfn, module.params.get('events_limit')) - - # format the stack output - - stack = get_stack_facts(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 - result['stack_outputs'] = {} - for output in stack.get('Outputs', []): - result['stack_outputs'][output['OutputKey']] = output['OutputValue'] - stack_resources = [] - reslist = cfn.list_stack_resources(StackName=stack_params['StackName']) - for res in reslist.get('StackResourceSummaries', []): - stack_resources.append({ - "logical_resource_id": res['LogicalResourceId'], - "physical_resource_id": res.get('PhysicalResourceId', ''), - "resource_type": res['ResourceType'], - "last_updated_time": res['LastUpdatedTimestamp'], - "status": res['ResourceStatus'], - "status_reason": res.get('ResourceStatusReason') # can be blank, apparently - }) - result['stack_resources'] = stack_resources - - elif state == 'absent': - # absent state is different because of the way delete_stack works. - # problem is it it doesn't give an error if stack isn't found - # so must describe the stack first - - try: - stack = get_stack_facts(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']) - 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'), - stack_params.get('ClientRequestToken', None)) - except Exception as err: - module.fail_json(msg=boto_exception(err), exception=traceback.format_exc()) - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/cloudformation_info.py b/lib/ansible/modules/cloud/amazon/cloudformation_info.py deleted file mode 100644 index f62b80235d..0000000000 --- a/lib/ansible/modules/cloud/amazon/cloudformation_info.py +++ /dev/null @@ -1,354 +0,0 @@ -#!/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 - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: cloudformation_info -short_description: Obtain information about an AWS CloudFormation stack -description: - - Gets information about an AWS CloudFormation stack. - - This module was called C(cloudformation_facts) before Ansible 2.9, returning C(ansible_facts). - Note that the M(cloudformation_info) module no longer returns C(ansible_facts)! -requirements: - - boto3 >= 1.0.0 - - python >= 2.6 -version_added: "2.2" -author: - - Justin Menga (@jmenga) - - Kevin Coming (@waffie1) -options: - stack_name: - description: - - The name or id of the CloudFormation stack. Gathers information on all stacks by default. - type: str - all_facts: - description: - - Get all stack information for the stack. - type: bool - default: false - stack_events: - description: - - Get stack events for the stack. - type: bool - default: false - stack_template: - description: - - Get stack template body for the stack. - type: bool - default: false - stack_resources: - description: - - Get stack resources for the stack. - type: bool - default: false - stack_policy: - description: - - Get stack policy for the stack. - type: bool - default: false - stack_change_sets: - description: - - Get stack change sets for the stack - type: bool - default: false - version_added: '2.10' -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Get summary information about a stack -- cloudformation_info: - stack_name: my-cloudformation-stack - register: output - -- debug: - msg: "{{ output['cloudformation']['my-cloudformation-stack'] }}" - -# When the module is called as cloudformation_facts, return values are published -# in ansible_facts['cloudformation'][<stack_name>] and can be used as follows. -# Note that this is deprecated and will stop working in Ansible 2.13. - -- cloudformation_facts: - stack_name: my-cloudformation-stack - -- debug: - msg: "{{ ansible_facts['cloudformation']['my-cloudformation-stack'] }}" - -# Get stack outputs, when you have the stack name available as a fact -- set_fact: - stack_name: my-awesome-stack - -- cloudformation_info: - stack_name: "{{ stack_name }}" - register: my_stack - -- debug: - msg: "{{ my_stack.cloudformation[stack_name].stack_outputs }}" - -# Get all stack information about a stack -- cloudformation_info: - stack_name: my-cloudformation-stack - all_facts: true - -# Get stack resource and stack policy information about a stack -- cloudformation_info: - stack_name: my-cloudformation-stack - stack_resources: true - stack_policy: true - -# Fail if the stack doesn't exist -- name: try to get facts about a stack but fail if it doesn't exist - cloudformation_info: - stack_name: nonexistent-stack - all_facts: yes - failed_when: cloudformation['nonexistent-stack'] is undefined -''' - -RETURN = ''' -stack_description: - description: Summary facts about the stack - returned: if the stack exists - type: dict -stack_outputs: - description: Dictionary of stack outputs keyed by the value of each output 'OutputKey' parameter and corresponding value of each - output 'OutputValue' parameter - returned: if the stack exists - type: dict - sample: - ApplicationDatabaseName: dazvlpr01xj55a.ap-southeast-2.rds.amazonaws.com -stack_parameters: - description: Dictionary of stack parameters keyed by the value of each parameter 'ParameterKey' parameter and corresponding value of - each parameter 'ParameterValue' parameter - returned: if the stack exists - type: dict - sample: - DatabaseEngine: mysql - DatabasePassword: "***" -stack_events: - description: All stack events for the stack - returned: only if all_facts or stack_events is true and the stack exists - type: list -stack_policy: - description: Describes the stack policy for the stack - returned: only if all_facts or stack_policy is true and the stack exists - type: dict -stack_template: - description: Describes the stack template for the stack - returned: only if all_facts or stack_template is true and the stack exists - type: dict -stack_resource_list: - description: Describes stack resources for the stack - returned: only if all_facts or stack_resourses is true and the stack exists - type: list -stack_resources: - description: Dictionary of stack resources keyed by the value of each resource 'LogicalResourceId' parameter and corresponding value of each - resource 'PhysicalResourceId' parameter - returned: only if all_facts or stack_resourses is true and the stack exists - type: dict - sample: - AutoScalingGroup: "dev-someapp-AutoscalingGroup-1SKEXXBCAN0S7" - AutoScalingSecurityGroup: "sg-abcd1234" - ApplicationDatabase: "dazvlpr01xj55a" -stack_change_sets: - description: A list of stack change sets. Each item in the list represents the details of a specific changeset - - returned: only if all_facts or stack_change_sets is true and the stack exists - type: list -''' - -import json -import traceback - -from functools import partial -from ansible.module_utils._text import to_native -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import (camel_dict_to_snake_dict, AWSRetry, boto3_tag_list_to_ansible_dict) - -try: - import botocore -except ImportError: - pass # handled by AnsibleAWSModule - - -class CloudFormationServiceManager: - """Handles CloudFormation Services""" - - def __init__(self, module): - self.module = module - self.client = module.client('cloudformation') - - @AWSRetry.exponential_backoff(retries=5, delay=5) - def describe_stacks_with_backoff(self, **kwargs): - paginator = self.client.get_paginator('describe_stacks') - return paginator.paginate(**kwargs).build_full_result()['Stacks'] - - def describe_stacks(self, stack_name=None): - try: - kwargs = {'StackName': stack_name} if stack_name else {} - response = self.describe_stacks_with_backoff(**kwargs) - if response is not None: - return response - self.module.fail_json(msg="Error describing stack(s) - an empty response was returned") - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - if 'does not exist' in e.response['Error']['Message']: - # missing stack, don't bail. - return {} - self.module.fail_json_aws(e, msg="Error describing stack " + stack_name) - - @AWSRetry.exponential_backoff(retries=5, delay=5) - def list_stack_resources_with_backoff(self, stack_name): - paginator = self.client.get_paginator('list_stack_resources') - return paginator.paginate(StackName=stack_name).build_full_result()['StackResourceSummaries'] - - def list_stack_resources(self, stack_name): - try: - return self.list_stack_resources_with_backoff(stack_name) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - self.module.fail_json_aws(e, msg="Error listing stack resources for stack " + stack_name) - - @AWSRetry.exponential_backoff(retries=5, delay=5) - def describe_stack_events_with_backoff(self, stack_name): - paginator = self.client.get_paginator('describe_stack_events') - return paginator.paginate(StackName=stack_name).build_full_result()['StackEvents'] - - def describe_stack_events(self, stack_name): - try: - return self.describe_stack_events_with_backoff(stack_name) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - self.module.fail_json_aws(e, msg="Error listing stack events for stack " + stack_name) - - @AWSRetry.exponential_backoff(retries=5, delay=5) - def list_stack_change_sets_with_backoff(self, stack_name): - paginator = self.client.get_paginator('list_change_sets') - return paginator.paginate(StackName=stack_name).build_full_result()['Summaries'] - - @AWSRetry.exponential_backoff(retries=5, delay=5) - def describe_stack_change_set_with_backoff(self, **kwargs): - paginator = self.client.get_paginator('describe_change_set') - return paginator.paginate(**kwargs).build_full_result() - - def describe_stack_change_sets(self, stack_name): - changes = [] - try: - change_sets = self.list_stack_change_sets_with_backoff(stack_name) - for item in change_sets: - changes.append(self.describe_stack_change_set_with_backoff( - StackName=stack_name, - ChangeSetName=item['ChangeSetName'])) - return changes - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - self.module.fail_json_aws(e, msg="Error describing stack change sets for stack " + stack_name) - - @AWSRetry.exponential_backoff(retries=5, delay=5) - def get_stack_policy_with_backoff(self, stack_name): - return self.client.get_stack_policy(StackName=stack_name) - - def get_stack_policy(self, stack_name): - try: - response = self.get_stack_policy_with_backoff(stack_name) - stack_policy = response.get('StackPolicyBody') - if stack_policy: - return json.loads(stack_policy) - return dict() - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - self.module.fail_json_aws(e, msg="Error getting stack policy for stack " + stack_name) - - @AWSRetry.exponential_backoff(retries=5, delay=5) - def get_template_with_backoff(self, stack_name): - return self.client.get_template(StackName=stack_name) - - def get_template(self, stack_name): - try: - response = self.get_template_with_backoff(stack_name) - return response.get('TemplateBody') - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - self.module.fail_json_aws(e, msg="Error getting stack template for stack " + stack_name) - - -def to_dict(items, key, value): - ''' Transforms a list of items to a Key/Value dictionary ''' - if items: - return dict(zip([i.get(key) for i in items], [i.get(value) for i in items])) - else: - return dict() - - -def main(): - argument_spec = dict( - stack_name=dict(), - all_facts=dict(required=False, default=False, type='bool'), - stack_policy=dict(required=False, default=False, type='bool'), - stack_events=dict(required=False, default=False, type='bool'), - stack_resources=dict(required=False, default=False, type='bool'), - stack_template=dict(required=False, default=False, type='bool'), - stack_change_sets=dict(required=False, default=False, type='bool'), - ) - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) - - is_old_facts = module._name == 'cloudformation_facts' - if is_old_facts: - module.deprecate("The 'cloudformation_facts' module has been renamed to 'cloudformation_info', " - "and the renamed one no longer returns ansible_facts", version='2.13') - - service_mgr = CloudFormationServiceManager(module) - - if is_old_facts: - result = {'ansible_facts': {'cloudformation': {}}} - else: - result = {'cloudformation': {}} - - for stack_description in service_mgr.describe_stacks(module.params.get('stack_name')): - facts = {'stack_description': stack_description} - stack_name = stack_description.get('StackName') - - # Create stack output and stack parameter dictionaries - if facts['stack_description']: - facts['stack_outputs'] = to_dict(facts['stack_description'].get('Outputs'), 'OutputKey', 'OutputValue') - facts['stack_parameters'] = to_dict(facts['stack_description'].get('Parameters'), - 'ParameterKey', 'ParameterValue') - facts['stack_tags'] = boto3_tag_list_to_ansible_dict(facts['stack_description'].get('Tags')) - - # Create optional stack outputs - all_facts = module.params.get('all_facts') - if all_facts or module.params.get('stack_resources'): - facts['stack_resource_list'] = service_mgr.list_stack_resources(stack_name) - facts['stack_resources'] = to_dict(facts.get('stack_resource_list'), - 'LogicalResourceId', 'PhysicalResourceId') - if all_facts or module.params.get('stack_template'): - facts['stack_template'] = service_mgr.get_template(stack_name) - if all_facts or module.params.get('stack_policy'): - facts['stack_policy'] = service_mgr.get_stack_policy(stack_name) - if all_facts or module.params.get('stack_events'): - facts['stack_events'] = service_mgr.describe_stack_events(stack_name) - if all_facts or module.params.get('stack_change_sets'): - facts['stack_change_sets'] = service_mgr.describe_stack_change_sets(stack_name) - - if is_old_facts: - result['ansible_facts']['cloudformation'][stack_name] = facts - else: - result['cloudformation'][stack_name] = camel_dict_to_snake_dict(facts, ignore_list=('stack_outputs', - 'stack_parameters', - 'stack_policy', - 'stack_resources', - 'stack_tags', - 'stack_template')) - - module.exit_json(changed=False, **result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2.py b/lib/ansible/modules/cloud/amazon/ec2.py deleted file mode 100644 index 91503bbf8e..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2.py +++ /dev/null @@ -1,1766 +0,0 @@ -#!/usr/bin/python -# This file is part of Ansible -# 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 - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'core'} - - -DOCUMENTATION = ''' ---- -module: ec2 -short_description: create, terminate, start or stop an instance in ec2 -description: - - Creates or terminates ec2 instances. - - > - Note: This module uses the older boto Python module to interact with the EC2 API. - M(ec2) will still receive bug fixes, but no new features. - Consider using the M(ec2_instance) module instead. - If M(ec2_instance) does not support a feature you need that is available in M(ec2), please - file a feature request. -version_added: "0.9" -options: - key_name: - description: - - Key pair to use on the instance. - - The SSH key must already exist in AWS in order to use this argument. - - Keys can be created / deleted using the M(ec2_key) module. - aliases: ['keypair'] - type: str - id: - version_added: "1.1" - description: - - Identifier for this instance or set of instances, so that the module will be idempotent with respect to EC2 instances. - - This identifier is valid for at least 24 hours after the termination of the instance, and should not be reused for another call later on. - - For details, see the description of client token at U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Run_Instance_Idempotency.html). - type: str - group: - description: - - Security group (or list of groups) to use with the instance. - aliases: [ 'groups' ] - type: list - elements: str - group_id: - version_added: "1.1" - description: - - Security group id (or list of ids) to use with the instance. - type: list - elements: str - zone: - version_added: "1.2" - description: - - AWS availability zone in which to launch the instance. - aliases: [ 'aws_zone', 'ec2_zone' ] - type: str - instance_type: - description: - - Instance type to use for the instance, see U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-types.html). - - Required when creating a new instance. - type: str - aliases: ['type'] - tenancy: - version_added: "1.9" - description: - - An instance with a tenancy of C(dedicated) runs on single-tenant hardware and can only be launched into a VPC. - - Note that to use dedicated tenancy you MUST specify a I(vpc_subnet_id) as well. - - Dedicated tenancy is not available for EC2 "micro" instances. - default: default - choices: [ "default", "dedicated" ] - type: str - spot_price: - version_added: "1.5" - description: - - Maximum spot price to bid. If not set, a regular on-demand instance is requested. - - A spot request is made with this maximum bid. When it is filled, the instance is started. - type: str - spot_type: - version_added: "2.0" - description: - - The type of spot request. - - After being interrupted a C(persistent) spot instance will be started once there is capacity to fill the request again. - default: "one-time" - choices: [ "one-time", "persistent" ] - type: str - image: - description: - - I(ami) ID to use for the instance. - - Required when I(state=present). - type: str - kernel: - description: - - Kernel eki to use for the instance. - type: str - ramdisk: - description: - - Ramdisk eri to use for the instance. - type: str - wait: - description: - - Wait for the instance to reach its desired state before returning. - - Does not wait for SSH, see the 'wait_for_connection' example for details. - type: bool - default: false - wait_timeout: - description: - - How long before wait gives up, in seconds. - default: 300 - type: int - spot_wait_timeout: - version_added: "1.5" - description: - - How long to wait for the spot instance request to be fulfilled. Affects 'Request valid until' for setting spot request lifespan. - default: 600 - type: int - count: - description: - - Number of instances to launch. - default: 1 - type: int - monitoring: - version_added: "1.1" - description: - - Enable detailed monitoring (CloudWatch) for instance. - type: bool - default: false - user_data: - version_added: "0.9" - description: - - Opaque blob of data which is made available to the EC2 instance. - type: str - instance_tags: - version_added: "1.0" - description: - - A hash/dictionary of tags to add to the new instance or for starting/stopping instance by tag; '{"key":"value"}' and '{"key":"value","key":"value"}'. - type: dict - placement_group: - version_added: "1.3" - description: - - Placement group for the instance when using EC2 Clustered Compute. - type: str - vpc_subnet_id: - version_added: "1.1" - description: - - the subnet ID in which to launch the instance (VPC). - type: str - assign_public_ip: - version_added: "1.5" - description: - - When provisioning within vpc, assign a public IP address. Boto library must be 2.13.0+. - type: bool - private_ip: - version_added: "1.2" - description: - - The private ip address to assign the instance (from the vpc subnet). - type: str - instance_profile_name: - version_added: "1.3" - description: - - Name of the IAM instance profile (i.e. what the EC2 console refers to as an "IAM Role") to use. Boto library must be 2.5.0+. - type: str - instance_ids: - version_added: "1.3" - description: - - "list of instance ids, currently used for states: absent, running, stopped" - aliases: ['instance_id'] - type: list - elements: str - source_dest_check: - version_added: "1.6" - description: - - Enable or Disable the Source/Destination checks (for NAT instances and Virtual Routers). - When initially creating an instance the EC2 API defaults this to C(True). - type: bool - termination_protection: - version_added: "2.0" - description: - - Enable or Disable the Termination Protection. - type: bool - default: false - instance_initiated_shutdown_behavior: - version_added: "2.2" - description: - - Set whether AWS will Stop or Terminate an instance on shutdown. This parameter is ignored when using instance-store. - images (which require termination on shutdown). - default: 'stop' - choices: [ "stop", "terminate" ] - type: str - state: - version_added: "1.3" - description: - - Create, terminate, start, stop or restart instances. The state 'restarted' was added in Ansible 2.2. - - When I(state=absent), I(instance_ids) is required. - - When I(state=running), I(state=stopped) or I(state=restarted) then either I(instance_ids) or I(instance_tags) is required. - default: 'present' - choices: ['absent', 'present', 'restarted', 'running', 'stopped'] - type: str - volumes: - version_added: "1.5" - description: - - A list of hash/dictionaries of volumes to add to the new instance. - type: list - elements: dict - suboptions: - device_name: - type: str - required: true - description: - - A name for the device (For example C(/dev/sda)). - delete_on_termination: - type: bool - default: false - description: - - Whether the volume should be automatically deleted when the instance is terminated. - ephemeral: - type: str - description: - - Whether the volume should be ephemeral. - - Data on ephemeral volumes is lost when the instance is stopped. - - Mutually exclusive with the I(snapshot) parameter. - encrypted: - type: bool - default: false - description: - - Whether the volume should be encrypted using the 'aws/ebs' KMS CMK. - snapshot: - type: str - description: - - The ID of an EBS snapshot to copy when creating the volume. - - Mutually exclusive with the I(ephemeral) parameter. - volume_type: - type: str - description: - - The type of volume to create. - - See U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html) for more information on the available volume types. - volume_size: - type: int - description: - - The size of the volume (in GiB). - iops: - type: int - description: - - The number of IOPS per second to provision for the volume. - - Required when I(volume_type=io1). - ebs_optimized: - version_added: "1.6" - description: - - Whether instance is using optimized EBS volumes, see U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSOptimized.html). - default: false - type: bool - exact_count: - version_added: "1.5" - description: - - An integer value which indicates how many instances that match the 'count_tag' parameter should be running. - Instances are either created or terminated based on this value. - type: int - count_tag: - version_added: "1.5" - description: - - Used with I(exact_count) to determine how many nodes based on a specific tag criteria should be running. - This can be expressed in multiple ways and is shown in the EXAMPLES section. For instance, one can request 25 servers - that are tagged with "class=webserver". The specified tag must already exist or be passed in as the I(instance_tags) option. - type: raw - network_interfaces: - version_added: "2.0" - description: - - A list of existing network interfaces to attach to the instance at launch. When specifying existing network interfaces, - none of the I(assign_public_ip), I(private_ip), I(vpc_subnet_id), I(group), or I(group_id) parameters may be used. (Those parameters are - for creating a new network interface at launch.) - aliases: ['network_interface'] - type: list - elements: str - spot_launch_group: - version_added: "2.1" - description: - - Launch group for spot requests, see U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/how-spot-instances-work.html#spot-launch-group). - type: str -author: - - "Tim Gerla (@tgerla)" - - "Lester Wade (@lwade)" - - "Seth Vidal (@skvidal)" -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Basic provisioning example -- ec2: - key_name: mykey - instance_type: t2.micro - image: ami-123456 - wait: yes - group: webserver - count: 3 - vpc_subnet_id: subnet-29e63245 - assign_public_ip: yes - -# Advanced example with tagging and CloudWatch -- ec2: - key_name: mykey - group: databases - instance_type: t2.micro - image: ami-123456 - wait: yes - wait_timeout: 500 - count: 5 - instance_tags: - db: postgres - monitoring: yes - vpc_subnet_id: subnet-29e63245 - assign_public_ip: yes - -# Single instance with additional IOPS volume from snapshot and volume delete on termination -- ec2: - key_name: mykey - group: webserver - instance_type: c3.medium - image: ami-123456 - wait: yes - wait_timeout: 500 - volumes: - - device_name: /dev/sdb - snapshot: snap-abcdef12 - volume_type: io1 - iops: 1000 - volume_size: 100 - delete_on_termination: true - monitoring: yes - vpc_subnet_id: subnet-29e63245 - assign_public_ip: yes - -# Single instance with ssd gp2 root volume -- ec2: - key_name: mykey - group: webserver - instance_type: c3.medium - image: ami-123456 - wait: yes - wait_timeout: 500 - volumes: - - device_name: /dev/xvda - volume_type: gp2 - volume_size: 8 - vpc_subnet_id: subnet-29e63245 - assign_public_ip: yes - count_tag: - Name: dbserver - exact_count: 1 - -# Multiple groups example -- ec2: - key_name: mykey - group: ['databases', 'internal-services', 'sshable', 'and-so-forth'] - instance_type: m1.large - image: ami-6e649707 - wait: yes - wait_timeout: 500 - count: 5 - instance_tags: - db: postgres - monitoring: yes - vpc_subnet_id: subnet-29e63245 - assign_public_ip: yes - -# Multiple instances with additional volume from snapshot -- ec2: - key_name: mykey - group: webserver - instance_type: m1.large - image: ami-6e649707 - wait: yes - wait_timeout: 500 - count: 5 - volumes: - - device_name: /dev/sdb - snapshot: snap-abcdef12 - volume_size: 10 - monitoring: yes - vpc_subnet_id: subnet-29e63245 - assign_public_ip: yes - -# Dedicated tenancy example -- local_action: - module: ec2 - assign_public_ip: yes - group_id: sg-1dc53f72 - key_name: mykey - image: ami-6e649707 - instance_type: m1.small - tenancy: dedicated - vpc_subnet_id: subnet-29e63245 - wait: yes - -# Spot instance example -- ec2: - spot_price: 0.24 - spot_wait_timeout: 600 - keypair: mykey - group_id: sg-1dc53f72 - instance_type: m1.small - image: ami-6e649707 - wait: yes - vpc_subnet_id: subnet-29e63245 - assign_public_ip: yes - spot_launch_group: report_generators - instance_initiated_shutdown_behavior: terminate - -# Examples using pre-existing network interfaces -- ec2: - key_name: mykey - instance_type: t2.small - image: ami-f005ba11 - network_interface: eni-deadbeef - -- ec2: - key_name: mykey - instance_type: t2.small - image: ami-f005ba11 - network_interfaces: ['eni-deadbeef', 'eni-5ca1ab1e'] - -# Launch instances, runs some tasks -# and then terminate them - -- name: Create a sandbox instance - hosts: localhost - gather_facts: False - vars: - keypair: my_keypair - instance_type: m1.small - security_group: my_securitygroup - image: my_ami_id - region: us-east-1 - tasks: - - name: Launch instance - ec2: - key_name: "{{ keypair }}" - group: "{{ security_group }}" - instance_type: "{{ instance_type }}" - image: "{{ image }}" - wait: true - region: "{{ region }}" - vpc_subnet_id: subnet-29e63245 - assign_public_ip: yes - register: ec2 - - - name: Add new instance to host group - add_host: - hostname: "{{ item.public_ip }}" - groupname: launched - loop: "{{ ec2.instances }}" - - - name: Wait for SSH to come up - delegate_to: "{{ item.public_dns_name }}" - wait_for_connection: - delay: 60 - timeout: 320 - loop: "{{ ec2.instances }}" - -- name: Configure instance(s) - hosts: launched - become: True - gather_facts: True - roles: - - my_awesome_role - - my_awesome_test - -- name: Terminate instances - hosts: localhost - tasks: - - name: Terminate instances that were previously launched - ec2: - state: 'absent' - instance_ids: '{{ ec2.instance_ids }}' - -# Start a few existing instances, run some tasks -# and stop the instances - -- name: Start sandbox instances - hosts: localhost - gather_facts: false - vars: - instance_ids: - - 'i-xxxxxx' - - 'i-xxxxxx' - - 'i-xxxxxx' - region: us-east-1 - tasks: - - name: Start the sandbox instances - ec2: - instance_ids: '{{ instance_ids }}' - region: '{{ region }}' - state: running - wait: True - vpc_subnet_id: subnet-29e63245 - assign_public_ip: yes - roles: - - do_neat_stuff - - do_more_neat_stuff - -- name: Stop sandbox instances - hosts: localhost - gather_facts: false - vars: - instance_ids: - - 'i-xxxxxx' - - 'i-xxxxxx' - - 'i-xxxxxx' - region: us-east-1 - tasks: - - name: Stop the sandbox instances - ec2: - instance_ids: '{{ instance_ids }}' - region: '{{ region }}' - state: stopped - wait: True - vpc_subnet_id: subnet-29e63245 - assign_public_ip: yes - -# -# Start stopped instances specified by tag -# -- local_action: - module: ec2 - instance_tags: - Name: ExtraPower - state: running - -# -# Restart instances specified by tag -# -- local_action: - module: ec2 - instance_tags: - Name: ExtraPower - state: restarted - -# -# Enforce that 5 instances with a tag "foo" are running -# (Highly recommended!) -# - -- ec2: - key_name: mykey - instance_type: c1.medium - image: ami-40603AD1 - wait: yes - group: webserver - instance_tags: - foo: bar - exact_count: 5 - count_tag: foo - vpc_subnet_id: subnet-29e63245 - assign_public_ip: yes - -# -# Enforce that 5 running instances named "database" with a "dbtype" of "postgres" -# - -- ec2: - key_name: mykey - instance_type: c1.medium - image: ami-40603AD1 - wait: yes - group: webserver - instance_tags: - Name: database - dbtype: postgres - exact_count: 5 - count_tag: - Name: database - dbtype: postgres - vpc_subnet_id: subnet-29e63245 - assign_public_ip: yes - -# -# count_tag complex argument examples -# - - # instances with tag foo -- ec2: - count_tag: - foo: - - # instances with tag foo=bar -- ec2: - count_tag: - foo: bar - - # instances with tags foo=bar & baz -- ec2: - count_tag: - foo: bar - baz: - - # instances with tags foo & bar & baz=bang -- ec2: - count_tag: - - foo - - bar - - baz: bang - -''' - -import time -import datetime -import traceback -from ast import literal_eval -from distutils.version import LooseVersion - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import get_aws_connection_info, ec2_argument_spec, ec2_connect -from ansible.module_utils.six import get_function_code, string_types -from ansible.module_utils._text import to_bytes, to_text - -try: - import boto.ec2 - from boto.ec2.blockdevicemapping import BlockDeviceType, BlockDeviceMapping - from boto.exception import EC2ResponseError - from boto import connect_ec2_endpoint - from boto import connect_vpc - HAS_BOTO = True -except ImportError: - HAS_BOTO = False - - -def find_running_instances_by_count_tag(module, ec2, vpc, count_tag, zone=None): - - # get reservations for instances that match tag(s) and are in the desired state - state = module.params.get('state') - if state not in ['running', 'stopped']: - state = None - reservations = get_reservations(module, ec2, vpc, tags=count_tag, state=state, zone=zone) - - instances = [] - for res in reservations: - if hasattr(res, 'instances'): - for inst in res.instances: - if inst.state == 'terminated' or inst.state == 'shutting-down': - continue - instances.append(inst) - - return reservations, instances - - -def _set_none_to_blank(dictionary): - result = dictionary - for k in result: - if isinstance(result[k], dict): - result[k] = _set_none_to_blank(result[k]) - elif not result[k]: - result[k] = "" - return result - - -def get_reservations(module, ec2, vpc, tags=None, state=None, zone=None): - # TODO: filters do not work with tags that have underscores - filters = dict() - - vpc_subnet_id = module.params.get('vpc_subnet_id') - vpc_id = None - if vpc_subnet_id: - filters.update({"subnet-id": vpc_subnet_id}) - if vpc: - vpc_id = vpc.get_all_subnets(subnet_ids=[vpc_subnet_id])[0].vpc_id - - if vpc_id: - filters.update({"vpc-id": vpc_id}) - - if tags is not None: - - if isinstance(tags, str): - try: - tags = literal_eval(tags) - except Exception: - pass - - # if not a string type, convert and make sure it's a text string - if isinstance(tags, int): - tags = to_text(tags) - - # if string, we only care that a tag of that name exists - if isinstance(tags, str): - filters.update({"tag-key": tags}) - - # if list, append each item to filters - if isinstance(tags, list): - for x in tags: - if isinstance(x, dict): - x = _set_none_to_blank(x) - filters.update(dict(("tag:" + tn, tv) for (tn, tv) in x.items())) - else: - filters.update({"tag-key": x}) - - # if dict, add the key and value to the filter - if isinstance(tags, dict): - tags = _set_none_to_blank(tags) - filters.update(dict(("tag:" + tn, tv) for (tn, tv) in tags.items())) - - # lets check to see if the filters dict is empty, if so then stop - if not filters: - module.fail_json(msg="Filters based on tag is empty => tags: %s" % (tags)) - - if state: - # http://stackoverflow.com/questions/437511/what-are-the-valid-instancestates-for-the-amazon-ec2-api - filters.update({'instance-state-name': state}) - - if zone: - filters.update({'availability-zone': zone}) - - if module.params.get('id'): - filters['client-token'] = module.params['id'] - - results = ec2.get_all_instances(filters=filters) - - return results - - -def get_instance_info(inst): - """ - Retrieves instance information from an instance - ID and returns it as a dictionary - """ - instance_info = {'id': inst.id, - 'ami_launch_index': inst.ami_launch_index, - 'private_ip': inst.private_ip_address, - 'private_dns_name': inst.private_dns_name, - 'public_ip': inst.ip_address, - 'dns_name': inst.dns_name, - 'public_dns_name': inst.public_dns_name, - 'state_code': inst.state_code, - 'architecture': inst.architecture, - 'image_id': inst.image_id, - 'key_name': inst.key_name, - 'placement': inst.placement, - 'region': inst.placement[:-1], - 'kernel': inst.kernel, - 'ramdisk': inst.ramdisk, - 'launch_time': inst.launch_time, - 'instance_type': inst.instance_type, - 'root_device_type': inst.root_device_type, - 'root_device_name': inst.root_device_name, - 'state': inst.state, - 'hypervisor': inst.hypervisor, - 'tags': inst.tags, - 'groups': dict((group.id, group.name) for group in inst.groups), - } - try: - instance_info['virtualization_type'] = getattr(inst, 'virtualization_type') - except AttributeError: - instance_info['virtualization_type'] = None - - try: - instance_info['ebs_optimized'] = getattr(inst, 'ebs_optimized') - except AttributeError: - instance_info['ebs_optimized'] = False - - try: - bdm_dict = {} - bdm = getattr(inst, 'block_device_mapping') - for device_name in bdm.keys(): - bdm_dict[device_name] = { - 'status': bdm[device_name].status, - 'volume_id': bdm[device_name].volume_id, - 'delete_on_termination': bdm[device_name].delete_on_termination - } - instance_info['block_device_mapping'] = bdm_dict - except AttributeError: - instance_info['block_device_mapping'] = False - - try: - instance_info['tenancy'] = getattr(inst, 'placement_tenancy') - except AttributeError: - instance_info['tenancy'] = 'default' - - return instance_info - - -def boto_supports_associate_public_ip_address(ec2): - """ - Check if Boto library has associate_public_ip_address in the NetworkInterfaceSpecification - class. Added in Boto 2.13.0 - - ec2: authenticated ec2 connection object - - Returns: - True if Boto library accepts associate_public_ip_address argument, else false - """ - - try: - network_interface = boto.ec2.networkinterface.NetworkInterfaceSpecification() - getattr(network_interface, "associate_public_ip_address") - return True - except AttributeError: - return False - - -def boto_supports_profile_name_arg(ec2): - """ - Check if Boto library has instance_profile_name argument. instance_profile_name has been added in Boto 2.5.0 - - ec2: authenticated ec2 connection object - - Returns: - True if Boto library accept instance_profile_name argument, else false - """ - run_instances_method = getattr(ec2, 'run_instances') - return 'instance_profile_name' in get_function_code(run_instances_method).co_varnames - - -def boto_supports_volume_encryption(): - """ - Check if Boto library supports encryption of EBS volumes (added in 2.29.0) - - Returns: - True if boto library has the named param as an argument on the request_spot_instances method, else False - """ - return hasattr(boto, 'Version') and LooseVersion(boto.Version) >= LooseVersion('2.29.0') - - -def create_block_device(module, ec2, volume): - # Not aware of a way to determine this programatically - # http://aws.amazon.com/about-aws/whats-new/2013/10/09/ebs-provisioned-iops-maximum-iops-gb-ratio-increased-to-30-1/ - MAX_IOPS_TO_SIZE_RATIO = 30 - - volume_type = volume.get('volume_type') - - if 'snapshot' not in volume and 'ephemeral' not in volume: - if 'volume_size' not in volume: - module.fail_json(msg='Size must be specified when creating a new volume or modifying the root volume') - if 'snapshot' in volume: - if volume_type == 'io1' and 'iops' not in volume: - module.fail_json(msg='io1 volumes must have an iops value set') - if 'iops' in volume: - snapshot = ec2.get_all_snapshots(snapshot_ids=[volume['snapshot']])[0] - size = volume.get('volume_size', snapshot.volume_size) - if int(volume['iops']) > MAX_IOPS_TO_SIZE_RATIO * size: - module.fail_json(msg='IOPS must be at most %d times greater than size' % MAX_IOPS_TO_SIZE_RATIO) - if 'ephemeral' in volume: - if 'snapshot' in volume: - module.fail_json(msg='Cannot set both ephemeral and snapshot') - if boto_supports_volume_encryption(): - return BlockDeviceType(snapshot_id=volume.get('snapshot'), - ephemeral_name=volume.get('ephemeral'), - size=volume.get('volume_size'), - volume_type=volume_type, - delete_on_termination=volume.get('delete_on_termination', False), - iops=volume.get('iops'), - encrypted=volume.get('encrypted', None)) - else: - return BlockDeviceType(snapshot_id=volume.get('snapshot'), - ephemeral_name=volume.get('ephemeral'), - size=volume.get('volume_size'), - volume_type=volume_type, - delete_on_termination=volume.get('delete_on_termination', False), - iops=volume.get('iops')) - - -def boto_supports_param_in_spot_request(ec2, param): - """ - Check if Boto library has a <param> in its request_spot_instances() method. For example, the placement_group parameter wasn't added until 2.3.0. - - ec2: authenticated ec2 connection object - - Returns: - True if boto library has the named param as an argument on the request_spot_instances method, else False - """ - method = getattr(ec2, 'request_spot_instances') - return param in get_function_code(method).co_varnames - - -def await_spot_requests(module, ec2, spot_requests, count): - """ - Wait for a group of spot requests to be fulfilled, or fail. - - module: Ansible module object - ec2: authenticated ec2 connection object - spot_requests: boto.ec2.spotinstancerequest.SpotInstanceRequest object returned by ec2.request_spot_instances - count: Total number of instances to be created by the spot requests - - Returns: - list of instance ID's created by the spot request(s) - """ - spot_wait_timeout = int(module.params.get('spot_wait_timeout')) - wait_complete = time.time() + spot_wait_timeout - - spot_req_inst_ids = dict() - while time.time() < wait_complete: - reqs = ec2.get_all_spot_instance_requests() - for sirb in spot_requests: - if sirb.id in spot_req_inst_ids: - continue - for sir in reqs: - if sir.id != sirb.id: - continue # this is not our spot instance - if sir.instance_id is not None: - spot_req_inst_ids[sirb.id] = sir.instance_id - elif sir.state == 'open': - continue # still waiting, nothing to do here - elif sir.state == 'active': - continue # Instance is created already, nothing to do here - elif sir.state == 'failed': - module.fail_json(msg="Spot instance request %s failed with status %s and fault %s:%s" % ( - sir.id, sir.status.code, sir.fault.code, sir.fault.message)) - elif sir.state == 'cancelled': - module.fail_json(msg="Spot instance request %s was cancelled before it could be fulfilled." % sir.id) - elif sir.state == 'closed': - # instance is terminating or marked for termination - # this may be intentional on the part of the operator, - # or it may have been terminated by AWS due to capacity, - # price, or group constraints in this case, we'll fail - # the module if the reason for the state is anything - # other than termination by user. Codes are documented at - # https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-bid-status.html - if sir.status.code == 'instance-terminated-by-user': - # do nothing, since the user likely did this on purpose - pass - else: - spot_msg = "Spot instance request %s was closed by AWS with the status %s and fault %s:%s" - module.fail_json(msg=spot_msg % (sir.id, sir.status.code, sir.fault.code, sir.fault.message)) - - if len(spot_req_inst_ids) < count: - time.sleep(5) - else: - return list(spot_req_inst_ids.values()) - module.fail_json(msg="wait for spot requests timeout on %s" % time.asctime()) - - -def enforce_count(module, ec2, vpc): - - exact_count = module.params.get('exact_count') - count_tag = module.params.get('count_tag') - zone = module.params.get('zone') - - # fail here if the exact count was specified without filtering - # on a tag, as this may lead to a undesired removal of instances - if exact_count and count_tag is None: - module.fail_json(msg="you must use the 'count_tag' option with exact_count") - - reservations, instances = find_running_instances_by_count_tag(module, ec2, vpc, count_tag, zone) - - changed = None - checkmode = False - instance_dict_array = [] - changed_instance_ids = None - - if len(instances) == exact_count: - changed = False - elif len(instances) < exact_count: - changed = True - to_create = exact_count - len(instances) - if not checkmode: - (instance_dict_array, changed_instance_ids, changed) \ - = create_instances(module, ec2, vpc, override_count=to_create) - - for inst in instance_dict_array: - instances.append(inst) - elif len(instances) > exact_count: - changed = True - to_remove = len(instances) - exact_count - if not checkmode: - all_instance_ids = sorted([x.id for x in instances]) - remove_ids = all_instance_ids[0:to_remove] - - instances = [x for x in instances if x.id not in remove_ids] - - (changed, instance_dict_array, changed_instance_ids) \ - = terminate_instances(module, ec2, remove_ids) - terminated_list = [] - for inst in instance_dict_array: - inst['state'] = "terminated" - terminated_list.append(inst) - instance_dict_array = terminated_list - - # ensure all instances are dictionaries - all_instances = [] - for inst in instances: - - if not isinstance(inst, dict): - warn_if_public_ip_assignment_changed(module, inst) - inst = get_instance_info(inst) - all_instances.append(inst) - - return (all_instances, instance_dict_array, changed_instance_ids, changed) - - -def create_instances(module, ec2, vpc, override_count=None): - """ - Creates new instances - - module : AnsibleModule object - ec2: authenticated ec2 connection object - - Returns: - A list of dictionaries with instance information - about the instances that were launched - """ - - key_name = module.params.get('key_name') - id = module.params.get('id') - group_name = module.params.get('group') - group_id = module.params.get('group_id') - zone = module.params.get('zone') - instance_type = module.params.get('instance_type') - tenancy = module.params.get('tenancy') - spot_price = module.params.get('spot_price') - spot_type = module.params.get('spot_type') - image = module.params.get('image') - if override_count: - count = override_count - else: - count = module.params.get('count') - monitoring = module.params.get('monitoring') - kernel = module.params.get('kernel') - ramdisk = module.params.get('ramdisk') - wait = module.params.get('wait') - wait_timeout = int(module.params.get('wait_timeout')) - spot_wait_timeout = int(module.params.get('spot_wait_timeout')) - placement_group = module.params.get('placement_group') - user_data = module.params.get('user_data') - instance_tags = module.params.get('instance_tags') - vpc_subnet_id = module.params.get('vpc_subnet_id') - assign_public_ip = module.boolean(module.params.get('assign_public_ip')) - private_ip = module.params.get('private_ip') - instance_profile_name = module.params.get('instance_profile_name') - volumes = module.params.get('volumes') - ebs_optimized = module.params.get('ebs_optimized') - exact_count = module.params.get('exact_count') - count_tag = module.params.get('count_tag') - source_dest_check = module.boolean(module.params.get('source_dest_check')) - termination_protection = module.boolean(module.params.get('termination_protection')) - network_interfaces = module.params.get('network_interfaces') - spot_launch_group = module.params.get('spot_launch_group') - instance_initiated_shutdown_behavior = module.params.get('instance_initiated_shutdown_behavior') - - vpc_id = None - if vpc_subnet_id: - if not vpc: - module.fail_json(msg="region must be specified") - else: - vpc_id = vpc.get_all_subnets(subnet_ids=[vpc_subnet_id])[0].vpc_id - else: - vpc_id = None - - try: - # Here we try to lookup the group id from the security group name - if group is set. - if group_name: - if vpc_id: - grp_details = ec2.get_all_security_groups(filters={'vpc_id': vpc_id}) - else: - grp_details = ec2.get_all_security_groups() - if isinstance(group_name, string_types): - group_name = [group_name] - unmatched = set(group_name).difference(str(grp.name) for grp in grp_details) - if len(unmatched) > 0: - module.fail_json(msg="The following group names are not valid: %s" % ', '.join(unmatched)) - group_id = [str(grp.id) for grp in grp_details if str(grp.name) in group_name] - # Now we try to lookup the group id testing if group exists. - elif group_id: - # wrap the group_id in a list if it's not one already - if isinstance(group_id, string_types): - group_id = [group_id] - grp_details = ec2.get_all_security_groups(group_ids=group_id) - group_name = [grp_item.name for grp_item in grp_details] - except boto.exception.NoAuthHandlerFound as e: - module.fail_json(msg=str(e)) - - # Lookup any instances that much our run id. - - running_instances = [] - count_remaining = int(count) - - if id is not None: - filter_dict = {'client-token': id, 'instance-state-name': 'running'} - previous_reservations = ec2.get_all_instances(None, filter_dict) - for res in previous_reservations: - for prev_instance in res.instances: - running_instances.append(prev_instance) - count_remaining = count_remaining - len(running_instances) - - # Both min_count and max_count equal count parameter. This means the launch request is explicit (we want count, or fail) in how many instances we want. - - if count_remaining == 0: - changed = False - else: - changed = True - try: - params = {'image_id': image, - 'key_name': key_name, - 'monitoring_enabled': monitoring, - 'placement': zone, - 'instance_type': instance_type, - 'kernel_id': kernel, - 'ramdisk_id': ramdisk} - if user_data is not None: - params['user_data'] = to_bytes(user_data, errors='surrogate_or_strict') - - if ebs_optimized: - params['ebs_optimized'] = ebs_optimized - - # 'tenancy' always has a default value, but it is not a valid parameter for spot instance request - if not spot_price: - params['tenancy'] = tenancy - - if boto_supports_profile_name_arg(ec2): - params['instance_profile_name'] = instance_profile_name - else: - if instance_profile_name is not None: - module.fail_json( - msg="instance_profile_name parameter requires Boto version 2.5.0 or higher") - - if assign_public_ip is not None: - if not boto_supports_associate_public_ip_address(ec2): - module.fail_json( - msg="assign_public_ip parameter requires Boto version 2.13.0 or higher.") - elif not vpc_subnet_id: - module.fail_json( - msg="assign_public_ip only available with vpc_subnet_id") - - else: - if private_ip: - interface = boto.ec2.networkinterface.NetworkInterfaceSpecification( - subnet_id=vpc_subnet_id, - private_ip_address=private_ip, - groups=group_id, - associate_public_ip_address=assign_public_ip) - else: - interface = boto.ec2.networkinterface.NetworkInterfaceSpecification( - subnet_id=vpc_subnet_id, - groups=group_id, - associate_public_ip_address=assign_public_ip) - interfaces = boto.ec2.networkinterface.NetworkInterfaceCollection(interface) - params['network_interfaces'] = interfaces - else: - if network_interfaces: - if isinstance(network_interfaces, string_types): - network_interfaces = [network_interfaces] - interfaces = [] - for i, network_interface_id in enumerate(network_interfaces): - interface = boto.ec2.networkinterface.NetworkInterfaceSpecification( - network_interface_id=network_interface_id, - device_index=i) - interfaces.append(interface) - params['network_interfaces'] = \ - boto.ec2.networkinterface.NetworkInterfaceCollection(*interfaces) - else: - params['subnet_id'] = vpc_subnet_id - if vpc_subnet_id: - params['security_group_ids'] = group_id - else: - params['security_groups'] = group_name - - if volumes: - bdm = BlockDeviceMapping() - for volume in volumes: - if 'device_name' not in volume: - module.fail_json(msg='Device name must be set for volume') - # Minimum volume size is 1GiB. We'll use volume size explicitly set to 0 - # to be a signal not to create this volume - if 'volume_size' not in volume or int(volume['volume_size']) > 0: - bdm[volume['device_name']] = create_block_device(module, ec2, volume) - - params['block_device_map'] = bdm - - # check to see if we're using spot pricing first before starting instances - if not spot_price: - if assign_public_ip is not None and private_ip: - params.update( - dict( - min_count=count_remaining, - max_count=count_remaining, - client_token=id, - placement_group=placement_group, - ) - ) - else: - params.update( - dict( - min_count=count_remaining, - max_count=count_remaining, - client_token=id, - placement_group=placement_group, - private_ip_address=private_ip, - ) - ) - - # For ordinary (not spot) instances, we can select 'stop' - # (the default) or 'terminate' here. - params['instance_initiated_shutdown_behavior'] = instance_initiated_shutdown_behavior or 'stop' - - try: - res = ec2.run_instances(**params) - except boto.exception.EC2ResponseError as e: - if (params['instance_initiated_shutdown_behavior'] != 'terminate' and - "InvalidParameterCombination" == e.error_code): - params['instance_initiated_shutdown_behavior'] = 'terminate' - res = ec2.run_instances(**params) - else: - raise - - instids = [i.id for i in res.instances] - while True: - try: - ec2.get_all_instances(instids) - break - except boto.exception.EC2ResponseError as e: - if "<Code>InvalidInstanceID.NotFound</Code>" in str(e): - # there's a race between start and get an instance - continue - else: - module.fail_json(msg=str(e)) - - # The instances returned through ec2.run_instances above can be in - # terminated state due to idempotency. See commit 7f11c3d for a complete - # explanation. - terminated_instances = [ - str(instance.id) for instance in res.instances if instance.state == 'terminated' - ] - if terminated_instances: - module.fail_json(msg="Instances with id(s) %s " % terminated_instances + - "were created previously but have since been terminated - " + - "use a (possibly different) 'instanceid' parameter") - - else: - if private_ip: - module.fail_json( - msg='private_ip only available with on-demand (non-spot) instances') - if boto_supports_param_in_spot_request(ec2, 'placement_group'): - params['placement_group'] = placement_group - elif placement_group: - module.fail_json( - msg="placement_group parameter requires Boto version 2.3.0 or higher.") - - # You can't tell spot instances to 'stop'; they will always be - # 'terminate'd. For convenience, we'll ignore the latter value. - if instance_initiated_shutdown_behavior and instance_initiated_shutdown_behavior != 'terminate': - module.fail_json( - msg="instance_initiated_shutdown_behavior=stop is not supported for spot instances.") - - if spot_launch_group and isinstance(spot_launch_group, string_types): - params['launch_group'] = spot_launch_group - - params.update(dict( - count=count_remaining, - type=spot_type, - )) - - # Set spot ValidUntil - # ValidUntil -> (timestamp). The end date of the request, in - # UTC format (for example, YYYY -MM -DD T*HH* :MM :SS Z). - utc_valid_until = ( - datetime.datetime.utcnow() - + datetime.timedelta(seconds=spot_wait_timeout)) - params['valid_until'] = utc_valid_until.strftime('%Y-%m-%dT%H:%M:%S.000Z') - - res = ec2.request_spot_instances(spot_price, **params) - - # Now we have to do the intermediate waiting - if wait: - instids = await_spot_requests(module, ec2, res, count) - else: - instids = [] - except boto.exception.BotoServerError as e: - module.fail_json(msg="Instance creation failed => %s: %s" % (e.error_code, e.error_message)) - - # wait here until the instances are up - num_running = 0 - wait_timeout = time.time() + wait_timeout - res_list = () - while wait_timeout > time.time() and num_running < len(instids): - try: - res_list = ec2.get_all_instances(instids) - except boto.exception.BotoServerError as e: - if e.error_code == 'InvalidInstanceID.NotFound': - time.sleep(1) - continue - else: - raise - - num_running = 0 - for res in res_list: - num_running += len([i for i in res.instances if i.state == 'running']) - if len(res_list) <= 0: - # got a bad response of some sort, possibly due to - # stale/cached data. Wait a second and then try again - time.sleep(1) - continue - if wait and num_running < len(instids): - time.sleep(5) - else: - break - - if wait and wait_timeout <= time.time(): - # waiting took too long - module.fail_json(msg="wait for instances running timeout on %s" % time.asctime()) - - # We do this after the loop ends so that we end up with one list - for res in res_list: - running_instances.extend(res.instances) - - # Enabled by default by AWS - if source_dest_check is False: - for inst in res.instances: - inst.modify_attribute('sourceDestCheck', False) - - # Disabled by default by AWS - if termination_protection is True: - for inst in res.instances: - inst.modify_attribute('disableApiTermination', True) - - # Leave this as late as possible to try and avoid InvalidInstanceID.NotFound - if instance_tags and instids: - try: - ec2.create_tags(instids, instance_tags) - except boto.exception.EC2ResponseError as e: - module.fail_json(msg="Instance tagging failed => %s: %s" % (e.error_code, e.error_message)) - - instance_dict_array = [] - created_instance_ids = [] - for inst in running_instances: - inst.update() - d = get_instance_info(inst) - created_instance_ids.append(inst.id) - instance_dict_array.append(d) - - return (instance_dict_array, created_instance_ids, changed) - - -def terminate_instances(module, ec2, instance_ids): - """ - Terminates a list of instances - - module: Ansible module object - ec2: authenticated ec2 connection object - termination_list: a list of instances to terminate in the form of - [ {id: <inst-id>}, ..] - - Returns a dictionary of instance information - about the instances terminated. - - If the instance to be terminated is running - "changed" will be set to False. - - """ - - # Whether to wait for termination to complete before returning - wait = module.params.get('wait') - wait_timeout = int(module.params.get('wait_timeout')) - - changed = False - instance_dict_array = [] - - if not isinstance(instance_ids, list) or len(instance_ids) < 1: - module.fail_json(msg='instance_ids should be a list of instances, aborting') - - terminated_instance_ids = [] - for res in ec2.get_all_instances(instance_ids): - for inst in res.instances: - if inst.state == 'running' or inst.state == 'stopped': - terminated_instance_ids.append(inst.id) - instance_dict_array.append(get_instance_info(inst)) - try: - ec2.terminate_instances([inst.id]) - except EC2ResponseError as e: - module.fail_json(msg='Unable to terminate instance {0}, error: {1}'.format(inst.id, e)) - changed = True - - # wait here until the instances are 'terminated' - if wait: - num_terminated = 0 - wait_timeout = time.time() + wait_timeout - while wait_timeout > time.time() and num_terminated < len(terminated_instance_ids): - response = ec2.get_all_instances(instance_ids=terminated_instance_ids, - filters={'instance-state-name': 'terminated'}) - try: - num_terminated = sum([len(res.instances) for res in response]) - except Exception as e: - # got a bad response of some sort, possibly due to - # stale/cached data. Wait a second and then try again - time.sleep(1) - continue - - if num_terminated < len(terminated_instance_ids): - time.sleep(5) - - # waiting took too long - if wait_timeout < time.time() and num_terminated < len(terminated_instance_ids): - module.fail_json(msg="wait for instance termination timeout on %s" % time.asctime()) - # Lets get the current state of the instances after terminating - issue600 - instance_dict_array = [] - for res in ec2.get_all_instances(instance_ids=terminated_instance_ids, filters={'instance-state-name': 'terminated'}): - for inst in res.instances: - instance_dict_array.append(get_instance_info(inst)) - - return (changed, instance_dict_array, terminated_instance_ids) - - -def startstop_instances(module, ec2, instance_ids, state, instance_tags): - """ - Starts or stops a list of existing instances - - module: Ansible module object - ec2: authenticated ec2 connection object - instance_ids: The list of instances to start in the form of - [ {id: <inst-id>}, ..] - instance_tags: A dict of tag keys and values in the form of - {key: value, ... } - state: Intended state ("running" or "stopped") - - Returns a dictionary of instance information - about the instances started/stopped. - - If the instance was not able to change state, - "changed" will be set to False. - - Note that if instance_ids and instance_tags are both non-empty, - this method will process the intersection of the two - """ - - wait = module.params.get('wait') - wait_timeout = int(module.params.get('wait_timeout')) - group_id = module.params.get('group_id') - group_name = module.params.get('group') - changed = False - instance_dict_array = [] - - if not isinstance(instance_ids, list) or len(instance_ids) < 1: - # Fail unless the user defined instance tags - if not instance_tags: - module.fail_json(msg='instance_ids should be a list of instances, aborting') - - # To make an EC2 tag filter, we need to prepend 'tag:' to each key. - # An empty filter does no filtering, so it's safe to pass it to the - # get_all_instances method even if the user did not specify instance_tags - filters = {} - if instance_tags: - for key, value in instance_tags.items(): - filters["tag:" + key] = value - - if module.params.get('id'): - filters['client-token'] = module.params['id'] - # Check that our instances are not in the state we want to take - - # Check (and eventually change) instances attributes and instances state - existing_instances_array = [] - for res in ec2.get_all_instances(instance_ids, filters=filters): - for inst in res.instances: - - warn_if_public_ip_assignment_changed(module, inst) - - changed = (check_source_dest_attr(module, inst, ec2) or - check_termination_protection(module, inst) or changed) - - # Check security groups and if we're using ec2-vpc; ec2-classic security groups may not be modified - if inst.vpc_id and group_name: - grp_details = ec2.get_all_security_groups(filters={'vpc_id': inst.vpc_id}) - if isinstance(group_name, string_types): - group_name = [group_name] - unmatched = set(group_name) - set(to_text(grp.name) for grp in grp_details) - if unmatched: - module.fail_json(msg="The following group names are not valid: %s" % ', '.join(unmatched)) - group_ids = [to_text(grp.id) for grp in grp_details if to_text(grp.name) in group_name] - elif inst.vpc_id and group_id: - if isinstance(group_id, string_types): - group_id = [group_id] - grp_details = ec2.get_all_security_groups(group_ids=group_id) - group_ids = [grp_item.id for grp_item in grp_details] - if inst.vpc_id and (group_name or group_id): - if set(sg.id for sg in inst.groups) != set(group_ids): - changed = inst.modify_attribute('groupSet', group_ids) - - # Check instance state - if inst.state != state: - instance_dict_array.append(get_instance_info(inst)) - try: - if state == 'running': - inst.start() - else: - inst.stop() - except EC2ResponseError as e: - module.fail_json(msg='Unable to change state for instance {0}, error: {1}'.format(inst.id, e)) - changed = True - existing_instances_array.append(inst.id) - - instance_ids = list(set(existing_instances_array + (instance_ids or []))) - # Wait for all the instances to finish starting or stopping - wait_timeout = time.time() + wait_timeout - while wait and wait_timeout > time.time(): - instance_dict_array = [] - matched_instances = [] - for res in ec2.get_all_instances(instance_ids): - for i in res.instances: - if i.state == state: - instance_dict_array.append(get_instance_info(i)) - matched_instances.append(i) - if len(matched_instances) < len(instance_ids): - time.sleep(5) - else: - break - - if wait and wait_timeout <= time.time(): - # waiting took too long - module.fail_json(msg="wait for instances running timeout on %s" % time.asctime()) - - return (changed, instance_dict_array, instance_ids) - - -def restart_instances(module, ec2, instance_ids, state, instance_tags): - """ - Restarts a list of existing instances - - module: Ansible module object - ec2: authenticated ec2 connection object - instance_ids: The list of instances to start in the form of - [ {id: <inst-id>}, ..] - instance_tags: A dict of tag keys and values in the form of - {key: value, ... } - state: Intended state ("restarted") - - Returns a dictionary of instance information - about the instances. - - If the instance was not able to change state, - "changed" will be set to False. - - Wait will not apply here as this is a OS level operation. - - Note that if instance_ids and instance_tags are both non-empty, - this method will process the intersection of the two. - """ - - changed = False - instance_dict_array = [] - - if not isinstance(instance_ids, list) or len(instance_ids) < 1: - # Fail unless the user defined instance tags - if not instance_tags: - module.fail_json(msg='instance_ids should be a list of instances, aborting') - - # To make an EC2 tag filter, we need to prepend 'tag:' to each key. - # An empty filter does no filtering, so it's safe to pass it to the - # get_all_instances method even if the user did not specify instance_tags - filters = {} - if instance_tags: - for key, value in instance_tags.items(): - filters["tag:" + key] = value - if module.params.get('id'): - filters['client-token'] = module.params['id'] - - # Check that our instances are not in the state we want to take - - # Check (and eventually change) instances attributes and instances state - for res in ec2.get_all_instances(instance_ids, filters=filters): - for inst in res.instances: - - warn_if_public_ip_assignment_changed(module, inst) - - changed = (check_source_dest_attr(module, inst, ec2) or - check_termination_protection(module, inst) or changed) - - # Check instance state - if inst.state != state: - instance_dict_array.append(get_instance_info(inst)) - try: - inst.reboot() - except EC2ResponseError as e: - module.fail_json(msg='Unable to change state for instance {0}, error: {1}'.format(inst.id, e)) - changed = True - - return (changed, instance_dict_array, instance_ids) - - -def check_termination_protection(module, inst): - """ - Check the instance disableApiTermination attribute. - - module: Ansible module object - inst: EC2 instance object - - returns: True if state changed None otherwise - """ - - termination_protection = module.params.get('termination_protection') - - if (inst.get_attribute('disableApiTermination')['disableApiTermination'] != termination_protection and termination_protection is not None): - inst.modify_attribute('disableApiTermination', termination_protection) - return True - - -def check_source_dest_attr(module, inst, ec2): - """ - Check the instance sourceDestCheck attribute. - - module: Ansible module object - inst: EC2 instance object - - returns: True if state changed None otherwise - """ - - source_dest_check = module.params.get('source_dest_check') - - if source_dest_check is not None: - try: - if inst.vpc_id is not None and inst.get_attribute('sourceDestCheck')['sourceDestCheck'] != source_dest_check: - inst.modify_attribute('sourceDestCheck', source_dest_check) - return True - except boto.exception.EC2ResponseError as exc: - # instances with more than one Elastic Network Interface will - # fail, because they have the sourceDestCheck attribute defined - # per-interface - if exc.code == 'InvalidInstanceID': - for interface in inst.interfaces: - if interface.source_dest_check != source_dest_check: - ec2.modify_network_interface_attribute(interface.id, "sourceDestCheck", source_dest_check) - return True - else: - module.fail_json(msg='Failed to handle source_dest_check state for instance {0}, error: {1}'.format(inst.id, exc), - exception=traceback.format_exc()) - - -def warn_if_public_ip_assignment_changed(module, instance): - # This is a non-modifiable attribute. - assign_public_ip = module.params.get('assign_public_ip') - - # Check that public ip assignment is the same and warn if not - public_dns_name = getattr(instance, 'public_dns_name', None) - if (assign_public_ip or public_dns_name) and (not public_dns_name or assign_public_ip is False): - 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.id)) - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - key_name=dict(aliases=['keypair']), - id=dict(), - group=dict(type='list', aliases=['groups']), - group_id=dict(type='list'), - zone=dict(aliases=['aws_zone', 'ec2_zone']), - instance_type=dict(aliases=['type']), - spot_price=dict(), - spot_type=dict(default='one-time', choices=["one-time", "persistent"]), - spot_launch_group=dict(), - image=dict(), - kernel=dict(), - count=dict(type='int', default='1'), - monitoring=dict(type='bool', default=False), - ramdisk=dict(), - wait=dict(type='bool', default=False), - wait_timeout=dict(type='int', default=300), - spot_wait_timeout=dict(type='int', default=600), - placement_group=dict(), - user_data=dict(), - instance_tags=dict(type='dict'), - vpc_subnet_id=dict(), - assign_public_ip=dict(type='bool'), - private_ip=dict(), - instance_profile_name=dict(), - instance_ids=dict(type='list', aliases=['instance_id']), - source_dest_check=dict(type='bool', default=None), - termination_protection=dict(type='bool', default=None), - state=dict(default='present', choices=['present', 'absent', 'running', 'restarted', 'stopped']), - instance_initiated_shutdown_behavior=dict(default='stop', choices=['stop', 'terminate']), - exact_count=dict(type='int', default=None), - count_tag=dict(type='raw'), - volumes=dict(type='list'), - ebs_optimized=dict(type='bool', default=False), - tenancy=dict(default='default', choices=['default', 'dedicated']), - network_interfaces=dict(type='list', aliases=['network_interface']) - ) - ) - - module = AnsibleModule( - argument_spec=argument_spec, - mutually_exclusive=[ - # Can be uncommented when we finish the deprecation cycle. - # ['group', 'group_id'], - ['exact_count', 'count'], - ['exact_count', 'state'], - ['exact_count', 'instance_ids'], - ['network_interfaces', 'assign_public_ip'], - ['network_interfaces', 'group'], - ['network_interfaces', 'group_id'], - ['network_interfaces', 'private_ip'], - ['network_interfaces', 'vpc_subnet_id'], - ], - ) - - if module.params.get('group') and module.params.get('group_id'): - module.deprecate( - msg='Support for passing both group and group_id has been deprecated. ' - 'Currently group_id is ignored, in future passing both will result in an error', - version='2.14') - - if not HAS_BOTO: - module.fail_json(msg='boto required for this module') - - try: - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module) - if module.params.get('region') or not module.params.get('ec2_url'): - ec2 = ec2_connect(module) - elif module.params.get('ec2_url'): - ec2 = connect_ec2_endpoint(ec2_url, **aws_connect_kwargs) - - if 'region' not in aws_connect_kwargs: - aws_connect_kwargs['region'] = ec2.region - - vpc = connect_vpc(**aws_connect_kwargs) - except boto.exception.NoAuthHandlerFound as e: - module.fail_json(msg="Failed to get connection: %s" % e.message, exception=traceback.format_exc()) - - tagged_instances = [] - - state = module.params['state'] - - if state == 'absent': - instance_ids = module.params['instance_ids'] - if not instance_ids: - module.fail_json(msg='instance_ids list is required for absent state') - - (changed, instance_dict_array, new_instance_ids) = terminate_instances(module, ec2, instance_ids) - - elif state in ('running', 'stopped'): - instance_ids = module.params.get('instance_ids') - instance_tags = module.params.get('instance_tags') - if not (isinstance(instance_ids, list) or isinstance(instance_tags, dict)): - module.fail_json(msg='running list needs to be a list of instances or set of tags to run: %s' % instance_ids) - - (changed, instance_dict_array, new_instance_ids) = startstop_instances(module, ec2, instance_ids, state, instance_tags) - - elif state in ('restarted'): - instance_ids = module.params.get('instance_ids') - instance_tags = module.params.get('instance_tags') - if not (isinstance(instance_ids, list) or isinstance(instance_tags, dict)): - module.fail_json(msg='running list needs to be a list of instances or set of tags to run: %s' % instance_ids) - - (changed, instance_dict_array, new_instance_ids) = restart_instances(module, ec2, instance_ids, state, instance_tags) - - elif state == 'present': - # Changed is always set to true when provisioning new instances - if not module.params.get('image'): - module.fail_json(msg='image parameter is required for new instance') - - if module.params.get('exact_count') is None: - (instance_dict_array, new_instance_ids, changed) = create_instances(module, ec2, vpc) - else: - (tagged_instances, instance_dict_array, new_instance_ids, changed) = enforce_count(module, ec2, vpc) - - # Always return instances in the same order - if new_instance_ids: - new_instance_ids.sort() - if instance_dict_array: - instance_dict_array.sort(key=lambda x: x['id']) - if tagged_instances: - tagged_instances.sort(key=lambda x: x['id']) - - module.exit_json(changed=changed, instance_ids=new_instance_ids, instances=instance_dict_array, tagged_instances=tagged_instances) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_ami.py b/lib/ansible/modules/cloud/amazon/ec2_ami.py deleted file mode 100644 index ec4e46790c..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_ami.py +++ /dev/null @@ -1,738 +0,0 @@ -#!/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 - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_ami -version_added: "1.3" -short_description: Create or destroy an image (AMI) in ec2 -description: - - Registers or deregisters ec2 images. -options: - instance_id: - description: - - Instance ID to create the AMI from. - type: str - name: - description: - - The name of the new AMI. - type: str - architecture: - version_added: "2.3" - description: - - The target architecture of the image to register - default: "x86_64" - type: str - kernel_id: - version_added: "2.3" - description: - - The target kernel id of the image to register. - type: str - virtualization_type: - version_added: "2.3" - description: - - The virtualization type of the image to register. - default: "hvm" - type: str - root_device_name: - version_added: "2.3" - description: - - The root device name of the image to register. - type: str - wait: - description: - - Wait for the AMI to be in state 'available' before returning. - default: false - type: bool - wait_timeout: - description: - - How long before wait gives up, in seconds. - default: 900 - type: int - state: - description: - - Register or deregister an AMI. - default: 'present' - choices: [ "absent", "present" ] - type: str - description: - description: - - Human-readable string describing the contents and purpose of the AMI. - type: str - no_reboot: - description: - - Flag indicating that the bundling process should not attempt to shutdown the instance before bundling. If this flag is True, the - responsibility of maintaining file system integrity is left to the owner of the instance. - default: false - type: bool - image_id: - description: - - Image ID to be deregistered. - type: str - device_mapping: - version_added: "2.0" - description: - - List of device hashes/dictionaries with custom configurations (same block-device-mapping parameters). - type: list - elements: dict - suboptions: - device_name: - type: str - description: The device name. For example C(/dev/sda). - volume_type: - type: str - description: The volume type. Defaults to C(gp2) when not set. - delete_on_termination: - type: bool - description: Whether the device should be automatically deleted when the Instance is terminated. - no_device: - type: bool - description: Suppresses the specified device included in the block device mapping of the AMI. - snapshot_id: - type: str - description: The ID of the Snapshot. - iops: - type: int - description: When using an C(io1) I(volume_type) this sets the number of IOPS provisioned for the volume - encrypted: - type: bool - description: Whether the volume should be encrypted. - volume_size: - aliases: ['size'] - type: int - description: The size of the volume (in GiB) - delete_snapshot: - description: - - Delete snapshots when deregistering the AMI. - default: false - type: bool - tags: - description: - - A dictionary of tags to add to the new image; '{"key":"value"}' and '{"key":"value","key":"value"}' - version_added: "2.0" - type: dict - purge_tags: - description: Whether to remove existing tags that aren't passed in the C(tags) parameter - version_added: "2.5" - default: false - type: bool - launch_permissions: - description: - - Users and groups that should be able to launch the AMI. Expects dictionary with a key of user_ids and/or group_names. user_ids should - be a list of account ids. group_name should be a list of groups, "all" is the only acceptable value currently. - - You must pass all desired launch permissions if you wish to modify existing launch permissions (passing just groups will remove all users) - version_added: "2.0" - type: dict - image_location: - description: - - The s3 location of an image to use for the AMI. - version_added: "2.5" - type: str - enhanced_networking: - description: - - A boolean representing whether enhanced networking with ENA is enabled or not. - version_added: "2.5" - type: bool - billing_products: - description: - - A list of valid billing codes. To be used with valid accounts by aws marketplace vendors. - version_added: "2.5" - type: list - elements: str - ramdisk_id: - description: - - The ID of the RAM disk. - version_added: "2.5" - type: str - sriov_net_support: - description: - - Set to simple to enable enhanced networking with the Intel 82599 Virtual Function interface for the AMI and any instances that you launch from the AMI. - version_added: "2.5" - type: str -author: - - "Evan Duffield (@scicoin-project) <eduffield@iacquire.com>" - - "Constantin Bugneac (@Constantin07) <constantin.bugneac@endava.com>" - - "Ross Williams (@gunzy83) <gunzy83au@gmail.com>" - - "Willem van Ketwich (@wilvk) <willvk@gmail.com>" -extends_documentation_fragment: - - aws - - ec2 -''' - -# Thank you to iAcquire for sponsoring development of this module. - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Basic AMI Creation -- ec2_ami: - instance_id: i-xxxxxx - wait: yes - name: newtest - tags: - Name: newtest - Service: TestService - -# Basic AMI Creation, without waiting -- ec2_ami: - instance_id: i-xxxxxx - wait: no - name: newtest - -# AMI Registration from EBS Snapshot -- ec2_ami: - name: newtest - state: present - architecture: x86_64 - virtualization_type: hvm - root_device_name: /dev/xvda - device_mapping: - - device_name: /dev/xvda - volume_size: 8 - snapshot_id: snap-xxxxxxxx - delete_on_termination: true - volume_type: gp2 - -# AMI Creation, with a custom root-device size and another EBS attached -- ec2_ami: - instance_id: i-xxxxxx - name: newtest - device_mapping: - - device_name: /dev/sda1 - size: XXX - delete_on_termination: true - volume_type: gp2 - - device_name: /dev/sdb - size: YYY - delete_on_termination: false - volume_type: gp2 - -# AMI Creation, excluding a volume attached at /dev/sdb -- ec2_ami: - instance_id: i-xxxxxx - name: newtest - device_mapping: - - device_name: /dev/sda1 - size: XXX - delete_on_termination: true - volume_type: gp2 - - device_name: /dev/sdb - no_device: yes - -# Deregister/Delete AMI (keep associated snapshots) -- ec2_ami: - image_id: "{{ instance.image_id }}" - delete_snapshot: False - state: absent - -# Deregister AMI (delete associated snapshots too) -- ec2_ami: - image_id: "{{ instance.image_id }}" - delete_snapshot: True - state: absent - -# Update AMI Launch Permissions, making it public -- ec2_ami: - image_id: "{{ instance.image_id }}" - state: present - launch_permissions: - group_names: ['all'] - -# Allow AMI to be launched by another account -- ec2_ami: - image_id: "{{ instance.image_id }}" - state: present - launch_permissions: - user_ids: ['123456789012'] -''' - -RETURN = ''' -architecture: - description: Architecture of image. - returned: when AMI is created or already exists - type: str - sample: "x86_64" -block_device_mapping: - description: Block device mapping associated with image. - returned: when AMI is created or already exists - type: dict - sample: { - "/dev/sda1": { - "delete_on_termination": true, - "encrypted": false, - "size": 10, - "snapshot_id": "snap-1a03b80e7", - "volume_type": "standard" - } - } -creationDate: - description: Creation date of image. - returned: when AMI is created or already exists - type: str - sample: "2015-10-15T22:43:44.000Z" -description: - description: Description of image. - returned: when AMI is created or already exists - type: str - sample: "nat-server" -hypervisor: - description: Type of hypervisor. - returned: when AMI is created or already exists - type: str - sample: "xen" -image_id: - description: ID of the image. - returned: when AMI is created or already exists - type: str - sample: "ami-1234abcd" -is_public: - description: Whether image is public. - returned: when AMI is created or already exists - type: bool - sample: false -launch_permission: - description: Permissions allowing other accounts to access the AMI. - returned: when AMI is created or already exists - type: list - sample: - - group: "all" -location: - description: Location of image. - returned: when AMI is created or already exists - type: str - sample: "315210894379/nat-server" -name: - description: AMI name of image. - returned: when AMI is created or already exists - type: str - sample: "nat-server" -ownerId: - description: Owner of image. - returned: when AMI is created or already exists - type: str - sample: "435210894375" -platform: - description: Platform of image. - returned: when AMI is created or already exists - type: str - sample: null -root_device_name: - description: Root device name of image. - returned: when AMI is created or already exists - type: str - sample: "/dev/sda1" -root_device_type: - description: Root device type of image. - returned: when AMI is created or already exists - type: str - sample: "ebs" -state: - description: State of image. - returned: when AMI is created or already exists - type: str - sample: "available" -tags: - description: A dictionary of tags assigned to image. - returned: when AMI is created or already exists - type: dict - sample: { - "Env": "devel", - "Name": "nat-server" - } -virtualization_type: - description: Image virtualization type. - returned: when AMI is created or already exists - type: str - sample: "hvm" -snapshots_deleted: - description: A list of snapshot ids deleted after deregistering image. - returned: after AMI is deregistered, if I(delete_snapshot=true) - type: list - sample: [ - "snap-fbcccb8f", - "snap-cfe7cdb4" - ] -''' - -import time -from ansible.module_utils.ec2 import ansible_dict_to_boto3_tag_list, boto3_tag_list_to_ansible_dict, camel_dict_to_snake_dict, compare_aws_tags -from ansible.module_utils.aws.core import AnsibleAWSModule - -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule - - -def get_block_device_mapping(image): - bdm_dict = dict() - if image is not None and image.get('block_device_mappings') is not None: - bdm = image.get('block_device_mappings') - for device in bdm: - device_name = device.get('device_name') - if 'ebs' in device: - ebs = device.get("ebs") - bdm_dict_item = { - 'size': ebs.get("volume_size"), - 'snapshot_id': ebs.get("snapshot_id"), - 'volume_type': ebs.get("volume_type"), - 'encrypted': ebs.get("encrypted"), - 'delete_on_termination': ebs.get("delete_on_termination") - } - elif 'virtual_name' in device: - bdm_dict_item = dict(virtual_name=device['virtual_name']) - bdm_dict[device_name] = bdm_dict_item - return bdm_dict - - -def get_ami_info(camel_image): - image = camel_dict_to_snake_dict(camel_image) - return dict( - image_id=image.get("image_id"), - state=image.get("state"), - architecture=image.get("architecture"), - block_device_mapping=get_block_device_mapping(image), - creationDate=image.get("creation_date"), - description=image.get("description"), - hypervisor=image.get("hypervisor"), - is_public=image.get("public"), - location=image.get("image_location"), - ownerId=image.get("owner_id"), - root_device_name=image.get("root_device_name"), - root_device_type=image.get("root_device_type"), - virtualization_type=image.get("virtualization_type"), - name=image.get("name"), - tags=boto3_tag_list_to_ansible_dict(image.get('tags')), - platform=image.get("platform"), - enhanced_networking=image.get("ena_support"), - image_owner_alias=image.get("image_owner_alias"), - image_type=image.get("image_type"), - kernel_id=image.get("kernel_id"), - product_codes=image.get("product_codes"), - ramdisk_id=image.get("ramdisk_id"), - sriov_net_support=image.get("sriov_net_support"), - state_reason=image.get("state_reason"), - launch_permissions=image.get('launch_permissions') - ) - - -def create_image(module, connection): - instance_id = module.params.get('instance_id') - name = module.params.get('name') - wait = module.params.get('wait') - wait_timeout = module.params.get('wait_timeout') - description = module.params.get('description') - architecture = module.params.get('architecture') - kernel_id = module.params.get('kernel_id') - root_device_name = module.params.get('root_device_name') - virtualization_type = module.params.get('virtualization_type') - no_reboot = module.params.get('no_reboot') - device_mapping = module.params.get('device_mapping') - tags = module.params.get('tags') - launch_permissions = module.params.get('launch_permissions') - image_location = module.params.get('image_location') - enhanced_networking = module.params.get('enhanced_networking') - billing_products = module.params.get('billing_products') - ramdisk_id = module.params.get('ramdisk_id') - sriov_net_support = module.params.get('sriov_net_support') - - try: - params = { - 'Name': name, - 'Description': description - } - - block_device_mapping = None - - if device_mapping: - block_device_mapping = [] - for device in device_mapping: - device['Ebs'] = {} - if 'device_name' not in device: - module.fail_json(msg="Error - Device name must be set for volume.") - device = rename_item_if_exists(device, 'device_name', 'DeviceName') - device = rename_item_if_exists(device, 'virtual_name', 'VirtualName') - device = rename_item_if_exists(device, 'no_device', 'NoDevice') - device = rename_item_if_exists(device, 'volume_type', 'VolumeType', 'Ebs') - device = rename_item_if_exists(device, 'snapshot_id', 'SnapshotId', 'Ebs') - device = rename_item_if_exists(device, 'delete_on_termination', 'DeleteOnTermination', 'Ebs') - device = rename_item_if_exists(device, 'size', 'VolumeSize', 'Ebs', attribute_type=int) - device = rename_item_if_exists(device, 'volume_size', 'VolumeSize', 'Ebs', attribute_type=int) - device = rename_item_if_exists(device, 'iops', 'Iops', 'Ebs') - device = rename_item_if_exists(device, 'encrypted', 'Encrypted', 'Ebs') - block_device_mapping.append(device) - if block_device_mapping: - params['BlockDeviceMappings'] = block_device_mapping - if instance_id: - params['InstanceId'] = instance_id - params['NoReboot'] = no_reboot - image_id = connection.create_image(**params).get('ImageId') - else: - if architecture: - params['Architecture'] = architecture - if virtualization_type: - params['VirtualizationType'] = virtualization_type - if image_location: - params['ImageLocation'] = image_location - if enhanced_networking: - params['EnaSupport'] = enhanced_networking - if billing_products: - params['BillingProducts'] = billing_products - if ramdisk_id: - params['RamdiskId'] = ramdisk_id - if sriov_net_support: - params['SriovNetSupport'] = sriov_net_support - if kernel_id: - params['KernelId'] = kernel_id - if root_device_name: - params['RootDeviceName'] = root_device_name - image_id = connection.register_image(**params).get('ImageId') - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Error registering image") - - if wait: - waiter = connection.get_waiter('image_available') - delay = wait_timeout // 30 - max_attempts = 30 - waiter.wait(ImageIds=[image_id], WaiterConfig=dict(Delay=delay, MaxAttempts=max_attempts)) - - if tags: - try: - connection.create_tags(Resources=[image_id], Tags=ansible_dict_to_boto3_tag_list(tags)) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Error tagging image") - - if launch_permissions: - try: - params = dict(Attribute='LaunchPermission', ImageId=image_id, LaunchPermission=dict(Add=list())) - for group_name in launch_permissions.get('group_names', []): - params['LaunchPermission']['Add'].append(dict(Group=group_name)) - for user_id in launch_permissions.get('user_ids', []): - params['LaunchPermission']['Add'].append(dict(UserId=str(user_id))) - if params['LaunchPermission']['Add']: - connection.modify_image_attribute(**params) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Error setting launch permissions for image %s" % image_id) - - module.exit_json(msg="AMI creation operation complete.", changed=True, - **get_ami_info(get_image_by_id(module, connection, image_id))) - - -def deregister_image(module, connection): - image_id = module.params.get('image_id') - delete_snapshot = module.params.get('delete_snapshot') - wait = module.params.get('wait') - wait_timeout = module.params.get('wait_timeout') - image = get_image_by_id(module, connection, image_id) - - if image is None: - module.exit_json(changed=False) - - # Get all associated snapshot ids before deregistering image otherwise this information becomes unavailable. - snapshots = [] - if 'BlockDeviceMappings' in image: - for mapping in image.get('BlockDeviceMappings'): - snapshot_id = mapping.get('Ebs', {}).get('SnapshotId') - if snapshot_id is not None: - snapshots.append(snapshot_id) - - # When trying to re-deregister an already deregistered image it doesn't raise an exception, it just returns an object without image attributes. - if 'ImageId' in image: - try: - connection.deregister_image(ImageId=image_id) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Error deregistering image") - else: - module.exit_json(msg="Image %s has already been deregistered." % image_id, changed=False) - - image = get_image_by_id(module, connection, image_id) - wait_timeout = time.time() + wait_timeout - - while wait and wait_timeout > time.time() and image is not None: - image = get_image_by_id(module, connection, image_id) - time.sleep(3) - - if wait and wait_timeout <= time.time(): - module.fail_json(msg="Timed out waiting for image to be deregistered.") - - exit_params = {'msg': "AMI deregister operation complete.", 'changed': True} - - if delete_snapshot: - try: - for snapshot_id in snapshots: - connection.delete_snapshot(SnapshotId=snapshot_id) - except botocore.exceptions.ClientError as e: - # Don't error out if root volume snapshot was already deregistered as part of deregister_image - if e.response['Error']['Code'] == 'InvalidSnapshot.NotFound': - pass - exit_params['snapshots_deleted'] = snapshots - - module.exit_json(**exit_params) - - -def update_image(module, connection, image_id): - launch_permissions = module.params.get('launch_permissions') - image = get_image_by_id(module, connection, image_id) - if image is None: - module.fail_json(msg="Image %s does not exist" % image_id, changed=False) - changed = False - - if launch_permissions is not None: - current_permissions = image['LaunchPermissions'] - - current_users = set(permission['UserId'] for permission in current_permissions if 'UserId' in permission) - desired_users = set(str(user_id) for user_id in launch_permissions.get('user_ids', [])) - current_groups = set(permission['Group'] for permission in current_permissions if 'Group' in permission) - desired_groups = set(launch_permissions.get('group_names', [])) - - to_add_users = desired_users - current_users - to_remove_users = current_users - desired_users - to_add_groups = desired_groups - current_groups - to_remove_groups = current_groups - desired_groups - - to_add = [dict(Group=group) for group in to_add_groups] + [dict(UserId=user_id) for user_id in to_add_users] - to_remove = [dict(Group=group) for group in to_remove_groups] + [dict(UserId=user_id) for user_id in to_remove_users] - - if to_add or to_remove: - try: - connection.modify_image_attribute(ImageId=image_id, Attribute='launchPermission', - LaunchPermission=dict(Add=to_add, Remove=to_remove)) - changed = True - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Error updating launch permissions of image %s" % image_id) - - desired_tags = module.params.get('tags') - if desired_tags is not None: - current_tags = boto3_tag_list_to_ansible_dict(image.get('Tags')) - tags_to_add, tags_to_remove = compare_aws_tags(current_tags, desired_tags, purge_tags=module.params.get('purge_tags')) - - if tags_to_remove: - try: - connection.delete_tags(Resources=[image_id], Tags=[dict(Key=tagkey) for tagkey in tags_to_remove]) - changed = True - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Error updating tags") - - if tags_to_add: - try: - connection.create_tags(Resources=[image_id], Tags=ansible_dict_to_boto3_tag_list(tags_to_add)) - changed = True - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Error updating tags") - - description = module.params.get('description') - if description and description != image['Description']: - try: - connection.modify_image_attribute(Attribute='Description ', ImageId=image_id, Description=dict(Value=description)) - changed = True - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Error setting description for image %s" % image_id) - - if changed: - module.exit_json(msg="AMI updated.", changed=True, - **get_ami_info(get_image_by_id(module, connection, image_id))) - else: - module.exit_json(msg="AMI not updated.", changed=False, - **get_ami_info(get_image_by_id(module, connection, image_id))) - - -def get_image_by_id(module, connection, image_id): - try: - try: - images_response = connection.describe_images(ImageIds=[image_id]) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Error retrieving image %s" % image_id) - images = images_response.get('Images') - no_images = len(images) - if no_images == 0: - return None - if no_images == 1: - result = images[0] - try: - result['LaunchPermissions'] = connection.describe_image_attribute(Attribute='launchPermission', ImageId=image_id)['LaunchPermissions'] - result['ProductCodes'] = connection.describe_image_attribute(Attribute='productCodes', ImageId=image_id)['ProductCodes'] - except botocore.exceptions.ClientError as e: - if e.response['Error']['Code'] != 'InvalidAMIID.Unavailable': - module.fail_json_aws(e, msg="Error retrieving image attributes for image %s" % image_id) - except botocore.exceptions.BotoCoreError as e: - module.fail_json_aws(e, msg="Error retrieving image attributes for image %s" % image_id) - return result - module.fail_json(msg="Invalid number of instances (%s) found for image_id: %s." % (str(len(images)), image_id)) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: - module.fail_json_aws(e, msg="Error retrieving image by image_id") - - -def rename_item_if_exists(dict_object, attribute, new_attribute, child_node=None, attribute_type=None): - new_item = dict_object.get(attribute) - if new_item is not None: - if attribute_type is not None: - new_item = attribute_type(new_item) - if child_node is None: - dict_object[new_attribute] = new_item - else: - dict_object[child_node][new_attribute] = new_item - dict_object.pop(attribute) - return dict_object - - -def main(): - argument_spec = dict( - instance_id=dict(), - image_id=dict(), - architecture=dict(default='x86_64'), - kernel_id=dict(), - virtualization_type=dict(default='hvm'), - root_device_name=dict(), - delete_snapshot=dict(default=False, type='bool'), - name=dict(), - wait=dict(type='bool', default=False), - wait_timeout=dict(default=900, type='int'), - description=dict(default=''), - no_reboot=dict(default=False, type='bool'), - state=dict(default='present', choices=['present', 'absent']), - device_mapping=dict(type='list'), - tags=dict(type='dict'), - launch_permissions=dict(type='dict'), - image_location=dict(), - enhanced_networking=dict(type='bool'), - billing_products=dict(type='list'), - ramdisk_id=dict(), - sriov_net_support=dict(), - purge_tags=dict(type='bool', default=False) - ) - - module = AnsibleAWSModule( - argument_spec=argument_spec, - required_if=[ - ['state', 'absent', ['image_id']], - ] - ) - - # Using a required_one_of=[['name', 'image_id']] overrides the message that should be provided by - # the required_if for state=absent, so check manually instead - if not any([module.params['image_id'], module.params['name']]): - module.fail_json(msg="one of the following is required: name, image_id") - - connection = module.client('ec2') - - if module.params.get('state') == 'absent': - deregister_image(module, connection) - elif module.params.get('state') == 'present': - if module.params.get('image_id'): - update_image(module, connection, module.params.get('image_id')) - if not module.params.get('instance_id') and not module.params.get('device_mapping'): - module.fail_json(msg="The parameters instance_id or device_mapping (register from EBS snapshot) are required for a new image.") - create_image(module, connection) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_ami_info.py b/lib/ansible/modules/cloud/amazon/ec2_ami_info.py deleted file mode 100644 index 41e1aa83f9..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_ami_info.py +++ /dev/null @@ -1,281 +0,0 @@ -#!/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 - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_ami_info -version_added: '2.5' -short_description: Gather information about ec2 AMIs -description: - - Gather information about ec2 AMIs - - This module was called C(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. - aliases: [image_id] - 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_DescribeImages.html) for possible filters. - - Filter names and values are case sensitive. - type: dict - owners: - description: - - Filter the images by the owner. Valid options are an AWS account ID, self, - or an AWS owner alias ( amazon | aws-marketplace | microsoft ). - aliases: [owner] - type: list - elements: str - executable_users: - description: - - Filter images by users with explicit launch permissions. Valid options are an AWS account ID, self, or all (public AMIs). - aliases: [executable_user] - type: list - elements: str - describe_image_attributes: - description: - - Describe attributes (like launchPermission) of the images found. - default: no - type: bool - -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -- name: gather information about an AMI using ami-id - ec2_ami_info: - image_ids: ami-5b488823 - -- name: gather information about all AMIs with tag key Name and value webapp - ec2_ami_info: - filters: - "tag:Name": webapp - -- name: gather information about an AMI with 'AMI Name' equal to foobar - ec2_ami_info: - filters: - name: foobar - -- name: gather information about Ubuntu 17.04 AMIs published by Canonical (099720109477) - ec2_ami_info: - owners: 099720109477 - filters: - name: "ubuntu/images/ubuntu-zesty-17.04-*" -''' - -RETURN = ''' -images: - description: A list of images. - returned: always - type: list - elements: dict - contains: - architecture: - description: The architecture of the image. - returned: always - type: str - sample: x86_64 - block_device_mappings: - description: Any block device mapping entries. - returned: always - type: list - elements: dict - contains: - device_name: - description: The device name exposed to the instance. - returned: always - type: str - sample: /dev/sda1 - ebs: - description: EBS volumes - returned: always - type: complex - creation_date: - description: The date and time the image was created. - returned: always - type: str - sample: '2017-10-16T19:22:13.000Z' - description: - description: The description of the AMI. - returned: always - type: str - sample: '' - ena_support: - description: Whether enhanced networking with ENA is enabled. - returned: always - type: bool - sample: true - hypervisor: - description: The hypervisor type of the image. - returned: always - type: str - sample: xen - image_id: - description: The ID of the AMI. - returned: always - type: str - sample: ami-5b466623 - image_location: - description: The location of the AMI. - returned: always - type: str - sample: 408466080000/Webapp - image_type: - description: The type of image. - returned: always - type: str - sample: machine - launch_permissions: - description: A List of AWS accounts may launch the AMI. - returned: When image is owned by calling account and I(describe_image_attributes) is yes. - type: list - elements: dict - contains: - group: - description: A value of 'all' means the AMI is public. - type: str - user_id: - description: An AWS account ID with permissions to launch the AMI. - type: str - sample: [{"group": "all"}, {"user_id": "408466080000"}] - name: - description: The name of the AMI that was provided during image creation. - returned: always - type: str - sample: Webapp - owner_id: - description: The AWS account ID of the image owner. - returned: always - type: str - sample: '408466080000' - public: - description: Whether the image has public launch permissions. - returned: always - type: bool - sample: true - 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 - sriov_net_support: - description: Whether enhanced networking is enabled. - returned: always - type: str - sample: simple - state: - description: The current state of the AMI. - returned: always - type: str - sample: available - tags: - description: Any tags assigned to the image. - returned: always - type: dict - virtualization_type: - description: The type of virtualization of the AMI. - returned: always - type: str - sample: hvm -''' - -try: - from botocore.exceptions import ClientError, BotoCoreError -except ImportError: - pass # caught by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import ansible_dict_to_boto3_filter_list, camel_dict_to_snake_dict, boto3_tag_list_to_ansible_dict - - -def list_ec2_images(ec2_client, module): - - image_ids = module.params.get("image_ids") - owners = module.params.get("owners") - executable_users = module.params.get("executable_users") - filters = module.params.get("filters") - owner_param = [] - - # describe_images is *very* slow if you pass the `Owners` - # param (unless it's self), for some reason. - # Converting the owners to filters and removing from the - # owners param greatly speeds things up. - # Implementation based on aioue's suggestion in #24886 - for owner in owners: - if owner.isdigit(): - if 'owner-id' not in filters: - filters['owner-id'] = list() - filters['owner-id'].append(owner) - elif owner == 'self': - # self not a valid owner-alias filter (https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeImages.html) - owner_param.append(owner) - else: - if 'owner-alias' not in filters: - filters['owner-alias'] = list() - filters['owner-alias'].append(owner) - - filters = ansible_dict_to_boto3_filter_list(filters) - - try: - images = ec2_client.describe_images(ImageIds=image_ids, Filters=filters, Owners=owner_param, ExecutableUsers=executable_users) - images = [camel_dict_to_snake_dict(image) for image in images["Images"]] - except (ClientError, BotoCoreError) as err: - module.fail_json_aws(err, msg="error describing images") - for image in images: - try: - image['tags'] = boto3_tag_list_to_ansible_dict(image.get('tags', [])) - if module.params.get("describe_image_attributes"): - launch_permissions = ec2_client.describe_image_attribute(Attribute='launchPermission', ImageId=image['image_id'])['LaunchPermissions'] - image['launch_permissions'] = [camel_dict_to_snake_dict(perm) for perm in launch_permissions] - except (ClientError, BotoCoreError) as err: - # describing launch permissions of images owned by others is not permitted, but shouldn't cause failures - pass - - images.sort(key=lambda e: e.get('creation_date', '')) # it may be possible that creation_date does not always exist - module.exit_json(images=images) - - -def main(): - - argument_spec = dict( - image_ids=dict(default=[], type='list', aliases=['image_id']), - filters=dict(default={}, type='dict'), - owners=dict(default=[], type='list', aliases=['owner']), - executable_users=dict(default=[], type='list', aliases=['executable_user']), - describe_image_attributes=dict(default=False, type='bool') - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) - if module._module._name == 'ec2_ami_facts': - module._module.deprecate("The 'ec2_ami_facts' module has been renamed to 'ec2_ami_info'", version='2.13') - - ec2_client = module.client('ec2') - - list_ec2_images(ec2_client, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_elb_lb.py b/lib/ansible/modules/cloud/amazon/ec2_elb_lb.py deleted file mode 100644 index 0e7ed5456e..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_elb_lb.py +++ /dev/null @@ -1,1365 +0,0 @@ -#!/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 - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = """ ---- -module: ec2_elb_lb -description: - - Returns information about the load balancer. - - Will be marked changed when called only if state is changed. -short_description: Creates, updates or destroys an Amazon ELB. -version_added: "1.5" -author: - - "Jim Dalton (@jsdalton)" -options: - state: - description: - - Create or destroy the ELB - type: str - choices: [ absent, present ] - required: true - name: - description: - - The name of the ELB - type: str - required: true - listeners: - description: - - List of ports/protocols for this ELB to listen on (see example) - type: list - purge_listeners: - description: - - Purge existing listeners on ELB that are not found in listeners - type: bool - default: yes - instance_ids: - description: - - List of instance ids to attach to this ELB - type: list - version_added: "2.1" - purge_instance_ids: - description: - - Purge existing instance ids on ELB that are not found in instance_ids - type: bool - default: no - version_added: "2.1" - zones: - description: - - List of availability zones to enable on this ELB - type: list - purge_zones: - description: - - Purge existing availability zones on ELB that are not found in zones - type: bool - default: no - security_group_ids: - description: - - A list of security groups to apply to the elb - type: list - version_added: "1.6" - security_group_names: - description: - - A list of security group names to apply to the elb - type: list - version_added: "2.0" - health_check: - description: - - An associative array of health check configuration settings (see example) - type: dict - access_logs: - description: - - An associative array of access logs configuration settings (see example) - type: dict - version_added: "2.0" - subnets: - description: - - A list of VPC subnets to use when creating ELB. Zones should be empty if using this. - type: list - version_added: "1.7" - purge_subnets: - description: - - Purge existing subnet on ELB that are not found in subnets - type: bool - default: no - version_added: "1.7" - scheme: - description: - - The scheme to use when creating the ELB. For a private VPC-visible ELB use 'internal'. - If you choose to update your scheme with a different value the ELB will be destroyed and - recreated. To update scheme you must use the option wait. - type: str - choices: ["internal", "internet-facing"] - default: 'internet-facing' - version_added: "1.7" - validate_certs: - description: - - When set to C(no), SSL certificates will not be validated for boto versions >= 2.6.0. - type: bool - default: yes - version_added: "1.5" - connection_draining_timeout: - description: - - Wait a specified timeout allowing connections to drain before terminating an instance - type: int - version_added: "1.8" - idle_timeout: - description: - - ELB connections from clients and to servers are timed out after this amount of time - type: int - version_added: "2.0" - cross_az_load_balancing: - description: - - Distribute load across all configured Availability Zones - type: bool - default: no - version_added: "1.8" - stickiness: - description: - - An associative array of stickiness policy settings. Policy will be applied to all listeners ( see example ) - type: dict - version_added: "2.0" - wait: - description: - - When specified, Ansible will check the status of the load balancer to ensure it has been successfully - removed from AWS. - type: bool - default: no - version_added: "2.1" - wait_timeout: - description: - - Used in conjunction with wait. Number of seconds to wait for the elb to be terminated. - A maximum of 600 seconds (10 minutes) is allowed. - type: int - default: 60 - version_added: "2.1" - tags: - description: - - An associative array of tags. To delete all tags, supply an empty dict. - type: dict - version_added: "2.1" - -extends_documentation_fragment: - - aws - - ec2 -""" - -EXAMPLES = """ -# Note: None of these examples set aws_access_key, aws_secret_key, or region. -# It is assumed that their matching environment variables are set. - -# Basic provisioning example (non-VPC) - -- local_action: - module: ec2_elb_lb - name: "test-please-delete" - state: present - zones: - - us-east-1a - - us-east-1d - listeners: - - protocol: http # options are http, https, ssl, tcp - load_balancer_port: 80 - instance_port: 80 - proxy_protocol: True - - protocol: https - load_balancer_port: 443 - instance_protocol: http # optional, defaults to value of protocol setting - instance_port: 80 - # ssl certificate required for https or ssl - ssl_certificate_id: "arn:aws:iam::123456789012:server-certificate/company/servercerts/ProdServerCert" - -# Internal ELB example - -- local_action: - module: ec2_elb_lb - name: "test-vpc" - scheme: internal - state: present - instance_ids: - - i-abcd1234 - purge_instance_ids: true - subnets: - - subnet-abcd1234 - - subnet-1a2b3c4d - listeners: - - protocol: http # options are http, https, ssl, tcp - load_balancer_port: 80 - instance_port: 80 - -# Configure a health check and the access logs -- local_action: - module: ec2_elb_lb - name: "test-please-delete" - state: present - zones: - - us-east-1d - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - health_check: - ping_protocol: http # options are http, https, ssl, tcp - ping_port: 80 - ping_path: "/index.html" # not required for tcp or ssl - response_timeout: 5 # seconds - interval: 30 # seconds - unhealthy_threshold: 2 - healthy_threshold: 10 - access_logs: - interval: 5 # minutes (defaults to 60) - s3_location: "my-bucket" # This value is required if access_logs is set - s3_prefix: "logs" - -# Ensure ELB is gone -- local_action: - module: ec2_elb_lb - name: "test-please-delete" - state: absent - -# Ensure ELB is gone and wait for check (for default timeout) -- local_action: - module: ec2_elb_lb - name: "test-please-delete" - state: absent - wait: yes - -# Ensure ELB is gone and wait for check with timeout value -- local_action: - module: ec2_elb_lb - name: "test-please-delete" - state: absent - wait: yes - wait_timeout: 600 - -# Normally, this module will purge any listeners that exist on the ELB -# but aren't specified in the listeners parameter. If purge_listeners is -# false it leaves them alone -- local_action: - module: ec2_elb_lb - name: "test-please-delete" - state: present - zones: - - us-east-1a - - us-east-1d - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - purge_listeners: no - -# Normally, this module will leave availability zones that are enabled -# on the ELB alone. If purge_zones is true, then any extraneous zones -# will be removed -- local_action: - module: ec2_elb_lb - name: "test-please-delete" - state: present - zones: - - us-east-1a - - us-east-1d - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - purge_zones: yes - -# Creates a ELB and assigns a list of subnets to it. -- local_action: - module: ec2_elb_lb - state: present - name: 'New ELB' - security_group_ids: 'sg-123456, sg-67890' - region: us-west-2 - subnets: 'subnet-123456,subnet-67890' - purge_subnets: yes - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - -# Create an ELB with connection draining, increased idle timeout and cross availability -# zone load balancing -- local_action: - module: ec2_elb_lb - name: "New ELB" - state: present - connection_draining_timeout: 60 - idle_timeout: 300 - cross_az_load_balancing: "yes" - region: us-east-1 - zones: - - us-east-1a - - us-east-1d - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - -# Create an ELB with load balancer stickiness enabled -- local_action: - module: ec2_elb_lb - name: "New ELB" - state: present - region: us-east-1 - zones: - - us-east-1a - - us-east-1d - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - stickiness: - type: loadbalancer - enabled: yes - expiration: 300 - -# Create an ELB with application stickiness enabled -- local_action: - module: ec2_elb_lb - name: "New ELB" - state: present - region: us-east-1 - zones: - - us-east-1a - - us-east-1d - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - stickiness: - type: application - enabled: yes - cookie: SESSIONID - -# Create an ELB and add tags -- local_action: - module: ec2_elb_lb - name: "New ELB" - state: present - region: us-east-1 - zones: - - us-east-1a - - us-east-1d - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - tags: - Name: "New ELB" - stack: "production" - client: "Bob" - -# Delete all tags from an ELB -- local_action: - module: ec2_elb_lb - name: "New ELB" - state: present - region: us-east-1 - zones: - - us-east-1a - - us-east-1d - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - tags: {} -""" - -import random -import time -import traceback - -try: - import boto - import boto.ec2.elb - import boto.ec2.elb.attributes - import boto.vpc - from boto.ec2.elb.healthcheck import HealthCheck - from boto.ec2.tag import Tag - HAS_BOTO = True -except ImportError: - HAS_BOTO = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import ec2_argument_spec, connect_to_aws, AnsibleAWSError, get_aws_connection_info -from ansible.module_utils.six import string_types -from ansible.module_utils._text import to_native - - -def _throttleable_operation(max_retries): - def _operation_wrapper(op): - def _do_op(*args, **kwargs): - retry = 0 - while True: - try: - return op(*args, **kwargs) - except boto.exception.BotoServerError as e: - if retry < max_retries and e.code in \ - ("Throttling", "RequestLimitExceeded"): - retry = retry + 1 - time.sleep(min(random.random() * (2 ** retry), 300)) - continue - else: - raise - return _do_op - return _operation_wrapper - - -def _get_vpc_connection(module, region, aws_connect_params): - try: - return connect_to_aws(boto.vpc, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: - module.fail_json(msg=str(e)) - - -_THROTTLING_RETRIES = 5 - - -class ElbManager(object): - """Handles ELB creation and destruction""" - - def __init__(self, module, name, listeners=None, purge_listeners=None, - zones=None, purge_zones=None, security_group_ids=None, - health_check=None, subnets=None, purge_subnets=None, - scheme="internet-facing", connection_draining_timeout=None, - idle_timeout=None, - cross_az_load_balancing=None, access_logs=None, - stickiness=None, wait=None, wait_timeout=None, tags=None, - region=None, - instance_ids=None, purge_instance_ids=None, **aws_connect_params): - - self.module = module - self.name = name - self.listeners = listeners - self.purge_listeners = purge_listeners - self.instance_ids = instance_ids - self.purge_instance_ids = purge_instance_ids - self.zones = zones - self.purge_zones = purge_zones - self.security_group_ids = security_group_ids - self.health_check = health_check - self.subnets = subnets - self.purge_subnets = purge_subnets - self.scheme = scheme - self.connection_draining_timeout = connection_draining_timeout - self.idle_timeout = idle_timeout - self.cross_az_load_balancing = cross_az_load_balancing - self.access_logs = access_logs - self.stickiness = stickiness - self.wait = wait - self.wait_timeout = wait_timeout - self.tags = tags - - self.aws_connect_params = aws_connect_params - self.region = region - - self.changed = False - self.status = 'gone' - self.elb_conn = self._get_elb_connection() - - try: - self.elb = self._get_elb() - except boto.exception.BotoServerError as e: - module.fail_json(msg='unable to get all load balancers: %s' % e.message, exception=traceback.format_exc()) - - self.ec2_conn = self._get_ec2_connection() - - @_throttleable_operation(_THROTTLING_RETRIES) - def ensure_ok(self): - """Create the ELB""" - if not self.elb: - # Zones and listeners will be added at creation - self._create_elb() - else: - if self._get_scheme(): - # the only way to change the scheme is by recreating the resource - self.ensure_gone() - self._create_elb() - else: - self._set_zones() - self._set_security_groups() - self._set_elb_listeners() - self._set_subnets() - self._set_health_check() - # boto has introduced support for some ELB attributes in - # different versions, so we check first before trying to - # set them to avoid errors - if self._check_attribute_support('connection_draining'): - self._set_connection_draining_timeout() - if self._check_attribute_support('connecting_settings'): - self._set_idle_timeout() - if self._check_attribute_support('cross_zone_load_balancing'): - self._set_cross_az_load_balancing() - if self._check_attribute_support('access_log'): - self._set_access_log() - # add sticky options - self.select_stickiness_policy() - - # ensure backend server policies are correct - self._set_backend_policies() - # set/remove instance ids - self._set_instance_ids() - - self._set_tags() - - def ensure_gone(self): - """Destroy the ELB""" - if self.elb: - self._delete_elb() - if self.wait: - elb_removed = self._wait_for_elb_removed() - # Unfortunately even though the ELB itself is removed quickly - # the interfaces take longer so reliant security groups cannot - # be deleted until the interface has registered as removed. - elb_interface_removed = self._wait_for_elb_interface_removed() - if not (elb_removed and elb_interface_removed): - self.module.fail_json(msg='Timed out waiting for removal of load balancer.') - - def get_info(self): - try: - check_elb = self.elb_conn.get_all_load_balancers(self.name)[0] - except Exception: - check_elb = None - - if not check_elb: - info = { - 'name': self.name, - 'status': self.status, - 'region': self.region - } - else: - try: - lb_cookie_policy = check_elb.policies.lb_cookie_stickiness_policies[0].__dict__['policy_name'] - except Exception: - lb_cookie_policy = None - try: - app_cookie_policy = check_elb.policies.app_cookie_stickiness_policies[0].__dict__['policy_name'] - except Exception: - app_cookie_policy = None - - info = { - 'name': check_elb.name, - 'dns_name': check_elb.dns_name, - 'zones': check_elb.availability_zones, - 'security_group_ids': check_elb.security_groups, - 'status': self.status, - 'subnets': self.subnets, - 'scheme': check_elb.scheme, - 'hosted_zone_name': check_elb.canonical_hosted_zone_name, - 'hosted_zone_id': check_elb.canonical_hosted_zone_name_id, - 'lb_cookie_policy': lb_cookie_policy, - 'app_cookie_policy': app_cookie_policy, - 'proxy_policy': self._get_proxy_protocol_policy(), - 'backends': self._get_backend_policies(), - 'instances': [instance.id for instance in check_elb.instances], - 'out_of_service_count': 0, - 'in_service_count': 0, - 'unknown_instance_state_count': 0, - 'region': self.region - } - - # status of instances behind the ELB - if info['instances']: - info['instance_health'] = [dict( - instance_id=instance_state.instance_id, - reason_code=instance_state.reason_code, - state=instance_state.state - ) for instance_state in self.elb_conn.describe_instance_health(self.name)] - else: - info['instance_health'] = [] - - # instance state counts: InService or OutOfService - if info['instance_health']: - for instance_state in info['instance_health']: - if instance_state['state'] == "InService": - info['in_service_count'] += 1 - elif instance_state['state'] == "OutOfService": - info['out_of_service_count'] += 1 - else: - info['unknown_instance_state_count'] += 1 - - if check_elb.health_check: - info['health_check'] = { - 'target': check_elb.health_check.target, - 'interval': check_elb.health_check.interval, - 'timeout': check_elb.health_check.timeout, - 'healthy_threshold': check_elb.health_check.healthy_threshold, - 'unhealthy_threshold': check_elb.health_check.unhealthy_threshold, - } - - if check_elb.listeners: - info['listeners'] = [self._api_listener_as_tuple(l) - for l in check_elb.listeners] - elif self.status == 'created': - # When creating a new ELB, listeners don't show in the - # immediately returned result, so just include the - # ones that were added - info['listeners'] = [self._listener_as_tuple(l) - for l in self.listeners] - else: - info['listeners'] = [] - - if self._check_attribute_support('connection_draining'): - info['connection_draining_timeout'] = int(self.elb_conn.get_lb_attribute(self.name, 'ConnectionDraining').timeout) - - if self._check_attribute_support('connecting_settings'): - info['idle_timeout'] = self.elb_conn.get_lb_attribute(self.name, 'ConnectingSettings').idle_timeout - - if self._check_attribute_support('cross_zone_load_balancing'): - is_cross_az_lb_enabled = self.elb_conn.get_lb_attribute(self.name, 'CrossZoneLoadBalancing') - if is_cross_az_lb_enabled: - info['cross_az_load_balancing'] = 'yes' - else: - info['cross_az_load_balancing'] = 'no' - - # return stickiness info? - - info['tags'] = self.tags - - return info - - @_throttleable_operation(_THROTTLING_RETRIES) - def _wait_for_elb_removed(self): - polling_increment_secs = 15 - max_retries = (self.wait_timeout // polling_increment_secs) - status_achieved = False - - for x in range(0, max_retries): - try: - self.elb_conn.get_all_lb_attributes(self.name) - except (boto.exception.BotoServerError, Exception) as e: - if "LoadBalancerNotFound" in e.code: - status_achieved = True - break - else: - time.sleep(polling_increment_secs) - - return status_achieved - - @_throttleable_operation(_THROTTLING_RETRIES) - def _wait_for_elb_interface_removed(self): - polling_increment_secs = 15 - max_retries = (self.wait_timeout // polling_increment_secs) - status_achieved = False - - elb_interfaces = self.ec2_conn.get_all_network_interfaces( - filters={'attachment.instance-owner-id': 'amazon-elb', - 'description': 'ELB {0}'.format(self.name)}) - - for x in range(0, max_retries): - for interface in elb_interfaces: - try: - result = self.ec2_conn.get_all_network_interfaces(interface.id) - if result == []: - status_achieved = True - break - else: - time.sleep(polling_increment_secs) - except (boto.exception.BotoServerError, Exception) as e: - if 'InvalidNetworkInterfaceID' in e.code: - status_achieved = True - break - else: - self.module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - return status_achieved - - @_throttleable_operation(_THROTTLING_RETRIES) - def _get_elb(self): - elbs = self.elb_conn.get_all_load_balancers() - for elb in elbs: - if self.name == elb.name: - self.status = 'ok' - return elb - - def _get_elb_connection(self): - try: - return connect_to_aws(boto.ec2.elb, self.region, - **self.aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: - self.module.fail_json(msg=str(e)) - - def _get_ec2_connection(self): - try: - return connect_to_aws(boto.ec2, self.region, - **self.aws_connect_params) - except (boto.exception.NoAuthHandlerFound, Exception) as e: - self.module.fail_json(msg=to_native(e), exception=traceback.format_exc()) - - @_throttleable_operation(_THROTTLING_RETRIES) - def _delete_elb(self): - # True if succeeds, exception raised if not - result = self.elb_conn.delete_load_balancer(name=self.name) - if result: - self.changed = True - self.status = 'deleted' - - def _create_elb(self): - listeners = [self._listener_as_tuple(l) for l in self.listeners] - self.elb = self.elb_conn.create_load_balancer(name=self.name, - zones=self.zones, - security_groups=self.security_group_ids, - complex_listeners=listeners, - subnets=self.subnets, - scheme=self.scheme) - if self.elb: - # HACK: Work around a boto bug in which the listeners attribute is - # always set to the listeners argument to create_load_balancer, and - # not the complex_listeners - # We're not doing a self.elb = self._get_elb here because there - # might be eventual consistency issues and it doesn't necessarily - # make sense to wait until the ELB gets returned from the EC2 API. - # This is necessary in the event we hit the throttling errors and - # need to retry ensure_ok - # See https://github.com/boto/boto/issues/3526 - self.elb.listeners = self.listeners - self.changed = True - self.status = 'created' - - def _create_elb_listeners(self, listeners): - """Takes a list of listener tuples and creates them""" - # True if succeeds, exception raised if not - self.changed = self.elb_conn.create_load_balancer_listeners(self.name, - complex_listeners=listeners) - - def _delete_elb_listeners(self, listeners): - """Takes a list of listener tuples and deletes them from the elb""" - ports = [l[0] for l in listeners] - - # True if succeeds, exception raised if not - self.changed = self.elb_conn.delete_load_balancer_listeners(self.name, - ports) - - def _set_elb_listeners(self): - """ - Creates listeners specified by self.listeners; overwrites existing - listeners on these ports; removes extraneous listeners - """ - listeners_to_add = [] - listeners_to_remove = [] - listeners_to_keep = [] - - # Check for any listeners we need to create or overwrite - for listener in self.listeners: - listener_as_tuple = self._listener_as_tuple(listener) - - # First we loop through existing listeners to see if one is - # already specified for this port - existing_listener_found = None - for existing_listener in self.elb.listeners: - # Since ELB allows only one listener on each incoming port, a - # single match on the incoming port is all we're looking for - if existing_listener[0] == int(listener['load_balancer_port']): - existing_listener_found = self._api_listener_as_tuple(existing_listener) - break - - if existing_listener_found: - # Does it match exactly? - if listener_as_tuple != existing_listener_found: - # The ports are the same but something else is different, - # so we'll remove the existing one and add the new one - listeners_to_remove.append(existing_listener_found) - listeners_to_add.append(listener_as_tuple) - else: - # We already have this listener, so we're going to keep it - listeners_to_keep.append(existing_listener_found) - else: - # We didn't find an existing listener, so just add the new one - listeners_to_add.append(listener_as_tuple) - - # Check for any extraneous listeners we need to remove, if desired - if self.purge_listeners: - for existing_listener in self.elb.listeners: - existing_listener_tuple = self._api_listener_as_tuple(existing_listener) - if existing_listener_tuple in listeners_to_remove: - # Already queued for removal - continue - if existing_listener_tuple in listeners_to_keep: - # Keep this one around - continue - # Since we're not already removing it and we don't need to keep - # it, let's get rid of it - listeners_to_remove.append(existing_listener_tuple) - - if listeners_to_remove: - self._delete_elb_listeners(listeners_to_remove) - - if listeners_to_add: - self._create_elb_listeners(listeners_to_add) - - def _api_listener_as_tuple(self, listener): - """Adds ssl_certificate_id to ELB API tuple if present""" - base_tuple = listener.get_complex_tuple() - if listener.ssl_certificate_id and len(base_tuple) < 5: - return base_tuple + (listener.ssl_certificate_id,) - return base_tuple - - def _listener_as_tuple(self, listener): - """Formats listener as a 4- or 5-tuples, in the order specified by the - ELB API""" - # N.B. string manipulations on protocols below (str(), upper()) is to - # ensure format matches output from ELB API - listener_list = [ - int(listener['load_balancer_port']), - int(listener['instance_port']), - str(listener['protocol'].upper()), - ] - - # Instance protocol is not required by ELB API; it defaults to match - # load balancer protocol. We'll mimic that behavior here - if 'instance_protocol' in listener: - listener_list.append(str(listener['instance_protocol'].upper())) - else: - listener_list.append(str(listener['protocol'].upper())) - - if 'ssl_certificate_id' in listener: - listener_list.append(str(listener['ssl_certificate_id'])) - - return tuple(listener_list) - - def _enable_zones(self, zones): - try: - self.elb.enable_zones(zones) - except boto.exception.BotoServerError as e: - self.module.fail_json(msg='unable to enable zones: %s' % e.message, exception=traceback.format_exc()) - - self.changed = True - - def _disable_zones(self, zones): - try: - self.elb.disable_zones(zones) - except boto.exception.BotoServerError as e: - self.module.fail_json(msg='unable to disable zones: %s' % e.message, exception=traceback.format_exc()) - self.changed = True - - def _attach_subnets(self, subnets): - self.elb_conn.attach_lb_to_subnets(self.name, subnets) - self.changed = True - - def _detach_subnets(self, subnets): - self.elb_conn.detach_lb_from_subnets(self.name, subnets) - self.changed = True - - def _set_subnets(self): - """Determine which subnets need to be attached or detached on the ELB""" - if self.subnets: - if self.purge_subnets: - subnets_to_detach = list(set(self.elb.subnets) - set(self.subnets)) - subnets_to_attach = list(set(self.subnets) - set(self.elb.subnets)) - else: - subnets_to_detach = None - subnets_to_attach = list(set(self.subnets) - set(self.elb.subnets)) - - if subnets_to_attach: - self._attach_subnets(subnets_to_attach) - if subnets_to_detach: - self._detach_subnets(subnets_to_detach) - - def _get_scheme(self): - """Determine if the current scheme is different than the scheme of the ELB""" - if self.scheme: - if self.elb.scheme != self.scheme: - if not self.wait: - self.module.fail_json(msg="Unable to modify scheme without using the wait option") - return True - return False - - def _set_zones(self): - """Determine which zones need to be enabled or disabled on the ELB""" - if self.zones: - if self.purge_zones: - zones_to_disable = list(set(self.elb.availability_zones) - - set(self.zones)) - zones_to_enable = list(set(self.zones) - - set(self.elb.availability_zones)) - else: - zones_to_disable = None - zones_to_enable = list(set(self.zones) - - set(self.elb.availability_zones)) - if zones_to_enable: - self._enable_zones(zones_to_enable) - # N.B. This must come second, in case it would have removed all zones - if zones_to_disable: - self._disable_zones(zones_to_disable) - - def _set_security_groups(self): - if self.security_group_ids is not None and set(self.elb.security_groups) != set(self.security_group_ids): - self.elb_conn.apply_security_groups_to_lb(self.name, self.security_group_ids) - self.changed = True - - def _set_health_check(self): - """Set health check values on ELB as needed""" - if self.health_check: - # This just makes it easier to compare each of the attributes - # and look for changes. Keys are attributes of the current - # health_check; values are desired values of new health_check - health_check_config = { - "target": self._get_health_check_target(), - "timeout": self.health_check['response_timeout'], - "interval": self.health_check['interval'], - "unhealthy_threshold": self.health_check['unhealthy_threshold'], - "healthy_threshold": self.health_check['healthy_threshold'], - } - - update_health_check = False - - # The health_check attribute is *not* set on newly created - # ELBs! So we have to create our own. - if not self.elb.health_check: - self.elb.health_check = HealthCheck() - - for attr, desired_value in health_check_config.items(): - if getattr(self.elb.health_check, attr) != desired_value: - setattr(self.elb.health_check, attr, desired_value) - update_health_check = True - - if update_health_check: - self.elb.configure_health_check(self.elb.health_check) - self.changed = True - - def _check_attribute_support(self, attr): - return hasattr(boto.ec2.elb.attributes.LbAttributes(), attr) - - def _set_cross_az_load_balancing(self): - attributes = self.elb.get_attributes() - if self.cross_az_load_balancing: - if not attributes.cross_zone_load_balancing.enabled: - self.changed = True - attributes.cross_zone_load_balancing.enabled = True - else: - if attributes.cross_zone_load_balancing.enabled: - self.changed = True - attributes.cross_zone_load_balancing.enabled = False - self.elb_conn.modify_lb_attribute(self.name, 'CrossZoneLoadBalancing', - attributes.cross_zone_load_balancing.enabled) - - def _set_access_log(self): - attributes = self.elb.get_attributes() - if self.access_logs: - if 's3_location' not in self.access_logs: - self.module.fail_json(msg='s3_location information required') - - access_logs_config = { - "enabled": True, - "s3_bucket_name": self.access_logs['s3_location'], - "s3_bucket_prefix": self.access_logs.get('s3_prefix', ''), - "emit_interval": self.access_logs.get('interval', 60), - } - - update_access_logs_config = False - for attr, desired_value in access_logs_config.items(): - if getattr(attributes.access_log, attr) != desired_value: - setattr(attributes.access_log, attr, desired_value) - update_access_logs_config = True - if update_access_logs_config: - self.elb_conn.modify_lb_attribute(self.name, 'AccessLog', attributes.access_log) - self.changed = True - elif attributes.access_log.enabled: - attributes.access_log.enabled = False - self.changed = True - self.elb_conn.modify_lb_attribute(self.name, 'AccessLog', attributes.access_log) - - def _set_connection_draining_timeout(self): - attributes = self.elb.get_attributes() - if self.connection_draining_timeout is not None: - if not attributes.connection_draining.enabled or \ - attributes.connection_draining.timeout != self.connection_draining_timeout: - self.changed = True - attributes.connection_draining.enabled = True - attributes.connection_draining.timeout = self.connection_draining_timeout - self.elb_conn.modify_lb_attribute(self.name, 'ConnectionDraining', attributes.connection_draining) - else: - if attributes.connection_draining.enabled: - self.changed = True - attributes.connection_draining.enabled = False - self.elb_conn.modify_lb_attribute(self.name, 'ConnectionDraining', attributes.connection_draining) - - def _set_idle_timeout(self): - attributes = self.elb.get_attributes() - if self.idle_timeout is not None: - if attributes.connecting_settings.idle_timeout != self.idle_timeout: - self.changed = True - attributes.connecting_settings.idle_timeout = self.idle_timeout - self.elb_conn.modify_lb_attribute(self.name, 'ConnectingSettings', attributes.connecting_settings) - - def _policy_name(self, policy_type): - return 'ec2-elb-lb-{0}'.format(to_native(policy_type, errors='surrogate_or_strict')) - - def _create_policy(self, policy_param, policy_meth, policy): - getattr(self.elb_conn, policy_meth)(policy_param, self.elb.name, policy) - - def _delete_policy(self, elb_name, policy): - self.elb_conn.delete_lb_policy(elb_name, policy) - - def _update_policy(self, policy_param, policy_meth, policy_attr, policy): - self._delete_policy(self.elb.name, policy) - self._create_policy(policy_param, policy_meth, policy) - - def _set_listener_policy(self, listeners_dict, policy=None): - policy = [] if policy is None else policy - - for listener_port in listeners_dict: - if listeners_dict[listener_port].startswith('HTTP'): - self.elb_conn.set_lb_policies_of_listener(self.elb.name, listener_port, policy) - - def _set_stickiness_policy(self, elb_info, listeners_dict, policy, **policy_attrs): - for p in getattr(elb_info.policies, policy_attrs['attr']): - if str(p.__dict__['policy_name']) == str(policy[0]): - if str(p.__dict__[policy_attrs['dict_key']]) != str(policy_attrs['param_value'] or 0): - self._set_listener_policy(listeners_dict) - self._update_policy(policy_attrs['param_value'], policy_attrs['method'], policy_attrs['attr'], policy[0]) - self.changed = True - break - else: - self._create_policy(policy_attrs['param_value'], policy_attrs['method'], policy[0]) - self.changed = True - - self._set_listener_policy(listeners_dict, policy) - - def select_stickiness_policy(self): - if self.stickiness: - - if 'cookie' in self.stickiness and 'expiration' in self.stickiness: - self.module.fail_json(msg='\'cookie\' and \'expiration\' can not be set at the same time') - - elb_info = self.elb_conn.get_all_load_balancers(self.elb.name)[0] - d = {} - for listener in elb_info.listeners: - d[listener[0]] = listener[2] - listeners_dict = d - - if self.stickiness['type'] == 'loadbalancer': - policy = [] - policy_type = 'LBCookieStickinessPolicyType' - - if self.module.boolean(self.stickiness['enabled']): - - if 'expiration' not in self.stickiness: - self.module.fail_json(msg='expiration must be set when type is loadbalancer') - - try: - expiration = self.stickiness['expiration'] if int(self.stickiness['expiration']) else None - except ValueError: - self.module.fail_json(msg='expiration must be set to an integer') - - policy_attrs = { - 'type': policy_type, - 'attr': 'lb_cookie_stickiness_policies', - 'method': 'create_lb_cookie_stickiness_policy', - 'dict_key': 'cookie_expiration_period', - 'param_value': expiration - } - policy.append(self._policy_name(policy_attrs['type'])) - - self._set_stickiness_policy(elb_info, listeners_dict, policy, **policy_attrs) - elif not self.module.boolean(self.stickiness['enabled']): - if len(elb_info.policies.lb_cookie_stickiness_policies): - if elb_info.policies.lb_cookie_stickiness_policies[0].policy_name == self._policy_name(policy_type): - self.changed = True - else: - self.changed = False - self._set_listener_policy(listeners_dict) - self._delete_policy(self.elb.name, self._policy_name(policy_type)) - - elif self.stickiness['type'] == 'application': - policy = [] - policy_type = 'AppCookieStickinessPolicyType' - if self.module.boolean(self.stickiness['enabled']): - - if 'cookie' not in self.stickiness: - self.module.fail_json(msg='cookie must be set when type is application') - - policy_attrs = { - 'type': policy_type, - 'attr': 'app_cookie_stickiness_policies', - 'method': 'create_app_cookie_stickiness_policy', - 'dict_key': 'cookie_name', - 'param_value': self.stickiness['cookie'] - } - policy.append(self._policy_name(policy_attrs['type'])) - self._set_stickiness_policy(elb_info, listeners_dict, policy, **policy_attrs) - elif not self.module.boolean(self.stickiness['enabled']): - if len(elb_info.policies.app_cookie_stickiness_policies): - if elb_info.policies.app_cookie_stickiness_policies[0].policy_name == self._policy_name(policy_type): - self.changed = True - self._set_listener_policy(listeners_dict) - self._delete_policy(self.elb.name, self._policy_name(policy_type)) - - else: - self._set_listener_policy(listeners_dict) - - def _get_backend_policies(self): - """Get a list of backend policies""" - policies = [] - if self.elb.backends is not None: - for backend in self.elb.backends: - if backend.policies is not None: - for policy in backend.policies: - policies.append(str(backend.instance_port) + ':' + policy.policy_name) - - return policies - - def _set_backend_policies(self): - """Sets policies for all backends""" - ensure_proxy_protocol = False - replace = [] - backend_policies = self._get_backend_policies() - - # Find out what needs to be changed - for listener in self.listeners: - want = False - - if 'proxy_protocol' in listener and listener['proxy_protocol']: - ensure_proxy_protocol = True - want = True - - if str(listener['instance_port']) + ':ProxyProtocol-policy' in backend_policies: - if not want: - replace.append({'port': listener['instance_port'], 'policies': []}) - elif want: - replace.append({'port': listener['instance_port'], 'policies': ['ProxyProtocol-policy']}) - - # enable or disable proxy protocol - if ensure_proxy_protocol: - self._set_proxy_protocol_policy() - - # Make the backend policies so - for item in replace: - self.elb_conn.set_lb_policies_of_backend_server(self.elb.name, item['port'], item['policies']) - self.changed = True - - def _get_proxy_protocol_policy(self): - """Find out if the elb has a proxy protocol enabled""" - if self.elb.policies is not None and self.elb.policies.other_policies is not None: - for policy in self.elb.policies.other_policies: - if policy.policy_name == 'ProxyProtocol-policy': - return policy.policy_name - - return None - - def _set_proxy_protocol_policy(self): - """Install a proxy protocol policy if needed""" - proxy_policy = self._get_proxy_protocol_policy() - - if proxy_policy is None: - self.elb_conn.create_lb_policy( - self.elb.name, 'ProxyProtocol-policy', 'ProxyProtocolPolicyType', {'ProxyProtocol': True} - ) - self.changed = True - - # TODO: remove proxy protocol policy if not needed anymore? There is no side effect to leaving it there - - def _diff_list(self, a, b): - """Find the entries in list a that are not in list b""" - b = set(b) - return [aa for aa in a if aa not in b] - - def _get_instance_ids(self): - """Get the current list of instance ids installed in the elb""" - instances = [] - if self.elb.instances is not None: - for instance in self.elb.instances: - instances.append(instance.id) - - return instances - - def _set_instance_ids(self): - """Register or deregister instances from an lb instance""" - assert_instances = self.instance_ids or [] - - has_instances = self._get_instance_ids() - - add_instances = self._diff_list(assert_instances, has_instances) - if add_instances: - self.elb_conn.register_instances(self.elb.name, add_instances) - self.changed = True - - if self.purge_instance_ids: - remove_instances = self._diff_list(has_instances, assert_instances) - if remove_instances: - self.elb_conn.deregister_instances(self.elb.name, remove_instances) - self.changed = True - - def _set_tags(self): - """Add/Delete tags""" - if self.tags is None: - return - - params = {'LoadBalancerNames.member.1': self.name} - - tagdict = dict() - - # get the current list of tags from the ELB, if ELB exists - if self.elb: - current_tags = self.elb_conn.get_list('DescribeTags', params, - [('member', Tag)]) - tagdict = dict((tag.Key, tag.Value) for tag in current_tags - if hasattr(tag, 'Key')) - - # Add missing tags - dictact = dict(set(self.tags.items()) - set(tagdict.items())) - if dictact: - for i, key in enumerate(dictact): - params['Tags.member.%d.Key' % (i + 1)] = key - params['Tags.member.%d.Value' % (i + 1)] = dictact[key] - - self.elb_conn.make_request('AddTags', params) - self.changed = True - - # Remove extra tags - dictact = dict(set(tagdict.items()) - set(self.tags.items())) - if dictact: - for i, key in enumerate(dictact): - params['Tags.member.%d.Key' % (i + 1)] = key - - self.elb_conn.make_request('RemoveTags', params) - self.changed = True - - def _get_health_check_target(self): - """Compose target string from healthcheck parameters""" - protocol = self.health_check['ping_protocol'].upper() - path = "" - - if protocol in ['HTTP', 'HTTPS'] and 'ping_path' in self.health_check: - path = self.health_check['ping_path'] - - return "%s:%s%s" % (protocol, self.health_check['ping_port'], path) - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - state={'required': True, 'choices': ['present', 'absent']}, - name={'required': True}, - listeners={'default': None, 'required': False, 'type': 'list'}, - purge_listeners={'default': True, 'required': False, 'type': 'bool'}, - instance_ids={'default': None, 'required': False, 'type': 'list'}, - purge_instance_ids={'default': False, 'required': False, 'type': 'bool'}, - zones={'default': None, 'required': False, 'type': 'list'}, - purge_zones={'default': False, 'required': False, 'type': 'bool'}, - security_group_ids={'default': None, 'required': False, 'type': 'list'}, - security_group_names={'default': None, 'required': False, 'type': 'list'}, - health_check={'default': None, 'required': False, 'type': 'dict'}, - subnets={'default': None, 'required': False, 'type': 'list'}, - purge_subnets={'default': False, 'required': False, 'type': 'bool'}, - scheme={'default': 'internet-facing', 'required': False, 'choices': ['internal', 'internet-facing']}, - connection_draining_timeout={'default': None, 'required': False, 'type': 'int'}, - idle_timeout={'default': None, 'type': 'int', 'required': False}, - cross_az_load_balancing={'default': None, 'type': 'bool', 'required': False}, - stickiness={'default': None, 'required': False, 'type': 'dict'}, - access_logs={'default': None, 'required': False, 'type': 'dict'}, - wait={'default': False, 'type': 'bool', 'required': False}, - wait_timeout={'default': 60, 'type': 'int', 'required': False}, - tags={'default': None, 'required': False, 'type': 'dict'} - ) - ) - - module = AnsibleModule( - argument_spec=argument_spec, - mutually_exclusive=[['security_group_ids', 'security_group_names']] - ) - - if not HAS_BOTO: - module.fail_json(msg='boto required for this module') - - region, ec2_url, aws_connect_params = get_aws_connection_info(module) - if not region: - module.fail_json(msg="Region must be specified as a parameter, in EC2_REGION or AWS_REGION environment variables or in boto configuration file") - - name = module.params['name'] - state = module.params['state'] - listeners = module.params['listeners'] - purge_listeners = module.params['purge_listeners'] - instance_ids = module.params['instance_ids'] - purge_instance_ids = module.params['purge_instance_ids'] - zones = module.params['zones'] - purge_zones = module.params['purge_zones'] - security_group_ids = module.params['security_group_ids'] - security_group_names = module.params['security_group_names'] - health_check = module.params['health_check'] - access_logs = module.params['access_logs'] - subnets = module.params['subnets'] - purge_subnets = module.params['purge_subnets'] - scheme = module.params['scheme'] - connection_draining_timeout = module.params['connection_draining_timeout'] - idle_timeout = module.params['idle_timeout'] - cross_az_load_balancing = module.params['cross_az_load_balancing'] - stickiness = module.params['stickiness'] - wait = module.params['wait'] - wait_timeout = module.params['wait_timeout'] - tags = module.params['tags'] - - if state == 'present' and not listeners: - module.fail_json(msg="At least one listener is required for ELB creation") - - if state == 'present' and not (zones or subnets): - module.fail_json(msg="At least one availability zone or subnet is required for ELB creation") - - if wait_timeout > 600: - module.fail_json(msg='wait_timeout maximum is 600 seconds') - - if security_group_names: - security_group_ids = [] - try: - ec2 = connect_to_aws(boto.ec2, region, **aws_connect_params) - if subnets: # We have at least one subnet, ergo this is a VPC - vpc_conn = _get_vpc_connection(module=module, region=region, aws_connect_params=aws_connect_params) - vpc_id = vpc_conn.get_all_subnets([subnets[0]])[0].vpc_id - filters = {'vpc_id': vpc_id} - else: - filters = None - grp_details = ec2.get_all_security_groups(filters=filters) - - for group_name in security_group_names: - if isinstance(group_name, string_types): - group_name = [group_name] - - group_id = [str(grp.id) for grp in grp_details if str(grp.name) in group_name] - security_group_ids.extend(group_id) - except boto.exception.NoAuthHandlerFound as e: - module.fail_json(msg=str(e)) - - elb_man = ElbManager(module, name, listeners, purge_listeners, zones, - purge_zones, security_group_ids, health_check, - subnets, purge_subnets, scheme, - connection_draining_timeout, idle_timeout, - cross_az_load_balancing, - access_logs, stickiness, wait, wait_timeout, tags, - region=region, instance_ids=instance_ids, purge_instance_ids=purge_instance_ids, - **aws_connect_params) - - # check for unsupported attributes for this version of boto - if cross_az_load_balancing and not elb_man._check_attribute_support('cross_zone_load_balancing'): - module.fail_json(msg="You must install boto >= 2.18.0 to use the cross_az_load_balancing attribute") - - if connection_draining_timeout and not elb_man._check_attribute_support('connection_draining'): - module.fail_json(msg="You must install boto >= 2.28.0 to use the connection_draining_timeout attribute") - - if idle_timeout and not elb_man._check_attribute_support('connecting_settings'): - module.fail_json(msg="You must install boto >= 2.33.0 to use the idle_timeout attribute") - - if state == 'present': - elb_man.ensure_ok() - elif state == 'absent': - elb_man.ensure_gone() - - ansible_facts = {'ec2_elb': 'info'} - ec2_facts_result = dict(changed=elb_man.changed, - elb=elb_man.get_info(), - ansible_facts=ansible_facts) - - module.exit_json(**ec2_facts_result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_eni.py b/lib/ansible/modules/cloud/amazon/ec2_eni.py deleted file mode 100644 index 8b6dbd1c32..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_eni.py +++ /dev/null @@ -1,633 +0,0 @@ -#!/usr/bin/python -# -# 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 - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_eni -short_description: Create and optionally attach an Elastic Network Interface (ENI) to an instance -description: - - Create and optionally attach an Elastic Network Interface (ENI) to an instance. If an ENI ID or private_ip is - provided, the existing ENI (if any) will be modified. The 'attached' parameter controls the attachment status - of the network interface. -version_added: "2.0" -author: "Rob White (@wimnat)" -options: - eni_id: - description: - - The ID of the ENI (to modify). - - If I(eni_id=None) and I(state=present), a new eni will be created. - type: str - instance_id: - description: - - Instance ID that you wish to attach ENI to. - - Since version 2.2, use the I(attached) parameter to attach or detach an ENI. Prior to 2.2, to detach an ENI from an instance, use C(None). - type: str - private_ip_address: - description: - - Private IP address. - type: str - subnet_id: - description: - - ID of subnet in which to create the ENI. - type: str - description: - description: - - Optional description of the ENI. - type: str - security_groups: - description: - - List of security groups associated with the interface. Only used when I(state=present). - - Since version 2.2, you can specify security groups by ID or by name or a combination of both. Prior to 2.2, you can specify only by ID. - type: list - elements: str - state: - description: - - Create or delete ENI. - default: present - choices: [ 'present', 'absent' ] - type: str - device_index: - description: - - The index of the device for the network interface attachment on the instance. - default: 0 - type: int - attached: - description: - - Specifies if network interface should be attached or detached from instance. If omitted, attachment status - won't change - version_added: 2.2 - type: bool - force_detach: - description: - - Force detachment of the interface. This applies either when explicitly detaching the interface by setting I(instance_id=None) - or when deleting an interface with I(state=absent). - default: false - type: bool - delete_on_termination: - description: - - Delete the interface when the instance it is attached to is terminated. You can only specify this flag when the - interface is being modified, not on creation. - required: false - type: bool - source_dest_check: - description: - - By default, interfaces perform source/destination checks. NAT instances however need this check to be disabled. - You can only specify this flag when the interface is being modified, not on creation. - required: false - type: bool - secondary_private_ip_addresses: - description: - - A list of IP addresses to assign as secondary IP addresses to the network interface. - This option is mutually exclusive of I(secondary_private_ip_address_count) - required: false - version_added: 2.2 - type: list - elements: str - purge_secondary_private_ip_addresses: - description: - - To be used with I(secondary_private_ip_addresses) to determine whether or not to remove any secondary IP addresses other than those specified. - - Set I(secondary_private_ip_addresses=[]) to purge all secondary addresses. - default: false - type: bool - version_added: 2.5 - secondary_private_ip_address_count: - description: - - The number of secondary IP addresses to assign to the network interface. This option is mutually exclusive of I(secondary_private_ip_addresses) - required: false - version_added: 2.2 - type: int - allow_reassignment: - description: - - Indicates whether to allow an IP address that is already assigned to another network interface or instance - to be reassigned to the specified network interface. - required: false - default: false - type: bool - version_added: 2.7 -extends_documentation_fragment: - - aws - - ec2 -notes: - - This module identifies and ENI based on either the I(eni_id), a combination of I(private_ip_address) and I(subnet_id), - or a combination of I(instance_id) and I(device_id). Any of these options will let you specify a particular ENI. -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Create an ENI. As no security group is defined, ENI will be created in default security group -- ec2_eni: - private_ip_address: 172.31.0.20 - subnet_id: subnet-xxxxxxxx - state: present - -# Create an ENI and attach it to an instance -- ec2_eni: - instance_id: i-xxxxxxx - device_index: 1 - private_ip_address: 172.31.0.20 - subnet_id: subnet-xxxxxxxx - state: present - -# Create an ENI with two secondary addresses -- ec2_eni: - subnet_id: subnet-xxxxxxxx - state: present - secondary_private_ip_address_count: 2 - -# Assign a secondary IP address to an existing ENI -# This will purge any existing IPs -- ec2_eni: - subnet_id: subnet-xxxxxxxx - eni_id: eni-yyyyyyyy - state: present - secondary_private_ip_addresses: - - 172.16.1.1 - -# Remove any secondary IP addresses from an existing ENI -- ec2_eni: - subnet_id: subnet-xxxxxxxx - eni_id: eni-yyyyyyyy - state: present - secondary_private_ip_address_count: 0 - -# Destroy an ENI, detaching it from any instance if necessary -- ec2_eni: - eni_id: eni-xxxxxxx - force_detach: true - state: absent - -# Update an ENI -- ec2_eni: - eni_id: eni-xxxxxxx - description: "My new description" - state: present - -# Update an ENI identifying it by private_ip_address and subnet_id -- ec2_eni: - subnet_id: subnet-xxxxxxx - private_ip_address: 172.16.1.1 - description: "My new description" - -# Detach an ENI from an instance -- ec2_eni: - eni_id: eni-xxxxxxx - instance_id: None - state: present - -### Delete an interface on termination -# First create the interface -- ec2_eni: - instance_id: i-xxxxxxx - device_index: 1 - private_ip_address: 172.31.0.20 - subnet_id: subnet-xxxxxxxx - state: present - register: eni - -# Modify the interface to enable the delete_on_terminaton flag -- ec2_eni: - eni_id: "{{ eni.interface.id }}" - delete_on_termination: true - -''' - - -RETURN = ''' -interface: - description: Network interface attributes - returned: when state != absent - type: complex - contains: - description: - description: interface description - type: str - sample: Firewall network interface - groups: - description: list of security groups - type: list - elements: dict - sample: [ { "sg-f8a8a9da": "default" } ] - id: - description: network interface id - type: str - sample: "eni-1d889198" - mac_address: - description: interface's physical address - type: str - sample: "00:00:5E:00:53:23" - owner_id: - description: aws account id - type: str - sample: 812381371 - private_ip_address: - description: primary ip address of this interface - type: str - sample: 10.20.30.40 - private_ip_addresses: - description: list of all private ip addresses associated to this interface - type: list - elements: dict - sample: [ { "primary_address": true, "private_ip_address": "10.20.30.40" } ] - source_dest_check: - description: value of source/dest check flag - type: bool - sample: True - status: - description: network interface status - type: str - sample: "pending" - subnet_id: - description: which vpc subnet the interface is bound - type: str - sample: subnet-b0a0393c - vpc_id: - description: which vpc this network interface is bound - type: str - sample: vpc-9a9a9da - -''' - -import time -import re - -try: - import boto.ec2 - import boto.vpc - from boto.exception import BotoServerError - HAS_BOTO = True -except ImportError: - HAS_BOTO = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (AnsibleAWSError, connect_to_aws, - ec2_argument_spec, get_aws_connection_info, - get_ec2_security_group_ids_from_names) - - -def get_eni_info(interface): - - # Private addresses - private_addresses = [] - for ip in interface.private_ip_addresses: - private_addresses.append({'private_ip_address': ip.private_ip_address, 'primary_address': ip.primary}) - - interface_info = {'id': interface.id, - 'subnet_id': interface.subnet_id, - 'vpc_id': interface.vpc_id, - 'description': interface.description, - 'owner_id': interface.owner_id, - 'status': interface.status, - 'mac_address': interface.mac_address, - 'private_ip_address': interface.private_ip_address, - 'source_dest_check': interface.source_dest_check, - 'groups': dict((group.id, group.name) for group in interface.groups), - 'private_ip_addresses': private_addresses - } - - if interface.attachment is not None: - interface_info['attachment'] = {'attachment_id': interface.attachment.id, - 'instance_id': interface.attachment.instance_id, - 'device_index': interface.attachment.device_index, - 'status': interface.attachment.status, - 'attach_time': interface.attachment.attach_time, - 'delete_on_termination': interface.attachment.delete_on_termination, - } - - return interface_info - - -def wait_for_eni(eni, status): - - while True: - time.sleep(3) - eni.update() - # If the status is detached we just need attachment to disappear - if eni.attachment is None: - if status == "detached": - break - else: - if status == "attached" and eni.attachment.status == "attached": - break - - -def create_eni(connection, vpc_id, module): - - instance_id = module.params.get("instance_id") - attached = module.params.get("attached") - if instance_id == 'None': - instance_id = None - device_index = module.params.get("device_index") - subnet_id = module.params.get('subnet_id') - private_ip_address = module.params.get('private_ip_address') - description = module.params.get('description') - security_groups = get_ec2_security_group_ids_from_names(module.params.get('security_groups'), connection, vpc_id=vpc_id, boto3=False) - secondary_private_ip_addresses = module.params.get("secondary_private_ip_addresses") - secondary_private_ip_address_count = module.params.get("secondary_private_ip_address_count") - changed = False - - try: - eni = connection.create_network_interface(subnet_id, private_ip_address, description, security_groups) - if attached and instance_id is not None: - try: - eni.attach(instance_id, device_index) - except BotoServerError: - eni.delete() - raise - # Wait to allow creation / attachment to finish - wait_for_eni(eni, "attached") - eni.update() - - if secondary_private_ip_address_count is not None: - try: - connection.assign_private_ip_addresses(network_interface_id=eni.id, secondary_private_ip_address_count=secondary_private_ip_address_count) - except BotoServerError: - eni.delete() - raise - - if secondary_private_ip_addresses is not None: - try: - connection.assign_private_ip_addresses(network_interface_id=eni.id, private_ip_addresses=secondary_private_ip_addresses) - except BotoServerError: - eni.delete() - raise - - changed = True - - except BotoServerError as e: - module.fail_json(msg=e.message) - - module.exit_json(changed=changed, interface=get_eni_info(eni)) - - -def modify_eni(connection, vpc_id, module, eni): - - instance_id = module.params.get("instance_id") - attached = module.params.get("attached") - do_detach = module.params.get('state') == 'detached' - device_index = module.params.get("device_index") - description = module.params.get('description') - security_groups = module.params.get('security_groups') - force_detach = module.params.get("force_detach") - source_dest_check = module.params.get("source_dest_check") - delete_on_termination = module.params.get("delete_on_termination") - secondary_private_ip_addresses = module.params.get("secondary_private_ip_addresses") - purge_secondary_private_ip_addresses = module.params.get("purge_secondary_private_ip_addresses") - secondary_private_ip_address_count = module.params.get("secondary_private_ip_address_count") - allow_reassignment = module.params.get("allow_reassignment") - changed = False - - try: - if description is not None: - if eni.description != description: - connection.modify_network_interface_attribute(eni.id, "description", description) - changed = True - if len(security_groups) > 0: - groups = get_ec2_security_group_ids_from_names(security_groups, connection, vpc_id=vpc_id, boto3=False) - if sorted(get_sec_group_list(eni.groups)) != sorted(groups): - connection.modify_network_interface_attribute(eni.id, "groupSet", groups) - changed = True - if source_dest_check is not None: - if eni.source_dest_check != source_dest_check: - connection.modify_network_interface_attribute(eni.id, "sourceDestCheck", source_dest_check) - changed = True - if delete_on_termination is not None and eni.attachment is not None: - if eni.attachment.delete_on_termination is not delete_on_termination: - connection.modify_network_interface_attribute(eni.id, "deleteOnTermination", delete_on_termination, eni.attachment.id) - changed = True - - current_secondary_addresses = [i.private_ip_address for i in eni.private_ip_addresses if not i.primary] - if secondary_private_ip_addresses is not None: - secondary_addresses_to_remove = list(set(current_secondary_addresses) - set(secondary_private_ip_addresses)) - if secondary_addresses_to_remove and purge_secondary_private_ip_addresses: - connection.unassign_private_ip_addresses(network_interface_id=eni.id, - private_ip_addresses=list(set(current_secondary_addresses) - - set(secondary_private_ip_addresses)), - dry_run=False) - changed = True - - secondary_addresses_to_add = list(set(secondary_private_ip_addresses) - set(current_secondary_addresses)) - if secondary_addresses_to_add: - connection.assign_private_ip_addresses(network_interface_id=eni.id, - private_ip_addresses=secondary_addresses_to_add, - secondary_private_ip_address_count=None, - allow_reassignment=allow_reassignment, dry_run=False) - changed = True - if secondary_private_ip_address_count is not None: - current_secondary_address_count = len(current_secondary_addresses) - - if secondary_private_ip_address_count > current_secondary_address_count: - connection.assign_private_ip_addresses(network_interface_id=eni.id, - private_ip_addresses=None, - secondary_private_ip_address_count=(secondary_private_ip_address_count - - current_secondary_address_count), - allow_reassignment=allow_reassignment, dry_run=False) - changed = True - elif secondary_private_ip_address_count < current_secondary_address_count: - # How many of these addresses do we want to remove - secondary_addresses_to_remove_count = current_secondary_address_count - secondary_private_ip_address_count - connection.unassign_private_ip_addresses(network_interface_id=eni.id, - private_ip_addresses=current_secondary_addresses[:secondary_addresses_to_remove_count], - dry_run=False) - - if attached is True: - if eni.attachment and eni.attachment.instance_id != instance_id: - detach_eni(eni, module) - eni.attach(instance_id, device_index) - wait_for_eni(eni, "attached") - changed = True - if eni.attachment is None: - eni.attach(instance_id, device_index) - wait_for_eni(eni, "attached") - changed = True - elif attached is False: - detach_eni(eni, module) - - except BotoServerError as e: - module.fail_json(msg=e.message) - - eni.update() - module.exit_json(changed=changed, interface=get_eni_info(eni)) - - -def delete_eni(connection, module): - - eni_id = module.params.get("eni_id") - force_detach = module.params.get("force_detach") - - try: - eni_result_set = connection.get_all_network_interfaces(eni_id) - eni = eni_result_set[0] - - if force_detach is True: - if eni.attachment is not None: - eni.detach(force_detach) - # Wait to allow detachment to finish - wait_for_eni(eni, "detached") - eni.update() - eni.delete() - changed = True - else: - eni.delete() - changed = True - - module.exit_json(changed=changed) - except BotoServerError as e: - regex = re.compile('The networkInterface ID \'.*\' does not exist') - if regex.search(e.message) is not None: - module.exit_json(changed=False) - else: - module.fail_json(msg=e.message) - - -def detach_eni(eni, module): - - attached = module.params.get("attached") - - force_detach = module.params.get("force_detach") - if eni.attachment is not None: - eni.detach(force_detach) - wait_for_eni(eni, "detached") - if attached: - return - eni.update() - module.exit_json(changed=True, interface=get_eni_info(eni)) - else: - module.exit_json(changed=False, interface=get_eni_info(eni)) - - -def uniquely_find_eni(connection, module): - - eni_id = module.params.get("eni_id") - private_ip_address = module.params.get('private_ip_address') - subnet_id = module.params.get('subnet_id') - instance_id = module.params.get('instance_id') - device_index = module.params.get('device_index') - attached = module.params.get('attached') - - try: - filters = {} - - # proceed only if we're univocally specifying an ENI - if eni_id is None and private_ip_address is None and (instance_id is None and device_index is None): - return None - - if private_ip_address and subnet_id: - filters['private-ip-address'] = private_ip_address - filters['subnet-id'] = subnet_id - - if not attached and instance_id and device_index: - filters['attachment.instance-id'] = instance_id - filters['attachment.device-index'] = device_index - - if eni_id is None and len(filters) == 0: - return None - - eni_result = connection.get_all_network_interfaces(eni_id, filters=filters) - if len(eni_result) == 1: - return eni_result[0] - else: - return None - - except BotoServerError as e: - module.fail_json(msg=e.message) - - return None - - -def get_sec_group_list(groups): - - # Build list of remote security groups - remote_security_groups = [] - for group in groups: - remote_security_groups.append(group.id.encode()) - - return remote_security_groups - - -def _get_vpc_id(connection, module, subnet_id): - - try: - return connection.get_all_subnets(subnet_ids=[subnet_id])[0].vpc_id - except BotoServerError as e: - module.fail_json(msg=e.message) - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - eni_id=dict(default=None, type='str'), - instance_id=dict(default=None, type='str'), - private_ip_address=dict(type='str'), - subnet_id=dict(type='str'), - description=dict(type='str'), - security_groups=dict(default=[], type='list'), - device_index=dict(default=0, type='int'), - state=dict(default='present', choices=['present', 'absent']), - force_detach=dict(default='no', type='bool'), - source_dest_check=dict(default=None, type='bool'), - delete_on_termination=dict(default=None, type='bool'), - secondary_private_ip_addresses=dict(default=None, type='list'), - purge_secondary_private_ip_addresses=dict(default=False, type='bool'), - secondary_private_ip_address_count=dict(default=None, type='int'), - allow_reassignment=dict(default=False, type='bool'), - attached=dict(default=None, type='bool') - ) - ) - - module = AnsibleModule(argument_spec=argument_spec, - mutually_exclusive=[ - ['secondary_private_ip_addresses', 'secondary_private_ip_address_count'] - ], - required_if=([ - ('state', 'absent', ['eni_id']), - ('attached', True, ['instance_id']), - ('purge_secondary_private_ip_addresses', True, ['secondary_private_ip_addresses']) - ]) - ) - - if not HAS_BOTO: - module.fail_json(msg='boto required for this module') - - region, ec2_url, aws_connect_params = get_aws_connection_info(module) - - if region: - try: - connection = connect_to_aws(boto.ec2, region, **aws_connect_params) - vpc_connection = connect_to_aws(boto.vpc, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: - module.fail_json(msg=str(e)) - else: - module.fail_json(msg="region must be specified") - - state = module.params.get("state") - - if state == 'present': - eni = uniquely_find_eni(connection, module) - if eni is None: - subnet_id = module.params.get("subnet_id") - if subnet_id is None: - module.fail_json(msg="subnet_id is required when creating a new ENI") - - vpc_id = _get_vpc_id(vpc_connection, module, subnet_id) - create_eni(connection, vpc_id, module) - else: - vpc_id = eni.vpc_id - modify_eni(connection, vpc_id, module, eni) - - elif state == 'absent': - delete_eni(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_eni_info.py b/lib/ansible/modules/cloud/amazon/ec2_eni_info.py deleted file mode 100644 index 99922a84d1..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_eni_info.py +++ /dev/null @@ -1,275 +0,0 @@ -#!/usr/bin/python -# -# 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 - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_eni_info -short_description: Gather information about ec2 ENI interfaces in AWS -description: - - 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. -version_added: "2.0" -author: "Rob White (@wimnat)" -requirements: [ boto3 ] -options: - 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_DescribeNetworkInterfaces.html) for possible filters. - type: dict -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Gather information about all ENIs -- ec2_eni_info: - -# Gather information about a particular ENI -- ec2_eni_info: - filters: - network-interface-id: eni-xxxxxxx - -''' - -RETURN = ''' -network_interfaces: - description: List of matching elastic network interfaces - returned: always - type: complex - contains: - association: - description: Info of associated elastic IP (EIP) - returned: always, empty dict if no association exists - type: dict - sample: { - allocation_id: "eipalloc-5sdf123", - association_id: "eipassoc-8sdf123", - ip_owner_id: "4415120123456", - public_dns_name: "ec2-52-1-0-63.compute-1.amazonaws.com", - public_ip: "52.1.0.63" - } - attachment: - description: Info about attached ec2 instance - returned: always, empty dict if ENI is not attached - type: dict - sample: { - attach_time: "2017-08-05T15:25:47+00:00", - attachment_id: "eni-attach-149d21234", - delete_on_termination: false, - device_index: 1, - instance_id: "i-15b8d3cadbafa1234", - instance_owner_id: "4415120123456", - status: "attached" - } - availability_zone: - description: Availability zone of ENI - returned: always - type: str - sample: "us-east-1b" - description: - description: Description text for ENI - returned: always - type: str - sample: "My favourite network interface" - groups: - description: List of attached security groups - returned: always - type: list - sample: [ - { - group_id: "sg-26d0f1234", - group_name: "my_ec2_security_group" - } - ] - id: - description: The id of the ENI (alias for network_interface_id) - returned: always - type: str - sample: "eni-392fsdf" - interface_type: - description: Type of the network interface - returned: always - type: str - sample: "interface" - ipv6_addresses: - description: List of IPv6 addresses for this interface - returned: always - type: list - sample: [] - mac_address: - description: MAC address of the network interface - returned: always - type: str - sample: "0a:f8:10:2f:ab:a1" - network_interface_id: - description: The id of the ENI - returned: always - type: str - sample: "eni-392fsdf" - owner_id: - description: AWS account id of the owner of the ENI - returned: always - type: str - sample: "4415120123456" - private_dns_name: - description: Private DNS name for the ENI - returned: always - type: str - sample: "ip-172-16-1-180.ec2.internal" - private_ip_address: - description: Private IP address for the ENI - returned: always - type: str - sample: "172.16.1.180" - private_ip_addresses: - description: List of private IP addresses attached to the ENI - returned: always - type: list - sample: [] - requester_id: - description: The ID of the entity that launched the ENI - returned: always - type: str - sample: "AIDAIONYVJQNIAZFT3ABC" - requester_managed: - description: Indicates whether the network interface is being managed by an AWS service. - returned: always - type: bool - sample: false - source_dest_check: - description: Indicates whether the network interface performs source/destination checking. - returned: always - type: bool - sample: false - status: - description: Indicates if the network interface is attached to an instance or not - returned: always - type: str - sample: "in-use" - subnet_id: - description: Subnet ID the ENI is in - returned: always - type: str - sample: "subnet-7bbf01234" - tag_set: - description: Dictionary of tags added to the ENI - returned: always - type: dict - sample: {} - vpc_id: - description: ID of the VPC the network interface it part of - returned: always - type: str - sample: "vpc-b3f1f123" -''' - -try: - from botocore.exceptions import ClientError, NoCredentialsError - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import ansible_dict_to_boto3_filter_list, boto3_conn -from ansible.module_utils.ec2 import boto3_tag_list_to_ansible_dict, camel_dict_to_snake_dict -from ansible.module_utils.ec2 import ec2_argument_spec, get_aws_connection_info - - -def list_eni(connection, module): - - if module.params.get("filters") is None: - filters = [] - else: - filters = ansible_dict_to_boto3_filter_list(module.params.get("filters")) - - try: - network_interfaces_result = connection.describe_network_interfaces(Filters=filters)['NetworkInterfaces'] - except (ClientError, NoCredentialsError) as e: - module.fail_json(msg=e.message) - - # Modify boto3 tags list to be ansible friendly dict and then camel_case - camel_network_interfaces = [] - for network_interface in network_interfaces_result: - network_interface['TagSet'] = boto3_tag_list_to_ansible_dict(network_interface['TagSet']) - # Added id to interface info to be compatible with return values of ec2_eni module: - network_interface['Id'] = network_interface['NetworkInterfaceId'] - camel_network_interfaces.append(camel_dict_to_snake_dict(network_interface)) - - module.exit_json(network_interfaces=camel_network_interfaces) - - -def get_eni_info(interface): - - # Private addresses - private_addresses = [] - for ip in interface.private_ip_addresses: - private_addresses.append({'private_ip_address': ip.private_ip_address, 'primary_address': ip.primary}) - - interface_info = {'id': interface.id, - 'subnet_id': interface.subnet_id, - 'vpc_id': interface.vpc_id, - 'description': interface.description, - 'owner_id': interface.owner_id, - 'status': interface.status, - 'mac_address': interface.mac_address, - 'private_ip_address': interface.private_ip_address, - 'source_dest_check': interface.source_dest_check, - 'groups': dict((group.id, group.name) for group in interface.groups), - 'private_ip_addresses': private_addresses - } - - if hasattr(interface, 'publicDnsName'): - interface_info['association'] = {'public_ip_address': interface.publicIp, - 'public_dns_name': interface.publicDnsName, - 'ip_owner_id': interface.ipOwnerId - } - - if interface.attachment is not None: - interface_info['attachment'] = {'attachment_id': interface.attachment.id, - 'instance_id': interface.attachment.instance_id, - 'device_index': interface.attachment.device_index, - 'status': interface.attachment.status, - 'attach_time': interface.attachment.attach_time, - 'delete_on_termination': interface.attachment.delete_on_termination, - } - - return interface_info - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - filters=dict(default=None, type='dict') - ) - ) - - module = AnsibleModule(argument_spec=argument_spec) - if module._name == 'ec2_eni_facts': - module.deprecate("The 'ec2_eni_facts' module has been renamed to 'ec2_eni_info'", version='2.13') - - if not HAS_BOTO3: - module.fail_json(msg='boto3 required for this module') - - region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) - - connection = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_params) - - list_eni(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_group.py b/lib/ansible/modules/cloud/amazon/ec2_group.py deleted file mode 100644 index bc416f66b5..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_group.py +++ /dev/null @@ -1,1345 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# This file is part of Ansible -# 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 - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'core'} - -DOCUMENTATION = ''' ---- -module: ec2_group -author: "Andrew de Quincey (@adq)" -version_added: "1.3" -requirements: [ boto3 ] -short_description: maintain an ec2 VPC security group. -description: - - Maintains ec2 security groups. This module has a dependency on python-boto >= 2.5. -options: - name: - description: - - Name of the security group. - - One of and only one of I(name) or I(group_id) is required. - - Required if I(state=present). - required: false - type: str - group_id: - description: - - Id of group to delete (works only with absent). - - One of and only one of I(name) or I(group_id) is required. - required: false - version_added: "2.4" - type: str - description: - description: - - Description of the security group. Required when C(state) is C(present). - required: false - type: str - vpc_id: - description: - - ID of the VPC to create the group in. - required: false - type: str - rules: - description: - - List of firewall inbound rules to enforce in this group (see example). If none are supplied, - no inbound rules will be enabled. Rules list may include its own name in `group_name`. - This allows idempotent loopback additions (e.g. allow group to access itself). - Rule sources list support was added in version 2.4. This allows to define multiple sources per - source type as well as multiple source types per rule. Prior to 2.4 an individual source is allowed. - In version 2.5 support for rule descriptions was added. - required: false - type: list - elements: dict - suboptions: - cidr_ip: - type: str - description: - - The IPv4 CIDR range traffic is coming from. - - You can specify only one of I(cidr_ip), I(cidr_ipv6), I(ip_prefix), I(group_id) - and I(group_name). - cidr_ipv6: - type: str - description: - - The IPv6 CIDR range traffic is coming from. - - You can specify only one of I(cidr_ip), I(cidr_ipv6), I(ip_prefix), I(group_id) - and I(group_name). - ip_prefix: - type: str - description: - - The IP Prefix U(https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-prefix-lists.html) - that traffic is coming from. - - You can specify only one of I(cidr_ip), I(cidr_ipv6), I(ip_prefix), I(group_id) - and I(group_name). - group_id: - type: str - description: - - The ID of the Security Group that traffic is coming from. - - You can specify only one of I(cidr_ip), I(cidr_ipv6), I(ip_prefix), I(group_id) - and I(group_name). - group_name: - type: str - description: - - Name of the Security Group that traffic is coming from. - - If the Security Group doesn't exist a new Security Group will be - created with I(group_desc) as the description. - - You can specify only one of I(cidr_ip), I(cidr_ipv6), I(ip_prefix), I(group_id) - and I(group_name). - group_desc: - type: str - description: - - If the I(group_name) is set and the Security Group doesn't exist a new Security Group will be - created with I(group_desc) as the description. - proto: - type: str - description: - - The IP protocol name (C(tcp), C(udp), C(icmp), C(icmpv6)) or number (U(https://en.wikipedia.org/wiki/List_of_IP_protocol_numbers)) - from_port: - type: int - description: The start of the range of ports that traffic is coming from. A value of C(-1) indicates all ports. - to_port: - type: int - description: The end of the range of ports that traffic is coming from. A value of C(-1) indicates all ports. - rule_desc: - type: str - description: A description for the rule. - rules_egress: - description: - - List of firewall outbound rules to enforce in this group (see example). If none are supplied, - a default all-out rule is assumed. If an empty list is supplied, no outbound rules will be enabled. - Rule Egress sources list support was added in version 2.4. In version 2.5 support for rule descriptions - was added. - required: false - version_added: "1.6" - type: list - elements: dict - suboptions: - cidr_ip: - type: str - description: - - The IPv4 CIDR range traffic is going to. - - You can specify only one of I(cidr_ip), I(cidr_ipv6), I(ip_prefix), I(group_id) - and I(group_name). - cidr_ipv6: - type: str - description: - - The IPv6 CIDR range traffic is going to. - - You can specify only one of I(cidr_ip), I(cidr_ipv6), I(ip_prefix), I(group_id) - and I(group_name). - ip_prefix: - type: str - description: - - The IP Prefix U(https://docs.aws.amazon.com/cli/latest/reference/ec2/describe-prefix-lists.html) - that traffic is going to. - - You can specify only one of I(cidr_ip), I(cidr_ipv6), I(ip_prefix), I(group_id) - and I(group_name). - group_id: - type: str - description: - - The ID of the Security Group that traffic is going to. - - You can specify only one of I(cidr_ip), I(cidr_ipv6), I(ip_prefix), I(group_id) - and I(group_name). - group_name: - type: str - description: - - Name of the Security Group that traffic is going to. - - If the Security Group doesn't exist a new Security Group will be - created with I(group_desc) as the description. - - You can specify only one of I(cidr_ip), I(cidr_ipv6), I(ip_prefix), I(group_id) - and I(group_name). - group_desc: - type: str - description: - - If the I(group_name) is set and the Security Group doesn't exist a new Security Group will be - created with I(group_desc) as the description. - proto: - type: str - description: - - The IP protocol name (C(tcp), C(udp), C(icmp), C(icmpv6)) or number (U(https://en.wikipedia.org/wiki/List_of_IP_protocol_numbers)) - from_port: - type: int - description: The start of the range of ports that traffic is going to. A value of C(-1) indicates all ports. - to_port: - type: int - description: The end of the range of ports that traffic is going to. A value of C(-1) indicates all ports. - rule_desc: - type: str - description: A description for the rule. - state: - version_added: "1.4" - description: - - Create or delete a security group. - required: false - default: 'present' - choices: [ "present", "absent" ] - aliases: [] - type: str - purge_rules: - version_added: "1.8" - description: - - Purge existing rules on security group that are not found in rules. - required: false - default: 'true' - aliases: [] - type: bool - purge_rules_egress: - version_added: "1.8" - description: - - Purge existing rules_egress on security group that are not found in rules_egress. - required: false - default: 'true' - aliases: [] - type: bool - tags: - version_added: "2.4" - description: - - A dictionary of one or more tags to assign to the security group. - required: false - type: dict - aliases: ['resource_tags'] - purge_tags: - version_added: "2.4" - description: - - If yes, existing tags will be purged from the resource to match exactly what is defined by I(tags) parameter. If the I(tags) parameter is not set then - tags will not be modified. - required: false - default: yes - type: bool - -extends_documentation_fragment: - - aws - - ec2 - -notes: - - If a rule declares a group_name and that group doesn't exist, it will be - automatically created. In that case, group_desc should be provided as well. - The module will refuse to create a depended-on group without a description. - - Preview diff mode support is added in version 2.7. -''' - -EXAMPLES = ''' -- name: example using security group rule descriptions - ec2_group: - name: "{{ name }}" - description: sg with rule descriptions - vpc_id: vpc-xxxxxxxx - profile: "{{ aws_profile }}" - region: us-east-1 - rules: - - proto: tcp - ports: - - 80 - cidr_ip: 0.0.0.0/0 - rule_desc: allow all on port 80 - -- name: example ec2 group - ec2_group: - name: example - description: an example EC2 group - vpc_id: 12345 - region: eu-west-1 - aws_secret_key: SECRET - aws_access_key: ACCESS - rules: - - proto: tcp - from_port: 80 - to_port: 80 - cidr_ip: 0.0.0.0/0 - - proto: tcp - from_port: 22 - to_port: 22 - cidr_ip: 10.0.0.0/8 - - proto: tcp - from_port: 443 - to_port: 443 - # this should only be needed for EC2 Classic security group rules - # because in a VPC an ELB will use a user-account security group - group_id: amazon-elb/sg-87654321/amazon-elb-sg - - proto: tcp - from_port: 3306 - to_port: 3306 - group_id: 123412341234/sg-87654321/exact-name-of-sg - - proto: udp - from_port: 10050 - to_port: 10050 - cidr_ip: 10.0.0.0/8 - - proto: udp - from_port: 10051 - to_port: 10051 - group_id: sg-12345678 - - proto: icmp - from_port: 8 # icmp type, -1 = any type - to_port: -1 # icmp subtype, -1 = any subtype - cidr_ip: 10.0.0.0/8 - - proto: all - # the containing group name may be specified here - group_name: example - - proto: all - # in the 'proto' attribute, if you specify -1, all, or a protocol number other than tcp, udp, icmp, or 58 (ICMPv6), - # traffic on all ports is allowed, regardless of any ports you specify - from_port: 10050 # this value is ignored - to_port: 10050 # this value is ignored - cidr_ip: 10.0.0.0/8 - - rules_egress: - - proto: tcp - from_port: 80 - to_port: 80 - cidr_ip: 0.0.0.0/0 - cidr_ipv6: 64:ff9b::/96 - group_name: example-other - # description to use if example-other needs to be created - group_desc: other example EC2 group - -- name: example2 ec2 group - ec2_group: - name: example2 - description: an example2 EC2 group - vpc_id: 12345 - region: eu-west-1 - rules: - # 'ports' rule keyword was introduced in version 2.4. It accepts a single port value or a list of values including ranges (from_port-to_port). - - proto: tcp - ports: 22 - group_name: example-vpn - - proto: tcp - ports: - - 80 - - 443 - - 8080-8099 - cidr_ip: 0.0.0.0/0 - # Rule sources list support was added in version 2.4. This allows to define multiple sources per source type as well as multiple source types per rule. - - proto: tcp - ports: - - 6379 - - 26379 - group_name: - - example-vpn - - example-redis - - proto: tcp - ports: 5665 - group_name: example-vpn - cidr_ip: - - 172.16.1.0/24 - - 172.16.17.0/24 - cidr_ipv6: - - 2607:F8B0::/32 - - 64:ff9b::/96 - group_id: - - sg-edcd9784 - diff: True - -- name: "Delete group by its id" - ec2_group: - region: eu-west-1 - group_id: sg-33b4ee5b - state: absent -''' - -RETURN = ''' -group_name: - description: Security group name - sample: My Security Group - type: str - returned: on create/update -group_id: - description: Security group id - sample: sg-abcd1234 - type: str - returned: on create/update -description: - description: Description of security group - sample: My Security Group - type: str - returned: on create/update -tags: - description: Tags associated with the security group - sample: - Name: My Security Group - Purpose: protecting stuff - type: dict - returned: on create/update -vpc_id: - description: ID of VPC to which the security group belongs - sample: vpc-abcd1234 - type: str - returned: on create/update -ip_permissions: - description: Inbound rules associated with the security group. - sample: - - from_port: 8182 - ip_protocol: tcp - ip_ranges: - - cidr_ip: "1.1.1.1/32" - ipv6_ranges: [] - prefix_list_ids: [] - to_port: 8182 - user_id_group_pairs: [] - type: list - returned: on create/update -ip_permissions_egress: - description: Outbound rules associated with the security group. - sample: - - ip_protocol: -1 - ip_ranges: - - cidr_ip: "0.0.0.0/0" - ipv6_ranges: [] - prefix_list_ids: [] - user_id_group_pairs: [] - type: list - returned: on create/update -owner_id: - description: AWS Account ID of the security group - sample: 123456789012 - type: int - returned: on create/update -''' - -import json -import re -import itertools -from copy import deepcopy -from time import sleep -from collections import namedtuple -from ansible.module_utils.aws.core import AnsibleAWSModule, is_boto3_error_code -from ansible.module_utils.aws.iam import get_aws_account_id -from ansible.module_utils.aws.waiters import get_waiter -from ansible.module_utils.ec2 import AWSRetry, camel_dict_to_snake_dict, compare_aws_tags -from ansible.module_utils.ec2 import ansible_dict_to_boto3_filter_list, boto3_tag_list_to_ansible_dict, ansible_dict_to_boto3_tag_list -from ansible.module_utils.common.network import to_ipv6_subnet, to_subnet -from ansible.module_utils.compat.ipaddress import ip_network, IPv6Network -from ansible.module_utils._text import to_text -from ansible.module_utils.six import string_types - -try: - from botocore.exceptions import BotoCoreError, ClientError -except ImportError: - pass # caught by AnsibleAWSModule - - -Rule = namedtuple('Rule', ['port_range', 'protocol', 'target', 'target_type', 'description']) -valid_targets = set(['ipv4', 'ipv6', 'group', 'ip_prefix']) -current_account_id = None - - -def rule_cmp(a, b): - """Compare rules without descriptions""" - for prop in ['port_range', 'protocol', 'target', 'target_type']: - if prop == 'port_range' and to_text(a.protocol) == to_text(b.protocol): - # equal protocols can interchange `(-1, -1)` and `(None, None)` - if a.port_range in ((None, None), (-1, -1)) and b.port_range in ((None, None), (-1, -1)): - continue - elif getattr(a, prop) != getattr(b, prop): - return False - elif getattr(a, prop) != getattr(b, prop): - return False - return True - - -def rules_to_permissions(rules): - return [to_permission(rule) for rule in rules] - - -def to_permission(rule): - # take a Rule, output the serialized grant - perm = { - 'IpProtocol': rule.protocol, - } - perm['FromPort'], perm['ToPort'] = rule.port_range - if rule.target_type == 'ipv4': - perm['IpRanges'] = [{ - 'CidrIp': rule.target, - }] - if rule.description: - perm['IpRanges'][0]['Description'] = rule.description - elif rule.target_type == 'ipv6': - perm['Ipv6Ranges'] = [{ - 'CidrIpv6': rule.target, - }] - if rule.description: - perm['Ipv6Ranges'][0]['Description'] = rule.description - elif rule.target_type == 'group': - if isinstance(rule.target, tuple): - pair = {} - if rule.target[0]: - pair['UserId'] = rule.target[0] - # group_id/group_name are mutually exclusive - give group_id more precedence as it is more specific - if rule.target[1]: - pair['GroupId'] = rule.target[1] - elif rule.target[2]: - pair['GroupName'] = rule.target[2] - perm['UserIdGroupPairs'] = [pair] - else: - perm['UserIdGroupPairs'] = [{ - 'GroupId': rule.target - }] - if rule.description: - perm['UserIdGroupPairs'][0]['Description'] = rule.description - elif rule.target_type == 'ip_prefix': - perm['PrefixListIds'] = [{ - 'PrefixListId': rule.target, - }] - if rule.description: - perm['PrefixListIds'][0]['Description'] = rule.description - elif rule.target_type not in valid_targets: - raise ValueError('Invalid target type for rule {0}'.format(rule)) - return fix_port_and_protocol(perm) - - -def rule_from_group_permission(perm): - def ports_from_permission(p): - if 'FromPort' not in p and 'ToPort' not in p: - return (None, None) - return (int(perm['FromPort']), int(perm['ToPort'])) - - # outputs a rule tuple - for target_key, target_subkey, target_type in [ - ('IpRanges', 'CidrIp', 'ipv4'), - ('Ipv6Ranges', 'CidrIpv6', 'ipv6'), - ('PrefixListIds', 'PrefixListId', 'ip_prefix'), - ]: - if target_key not in perm: - continue - for r in perm[target_key]: - # there may be several IP ranges here, which is ok - yield Rule( - ports_from_permission(perm), - to_text(perm['IpProtocol']), - r[target_subkey], - target_type, - r.get('Description') - ) - if 'UserIdGroupPairs' in perm and perm['UserIdGroupPairs']: - for pair in perm['UserIdGroupPairs']: - target = ( - pair.get('UserId', None), - pair.get('GroupId', None), - pair.get('GroupName', None), - ) - if pair.get('UserId', '').startswith('amazon-'): - # amazon-elb and amazon-prefix rules don't need - # group-id specified, so remove it when querying - # from permission - target = ( - target[0], - None, - target[2], - ) - elif 'VpcPeeringConnectionId' in pair or pair['UserId'] != current_account_id: - target = ( - pair.get('UserId', None), - pair.get('GroupId', None), - pair.get('GroupName', None), - ) - - yield Rule( - ports_from_permission(perm), - to_text(perm['IpProtocol']), - target, - 'group', - pair.get('Description') - ) - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0, catch_extra_error_codes=['InvalidGroup.NotFound']) -def get_security_groups_with_backoff(connection, **kwargs): - return connection.describe_security_groups(**kwargs) - - -@AWSRetry.backoff(tries=5, delay=5, backoff=2.0) -def sg_exists_with_backoff(connection, **kwargs): - try: - return connection.describe_security_groups(**kwargs) - except is_boto3_error_code('InvalidGroup.NotFound'): - return {'SecurityGroups': []} - - -def deduplicate_rules_args(rules): - """Returns unique rules""" - if rules is None: - return None - return list(dict(zip((json.dumps(r, sort_keys=True) for r in rules), rules)).values()) - - -def validate_rule(module, rule): - VALID_PARAMS = ('cidr_ip', 'cidr_ipv6', 'ip_prefix', - 'group_id', 'group_name', 'group_desc', - 'proto', 'from_port', 'to_port', 'rule_desc') - if not isinstance(rule, dict): - module.fail_json(msg='Invalid rule parameter type [%s].' % type(rule)) - for k in rule: - if k not in VALID_PARAMS: - module.fail_json(msg='Invalid rule parameter \'{0}\' for rule: {1}'.format(k, rule)) - - if 'group_id' in rule and 'cidr_ip' in rule: - module.fail_json(msg='Specify group_id OR cidr_ip, not both') - elif 'group_name' in rule and 'cidr_ip' in rule: - module.fail_json(msg='Specify group_name OR cidr_ip, not both') - elif 'group_id' in rule and 'cidr_ipv6' in rule: - module.fail_json(msg="Specify group_id OR cidr_ipv6, not both") - elif 'group_name' in rule and 'cidr_ipv6' in rule: - module.fail_json(msg="Specify group_name OR cidr_ipv6, not both") - elif 'cidr_ip' in rule and 'cidr_ipv6' in rule: - module.fail_json(msg="Specify cidr_ip OR cidr_ipv6, not both") - elif 'group_id' in rule and 'group_name' in rule: - module.fail_json(msg='Specify group_id OR group_name, not both') - - -def get_target_from_rule(module, client, rule, name, group, groups, vpc_id): - """ - Returns tuple of (target_type, target, group_created) after validating rule params. - - rule: Dict describing a rule. - name: Name of the security group being managed. - groups: Dict of all available security groups. - - AWS accepts an ip range or a security group as target of a rule. This - function validate the rule specification and return either a non-None - group_id or a non-None ip range. - """ - FOREIGN_SECURITY_GROUP_REGEX = r'^([^/]+)/?(sg-\S+)?/(\S+)' - group_id = None - group_name = None - target_group_created = False - - validate_rule(module, rule) - if rule.get('group_id') and re.match(FOREIGN_SECURITY_GROUP_REGEX, rule['group_id']): - # this is a foreign Security Group. Since you can't fetch it you must create an instance of it - owner_id, group_id, group_name = re.match(FOREIGN_SECURITY_GROUP_REGEX, rule['group_id']).groups() - group_instance = dict(UserId=owner_id, GroupId=group_id, GroupName=group_name) - groups[group_id] = group_instance - groups[group_name] = group_instance - # group_id/group_name are mutually exclusive - give group_id more precedence as it is more specific - if group_id and group_name: - group_name = None - return 'group', (owner_id, group_id, group_name), False - elif 'group_id' in rule: - return 'group', rule['group_id'], False - elif 'group_name' in rule: - group_name = rule['group_name'] - if group_name == name: - group_id = group['GroupId'] - groups[group_id] = group - groups[group_name] = group - elif group_name in groups and group.get('VpcId') and groups[group_name].get('VpcId'): - # both are VPC groups, this is ok - group_id = groups[group_name]['GroupId'] - elif group_name in groups and not (group.get('VpcId') or groups[group_name].get('VpcId')): - # both are EC2 classic, this is ok - group_id = groups[group_name]['GroupId'] - else: - auto_group = None - filters = {'group-name': group_name} - if vpc_id: - filters['vpc-id'] = vpc_id - # if we got here, either the target group does not exist, or there - # is a mix of EC2 classic + VPC groups. Mixing of EC2 classic + VPC - # is bad, so we have to create a new SG because no compatible group - # exists - if not rule.get('group_desc', '').strip(): - # retry describing the group once - try: - auto_group = get_security_groups_with_backoff(client, Filters=ansible_dict_to_boto3_filter_list(filters)).get('SecurityGroups', [])[0] - except (is_boto3_error_code('InvalidGroup.NotFound'), IndexError): - module.fail_json(msg="group %s will be automatically created by rule %s but " - "no description was provided" % (group_name, rule)) - except ClientError as e: # pylint: disable=duplicate-except - module.fail_json_aws(e) - elif not module.check_mode: - params = dict(GroupName=group_name, Description=rule['group_desc']) - if vpc_id: - params['VpcId'] = vpc_id - try: - auto_group = client.create_security_group(**params) - get_waiter( - client, 'security_group_exists', - ).wait( - GroupIds=[auto_group['GroupId']], - ) - except is_boto3_error_code('InvalidGroup.Duplicate'): - # The group exists, but didn't show up in any of our describe-security-groups calls - # Try searching on a filter for the name, and allow a retry window for AWS to update - # the model on their end. - try: - auto_group = get_security_groups_with_backoff(client, Filters=ansible_dict_to_boto3_filter_list(filters)).get('SecurityGroups', [])[0] - except IndexError as e: - module.fail_json(msg="Could not create or use existing group '{0}' in rule. Make sure the group exists".format(group_name)) - except ClientError as e: - module.fail_json_aws( - e, - msg="Could not create or use existing group '{0}' in rule. Make sure the group exists".format(group_name)) - if auto_group is not None: - group_id = auto_group['GroupId'] - groups[group_id] = auto_group - groups[group_name] = auto_group - target_group_created = True - return 'group', group_id, target_group_created - elif 'cidr_ip' in rule: - return 'ipv4', validate_ip(module, rule['cidr_ip']), False - elif 'cidr_ipv6' in rule: - return 'ipv6', validate_ip(module, rule['cidr_ipv6']), False - elif 'ip_prefix' in rule: - return 'ip_prefix', rule['ip_prefix'], False - - module.fail_json(msg="Could not match target for rule {0}".format(rule), failed_rule=rule) - - -def ports_expand(ports): - # takes a list of ports and returns a list of (port_from, port_to) - ports_expanded = [] - for port in ports: - if not isinstance(port, string_types): - ports_expanded.append((port,) * 2) - elif '-' in port: - ports_expanded.append(tuple(int(p.strip()) for p in port.split('-', 1))) - else: - ports_expanded.append((int(port.strip()),) * 2) - - return ports_expanded - - -def rule_expand_ports(rule): - # takes a rule dict and returns a list of expanded rule dicts - if 'ports' not in rule: - if isinstance(rule.get('from_port'), string_types): - rule['from_port'] = int(rule.get('from_port')) - if isinstance(rule.get('to_port'), string_types): - rule['to_port'] = int(rule.get('to_port')) - return [rule] - - ports = rule['ports'] if isinstance(rule['ports'], list) else [rule['ports']] - - rule_expanded = [] - for from_to in ports_expand(ports): - temp_rule = rule.copy() - del temp_rule['ports'] - temp_rule['from_port'], temp_rule['to_port'] = sorted(from_to) - rule_expanded.append(temp_rule) - - return rule_expanded - - -def rules_expand_ports(rules): - # takes a list of rules and expands it based on 'ports' - if not rules: - return rules - - return [rule for rule_complex in rules - for rule in rule_expand_ports(rule_complex)] - - -def rule_expand_source(rule, source_type): - # takes a rule dict and returns a list of expanded rule dicts for specified source_type - sources = rule[source_type] if isinstance(rule[source_type], list) else [rule[source_type]] - source_types_all = ('cidr_ip', 'cidr_ipv6', 'group_id', 'group_name', 'ip_prefix') - - rule_expanded = [] - for source in sources: - temp_rule = rule.copy() - for s in source_types_all: - temp_rule.pop(s, None) - temp_rule[source_type] = source - rule_expanded.append(temp_rule) - - return rule_expanded - - -def rule_expand_sources(rule): - # takes a rule dict and returns a list of expanded rule discts - source_types = (stype for stype in ('cidr_ip', 'cidr_ipv6', 'group_id', 'group_name', 'ip_prefix') if stype in rule) - - return [r for stype in source_types - for r in rule_expand_source(rule, stype)] - - -def rules_expand_sources(rules): - # takes a list of rules and expands it based on 'cidr_ip', 'group_id', 'group_name' - if not rules: - return rules - - return [rule for rule_complex in rules - for rule in rule_expand_sources(rule_complex)] - - -def update_rules_description(module, client, rule_type, group_id, ip_permissions): - if module.check_mode: - return - try: - if rule_type == "in": - client.update_security_group_rule_descriptions_ingress(GroupId=group_id, IpPermissions=ip_permissions) - if rule_type == "out": - client.update_security_group_rule_descriptions_egress(GroupId=group_id, IpPermissions=ip_permissions) - except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e, msg="Unable to update rule description for group %s" % group_id) - - -def fix_port_and_protocol(permission): - for key in ('FromPort', 'ToPort'): - if key in permission: - if permission[key] is None: - del permission[key] - else: - permission[key] = int(permission[key]) - - permission['IpProtocol'] = to_text(permission['IpProtocol']) - - return permission - - -def remove_old_permissions(client, module, revoke_ingress, revoke_egress, group_id): - if revoke_ingress: - revoke(client, module, revoke_ingress, group_id, 'in') - if revoke_egress: - revoke(client, module, revoke_egress, group_id, 'out') - return bool(revoke_ingress or revoke_egress) - - -def revoke(client, module, ip_permissions, group_id, rule_type): - if not module.check_mode: - try: - if rule_type == 'in': - client.revoke_security_group_ingress(GroupId=group_id, IpPermissions=ip_permissions) - elif rule_type == 'out': - client.revoke_security_group_egress(GroupId=group_id, IpPermissions=ip_permissions) - except (BotoCoreError, ClientError) as e: - rules = 'ingress rules' if rule_type == 'in' else 'egress rules' - module.fail_json_aws(e, "Unable to revoke {0}: {1}".format(rules, ip_permissions)) - - -def add_new_permissions(client, module, new_ingress, new_egress, group_id): - if new_ingress: - authorize(client, module, new_ingress, group_id, 'in') - if new_egress: - authorize(client, module, new_egress, group_id, 'out') - return bool(new_ingress or new_egress) - - -def authorize(client, module, ip_permissions, group_id, rule_type): - if not module.check_mode: - try: - if rule_type == 'in': - client.authorize_security_group_ingress(GroupId=group_id, IpPermissions=ip_permissions) - elif rule_type == 'out': - client.authorize_security_group_egress(GroupId=group_id, IpPermissions=ip_permissions) - except (BotoCoreError, ClientError) as e: - rules = 'ingress rules' if rule_type == 'in' else 'egress rules' - module.fail_json_aws(e, "Unable to authorize {0}: {1}".format(rules, ip_permissions)) - - -def validate_ip(module, cidr_ip): - split_addr = cidr_ip.split('/') - if len(split_addr) == 2: - # this_ip is a IPv4 or IPv6 CIDR that may or may not have host bits set - # Get the network bits if IPv4, and validate if IPv6. - try: - ip = to_subnet(split_addr[0], split_addr[1]) - if ip != cidr_ip: - module.warn("One of your CIDR addresses ({0}) has host bits set. To get rid of this warning, " - "check the network mask and make sure that only network bits are set: {1}.".format( - cidr_ip, ip)) - except ValueError: - # to_subnet throws a ValueError on IPv6 networks, so we should be working with v6 if we get here - try: - isinstance(ip_network(to_text(cidr_ip)), IPv6Network) - ip = cidr_ip - except ValueError: - # If a host bit is set on something other than a /128, IPv6Network will throw a ValueError - # The ipv6_cidr in this case probably looks like "2001:DB8:A0B:12F0::1/64" and we just want the network bits - ip6 = to_ipv6_subnet(split_addr[0]) + "/" + split_addr[1] - if ip6 != cidr_ip: - module.warn("One of your IPv6 CIDR addresses ({0}) has host bits set. To get rid of this warning, " - "check the network mask and make sure that only network bits are set: {1}.".format(cidr_ip, ip6)) - return ip6 - return ip - return cidr_ip - - -def update_tags(client, module, group_id, current_tags, tags, purge_tags): - tags_need_modify, tags_to_delete = compare_aws_tags(current_tags, tags, purge_tags) - - if not module.check_mode: - if tags_to_delete: - try: - client.delete_tags(Resources=[group_id], Tags=[{'Key': tag} for tag in tags_to_delete]) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to delete tags {0}".format(tags_to_delete)) - - # Add/update tags - if tags_need_modify: - try: - client.create_tags(Resources=[group_id], Tags=ansible_dict_to_boto3_tag_list(tags_need_modify)) - except (BotoCoreError, ClientError) as e: - module.fail_json(e, msg="Unable to add tags {0}".format(tags_need_modify)) - - return bool(tags_need_modify or tags_to_delete) - - -def update_rule_descriptions(module, group_id, present_ingress, named_tuple_ingress_list, present_egress, named_tuple_egress_list): - changed = False - client = module.client('ec2') - ingress_needs_desc_update = [] - egress_needs_desc_update = [] - - for present_rule in present_egress: - needs_update = [r for r in named_tuple_egress_list if rule_cmp(r, present_rule) and r.description != present_rule.description] - for r in needs_update: - named_tuple_egress_list.remove(r) - egress_needs_desc_update.extend(needs_update) - for present_rule in present_ingress: - needs_update = [r for r in named_tuple_ingress_list if rule_cmp(r, present_rule) and r.description != present_rule.description] - for r in needs_update: - named_tuple_ingress_list.remove(r) - ingress_needs_desc_update.extend(needs_update) - - if ingress_needs_desc_update: - update_rules_description(module, client, 'in', group_id, rules_to_permissions(ingress_needs_desc_update)) - changed |= True - if egress_needs_desc_update: - update_rules_description(module, client, 'out', group_id, rules_to_permissions(egress_needs_desc_update)) - changed |= True - return changed - - -def create_security_group(client, module, name, description, vpc_id): - if not module.check_mode: - params = dict(GroupName=name, Description=description) - if vpc_id: - params['VpcId'] = vpc_id - try: - group = client.create_security_group(**params) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to create security group") - # When a group is created, an egress_rule ALLOW ALL - # to 0.0.0.0/0 is added automatically but it's not - # reflected in the object returned by the AWS API - # call. We re-read the group for getting an updated object - # amazon sometimes takes a couple seconds to update the security group so wait till it exists - while True: - sleep(3) - group = get_security_groups_with_backoff(client, GroupIds=[group['GroupId']])['SecurityGroups'][0] - if group.get('VpcId') and not group.get('IpPermissionsEgress'): - pass - else: - break - return group - return None - - -def wait_for_rule_propagation(module, group, desired_ingress, desired_egress, purge_ingress, purge_egress): - group_id = group['GroupId'] - tries = 6 - - def await_rules(group, desired_rules, purge, rule_key): - for i in range(tries): - current_rules = set(sum([list(rule_from_group_permission(p)) for p in group[rule_key]], [])) - if purge and len(current_rules ^ set(desired_rules)) == 0: - return group - elif purge: - conflicts = current_rules ^ set(desired_rules) - # For cases where set comparison is equivalent, but invalid port/proto exist - for a, b in itertools.combinations(conflicts, 2): - if rule_cmp(a, b): - conflicts.discard(a) - conflicts.discard(b) - if not len(conflicts): - return group - elif current_rules.issuperset(desired_rules) and not purge: - return group - sleep(10) - group = get_security_groups_with_backoff(module.client('ec2'), GroupIds=[group_id])['SecurityGroups'][0] - module.warn("Ran out of time waiting for {0} {1}. Current: {2}, Desired: {3}".format(group_id, rule_key, current_rules, desired_rules)) - return group - - group = get_security_groups_with_backoff(module.client('ec2'), GroupIds=[group_id])['SecurityGroups'][0] - if 'VpcId' in group and module.params.get('rules_egress') is not None: - group = await_rules(group, desired_egress, purge_egress, 'IpPermissionsEgress') - return await_rules(group, desired_ingress, purge_ingress, 'IpPermissions') - - -def group_exists(client, module, vpc_id, group_id, name): - params = {'Filters': []} - if group_id: - params['GroupIds'] = [group_id] - if name: - # Add name to filters rather than params['GroupNames'] - # because params['GroupNames'] only checks the default vpc if no vpc is provided - params['Filters'].append({'Name': 'group-name', 'Values': [name]}) - if vpc_id: - params['Filters'].append({'Name': 'vpc-id', 'Values': [vpc_id]}) - # Don't filter by description to maintain backwards compatibility - - try: - security_groups = sg_exists_with_backoff(client, **params).get('SecurityGroups', []) - all_groups = get_security_groups_with_backoff(client).get('SecurityGroups', []) - except (BotoCoreError, ClientError) as e: # pylint: disable=duplicate-except - module.fail_json_aws(e, msg="Error in describe_security_groups") - - if security_groups: - groups = dict((group['GroupId'], group) for group in all_groups) - groups.update(dict((group['GroupName'], group) for group in all_groups)) - if vpc_id: - vpc_wins = dict((group['GroupName'], group) for group in all_groups if group.get('VpcId') and group['VpcId'] == vpc_id) - groups.update(vpc_wins) - # maintain backwards compatibility by using the last matching group - return security_groups[-1], groups - return None, {} - - -def verify_rules_with_descriptions_permitted(client, module, rules, rules_egress): - if not hasattr(client, "update_security_group_rule_descriptions_egress"): - all_rules = rules if rules else [] + rules_egress if rules_egress else [] - if any('rule_desc' in rule for rule in all_rules): - module.fail_json(msg="Using rule descriptions requires botocore version >= 1.7.2.") - - -def get_diff_final_resource(client, module, security_group): - def get_account_id(security_group, module): - try: - owner_id = security_group.get('owner_id', module.client('sts').get_caller_identity()['Account']) - except (BotoCoreError, ClientError) as e: - owner_id = "Unable to determine owner_id: {0}".format(to_text(e)) - return owner_id - - def get_final_tags(security_group_tags, specified_tags, purge_tags): - if specified_tags is None: - return security_group_tags - tags_need_modify, tags_to_delete = compare_aws_tags(security_group_tags, specified_tags, purge_tags) - end_result_tags = dict((k, v) for k, v in specified_tags.items() if k not in tags_to_delete) - end_result_tags.update(dict((k, v) for k, v in security_group_tags.items() if k not in tags_to_delete)) - end_result_tags.update(tags_need_modify) - return end_result_tags - - def get_final_rules(client, module, security_group_rules, specified_rules, purge_rules): - if specified_rules is None: - return security_group_rules - if purge_rules: - final_rules = [] - else: - final_rules = list(security_group_rules) - specified_rules = flatten_nested_targets(module, deepcopy(specified_rules)) - for rule in specified_rules: - format_rule = { - 'from_port': None, 'to_port': None, 'ip_protocol': rule.get('proto', 'tcp'), - 'ip_ranges': [], 'ipv6_ranges': [], 'prefix_list_ids': [], 'user_id_group_pairs': [] - } - if rule.get('proto', 'tcp') in ('all', '-1', -1): - format_rule['ip_protocol'] = '-1' - format_rule.pop('from_port') - format_rule.pop('to_port') - elif rule.get('ports'): - if rule.get('ports') and (isinstance(rule['ports'], string_types) or isinstance(rule['ports'], int)): - rule['ports'] = [rule['ports']] - for port in rule.get('ports'): - if isinstance(port, string_types) and '-' in port: - format_rule['from_port'], format_rule['to_port'] = port.split('-') - else: - format_rule['from_port'] = format_rule['to_port'] = port - elif rule.get('from_port') or rule.get('to_port'): - format_rule['from_port'] = rule.get('from_port', rule.get('to_port')) - format_rule['to_port'] = rule.get('to_port', rule.get('from_port')) - for source_type in ('cidr_ip', 'cidr_ipv6', 'prefix_list_id'): - if rule.get(source_type): - rule_key = {'cidr_ip': 'ip_ranges', 'cidr_ipv6': 'ipv6_ranges', 'prefix_list_id': 'prefix_list_ids'}.get(source_type) - if rule.get('rule_desc'): - format_rule[rule_key] = [{source_type: rule[source_type], 'description': rule['rule_desc']}] - else: - if not isinstance(rule[source_type], list): - rule[source_type] = [rule[source_type]] - format_rule[rule_key] = [{source_type: target} for target in rule[source_type]] - if rule.get('group_id') or rule.get('group_name'): - rule_sg = camel_dict_to_snake_dict(group_exists(client, module, module.params['vpc_id'], rule.get('group_id'), rule.get('group_name'))[0]) - format_rule['user_id_group_pairs'] = [{ - 'description': rule_sg.get('description', rule_sg.get('group_desc')), - 'group_id': rule_sg.get('group_id', rule.get('group_id')), - 'group_name': rule_sg.get('group_name', rule.get('group_name')), - 'peering_status': rule_sg.get('peering_status'), - 'user_id': rule_sg.get('user_id', get_account_id(security_group, module)), - 'vpc_id': rule_sg.get('vpc_id', module.params['vpc_id']), - 'vpc_peering_connection_id': rule_sg.get('vpc_peering_connection_id') - }] - for k, v in list(format_rule['user_id_group_pairs'][0].items()): - if v is None: - format_rule['user_id_group_pairs'][0].pop(k) - final_rules.append(format_rule) - # Order final rules consistently - final_rules.sort(key=get_ip_permissions_sort_key) - return final_rules - security_group_ingress = security_group.get('ip_permissions', []) - specified_ingress = module.params['rules'] - purge_ingress = module.params['purge_rules'] - security_group_egress = security_group.get('ip_permissions_egress', []) - specified_egress = module.params['rules_egress'] - purge_egress = module.params['purge_rules_egress'] - return { - 'description': module.params['description'], - 'group_id': security_group.get('group_id', 'sg-xxxxxxxx'), - 'group_name': security_group.get('group_name', module.params['name']), - 'ip_permissions': get_final_rules(client, module, security_group_ingress, specified_ingress, purge_ingress), - 'ip_permissions_egress': get_final_rules(client, module, security_group_egress, specified_egress, purge_egress), - 'owner_id': get_account_id(security_group, module), - 'tags': get_final_tags(security_group.get('tags', {}), module.params['tags'], module.params['purge_tags']), - 'vpc_id': security_group.get('vpc_id', module.params['vpc_id'])} - - -def flatten_nested_targets(module, rules): - def _flatten(targets): - for target in targets: - if isinstance(target, list): - for t in _flatten(target): - yield t - elif isinstance(target, string_types): - yield target - - if rules is not None: - for rule in rules: - target_list_type = None - if isinstance(rule.get('cidr_ip'), list): - target_list_type = 'cidr_ip' - elif isinstance(rule.get('cidr_ipv6'), list): - target_list_type = 'cidr_ipv6' - if target_list_type is not None: - rule[target_list_type] = list(_flatten(rule[target_list_type])) - return rules - - -def get_rule_sort_key(dicts): - if dicts.get('cidr_ip'): - return dicts.get('cidr_ip') - elif dicts.get('cidr_ipv6'): - return dicts.get('cidr_ipv6') - elif dicts.get('prefix_list_id'): - return dicts.get('prefix_list_id') - elif dicts.get('group_id'): - return dicts.get('group_id') - return None - - -def get_ip_permissions_sort_key(rule): - if rule.get('ip_ranges'): - rule.get('ip_ranges').sort(key=get_rule_sort_key) - return rule.get('ip_ranges')[0]['cidr_ip'] - elif rule.get('ipv6_ranges'): - rule.get('ipv6_ranges').sort(key=get_rule_sort_key) - return rule.get('ipv6_ranges')[0]['cidr_ipv6'] - elif rule.get('prefix_list_ids'): - rule.get('prefix_list_ids').sort(key=get_rule_sort_key) - return rule.get('prefix_list_ids')[0]['prefix_list_id'] - elif rule.get('user_id_group_pairs'): - rule.get('user_id_group_pairs').sort(key=get_rule_sort_key) - return rule.get('user_id_group_pairs')[0]['group_id'] - return None - - -def main(): - argument_spec = dict( - name=dict(), - group_id=dict(), - description=dict(), - vpc_id=dict(), - rules=dict(type='list'), - rules_egress=dict(type='list'), - state=dict(default='present', type='str', choices=['present', 'absent']), - purge_rules=dict(default=True, required=False, type='bool'), - purge_rules_egress=dict(default=True, required=False, type='bool'), - tags=dict(required=False, type='dict', aliases=['resource_tags']), - purge_tags=dict(default=True, required=False, type='bool') - ) - module = AnsibleAWSModule( - argument_spec=argument_spec, - supports_check_mode=True, - required_one_of=[['name', 'group_id']], - required_if=[['state', 'present', ['name']]], - ) - - name = module.params['name'] - group_id = module.params['group_id'] - description = module.params['description'] - vpc_id = module.params['vpc_id'] - rules = flatten_nested_targets(module, deepcopy(module.params['rules'])) - rules_egress = flatten_nested_targets(module, deepcopy(module.params['rules_egress'])) - rules = deduplicate_rules_args(rules_expand_sources(rules_expand_ports(rules))) - rules_egress = deduplicate_rules_args(rules_expand_sources(rules_expand_ports(rules_egress))) - state = module.params.get('state') - purge_rules = module.params['purge_rules'] - purge_rules_egress = module.params['purge_rules_egress'] - tags = module.params['tags'] - purge_tags = module.params['purge_tags'] - - if state == 'present' and not description: - module.fail_json(msg='Must provide description when state is present.') - - changed = False - client = module.client('ec2') - - verify_rules_with_descriptions_permitted(client, module, rules, rules_egress) - group, groups = group_exists(client, module, vpc_id, group_id, name) - group_created_new = not bool(group) - - global current_account_id - current_account_id = get_aws_account_id(module) - - before = {} - after = {} - - # Ensure requested group is absent - if state == 'absent': - if group: - # found a match, delete it - before = camel_dict_to_snake_dict(group, ignore_list=['Tags']) - before['tags'] = boto3_tag_list_to_ansible_dict(before.get('tags', [])) - try: - if not module.check_mode: - client.delete_security_group(GroupId=group['GroupId']) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Unable to delete security group '%s'" % group) - else: - group = None - changed = True - else: - # no match found, no changes required - pass - - # Ensure requested group is present - elif state == 'present': - if group: - # existing group - before = camel_dict_to_snake_dict(group, ignore_list=['Tags']) - before['tags'] = boto3_tag_list_to_ansible_dict(before.get('tags', [])) - if group['Description'] != description: - module.warn("Group description does not match existing group. Descriptions cannot be changed without deleting " - "and re-creating the security group. Try using state=absent to delete, then rerunning this task.") - else: - # no match found, create it - group = create_security_group(client, module, name, description, vpc_id) - changed = True - - if tags is not None and group is not None: - current_tags = boto3_tag_list_to_ansible_dict(group.get('Tags', [])) - changed |= update_tags(client, module, group['GroupId'], current_tags, tags, purge_tags) - - if group: - named_tuple_ingress_list = [] - named_tuple_egress_list = [] - current_ingress = sum([list(rule_from_group_permission(p)) for p in group['IpPermissions']], []) - current_egress = sum([list(rule_from_group_permission(p)) for p in group['IpPermissionsEgress']], []) - - for new_rules, rule_type, named_tuple_rule_list in [(rules, 'in', named_tuple_ingress_list), - (rules_egress, 'out', named_tuple_egress_list)]: - if new_rules is None: - continue - for rule in new_rules: - target_type, target, target_group_created = get_target_from_rule( - module, client, rule, name, group, groups, vpc_id) - changed |= target_group_created - - if rule.get('proto', 'tcp') in ('all', '-1', -1): - rule['proto'] = '-1' - rule['from_port'] = None - rule['to_port'] = None - try: - int(rule.get('proto', 'tcp')) - rule['proto'] = to_text(rule.get('proto', 'tcp')) - rule['from_port'] = None - rule['to_port'] = None - except ValueError: - # rule does not use numeric protocol spec - pass - - named_tuple_rule_list.append( - Rule( - port_range=(rule['from_port'], rule['to_port']), - protocol=to_text(rule.get('proto', 'tcp')), - target=target, target_type=target_type, - description=rule.get('rule_desc'), - ) - ) - - # List comprehensions for rules to add, rules to modify, and rule ids to determine purging - new_ingress_permissions = [to_permission(r) for r in (set(named_tuple_ingress_list) - set(current_ingress))] - new_egress_permissions = [to_permission(r) for r in (set(named_tuple_egress_list) - set(current_egress))] - - if module.params.get('rules_egress') is None and 'VpcId' in group: - # when no egress rules are specified and we're in a VPC, - # we add in a default allow all out rule, which was the - # default behavior before egress rules were added - rule = Rule((None, None), '-1', '0.0.0.0/0', 'ipv4', None) - if rule in current_egress: - named_tuple_egress_list.append(rule) - if rule not in current_egress: - current_egress.append(rule) - - # List comprehensions for rules to add, rules to modify, and rule ids to determine purging - present_ingress = list(set(named_tuple_ingress_list).union(set(current_ingress))) - present_egress = list(set(named_tuple_egress_list).union(set(current_egress))) - - if purge_rules: - revoke_ingress = [] - for p in present_ingress: - if not any([rule_cmp(p, b) for b in named_tuple_ingress_list]): - revoke_ingress.append(to_permission(p)) - else: - revoke_ingress = [] - if purge_rules_egress and module.params.get('rules_egress') is not None: - if module.params.get('rules_egress') is []: - revoke_egress = [ - to_permission(r) for r in set(present_egress) - set(named_tuple_egress_list) - if r != Rule((None, None), '-1', '0.0.0.0/0', 'ipv4', None) - ] - else: - revoke_egress = [] - for p in present_egress: - if not any([rule_cmp(p, b) for b in named_tuple_egress_list]): - revoke_egress.append(to_permission(p)) - else: - revoke_egress = [] - - # named_tuple_ingress_list and named_tuple_egress_list got updated by - # method update_rule_descriptions, deep copy these two lists to new - # variables for the record of the 'desired' ingress and egress sg permissions - desired_ingress = deepcopy(named_tuple_ingress_list) - desired_egress = deepcopy(named_tuple_egress_list) - - changed |= update_rule_descriptions(module, group['GroupId'], present_ingress, named_tuple_ingress_list, present_egress, named_tuple_egress_list) - - # Revoke old rules - changed |= remove_old_permissions(client, module, revoke_ingress, revoke_egress, group['GroupId']) - rule_msg = 'Revoking {0}, and egress {1}'.format(revoke_ingress, revoke_egress) - - new_ingress_permissions = [to_permission(r) for r in (set(named_tuple_ingress_list) - set(current_ingress))] - new_ingress_permissions = rules_to_permissions(set(named_tuple_ingress_list) - set(current_ingress)) - new_egress_permissions = rules_to_permissions(set(named_tuple_egress_list) - set(current_egress)) - # Authorize new rules - changed |= add_new_permissions(client, module, new_ingress_permissions, new_egress_permissions, group['GroupId']) - - if group_created_new and module.params.get('rules') is None and module.params.get('rules_egress') is None: - # A new group with no rules provided is already being awaited. - # When it is created we wait for the default egress rule to be added by AWS - security_group = get_security_groups_with_backoff(client, GroupIds=[group['GroupId']])['SecurityGroups'][0] - elif changed and not module.check_mode: - # keep pulling until current security group rules match the desired ingress and egress rules - security_group = wait_for_rule_propagation(module, group, desired_ingress, desired_egress, purge_rules, purge_rules_egress) - else: - security_group = get_security_groups_with_backoff(client, GroupIds=[group['GroupId']])['SecurityGroups'][0] - security_group = camel_dict_to_snake_dict(security_group, ignore_list=['Tags']) - security_group['tags'] = boto3_tag_list_to_ansible_dict(security_group.get('tags', [])) - - else: - security_group = {'group_id': None} - - if module._diff: - if module.params['state'] == 'present': - after = get_diff_final_resource(client, module, security_group) - if before.get('ip_permissions'): - before['ip_permissions'].sort(key=get_ip_permissions_sort_key) - - security_group['diff'] = [{'before': before, 'after': after}] - - module.exit_json(changed=changed, **security_group) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_group_info.py b/lib/ansible/modules/cloud/amazon/ec2_group_info.py deleted file mode 100644 index 733f7d0622..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_group_info.py +++ /dev/null @@ -1,143 +0,0 @@ -#!/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 - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_group_info -short_description: Gather information about ec2 security groups in AWS. -description: - - Gather information about ec2 security groups in AWS. - - This module was called C(ec2_group_facts) before Ansible 2.9. The usage did not change. -version_added: "2.3" -requirements: [ boto3 ] -author: -- Henrique Rodrigues (@Sodki) -options: - 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_DescribeSecurityGroups.html) for - possible filters. Filter names and values are case sensitive. You can also use underscores (_) - instead of dashes (-) in the filter keys, which will take precedence in case of conflict. - required: false - default: {} - type: dict -notes: - - By default, the module will return all security groups. To limit results use the appropriate filters. - -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Gather information about all security groups -- ec2_group_info: - -# Gather information about all security groups in a specific VPC -- ec2_group_info: - filters: - vpc-id: vpc-12345678 - -# Gather information about all security groups in a specific VPC -- ec2_group_info: - filters: - vpc-id: vpc-12345678 - -# Gather information about a security group -- ec2_group_info: - filters: - group-name: example-1 - -# Gather information about a security group by id -- ec2_group_info: - filters: - group-id: sg-12345678 - -# Gather information about a security group with multiple filters, also mixing the use of underscores as filter keys -- ec2_group_info: - filters: - group_id: sg-12345678 - vpc-id: vpc-12345678 - -# Gather information about various security groups -- ec2_group_info: - filters: - group-name: - - example-1 - - example-2 - - example-3 - -# Gather information about any security group with a tag key Name and value Example. -# The quotes around 'tag:name' are important because of the colon in the value -- ec2_group_info: - filters: - "tag:Name": Example -''' - -RETURN = ''' -security_groups: - description: Security groups that match the provided filters. Each element consists of a dict with all the information related to that security group. - type: list - returned: always - sample: -''' - -try: - from botocore.exceptions import BotoCoreError, ClientError -except ImportError: - pass # caught by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import (boto3_tag_list_to_ansible_dict, ansible_dict_to_boto3_filter_list, camel_dict_to_snake_dict) - - -def main(): - argument_spec = dict( - filters=dict(default={}, type='dict') - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) - if module._name == 'ec2_group_facts': - module.deprecate("The 'ec2_group_facts' module has been renamed to 'ec2_group_info'", version='2.13') - - connection = module.client('ec2') - - # Replace filter key underscores with dashes, for compatibility, except if we're dealing with tags - sanitized_filters = module.params.get("filters") - for key in list(sanitized_filters): - if not key.startswith("tag:"): - sanitized_filters[key.replace("_", "-")] = sanitized_filters.pop(key) - - try: - security_groups = connection.describe_security_groups( - Filters=ansible_dict_to_boto3_filter_list(sanitized_filters) - ) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to describe security groups') - - snaked_security_groups = [] - for security_group in security_groups['SecurityGroups']: - # Modify boto3 tags list to be ansible friendly dict - # but don't camel case tags - security_group = camel_dict_to_snake_dict(security_group) - security_group['tags'] = boto3_tag_list_to_ansible_dict(security_group.get('tags', {}), tag_name_key_name='key', tag_value_key_name='value') - snaked_security_groups.append(security_group) - - module.exit_json(security_groups=snaked_security_groups) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_key.py b/lib/ansible/modules/cloud/amazon/ec2_key.py deleted file mode 100644 index de67af8bc0..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_key.py +++ /dev/null @@ -1,271 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# 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 - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_key -version_added: "1.5" -short_description: create or delete an ec2 key pair -description: - - create or delete an ec2 key pair. -options: - name: - description: - - Name of the key pair. - required: true - type: str - key_material: - description: - - Public key material. - required: false - type: str - force: - description: - - Force overwrite of already existing key pair if key has changed. - required: false - default: true - type: bool - version_added: "2.3" - state: - description: - - create or delete keypair - required: false - choices: [ present, absent ] - default: 'present' - type: str - wait: - description: - - This option has no effect since version 2.5 and will be removed in 2.14. - version_added: "1.6" - type: bool - wait_timeout: - description: - - This option has no effect since version 2.5 and will be removed in 2.14. - version_added: "1.6" - type: int - required: false - -extends_documentation_fragment: - - aws - - ec2 -requirements: [ boto3 ] -author: - - "Vincent Viallet (@zbal)" - - "Prasad Katti (@prasadkatti)" -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -- name: create a new ec2 key pair, returns generated private key - ec2_key: - name: my_keypair - -- name: create key pair using provided key_material - ec2_key: - name: my_keypair - key_material: 'ssh-rsa AAAAxyz...== me@example.com' - -- name: create key pair using key_material obtained using 'file' lookup plugin - ec2_key: - name: my_keypair - key_material: "{{ lookup('file', '/path/to/public_key/id_rsa.pub') }}" - -# try creating a key pair with the name of an already existing keypair -# but don't overwrite it even if the key is different (force=false) -- name: try creating a key pair with name of an already existing keypair - ec2_key: - name: my_existing_keypair - key_material: 'ssh-rsa AAAAxyz...== me@example.com' - force: false - -- name: remove key pair by name - ec2_key: - name: my_keypair - state: absent -''' - -RETURN = ''' -changed: - description: whether a keypair was created/deleted - returned: always - type: bool - sample: true -msg: - description: short message describing the action taken - returned: always - type: str - sample: key pair created -key: - description: details of the keypair (this is set to null when state is absent) - returned: always - type: complex - contains: - fingerprint: - description: fingerprint of the key - returned: when state is present - type: str - sample: 'b0:22:49:61:d9:44:9d:0c:7e:ac:8a:32:93:21:6c:e8:fb:59:62:43' - name: - description: name of the keypair - returned: when state is present - type: str - sample: my_keypair - private_key: - description: private key of a newly created keypair - returned: when a new keypair is created by AWS (key_material is not provided) - type: str - sample: '-----BEGIN RSA PRIVATE KEY----- - MIIEowIBAAKC... - -----END RSA PRIVATE KEY-----' -''' - -import uuid - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils._text import to_bytes - -try: - from botocore.exceptions import ClientError -except ImportError: - pass # caught by AnsibleAWSModule - - -def extract_key_data(key): - - data = { - 'name': key['KeyName'], - 'fingerprint': key['KeyFingerprint'] - } - if 'KeyMaterial' in key: - data['private_key'] = key['KeyMaterial'] - return data - - -def get_key_fingerprint(module, ec2_client, key_material): - ''' - EC2's fingerprints are non-trivial to generate, so push this key - to a temporary name and make ec2 calculate the fingerprint for us. - http://blog.jbrowne.com/?p=23 - https://forums.aws.amazon.com/thread.jspa?messageID=352828 - ''' - - # find an unused name - name_in_use = True - while name_in_use: - random_name = "ansible-" + str(uuid.uuid4()) - name_in_use = find_key_pair(module, ec2_client, random_name) - - temp_key = import_key_pair(module, ec2_client, random_name, key_material) - delete_key_pair(module, ec2_client, random_name, finish_task=False) - return temp_key['KeyFingerprint'] - - -def find_key_pair(module, ec2_client, name): - - try: - key = ec2_client.describe_key_pairs(KeyNames=[name])['KeyPairs'][0] - except ClientError as err: - if err.response['Error']['Code'] == "InvalidKeyPair.NotFound": - return None - module.fail_json_aws(err, msg="error finding keypair") - except IndexError: - key = None - return key - - -def create_key_pair(module, ec2_client, name, key_material, force): - - key = find_key_pair(module, ec2_client, name) - if key: - if key_material and force: - if not module.check_mode: - new_fingerprint = get_key_fingerprint(module, ec2_client, key_material) - if key['KeyFingerprint'] != new_fingerprint: - delete_key_pair(module, ec2_client, name, finish_task=False) - key = import_key_pair(module, ec2_client, name, key_material) - key_data = extract_key_data(key) - module.exit_json(changed=True, key=key_data, msg="key pair updated") - else: - # Assume a change will be made in check mode since a comparison can't be done - module.exit_json(changed=True, key=extract_key_data(key), msg="key pair updated") - key_data = extract_key_data(key) - module.exit_json(changed=False, key=key_data, msg="key pair already exists") - else: - # key doesn't exist, create it now - key_data = None - if not module.check_mode: - if key_material: - key = import_key_pair(module, ec2_client, name, key_material) - else: - try: - key = ec2_client.create_key_pair(KeyName=name) - except ClientError as err: - module.fail_json_aws(err, msg="error creating key") - key_data = extract_key_data(key) - module.exit_json(changed=True, key=key_data, msg="key pair created") - - -def import_key_pair(module, ec2_client, name, key_material): - - try: - key = ec2_client.import_key_pair(KeyName=name, PublicKeyMaterial=to_bytes(key_material)) - except ClientError as err: - module.fail_json_aws(err, msg="error importing key") - return key - - -def delete_key_pair(module, ec2_client, name, finish_task=True): - - key = find_key_pair(module, ec2_client, name) - if key: - if not module.check_mode: - try: - ec2_client.delete_key_pair(KeyName=name) - except ClientError as err: - module.fail_json_aws(err, msg="error deleting key") - if not finish_task: - return - module.exit_json(changed=True, key=None, msg="key deleted") - module.exit_json(key=None, msg="key did not exist") - - -def main(): - - argument_spec = dict( - name=dict(required=True), - key_material=dict(), - force=dict(type='bool', default=True), - state=dict(default='present', choices=['present', 'absent']), - wait=dict(type='bool', removed_in_version='2.14'), - wait_timeout=dict(type='int', removed_in_version='2.14') - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) - - ec2_client = module.client('ec2') - - name = module.params['name'] - state = module.params.get('state') - key_material = module.params.get('key_material') - force = module.params.get('force') - - if state == 'absent': - delete_key_pair(module, ec2_client, name) - elif state == 'present': - create_key_pair(module, ec2_client, name, key_material, force) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_metadata_facts.py b/lib/ansible/modules/cloud/amazon/ec2_metadata_facts.py deleted file mode 100644 index 2eee43811b..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_metadata_facts.py +++ /dev/null @@ -1,564 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# 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 - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'core'} - - -DOCUMENTATION = ''' ---- -module: ec2_metadata_facts -short_description: Gathers facts (instance metadata) about remote hosts within ec2 -version_added: "1.0" -author: - - Silviu Dicu (@silviud) - - Vinay Dandekar (@roadmapper) -description: - - This module fetches data from the instance metadata endpoint in ec2 as per - U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html). - - The module must be called from within the EC2 instance itself. -notes: - - Parameters to filter on ec2_metadata_facts may be added later. -''' - -EXAMPLES = ''' -# Gather EC2 metadata facts -- ec2_metadata_facts: - -- debug: - msg: "This instance is a t1.micro" - when: ansible_ec2_instance_type == "t1.micro" -''' - -RETURN = ''' -ansible_facts: - description: Dictionary of new facts representing discovered properties of the EC2 instance. - returned: changed - type: complex - contains: - ansible_ec2_ami_id: - description: The AMI ID used to launch the instance. - type: str - sample: "ami-XXXXXXXX" - ansible_ec2_ami_launch_index: - description: - - If you started more than one instance at the same time, this value indicates the order in which the instance was launched. - - The value of the first instance launched is 0. - type: str - sample: "0" - ansible_ec2_ami_manifest_path: - description: - - The path to the AMI manifest file in Amazon S3. - - If you used an Amazon EBS-backed AMI to launch the instance, the returned result is unknown. - type: str - sample: "(unknown)" - ansible_ec2_ancestor_ami_ids: - description: - - The AMI IDs of any instances that were rebundled to create this AMI. - - This value will only exist if the AMI manifest file contained an ancestor-amis key. - type: str - sample: "(unknown)" - ansible_ec2_block_device_mapping_ami: - description: The virtual device that contains the root/boot file system. - type: str - sample: "/dev/sda1" - ansible_ec2_block_device_mapping_ebsN: - description: - - The virtual devices associated with Amazon EBS volumes, if any are present. - - Amazon EBS volumes are only available in metadata if they were present at launch time or when the instance was last started. - - The N indicates the index of the Amazon EBS volume (such as ebs1 or ebs2). - type: str - sample: "/dev/xvdb" - ansible_ec2_block_device_mapping_ephemeralN: - description: The virtual devices associated with ephemeral devices, if any are present. The N indicates the index of the ephemeral volume. - type: str - sample: "/dev/xvdc" - ansible_ec2_block_device_mapping_root: - description: - - The virtual devices or partitions associated with the root devices, or partitions on the virtual device, - where the root (/ or C) file system is associated with the given instance. - type: str - sample: "/dev/sda1" - ansible_ec2_block_device_mapping_swap: - description: The virtual devices associated with swap. Not always present. - type: str - sample: "/dev/sda2" - ansible_ec2_fws_instance_monitoring: - description: "Value showing whether the customer has enabled detailed one-minute monitoring in CloudWatch." - type: str - sample: "enabled" - ansible_ec2_hostname: - description: - - The private IPv4 DNS hostname of the instance. - - In cases where multiple network interfaces are present, this refers to the eth0 device (the device for which the device number is 0). - type: str - sample: "ip-10-0-0-1.ec2.internal" - ansible_ec2_iam_info: - description: - - If there is an IAM role associated with the instance, contains information about the last time the instance profile was updated, - including the instance's LastUpdated date, InstanceProfileArn, and InstanceProfileId. Otherwise, not present. - type: complex - sample: "" - contains: - LastUpdated: - description: The last time which InstanceProfile is associated with the Instance changed. - type: str - InstanceProfileArn: - description: The ARN of the InstanceProfile associated with the Instance. - type: str - InstanceProfileId: - description: The Id of the InstanceProfile associated with the Instance. - type: str - ansible_ec2_iam_info_instanceprofilearn: - description: The IAM instance profile ARN. - type: str - sample: "arn:aws:iam::<account id>:instance-profile/<role name>" - ansible_ec2_iam_info_instanceprofileid: - description: IAM instance profile ID. - type: str - sample: "" - ansible_ec2_iam_info_lastupdated: - description: IAM info last updated time. - type: str - sample: "2017-05-12T02:42:27Z" - ansible_ec2_iam_instance_profile_role: - description: IAM instance role. - type: str - sample: "role_name" - ansible_ec2_iam_security_credentials_<role name>: - description: - - If there is an IAM role associated with the instance, role-name is the name of the role, - and role-name contains the temporary security credentials associated with the role. Otherwise, not present. - type: str - sample: "" - ansible_ec2_iam_security_credentials_<role name>_accesskeyid: - description: IAM role access key ID. - type: str - sample: "" - ansible_ec2_iam_security_credentials_<role name>_code: - description: IAM code. - type: str - sample: "Success" - ansible_ec2_iam_security_credentials_<role name>_expiration: - description: IAM role credentials expiration time. - type: str - sample: "2017-05-12T09:11:41Z" - ansible_ec2_iam_security_credentials_<role name>_lastupdated: - description: IAM role last updated time. - type: str - sample: "2017-05-12T02:40:44Z" - ansible_ec2_iam_security_credentials_<role name>_secretaccesskey: - description: IAM role secret access key. - type: str - sample: "" - ansible_ec2_iam_security_credentials_<role name>_token: - description: IAM role token. - type: str - sample: "" - ansible_ec2_iam_security_credentials_<role name>_type: - description: IAM role type. - type: str - sample: "AWS-HMAC" - ansible_ec2_instance_action: - description: Notifies the instance that it should reboot in preparation for bundling. - type: str - sample: "none" - ansible_ec2_instance_id: - description: The ID of this instance. - type: str - sample: "i-XXXXXXXXXXXXXXXXX" - ansible_ec2_instance_identity_document: - description: JSON containing instance attributes, such as instance-id, private IP address, etc. - type: str - sample: "" - ansible_ec2_instance_identity_document_accountid: - description: "" - type: str - sample: "012345678901" - ansible_ec2_instance_identity_document_architecture: - description: Instance system architecture. - type: str - sample: "x86_64" - ansible_ec2_instance_identity_document_availabilityzone: - description: The Availability Zone in which the instance launched. - type: str - sample: "us-east-1a" - ansible_ec2_instance_identity_document_billingproducts: - description: Billing products for this instance. - type: str - sample: "" - ansible_ec2_instance_identity_document_devpayproductcodes: - description: Product codes for the launched AMI. - type: str - sample: "" - ansible_ec2_instance_identity_document_imageid: - description: The AMI ID used to launch the instance. - type: str - sample: "ami-01234567" - ansible_ec2_instance_identity_document_instanceid: - description: The ID of this instance. - type: str - sample: "i-0123456789abcdef0" - ansible_ec2_instance_identity_document_instancetype: - description: The type of instance. - type: str - sample: "m4.large" - ansible_ec2_instance_identity_document_kernelid: - description: The ID of the kernel launched with this instance, if applicable. - type: str - sample: "" - ansible_ec2_instance_identity_document_pendingtime: - description: The instance pending time. - type: str - sample: "2017-05-11T20:51:20Z" - ansible_ec2_instance_identity_document_privateip: - description: - - The private IPv4 address of the instance. - - In cases where multiple network interfaces are present, this refers to the eth0 device (the device for which the device number is 0). - type: str - sample: "10.0.0.1" - ansible_ec2_instance_identity_document_ramdiskid: - description: The ID of the RAM disk specified at launch time, if applicable. - type: str - sample: "" - ansible_ec2_instance_identity_document_region: - description: The Region in which the instance launched. - type: str - sample: "us-east-1" - ansible_ec2_instance_identity_document_version: - description: Identity document version. - type: str - sample: "2010-08-31" - ansible_ec2_instance_identity_pkcs7: - description: Used to verify the document's authenticity and content against the signature. - type: str - sample: "" - ansible_ec2_instance_identity_rsa2048: - description: Used to verify the document's authenticity and content against the signature. - type: str - sample: "" - ansible_ec2_instance_identity_signature: - description: Data that can be used by other parties to verify its origin and authenticity. - type: str - sample: "" - ansible_ec2_instance_type: - description: The type of instance. - type: str - sample: "m4.large" - ansible_ec2_local_hostname: - description: - - The private IPv4 DNS hostname of the instance. - - In cases where multiple network interfaces are present, this refers to the eth0 device (the device for which the device number is 0). - type: str - sample: "ip-10-0-0-1.ec2.internal" - ansible_ec2_local_ipv4: - description: - - The private IPv4 address of the instance. - - In cases where multiple network interfaces are present, this refers to the eth0 device (the device for which the device number is 0). - type: str - sample: "10.0.0.1" - ansible_ec2_mac: - description: - - The instance's media access control (MAC) address. - - In cases where multiple network interfaces are present, this refers to the eth0 device (the device for which the device number is 0). - type: str - sample: "00:11:22:33:44:55" - ansible_ec2_metrics_vhostmd: - description: Metrics. - type: str - sample: "" - ansible_ec2_network_interfaces_macs_<mac address>_device_number: - description: - - The unique device number associated with that interface. The device number corresponds to the device name; - for example, a device-number of 2 is for the eth2 device. - - This category corresponds to the DeviceIndex and device-index fields that are used by the Amazon EC2 API and the EC2 commands for the AWS CLI. - type: str - sample: "0" - ansible_ec2_network_interfaces_macs_<mac address>_interface_id: - description: The elastic network interface ID. - type: str - sample: "eni-12345678" - ansible_ec2_network_interfaces_macs_<mac address>_ipv4_associations_<ip address>: - description: The private IPv4 addresses that are associated with each public-ip address and assigned to that interface. - type: str - sample: "" - ansible_ec2_network_interfaces_macs_<mac address>_ipv6s: - description: The IPv6 addresses associated with the interface. Returned only for instances launched into a VPC. - type: str - sample: "" - ansible_ec2_network_interfaces_macs_<mac address>_local_hostname: - description: The interface's local hostname. - type: str - sample: "" - ansible_ec2_network_interfaces_macs_<mac address>_local_ipv4s: - description: The private IPv4 addresses associated with the interface. - type: str - sample: "" - ansible_ec2_network_interfaces_macs_<mac address>_mac: - description: The instance's MAC address. - type: str - sample: "00:11:22:33:44:55" - ansible_ec2_network_interfaces_macs_<mac address>_owner_id: - description: - - The ID of the owner of the network interface. - - In multiple-interface environments, an interface can be attached by a third party, such as Elastic Load Balancing. - - Traffic on an interface is always billed to the interface owner. - type: str - sample: "01234567890" - ansible_ec2_network_interfaces_macs_<mac address>_public_hostname: - description: - - The interface's public DNS (IPv4). If the instance is in a VPC, - this category is only returned if the enableDnsHostnames attribute is set to true. - type: str - sample: "ec2-1-2-3-4.compute-1.amazonaws.com" - ansible_ec2_network_interfaces_macs_<mac address>_public_ipv4s: - description: The Elastic IP addresses associated with the interface. There may be multiple IPv4 addresses on an instance. - type: str - sample: "1.2.3.4" - ansible_ec2_network_interfaces_macs_<mac address>_security_group_ids: - description: The IDs of the security groups to which the network interface belongs. Returned only for instances launched into a VPC. - type: str - sample: "sg-01234567,sg-01234568" - ansible_ec2_network_interfaces_macs_<mac address>_security_groups: - description: Security groups to which the network interface belongs. Returned only for instances launched into a VPC. - type: str - sample: "secgroup1,secgroup2" - ansible_ec2_network_interfaces_macs_<mac address>_subnet_id: - description: The ID of the subnet in which the interface resides. Returned only for instances launched into a VPC. - type: str - sample: "subnet-01234567" - ansible_ec2_network_interfaces_macs_<mac address>_subnet_ipv4_cidr_block: - description: The IPv4 CIDR block of the subnet in which the interface resides. Returned only for instances launched into a VPC. - type: str - sample: "10.0.1.0/24" - ansible_ec2_network_interfaces_macs_<mac address>_subnet_ipv6_cidr_blocks: - description: The IPv6 CIDR block of the subnet in which the interface resides. Returned only for instances launched into a VPC. - type: str - sample: "" - ansible_ec2_network_interfaces_macs_<mac address>_vpc_id: - description: The ID of the VPC in which the interface resides. Returned only for instances launched into a VPC. - type: str - sample: "vpc-0123456" - ansible_ec2_network_interfaces_macs_<mac address>_vpc_ipv4_cidr_block: - description: The IPv4 CIDR block of the VPC in which the interface resides. Returned only for instances launched into a VPC. - type: str - sample: "10.0.0.0/16" - ansible_ec2_network_interfaces_macs_<mac address>_vpc_ipv4_cidr_blocks: - description: The IPv4 CIDR block of the VPC in which the interface resides. Returned only for instances launched into a VPC. - type: str - sample: "10.0.0.0/16" - ansible_ec2_network_interfaces_macs_<mac address>_vpc_ipv6_cidr_blocks: - description: The IPv6 CIDR block of the VPC in which the interface resides. Returned only for instances launched into a VPC. - type: str - sample: "" - ansible_ec2_placement_availability_zone: - description: The Availability Zone in which the instance launched. - type: str - sample: "us-east-1a" - ansible_ec2_placement_region: - description: The Region in which the instance launched. - type: str - sample: "us-east-1" - ansible_ec2_product_codes: - description: Product codes associated with the instance, if any. - type: str - sample: "aw0evgkw8e5c1q413zgy5pjce" - ansible_ec2_profile: - description: EC2 instance hardware profile. - type: str - sample: "default-hvm" - ansible_ec2_public_hostname: - description: - - The instance's public DNS. If the instance is in a VPC, this category is only returned if the enableDnsHostnames attribute is set to true. - type: str - sample: "ec2-1-2-3-4.compute-1.amazonaws.com" - ansible_ec2_public_ipv4: - description: The public IPv4 address. If an Elastic IP address is associated with the instance, the value returned is the Elastic IP address. - type: str - sample: "1.2.3.4" - ansible_ec2_public_key: - description: Public key. Only available if supplied at instance launch time. - type: str - sample: "" - ansible_ec2_ramdisk_id: - description: The ID of the RAM disk specified at launch time, if applicable. - type: str - sample: "" - ansible_ec2_reservation_id: - description: The ID of the reservation. - type: str - sample: "r-0123456789abcdef0" - ansible_ec2_security_groups: - description: - - The names of the security groups applied to the instance. After launch, you can only change the security groups of instances running in a VPC. - - Such changes are reflected here and in network/interfaces/macs/mac/security-groups. - type: str - sample: "securitygroup1,securitygroup2" - ansible_ec2_services_domain: - description: The domain for AWS resources for the region; for example, amazonaws.com for us-east-1. - type: str - sample: "amazonaws.com" - ansible_ec2_services_partition: - description: - - The partition that the resource is in. For standard AWS regions, the partition is aws. - - If you have resources in other partitions, the partition is aws-partitionname. - - For example, the partition for resources in the China (Beijing) region is aws-cn. - type: str - sample: "aws" - ansible_ec2_spot_termination_time: - description: - - The approximate time, in UTC, that the operating system for your Spot instance will receive the shutdown signal. - - This item is present and contains a time value only if the Spot instance has been marked for termination by Amazon EC2. - - The termination-time item is not set to a time if you terminated the Spot instance yourself. - type: str - sample: "2015-01-05T18:02:00Z" - ansible_ec2_user_data: - description: The instance user data. - type: str - sample: "#!/bin/bash" -''' - -import json -import re -import socket -import time - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_text -from ansible.module_utils.urls import fetch_url -from ansible.module_utils.six.moves.urllib.parse import quote - -socket.setdefaulttimeout(5) - - -class Ec2Metadata(object): - ec2_metadata_uri = 'http://169.254.169.254/latest/meta-data/' - ec2_sshdata_uri = 'http://169.254.169.254/latest/meta-data/public-keys/0/openssh-key' - ec2_userdata_uri = 'http://169.254.169.254/latest/user-data/' - ec2_dynamicdata_uri = 'http://169.254.169.254/latest/dynamic/' - - def __init__(self, module, ec2_metadata_uri=None, ec2_sshdata_uri=None, ec2_userdata_uri=None, ec2_dynamicdata_uri=None): - self.module = module - self.uri_meta = ec2_metadata_uri or self.ec2_metadata_uri - self.uri_user = ec2_userdata_uri or self.ec2_userdata_uri - self.uri_ssh = ec2_sshdata_uri or self.ec2_sshdata_uri - self.uri_dynamic = ec2_dynamicdata_uri or self.ec2_dynamicdata_uri - self._data = {} - self._prefix = 'ansible_ec2_%s' - - def _fetch(self, url): - encoded_url = quote(url, safe='%/:=&?~#+!$,;\'@()*[]') - response, info = fetch_url(self.module, encoded_url, force=True) - - if info.get('status') not in (200, 404): - time.sleep(3) - # request went bad, retry once then raise - self.module.warn('Retrying query to metadata service. First attempt failed: {0}'.format(info['msg'])) - response, info = fetch_url(self.module, encoded_url, force=True) - if info.get('status') not in (200, 404): - # fail out now - self.module.fail_json(msg='Failed to retrieve metadata from AWS: {0}'.format(info['msg']), response=info) - if response: - data = response.read() - else: - data = None - return to_text(data) - - def _mangle_fields(self, fields, uri, filter_patterns=None): - filter_patterns = ['public-keys-0'] if filter_patterns is None else filter_patterns - - new_fields = {} - for key, value in fields.items(): - split_fields = key[len(uri):].split('/') - # Parse out the IAM role name (which is _not_ the same as the instance profile name) - if len(split_fields) == 3 and split_fields[0:2] == ['iam', 'security-credentials'] and ':' not in split_fields[2]: - new_fields[self._prefix % "iam-instance-profile-role"] = split_fields[2] - if len(split_fields) > 1 and split_fields[1]: - new_key = "-".join(split_fields) - new_fields[self._prefix % new_key] = value - else: - new_key = "".join(split_fields) - new_fields[self._prefix % new_key] = value - for pattern in filter_patterns: - for key in dict(new_fields): - match = re.search(pattern, key) - if match: - new_fields.pop(key) - return new_fields - - def fetch(self, uri, recurse=True): - raw_subfields = self._fetch(uri) - if not raw_subfields: - return - subfields = raw_subfields.split('\n') - for field in subfields: - if field.endswith('/') and recurse: - self.fetch(uri + field) - if uri.endswith('/'): - new_uri = uri + field - else: - new_uri = uri + '/' + field - if new_uri not in self._data and not new_uri.endswith('/'): - content = self._fetch(new_uri) - if field == 'security-groups' or field == 'security-group-ids': - sg_fields = ",".join(content.split('\n')) - self._data['%s' % (new_uri)] = sg_fields - else: - try: - dict = json.loads(content) - self._data['%s' % (new_uri)] = content - for (key, value) in dict.items(): - self._data['%s:%s' % (new_uri, key.lower())] = value - except Exception: - self._data['%s' % (new_uri)] = content # not a stringified JSON string - - def fix_invalid_varnames(self, data): - """Change ':'' and '-' to '_' to ensure valid template variable names""" - new_data = data.copy() - for key, value in data.items(): - if ':' in key or '-' in key: - newkey = re.sub(':|-', '_', key) - new_data[newkey] = value - del new_data[key] - - return new_data - - def run(self): - self.fetch(self.uri_meta) # populate _data with metadata - data = self._mangle_fields(self._data, self.uri_meta) - data[self._prefix % 'user-data'] = self._fetch(self.uri_user) - data[self._prefix % 'public-key'] = self._fetch(self.uri_ssh) - - self._data = {} # clear out metadata in _data - self.fetch(self.uri_dynamic) # populate _data with dynamic data - dyndata = self._mangle_fields(self._data, self.uri_dynamic) - data.update(dyndata) - data = self.fix_invalid_varnames(data) - - # Maintain old key for backwards compatibility - if 'ansible_ec2_instance_identity_document_region' in data: - data['ansible_ec2_placement_region'] = data['ansible_ec2_instance_identity_document_region'] - return data - - -def main(): - module = AnsibleModule( - argument_spec={}, - supports_check_mode=True, - ) - - ec2_metadata_facts = Ec2Metadata(module).run() - ec2_metadata_facts_result = dict(changed=False, ansible_facts=ec2_metadata_facts) - - module.exit_json(**ec2_metadata_facts_result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_snapshot.py b/lib/ansible/modules/cloud/amazon/ec2_snapshot.py deleted file mode 100644 index d3c76ee831..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_snapshot.py +++ /dev/null @@ -1,336 +0,0 @@ -#!/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 - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'core'} - - -DOCUMENTATION = ''' ---- -module: ec2_snapshot -short_description: Creates a snapshot from an existing volume -description: - - Creates an EC2 snapshot from an existing EBS volume. -version_added: "1.5" -options: - volume_id: - description: - - Volume from which to take the snapshot. - required: false - type: str - description: - description: - - Description to be applied to the snapshot. - required: false - type: str - instance_id: - description: - - Instance that has the required volume to snapshot mounted. - required: false - type: str - device_name: - description: - - Device name of a mounted volume to be snapshotted. - required: false - type: str - snapshot_tags: - description: - - A dictionary of tags to add to the snapshot. - type: dict - required: false - version_added: "1.6" - wait: - description: - - Wait for the snapshot to be ready. - type: bool - required: false - default: yes - version_added: "1.5.1" - wait_timeout: - description: - - How long before wait gives up, in seconds. - - Specify 0 to wait forever. - required: false - default: 0 - version_added: "1.5.1" - type: int - state: - description: - - Whether to add or create a snapshot. - required: false - default: present - choices: ['absent', 'present'] - version_added: "1.9" - type: str - snapshot_id: - description: - - Snapshot id to remove. - required: false - version_added: "1.9" - type: str - last_snapshot_min_age: - description: - - If the volume's most recent snapshot has started less than `last_snapshot_min_age' minutes ago, a new snapshot will not be created. - required: false - default: 0 - version_added: "2.0" - type: int - -author: "Will Thames (@willthames)" -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Simple snapshot of volume using volume_id -- ec2_snapshot: - volume_id: vol-abcdef12 - description: snapshot of /data from DB123 taken 2013/11/28 12:18:32 - -# Snapshot of volume mounted on device_name attached to instance_id -- ec2_snapshot: - instance_id: i-12345678 - device_name: /dev/sdb1 - description: snapshot of /data from DB123 taken 2013/11/28 12:18:32 - -# Snapshot of volume with tagging -- ec2_snapshot: - instance_id: i-12345678 - device_name: /dev/sdb1 - snapshot_tags: - frequency: hourly - source: /data - -# Remove a snapshot -- local_action: - module: ec2_snapshot - snapshot_id: snap-abcd1234 - state: absent - -# Create a snapshot only if the most recent one is older than 1 hour -- local_action: - module: ec2_snapshot - volume_id: vol-abcdef12 - last_snapshot_min_age: 60 -''' - -RETURN = ''' -snapshot_id: - description: The ID of the snapshot. Each snapshot receives a unique identifier when it is created. - type: str - returned: always - sample: snap-01234567 -tags: - description: Any tags assigned to the snapshot. - type: dict - returned: always - sample: "{ 'Name': 'instance-name' }" -volume_id: - description: The ID of the volume that was used to create the snapshot. - type: str - returned: always - sample: vol-01234567 -volume_size: - description: The size of the volume, in GiB. - type: int - returned: always - sample: 8 -''' - -import time -import datetime - -try: - import boto.exception -except ImportError: - pass # Taken care of by ec2.HAS_BOTO - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import HAS_BOTO, ec2_argument_spec, ec2_connect - - -# Find the most recent snapshot -def _get_snapshot_starttime(snap): - return datetime.datetime.strptime(snap.start_time, '%Y-%m-%dT%H:%M:%S.%fZ') - - -def _get_most_recent_snapshot(snapshots, max_snapshot_age_secs=None, now=None): - """ - Gets the most recently created snapshot and optionally filters the result - if the snapshot is too old - :param snapshots: list of snapshots to search - :param max_snapshot_age_secs: filter the result if its older than this - :param now: simulate time -- used for unit testing - :return: - """ - if len(snapshots) == 0: - return None - - if not now: - now = datetime.datetime.utcnow() - - youngest_snapshot = max(snapshots, key=_get_snapshot_starttime) - - # See if the snapshot is younger that the given max age - snapshot_start = datetime.datetime.strptime(youngest_snapshot.start_time, '%Y-%m-%dT%H:%M:%S.%fZ') - snapshot_age = now - snapshot_start - - if max_snapshot_age_secs is not None: - if snapshot_age.total_seconds() > max_snapshot_age_secs: - return None - - return youngest_snapshot - - -def _create_with_wait(snapshot, wait_timeout_secs, sleep_func=time.sleep): - """ - Wait for the snapshot to be created - :param snapshot: - :param wait_timeout_secs: fail this step after this many seconds - :param sleep_func: - :return: - """ - time_waited = 0 - snapshot.update() - while snapshot.status != 'completed': - sleep_func(3) - snapshot.update() - time_waited += 3 - if wait_timeout_secs and time_waited > wait_timeout_secs: - return False - return True - - -def create_snapshot(module, ec2, state=None, description=None, wait=None, - wait_timeout=None, volume_id=None, instance_id=None, - snapshot_id=None, device_name=None, snapshot_tags=None, - last_snapshot_min_age=None): - snapshot = None - changed = False - - required = [volume_id, snapshot_id, instance_id] - if required.count(None) != len(required) - 1: # only 1 must be set - module.fail_json(msg='One and only one of volume_id or instance_id or snapshot_id must be specified') - if instance_id and not device_name or device_name and not instance_id: - module.fail_json(msg='Instance ID and device name must both be specified') - - if instance_id: - try: - volumes = ec2.get_all_volumes(filters={'attachment.instance-id': instance_id, 'attachment.device': device_name}) - except boto.exception.BotoServerError as e: - module.fail_json(msg="%s: %s" % (e.error_code, e.error_message)) - - if not volumes: - module.fail_json(msg="Could not find volume with name %s attached to instance %s" % (device_name, instance_id)) - - volume_id = volumes[0].id - - if state == 'absent': - if not snapshot_id: - module.fail_json(msg='snapshot_id must be set when state is absent') - try: - ec2.delete_snapshot(snapshot_id) - except boto.exception.BotoServerError as e: - # exception is raised if snapshot does not exist - if e.error_code == 'InvalidSnapshot.NotFound': - module.exit_json(changed=False) - else: - module.fail_json(msg="%s: %s" % (e.error_code, e.error_message)) - - # successful delete - module.exit_json(changed=True) - - if last_snapshot_min_age > 0: - try: - current_snapshots = ec2.get_all_snapshots(filters={'volume_id': volume_id}) - except boto.exception.BotoServerError as e: - module.fail_json(msg="%s: %s" % (e.error_code, e.error_message)) - - last_snapshot_min_age = last_snapshot_min_age * 60 # Convert to seconds - snapshot = _get_most_recent_snapshot(current_snapshots, - max_snapshot_age_secs=last_snapshot_min_age) - try: - # Create a new snapshot if we didn't find an existing one to use - if snapshot is None: - snapshot = ec2.create_snapshot(volume_id, description=description) - changed = True - if wait: - if not _create_with_wait(snapshot, wait_timeout): - module.fail_json(msg='Timed out while creating snapshot.') - if snapshot_tags: - for k, v in snapshot_tags.items(): - snapshot.add_tag(k, v) - except boto.exception.BotoServerError as e: - module.fail_json(msg="%s: %s" % (e.error_code, e.error_message)) - - module.exit_json(changed=changed, - snapshot_id=snapshot.id, - volume_id=snapshot.volume_id, - volume_size=snapshot.volume_size, - tags=snapshot.tags.copy()) - - -def create_snapshot_ansible_module(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - volume_id=dict(), - description=dict(), - instance_id=dict(), - snapshot_id=dict(), - device_name=dict(), - wait=dict(type='bool', default=True), - wait_timeout=dict(type='int', default=0), - last_snapshot_min_age=dict(type='int', default=0), - snapshot_tags=dict(type='dict', default=dict()), - state=dict(choices=['absent', 'present'], default='present'), - ) - ) - module = AnsibleModule(argument_spec=argument_spec) - return module - - -def main(): - module = create_snapshot_ansible_module() - - if not HAS_BOTO: - module.fail_json(msg='boto required for this module') - - volume_id = module.params.get('volume_id') - snapshot_id = module.params.get('snapshot_id') - description = module.params.get('description') - instance_id = module.params.get('instance_id') - device_name = module.params.get('device_name') - wait = module.params.get('wait') - wait_timeout = module.params.get('wait_timeout') - last_snapshot_min_age = module.params.get('last_snapshot_min_age') - snapshot_tags = module.params.get('snapshot_tags') - state = module.params.get('state') - - ec2 = ec2_connect(module) - - create_snapshot( - module=module, - state=state, - description=description, - wait=wait, - wait_timeout=wait_timeout, - ec2=ec2, - volume_id=volume_id, - instance_id=instance_id, - snapshot_id=snapshot_id, - device_name=device_name, - snapshot_tags=snapshot_tags, - last_snapshot_min_age=last_snapshot_min_age - ) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_snapshot_info.py b/lib/ansible/modules/cloud/amazon/ec2_snapshot_info.py deleted file mode 100644 index b48ad4efa9..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_snapshot_info.py +++ /dev/null @@ -1,258 +0,0 @@ -#!/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 - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_snapshot_info -short_description: Gather information about ec2 volume snapshots in AWS -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. -version_added: "2.1" -requirements: [ boto3 ] -author: "Rob White (@wimnat)" -options: - snapshot_ids: - description: - - If you specify one or more snapshot IDs, only snapshots that have the specified IDs are returned. - required: false - default: [] - type: list - elements: str - owner_ids: - description: - - If you specify one or more snapshot owners, only snapshots from the specified owners and for which you have - access are returned. - required: false - default: [] - type: list - elements: str - restorable_by_user_ids: - description: - - If you specify a list of restorable users, only snapshots with create snapshot permissions for those users are - returned. - required: false - default: [] - 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_DescribeSnapshots.html) for possible filters. Filter - names and values are case sensitive. - required: false - type: dict - default: {} -notes: - - By default, the module will return all snapshots, including public ones. To limit results to snapshots owned by - the account use the filter 'owner-id'. - -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Gather information about all snapshots, including public ones -- ec2_snapshot_info: - -# Gather information about all snapshots owned by the account 0123456789 -- ec2_snapshot_info: - filters: - owner-id: 0123456789 - -# Or alternatively... -- ec2_snapshot_info: - owner_ids: - - 0123456789 - -# Gather information about a particular snapshot using ID -- ec2_snapshot_info: - filters: - snapshot-id: snap-00112233 - -# Or alternatively... -- ec2_snapshot_info: - snapshot_ids: - - snap-00112233 - -# Gather information about any snapshot with a tag key Name and value Example -- ec2_snapshot_info: - filters: - "tag:Name": Example - -# Gather information about any snapshot with an error status -- ec2_snapshot_info: - filters: - status: error - -''' - -RETURN = ''' -snapshot_id: - description: The ID of the snapshot. Each snapshot receives a unique identifier when it is created. - type: str - returned: always - sample: snap-01234567 -volume_id: - description: The ID of the volume that was used to create the snapshot. - type: str - returned: always - sample: vol-01234567 -state: - description: The snapshot state (completed, pending or error). - type: str - returned: always - sample: completed -state_message: - description: Encrypted Amazon EBS snapshots are copied asynchronously. If a snapshot copy operation fails (for example, if the proper - AWS Key Management Service (AWS KMS) permissions are not obtained) this field displays error state details to help you diagnose why the - error occurred. - type: str - returned: always - sample: -start_time: - description: The time stamp when the snapshot was initiated. - type: str - returned: always - sample: "2015-02-12T02:14:02+00:00" -progress: - description: The progress of the snapshot, as a percentage. - type: str - returned: always - sample: "100%" -owner_id: - description: The AWS account ID of the EBS snapshot owner. - type: str - returned: always - sample: "099720109477" -description: - description: The description for the snapshot. - type: str - returned: always - sample: "My important backup" -volume_size: - description: The size of the volume, in GiB. - type: int - returned: always - sample: 8 -owner_alias: - description: The AWS account alias (for example, amazon, self) or AWS account ID that owns the snapshot. - type: str - returned: always - sample: "033440102211" -tags: - description: Any tags assigned to the snapshot. - type: dict - returned: always - sample: "{ 'my_tag_key': 'my_tag_value' }" -encrypted: - description: Indicates whether the snapshot is encrypted. - type: bool - returned: always - sample: "True" -kms_key_id: - description: The full ARN of the AWS Key Management Service (AWS KMS) customer master key (CMK) that was used to \ - protect the volume encryption key for the parent volume. - type: str - returned: always - sample: "74c9742a-a1b2-45cb-b3fe-abcdef123456" -data_encryption_key_id: - description: The data encryption key identifier for the snapshot. This value is a unique identifier that \ - corresponds to the data encryption key that was used to encrypt the original volume or snapshot copy. - type: str - returned: always - sample: "arn:aws:kms:ap-southeast-2:012345678900:key/74c9742a-a1b2-45cb-b3fe-abcdef123456" - -''' - -try: - import boto3 - from botocore.exceptions import ClientError - HAS_BOTO3 = True -except ImportError: - HAS_BOTO3 = False - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (ansible_dict_to_boto3_filter_list, - boto3_conn, boto3_tag_list_to_ansible_dict, camel_dict_to_snake_dict, - ec2_argument_spec, get_aws_connection_info) - - -def list_ec2_snapshots(connection, module): - - snapshot_ids = module.params.get("snapshot_ids") - owner_ids = [str(owner_id) for owner_id in module.params.get("owner_ids")] - restorable_by_user_ids = [str(user_id) for user_id in module.params.get("restorable_by_user_ids")] - filters = ansible_dict_to_boto3_filter_list(module.params.get("filters")) - - try: - snapshots = connection.describe_snapshots(SnapshotIds=snapshot_ids, OwnerIds=owner_ids, RestorableByUserIds=restorable_by_user_ids, Filters=filters) - except ClientError as e: - if e.response['Error']['Code'] == "InvalidSnapshot.NotFound": - if len(snapshot_ids) > 1: - module.warn("Some of your snapshots may exist, but %s" % str(e)) - snapshots = {'Snapshots': []} - else: - module.fail_json(msg="Failed to describe snapshots: %s" % str(e)) - - # Turn the boto3 result in to ansible_friendly_snaked_names - snaked_snapshots = [] - for snapshot in snapshots['Snapshots']: - snaked_snapshots.append(camel_dict_to_snake_dict(snapshot)) - - # Turn the boto3 result in to ansible friendly tag dictionary - for snapshot in snaked_snapshots: - if 'tags' in snapshot: - snapshot['tags'] = boto3_tag_list_to_ansible_dict(snapshot['tags'], 'key', 'value') - - module.exit_json(snapshots=snaked_snapshots) - - -def main(): - - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - snapshot_ids=dict(default=[], type='list'), - owner_ids=dict(default=[], type='list'), - restorable_by_user_ids=dict(default=[], type='list'), - filters=dict(default={}, type='dict') - ) - ) - - module = AnsibleModule(argument_spec=argument_spec, - mutually_exclusive=[ - ['snapshot_ids', 'owner_ids', 'restorable_by_user_ids', 'filters'] - ] - ) - if module._name == 'ec2_snapshot_facts': - module.deprecate("The 'ec2_snapshot_facts' module has been renamed to 'ec2_snapshot_info'", version='2.13') - - if not HAS_BOTO3: - module.fail_json(msg='boto3 required for this module') - - region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) - - if region: - connection = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_params) - else: - module.fail_json(msg="region must be specified") - - list_ec2_snapshots(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_tag.py b/lib/ansible/modules/cloud/amazon/ec2_tag.py deleted file mode 100644 index 9e10b5eda6..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_tag.py +++ /dev/null @@ -1,201 +0,0 @@ -#!/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 - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_tag -short_description: create and remove tags on ec2 resources -description: - - 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. -version_added: "1.3" -requirements: [ "boto3", "botocore" ] -options: - resource: - description: - - The EC2 resource id. - required: true - type: str - state: - description: - - Whether the tags should be present or absent on the resource. - - The use of I(state=list) to interrogate the tags of an instance has been - deprecated and will be removed in Anisble 2.14. The 'list' - functionality has been moved to a dedicated module M(ec2_tag_info). - default: present - choices: ['present', 'absent', 'list'] - type: str - tags: - description: - - A dictionary of tags to add or remove from the resource. - - If the value provided for a key is not set and I(state=absent), the tag will be removed regardless of its current value. - - Required when I(state=present) or I(state=absent). - type: dict - purge_tags: - description: - - Whether unspecified tags should be removed from the resource. - - Note that when combined with I(state=absent), specified tags with non-matching values are not purged. - type: bool - default: false - version_added: '2.7' - -author: - - Lester Wade (@lwade) - - Paul Arthur (@flowerysong) -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -- name: Ensure tags are present on a resource - ec2_tag: - region: eu-west-1 - resource: vol-XXXXXX - state: present - tags: - Name: ubervol - env: prod - -- name: Ensure all volumes are tagged - ec2_tag: - region: eu-west-1 - resource: '{{ item.id }}' - state: present - tags: - Name: dbserver - Env: production - loop: '{{ ec2_vol.volumes }}' - -- name: Remove the Env tag - ec2_tag: - region: eu-west-1 - resource: i-xxxxxxxxxxxxxxxxx - tags: - Env: - state: absent - -- name: Remove the Env tag if it's currently 'development' - ec2_tag: - region: eu-west-1 - resource: i-xxxxxxxxxxxxxxxxx - tags: - Env: development - state: absent - -- name: Remove all tags except for Name from an instance - ec2_tag: - region: eu-west-1 - resource: i-xxxxxxxxxxxxxxxxx - tags: - Name: '' - state: absent - purge_tags: true -''' - -RETURN = ''' -tags: - description: A dict containing the tags on the resource - returned: always - type: dict -added_tags: - description: A dict of tags that were added to the resource - returned: If tags were added - type: dict -removed_tags: - description: A dict of tags that were removed from the resource - returned: If tags were removed - type: dict -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import boto3_tag_list_to_ansible_dict, ansible_dict_to_boto3_tag_list, compare_aws_tags - -try: - from botocore.exceptions import BotoCoreError, ClientError -except Exception: - pass # Handled by AnsibleAWSModule - - -def get_tags(ec2, module, resource): - filters = [{'Name': 'resource-id', 'Values': [resource]}] - try: - return boto3_tag_list_to_ansible_dict(ec2.describe_tags(Filters=filters)['Tags']) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to fetch tags for resource {0}'.format(resource)) - - -def main(): - argument_spec = dict( - resource=dict(required=True), - tags=dict(type='dict'), - purge_tags=dict(type='bool', default=False), - state=dict(default='present', choices=['present', 'absent', 'list']), - ) - required_if = [('state', 'present', ['tags']), ('state', 'absent', ['tags'])] - - module = AnsibleAWSModule(argument_spec=argument_spec, required_if=required_if, supports_check_mode=True) - - resource = module.params['resource'] - tags = module.params['tags'] - state = module.params['state'] - purge_tags = module.params['purge_tags'] - - result = {'changed': False} - - ec2 = module.client('ec2') - - current_tags = get_tags(ec2, module, resource) - - if state == 'list': - module.deprecate( - 'Using the "list" state has been deprecated. Please use the ec2_tag_info module instead', version='2.14') - module.exit_json(changed=False, tags=current_tags) - - add_tags, remove = compare_aws_tags(current_tags, tags, purge_tags=purge_tags) - - remove_tags = {} - if state == 'absent': - for key in tags: - if key in current_tags and (tags[key] is None or current_tags[key] == tags[key]): - remove_tags[key] = current_tags[key] - - for key in remove: - remove_tags[key] = current_tags[key] - - if remove_tags: - result['changed'] = True - result['removed_tags'] = remove_tags - if not module.check_mode: - try: - ec2.delete_tags(Resources=[resource], Tags=ansible_dict_to_boto3_tag_list(remove_tags)) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to remove tags {0} from resource {1}'.format(remove_tags, resource)) - - if state == 'present' and add_tags: - result['changed'] = True - result['added_tags'] = add_tags - current_tags.update(add_tags) - if not module.check_mode: - try: - ec2.create_tags(Resources=[resource], Tags=ansible_dict_to_boto3_tag_list(add_tags)) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to set tags {0} on resource {1}'.format(add_tags, resource)) - - result['tags'] = get_tags(ec2, module, resource) - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_tag_info.py b/lib/ansible/modules/cloud/amazon/ec2_tag_info.py deleted file mode 100644 index 6e607763b8..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_tag_info.py +++ /dev/null @@ -1,92 +0,0 @@ -#!/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 - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_tag_info -short_description: list tags on ec2 resources -description: - - 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(ec2_tag) module. -version_added: "2.10" -requirements: [ "boto3", "botocore" ] -options: - resource: - description: - - The EC2 resource id (for example i-XXXXXX or vpc-XXXXXX). - required: true - type: str - -author: - - Mark Chappell (@tremble) -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -- name: Retrieve all tags on an instance - ec2_tag_info: - region: eu-west-1 - resource: i-xxxxxxxxxxxxxxxxx - register: instance_tags - -- name: Retrieve all tags on a VPC - ec2_tag_info: - region: eu-west-1 - resource: vpc-xxxxxxxxxxxxxxxxx - register: vpc_tags -''' - -RETURN = ''' -tags: - description: A dict containing the tags on the resource - returned: always - type: dict -''' - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import boto3_tag_list_to_ansible_dict, AWSRetry - -try: - from botocore.exceptions import BotoCoreError, ClientError -except Exception: - pass # Handled by AnsibleAWSModule - - -@AWSRetry.jittered_backoff() -def get_tags(ec2, module, resource): - filters = [{'Name': 'resource-id', 'Values': [resource]}] - return boto3_tag_list_to_ansible_dict(ec2.describe_tags(Filters=filters)['Tags']) - - -def main(): - argument_spec = dict( - resource=dict(required=True), - ) - - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) - resource = module.params['resource'] - ec2 = module.client('ec2') - - try: - current_tags = get_tags(ec2, module, resource) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg='Failed to fetch tags for resource {0}'.format(resource)) - - module.exit_json(changed=False, tags=current_tags) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_vol.py b/lib/ansible/modules/cloud/amazon/ec2_vol.py deleted file mode 100644 index aeff60d06e..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_vol.py +++ /dev/null @@ -1,632 +0,0 @@ -#!/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 - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'core'} - - -DOCUMENTATION = ''' ---- -module: ec2_vol -short_description: Create and attach a volume, return volume id and device map -description: - - Creates an EBS volume and optionally attaches it to an instance. - - If both I(instance) and I(name) are given and the instance has a device at the device name, then no volume is created and no attachment is made. - - This module has a dependency on python-boto. -version_added: "1.1" -options: - instance: - description: - - Instance ID if you wish to attach the volume. Since 1.9 you can set to None to detach. - type: str - name: - description: - - Volume Name tag if you wish to attach an existing volume (requires instance) - version_added: "1.6" - type: str - id: - description: - - Volume id if you wish to attach an existing volume (requires instance) or remove an existing volume - version_added: "1.6" - type: str - volume_size: - description: - - Size of volume (in GiB) to create. - type: int - volume_type: - description: - - Type of EBS volume; standard (magnetic), gp2 (SSD), io1 (Provisioned IOPS), st1 (Throughput Optimized HDD), sc1 (Cold HDD). - "Standard" is the old EBS default and continues to remain the Ansible default for backwards compatibility. - default: standard - version_added: "1.9" - choices: ['standard', 'gp2', 'io1', 'st1', 'sc1'] - type: str - iops: - description: - - The provisioned IOPs you want to associate with this volume (integer). - - By default AWS will set this to 100. - version_added: "1.3" - type: int - encrypted: - description: - - Enable encryption at rest for this volume. - default: false - type: bool - version_added: "1.8" - kms_key_id: - description: - - Specify the id of the KMS key to use. - version_added: "2.3" - type: str - device_name: - description: - - Device id to override device mapping. Assumes /dev/sdf for Linux/UNIX and /dev/xvdf for Windows. - type: str - delete_on_termination: - description: - - When set to C(true), the volume will be deleted upon instance termination. - type: bool - default: false - version_added: "2.1" - zone: - description: - - Zone in which to create the volume, if unset uses the zone the instance is in (if set). - aliases: ['availability_zone', 'aws_zone', 'ec2_zone'] - type: str - snapshot: - description: - - Snapshot ID on which to base the volume. - version_added: "1.5" - type: str - validate_certs: - description: - - When set to "no", SSL certificates will not be validated for boto versions >= 2.6.0. - type: bool - default: true - version_added: "1.5" - state: - description: - - Whether to ensure the volume is present or absent, or to list existing volumes (The C(list) option was added in version 1.8). - default: present - choices: ['absent', 'present', 'list'] - version_added: "1.6" - type: str - tags: - description: - - tag:value pairs to add to the volume after creation. - default: {} - version_added: "2.3" - type: dict -author: "Lester Wade (@lwade)" -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Simple attachment action -- ec2_vol: - instance: XXXXXX - volume_size: 5 - device_name: sdd - -# Example using custom iops params -- ec2_vol: - instance: XXXXXX - volume_size: 5 - iops: 100 - device_name: sdd - -# Example using snapshot id -- ec2_vol: - instance: XXXXXX - snapshot: "{{ snapshot }}" - -# Playbook example combined with instance launch -- ec2: - keypair: "{{ keypair }}" - image: "{{ image }}" - wait: yes - count: 3 - register: ec2 -- ec2_vol: - instance: "{{ item.id }}" - volume_size: 5 - loop: "{{ ec2.instances }}" - register: ec2_vol - -# Example: Launch an instance and then add a volume if not already attached -# * Volume will be created with the given name if not already created. -# * Nothing will happen if the volume is already attached. -# * Requires Ansible 2.0 - -- ec2: - keypair: "{{ keypair }}" - image: "{{ image }}" - zone: YYYYYY - id: my_instance - wait: yes - count: 1 - register: ec2 - -- ec2_vol: - instance: "{{ item.id }}" - name: my_existing_volume_Name_tag - device_name: /dev/xvdf - loop: "{{ ec2.instances }}" - register: ec2_vol - -# Remove a volume -- ec2_vol: - id: vol-XXXXXXXX - state: absent - -# Detach a volume (since 1.9) -- ec2_vol: - id: vol-XXXXXXXX - instance: None - -# List volumes for an instance -- ec2_vol: - instance: i-XXXXXX - state: list - -# Create new volume using SSD storage -- ec2_vol: - instance: XXXXXX - volume_size: 50 - volume_type: gp2 - device_name: /dev/xvdf - -# Attach an existing volume to instance. The volume will be deleted upon instance termination. -- ec2_vol: - instance: XXXXXX - id: XXXXXX - device_name: /dev/sdf - delete_on_termination: yes -''' - -RETURN = ''' -device: - description: device name of attached volume - returned: when success - type: str - sample: "/def/sdf" -volume_id: - description: the id of volume - returned: when success - type: str - sample: "vol-35b333d9" -volume_type: - description: the volume type - returned: when success - type: str - sample: "standard" -volume: - description: a dictionary containing detailed attributes of the volume - returned: when success - type: str - sample: { - "attachment_set": { - "attach_time": "2015-10-23T00:22:29.000Z", - "deleteOnTermination": "false", - "device": "/dev/sdf", - "instance_id": "i-8356263c", - "status": "attached" - }, - "create_time": "2015-10-21T14:36:08.870Z", - "encrypted": false, - "id": "vol-35b333d9", - "iops": null, - "size": 1, - "snapshot_id": "", - "status": "in-use", - "tags": { - "env": "dev" - }, - "type": "standard", - "zone": "us-east-1b" - } -''' - -import time - -from distutils.version import LooseVersion - -try: - import boto - import boto.ec2 - import boto.exception - from boto.exception import BotoServerError - from boto.ec2.blockdevicemapping import BlockDeviceType, BlockDeviceMapping -except ImportError: - pass # Taken care of by ec2.HAS_BOTO - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (HAS_BOTO, AnsibleAWSError, connect_to_aws, ec2_argument_spec, - get_aws_connection_info) - - -def get_volume(module, ec2): - name = module.params.get('name') - id = module.params.get('id') - zone = module.params.get('zone') - filters = {} - volume_ids = None - - # If no name or id supplied, just try volume creation based on module parameters - if id is None and name is None: - return None - - if zone: - filters['availability_zone'] = zone - if name: - filters['tag:Name'] = name - if id: - volume_ids = [id] - try: - vols = ec2.get_all_volumes(volume_ids=volume_ids, filters=filters) - except boto.exception.BotoServerError as e: - module.fail_json(msg="%s: %s" % (e.error_code, e.error_message)) - - if not vols: - if id: - msg = "Could not find the volume with id: %s" % id - if name: - msg += (" and name: %s" % name) - module.fail_json(msg=msg) - else: - return None - - if len(vols) > 1: - module.fail_json(msg="Found more than one volume in zone (if specified) with name: %s" % name) - return vols[0] - - -def get_volumes(module, ec2): - - instance = module.params.get('instance') - - try: - if not instance: - vols = ec2.get_all_volumes() - else: - vols = ec2.get_all_volumes(filters={'attachment.instance-id': instance}) - except boto.exception.BotoServerError as e: - module.fail_json(msg="%s: %s" % (e.error_code, e.error_message)) - return vols - - -def delete_volume(module, ec2): - volume_id = module.params['id'] - try: - ec2.delete_volume(volume_id) - module.exit_json(changed=True) - except boto.exception.EC2ResponseError as ec2_error: - if ec2_error.code == 'InvalidVolume.NotFound': - module.exit_json(changed=False) - module.fail_json(msg=ec2_error.message) - - -def boto_supports_volume_encryption(): - """ - Check if Boto library supports encryption of EBS volumes (added in 2.29.0) - - Returns: - True if boto library has the named param as an argument on the request_spot_instances method, else False - """ - return hasattr(boto, 'Version') and LooseVersion(boto.Version) >= LooseVersion('2.29.0') - - -def boto_supports_kms_key_id(): - """ - Check if Boto library supports kms_key_ids (added in 2.39.0) - - Returns: - True if version is equal to or higher then the version needed, else False - """ - return hasattr(boto, 'Version') and LooseVersion(boto.Version) >= LooseVersion('2.39.0') - - -def create_volume(module, ec2, zone): - changed = False - name = module.params.get('name') - iops = module.params.get('iops') - encrypted = module.params.get('encrypted') - kms_key_id = module.params.get('kms_key_id') - volume_size = module.params.get('volume_size') - volume_type = module.params.get('volume_type') - snapshot = module.params.get('snapshot') - tags = module.params.get('tags') - # If custom iops is defined we use volume_type "io1" rather than the default of "standard" - if iops: - volume_type = 'io1' - - volume = get_volume(module, ec2) - if volume is None: - try: - if boto_supports_volume_encryption(): - if kms_key_id is not None: - volume = ec2.create_volume(volume_size, zone, snapshot, volume_type, iops, encrypted, kms_key_id) - else: - volume = ec2.create_volume(volume_size, zone, snapshot, volume_type, iops, encrypted) - changed = True - else: - volume = ec2.create_volume(volume_size, zone, snapshot, volume_type, iops) - changed = True - - while volume.status != 'available': - time.sleep(3) - volume.update() - - if name: - tags["Name"] = name - if tags: - ec2.create_tags([volume.id], tags) - except boto.exception.BotoServerError as e: - module.fail_json(msg="%s: %s" % (e.error_code, e.error_message)) - - return volume, changed - - -def attach_volume(module, ec2, volume, instance): - - device_name = module.params.get('device_name') - delete_on_termination = module.params.get('delete_on_termination') - changed = False - - # If device_name isn't set, make a choice based on best practices here: - # https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/block-device-mapping-concepts.html - - # In future this needs to be more dynamic but combining block device mapping best practices - # (bounds for devices, as above) with instance.block_device_mapping data would be tricky. For me ;) - - # Use password data attribute to tell whether the instance is Windows or Linux - if device_name is None: - try: - if not ec2.get_password_data(instance.id): - device_name = '/dev/sdf' - else: - device_name = '/dev/xvdf' - except boto.exception.BotoServerError as e: - module.fail_json(msg="%s: %s" % (e.error_code, e.error_message)) - - if volume.attachment_state() is not None: - adata = volume.attach_data - if adata.instance_id != instance.id: - module.fail_json(msg="Volume %s is already attached to another instance: %s" - % (volume.id, adata.instance_id)) - else: - # Volume is already attached to right instance - changed = modify_dot_attribute(module, ec2, instance, device_name) - else: - try: - volume.attach(instance.id, device_name) - while volume.attachment_state() != 'attached': - time.sleep(3) - volume.update() - changed = True - except boto.exception.BotoServerError as e: - module.fail_json(msg="%s: %s" % (e.error_code, e.error_message)) - - modify_dot_attribute(module, ec2, instance, device_name) - - return volume, changed - - -def modify_dot_attribute(module, ec2, instance, device_name): - """ Modify delete_on_termination attribute """ - - delete_on_termination = module.params.get('delete_on_termination') - changed = False - - try: - instance.update() - dot = instance.block_device_mapping[device_name].delete_on_termination - except boto.exception.BotoServerError as e: - module.fail_json(msg="%s: %s" % (e.error_code, e.error_message)) - - if delete_on_termination != dot: - try: - bdt = BlockDeviceType(delete_on_termination=delete_on_termination) - bdm = BlockDeviceMapping() - bdm[device_name] = bdt - - ec2.modify_instance_attribute(instance_id=instance.id, attribute='blockDeviceMapping', value=bdm) - - while instance.block_device_mapping[device_name].delete_on_termination != delete_on_termination: - time.sleep(3) - instance.update() - changed = True - except boto.exception.BotoServerError as e: - module.fail_json(msg="%s: %s" % (e.error_code, e.error_message)) - - return changed - - -def detach_volume(module, ec2, volume): - - changed = False - - if volume.attachment_state() is not None: - adata = volume.attach_data - volume.detach() - while volume.attachment_state() is not None: - time.sleep(3) - volume.update() - changed = True - - return volume, changed - - -def get_volume_info(volume, state): - - # If we're just listing volumes then do nothing, else get the latest update for the volume - if state != 'list': - volume.update() - - volume_info = {} - attachment = volume.attach_data - - volume_info = { - 'create_time': volume.create_time, - 'encrypted': volume.encrypted, - 'id': volume.id, - 'iops': volume.iops, - 'size': volume.size, - 'snapshot_id': volume.snapshot_id, - 'status': volume.status, - 'type': volume.type, - 'zone': volume.zone, - 'attachment_set': { - 'attach_time': attachment.attach_time, - 'device': attachment.device, - 'instance_id': attachment.instance_id, - 'status': attachment.status - }, - 'tags': volume.tags - } - if hasattr(attachment, 'deleteOnTermination'): - volume_info['attachment_set']['deleteOnTermination'] = attachment.deleteOnTermination - - return volume_info - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - instance=dict(), - id=dict(), - name=dict(), - volume_size=dict(type='int'), - volume_type=dict(choices=['standard', 'gp2', 'io1', 'st1', 'sc1'], default='standard'), - iops=dict(type='int'), - encrypted=dict(type='bool', default=False), - kms_key_id=dict(), - device_name=dict(), - delete_on_termination=dict(type='bool', default=False), - zone=dict(aliases=['availability_zone', 'aws_zone', 'ec2_zone']), - snapshot=dict(), - state=dict(choices=['absent', 'present', 'list'], default='present'), - tags=dict(type='dict', default={}) - ) - ) - module = AnsibleModule(argument_spec=argument_spec) - - if not HAS_BOTO: - module.fail_json(msg='boto required for this module') - - id = module.params.get('id') - name = module.params.get('name') - instance = module.params.get('instance') - volume_size = module.params.get('volume_size') - encrypted = module.params.get('encrypted') - kms_key_id = module.params.get('kms_key_id') - device_name = module.params.get('device_name') - zone = module.params.get('zone') - snapshot = module.params.get('snapshot') - state = module.params.get('state') - tags = module.params.get('tags') - - # Ensure we have the zone or can get the zone - if instance is None and zone is None and state == 'present': - module.fail_json(msg="You must specify either instance or zone") - - # Set volume detach flag - if instance == 'None' or instance == '': - instance = None - detach_vol_flag = True - else: - detach_vol_flag = False - - # Set changed flag - changed = False - - region, ec2_url, aws_connect_params = get_aws_connection_info(module) - - if region: - try: - ec2 = connect_to_aws(boto.ec2, region, **aws_connect_params) - except (boto.exception.NoAuthHandlerFound, AnsibleAWSError) as e: - module.fail_json(msg=str(e)) - else: - module.fail_json(msg="region must be specified") - - if state == 'list': - returned_volumes = [] - vols = get_volumes(module, ec2) - - for v in vols: - attachment = v.attach_data - - returned_volumes.append(get_volume_info(v, state)) - - module.exit_json(changed=False, volumes=returned_volumes) - - if encrypted and not boto_supports_volume_encryption(): - module.fail_json(msg="You must use boto >= v2.29.0 to use encrypted volumes") - - if kms_key_id is not None and not boto_supports_kms_key_id(): - module.fail_json(msg="You must use boto >= v2.39.0 to use kms_key_id") - - # Here we need to get the zone info for the instance. This covers situation where - # instance is specified but zone isn't. - # Useful for playbooks chaining instance launch with volume create + attach and where the - # zone doesn't matter to the user. - inst = None - if instance: - try: - reservation = ec2.get_all_instances(instance_ids=instance) - except BotoServerError as e: - module.fail_json(msg=e.message) - inst = reservation[0].instances[0] - zone = inst.placement - - # Check if there is a volume already mounted there. - if device_name: - if device_name in inst.block_device_mapping: - module.exit_json(msg="Volume mapping for %s already exists on instance %s" % (device_name, instance), - volume_id=inst.block_device_mapping[device_name].volume_id, - device=device_name, - changed=False) - - # Delaying the checks until after the instance check allows us to get volume ids for existing volumes - # without needing to pass an unused volume_size - if not volume_size and not (id or name or snapshot): - module.fail_json(msg="You must specify volume_size or identify an existing volume by id, name, or snapshot") - - if volume_size and id: - module.fail_json(msg="Cannot specify volume_size together with id") - - if state == 'present': - volume, changed = create_volume(module, ec2, zone) - if detach_vol_flag: - volume, changed = detach_volume(module, ec2, volume) - elif inst is not None: - volume, changed = attach_volume(module, ec2, volume, inst) - - # Add device, volume_id and volume_type parameters separately to maintain backward compatibility - volume_info = get_volume_info(volume, state) - - # deleteOnTermination is not correctly reflected on attachment - if module.params.get('delete_on_termination'): - for attempt in range(0, 8): - if volume_info['attachment_set'].get('deleteOnTermination') == 'true': - break - time.sleep(5) - volume = ec2.get_all_volumes(volume_ids=volume.id)[0] - volume_info = get_volume_info(volume, state) - module.exit_json(changed=changed, volume=volume_info, device=volume_info['attachment_set']['device'], - volume_id=volume_info['id'], volume_type=volume_info['type']) - elif state == 'absent': - delete_volume(module, ec2) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_vol_info.py b/lib/ansible/modules/cloud/amazon/ec2_vol_info.py deleted file mode 100644 index 0989f9bb6b..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_vol_info.py +++ /dev/null @@ -1,141 +0,0 @@ -#!/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 - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_vol_info -short_description: Gather information about ec2 volumes in AWS -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. -version_added: "2.1" -requirements: [ boto3 ] -author: "Rob White (@wimnat)" -options: - filters: - type: dict - 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_DescribeVolumes.html) for possible filters. -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Gather information about all volumes -- ec2_vol_info: - -# Gather information about a particular volume using volume ID -- ec2_vol_info: - filters: - volume-id: vol-00112233 - -# Gather information about any volume with a tag key Name and value Example -- ec2_vol_info: - filters: - "tag:Name": Example - -# Gather information about any volume that is attached -- ec2_vol_info: - filters: - attachment.status: attached - -''' - -# TODO: Disabled the RETURN as it was breaking docs building. Someone needs to -# fix this -RETURN = '''# ''' - -import traceback - -try: - from botocore.exceptions import ClientError -except ImportError: - pass # caught by AnsibleAWSModule - -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import AWSRetry -from ansible.module_utils.ec2 import boto3_tag_list_to_ansible_dict, ansible_dict_to_boto3_filter_list, camel_dict_to_snake_dict - - -def get_volume_info(volume, region): - - attachment = volume["attachments"] - - volume_info = { - 'create_time': volume["create_time"], - 'id': volume["volume_id"], - 'encrypted': volume["encrypted"], - 'iops': volume["iops"] if "iops" in volume else None, - 'size': volume["size"], - 'snapshot_id': volume["snapshot_id"], - 'status': volume["state"], - 'type': volume["volume_type"], - 'zone': volume["availability_zone"], - 'region': region, - 'attachment_set': { - 'attach_time': attachment[0]["attach_time"] if len(attachment) > 0 else None, - 'device': attachment[0]["device"] if len(attachment) > 0 else None, - 'instance_id': attachment[0]["instance_id"] if len(attachment) > 0 else None, - 'status': attachment[0]["state"] if len(attachment) > 0 else None, - 'delete_on_termination': attachment[0]["delete_on_termination"] if len(attachment) > 0 else None - }, - 'tags': boto3_tag_list_to_ansible_dict(volume['tags']) if "tags" in volume else None - } - - return volume_info - - -@AWSRetry.jittered_backoff() -def describe_volumes_with_backoff(connection, filters): - paginator = connection.get_paginator('describe_volumes') - return paginator.paginate(Filters=filters).build_full_result() - - -def list_ec2_volumes(connection, module): - - # Replace filter key underscores with dashes, for compatibility, except if we're dealing with tags - sanitized_filters = module.params.get("filters") - for key in list(sanitized_filters): - if not key.startswith("tag:"): - sanitized_filters[key.replace("_", "-")] = sanitized_filters.pop(key) - volume_dict_array = [] - - try: - all_volumes = describe_volumes_with_backoff(connection, ansible_dict_to_boto3_filter_list(sanitized_filters)) - except ClientError as e: - module.fail_json_aws(e, msg="Failed to describe volumes.") - - for volume in all_volumes["Volumes"]: - volume = camel_dict_to_snake_dict(volume, ignore_list=['Tags']) - volume_dict_array.append(get_volume_info(volume, module.region)) - module.exit_json(volumes=volume_dict_array) - - -def main(): - argument_spec = dict(filters=dict(default={}, type='dict')) - - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) - if module._name == 'ec2_vol_facts': - module.deprecate("The 'ec2_vol_facts' module has been renamed to 'ec2_vol_info'", version='2.13') - - connection = module.client('ec2') - - list_ec2_volumes(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc_dhcp_option.py b/lib/ansible/modules/cloud/amazon/ec2_vpc_dhcp_option.py deleted file mode 100644 index d111bed0f0..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc_dhcp_option.py +++ /dev/null @@ -1,414 +0,0 @@ -#!/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 - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'community'} - - -DOCUMENTATION = """ ---- -module: ec2_vpc_dhcp_option -short_description: Manages DHCP Options, and can ensure the DHCP options for the given VPC match what's - requested -description: - - This module removes, or creates DHCP option sets, and can associate them to a VPC. - Optionally, a new DHCP Options set can be created that converges a VPC's existing - DHCP option set with values provided. - When dhcp_options_id is provided, the module will - 1. remove (with state='absent') - 2. ensure tags are applied (if state='present' and tags are provided - 3. attach it to a VPC (if state='present' and a vpc_id is provided. - If any of the optional values are missing, they will either be treated - as a no-op (i.e., inherit what already exists for the VPC) - To remove existing options while inheriting, supply an empty value - (e.g. set ntp_servers to [] if you want to remove them from the VPC's options) - Most of the options should be self-explanatory. -author: "Joel Thompson (@joelthompson)" -version_added: 2.1 -options: - domain_name: - description: - - The domain name to set in the DHCP option sets - type: str - dns_servers: - description: - - A list of hosts to set the DNS servers for the VPC to. (Should be a - list of IP addresses rather than host names.) - type: list - elements: str - ntp_servers: - description: - - List of hosts to advertise as NTP servers for the VPC. - type: list - elements: str - netbios_name_servers: - description: - - List of hosts to advertise as NetBIOS servers. - type: list - elements: str - netbios_node_type: - description: - - NetBIOS node type to advertise in the DHCP options. - The AWS recommendation is to use 2 (when using netbios name services) - U(https://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_DHCP_Options.html) - type: int - vpc_id: - description: - - VPC ID to associate with the requested DHCP option set. - If no vpc id is provided, and no matching option set is found then a new - DHCP option set is created. - type: str - delete_old: - description: - - Whether to delete the old VPC DHCP option set when associating a new one. - This is primarily useful for debugging/development purposes when you - want to quickly roll back to the old option set. Note that this setting - will be ignored, and the old DHCP option set will be preserved, if it - is in use by any other VPC. (Otherwise, AWS will return an error.) - type: bool - default: 'yes' - inherit_existing: - description: - - For any DHCP options not specified in these parameters, whether to - inherit them from the options set already applied to vpc_id, or to - reset them to be empty. - type: bool - default: 'no' - tags: - description: - - Tags to be applied to a VPC options set if a new one is created, or - if the resource_id is provided. (options must match) - aliases: [ 'resource_tags'] - version_added: "2.1" - type: dict - dhcp_options_id: - description: - - The resource_id of an existing DHCP options set. - If this is specified, then it will override other settings, except tags - (which will be updated to match) - version_added: "2.1" - type: str - state: - description: - - create/assign or remove the DHCP options. - If state is set to absent, then a DHCP options set matched either - by id, or tags and options will be removed if possible. - default: present - choices: [ 'absent', 'present' ] - version_added: "2.1" - type: str -extends_documentation_fragment: - - aws - - ec2 -requirements: - - boto -""" - -RETURN = """ -new_options: - description: The DHCP options created, associated or found - returned: when appropriate - type: dict - sample: - domain-name-servers: - - 10.0.0.1 - - 10.0.1.1 - netbois-name-servers: - - 10.0.0.1 - - 10.0.1.1 - netbios-node-type: 2 - domain-name: "my.example.com" -dhcp_options_id: - description: The aws resource id of the primary DCHP options set created, found or removed - type: str - returned: when available -changed: - description: Whether the dhcp options were changed - type: bool - returned: always -""" - -EXAMPLES = """ -# Completely overrides the VPC DHCP options associated with VPC vpc-123456 and deletes any existing -# DHCP option set that may have been attached to that VPC. -- ec2_vpc_dhcp_option: - domain_name: "foo.example.com" - region: us-east-1 - dns_servers: - - 10.0.0.1 - - 10.0.1.1 - ntp_servers: - - 10.0.0.2 - - 10.0.1.2 - netbios_name_servers: - - 10.0.0.1 - - 10.0.1.1 - netbios_node_type: 2 - vpc_id: vpc-123456 - delete_old: True - inherit_existing: False - - -# Ensure the DHCP option set for the VPC has 10.0.0.4 and 10.0.1.4 as the specified DNS servers, but -# keep any other existing settings. Also, keep the old DHCP option set around. -- ec2_vpc_dhcp_option: - region: us-east-1 - dns_servers: - - "{{groups['dns-primary']}}" - - "{{groups['dns-secondary']}}" - vpc_id: vpc-123456 - inherit_existing: True - delete_old: False - - -## Create a DHCP option set with 4.4.4.4 and 8.8.8.8 as the specified DNS servers, with tags -## but do not assign to a VPC -- ec2_vpc_dhcp_option: - region: us-east-1 - dns_servers: - - 4.4.4.4 - - 8.8.8.8 - tags: - Name: google servers - Environment: Test - -## Delete a DHCP options set that matches the tags and options specified -- ec2_vpc_dhcp_option: - region: us-east-1 - dns_servers: - - 4.4.4.4 - - 8.8.8.8 - tags: - Name: google servers - Environment: Test - state: absent - -## Associate a DHCP options set with a VPC by ID -- ec2_vpc_dhcp_option: - region: us-east-1 - dhcp_options_id: dopt-12345678 - vpc_id: vpc-123456 - -""" - -import collections -import traceback -from time import sleep, time -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import HAS_BOTO, connect_to_aws, ec2_argument_spec, get_aws_connection_info - -if HAS_BOTO: - import boto.vpc - import boto.ec2 - from boto.exception import EC2ResponseError - - -def get_resource_tags(vpc_conn, resource_id): - return dict((t.name, t.value) for t in vpc_conn.get_all_tags(filters={'resource-id': resource_id})) - - -def retry_not_found(to_call, *args, **kwargs): - start_time = time() - while time() < start_time + 300: - try: - return to_call(*args, **kwargs) - except EC2ResponseError as e: - if e.error_code in ['InvalidDhcpOptionID.NotFound', 'InvalidDhcpOptionsID.NotFound']: - sleep(3) - continue - raise e - - -def ensure_tags(module, vpc_conn, resource_id, tags, add_only, check_mode): - try: - cur_tags = get_resource_tags(vpc_conn, resource_id) - if tags == cur_tags: - return {'changed': False, 'tags': cur_tags} - - to_delete = dict((k, cur_tags[k]) for k in cur_tags if k not in tags) - if to_delete and not add_only: - retry_not_found(vpc_conn.delete_tags, resource_id, to_delete, dry_run=check_mode) - - to_add = dict((k, tags[k]) for k in tags if k not in cur_tags) - if to_add: - retry_not_found(vpc_conn.create_tags, resource_id, to_add, dry_run=check_mode) - - latest_tags = get_resource_tags(vpc_conn, resource_id) - return {'changed': True, 'tags': latest_tags} - except EC2ResponseError as e: - module.fail_json(msg="Failed to modify tags: %s" % e.message, exception=traceback.format_exc()) - - -def fetch_dhcp_options_for_vpc(vpc_conn, vpc_id): - """ - Returns the DHCP options object currently associated with the requested VPC ID using the VPC - connection variable. - """ - vpcs = vpc_conn.get_all_vpcs(vpc_ids=[vpc_id]) - if len(vpcs) != 1 or vpcs[0].dhcp_options_id == "default": - return None - dhcp_options = vpc_conn.get_all_dhcp_options(dhcp_options_ids=[vpcs[0].dhcp_options_id]) - if len(dhcp_options) != 1: - return None - return dhcp_options[0] - - -def match_dhcp_options(vpc_conn, tags=None, options=None): - """ - Finds a DHCP Options object that optionally matches the tags and options provided - """ - dhcp_options = vpc_conn.get_all_dhcp_options() - for dopts in dhcp_options: - if (not tags) or get_resource_tags(vpc_conn, dopts.id) == tags: - if (not options) or dopts.options == options: - return(True, dopts) - return(False, None) - - -def remove_dhcp_options_by_id(vpc_conn, dhcp_options_id): - associations = vpc_conn.get_all_vpcs(filters={'dhcpOptionsId': dhcp_options_id}) - if len(associations) > 0: - return False - else: - vpc_conn.delete_dhcp_options(dhcp_options_id) - return True - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - dhcp_options_id=dict(type='str', default=None), - domain_name=dict(type='str', default=None), - dns_servers=dict(type='list', default=None), - ntp_servers=dict(type='list', default=None), - netbios_name_servers=dict(type='list', default=None), - netbios_node_type=dict(type='int', default=None), - vpc_id=dict(type='str', default=None), - delete_old=dict(type='bool', default=True), - inherit_existing=dict(type='bool', default=False), - tags=dict(type='dict', default=None, aliases=['resource_tags']), - state=dict(type='str', default='present', choices=['present', 'absent']) - ) - ) - - module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - - params = module.params - found = False - changed = False - new_options = collections.defaultdict(lambda: None) - - if not HAS_BOTO: - module.fail_json(msg='boto is required for this module') - - region, ec2_url, boto_params = get_aws_connection_info(module) - connection = connect_to_aws(boto.vpc, region, **boto_params) - - existing_options = None - - # First check if we were given a dhcp_options_id - if not params['dhcp_options_id']: - # No, so create new_options from the parameters - if params['dns_servers'] is not None: - new_options['domain-name-servers'] = params['dns_servers'] - if params['netbios_name_servers'] is not None: - new_options['netbios-name-servers'] = params['netbios_name_servers'] - if params['ntp_servers'] is not None: - new_options['ntp-servers'] = params['ntp_servers'] - if params['domain_name'] is not None: - # needs to be a list for comparison with boto objects later - new_options['domain-name'] = [params['domain_name']] - if params['netbios_node_type'] is not None: - # needs to be a list for comparison with boto objects later - new_options['netbios-node-type'] = [str(params['netbios_node_type'])] - # If we were given a vpc_id then we need to look at the options on that - if params['vpc_id']: - existing_options = fetch_dhcp_options_for_vpc(connection, params['vpc_id']) - # if we've been asked to inherit existing options, do that now - if params['inherit_existing']: - if existing_options: - for option in ['domain-name-servers', 'netbios-name-servers', 'ntp-servers', 'domain-name', 'netbios-node-type']: - if existing_options.options.get(option) and new_options[option] != [] and (not new_options[option] or [''] == new_options[option]): - new_options[option] = existing_options.options.get(option) - - # Do the vpc's dhcp options already match what we're asked for? if so we are done - if existing_options and new_options == existing_options.options: - module.exit_json(changed=changed, new_options=new_options, dhcp_options_id=existing_options.id) - - # If no vpc_id was given, or the options don't match then look for an existing set using tags - found, dhcp_option = match_dhcp_options(connection, params['tags'], new_options) - - # Now let's cover the case where there are existing options that we were told about by id - # If a dhcp_options_id was supplied we don't look at options inside, just set tags (if given) - else: - supplied_options = connection.get_all_dhcp_options(filters={'dhcp-options-id': params['dhcp_options_id']}) - if len(supplied_options) != 1: - if params['state'] != 'absent': - module.fail_json(msg=" a dhcp_options_id was supplied, but does not exist") - else: - found = True - dhcp_option = supplied_options[0] - if params['state'] != 'absent' and params['tags']: - ensure_tags(module, connection, dhcp_option.id, params['tags'], False, module.check_mode) - - # Now we have the dhcp options set, let's do the necessary - - # if we found options we were asked to remove then try to do so - if params['state'] == 'absent': - if not module.check_mode: - if found: - changed = remove_dhcp_options_by_id(connection, dhcp_option.id) - module.exit_json(changed=changed, new_options={}) - - # otherwise if we haven't found the required options we have something to do - elif not module.check_mode and not found: - - # create some dhcp options if we weren't able to use existing ones - if not found: - # Convert netbios-node-type and domain-name back to strings - if new_options['netbios-node-type']: - new_options['netbios-node-type'] = new_options['netbios-node-type'][0] - if new_options['domain-name']: - new_options['domain-name'] = new_options['domain-name'][0] - - # create the new dhcp options set requested - dhcp_option = connection.create_dhcp_options( - new_options['domain-name'], - new_options['domain-name-servers'], - new_options['ntp-servers'], - new_options['netbios-name-servers'], - new_options['netbios-node-type']) - - # wait for dhcp option to be accessible - found_dhcp_opt = False - start_time = time() - try: - found_dhcp_opt = retry_not_found(connection.get_all_dhcp_options, dhcp_options_ids=[dhcp_option.id]) - except EC2ResponseError as e: - module.fail_json(msg="Failed to describe DHCP options", exception=traceback.format_exc) - if not found_dhcp_opt: - module.fail_json(msg="Failed to wait for {0} to be available.".format(dhcp_option.id)) - - changed = True - if params['tags']: - ensure_tags(module, connection, dhcp_option.id, params['tags'], False, module.check_mode) - - # If we were given a vpc_id, then attach the options we now have to that before we finish - if params['vpc_id'] and not module.check_mode: - changed = True - connection.associate_dhcp_options(dhcp_option.id, params['vpc_id']) - # and remove old ones if that was requested - if params['delete_old'] and existing_options: - remove_dhcp_options_by_id(connection, existing_options.id) - - module.exit_json(changed=changed, new_options=new_options, dhcp_options_id=dhcp_option.id) - - -if __name__ == "__main__": - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc_dhcp_option_info.py b/lib/ansible/modules/cloud/amazon/ec2_vpc_dhcp_option_info.py deleted file mode 100644 index ac909fb1a9..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc_dhcp_option_info.py +++ /dev/null @@ -1,157 +0,0 @@ -#!/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 - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ec2_vpc_dhcp_option_info -short_description: Gather information about dhcp options sets in AWS -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. -version_added: "2.2" -requirements: [ boto3 ] -author: "Nick Aslanidis (@naslanidis)" -options: - 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_DescribeDhcpOptions.html) for possible filters. - type: dict - dhcp_options_ids: - description: - - Get details of specific DHCP Option IDs. - aliases: ['DhcpOptionIds'] - type: list - elements: str - dry_run: - description: - - Checks whether you have the required permissions to view the DHCP - Options. - aliases: ['DryRun'] - version_added: "2.4" - type: bool -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# # Note: These examples do not set authentication details, see the AWS Guide for details. - -- name: Gather information about all DHCP Option sets for an account or profile - ec2_vpc_dhcp_option_info: - region: ap-southeast-2 - profile: production - register: dhcp_info - -- name: Gather information about a filtered list of DHCP Option sets - ec2_vpc_dhcp_option_info: - region: ap-southeast-2 - profile: production - filters: - "tag:Name": "abc-123" - register: dhcp_info - -- name: Gather information about a specific DHCP Option set by DhcpOptionId - ec2_vpc_dhcp_option_info: - region: ap-southeast-2 - profile: production - DhcpOptionsIds: dopt-123fece2 - register: dhcp_info - -''' - -RETURN = ''' -dhcp_options: - description: The dhcp option sets for the account - returned: always - type: list - -changed: - description: True if listing the dhcp options succeeds - type: bool - returned: always -''' - -import traceback - -try: - import botocore -except ImportError: - pass # caught by imported HAS_BOTO3 - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import (ec2_argument_spec, boto3_conn, HAS_BOTO3, - ansible_dict_to_boto3_filter_list, get_aws_connection_info, - camel_dict_to_snake_dict, boto3_tag_list_to_ansible_dict) - - -def get_dhcp_options_info(dhcp_option): - dhcp_option_info = {'DhcpOptionsId': dhcp_option['DhcpOptionsId'], - 'DhcpConfigurations': dhcp_option['DhcpConfigurations'], - 'Tags': boto3_tag_list_to_ansible_dict(dhcp_option.get('Tags', [{'Value': '', 'Key': 'Name'}]))} - return dhcp_option_info - - -def list_dhcp_options(client, module): - params = dict(Filters=ansible_dict_to_boto3_filter_list(module.params.get('filters'))) - - if module.params.get("dry_run"): - params['DryRun'] = True - - if module.params.get("dhcp_options_ids"): - params['DhcpOptionsIds'] = module.params.get("dhcp_options_ids") - - try: - all_dhcp_options = client.describe_dhcp_options(**params) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=str(e), exception=traceback.format_exc(), - **camel_dict_to_snake_dict(e.response)) - - return [camel_dict_to_snake_dict(get_dhcp_options_info(option)) - for option in all_dhcp_options['DhcpOptions']] - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update( - dict( - filters=dict(type='dict', default={}), - dry_run=dict(type='bool', default=False, aliases=['DryRun']), - dhcp_options_ids=dict(type='list', aliases=['DhcpOptionIds']) - ) - ) - - module = AnsibleModule(argument_spec=argument_spec, - supports_check_mode=True) - if module._name == 'ec2_vpc_dhcp_option_facts': - module.deprecate("The 'ec2_vpc_dhcp_option_facts' module has been renamed to 'ec2_vpc_dhcp_option_info'", version='2.13') - - # Validate Requirements - if not HAS_BOTO3: - module.fail_json(msg='boto3 and botocore are required.') - - try: - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - connection = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_kwargs) - except botocore.exceptions.NoCredentialsError as e: - module.fail_json(msg="Can't authorize connection - " + str(e)) - - # call your function here - results = list_dhcp_options(connection, module) - - module.exit_json(dhcp_options=results) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc_net.py b/lib/ansible/modules/cloud/amazon/ec2_vpc_net.py deleted file mode 100644 index 30e4b1e94c..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc_net.py +++ /dev/null @@ -1,524 +0,0 @@ -#!/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 - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'core'} - - -DOCUMENTATION = ''' ---- -module: ec2_vpc_net -short_description: Configure AWS virtual private clouds -description: - - Create, modify, and terminate AWS virtual private clouds. -version_added: "2.0" -author: - - Jonathan Davila (@defionscode) - - Sloane Hertel (@s-hertel) -options: - name: - description: - - The name to give your VPC. This is used in combination with C(cidr_block) to determine if a VPC already exists. - required: yes - type: str - cidr_block: - description: - - The primary CIDR of the VPC. After 2.5 a list of CIDRs can be provided. The first in the list will be used as the primary CIDR - and is used in conjunction with the C(name) to ensure idempotence. - required: yes - type: list - elements: str - ipv6_cidr: - description: - - Request an Amazon-provided IPv6 CIDR block with /56 prefix length. You cannot specify the range of IPv6 addresses, - or the size of the CIDR block. - default: False - type: bool - version_added: '2.10' - purge_cidrs: - description: - - Remove CIDRs that are associated with the VPC and are not specified in C(cidr_block). - default: no - type: bool - version_added: '2.5' - tenancy: - description: - - Whether to be default or dedicated tenancy. This cannot be changed after the VPC has been created. - default: default - choices: [ 'default', 'dedicated' ] - type: str - dns_support: - description: - - Whether to enable AWS DNS support. - default: yes - type: bool - dns_hostnames: - description: - - Whether to enable AWS hostname support. - default: yes - type: bool - dhcp_opts_id: - description: - - The id of the DHCP options to use for this VPC. - type: str - tags: - description: - - The tags you want attached to the VPC. This is independent of the name value, note if you pass a 'Name' key it would override the Name of - the VPC if it's different. - aliases: [ 'resource_tags' ] - type: dict - state: - description: - - The state of the VPC. Either absent or present. - default: present - choices: [ 'present', 'absent' ] - type: str - multi_ok: - description: - - By default the module will not create another VPC if there is another VPC with the same name and CIDR block. Specify this as true if you want - duplicate VPCs created. - type: bool - default: false -requirements: - - boto3 - - botocore -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -- name: create a VPC with dedicated tenancy and a couple of tags - ec2_vpc_net: - name: Module_dev2 - cidr_block: 10.10.0.0/16 - region: us-east-1 - tags: - module: ec2_vpc_net - this: works - tenancy: dedicated - -- name: create a VPC with dedicated tenancy and request an IPv6 CIDR - ec2_vpc_net: - name: Module_dev2 - cidr_block: 10.10.0.0/16 - ipv6_cidr: True - region: us-east-1 - tenancy: dedicated -''' - -RETURN = ''' -vpc: - description: info about the VPC that was created or deleted - returned: always - type: complex - contains: - cidr_block: - description: The CIDR of the VPC - returned: always - type: str - sample: 10.0.0.0/16 - cidr_block_association_set: - description: IPv4 CIDR blocks associated with the VPC - returned: success - type: list - sample: - "cidr_block_association_set": [ - { - "association_id": "vpc-cidr-assoc-97aeeefd", - "cidr_block": "20.0.0.0/24", - "cidr_block_state": { - "state": "associated" - } - } - ] - classic_link_enabled: - description: indicates whether ClassicLink is enabled - returned: always - type: bool - sample: false - dhcp_options_id: - description: the id of the DHCP options associated with this VPC - returned: always - type: str - sample: dopt-0fb8bd6b - id: - description: VPC resource id - returned: always - type: str - sample: vpc-c2e00da5 - instance_tenancy: - description: indicates whether VPC uses default or dedicated tenancy - returned: always - type: str - sample: default - ipv6_cidr_block_association_set: - description: IPv6 CIDR blocks associated with the VPC - returned: success - type: list - sample: - "ipv6_cidr_block_association_set": [ - { - "association_id": "vpc-cidr-assoc-97aeeefd", - "ipv6_cidr_block": "2001:db8::/56", - "ipv6_cidr_block_state": { - "state": "associated" - } - } - ] - is_default: - description: indicates whether this is the default VPC - returned: always - type: bool - sample: false - state: - description: state of the VPC - returned: always - type: str - sample: available - tags: - description: tags attached to the VPC, includes name - returned: always - type: complex - contains: - Name: - description: name tag for the VPC - returned: always - type: str - sample: pk_vpc4 -''' - -try: - import botocore -except ImportError: - pass # Handled by AnsibleAWSModule - -from time import sleep, time -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.ec2 import (AWSRetry, camel_dict_to_snake_dict, compare_aws_tags, - ansible_dict_to_boto3_tag_list, boto3_tag_list_to_ansible_dict) -from ansible.module_utils.six import string_types -from ansible.module_utils._text import to_native -from ansible.module_utils.network.common.utils import to_subnet - - -def vpc_exists(module, vpc, name, cidr_block, multi): - """Returns None or a vpc object depending on the existence of a VPC. When supplied - with a CIDR, it will check for matching tags to determine if it is a match - otherwise it will assume the VPC does not exist and thus return None. - """ - try: - matching_vpcs = vpc.describe_vpcs(Filters=[{'Name': 'tag:Name', 'Values': [name]}, {'Name': 'cidr-block', 'Values': cidr_block}])['Vpcs'] - # If an exact matching using a list of CIDRs isn't found, check for a match with the first CIDR as is documented for C(cidr_block) - if not matching_vpcs: - matching_vpcs = vpc.describe_vpcs(Filters=[{'Name': 'tag:Name', 'Values': [name]}, {'Name': 'cidr-block', 'Values': [cidr_block[0]]}])['Vpcs'] - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to describe VPCs") - - if multi: - return None - elif len(matching_vpcs) == 1: - return matching_vpcs[0]['VpcId'] - elif len(matching_vpcs) > 1: - module.fail_json(msg='Currently there are %d VPCs that have the same name and ' - 'CIDR block you specified. If you would like to create ' - 'the VPC anyway please pass True to the multi_ok param.' % len(matching_vpcs)) - return None - - -@AWSRetry.backoff(delay=3, tries=8, catch_extra_error_codes=['InvalidVpcID.NotFound']) -def get_classic_link_with_backoff(connection, vpc_id): - try: - return connection.describe_vpc_classic_link(VpcIds=[vpc_id])['Vpcs'][0].get('ClassicLinkEnabled') - except botocore.exceptions.ClientError as e: - if e.response["Error"]["Message"] == "The functionality you requested is not available in this region.": - return False - else: - raise - - -def get_vpc(module, connection, vpc_id): - # wait for vpc to be available - try: - connection.get_waiter('vpc_available').wait(VpcIds=[vpc_id]) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Unable to wait for VPC {0} to be available.".format(vpc_id)) - - try: - vpc_obj = connection.describe_vpcs(VpcIds=[vpc_id], aws_retry=True)['Vpcs'][0] - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to describe VPCs") - try: - vpc_obj['ClassicLinkEnabled'] = get_classic_link_with_backoff(connection, vpc_id) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to describe VPCs") - - return vpc_obj - - -def update_vpc_tags(connection, module, vpc_id, tags, name): - if tags is None: - tags = dict() - - tags.update({'Name': name}) - tags = dict((k, to_native(v)) for k, v in tags.items()) - try: - current_tags = dict((t['Key'], t['Value']) for t in connection.describe_tags(Filters=[{'Name': 'resource-id', 'Values': [vpc_id]}])['Tags']) - tags_to_update, dummy = compare_aws_tags(current_tags, tags, False) - if tags_to_update: - if not module.check_mode: - tags = ansible_dict_to_boto3_tag_list(tags_to_update) - vpc_obj = connection.create_tags(Resources=[vpc_id], Tags=tags, aws_retry=True) - - # Wait for tags to be updated - expected_tags = boto3_tag_list_to_ansible_dict(tags) - filters = [{'Name': 'tag:{0}'.format(key), 'Values': [value]} for key, value in expected_tags.items()] - connection.get_waiter('vpc_available').wait(VpcIds=[vpc_id], Filters=filters) - - return True - else: - return False - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to update tags") - - -def update_dhcp_opts(connection, module, vpc_obj, dhcp_id): - if vpc_obj['DhcpOptionsId'] != dhcp_id: - if not module.check_mode: - try: - connection.associate_dhcp_options(DhcpOptionsId=dhcp_id, VpcId=vpc_obj['VpcId']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to associate DhcpOptionsId {0}".format(dhcp_id)) - - try: - # Wait for DhcpOptionsId to be updated - filters = [{'Name': 'dhcp-options-id', 'Values': [dhcp_id]}] - connection.get_waiter('vpc_available').wait(VpcIds=[vpc_obj['VpcId']], Filters=filters) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json(msg="Failed to wait for DhcpOptionsId to be updated") - - return True - else: - return False - - -def create_vpc(connection, module, cidr_block, tenancy): - try: - if not module.check_mode: - vpc_obj = connection.create_vpc(CidrBlock=cidr_block, InstanceTenancy=tenancy) - else: - module.exit_json(changed=True) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, "Failed to create the VPC") - - # wait for vpc to exist - try: - connection.get_waiter('vpc_exists').wait(VpcIds=[vpc_obj['Vpc']['VpcId']]) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Unable to wait for VPC {0} to be created.".format(vpc_obj['Vpc']['VpcId'])) - - return vpc_obj['Vpc']['VpcId'] - - -def wait_for_vpc_attribute(connection, module, vpc_id, attribute, expected_value): - start_time = time() - updated = False - while time() < start_time + 300: - current_value = connection.describe_vpc_attribute( - Attribute=attribute, - VpcId=vpc_id - )['{0}{1}'.format(attribute[0].upper(), attribute[1:])]['Value'] - if current_value != expected_value: - sleep(3) - else: - updated = True - break - if not updated: - module.fail_json(msg="Failed to wait for {0} to be updated".format(attribute)) - - -def get_cidr_network_bits(module, cidr_block): - fixed_cidrs = [] - for cidr in cidr_block: - split_addr = cidr.split('/') - if len(split_addr) == 2: - # this_ip is a IPv4 CIDR that may or may not have host bits set - # Get the network bits. - valid_cidr = to_subnet(split_addr[0], split_addr[1]) - if cidr != valid_cidr: - module.warn("One of your CIDR addresses ({0}) has host bits set. To get rid of this warning, " - "check the network mask and make sure that only network bits are set: {1}.".format(cidr, valid_cidr)) - fixed_cidrs.append(valid_cidr) - else: - # let AWS handle invalid CIDRs - fixed_cidrs.append(cidr) - return fixed_cidrs - - -def main(): - argument_spec = dict( - name=dict(required=True), - cidr_block=dict(type='list', required=True), - ipv6_cidr=dict(type='bool', default=False), - tenancy=dict(choices=['default', 'dedicated'], default='default'), - dns_support=dict(type='bool', default=True), - dns_hostnames=dict(type='bool', default=True), - dhcp_opts_id=dict(), - tags=dict(type='dict', aliases=['resource_tags']), - state=dict(choices=['present', 'absent'], default='present'), - multi_ok=dict(type='bool', default=False), - purge_cidrs=dict(type='bool', default=False), - ) - - module = AnsibleAWSModule( - argument_spec=argument_spec, - supports_check_mode=True - ) - - name = module.params.get('name') - cidr_block = get_cidr_network_bits(module, module.params.get('cidr_block')) - ipv6_cidr = module.params.get('ipv6_cidr') - purge_cidrs = module.params.get('purge_cidrs') - tenancy = module.params.get('tenancy') - dns_support = module.params.get('dns_support') - dns_hostnames = module.params.get('dns_hostnames') - dhcp_id = module.params.get('dhcp_opts_id') - tags = module.params.get('tags') - state = module.params.get('state') - multi = module.params.get('multi_ok') - - changed = False - - connection = module.client( - 'ec2', - retry_decorator=AWSRetry.jittered_backoff( - retries=8, delay=3, catch_extra_error_codes=['InvalidVpcID.NotFound'] - ) - ) - - if dns_hostnames and not dns_support: - module.fail_json(msg='In order to enable DNS Hostnames you must also enable DNS support') - - if state == 'present': - - # Check if VPC exists - vpc_id = vpc_exists(module, connection, name, cidr_block, multi) - - if vpc_id is None: - vpc_id = create_vpc(connection, module, cidr_block[0], tenancy) - changed = True - - vpc_obj = get_vpc(module, connection, vpc_id) - - associated_cidrs = dict((cidr['CidrBlock'], cidr['AssociationId']) for cidr in vpc_obj.get('CidrBlockAssociationSet', []) - if cidr['CidrBlockState']['State'] != 'disassociated') - to_add = [cidr for cidr in cidr_block if cidr not in associated_cidrs] - to_remove = [associated_cidrs[cidr] for cidr in associated_cidrs if cidr not in cidr_block] - expected_cidrs = [cidr for cidr in associated_cidrs if associated_cidrs[cidr] not in to_remove] + to_add - - if len(cidr_block) > 1: - for cidr in to_add: - changed = True - try: - connection.associate_vpc_cidr_block(CidrBlock=cidr, VpcId=vpc_id) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, "Unable to associate CIDR {0}.".format(ipv6_cidr)) - if ipv6_cidr: - if 'Ipv6CidrBlockAssociationSet' in vpc_obj.keys(): - module.warn("Only one IPv6 CIDR is permitted per VPC, {0} already has CIDR {1}".format( - vpc_id, - vpc_obj['Ipv6CidrBlockAssociationSet'][0]['Ipv6CidrBlock'])) - else: - try: - connection.associate_vpc_cidr_block(AmazonProvidedIpv6CidrBlock=ipv6_cidr, VpcId=vpc_id) - changed = True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, "Unable to associate CIDR {0}.".format(ipv6_cidr)) - - if purge_cidrs: - for association_id in to_remove: - changed = True - try: - connection.disassociate_vpc_cidr_block(AssociationId=association_id) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, "Unable to disassociate {0}. You must detach or delete all gateways and resources that " - "are associated with the CIDR block before you can disassociate it.".format(association_id)) - - if dhcp_id is not None: - try: - if update_dhcp_opts(connection, module, vpc_obj, dhcp_id): - changed = True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, "Failed to update DHCP options") - - if tags is not None or name is not None: - try: - if update_vpc_tags(connection, module, vpc_id, tags, name): - changed = True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to update tags") - - current_dns_enabled = connection.describe_vpc_attribute(Attribute='enableDnsSupport', VpcId=vpc_id, aws_retry=True)['EnableDnsSupport']['Value'] - current_dns_hostnames = connection.describe_vpc_attribute(Attribute='enableDnsHostnames', VpcId=vpc_id, aws_retry=True)['EnableDnsHostnames']['Value'] - if current_dns_enabled != dns_support: - changed = True - if not module.check_mode: - try: - connection.modify_vpc_attribute(VpcId=vpc_id, EnableDnsSupport={'Value': dns_support}) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, "Failed to update enabled dns support attribute") - if current_dns_hostnames != dns_hostnames: - changed = True - if not module.check_mode: - try: - connection.modify_vpc_attribute(VpcId=vpc_id, EnableDnsHostnames={'Value': dns_hostnames}) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, "Failed to update enabled dns hostnames attribute") - - # wait for associated cidrs to match - if to_add or to_remove: - try: - connection.get_waiter('vpc_available').wait( - VpcIds=[vpc_id], - Filters=[{'Name': 'cidr-block-association.cidr-block', 'Values': expected_cidrs}] - ) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, "Failed to wait for CIDRs to update") - - # try to wait for enableDnsSupport and enableDnsHostnames to match - wait_for_vpc_attribute(connection, module, vpc_id, 'enableDnsSupport', dns_support) - wait_for_vpc_attribute(connection, module, vpc_id, 'enableDnsHostnames', dns_hostnames) - - final_state = camel_dict_to_snake_dict(get_vpc(module, connection, vpc_id)) - final_state['tags'] = boto3_tag_list_to_ansible_dict(final_state.get('tags', [])) - final_state['id'] = final_state.pop('vpc_id') - - module.exit_json(changed=changed, vpc=final_state) - - elif state == 'absent': - - # Check if VPC exists - vpc_id = vpc_exists(module, connection, name, cidr_block, multi) - - if vpc_id is not None: - try: - if not module.check_mode: - connection.delete_vpc(VpcId=vpc_id) - changed = True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to delete VPC {0} You may want to use the ec2_vpc_subnet, ec2_vpc_igw, " - "and/or ec2_vpc_route_table modules to ensure the other components are absent.".format(vpc_id)) - - module.exit_json(changed=changed, vpc={}) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc_net_info.py b/lib/ansible/modules/cloud/amazon/ec2_vpc_net_info.py deleted file mode 100644 index 96c4f46155..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc_net_info.py +++ /dev/null @@ -1,306 +0,0 @@ -#!/usr/bin/python -# -# 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 - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'core'} - - -DOCUMENTATION = ''' ---- -module: ec2_vpc_net_info -short_description: Gather information about ec2 VPCs in AWS -description: - - 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. -version_added: "2.1" -author: "Rob White (@wimnat)" -requirements: - - boto3 - - botocore -options: - vpc_ids: - description: - - A list of VPC IDs that exist in your account. - version_added: "2.5" - 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_DescribeVpcs.html) for possible filters. - type: dict -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Gather information about all VPCs -- ec2_vpc_net_info: - -# Gather information about a particular VPC using VPC ID -- ec2_vpc_net_info: - vpc_ids: vpc-00112233 - -# Gather information about any VPC with a tag key Name and value Example -- ec2_vpc_net_info: - filters: - "tag:Name": Example - -''' - -RETURN = ''' -vpcs: - description: Returns an array of complex objects as described below. - returned: success - type: complex - contains: - id: - description: The ID of the VPC (for backwards compatibility). - returned: always - type: str - vpc_id: - description: The ID of the VPC . - returned: always - type: str - state: - description: The state of the VPC. - returned: always - type: str - tags: - description: A dict of tags associated with the VPC. - returned: always - type: dict - instance_tenancy: - description: The instance tenancy setting for the VPC. - returned: always - type: str - is_default: - description: True if this is the default VPC for account. - returned: always - type: bool - cidr_block: - description: The IPv4 CIDR block assigned to the VPC. - returned: always - type: str - classic_link_dns_supported: - description: True/False depending on attribute setting for classic link DNS support. - returned: always - type: bool - classic_link_enabled: - description: True/False depending on if classic link support is enabled. - returned: always - type: bool - enable_dns_hostnames: - description: True/False depending on attribute setting for DNS hostnames support. - returned: always - type: bool - enable_dns_support: - description: True/False depending on attribute setting for DNS support. - returned: always - type: bool - cidr_block_association_set: - description: An array of IPv4 cidr block association set information. - returned: always - type: complex - contains: - association_id: - description: The association ID - returned: always - type: str - cidr_block: - description: The IPv4 CIDR block that is associated with the VPC. - returned: always - type: str - cidr_block_state: - description: A hash/dict that contains a single item. The state of the cidr block association. - returned: always - type: dict - contains: - state: - description: The CIDR block association state. - returned: always - type: str - ipv6_cidr_block_association_set: - description: An array of IPv6 cidr block association set information. - returned: always - type: complex - contains: - association_id: - description: The association ID - returned: always - type: str - ipv6_cidr_block: - description: The IPv6 CIDR block that is associated with the VPC. - returned: always - type: str - ipv6_cidr_block_state: - description: A hash/dict that contains a single item. The state of the cidr block association. - returned: always - type: dict - contains: - state: - description: The CIDR block association state. - returned: always - type: str -''' - -import traceback -from ansible.module_utils._text import to_native -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import ( - boto3_conn, - ec2_argument_spec, - get_aws_connection_info, - AWSRetry, - HAS_BOTO3, - boto3_tag_list_to_ansible_dict, - camel_dict_to_snake_dict, - ansible_dict_to_boto3_filter_list -) - -try: - import botocore -except ImportError: - pass # caught by imported HAS_BOTO3 - - -@AWSRetry.exponential_backoff() -def describe_vpc_attr_with_backoff(connection, vpc_id, vpc_attribute): - """ - Describe VPC Attributes with AWSRetry backoff throttling support. - - connection : boto3 client connection object - vpc_id : The VPC ID to pull attribute value from - vpc_attribute : The VPC attribute to get the value from - valid options = enableDnsSupport or enableDnsHostnames - """ - - return connection.describe_vpc_attribute(VpcId=vpc_id, Attribute=vpc_attribute) - - -def describe_vpcs(connection, module): - """ - Describe VPCs. - - connection : boto3 client connection object - module : AnsibleModule object - """ - # collect parameters - filters = ansible_dict_to_boto3_filter_list(module.params.get('filters')) - vpc_ids = module.params.get('vpc_ids') - - # init empty list for return vars - vpc_info = list() - vpc_list = list() - - # Get the basic VPC info - try: - response = connection.describe_vpcs(VpcIds=vpc_ids, Filters=filters) - except botocore.exceptions.ClientError as e: - module.fail_json(msg="Unable to describe VPCs {0}: {1}".format(vpc_ids, to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json(msg="Unable to describe VPCs {0}: {1}".format(vpc_ids, to_native(e)), - exception=traceback.format_exc()) - - # Loop through results and create a list of VPC IDs - for vpc in response['Vpcs']: - vpc_list.append(vpc['VpcId']) - - # We can get these results in bulk but still needs two separate calls to the API - try: - cl_enabled = connection.describe_vpc_classic_link(VpcIds=vpc_list) - except botocore.exceptions.ClientError as e: - if e.response["Error"]["Message"] == "The functionality you requested is not available in this region.": - cl_enabled = {'Vpcs': [{'VpcId': vpc_id, 'ClassicLinkEnabled': False} for vpc_id in vpc_list]} - else: - module.fail_json(msg="Unable to describe if ClassicLink is enabled: {0}".format(to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json(msg="Unable to describe if ClassicLink is enabled: {0}".format(to_native(e)), - exception=traceback.format_exc()) - - try: - cl_dns_support = connection.describe_vpc_classic_link_dns_support(VpcIds=vpc_list) - except botocore.exceptions.ClientError as e: - if e.response["Error"]["Message"] == "The functionality you requested is not available in this region.": - cl_dns_support = {'Vpcs': [{'VpcId': vpc_id, 'ClassicLinkDnsSupported': False} for vpc_id in vpc_list]} - else: - module.fail_json(msg="Unable to describe if ClassicLinkDns is supported: {0}".format(to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json(msg="Unable to describe if ClassicLinkDns is supported: {0}".format(to_native(e)), - exception=traceback.format_exc()) - - # Loop through the results and add the other VPC attributes we gathered - for vpc in response['Vpcs']: - error_message = "Unable to describe VPC attribute {0}: {1}" - # We have to make two separate calls per VPC to get these attributes. - try: - dns_support = describe_vpc_attr_with_backoff(connection, vpc['VpcId'], 'enableDnsSupport') - except botocore.exceptions.ClientError as e: - module.fail_json(msg=error_message.format('enableDnsSupport', to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json(msg=error_message.format('enableDnsSupport', to_native(e)), - exception=traceback.format_exc()) - try: - dns_hostnames = describe_vpc_attr_with_backoff(connection, vpc['VpcId'], 'enableDnsHostnames') - except botocore.exceptions.ClientError as e: - module.fail_json(msg=error_message.format('enableDnsHostnames', to_native(e)), - exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - except botocore.exceptions.BotoCoreError as e: - module.fail_json(msg=error_message.format('enableDnsHostnames', to_native(e)), - exception=traceback.format_exc()) - - # loop through the ClassicLink Enabled results and add the value for the correct VPC - for item in cl_enabled['Vpcs']: - if vpc['VpcId'] == item['VpcId']: - vpc['ClassicLinkEnabled'] = item['ClassicLinkEnabled'] - - # loop through the ClassicLink DNS support results and add the value for the correct VPC - for item in cl_dns_support['Vpcs']: - if vpc['VpcId'] == item['VpcId']: - vpc['ClassicLinkDnsSupported'] = item['ClassicLinkDnsSupported'] - - # add the two DNS attributes - vpc['EnableDnsSupport'] = dns_support['EnableDnsSupport'].get('Value') - vpc['EnableDnsHostnames'] = dns_hostnames['EnableDnsHostnames'].get('Value') - # for backwards compatibility - vpc['id'] = vpc['VpcId'] - vpc_info.append(camel_dict_to_snake_dict(vpc)) - # convert tag list to ansible dict - vpc_info[-1]['tags'] = boto3_tag_list_to_ansible_dict(vpc.get('Tags', [])) - - module.exit_json(vpcs=vpc_info) - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - vpc_ids=dict(type='list', default=[]), - filters=dict(type='dict', default={}) - )) - - module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - if module._name == 'ec2_vpc_net_facts': - module.deprecate("The 'ec2_vpc_net_facts' module has been renamed to 'ec2_vpc_net_info'", version='2.13') - - if not HAS_BOTO3: - module.fail_json(msg='boto3 and botocore are required for this module') - - region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) - connection = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_params) - - describe_vpcs(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc_subnet.py b/lib/ansible/modules/cloud/amazon/ec2_vpc_subnet.py deleted file mode 100644 index 5085e99b79..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc_subnet.py +++ /dev/null @@ -1,604 +0,0 @@ -#!/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 - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'core'} - - -DOCUMENTATION = ''' ---- -module: ec2_vpc_subnet -short_description: Manage subnets in AWS virtual private clouds -description: - - Manage subnets in AWS virtual private clouds. -version_added: "2.0" -author: -- Robert Estelle (@erydo) -- Brad Davidson (@brandond) -requirements: [ boto3 ] -options: - az: - description: - - "The availability zone for the subnet." - type: str - cidr: - description: - - "The CIDR block for the subnet. E.g. 192.0.2.0/24." - type: str - required: true - ipv6_cidr: - description: - - "The IPv6 CIDR block for the subnet. The VPC must have a /56 block assigned and this value must be a valid IPv6 /64 that falls in the VPC range." - - "Required if I(assign_instances_ipv6=true)" - version_added: "2.5" - type: str - tags: - description: - - "A dict of tags to apply to the subnet. Any tags currently applied to the subnet and not present here will be removed." - aliases: [ 'resource_tags' ] - type: dict - state: - description: - - "Create or remove the subnet." - default: present - choices: [ 'present', 'absent' ] - type: str - vpc_id: - description: - - "VPC ID of the VPC in which to create or delete the subnet." - required: true - type: str - map_public: - description: - - "Specify C(yes) to indicate that instances launched into the subnet should be assigned public IP address by default." - type: bool - default: 'no' - version_added: "2.4" - assign_instances_ipv6: - description: - - "Specify C(yes) to indicate that instances launched into the subnet should be automatically assigned an IPv6 address." - type: bool - default: false - version_added: "2.5" - wait: - description: - - "When I(wait=true) and I(state=present), module will wait for subnet to be in available state before continuing." - type: bool - default: true - version_added: "2.5" - wait_timeout: - description: - - "Number of seconds to wait for subnet to become available I(wait=True)." - default: 300 - version_added: "2.5" - type: int - purge_tags: - description: - - Whether or not to remove tags that do not appear in the I(tags) list. - type: bool - default: true - version_added: "2.5" -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -- name: Create subnet for database servers - ec2_vpc_subnet: - state: present - vpc_id: vpc-123456 - cidr: 10.0.1.16/28 - tags: - Name: Database Subnet - register: database_subnet - -- name: Remove subnet for database servers - ec2_vpc_subnet: - state: absent - vpc_id: vpc-123456 - cidr: 10.0.1.16/28 - -- name: Create subnet with IPv6 block assigned - ec2_vpc_subnet: - state: present - vpc_id: vpc-123456 - cidr: 10.1.100.0/24 - ipv6_cidr: 2001:db8:0:102::/64 - -- name: Remove IPv6 block assigned to subnet - ec2_vpc_subnet: - state: present - vpc_id: vpc-123456 - cidr: 10.1.100.0/24 - ipv6_cidr: '' -''' - -RETURN = ''' -subnet: - description: Dictionary of subnet values - returned: I(state=present) - type: complex - contains: - id: - description: Subnet resource id - returned: I(state=present) - type: str - sample: subnet-b883b2c4 - cidr_block: - description: The IPv4 CIDR of the Subnet - returned: I(state=present) - type: str - sample: "10.0.0.0/16" - ipv6_cidr_block: - description: The IPv6 CIDR block actively associated with the Subnet - returned: I(state=present) - type: str - sample: "2001:db8:0:102::/64" - availability_zone: - description: Availability zone of the Subnet - returned: I(state=present) - type: str - sample: us-east-1a - state: - description: state of the Subnet - returned: I(state=present) - type: str - sample: available - tags: - description: tags attached to the Subnet, includes name - returned: I(state=present) - type: dict - sample: {"Name": "My Subnet", "env": "staging"} - map_public_ip_on_launch: - description: whether public IP is auto-assigned to new instances - returned: I(state=present) - type: bool - sample: false - assign_ipv6_address_on_creation: - description: whether IPv6 address is auto-assigned to new instances - returned: I(state=present) - type: bool - sample: false - vpc_id: - description: the id of the VPC where this Subnet exists - returned: I(state=present) - type: str - sample: vpc-67236184 - available_ip_address_count: - description: number of available IPv4 addresses - returned: I(state=present) - type: str - sample: 251 - default_for_az: - description: indicates whether this is the default Subnet for this Availability Zone - returned: I(state=present) - type: bool - sample: false - ipv6_association_id: - description: The IPv6 association ID for the currently associated CIDR - returned: I(state=present) - type: str - sample: subnet-cidr-assoc-b85c74d2 - ipv6_cidr_block_association_set: - description: An array of IPv6 cidr block association set information. - returned: I(state=present) - type: complex - contains: - association_id: - description: The association ID - returned: always - type: str - ipv6_cidr_block: - description: The IPv6 CIDR block that is associated with the subnet. - returned: always - type: str - ipv6_cidr_block_state: - description: A hash/dict that contains a single item. The state of the cidr block association. - returned: always - type: dict - contains: - state: - description: The CIDR block association state. - returned: always - type: str -''' - - -import time - -try: - import botocore -except ImportError: - pass # caught by AnsibleAWSModule - -from ansible.module_utils._text import to_text -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils.aws.waiters import get_waiter -from ansible.module_utils.ec2 import (ansible_dict_to_boto3_filter_list, ansible_dict_to_boto3_tag_list, - camel_dict_to_snake_dict, boto3_tag_list_to_ansible_dict, compare_aws_tags, AWSRetry) - - -def get_subnet_info(subnet): - if 'Subnets' in subnet: - return [get_subnet_info(s) for s in subnet['Subnets']] - elif 'Subnet' in subnet: - subnet = camel_dict_to_snake_dict(subnet['Subnet']) - else: - subnet = camel_dict_to_snake_dict(subnet) - - if 'tags' in subnet: - subnet['tags'] = boto3_tag_list_to_ansible_dict(subnet['tags']) - else: - subnet['tags'] = dict() - - if 'subnet_id' in subnet: - subnet['id'] = subnet['subnet_id'] - del subnet['subnet_id'] - - subnet['ipv6_cidr_block'] = '' - subnet['ipv6_association_id'] = '' - ipv6set = subnet.get('ipv6_cidr_block_association_set') - if ipv6set: - for item in ipv6set: - if item.get('ipv6_cidr_block_state', {}).get('state') in ('associated', 'associating'): - subnet['ipv6_cidr_block'] = item['ipv6_cidr_block'] - subnet['ipv6_association_id'] = item['association_id'] - - return subnet - - -@AWSRetry.exponential_backoff() -def describe_subnets_with_backoff(client, **params): - return client.describe_subnets(**params) - - -def waiter_params(module, params, start_time): - if not module.botocore_at_least("1.7.0"): - remaining_wait_timeout = int(module.params['wait_timeout'] + start_time - time.time()) - params['WaiterConfig'] = {'Delay': 5, 'MaxAttempts': remaining_wait_timeout // 5} - return params - - -def handle_waiter(conn, module, waiter_name, params, start_time): - try: - get_waiter(conn, waiter_name).wait( - **waiter_params(module, params, start_time) - ) - except botocore.exceptions.WaiterError as e: - module.fail_json_aws(e, "Failed to wait for updates to complete") - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, "An exception happened while trying to wait for updates") - - -def create_subnet(conn, module, vpc_id, cidr, ipv6_cidr=None, az=None, start_time=None): - wait = module.params['wait'] - wait_timeout = module.params['wait_timeout'] - - params = dict(VpcId=vpc_id, - CidrBlock=cidr) - - if ipv6_cidr: - params['Ipv6CidrBlock'] = ipv6_cidr - - if az: - params['AvailabilityZone'] = az - - try: - subnet = get_subnet_info(conn.create_subnet(**params)) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't create subnet") - - # Sometimes AWS takes its time to create a subnet and so using - # new subnets's id to do things like create tags results in - # exception. - if wait and subnet.get('state') != 'available': - handle_waiter(conn, module, 'subnet_exists', {'SubnetIds': [subnet['id']]}, start_time) - try: - conn.get_waiter('subnet_available').wait( - **waiter_params(module, {'SubnetIds': [subnet['id']]}, start_time) - ) - subnet['state'] = 'available' - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, "Create subnet action timed out waiting for subnet to become available") - - return subnet - - -def ensure_tags(conn, module, subnet, tags, purge_tags, start_time): - changed = False - - filters = ansible_dict_to_boto3_filter_list({'resource-id': subnet['id'], 'resource-type': 'subnet'}) - try: - cur_tags = conn.describe_tags(Filters=filters) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't describe tags") - - to_update, to_delete = compare_aws_tags(boto3_tag_list_to_ansible_dict(cur_tags.get('Tags')), tags, purge_tags) - - if to_update: - try: - if not module.check_mode: - AWSRetry.exponential_backoff( - catch_extra_error_codes=['InvalidSubnetID.NotFound'] - )(conn.create_tags)( - Resources=[subnet['id']], - Tags=ansible_dict_to_boto3_tag_list(to_update) - ) - - changed = True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't create tags") - - if to_delete: - try: - if not module.check_mode: - tags_list = [] - for key in to_delete: - tags_list.append({'Key': key}) - - AWSRetry.exponential_backoff( - catch_extra_error_codes=['InvalidSubnetID.NotFound'] - )(conn.delete_tags)(Resources=[subnet['id']], Tags=tags_list) - - changed = True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't delete tags") - - if module.params['wait'] and not module.check_mode: - # Wait for tags to be updated - filters = [{'Name': 'tag:{0}'.format(k), 'Values': [v]} for k, v in tags.items()] - handle_waiter(conn, module, 'subnet_exists', - {'SubnetIds': [subnet['id']], 'Filters': filters}, start_time) - - return changed - - -def ensure_map_public(conn, module, subnet, map_public, check_mode, start_time): - if check_mode: - return - try: - conn.modify_subnet_attribute(SubnetId=subnet['id'], MapPublicIpOnLaunch={'Value': map_public}) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't modify subnet attribute") - - -def ensure_assign_ipv6_on_create(conn, module, subnet, assign_instances_ipv6, check_mode, start_time): - if check_mode: - return - try: - conn.modify_subnet_attribute(SubnetId=subnet['id'], AssignIpv6AddressOnCreation={'Value': assign_instances_ipv6}) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't modify subnet attribute") - - -def disassociate_ipv6_cidr(conn, module, subnet, start_time): - if subnet.get('assign_ipv6_address_on_creation'): - ensure_assign_ipv6_on_create(conn, module, subnet, False, False, start_time) - - try: - conn.disassociate_subnet_cidr_block(AssociationId=subnet['ipv6_association_id']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't disassociate ipv6 cidr block id {0} from subnet {1}" - .format(subnet['ipv6_association_id'], subnet['id'])) - - # Wait for cidr block to be disassociated - if module.params['wait']: - filters = ansible_dict_to_boto3_filter_list( - {'ipv6-cidr-block-association.state': ['disassociated'], - 'vpc-id': subnet['vpc_id']} - ) - handle_waiter(conn, module, 'subnet_exists', - {'SubnetIds': [subnet['id']], 'Filters': filters}, start_time) - - -def ensure_ipv6_cidr_block(conn, module, subnet, ipv6_cidr, check_mode, start_time): - wait = module.params['wait'] - changed = False - - if subnet['ipv6_association_id'] and not ipv6_cidr: - if not check_mode: - disassociate_ipv6_cidr(conn, module, subnet, start_time) - changed = True - - if ipv6_cidr: - filters = ansible_dict_to_boto3_filter_list({'ipv6-cidr-block-association.ipv6-cidr-block': ipv6_cidr, - 'vpc-id': subnet['vpc_id']}) - - try: - check_subnets = get_subnet_info(describe_subnets_with_backoff(conn, Filters=filters)) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't get subnet info") - - if check_subnets and check_subnets[0]['ipv6_cidr_block']: - module.fail_json(msg="The IPv6 CIDR '{0}' conflicts with another subnet".format(ipv6_cidr)) - - if subnet['ipv6_association_id']: - if not check_mode: - disassociate_ipv6_cidr(conn, module, subnet, start_time) - changed = True - - try: - if not check_mode: - associate_resp = conn.associate_subnet_cidr_block(SubnetId=subnet['id'], Ipv6CidrBlock=ipv6_cidr) - changed = True - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't associate ipv6 cidr {0} to {1}".format(ipv6_cidr, subnet['id'])) - else: - if not check_mode and wait: - filters = ansible_dict_to_boto3_filter_list( - {'ipv6-cidr-block-association.state': ['associated'], - 'vpc-id': subnet['vpc_id']} - ) - handle_waiter(conn, module, 'subnet_exists', - {'SubnetIds': [subnet['id']], 'Filters': filters}, start_time) - - if associate_resp.get('Ipv6CidrBlockAssociation', {}).get('AssociationId'): - subnet['ipv6_association_id'] = associate_resp['Ipv6CidrBlockAssociation']['AssociationId'] - subnet['ipv6_cidr_block'] = associate_resp['Ipv6CidrBlockAssociation']['Ipv6CidrBlock'] - if subnet['ipv6_cidr_block_association_set']: - subnet['ipv6_cidr_block_association_set'][0] = camel_dict_to_snake_dict(associate_resp['Ipv6CidrBlockAssociation']) - else: - subnet['ipv6_cidr_block_association_set'].append(camel_dict_to_snake_dict(associate_resp['Ipv6CidrBlockAssociation'])) - - return changed - - -def get_matching_subnet(conn, module, vpc_id, cidr): - filters = ansible_dict_to_boto3_filter_list({'vpc-id': vpc_id, 'cidr-block': cidr}) - try: - subnets = get_subnet_info(describe_subnets_with_backoff(conn, Filters=filters)) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't get matching subnet") - - if subnets: - return subnets[0] - - return None - - -def ensure_subnet_present(conn, module): - subnet = get_matching_subnet(conn, module, module.params['vpc_id'], module.params['cidr']) - changed = False - - # Initialize start so max time does not exceed the specified wait_timeout for multiple operations - start_time = time.time() - - if subnet is None: - if not module.check_mode: - subnet = create_subnet(conn, module, module.params['vpc_id'], module.params['cidr'], - ipv6_cidr=module.params['ipv6_cidr'], az=module.params['az'], start_time=start_time) - changed = True - # Subnet will be None when check_mode is true - if subnet is None: - return { - 'changed': changed, - 'subnet': {} - } - if module.params['wait']: - handle_waiter(conn, module, 'subnet_exists', {'SubnetIds': [subnet['id']]}, start_time) - - if module.params['ipv6_cidr'] != subnet.get('ipv6_cidr_block'): - if ensure_ipv6_cidr_block(conn, module, subnet, module.params['ipv6_cidr'], module.check_mode, start_time): - changed = True - - if module.params['map_public'] != subnet['map_public_ip_on_launch']: - ensure_map_public(conn, module, subnet, module.params['map_public'], module.check_mode, start_time) - changed = True - - if module.params['assign_instances_ipv6'] != subnet.get('assign_ipv6_address_on_creation'): - ensure_assign_ipv6_on_create(conn, module, subnet, module.params['assign_instances_ipv6'], module.check_mode, start_time) - changed = True - - if module.params['tags'] != subnet['tags']: - stringified_tags_dict = dict((to_text(k), to_text(v)) for k, v in module.params['tags'].items()) - if ensure_tags(conn, module, subnet, stringified_tags_dict, module.params['purge_tags'], start_time): - changed = True - - subnet = get_matching_subnet(conn, module, module.params['vpc_id'], module.params['cidr']) - if not module.check_mode and module.params['wait']: - # GET calls are not monotonic for map_public_ip_on_launch and assign_ipv6_address_on_creation - # so we only wait for those if necessary just before returning the subnet - subnet = ensure_final_subnet(conn, module, subnet, start_time) - - return { - 'changed': changed, - 'subnet': subnet - } - - -def ensure_final_subnet(conn, module, subnet, start_time): - for rewait in range(0, 30): - map_public_correct = False - assign_ipv6_correct = False - - if module.params['map_public'] == subnet['map_public_ip_on_launch']: - map_public_correct = True - else: - if module.params['map_public']: - handle_waiter(conn, module, 'subnet_has_map_public', {'SubnetIds': [subnet['id']]}, start_time) - else: - handle_waiter(conn, module, 'subnet_no_map_public', {'SubnetIds': [subnet['id']]}, start_time) - - if module.params['assign_instances_ipv6'] == subnet.get('assign_ipv6_address_on_creation'): - assign_ipv6_correct = True - else: - if module.params['assign_instances_ipv6']: - handle_waiter(conn, module, 'subnet_has_assign_ipv6', {'SubnetIds': [subnet['id']]}, start_time) - else: - handle_waiter(conn, module, 'subnet_no_assign_ipv6', {'SubnetIds': [subnet['id']]}, start_time) - - if map_public_correct and assign_ipv6_correct: - break - - time.sleep(5) - subnet = get_matching_subnet(conn, module, module.params['vpc_id'], module.params['cidr']) - - return subnet - - -def ensure_subnet_absent(conn, module): - subnet = get_matching_subnet(conn, module, module.params['vpc_id'], module.params['cidr']) - if subnet is None: - return {'changed': False} - - try: - if not module.check_mode: - conn.delete_subnet(SubnetId=subnet['id']) - if module.params['wait']: - handle_waiter(conn, module, 'subnet_deleted', {'SubnetIds': [subnet['id']]}, time.time()) - return {'changed': True} - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Couldn't delete subnet") - - -def main(): - argument_spec = dict( - az=dict(default=None, required=False), - cidr=dict(required=True), - ipv6_cidr=dict(default='', required=False), - state=dict(default='present', choices=['present', 'absent']), - tags=dict(default={}, required=False, type='dict', aliases=['resource_tags']), - vpc_id=dict(required=True), - map_public=dict(default=False, required=False, type='bool'), - assign_instances_ipv6=dict(default=False, required=False, type='bool'), - wait=dict(type='bool', default=True), - wait_timeout=dict(type='int', default=300, required=False), - purge_tags=dict(default=True, type='bool') - ) - - required_if = [('assign_instances_ipv6', True, ['ipv6_cidr'])] - - module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True, required_if=required_if) - - if module.params.get('assign_instances_ipv6') and not module.params.get('ipv6_cidr'): - module.fail_json(msg="assign_instances_ipv6 is True but ipv6_cidr is None or an empty string") - - if not module.botocore_at_least("1.7.0"): - module.warn("botocore >= 1.7.0 is required to use wait_timeout for custom wait times") - - connection = module.client('ec2') - - state = module.params.get('state') - - try: - if state == 'present': - result = ensure_subnet_present(connection, module) - elif state == 'absent': - result = ensure_subnet_absent(connection, module) - except botocore.exceptions.ClientError as e: - module.fail_json_aws(e) - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc_subnet_info.py b/lib/ansible/modules/cloud/amazon/ec2_vpc_subnet_info.py deleted file mode 100644 index d582e31f3d..0000000000 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc_subnet_info.py +++ /dev/null @@ -1,250 +0,0 @@ -#!/usr/bin/python -# -# 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 - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'core'} - - -DOCUMENTATION = ''' ---- -module: ec2_vpc_subnet_info -short_description: Gather information about ec2 VPC subnets in AWS -description: - - 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. -version_added: "2.1" -author: "Rob White (@wimnat)" -requirements: - - boto3 - - botocore -options: - subnet_ids: - description: - - A list of subnet IDs to gather information for. - version_added: "2.5" - aliases: ['subnet_id'] - 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_DescribeSubnets.html) for possible filters. - type: dict -extends_documentation_fragment: - - aws - - ec2 -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Gather information about all VPC subnets -- ec2_vpc_subnet_info: - -# Gather information about a particular VPC subnet using ID -- ec2_vpc_subnet_info: - subnet_ids: subnet-00112233 - -# Gather information about any VPC subnet with a tag key Name and value Example -- ec2_vpc_subnet_info: - filters: - "tag:Name": Example - -# Gather information about any VPC subnet within VPC with ID vpc-abcdef00 -- ec2_vpc_subnet_info: - filters: - vpc-id: vpc-abcdef00 - -# Gather information about a set of VPC subnets, publicA, publicB and publicC within a -# VPC with ID vpc-abcdef00 and then use the jinja map function to return the -# subnet_ids as a list. - -- ec2_vpc_subnet_info: - filters: - vpc-id: vpc-abcdef00 - "tag:Name": "{{ item }}" - loop: - - publicA - - publicB - - publicC - register: subnet_info - -- set_fact: - subnet_ids: "{{ subnet_info.subnets|map(attribute='id')|list }}" -''' - -RETURN = ''' -subnets: - description: Returns an array of complex objects as described below. - returned: success - type: complex - contains: - subnet_id: - description: The ID of the Subnet. - returned: always - type: str - id: - description: The ID of the Subnet (for backwards compatibility). - returned: always - type: str - vpc_id: - description: The ID of the VPC . - returned: always - type: str - state: - description: The state of the subnet. - returned: always - type: str - tags: - description: A dict of tags associated with the Subnet. - returned: always - type: dict - map_public_ip_on_launch: - description: True/False depending on attribute setting for public IP mapping. - returned: always - type: bool - default_for_az: - description: True if this is the default subnet for AZ. - returned: always - type: bool - cidr_block: - description: The IPv4 CIDR block assigned to the subnet. - returned: always - type: str - available_ip_address_count: - description: Count of available IPs in subnet. - returned: always - type: str - availability_zone: - description: The availability zone where the subnet exists. - returned: always - type: str - assign_ipv6_address_on_creation: - description: True/False depending on attribute setting for IPv6 address assignment. - returned: always - type: bool - ipv6_cidr_block_association_set: - description: An array of IPv6 cidr block association set information. - returned: always - type: complex - contains: - association_id: - description: The association ID - returned: always - type: str - ipv6_cidr_block: - description: The IPv6 CIDR block that is associated with the subnet. - returned: always - type: str - ipv6_cidr_block_state: - description: A hash/dict that contains a single item. The state of the cidr block association. - returned: always - type: dict - contains: - state: - description: The CIDR block association state. - returned: always - type: str -''' - -import traceback -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.ec2 import ( - boto3_conn, - ec2_argument_spec, - get_aws_connection_info, - AWSRetry, - HAS_BOTO3, - boto3_tag_list_to_ansible_dict, - camel_dict_to_snake_dict, - ansible_dict_to_boto3_filter_list -) -from ansible.module_utils._text import to_native - -try: - import botocore -except ImportError: - pass # caught by imported HAS_BOTO3 - - -@AWSRetry.exponential_backoff() -def describe_subnets_with_backoff(connection, subnet_ids, filters): - """ - Describe Subnets with AWSRetry backoff throttling support. - - connection : boto3 client connection object - subnet_ids : list of subnet ids for which to gather information - filters : additional filters to apply to request - """ - return connection.describe_subnets(SubnetIds=subnet_ids, Filters=filters) - - -def describe_subnets(connection, module): - """ - Describe Subnets. - - module : AnsibleModule object - connection : boto3 client connection object - """ - # collect parameters - filters = ansible_dict_to_boto3_filter_list(module.params.get('filters')) - subnet_ids = module.params.get('subnet_ids') - - if subnet_ids is None: - # Set subnet_ids to empty list if it is None - subnet_ids = [] - - # init empty list for return vars - subnet_info = list() - - # Get the basic VPC info - try: - response = describe_subnets_with_backoff(connection, subnet_ids, filters) - except botocore.exceptions.ClientError as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - - for subnet in response['Subnets']: - # for backwards compatibility - subnet['id'] = subnet['SubnetId'] - subnet_info.append(camel_dict_to_snake_dict(subnet)) - # convert tag list to ansible dict - subnet_info[-1]['tags'] = boto3_tag_list_to_ansible_dict(subnet.get('Tags', [])) - - module.exit_json(subnets=subnet_info) - - -def main(): - argument_spec = ec2_argument_spec() - argument_spec.update(dict( - subnet_ids=dict(type='list', default=[], aliases=['subnet_id']), - filters=dict(type='dict', default={}) - )) - - module = AnsibleModule(argument_spec=argument_spec, - supports_check_mode=True) - if module._name == 'ec2_vpc_subnet_facts': - module.deprecate("The 'ec2_vpc_subnet_facts' module has been renamed to 'ec2_vpc_subnet_info'", version='2.13') - - if not HAS_BOTO3: - module.fail_json(msg='boto3 is required for this module') - - region, ec2_url, aws_connect_params = get_aws_connection_info(module, boto3=True) - - if region: - try: - connection = boto3_conn(module, conn_type='client', resource='ec2', region=region, endpoint=ec2_url, **aws_connect_params) - except (botocore.exceptions.NoCredentialsError, botocore.exceptions.ProfileNotFound) as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc(), **camel_dict_to_snake_dict(e.response)) - else: - module.fail_json(msg="Region must be specified") - - describe_subnets(connection, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/cloud/amazon/s3_bucket.py b/lib/ansible/modules/cloud/amazon/s3_bucket.py deleted file mode 100644 index e0439d0531..0000000000 --- a/lib/ansible/modules/cloud/amazon/s3_bucket.py +++ /dev/null @@ -1,767 +0,0 @@ -#!/usr/bin/python -# -# This is a free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This Ansible library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this library. If not, see <http://www.gnu.org/licenses/>. - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['stableinterface'], - 'supported_by': 'core'} - - -DOCUMENTATION = ''' ---- -module: s3_bucket -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 -version_added: "2.0" -requirements: [ boto3 ] -author: "Rob White (@wimnat)" -options: - force: - description: - - When trying to delete a bucket, delete all keys (including versions and delete markers) - in the bucket first (an s3 bucket must be empty for a successful deletion) - type: bool - default: 'no' - name: - description: - - Name of the s3 bucket - required: true - type: str - policy: - description: - - The JSON policy as a string. - type: json - s3_url: - description: - - S3 URL endpoint for usage with DigitalOcean, Ceph, Eucalyptus and fakes3 etc. - - Assumes AWS if not specified. - - For Walrus, use FQDN of the endpoint without scheme nor path. - aliases: [ S3_URL ] - type: str - ceph: - description: - - Enable API compatibility with Ceph. It takes into account the S3 API subset working - with Ceph in order to provide the same module behaviour where possible. - type: bool - version_added: "2.2" - requester_pays: - description: - - With Requester Pays buckets, the requester instead of the bucket owner pays the cost - of the request and the data download from the bucket. - type: bool - default: False - state: - description: - - Create or remove the s3 bucket - required: false - default: present - choices: [ 'present', 'absent' ] - type: str - tags: - description: - - tags dict to apply to bucket - type: dict - purge_tags: - description: - - whether to remove tags that aren't present in the C(tags) parameter - type: bool - default: True - version_added: "2.9" - versioning: - description: - - Whether versioning is enabled or disabled (note that once versioning is enabled, it can only be suspended) - type: bool - encryption: - description: - - Describes the default server-side encryption to apply to new objects in the bucket. - In order to remove the server-side encryption, the encryption needs to be set to 'none' explicitly. - choices: [ 'none', 'AES256', 'aws:kms' ] - version_added: "2.9" - type: str - encryption_key_id: - description: KMS master key ID to use for the default encryption. This parameter is allowed if encryption is aws:kms. If - not specified then it will default to the AWS provided KMS key. - version_added: "2.9" - type: str -extends_documentation_fragment: - - aws - - ec2 -notes: - - If C(requestPayment), C(policy), C(tagging) or C(versioning) - operations/API aren't implemented by the endpoint, module doesn't fail - if each parameter satisfies the following condition. - I(requester_pays) is C(False), I(policy), I(tags), and I(versioning) are C(None). -''' - -EXAMPLES = ''' -# Note: These examples do not set authentication details, see the AWS Guide for details. - -# Create a simple s3 bucket -- s3_bucket: - name: mys3bucket - state: present - -# Create a simple s3 bucket on Ceph Rados Gateway -- s3_bucket: - name: mys3bucket - s3_url: http://your-ceph-rados-gateway-server.xxx - ceph: true - -# Remove an s3 bucket and any keys it contains -- s3_bucket: - name: mys3bucket - state: absent - force: yes - -# Create a bucket, add a policy from a file, enable requester pays, enable versioning and tag -- s3_bucket: - name: mys3bucket - policy: "{{ lookup('file','policy.json') }}" - requester_pays: yes - versioning: yes - tags: - example: tag1 - another: tag2 - -# Create a simple DigitalOcean Spaces bucket using their provided regional endpoint -- s3_bucket: - name: mydobucket - s3_url: 'https://nyc3.digitaloceanspaces.com' - -# Create a bucket with AES256 encryption -- s3_bucket: - name: mys3bucket - state: present - encryption: "AES256" - -# Create a bucket with aws:kms encryption, KMS key -- s3_bucket: - name: mys3bucket - state: present - encryption: "aws:kms" - encryption_key_id: "arn:aws:kms:us-east-1:1234/5678example" - -# Create a bucket with aws:kms encryption, default key -- s3_bucket: - name: mys3bucket - state: present - encryption: "aws:kms" -''' - -import json -import os -import time - -from ansible.module_utils.six.moves.urllib.parse import urlparse -from ansible.module_utils.six import string_types -from ansible.module_utils.basic import to_text -from ansible.module_utils.aws.core import AnsibleAWSModule, is_boto3_error_code -from ansible.module_utils.ec2 import compare_policies, boto3_tag_list_to_ansible_dict, ansible_dict_to_boto3_tag_list -from ansible.module_utils.ec2 import get_aws_connection_info, boto3_conn, AWSRetry - -try: - from botocore.exceptions import BotoCoreError, ClientError, EndpointConnectionError, WaiterError -except ImportError: - pass # caught by AnsibleAWSModule - - -def create_or_update_bucket(s3_client, module, location): - - policy = module.params.get("policy") - name = module.params.get("name") - requester_pays = module.params.get("requester_pays") - tags = module.params.get("tags") - purge_tags = module.params.get("purge_tags") - versioning = module.params.get("versioning") - encryption = module.params.get("encryption") - encryption_key_id = module.params.get("encryption_key_id") - changed = False - result = {} - - try: - bucket_is_present = bucket_exists(s3_client, name) - except EndpointConnectionError as e: - module.fail_json_aws(e, msg="Invalid endpoint provided: %s" % to_text(e)) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Failed to check bucket presence") - - if not bucket_is_present: - try: - bucket_changed = create_bucket(s3_client, name, location) - s3_client.get_waiter('bucket_exists').wait(Bucket=name) - changed = changed or bucket_changed - except WaiterError as e: - module.fail_json_aws(e, msg='An error occurred waiting for the bucket to become available') - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Failed while creating bucket") - - # Versioning - try: - versioning_status = get_bucket_versioning(s3_client, name) - except BotoCoreError as exp: - module.fail_json_aws(exp, msg="Failed to get bucket versioning") - except ClientError as exp: - if exp.response['Error']['Code'] != 'NotImplemented' or versioning is not None: - module.fail_json_aws(exp, msg="Failed to get bucket versioning") - else: - if versioning is not None: - required_versioning = None - if versioning and versioning_status.get('Status') != "Enabled": - required_versioning = 'Enabled' - elif not versioning and versioning_status.get('Status') == "Enabled": - required_versioning = 'Suspended' - - if required_versioning: - try: - put_bucket_versioning(s3_client, name, required_versioning) - changed = True - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Failed to update bucket versioning") - - versioning_status = wait_versioning_is_applied(module, s3_client, name, required_versioning) - - # This output format is there to ensure compatibility with previous versions of the module - result['versioning'] = { - 'Versioning': versioning_status.get('Status', 'Disabled'), - 'MfaDelete': versioning_status.get('MFADelete', 'Disabled'), - } - - # Requester pays - try: - requester_pays_status = get_bucket_request_payment(s3_client, name) - except BotoCoreError as exp: - module.fail_json_aws(exp, msg="Failed to get bucket request payment") - except ClientError as exp: - if exp.response['Error']['Code'] not in ('NotImplemented', 'XNotImplemented') or requester_pays: - module.fail_json_aws(exp, msg="Failed to get bucket request payment") - else: - if requester_pays: - payer = 'Requester' if requester_pays else 'BucketOwner' - if requester_pays_status != payer: - put_bucket_request_payment(s3_client, name, payer) - requester_pays_status = wait_payer_is_applied(module, s3_client, name, payer, should_fail=False) - if requester_pays_status is None: - # We have seen that it happens quite a lot of times that the put request was not taken into - # account, so we retry one more time - put_bucket_request_payment(s3_client, name, payer) - requester_pays_status = wait_payer_is_applied(module, s3_client, name, payer, should_fail=True) - changed = True - - result['requester_pays'] = requester_pays - - # Policy - try: - current_policy = get_bucket_policy(s3_client, name) - except BotoCoreError as exp: - module.fail_json_aws(exp, msg="Failed to get bucket policy") - except ClientError as exp: - if exp.response['Error']['Code'] != 'NotImplemented' or policy is not None: - module.fail_json_aws(exp, msg="Failed to get bucket policy") - else: - if policy is not None: - if isinstance(policy, string_types): - policy = json.loads(policy) - - if not policy and current_policy: - try: - delete_bucket_policy(s3_client, name) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Failed to delete bucket policy") - current_policy = wait_policy_is_applied(module, s3_client, name, policy) - changed = True - elif compare_policies(current_policy, policy): - try: - put_bucket_policy(s3_client, name, policy) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Failed to update bucket policy") - current_policy = wait_policy_is_applied(module, s3_client, name, policy, should_fail=False) - if current_policy is None: - # As for request payement, it happens quite a lot of times that the put request was not taken into - # account, so we retry one more time - put_bucket_policy(s3_client, name, policy) - current_policy = wait_policy_is_applied(module, s3_client, name, policy, should_fail=True) - changed = True - - result['policy'] = current_policy - - # Tags - try: - current_tags_dict = get_current_bucket_tags_dict(s3_client, name) - except BotoCoreError as exp: - module.fail_json_aws(exp, msg="Failed to get bucket tags") - except ClientError as exp: - if exp.response['Error']['Code'] not in ('NotImplemented', 'XNotImplemented') or tags is not None: - module.fail_json_aws(exp, msg="Failed to get bucket tags") - else: - if tags is not None: - # Tags are always returned as text - tags = dict((to_text(k), to_text(v)) for k, v in tags.items()) - if not purge_tags: - # Ensure existing tags that aren't updated by desired tags remain - current_copy = current_tags_dict.copy() - current_copy.update(tags) - tags = current_copy - if current_tags_dict != tags: - if tags: - try: - put_bucket_tagging(s3_client, name, tags) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Failed to update bucket tags") - else: - if purge_tags: - try: - delete_bucket_tagging(s3_client, name) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Failed to delete bucket tags") - current_tags_dict = wait_tags_are_applied(module, s3_client, name, tags) - changed = True - - result['tags'] = current_tags_dict - - # Encryption - try: - current_encryption = get_bucket_encryption(s3_client, name) - except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to get bucket encryption") - - if encryption is not None: - current_encryption_algorithm = current_encryption.get('SSEAlgorithm') if current_encryption else None - current_encryption_key = current_encryption.get('KMSMasterKeyID') if current_encryption else None - if encryption == 'none' and current_encryption_algorithm is not None: - try: - delete_bucket_encryption(s3_client, name) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Failed to delete bucket encryption") - current_encryption = wait_encryption_is_applied(module, s3_client, name, None) - changed = True - elif encryption != 'none' and (encryption != current_encryption_algorithm) or (encryption == 'aws:kms' and current_encryption_key != encryption_key_id): - expected_encryption = {'SSEAlgorithm': encryption} - if encryption == 'aws:kms' and encryption_key_id is not None: - expected_encryption.update({'KMSMasterKeyID': encryption_key_id}) - current_encryption = put_bucket_encryption_with_retry(module, s3_client, name, expected_encryption) - changed = True - - result['encryption'] = current_encryption - - module.exit_json(changed=changed, name=name, **result) - - -def bucket_exists(s3_client, bucket_name): - # head_bucket appeared to be really inconsistent, so we use list_buckets instead, - # and loop over all the buckets, even if we know it's less performant :( - all_buckets = s3_client.list_buckets(Bucket=bucket_name)['Buckets'] - return any(bucket['Name'] == bucket_name for bucket in all_buckets) - - -@AWSRetry.exponential_backoff(max_delay=120) -def create_bucket(s3_client, bucket_name, location): - try: - configuration = {} - if location not in ('us-east-1', None): - configuration['LocationConstraint'] = location - if len(configuration) > 0: - s3_client.create_bucket(Bucket=bucket_name, CreateBucketConfiguration=configuration) - else: - s3_client.create_bucket(Bucket=bucket_name) - return True - except ClientError as e: - error_code = e.response['Error']['Code'] - if error_code == 'BucketAlreadyOwnedByYou': - # We should never get there since we check the bucket presence before calling the create_or_update_bucket - # method. However, the AWS Api sometimes fails to report bucket presence, so we catch this exception - return False - else: - raise e - - -@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket', 'OperationAborted']) -def put_bucket_tagging(s3_client, bucket_name, tags): - s3_client.put_bucket_tagging(Bucket=bucket_name, Tagging={'TagSet': ansible_dict_to_boto3_tag_list(tags)}) - - -@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket', 'OperationAborted']) -def put_bucket_policy(s3_client, bucket_name, policy): - s3_client.put_bucket_policy(Bucket=bucket_name, Policy=json.dumps(policy)) - - -@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket', 'OperationAborted']) -def delete_bucket_policy(s3_client, bucket_name): - s3_client.delete_bucket_policy(Bucket=bucket_name) - - -@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket', 'OperationAborted']) -def get_bucket_policy(s3_client, bucket_name): - try: - current_policy = json.loads(s3_client.get_bucket_policy(Bucket=bucket_name).get('Policy')) - except ClientError as e: - if e.response['Error']['Code'] == 'NoSuchBucketPolicy': - current_policy = None - else: - raise e - return current_policy - - -@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket', 'OperationAborted']) -def put_bucket_request_payment(s3_client, bucket_name, payer): - s3_client.put_bucket_request_payment(Bucket=bucket_name, RequestPaymentConfiguration={'Payer': payer}) - - -@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket', 'OperationAborted']) -def get_bucket_request_payment(s3_client, bucket_name): - return s3_client.get_bucket_request_payment(Bucket=bucket_name).get('Payer') - - -@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket', 'OperationAborted']) -def get_bucket_versioning(s3_client, bucket_name): - return s3_client.get_bucket_versioning(Bucket=bucket_name) - - -@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket', 'OperationAborted']) -def put_bucket_versioning(s3_client, bucket_name, required_versioning): - s3_client.put_bucket_versioning(Bucket=bucket_name, VersioningConfiguration={'Status': required_versioning}) - - -@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket', 'OperationAborted']) -def get_bucket_encryption(s3_client, bucket_name): - if not hasattr(s3_client, "get_bucket_encryption"): - return None - - try: - result = s3_client.get_bucket_encryption(Bucket=bucket_name) - return result.get('ServerSideEncryptionConfiguration', {}).get('Rules', [])[0].get('ApplyServerSideEncryptionByDefault') - except ClientError as e: - if e.response['Error']['Code'] == 'ServerSideEncryptionConfigurationNotFoundError': - return None - else: - raise e - except (IndexError, KeyError): - return None - - -def put_bucket_encryption_with_retry(module, s3_client, name, expected_encryption): - max_retries = 3 - for retries in range(1, max_retries + 1): - try: - put_bucket_encryption(s3_client, name, expected_encryption) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Failed to set bucket encryption") - current_encryption = wait_encryption_is_applied(module, s3_client, name, expected_encryption, - should_fail=(retries == max_retries), retries=5) - if current_encryption == expected_encryption: - return current_encryption - - # We shouldn't get here, the only time this should happen is if - # current_encryption != expected_encryption and retries == max_retries - # Which should use module.fail_json and fail out first. - module.fail_json(msg='Failed to apply bucket encryption', - current=current_encryption, expected=expected_encryption, retries=retries) - - -@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket', 'OperationAborted']) -def put_bucket_encryption(s3_client, bucket_name, encryption): - server_side_encryption_configuration = {'Rules': [{'ApplyServerSideEncryptionByDefault': encryption}]} - s3_client.put_bucket_encryption(Bucket=bucket_name, ServerSideEncryptionConfiguration=server_side_encryption_configuration) - - -@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket', 'OperationAborted']) -def delete_bucket_tagging(s3_client, bucket_name): - s3_client.delete_bucket_tagging(Bucket=bucket_name) - - -@AWSRetry.exponential_backoff(max_delay=120, catch_extra_error_codes=['NoSuchBucket', 'OperationAborted']) -def delete_bucket_encryption(s3_client, bucket_name): - s3_client.delete_bucket_encryption(Bucket=bucket_name) - - -@AWSRetry.exponential_backoff(max_delay=240, catch_extra_error_codes=['OperationAborted']) -def delete_bucket(s3_client, bucket_name): - try: - s3_client.delete_bucket(Bucket=bucket_name) - except ClientError as e: - if e.response['Error']['Code'] == 'NoSuchBucket': - # This means bucket should have been in a deleting state when we checked it existence - # We just ignore the error - pass - else: - raise e - - -def wait_policy_is_applied(module, s3_client, bucket_name, expected_policy, should_fail=True): - for dummy in range(0, 12): - try: - current_policy = get_bucket_policy(s3_client, bucket_name) - except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to get bucket policy") - - if compare_policies(current_policy, expected_policy): - time.sleep(5) - else: - return current_policy - if should_fail: - module.fail_json(msg="Bucket policy failed to apply in the expected time", - requested_policy=expected_policy, live_policy=current_policy) - else: - return None - - -def wait_payer_is_applied(module, s3_client, bucket_name, expected_payer, should_fail=True): - for dummy in range(0, 12): - try: - requester_pays_status = get_bucket_request_payment(s3_client, bucket_name) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Failed to get bucket request payment") - if requester_pays_status != expected_payer: - time.sleep(5) - else: - return requester_pays_status - if should_fail: - module.fail_json(msg="Bucket request payment failed to apply in the expected time", - requested_status=expected_payer, live_status=requester_pays_status) - else: - return None - - -def wait_encryption_is_applied(module, s3_client, bucket_name, expected_encryption, should_fail=True, retries=12): - for dummy in range(0, retries): - try: - encryption = get_bucket_encryption(s3_client, bucket_name) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Failed to get updated encryption for bucket") - if encryption != expected_encryption: - time.sleep(5) - else: - return encryption - - if should_fail: - module.fail_json(msg="Bucket encryption failed to apply in the expected time", - requested_encryption=expected_encryption, live_encryption=encryption) - - return encryption - - -def wait_versioning_is_applied(module, s3_client, bucket_name, required_versioning): - for dummy in range(0, 24): - try: - versioning_status = get_bucket_versioning(s3_client, bucket_name) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Failed to get updated versioning for bucket") - if versioning_status.get('Status') != required_versioning: - time.sleep(8) - else: - return versioning_status - module.fail_json(msg="Bucket versioning failed to apply in the expected time", - requested_versioning=required_versioning, live_versioning=versioning_status) - - -def wait_tags_are_applied(module, s3_client, bucket_name, expected_tags_dict): - for dummy in range(0, 12): - try: - current_tags_dict = get_current_bucket_tags_dict(s3_client, bucket_name) - except (ClientError, BotoCoreError) as e: - module.fail_json_aws(e, msg="Failed to get bucket policy") - if current_tags_dict != expected_tags_dict: - time.sleep(5) - else: - return current_tags_dict - module.fail_json(msg="Bucket tags failed to apply in the expected time", - requested_tags=expected_tags_dict, live_tags=current_tags_dict) - - -def get_current_bucket_tags_dict(s3_client, bucket_name): - try: - current_tags = s3_client.get_bucket_tagging(Bucket=bucket_name).get('TagSet') - except ClientError as e: - if e.response['Error']['Code'] == 'NoSuchTagSet': - return {} - raise e - - return boto3_tag_list_to_ansible_dict(current_tags) - - -def paginated_list(s3_client, **pagination_params): - pg = s3_client.get_paginator('list_objects_v2') - for page in pg.paginate(**pagination_params): - yield [data['Key'] for data in page.get('Contents', [])] - - -def paginated_versions_list(s3_client, **pagination_params): - try: - pg = s3_client.get_paginator('list_object_versions') - for page in pg.paginate(**pagination_params): - # We have to merge the Versions and DeleteMarker lists here, as DeleteMarkers can still prevent a bucket deletion - yield [(data['Key'], data['VersionId']) for data in (page.get('Versions', []) + page.get('DeleteMarkers', []))] - except is_boto3_error_code('NoSuchBucket'): - yield [] - - -def destroy_bucket(s3_client, module): - - force = module.params.get("force") - name = module.params.get("name") - try: - bucket_is_present = bucket_exists(s3_client, name) - except EndpointConnectionError as e: - module.fail_json_aws(e, msg="Invalid endpoint provided: %s" % to_text(e)) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Failed to check bucket presence") - - if not bucket_is_present: - module.exit_json(changed=False) - - if force: - # if there are contents then we need to delete them (including versions) before we can delete the bucket - try: - for key_version_pairs in paginated_versions_list(s3_client, Bucket=name): - formatted_keys = [{'Key': key, 'VersionId': version} for key, version in key_version_pairs] - for fk in formatted_keys: - # remove VersionId from cases where they are `None` so that - # unversioned objects are deleted using `DeleteObject` - # rather than `DeleteObjectVersion`, improving backwards - # compatibility with older IAM policies. - if not fk.get('VersionId'): - fk.pop('VersionId') - - if formatted_keys: - resp = s3_client.delete_objects(Bucket=name, Delete={'Objects': formatted_keys}) - if resp.get('Errors'): - module.fail_json( - msg='Could not empty bucket before deleting. Could not delete objects: {0}'.format( - ', '.join([k['Key'] for k in resp['Errors']]) - ), - errors=resp['Errors'], response=resp - ) - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Failed while deleting bucket") - - try: - delete_bucket(s3_client, name) - s3_client.get_waiter('bucket_not_exists').wait(Bucket=name, WaiterConfig=dict(Delay=5, MaxAttempts=60)) - except WaiterError as e: - module.fail_json_aws(e, msg='An error occurred waiting for the bucket to be deleted.') - except (BotoCoreError, ClientError) as e: - module.fail_json_aws(e, msg="Failed to delete bucket") - - module.exit_json(changed=True) - - -def is_fakes3(s3_url): - """ Return True if s3_url has scheme fakes3:// """ - if s3_url is not None: - return urlparse(s3_url).scheme in ('fakes3', 'fakes3s') - else: - return False - - -def get_s3_client(module, aws_connect_kwargs, location, ceph, s3_url): - if s3_url and ceph: # TODO - test this - ceph = urlparse(s3_url) - params = dict(module=module, conn_type='client', resource='s3', use_ssl=ceph.scheme == 'https', region=location, endpoint=s3_url, **aws_connect_kwargs) - elif is_fakes3(s3_url): - fakes3 = urlparse(s3_url) - port = fakes3.port - if fakes3.scheme == 'fakes3s': - protocol = "https" - if port is None: - port = 443 - else: - protocol = "http" - if port is None: - port = 80 - params = dict(module=module, conn_type='client', resource='s3', region=location, - endpoint="%s://%s:%s" % (protocol, fakes3.hostname, to_text(port)), - use_ssl=fakes3.scheme == 'fakes3s', **aws_connect_kwargs) - else: - params = dict(module=module, conn_type='client', resource='s3', region=location, endpoint=s3_url, **aws_connect_kwargs) - return boto3_conn(**params) - - -def main(): - - argument_spec = dict( - force=dict(default=False, type='bool'), - policy=dict(type='json'), - name=dict(required=True), - requester_pays=dict(default=False, type='bool'), - s3_url=dict(aliases=['S3_URL']), - state=dict(default='present', choices=['present', 'absent']), - tags=dict(type='dict'), - purge_tags=dict(type='bool', default=True), - versioning=dict(type='bool'), - ceph=dict(default=False, type='bool'), - encryption=dict(choices=['none', 'AES256', 'aws:kms']), - encryption_key_id=dict() - ) - - required_by = dict( - encryption_key_id=('encryption',), - ) - - module = AnsibleAWSModule( - argument_spec=argument_spec, required_by=required_by - ) - - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module, boto3=True) - - if region in ('us-east-1', '', None): - # default to US Standard region - location = 'us-east-1' - else: - # Boto uses symbolic names for locations but region strings will - # actually work fine for everything except us-east-1 (US Standard) - location = region - - s3_url = module.params.get('s3_url') - ceph = module.params.get('ceph') - - # allow eucarc environment variables to be used if ansible vars aren't set - if not s3_url and 'S3_URL' in os.environ: - s3_url = os.environ['S3_URL'] - - if ceph and not s3_url: - module.fail_json(msg='ceph flavour requires s3_url') - - # Look at s3_url and tweak connection settings - # if connecting to Ceph RGW, Walrus or fakes3 - if s3_url: - for key in ['validate_certs', 'security_token', 'profile_name']: - aws_connect_kwargs.pop(key, None) - s3_client = get_s3_client(module, aws_connect_kwargs, location, ceph, s3_url) - - if s3_client is None: # this should never happen - module.fail_json(msg='Unknown error, failed to create s3 connection, no information from boto.') - - state = module.params.get("state") - encryption = module.params.get("encryption") - encryption_key_id = module.params.get("encryption_key_id") - - if not hasattr(s3_client, "get_bucket_encryption"): - if encryption is not None: - module.fail_json(msg="Using bucket encryption requires botocore version >= 1.7.41") - - # Parameter validation - if encryption_key_id is not None and encryption != 'aws:kms': - module.fail_json(msg="Only 'aws:kms' is a valid option for encryption parameter when you specify encryption_key_id.") - - if state == 'present': - create_or_update_bucket(s3_client, module, location) - elif state == 'absent': - destroy_bucket(s3_client, module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/plugins/action/aws_s3.py b/lib/ansible/plugins/action/aws_s3.py deleted file mode 100644 index a454922a10..0000000000 --- a/lib/ansible/plugins/action/aws_s3.py +++ /dev/null @@ -1,71 +0,0 @@ -# (c) 2012, Michael DeHaan <michael.dehaan@gmail.com> -# (c) 2018, Will Thames <will@thames.id.au> -# -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see <http://www.gnu.org/licenses/>. -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import os - -from ansible.errors import AnsibleError, AnsibleAction, AnsibleActionFail, AnsibleFileNotFound -from ansible.module_utils._text import to_text -from ansible.plugins.action import ActionBase -from ansible.utils.vars import merge_hash - - -class ActionModule(ActionBase): - - TRANSFERS_FILES = True - - def run(self, tmp=None, task_vars=None): - ''' handler for aws_s3 operations ''' - self._supports_async = True - - if task_vars is None: - task_vars = dict() - - result = super(ActionModule, self).run(tmp, task_vars) - del tmp # tmp no longer has any effect - - source = self._task.args.get('src', None) - - try: - new_module_args = self._task.args.copy() - if source: - source = os.path.expanduser(source) - - # For backward compatibility check if the file exists on the remote; it should take precedence - if not self._remote_file_exists(source): - try: - source = self._loader.get_real_file(self._find_needle('files', source), decrypt=False) - new_module_args['src'] = source - except AnsibleFileNotFound as e: - # module handles error message for nonexistent files - new_module_args['src'] = source - except AnsibleError as e: - raise AnsibleActionFail(to_text(e)) - - wrap_async = self._task.async_val and not self._connection.has_native_async - # execute the aws_s3 module with the updated args - result = merge_hash(result, self._execute_module(module_args=new_module_args, task_vars=task_vars, wrap_async=wrap_async)) - - if not wrap_async: - # remove a temporary path we created - self._remove_tmp_path(self._connection._shell.tmpdir) - - except AnsibleAction as e: - result.update(e.result) - return result diff --git a/lib/ansible/plugins/callback/aws_resource_actions.py b/lib/ansible/plugins/callback/aws_resource_actions.py deleted file mode 100644 index f871fe5479..0000000000 --- a/lib/ansible/plugins/callback/aws_resource_actions.py +++ /dev/null @@ -1,72 +0,0 @@ -# (C) 2018 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -# Make coding more python3-ish -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -DOCUMENTATION = ''' - callback: aws_resource_actions - type: aggregate - short_description: summarizes all "resource:actions" completed - version_added: "2.8" - description: - - Ansible callback plugin for collecting the AWS actions completed by all boto3 modules using - AnsibleAWSModule in a playbook. Botocore endpoint logs need to be enabled for those modules, which can - be done easily by setting debug_botocore_endpoint_logs to True for group/aws using module_defaults. - requirements: - - whitelisting in configuration - see examples section below for details. -''' - -EXAMPLES = ''' -example: > - To enable, add this to your ansible.cfg file in the defaults block - [defaults] - callback_whitelist = aws_resource_actions -sample output: > -# -# AWS ACTIONS: ['s3:PutBucketAcl', 's3:HeadObject', 's3:DeleteObject', 's3:PutObjectAcl', 's3:CreateMultipartUpload', -# 's3:DeleteBucket', 's3:GetObject', 's3:DeleteObjects', 's3:CreateBucket', 's3:CompleteMultipartUpload', -# 's3:ListObjectsV2', 's3:HeadBucket', 's3:UploadPart', 's3:PutObject'] -# -sample output: > -# -# AWS ACTIONS: ['ec2:DescribeVpcAttribute', 'ec2:DescribeVpcClassicLink', 'ec2:ModifyVpcAttribute', 'ec2:CreateTags', -# 'sts:GetCallerIdentity', 'ec2:DescribeSecurityGroups', 'ec2:DescribeTags', 'ec2:DescribeVpcs', 'ec2:CreateVpc'] -# -''' - -from ansible.plugins.callback import CallbackBase -from ansible.module_utils._text import to_native - - -class CallbackModule(CallbackBase): - CALLBACK_VERSION = 2.8 - CALLBACK_TYPE = 'aggregate' - CALLBACK_NAME = 'aws_resource_actions' - CALLBACK_NEEDS_WHITELIST = True - - def __init__(self): - self.aws_resource_actions = [] - super(CallbackModule, self).__init__() - - def extend_aws_resource_actions(self, result): - if result.get('resource_actions'): - self.aws_resource_actions.extend(result['resource_actions']) - - def runner_on_ok(self, host, res): - self.extend_aws_resource_actions(res) - - def runner_on_failed(self, host, res, ignore_errors=False): - self.extend_aws_resource_actions(res) - - def v2_runner_item_on_ok(self, result): - self.extend_aws_resource_actions(result._result) - - def v2_runner_item_on_failed(self, result): - self.extend_aws_resource_actions(result._result) - - def playbook_on_stats(self, stats): - if self.aws_resource_actions: - self.aws_resource_actions = sorted(list(to_native(action) for action in set(self.aws_resource_actions))) - self._display.display("AWS ACTIONS: {0}".format(self.aws_resource_actions)) diff --git a/lib/ansible/plugins/doc_fragments/aws.py b/lib/ansible/plugins/doc_fragments/aws.py deleted file mode 100644 index 668955196f..0000000000 --- a/lib/ansible/plugins/doc_fragments/aws.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright: (c) 2014, Will Thames <will@thames.id.au> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - - -class ModuleDocFragment(object): - - # AWS only documentation fragment - DOCUMENTATION = r''' -options: - debug_botocore_endpoint_logs: - description: - - Use a botocore.endpoint logger to parse the unique (rather than total) "resource:action" API calls made during a task, outputing - the set to the resource_actions key in the task results. Use the aws_resource_action callback to output to total list made during - a playbook. The ANSIBLE_DEBUG_BOTOCORE_LOGS environment variable may also be used. - type: bool - default: 'no' - version_added: "2.8" - ec2_url: - description: - - Url to use to connect to EC2 or your Eucalyptus cloud (by default the module will use EC2 endpoints). - Ignored for modules where region is required. Must be specified for all other modules if region is not used. - If not set then the value of the EC2_URL environment variable, if any, is used. - type: str - aws_secret_key: - description: - - AWS secret key. If not set then the value of the AWS_SECRET_ACCESS_KEY, AWS_SECRET_KEY, or EC2_SECRET_KEY environment variable is used. - type: str - aliases: [ ec2_secret_key, secret_key ] - aws_access_key: - description: - - AWS access key. If not set then the value of the AWS_ACCESS_KEY_ID, AWS_ACCESS_KEY or EC2_ACCESS_KEY environment variable is used. - type: str - aliases: [ ec2_access_key, access_key ] - security_token: - description: - - AWS STS security token. If not set then the value of the AWS_SECURITY_TOKEN or EC2_SECURITY_TOKEN environment variable is used. - type: str - aliases: [ access_token ] - version_added: "1.6" - validate_certs: - description: - - When set to "no", SSL certificates will not be validated for boto versions >= 2.6.0. - type: bool - default: yes - version_added: "1.5" - profile: - description: - - Uses a boto profile. Only works with boto >= 2.24.0. - type: str - version_added: "1.6" - aws_config: - description: - - A dictionary to modify the botocore configuration. - - Parameters can be found at U(https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html#botocore.config.Config). - - 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 - version_added: "2.10" -requirements: - - python >= 2.6 - - boto -notes: - - If parameters are not set within the module, the following - environment variables can be used in decreasing order of precedence - C(AWS_URL) or C(EC2_URL), - C(AWS_ACCESS_KEY_ID) or C(AWS_ACCESS_KEY) or C(EC2_ACCESS_KEY), - C(AWS_SECRET_ACCESS_KEY) or C(AWS_SECRET_KEY) or C(EC2_SECRET_KEY), - C(AWS_SECURITY_TOKEN) or C(EC2_SECURITY_TOKEN), - C(AWS_REGION) or C(EC2_REGION) - - Ansible uses the boto configuration file (typically ~/.boto) if no - credentials are provided. See https://boto.readthedocs.io/en/latest/boto_config_tut.html - - C(AWS_REGION) or C(EC2_REGION) can be typically be used to specify the - AWS region, when required, but this can also be configured in the boto config file -''' diff --git a/lib/ansible/plugins/doc_fragments/aws_credentials.py b/lib/ansible/plugins/doc_fragments/aws_credentials.py deleted file mode 100644 index ef37ca1932..0000000000 --- a/lib/ansible/plugins/doc_fragments/aws_credentials.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright: (c) 2017, Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - - -class ModuleDocFragment(object): - - # Plugin options for AWS credentials - DOCUMENTATION = r''' -options: - aws_profile: - description: The AWS profile - type: str - aliases: [ boto_profile ] - env: - - name: AWS_DEFAULT_PROFILE - - name: AWS_PROFILE - aws_access_key: - description: The AWS access key to use. - type: str - aliases: [ aws_access_key_id ] - env: - - name: EC2_ACCESS_KEY - - name: AWS_ACCESS_KEY - - name: AWS_ACCESS_KEY_ID - aws_secret_key: - description: The AWS secret key that corresponds to the access key. - type: str - aliases: [ aws_secret_access_key ] - env: - - name: EC2_SECRET_KEY - - name: AWS_SECRET_KEY - - name: AWS_SECRET_ACCESS_KEY - aws_security_token: - description: The AWS security token if using temporary access and secret keys. - type: str - env: - - name: EC2_SECURITY_TOKEN - - name: AWS_SESSION_TOKEN - - name: AWS_SECURITY_TOKEN -''' diff --git a/lib/ansible/plugins/doc_fragments/aws_region.py b/lib/ansible/plugins/doc_fragments/aws_region.py deleted file mode 100644 index e214d78a2e..0000000000 --- a/lib/ansible/plugins/doc_fragments/aws_region.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright: (c) 2017, Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - - -class ModuleDocFragment(object): - - # Plugin option for AWS region - DOCUMENTATION = r''' -options: - region: - description: The region for which to create the connection. - type: str - env: - - name: EC2_REGION - - name: AWS_REGION -''' diff --git a/lib/ansible/plugins/doc_fragments/ec2.py b/lib/ansible/plugins/doc_fragments/ec2.py deleted file mode 100644 index 0ec0cf7a8d..0000000000 --- a/lib/ansible/plugins/doc_fragments/ec2.py +++ /dev/null @@ -1,18 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright: (c) 2015, Ansible, Inc -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - - -class ModuleDocFragment(object): - - # EC2 only documentation fragment - DOCUMENTATION = r''' -options: - region: - description: - - The AWS region to use. If not specified then the value of the AWS_REGION or EC2_REGION environment variable, if any, is used. - See U(http://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region) - type: str - aliases: [ aws_region, ec2_region ] -''' diff --git a/lib/ansible/plugins/inventory/aws_ec2.py b/lib/ansible/plugins/inventory/aws_ec2.py deleted file mode 100644 index 5f75795616..0000000000 --- a/lib/ansible/plugins/inventory/aws_ec2.py +++ /dev/null @@ -1,659 +0,0 @@ -# Copyright (c) 2017 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 = ''' - name: aws_ec2 - plugin_type: inventory - short_description: EC2 inventory source - requirements: - - boto3 - - botocore - extends_documentation_fragment: - - inventory_cache - - constructed - - aws_credentials - description: - - Get inventory hosts from Amazon Web Services EC2. - - Uses a YAML configuration file that ends with C(aws_ec2.(yml|yaml)). - notes: - - If no credentials are provided and the control node has an associated IAM instance profile then the - role will be used for authentication. - author: - - Sloane Hertel (@s-hertel) - options: - plugin: - description: Token that ensures this is a source file for the plugin. - required: True - choices: ['aws_ec2'] - iam_role_arn: - description: The ARN of the IAM role to assume to perform the inventory lookup. You should still provide AWS - credentials with enough privilege to perform the AssumeRole action. - version_added: '2.9' - regions: - description: - - A list of regions in which to describe EC2 instances. - - If empty (the default) default this will include all regions, except possibly restricted ones like us-gov-west-1 and cn-north-1. - type: list - default: [] - hostnames: - description: - - A list in order of precedence for hostname variables. - - You can use the options specified in U(http://docs.aws.amazon.com/cli/latest/reference/ec2/describe-instances.html#options). - - To use tags as hostnames use the syntax tag:Name=Value to use the hostname Name_Value, or tag:Name to use the value of the Name tag. - type: list - default: [] - filters: - description: - - A dictionary of filter value pairs. - - Available filters are listed here U(http://docs.aws.amazon.com/cli/latest/reference/ec2/describe-instances.html#options). - type: dict - default: {} - include_extra_api_calls: - description: - - Add two additional API calls for every instance to include 'persistent' and 'events' host variables. - - Spot instances may be persistent and instances may have associated events. - type: bool - default: False - version_added: '2.8' - strict_permissions: - description: - - By default if a 403 (Forbidden) error code is encountered this plugin will fail. - - You can set this option to False in the inventory config file which will allow 403 errors to be gracefully skipped. - type: bool - default: True - use_contrib_script_compatible_sanitization: - description: - - By default this plugin is using a general group name sanitization to create safe and usable group names for use in Ansible. - This option allows you to override that, in efforts to allow migration from the old inventory script and - matches the sanitization of groups when the script's ``replace_dash_in_groups`` option is set to ``False``. - To replicate behavior of ``replace_dash_in_groups = True`` with constructed groups, - you will need to replace hyphens with underscores via the regex_replace filter for those entries. - - For this to work you should also turn off the TRANSFORM_INVALID_GROUP_CHARS setting, - otherwise the core engine will just use the standard sanitization on top. - - This is not the default as such names break certain functionality as not all characters are valid Python identifiers - which group names end up being used as. - type: bool - default: False - version_added: '2.8' -''' - -EXAMPLES = ''' -# Minimal example using environment vars or instance role credentials -# Fetch all hosts in us-east-1, the hostname is the public DNS if it exists, otherwise the private IP address -plugin: aws_ec2 -regions: - - us-east-1 - -# Example using filters, ignoring permission errors, and specifying the hostname precedence -plugin: aws_ec2 -boto_profile: aws_profile -# Populate inventory with instances in these regions -regions: - - us-east-1 - - us-east-2 -filters: - # All instances with their `Environment` tag set to `dev` - tag:Environment: dev - # All dev and QA hosts - tag:Environment: - - dev - - qa - instance.group-id: sg-xxxxxxxx -# Ignores 403 errors rather than failing -strict_permissions: False -# Note: I(hostnames) sets the inventory_hostname. To modify ansible_host without modifying -# inventory_hostname use compose (see example below). -hostnames: - - tag:Name=Tag1,Name=Tag2 # Return specific hosts only - - tag:CustomDNSName - - dns-name - - private-ip-address - -# Example using constructed features to create groups and set ansible_host -plugin: aws_ec2 -regions: - - us-east-1 - - us-west-1 -# keyed_groups may be used to create custom groups -strict: False -keyed_groups: - # Add e.g. x86_64 hosts to an arch_x86_64 group - - prefix: arch - key: 'architecture' - # Add hosts to tag_Name_Value groups for each Name/Value tag pair - - prefix: tag - key: tags - # Add hosts to e.g. instance_type_z3_tiny - - prefix: instance_type - key: instance_type - # Create security_groups_sg_abcd1234 group for each SG - - key: 'security_groups|json_query("[].group_id")' - prefix: 'security_groups' - # Create a group for each value of the Application tag - - key: tags.Application - separator: '' - # Create a group per region e.g. aws_region_us_east_2 - - key: placement.region - prefix: aws_region - # Create a group (or groups) based on the value of a custom tag "Role" and add them to a metagroup called "project" - - key: tags['Role'] - prefix: foo - parent_group: "project" -# Set individual variables with compose -compose: - # Use the private IP address to connect to the host - # (note: this does not modify inventory_hostname, which is set via I(hostnames)) - ansible_host: private_ip_address -''' - -import re - -from ansible.errors import AnsibleError -from ansible.module_utils._text import to_native, to_text -from ansible.module_utils.ec2 import ansible_dict_to_boto3_filter_list, boto3_tag_list_to_ansible_dict -from ansible.module_utils.ec2 import camel_dict_to_snake_dict -from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable -from ansible.utils.display import Display - -try: - import boto3 - import botocore -except ImportError: - raise AnsibleError('The ec2 dynamic inventory plugin requires boto3 and botocore.') - -display = Display() - -# The mappings give an array of keys to get from the filter name to the value -# returned by boto3's EC2 describe_instances method. - -instance_meta_filter_to_boto_attr = { - 'group-id': ('Groups', 'GroupId'), - 'group-name': ('Groups', 'GroupName'), - 'network-interface.attachment.instance-owner-id': ('OwnerId',), - 'owner-id': ('OwnerId',), - 'requester-id': ('RequesterId',), - 'reservation-id': ('ReservationId',), -} - -instance_data_filter_to_boto_attr = { - 'affinity': ('Placement', 'Affinity'), - 'architecture': ('Architecture',), - 'availability-zone': ('Placement', 'AvailabilityZone'), - 'block-device-mapping.attach-time': ('BlockDeviceMappings', 'Ebs', 'AttachTime'), - 'block-device-mapping.delete-on-termination': ('BlockDeviceMappings', 'Ebs', 'DeleteOnTermination'), - 'block-device-mapping.device-name': ('BlockDeviceMappings', 'DeviceName'), - 'block-device-mapping.status': ('BlockDeviceMappings', 'Ebs', 'Status'), - 'block-device-mapping.volume-id': ('BlockDeviceMappings', 'Ebs', 'VolumeId'), - 'client-token': ('ClientToken',), - 'dns-name': ('PublicDnsName',), - 'host-id': ('Placement', 'HostId'), - 'hypervisor': ('Hypervisor',), - 'iam-instance-profile.arn': ('IamInstanceProfile', 'Arn'), - 'image-id': ('ImageId',), - 'instance-id': ('InstanceId',), - 'instance-lifecycle': ('InstanceLifecycle',), - 'instance-state-code': ('State', 'Code'), - 'instance-state-name': ('State', 'Name'), - 'instance-type': ('InstanceType',), - 'instance.group-id': ('SecurityGroups', 'GroupId'), - 'instance.group-name': ('SecurityGroups', 'GroupName'), - 'ip-address': ('PublicIpAddress',), - 'kernel-id': ('KernelId',), - 'key-name': ('KeyName',), - 'launch-index': ('AmiLaunchIndex',), - 'launch-time': ('LaunchTime',), - 'monitoring-state': ('Monitoring', 'State'), - 'network-interface.addresses.private-ip-address': ('NetworkInterfaces', 'PrivateIpAddress'), - 'network-interface.addresses.primary': ('NetworkInterfaces', 'PrivateIpAddresses', 'Primary'), - 'network-interface.addresses.association.public-ip': ('NetworkInterfaces', 'PrivateIpAddresses', 'Association', 'PublicIp'), - 'network-interface.addresses.association.ip-owner-id': ('NetworkInterfaces', 'PrivateIpAddresses', 'Association', 'IpOwnerId'), - 'network-interface.association.public-ip': ('NetworkInterfaces', 'Association', 'PublicIp'), - 'network-interface.association.ip-owner-id': ('NetworkInterfaces', 'Association', 'IpOwnerId'), - 'network-interface.association.allocation-id': ('ElasticGpuAssociations', 'ElasticGpuId'), - 'network-interface.association.association-id': ('ElasticGpuAssociations', 'ElasticGpuAssociationId'), - 'network-interface.attachment.attachment-id': ('NetworkInterfaces', 'Attachment', 'AttachmentId'), - 'network-interface.attachment.instance-id': ('InstanceId',), - 'network-interface.attachment.device-index': ('NetworkInterfaces', 'Attachment', 'DeviceIndex'), - 'network-interface.attachment.status': ('NetworkInterfaces', 'Attachment', 'Status'), - 'network-interface.attachment.attach-time': ('NetworkInterfaces', 'Attachment', 'AttachTime'), - 'network-interface.attachment.delete-on-termination': ('NetworkInterfaces', 'Attachment', 'DeleteOnTermination'), - 'network-interface.availability-zone': ('Placement', 'AvailabilityZone'), - 'network-interface.description': ('NetworkInterfaces', 'Description'), - 'network-interface.group-id': ('NetworkInterfaces', 'Groups', 'GroupId'), - 'network-interface.group-name': ('NetworkInterfaces', 'Groups', 'GroupName'), - 'network-interface.ipv6-addresses.ipv6-address': ('NetworkInterfaces', 'Ipv6Addresses', 'Ipv6Address'), - 'network-interface.mac-address': ('NetworkInterfaces', 'MacAddress'), - 'network-interface.network-interface-id': ('NetworkInterfaces', 'NetworkInterfaceId'), - 'network-interface.owner-id': ('NetworkInterfaces', 'OwnerId'), - 'network-interface.private-dns-name': ('NetworkInterfaces', 'PrivateDnsName'), - # 'network-interface.requester-id': (), - 'network-interface.requester-managed': ('NetworkInterfaces', 'Association', 'IpOwnerId'), - 'network-interface.status': ('NetworkInterfaces', 'Status'), - 'network-interface.source-dest-check': ('NetworkInterfaces', 'SourceDestCheck'), - 'network-interface.subnet-id': ('NetworkInterfaces', 'SubnetId'), - 'network-interface.vpc-id': ('NetworkInterfaces', 'VpcId'), - 'placement-group-name': ('Placement', 'GroupName'), - 'platform': ('Platform',), - 'private-dns-name': ('PrivateDnsName',), - 'private-ip-address': ('PrivateIpAddress',), - 'product-code': ('ProductCodes', 'ProductCodeId'), - 'product-code.type': ('ProductCodes', 'ProductCodeType'), - 'ramdisk-id': ('RamdiskId',), - 'reason': ('StateTransitionReason',), - 'root-device-name': ('RootDeviceName',), - 'root-device-type': ('RootDeviceType',), - 'source-dest-check': ('SourceDestCheck',), - 'spot-instance-request-id': ('SpotInstanceRequestId',), - 'state-reason-code': ('StateReason', 'Code'), - 'state-reason-message': ('StateReason', 'Message'), - 'subnet-id': ('SubnetId',), - 'tag': ('Tags',), - 'tag-key': ('Tags',), - 'tag-value': ('Tags',), - 'tenancy': ('Placement', 'Tenancy'), - 'virtualization-type': ('VirtualizationType',), - 'vpc-id': ('VpcId',), -} - - -class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable): - - NAME = 'aws_ec2' - - def __init__(self): - super(InventoryModule, self).__init__() - - self.group_prefix = 'aws_ec2_' - - # credentials - self.boto_profile = None - self.aws_secret_access_key = None - self.aws_access_key_id = None - self.aws_security_token = None - self.iam_role_arn = None - - def _compile_values(self, obj, attr): - ''' - :param obj: A list or dict of instance attributes - :param attr: A key - :return The value(s) found via the attr - ''' - if obj is None: - return - - temp_obj = [] - - if isinstance(obj, list) or isinstance(obj, tuple): - for each in obj: - value = self._compile_values(each, attr) - if value: - temp_obj.append(value) - else: - temp_obj = obj.get(attr) - - has_indexes = any([isinstance(temp_obj, list), isinstance(temp_obj, tuple)]) - if has_indexes and len(temp_obj) == 1: - return temp_obj[0] - - return temp_obj - - def _get_boto_attr_chain(self, filter_name, instance): - ''' - :param filter_name: The filter - :param instance: instance dict returned by boto3 ec2 describe_instances() - ''' - allowed_filters = sorted(list(instance_data_filter_to_boto_attr.keys()) + list(instance_meta_filter_to_boto_attr.keys())) - if filter_name not in allowed_filters: - raise AnsibleError("Invalid filter '%s' provided; filter must be one of %s." % (filter_name, - allowed_filters)) - if filter_name in instance_data_filter_to_boto_attr: - boto_attr_list = instance_data_filter_to_boto_attr[filter_name] - else: - boto_attr_list = instance_meta_filter_to_boto_attr[filter_name] - - instance_value = instance - for attribute in boto_attr_list: - instance_value = self._compile_values(instance_value, attribute) - return instance_value - - def _get_credentials(self): - ''' - :return A dictionary of boto client credentials - ''' - boto_params = {} - for credential in (('aws_access_key_id', self.aws_access_key_id), - ('aws_secret_access_key', self.aws_secret_access_key), - ('aws_session_token', self.aws_security_token)): - if credential[1]: - boto_params[credential[0]] = credential[1] - - return boto_params - - def _get_connection(self, credentials, region='us-east-1'): - try: - connection = boto3.session.Session(profile_name=self.boto_profile).client('ec2', region, **credentials) - except (botocore.exceptions.ProfileNotFound, botocore.exceptions.PartialCredentialsError) as e: - if self.boto_profile: - try: - connection = boto3.session.Session(profile_name=self.boto_profile).client('ec2', region) - except (botocore.exceptions.ProfileNotFound, botocore.exceptions.PartialCredentialsError) as e: - raise AnsibleError("Insufficient credentials found: %s" % to_native(e)) - else: - raise AnsibleError("Insufficient credentials found: %s" % to_native(e)) - return connection - - def _boto3_assume_role(self, credentials, region): - """ - Assume an IAM role passed by iam_role_arn parameter - - :return: a dict containing the credentials of the assumed role - """ - - iam_role_arn = self.iam_role_arn - - try: - sts_connection = boto3.session.Session(profile_name=self.boto_profile).client('sts', region, **credentials) - sts_session = sts_connection.assume_role(RoleArn=iam_role_arn, RoleSessionName='ansible_aws_ec2_dynamic_inventory') - return dict( - aws_access_key_id=sts_session['Credentials']['AccessKeyId'], - aws_secret_access_key=sts_session['Credentials']['SecretAccessKey'], - aws_session_token=sts_session['Credentials']['SessionToken'] - ) - except botocore.exceptions.ClientError as e: - raise AnsibleError("Unable to assume IAM role: %s" % to_native(e)) - - def _boto3_conn(self, regions): - ''' - :param regions: A list of regions to create a boto3 client - - Generator that yields a boto3 client and the region - ''' - - credentials = self._get_credentials() - iam_role_arn = self.iam_role_arn - - if not regions: - try: - # as per https://boto3.amazonaws.com/v1/documentation/api/latest/guide/ec2-example-regions-avail-zones.html - client = self._get_connection(credentials) - resp = client.describe_regions() - regions = [x['RegionName'] for x in resp.get('Regions', [])] - except botocore.exceptions.NoRegionError: - # above seems to fail depending on boto3 version, ignore and lets try something else - pass - - # fallback to local list hardcoded in boto3 if still no regions - if not regions: - session = boto3.Session() - regions = session.get_available_regions('ec2') - - # I give up, now you MUST give me regions - if not regions: - raise AnsibleError('Unable to get regions list from available methods, you must specify the "regions" option to continue.') - - for region in regions: - connection = self._get_connection(credentials, region) - try: - if iam_role_arn is not None: - assumed_credentials = self._boto3_assume_role(credentials, region) - else: - assumed_credentials = credentials - connection = boto3.session.Session(profile_name=self.boto_profile).client('ec2', region, **assumed_credentials) - except (botocore.exceptions.ProfileNotFound, botocore.exceptions.PartialCredentialsError) as e: - if self.boto_profile: - try: - connection = boto3.session.Session(profile_name=self.boto_profile).client('ec2', region) - except (botocore.exceptions.ProfileNotFound, botocore.exceptions.PartialCredentialsError) as e: - raise AnsibleError("Insufficient credentials found: %s" % to_native(e)) - else: - raise AnsibleError("Insufficient credentials found: %s" % to_native(e)) - yield connection, region - - def _get_instances_by_region(self, regions, filters, strict_permissions): - ''' - :param regions: a list of regions in which to describe instances - :param filters: a list of boto3 filter dictionaries - :param strict_permissions: a boolean determining whether to fail or ignore 403 error codes - :return A list of instance dictionaries - ''' - all_instances = [] - - for connection, region in self._boto3_conn(regions): - try: - # By default find non-terminated/terminating instances - if not any([f['Name'] == 'instance-state-name' for f in filters]): - filters.append({'Name': 'instance-state-name', 'Values': ['running', 'pending', 'stopping', 'stopped']}) - paginator = connection.get_paginator('describe_instances') - reservations = paginator.paginate(Filters=filters).build_full_result().get('Reservations') - instances = [] - for r in reservations: - new_instances = r['Instances'] - for instance in new_instances: - instance.update(self._get_reservation_details(r)) - if self.get_option('include_extra_api_calls'): - instance.update(self._get_event_set_and_persistence(connection, instance['InstanceId'], instance.get('SpotInstanceRequestId'))) - instances.extend(new_instances) - except botocore.exceptions.ClientError as e: - if e.response['ResponseMetadata']['HTTPStatusCode'] == 403 and not strict_permissions: - instances = [] - else: - raise AnsibleError("Failed to describe instances: %s" % to_native(e)) - except botocore.exceptions.BotoCoreError as e: - raise AnsibleError("Failed to describe instances: %s" % to_native(e)) - - all_instances.extend(instances) - - return sorted(all_instances, key=lambda x: x['InstanceId']) - - def _get_reservation_details(self, reservation): - return { - 'OwnerId': reservation['OwnerId'], - 'RequesterId': reservation.get('RequesterId', ''), - 'ReservationId': reservation['ReservationId'] - } - - def _get_event_set_and_persistence(self, connection, instance_id, spot_instance): - host_vars = {'Events': '', 'Persistent': False} - try: - kwargs = {'InstanceIds': [instance_id]} - host_vars['Events'] = connection.describe_instance_status(**kwargs)['InstanceStatuses'][0].get('Events', '') - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - if not self.get_option('strict_permissions'): - pass - else: - raise AnsibleError("Failed to describe instance status: %s" % to_native(e)) - if spot_instance: - try: - kwargs = {'SpotInstanceRequestIds': [spot_instance]} - host_vars['Persistent'] = bool( - connection.describe_spot_instance_requests(**kwargs)['SpotInstanceRequests'][0].get('Type') == 'persistent' - ) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - if not self.get_option('strict_permissions'): - pass - else: - raise AnsibleError("Failed to describe spot instance requests: %s" % to_native(e)) - return host_vars - - def _get_tag_hostname(self, preference, instance): - tag_hostnames = preference.split('tag:', 1)[1] - if ',' in tag_hostnames: - tag_hostnames = tag_hostnames.split(',') - else: - tag_hostnames = [tag_hostnames] - tags = boto3_tag_list_to_ansible_dict(instance.get('Tags', [])) - for v in tag_hostnames: - if '=' in v: - tag_name, tag_value = v.split('=') - if tags.get(tag_name) == tag_value: - return to_text(tag_name) + "_" + to_text(tag_value) - else: - tag_value = tags.get(v) - if tag_value: - return to_text(tag_value) - return None - - def _get_hostname(self, instance, hostnames): - ''' - :param instance: an instance dict returned by boto3 ec2 describe_instances() - :param hostnames: a list of hostname destination variables in order of preference - :return the preferred identifer for the host - ''' - if not hostnames: - hostnames = ['dns-name', 'private-dns-name'] - - hostname = None - for preference in hostnames: - if 'tag' in preference: - if not preference.startswith('tag:'): - raise AnsibleError("To name a host by tags name_value, use 'tag:name=value'.") - hostname = self._get_tag_hostname(preference, instance) - else: - hostname = self._get_boto_attr_chain(preference, instance) - if hostname: - break - if hostname: - if ':' in to_text(hostname): - return self._sanitize_group_name((to_text(hostname))) - else: - return to_text(hostname) - - def _query(self, regions, filters, strict_permissions): - ''' - :param regions: a list of regions to query - :param filters: a list of boto3 filter dictionaries - :param hostnames: a list of hostname destination variables in order of preference - :param strict_permissions: a boolean determining whether to fail or ignore 403 error codes - ''' - return {'aws_ec2': self._get_instances_by_region(regions, filters, strict_permissions)} - - def _populate(self, groups, hostnames): - for group in groups: - group = self.inventory.add_group(group) - self._add_hosts(hosts=groups[group], group=group, hostnames=hostnames) - self.inventory.add_child('all', group) - - def _add_hosts(self, hosts, group, hostnames): - ''' - :param hosts: a list of hosts to be added to a group - :param group: the name of the group to which the hosts belong - :param hostnames: a list of hostname destination variables in order of preference - ''' - for host in hosts: - hostname = self._get_hostname(host, hostnames) - - host = camel_dict_to_snake_dict(host, ignore_list=['Tags']) - host['tags'] = boto3_tag_list_to_ansible_dict(host.get('tags', [])) - - # Allow easier grouping by region - host['placement']['region'] = host['placement']['availability_zone'][:-1] - - if not hostname: - continue - self.inventory.add_host(hostname, group=group) - for hostvar, hostval in host.items(): - self.inventory.set_variable(hostname, hostvar, hostval) - - # Use constructed if applicable - - strict = self.get_option('strict') - - # Composed variables - self._set_composite_vars(self.get_option('compose'), host, hostname, strict=strict) - - # Complex groups based on jinja2 conditionals, hosts that meet the conditional are added to group - self._add_host_to_composed_groups(self.get_option('groups'), host, hostname, strict=strict) - - # Create groups based on variable values and add the corresponding hosts to it - self._add_host_to_keyed_groups(self.get_option('keyed_groups'), host, hostname, strict=strict) - - def _set_credentials(self): - ''' - :param config_data: contents of the inventory config file - ''' - - self.boto_profile = self.get_option('aws_profile') - self.aws_access_key_id = self.get_option('aws_access_key') - self.aws_secret_access_key = self.get_option('aws_secret_key') - self.aws_security_token = self.get_option('aws_security_token') - self.iam_role_arn = self.get_option('iam_role_arn') - - if not self.boto_profile and not (self.aws_access_key_id and self.aws_secret_access_key): - session = botocore.session.get_session() - try: - credentials = session.get_credentials().get_frozen_credentials() - except AttributeError: - pass - else: - self.aws_access_key_id = credentials.access_key - self.aws_secret_access_key = credentials.secret_key - self.aws_security_token = credentials.token - - if not self.boto_profile and not (self.aws_access_key_id and self.aws_secret_access_key): - raise AnsibleError("Insufficient boto credentials found. Please provide them in your " - "inventory configuration file or set them as environment variables.") - - def verify_file(self, path): - ''' - :param loader: an ansible.parsing.dataloader.DataLoader object - :param path: the path to the inventory config file - :return the contents of the config file - ''' - if super(InventoryModule, self).verify_file(path): - if path.endswith(('aws_ec2.yml', 'aws_ec2.yaml')): - return True - display.debug("aws_ec2 inventory filename must end with 'aws_ec2.yml' or 'aws_ec2.yaml'") - return False - - def parse(self, inventory, loader, path, cache=True): - - super(InventoryModule, self).parse(inventory, loader, path) - - self._read_config_data(path) - - if self.get_option('use_contrib_script_compatible_sanitization'): - self._sanitize_group_name = self._legacy_script_compatible_group_sanitization - - self._set_credentials() - - # get user specifications - regions = self.get_option('regions') - filters = ansible_dict_to_boto3_filter_list(self.get_option('filters')) - hostnames = self.get_option('hostnames') - strict_permissions = self.get_option('strict_permissions') - - cache_key = self.get_cache_key(path) - # false when refresh_cache or --flush-cache is used - if cache: - # get the user-specified directive - cache = self.get_option('cache') - - # Generate inventory - cache_needs_update = False - if cache: - try: - results = self._cache[cache_key] - except KeyError: - # if cache expires or cache file doesn't exist - cache_needs_update = True - - if not cache or cache_needs_update: - results = self._query(regions, filters, strict_permissions) - - self._populate(results, hostnames) - - # If the cache has expired/doesn't exist or if refresh_inventory/flush cache is used - # when the user is using caching, update the cached inventory - if cache_needs_update or (not cache and self.get_option('cache')): - self._cache[cache_key] = results - - @staticmethod - def _legacy_script_compatible_group_sanitization(name): - - # note that while this mirrors what the script used to do, it has many issues with unicode and usability in python - regex = re.compile(r"[^A-Za-z0-9\_\-]") - - return regex.sub('_', name) diff --git a/lib/ansible/plugins/inventory/aws_rds.py b/lib/ansible/plugins/inventory/aws_rds.py deleted file mode 100644 index 6b89032473..0000000000 --- a/lib/ansible/plugins/inventory/aws_rds.py +++ /dev/null @@ -1,326 +0,0 @@ -# Copyright (c) 2018 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 = ''' - name: aws_rds - plugin_type: inventory - short_description: rds instance source - description: - - Get instances and clusters from Amazon Web Services RDS. - - Uses a YAML configuration file that ends with aws_rds.(yml|yaml). - options: - regions: - description: A list of regions in which to describe RDS instances and clusters. Available regions are listed here - U(https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html) - default: [] - filters: - description: A dictionary of filter value pairs. Available filters are listed here - U(https://docs.aws.amazon.com/cli/latest/reference/rds/describe-db-instances.html#options). If you filter by - db-cluster-id and I(include_clusters) is True it will apply to clusters as well. - default: {} - strict_permissions: - description: By default if an AccessDenied exception is encountered this plugin will fail. You can set strict_permissions to - False in the inventory config file which will allow the restrictions to be gracefully skipped. - type: bool - default: True - include_clusters: - description: Whether or not to query for Aurora clusters as well as instances - type: bool - default: False - statuses: - description: A list of desired states for instances/clusters to be added to inventory. Set to ['all'] as a shorthand to find everything. - type: list - default: - - creating - - available - extends_documentation_fragment: - - inventory_cache - - constructed - - aws_credentials - requirements: - - boto3 - - botocore - author: Sloane Hertel (@s-hertel) -''' - -EXAMPLES = ''' -plugin: aws_rds -regions: - - us-east-1 - - ca-central-1 -keyed_groups: - - key: 'db_parameter_groups|json_query("[].db_parameter_group_name")' - prefix: rds_parameter_group - - key: engine - prefix: rds - - key: tags - - key: region -''' - -from ansible.errors import AnsibleError -from ansible.module_utils._text import to_native -from ansible.module_utils.aws.core import is_boto3_error_code -from ansible.module_utils.ec2 import ansible_dict_to_boto3_filter_list, boto3_tag_list_to_ansible_dict -from ansible.module_utils.ec2 import camel_dict_to_snake_dict -from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable - -try: - import boto3 - import botocore -except ImportError: - raise AnsibleError('The RDS dynamic inventory plugin requires boto3 and botocore.') - - -class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable): - - NAME = 'aws_rds' - - def __init__(self): - super(InventoryModule, self).__init__() - self.credentials = {} - self.boto_profile = None - - def _boto3_conn(self, regions): - ''' - :param regions: A list of regions to create a boto3 client - - Generator that yields a boto3 client and the region - ''' - for region in regions: - try: - connection = boto3.session.Session(profile_name=self.boto_profile).client('rds', region, **self.credentials) - except (botocore.exceptions.ProfileNotFound, botocore.exceptions.PartialCredentialsError) as e: - if self.boto_profile: - try: - connection = boto3.session.Session(profile_name=self.boto_profile).client('rds', region) - except (botocore.exceptions.ProfileNotFound, botocore.exceptions.PartialCredentialsError) as e: - raise AnsibleError("Insufficient credentials found: %s" % to_native(e)) - else: - raise AnsibleError("Insufficient credentials found: %s" % to_native(e)) - yield connection, region - - def _get_hosts_by_region(self, connection, filters, strict): - - def _add_tags_for_hosts(connection, hosts, strict): - for host in hosts: - if 'DBInstanceArn' in host: - resource_arn = host['DBInstanceArn'] - else: - resource_arn = host['DBClusterArn'] - - try: - tags = connection.list_tags_for_resource(ResourceName=resource_arn)['TagList'] - except is_boto3_error_code('AccessDenied') as e: - if not strict: - tags = [] - else: - raise e - host['Tags'] = tags - - def wrapper(f, *args, **kwargs): - try: - results = f(*args, **kwargs) - if 'DBInstances' in results: - results = results['DBInstances'] - else: - results = results['DBClusters'] - _add_tags_for_hosts(connection, results, strict) - except is_boto3_error_code('AccessDenied') as e: # pylint: disable=duplicate-except - if not strict: - results = [] - else: - raise AnsibleError("Failed to query RDS: {0}".format(to_native(e))) - except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e: # pylint: disable=duplicate-except - raise AnsibleError("Failed to query RDS: {0}".format(to_native(e))) - return results - return wrapper - - def _get_all_hosts(self, regions, instance_filters, cluster_filters, strict, statuses, gather_clusters=False): - ''' - :param regions: a list of regions in which to describe hosts - :param instance_filters: a list of boto3 filter dictionaries - :param cluster_filters: a list of boto3 filter dictionaries - :param strict: a boolean determining whether to fail or ignore 403 error codes - :param statuses: a list of statuses that the returned hosts should match - :return A list of host dictionaries - ''' - all_instances = [] - all_clusters = [] - for connection, region in self._boto3_conn(regions): - paginator = connection.get_paginator('describe_db_instances') - all_instances.extend( - self._get_hosts_by_region(connection, instance_filters, strict) - (paginator.paginate(Filters=instance_filters).build_full_result) - ) - if gather_clusters: - all_clusters.extend( - self._get_hosts_by_region(connection, cluster_filters, strict) - (connection.describe_db_clusters, **{'Filters': cluster_filters}) - ) - sorted_hosts = list( - sorted(all_instances, key=lambda x: x['DBInstanceIdentifier']) + - sorted(all_clusters, key=lambda x: x['DBClusterIdentifier']) - ) - return self.find_hosts_with_valid_statuses(sorted_hosts, statuses) - - def find_hosts_with_valid_statuses(self, hosts, statuses): - if 'all' in statuses: - return hosts - valid_hosts = [] - for host in hosts: - if host.get('DBInstanceStatus') in statuses: - valid_hosts.append(host) - elif host.get('Status') in statuses: - valid_hosts.append(host) - return valid_hosts - - def _populate(self, hosts): - group = 'aws_rds' - self.inventory.add_group(group) - if hosts: - self._add_hosts(hosts=hosts, group=group) - self.inventory.add_child('all', group) - - def _populate_from_source(self, source_data): - hostvars = source_data.pop('_meta', {}).get('hostvars', {}) - for group in source_data: - if group == 'all': - continue - else: - self.inventory.add_group(group) - hosts = source_data[group].get('hosts', []) - for host in hosts: - self._populate_host_vars([host], hostvars.get(host, {}), group) - self.inventory.add_child('all', group) - - def _get_hostname(self, host): - if host.get('DBInstanceIdentifier'): - return host['DBInstanceIdentifier'] - else: - return host['DBClusterIdentifier'] - - def _format_inventory(self, hosts): - results = {'_meta': {'hostvars': {}}} - group = 'aws_rds' - results[group] = {'hosts': []} - for host in hosts: - hostname = self._get_hostname(host) - results[group]['hosts'].append(hostname) - h = self.inventory.get_host(hostname) - results['_meta']['hostvars'][h.name] = h.vars - return results - - def _add_hosts(self, hosts, group): - ''' - :param hosts: a list of hosts to be added to a group - :param group: the name of the group to which the hosts belong - ''' - for host in hosts: - hostname = self._get_hostname(host) - host = camel_dict_to_snake_dict(host, ignore_list=['Tags']) - host['tags'] = boto3_tag_list_to_ansible_dict(host.get('tags', [])) - - # Allow easier grouping by region - if 'availability_zone' in host: - host['region'] = host['availability_zone'][:-1] - elif 'availability_zones' in host: - host['region'] = host['availability_zones'][0][:-1] - - self.inventory.add_host(hostname, group=group) - for hostvar, hostval in host.items(): - self.inventory.set_variable(hostname, hostvar, hostval) - - # Use constructed if applicable - strict = self.get_option('strict') - # Composed variables - self._set_composite_vars(self.get_option('compose'), host, hostname, strict=strict) - # Complex groups based on jinja2 conditionals, hosts that meet the conditional are added to group - self._add_host_to_composed_groups(self.get_option('groups'), host, hostname, strict=strict) - # Create groups based on variable values and add the corresponding hosts to it - self._add_host_to_keyed_groups(self.get_option('keyed_groups'), host, hostname, strict=strict) - - def _set_credentials(self): - ''' - :param config_data: contents of the inventory config file - ''' - self.boto_profile = self.get_option('aws_profile') - aws_access_key_id = self.get_option('aws_access_key') - aws_secret_access_key = self.get_option('aws_secret_key') - aws_security_token = self.get_option('aws_security_token') - - if not self.boto_profile and not (aws_access_key_id and aws_secret_access_key): - session = botocore.session.get_session() - if session.get_credentials() is not None: - aws_access_key_id = session.get_credentials().access_key - aws_secret_access_key = session.get_credentials().secret_key - aws_security_token = session.get_credentials().token - - if not self.boto_profile and not (aws_access_key_id and aws_secret_access_key): - raise AnsibleError("Insufficient boto credentials found. Please provide them in your " - "inventory configuration file or set them as environment variables.") - - if aws_access_key_id: - self.credentials['aws_access_key_id'] = aws_access_key_id - if aws_secret_access_key: - self.credentials['aws_secret_access_key'] = aws_secret_access_key - if aws_security_token: - self.credentials['aws_session_token'] = aws_security_token - - def verify_file(self, path): - ''' - :param loader: an ansible.parsing.dataloader.DataLoader object - :param path: the path to the inventory config file - :return the contents of the config file - ''' - if super(InventoryModule, self).verify_file(path): - if path.endswith(('aws_rds.yml', 'aws_rds.yaml')): - return True - return False - - def parse(self, inventory, loader, path, cache=True): - super(InventoryModule, self).parse(inventory, loader, path) - - config_data = self._read_config_data(path) - self._set_credentials() - - # get user specifications - regions = self.get_option('regions') - filters = self.get_option('filters') - strict_permissions = self.get_option('strict_permissions') - statuses = self.get_option('statuses') - include_clusters = self.get_option('include_clusters') - instance_filters = ansible_dict_to_boto3_filter_list(filters) - cluster_filters = [] - if 'db-cluster-id' in filters and include_clusters: - cluster_filters = ansible_dict_to_boto3_filter_list({'db-cluster-id': filters['db-cluster-id']}) - - cache_key = self.get_cache_key(path) - # false when refresh_cache or --flush-cache is used - if cache: - # get the user-specified directive - cache = self.get_option('cache') - - # Generate inventory - formatted_inventory = {} - cache_needs_update = False - if cache: - try: - results = self._cache[cache_key] - except KeyError: - # if cache expires or cache file doesn't exist - cache_needs_update = True - else: - self._populate_from_source(results) - - if not cache or cache_needs_update: - results = self._get_all_hosts(regions, instance_filters, cluster_filters, strict_permissions, statuses, include_clusters) - self._populate(results) - formatted_inventory = self._format_inventory(results) - - # If the cache has expired/doesn't exist or if refresh_inventory/flush cache is used - # when the user is using caching, update the cached inventory - if cache_needs_update or (not cache and self.get_option('cache')): - self._cache[cache_key] = formatted_inventory diff --git a/lib/ansible/plugins/lookup/aws_account_attribute.py b/lib/ansible/plugins/lookup/aws_account_attribute.py deleted file mode 100644 index 23f311da42..0000000000 --- a/lib/ansible/plugins/lookup/aws_account_attribute.py +++ /dev/null @@ -1,131 +0,0 @@ -# (c) 2017 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 = """ -lookup: aws_account_attribute -author: - - Sloane Hertel <shertel@redhat.com> -version_added: "2.5" -requirements: - - boto3 - - botocore -extends_documentation_fragment: - - aws_credentials - - aws_region -short_description: Look up AWS account attributes. -description: - - Describes attributes of your AWS account. You can specify one of the listed - attribute choices or omit it to see all attributes. -options: - attribute: - description: The attribute for which to get the value(s). - choices: - - supported-platforms - - default-vpc - - max-instances - - vpc-max-security-groups-per-interface - - max-elastic-ips - - vpc-max-elastic-ips - - has-ec2-classic -""" - -EXAMPLES = """ -vars: - has_ec2_classic: "{{ lookup('aws_account_attribute', attribute='has-ec2-classic') }}" - # true | false - - default_vpc_id: "{{ lookup('aws_account_attribute', attribute='default-vpc') }}" - # vpc-xxxxxxxx | none - - account_details: "{{ lookup('aws_account_attribute', wantlist='true') }}" - # {'default-vpc': ['vpc-xxxxxxxx'], 'max-elastic-ips': ['5'], 'max-instances': ['20'], - # 'supported-platforms': ['VPC', 'EC2'], 'vpc-max-elastic-ips': ['5'], 'vpc-max-security-groups-per-interface': ['5']} - -""" - -RETURN = """ -_raw: - description: - Returns a boolean when I(attribute) is check_ec2_classic. Otherwise returns the value(s) of the attribute - (or all attributes if one is not specified). -""" - -from ansible.errors import AnsibleError - -try: - import boto3 - import botocore -except ImportError: - raise AnsibleError("The lookup aws_account_attribute requires boto3 and botocore.") - -from ansible.plugins import AnsiblePlugin -from ansible.plugins.lookup import LookupBase -from ansible.module_utils.ec2 import boto3_conn, get_aws_connection_info -from ansible.module_utils._text import to_native -from ansible.module_utils.six import string_types -import os - - -def _boto3_conn(region, credentials): - boto_profile = credentials.pop('aws_profile', None) - - try: - connection = boto3.session.Session(profile_name=boto_profile).client('ec2', region, **credentials) - except (botocore.exceptions.ProfileNotFound, botocore.exceptions.PartialCredentialsError) as e: - if boto_profile: - try: - connection = boto3.session.Session(profile_name=boto_profile).client('ec2', region) - except (botocore.exceptions.ProfileNotFound, botocore.exceptions.PartialCredentialsError) as e: - raise AnsibleError("Insufficient credentials found.") - else: - raise AnsibleError("Insufficient credentials found.") - return connection - - -def _get_credentials(options): - credentials = {} - credentials['aws_profile'] = options['aws_profile'] - credentials['aws_secret_access_key'] = options['aws_secret_key'] - credentials['aws_access_key_id'] = options['aws_access_key'] - credentials['aws_session_token'] = options['aws_security_token'] - - return credentials - - -class LookupModule(LookupBase): - def run(self, terms, variables, **kwargs): - - self.set_options(var_options=variables, direct=kwargs) - boto_credentials = _get_credentials(self._options) - - region = self._options['region'] - client = _boto3_conn(region, boto_credentials) - - attribute = kwargs.get('attribute') - params = {'AttributeNames': []} - check_ec2_classic = False - if 'has-ec2-classic' == attribute: - check_ec2_classic = True - params['AttributeNames'] = ['supported-platforms'] - elif attribute: - params['AttributeNames'] = [attribute] - - try: - response = client.describe_account_attributes(**params)['AccountAttributes'] - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - raise AnsibleError("Failed to describe account attributes: %s" % to_native(e)) - - if check_ec2_classic: - attr = response[0] - return any(value['AttributeValue'] == 'EC2' for value in attr['AttributeValues']) - - if attribute: - attr = response[0] - return [value['AttributeValue'] for value in attr['AttributeValues']] - - flattened = {} - for k_v_dict in response: - flattened[k_v_dict['AttributeName']] = [value['AttributeValue'] for value in k_v_dict['AttributeValues']] - return flattened diff --git a/lib/ansible/plugins/lookup/aws_secret.py b/lib/ansible/plugins/lookup/aws_secret.py deleted file mode 100644 index fa100e7df5..0000000000 --- a/lib/ansible/plugins/lookup/aws_secret.py +++ /dev/null @@ -1,140 +0,0 @@ -# Copyright: (c) 2018, Aaron Smith <ajsmith10381@gmail.com> -# 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""" -lookup: aws_secret -author: - - Aaron Smith <ajsmith10381@gmail.com> -version_added: "2.8" -requirements: - - boto3 - - botocore>=1.10.0 -extends_documentation_fragment: - - aws_credentials - - aws_region -short_description: Look up secrets stored in AWS Secrets Manager. -description: - - Look up secrets stored in AWS Secrets Manager provided the caller - has the appropriate permissions to read the secret. - - Lookup is based on the secret's `Name` value. - - Optional parameters can be passed into this lookup; `version_id` and `version_stage` -options: - _terms: - description: Name of the secret to look up in AWS Secrets Manager. - required: True - version_id: - description: Version of the secret(s). - required: False - version_stage: - description: Stage of the secret version. - required: False - join: - description: - - Join two or more entries to form an extended secret. - - This is useful for overcoming the 4096 character limit imposed by AWS. - type: boolean - default: false -""" - -EXAMPLES = r""" - - name: Create RDS instance with aws_secret lookup for password param - rds: - command: create - instance_name: app-db - db_engine: MySQL - size: 10 - instance_type: db.m1.small - username: dbadmin - password: "{{ lookup('aws_secret', 'DbSecret') }}" - tags: - Environment: staging -""" - -RETURN = r""" -_raw: - description: - Returns the value of the secret stored in AWS Secrets Manager. -""" - -from ansible.errors import AnsibleError - -try: - import boto3 - import botocore -except ImportError: - raise AnsibleError("The lookup aws_secret requires boto3 and botocore.") - -from ansible.plugins import AnsiblePlugin -from ansible.plugins.lookup import LookupBase -from ansible.module_utils._text import to_native - - -def _boto3_conn(region, credentials): - boto_profile = credentials.pop('aws_profile', None) - - try: - connection = boto3.session.Session(profile_name=boto_profile).client('secretsmanager', region, **credentials) - except (botocore.exceptions.ProfileNotFound, botocore.exceptions.PartialCredentialsError) as e: - if boto_profile: - try: - connection = boto3.session.Session(profile_name=boto_profile).client('secretsmanager', region) - except (botocore.exceptions.ProfileNotFound, botocore.exceptions.PartialCredentialsError) as e: - raise AnsibleError("Insufficient credentials found.") - else: - raise AnsibleError("Insufficient credentials found.") - return connection - - -class LookupModule(LookupBase): - def _get_credentials(self): - credentials = {} - credentials['aws_profile'] = self.get_option('aws_profile') - credentials['aws_secret_access_key'] = self.get_option('aws_secret_key') - credentials['aws_access_key_id'] = self.get_option('aws_access_key') - credentials['aws_session_token'] = self.get_option('aws_security_token') - - # fallback to IAM role credentials - if not credentials['aws_profile'] and not (credentials['aws_access_key_id'] and credentials['aws_secret_access_key']): - session = botocore.session.get_session() - if session.get_credentials() is not None: - credentials['aws_access_key_id'] = session.get_credentials().access_key - credentials['aws_secret_access_key'] = session.get_credentials().secret_key - credentials['aws_session_token'] = session.get_credentials().token - - return credentials - - def run(self, terms, variables, **kwargs): - - self.set_options(var_options=variables, direct=kwargs) - boto_credentials = self._get_credentials() - - region = self.get_option('region') - client = _boto3_conn(region, boto_credentials) - - secrets = [] - for term in terms: - params = {} - params['SecretId'] = term - if kwargs.get('version_id'): - params['VersionId'] = kwargs.get('version_id') - if kwargs.get('version_stage'): - params['VersionStage'] = kwargs.get('version_stage') - - try: - response = client.get_secret_value(**params) - if 'SecretBinary' in response: - secrets.append(response['SecretBinary']) - if 'SecretString' in response: - secrets.append(response['SecretString']) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - raise AnsibleError("Failed to retrieve secret: %s" % to_native(e)) - - if kwargs.get('join'): - joined_secret = [] - joined_secret.append(''.join(secrets)) - return joined_secret - else: - return secrets diff --git a/lib/ansible/plugins/lookup/aws_service_ip_ranges.py b/lib/ansible/plugins/lookup/aws_service_ip_ranges.py deleted file mode 100644 index 89072f6962..0000000000 --- a/lib/ansible/plugins/lookup/aws_service_ip_ranges.py +++ /dev/null @@ -1,79 +0,0 @@ -# (c) 2016 James Turner <turnerjsm@gmail.com> -# (c) 2017 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 = """ -lookup: aws_service_ip_ranges -author: - - James Turner <turnerjsm@gmail.com> -version_added: "2.5" -requirements: - - must have public internet connectivity -short_description: Look up the IP ranges for services provided in AWS such as EC2 and S3. -description: - - AWS publishes IP ranges used on the public internet by EC2, S3, CloudFront, CodeBuild, Route53, and Route53 Health Checking. - - This module produces a list of all the ranges (by default) or can narrow down the list to the specified region or service. -options: - service: - description: 'The service to filter ranges by. Options: EC2, S3, CLOUDFRONT, CODEbUILD, ROUTE53, ROUTE53_HEALTHCHECKS' - region: - description: 'The AWS region to narrow the ranges to. Examples: us-east-1, eu-west-2, ap-southeast-1' -""" - -EXAMPLES = """ -vars: - ec2_ranges: "{{ lookup('aws_service_ip_ranges', region='ap-southeast-2', service='EC2', wantlist=True) }}" -tasks: - -- name: "use list return option and iterate as a loop" - debug: msg="{% for cidr in ec2_ranges %}{{ cidr }} {% endfor %}" -# "52.62.0.0/15 52.64.0.0/17 52.64.128.0/17 52.65.0.0/16 52.95.241.0/24 52.95.255.16/28 54.66.0.0/16 " - -- name: "Pull S3 IP ranges, and print the default return style" - debug: msg="{{ lookup('aws_service_ip_ranges', region='us-east-1', service='S3') }}" -# "52.92.16.0/20,52.216.0.0/15,54.231.0.0/17" -""" - -RETURN = """ -_raw: - description: comma-separated list of CIDR ranges -""" - - -import json - -from ansible.errors import AnsibleError -from ansible.plugins.lookup import LookupBase -from ansible.module_utils.urls import open_url, ConnectionError, SSLValidationError -from ansible.module_utils._text import to_native -from ansible.module_utils.six.moves.urllib.error import HTTPError, URLError - - -class LookupModule(LookupBase): - def run(self, terms, variables, **kwargs): - try: - resp = open_url('https://ip-ranges.amazonaws.com/ip-ranges.json') - amazon_response = json.load(resp)['prefixes'] - except getattr(json.decoder, 'JSONDecodeError', ValueError) as e: - # on Python 3+, json.decoder.JSONDecodeError is raised for bad - # JSON. On 2.x it's a ValueError - raise AnsibleError("Could not decode AWS IP ranges: %s" % to_native(e)) - except HTTPError as e: - raise AnsibleError("Received HTTP error while pulling IP ranges: %s" % to_native(e)) - except SSLValidationError as e: - raise AnsibleError("Error validating the server's certificate for: %s" % to_native(e)) - except URLError as e: - raise AnsibleError("Failed look up IP range service: %s" % to_native(e)) - except ConnectionError as e: - raise AnsibleError("Error connecting to IP range service: %s" % to_native(e)) - - if 'region' in kwargs: - region = kwargs['region'] - amazon_response = (item for item in amazon_response if item['region'] == region) - if 'service' in kwargs: - service = str.upper(kwargs['service']) - amazon_response = (item for item in amazon_response if item['service'] == service) - - return [item['ip_prefix'] for item in amazon_response] diff --git a/lib/ansible/plugins/lookup/aws_ssm.py b/lib/ansible/plugins/lookup/aws_ssm.py deleted file mode 100644 index 7d875ce3e5..0000000000 --- a/lib/ansible/plugins/lookup/aws_ssm.py +++ /dev/null @@ -1,233 +0,0 @@ -# (c) 2016, Bill Wang <ozbillwang(at)gmail.com> -# (c) 2017, Marat Bakeev <hawara(at)gmail.com> -# (c) 2018, Michael De La Rue <siblemitcom.mddlr(at)spamgourmet.com> -# (c) 2017 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 = ''' -lookup: aws_ssm -author: - - Bill Wang <ozbillwang(at)gmail.com> - - Marat Bakeev <hawara(at)gmail.com> - - Michael De La Rue <siblemitcom.mddlr@spamgourmet.com> -version_added: 2.5 -requirements: - - boto3 - - botocore -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. - The first argument you pass the lookup can either be a parameter name or a hierarchy of - parameters. Hierarchies start with a forward slash and end with the parameter name. Up to - 5 layers may be specified. - - If looking up an explicitly listed parameter by name which does not exist then the lookup will - return a None value which will be interpreted by Jinja2 as an empty string. You can use the - ```default``` filter to give a default value in this case but must set the second parameter to - true (see examples below) - - When looking up a path for parameters under it a dictionary will be returned for each path. - If there is no parameter under that path then the return will be successful but the - dictionary will be empty. - - If the lookup fails due to lack of permissions or due to an AWS client error then the aws_ssm - will generate an error, normally crashing the current ansible task. This is normally the right - thing since ignoring a value that IAM isn't giving access to could cause bigger problems and - wrong behaviour or loss of data. If you want to continue in this case then you will have to set - up two ansible tasks, one which sets a variable and ignores failures one which uses the value - of that variable with a default. See the examples below. - -options: - decrypt: - description: A boolean to indicate whether to decrypt the parameter. - default: true - type: boolean - bypath: - description: A boolean to indicate whether the parameter is provided as a hierarchy. - default: false - type: boolean - recursive: - description: A boolean to indicate whether to retrieve all parameters within a hierarchy. - default: false - type: boolean - shortnames: - description: Indicates whether to return the name only without path if using a parameter hierarchy. - default: false - type: boolean -''' - -EXAMPLES = ''' -# lookup sample: -- name: lookup ssm parameter store in the current region - debug: msg="{{ lookup('aws_ssm', 'Hello' ) }}" - -- name: lookup ssm parameter store in nominated region - debug: msg="{{ lookup('aws_ssm', 'Hello', region='us-east-2' ) }}" - -- name: lookup ssm parameter store without decrypted - debug: msg="{{ lookup('aws_ssm', 'Hello', decrypt=False ) }}" - -- name: lookup ssm parameter store in nominated aws profile - debug: msg="{{ lookup('aws_ssm', 'Hello', aws_profile='myprofile' ) }}" - -- name: lookup ssm parameter store using explicit aws credentials - debug: msg="{{ lookup('aws_ssm', 'Hello', aws_access_key=my_aws_access_key, aws_secret_key=my_aws_secret_key, aws_security_token=my_security_token ) }}" - -- name: lookup ssm parameter store with all options. - debug: msg="{{ lookup('aws_ssm', 'Hello', decrypt=false, region='us-east-2', aws_profile='myprofile') }}" - -- name: lookup a key which doesn't exist, returns "" - debug: msg="{{ lookup('aws_ssm', 'NoKey') }}" - -- name: lookup a key which doesn't exist, returning a default ('root') - debug: msg="{{ lookup('aws_ssm', 'AdminID') | default('root', true) }}" - -- name: lookup a key which doesn't exist failing to store it in a fact - set_fact: - temp_secret: "{{ lookup('aws_ssm', '/NoAccess/hiddensecret') }}" - ignore_errors: true - -- name: show fact default to "access failed" if we don't have access - debug: msg="{{ 'the secret was:' ~ temp_secret | default('could not access secret') }}" - -- name: return a dictionary of ssm parameters from a hierarchy path - debug: msg="{{ lookup('aws_ssm', '/PATH/to/params', region='ap-southeast-2', bypath=true, recursive=true ) }}" - -- name: return a dictionary of ssm parameters from a hierarchy path with shortened names (param instead of /PATH/to/param) - debug: msg="{{ lookup('aws_ssm', '/PATH/to/params', region='ap-southeast-2', shortnames=true, bypath=true, recursive=true ) }}" - -- name: Iterate over a parameter hierarchy (one iteration per parameter) - debug: msg='Key contains {{ item.key }} , with value {{ item.value }}' - loop: '{{ lookup("aws_ssm", "/demo/", region="ap-southeast-2", bypath=True) | dict2items }}' - -- name: Iterate over multiple paths as dictionaries (one iteration per path) - debug: msg='Path contains {{ item }}' - loop: '{{ lookup("aws_ssm", "/demo/", "/demo1/", bypath=True)}}' - -''' - -from ansible.module_utils._text import to_native -from ansible.module_utils.ec2 import HAS_BOTO3, boto3_tag_list_to_ansible_dict -from ansible.errors import AnsibleError -from ansible.plugins.lookup import LookupBase -from ansible.utils.display import Display - -try: - from botocore.exceptions import ClientError - import botocore - import boto3 -except ImportError: - pass # will be captured by imported HAS_BOTO3 - -display = Display() - - -def _boto3_conn(region, credentials): - if 'boto_profile' in credentials: - boto_profile = credentials.pop('boto_profile') - else: - boto_profile = None - - try: - connection = boto3.session.Session(profile_name=boto_profile).client('ssm', region, **credentials) - except (botocore.exceptions.ProfileNotFound, botocore.exceptions.PartialCredentialsError): - if boto_profile: - try: - connection = boto3.session.Session(profile_name=boto_profile).client('ssm', region) - # FIXME: we should probably do better passing on of the error information - except (botocore.exceptions.ProfileNotFound, botocore.exceptions.PartialCredentialsError): - raise AnsibleError("Insufficient credentials found.") - else: - raise AnsibleError("Insufficient credentials found.") - return connection - - -class LookupModule(LookupBase): - def run(self, terms, variables=None, boto_profile=None, aws_profile=None, - aws_secret_key=None, aws_access_key=None, aws_security_token=None, region=None, - bypath=False, shortnames=False, recursive=False, decrypt=True): - ''' - :arg terms: a list of lookups to run. - e.g. ['parameter_name', 'parameter_name_too' ] - :kwarg variables: ansible variables active at the time of the lookup - :kwarg aws_secret_key: identity of the AWS key to use - :kwarg aws_access_key: AWS secret key (matching identity) - :kwarg aws_security_token: AWS session key if using STS - :kwarg decrypt: Set to True to get decrypted parameters - :kwarg region: AWS region in which to do the lookup - :kwarg bypath: Set to True to do a lookup of variables under a path - :kwarg recursive: Set to True to recurse below the path (requires bypath=True) - :returns: A list of parameter values or a list of dictionaries if bypath=True. - ''' - - if not HAS_BOTO3: - raise AnsibleError('botocore and boto3 are required for aws_ssm lookup.') - - ret = [] - response = {} - ssm_dict = {} - - credentials = {} - if aws_profile: - credentials['boto_profile'] = aws_profile - else: - credentials['boto_profile'] = boto_profile - credentials['aws_secret_access_key'] = aws_secret_key - credentials['aws_access_key_id'] = aws_access_key - credentials['aws_session_token'] = aws_security_token - - client = _boto3_conn(region, credentials) - - ssm_dict['WithDecryption'] = decrypt - - # Lookup by path - if bypath: - ssm_dict['Recursive'] = recursive - for term in terms: - ssm_dict["Path"] = term - display.vvv("AWS_ssm path lookup term: %s in region: %s" % (term, region)) - try: - response = client.get_parameters_by_path(**ssm_dict) - except ClientError as e: - raise AnsibleError("SSM lookup exception: {0}".format(to_native(e))) - paramlist = list() - paramlist.extend(response['Parameters']) - - # Manual pagination, since boto doesn't support it yet for get_parameters_by_path - while 'NextToken' in response: - response = client.get_parameters_by_path(NextToken=response['NextToken'], **ssm_dict) - paramlist.extend(response['Parameters']) - - # shorten parameter names. yes, this will return duplicate names with different values. - if shortnames: - for x in paramlist: - x['Name'] = x['Name'][x['Name'].rfind('/') + 1:] - - display.vvvv("AWS_ssm path lookup returned: %s" % str(paramlist)) - if len(paramlist): - ret.append(boto3_tag_list_to_ansible_dict(paramlist, - tag_name_key_name="Name", - tag_value_key_name="Value")) - else: - ret.append({}) - # Lookup by parameter name - always returns a list with one or no entry. - else: - display.vvv("AWS_ssm name lookup term: %s" % terms) - ssm_dict["Names"] = terms - try: - response = client.get_parameters(**ssm_dict) - except ClientError as e: - raise AnsibleError("SSM lookup exception: {0}".format(to_native(e))) - params = boto3_tag_list_to_ansible_dict(response['Parameters'], tag_name_key_name="Name", - tag_value_key_name="Value") - for i in terms: - if i.split(':', 1)[0] in params: - ret.append(params[i]) - elif i in response['InvalidParameters']: - ret.append(None) - else: - raise AnsibleError("Ansible internal error: aws_ssm lookup failed to understand boto3 return value: {0}".format(str(response))) - return ret - - display.vvvv("AWS_ssm path lookup returning: %s " % str(ret)) - return ret diff --git a/test/integration/targets/aws_caller_info/aliases b/test/integration/targets/aws_caller_info/aliases deleted file mode 100644 index 6e3860bee2..0000000000 --- a/test/integration/targets/aws_caller_info/aliases +++ /dev/null @@ -1,2 +0,0 @@ -cloud/aws -shippable/aws/group2 diff --git a/test/integration/targets/aws_caller_info/tasks/main.yaml b/test/integration/targets/aws_caller_info/tasks/main.yaml deleted file mode 100644 index 5645de6bc4..0000000000 --- a/test/integration/targets/aws_caller_info/tasks/main.yaml +++ /dev/null @@ -1,15 +0,0 @@ -- name: retrieve caller facts - aws_caller_info: - region: "{{ aws_region }}" - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" - security_token: "{{ security_token }}" - register: result - -- name: assert correct keys are returned - assert: - that: - - result.account is not none - - result.arn is not none - - result.user_id is not none - - result.account_alias is not none diff --git a/test/integration/targets/aws_s3/aliases b/test/integration/targets/aws_s3/aliases deleted file mode 100644 index 72a9fb4f57..0000000000 --- a/test/integration/targets/aws_s3/aliases +++ /dev/null @@ -1,2 +0,0 @@ -cloud/aws -shippable/aws/group4 diff --git a/test/integration/targets/aws_s3/defaults/main.yml b/test/integration/targets/aws_s3/defaults/main.yml deleted file mode 100644 index eb7dd2d371..0000000000 --- a/test/integration/targets/aws_s3/defaults/main.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -# defaults file for s3 -bucket_name: '{{resource_prefix}}' diff --git a/test/integration/targets/aws_s3/files/hello.txt b/test/integration/targets/aws_s3/files/hello.txt deleted file mode 100644 index 8ab686eafe..0000000000 --- a/test/integration/targets/aws_s3/files/hello.txt +++ /dev/null @@ -1 +0,0 @@ -Hello, World! diff --git a/test/integration/targets/aws_s3/meta/main.yml b/test/integration/targets/aws_s3/meta/main.yml deleted file mode 100644 index e69de29bb2..0000000000 --- a/test/integration/targets/aws_s3/meta/main.yml +++ /dev/null diff --git a/test/integration/targets/aws_s3/tasks/main.yml b/test/integration/targets/aws_s3/tasks/main.yml deleted file mode 100644 index a0a7aa7950..0000000000 --- a/test/integration/targets/aws_s3/tasks/main.yml +++ /dev/null @@ -1,590 +0,0 @@ ---- -# tasks file for test_s3 - -- name: set up aws connection info - set_fact: - aws_connection_info: &aws_connection_info - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" - security_token: "{{ security_token }}" - region: "{{ aws_region }}" - no_log: yes - -- block: - - name: Create temporary directory - tempfile: - state: directory - register: tmpdir - - - name: Create content - set_fact: - content: "{{ lookup('password', '/dev/null chars=ascii_letters,digits,hexdigits,punctuation') }}" - - - name: test create bucket without permissions - aws_s3: - bucket: "{{ bucket_name }}" - mode: create - register: result - ignore_errors: yes - - - assert: - that: - - result is failed - - "result.msg != 'MODULE FAILURE'" - - - name: test create bucket - aws_s3: - bucket: "{{ bucket_name }}" - mode: create - <<: *aws_connection_info - register: result - - - assert: - that: - - result is changed - - - name: trying to create a bucket name that already exists - aws_s3: - bucket: "{{ bucket_name }}" - mode: create - <<: *aws_connection_info - register: result - - - assert: - that: - - result is not changed - - - name: Create local upload.txt - copy: - content: "{{ content }}" - dest: "{{ tmpdir.path }}/upload.txt" - - - name: stat the file - stat: - path: "{{ tmpdir.path }}/upload.txt" - get_checksum: yes - register: upload_file - - - name: test putting an object in the bucket - aws_s3: - bucket: "{{ bucket_name }}" - mode: put - src: "{{ tmpdir.path }}/upload.txt" - object: delete.txt - <<: *aws_connection_info - retries: 3 - delay: 3 - register: result - - - assert: - that: - - result is changed - - result.msg == "PUT operation complete" - - - name: test using aws_s3 with async - aws_s3: - bucket: "{{ bucket_name }}" - mode: put - src: "{{ tmpdir.path }}/upload.txt" - object: delete.txt - <<: *aws_connection_info - register: test_async - async: 30 - poll: 0 - - - name: ensure it completed - async_status: - jid: "{{ test_async.ansible_job_id }}" - register: status - until: status is finished - retries: 10 - - - name: test put with overwrite=different and unmodified object - aws_s3: - bucket: "{{ bucket_name }}" - mode: put - src: "{{ tmpdir.path }}/upload.txt" - object: delete.txt - overwrite: different - <<: *aws_connection_info - retries: 3 - delay: 3 - register: result - - - assert: - that: - - result is not changed - - - name: check that roles file lookups work as expected - aws_s3: - bucket: "{{ bucket_name }}" - mode: put - src: hello.txt - object: delete.txt - <<: *aws_connection_info - retries: 3 - delay: 3 - register: result - - - assert: - that: - - result is changed - - result.msg == "PUT operation complete" - - - name: test put with overwrite=never - aws_s3: - bucket: "{{ bucket_name }}" - mode: put - src: "{{ tmpdir.path }}/upload.txt" - object: delete.txt - overwrite: never - <<: *aws_connection_info - retries: 3 - delay: 3 - register: result - - - assert: - that: - - result is not changed - - - name: test put with overwrite=different and modified object - aws_s3: - bucket: "{{ bucket_name }}" - mode: put - src: "{{ tmpdir.path }}/upload.txt" - object: delete.txt - overwrite: different - <<: *aws_connection_info - retries: 3 - delay: 3 - register: result - - - assert: - that: - - result is changed - - - name: test put with overwrite=always - aws_s3: - bucket: "{{ bucket_name }}" - mode: put - src: "{{ tmpdir.path }}/upload.txt" - object: delete.txt - overwrite: always - <<: *aws_connection_info - retries: 3 - delay: 3 - register: result - - - assert: - that: - - result is changed - - - name: test get object - aws_s3: - bucket: "{{ bucket_name }}" - mode: get - dest: "{{ tmpdir.path }}/download.txt" - object: delete.txt - <<: *aws_connection_info - retries: 3 - delay: 3 - register: result - until: "result.msg == 'GET operation complete'" - - - name: stat the file so we can compare the checksums - stat: - path: "{{ tmpdir.path }}/download.txt" - get_checksum: yes - register: download_file - - - assert: - that: - - upload_file.stat.checksum == download_file.stat.checksum - - - name: test get with overwrite=different and identical files - aws_s3: - bucket: "{{ bucket_name }}" - mode: get - dest: "{{ tmpdir.path }}/download.txt" - object: delete.txt - overwrite: different - <<: *aws_connection_info - retries: 3 - delay: 3 - register: result - - - assert: - that: - - result is not changed - - - name: modify destination - copy: - dest: "{{ tmpdir.path }}/download.txt" - src: hello.txt - - - name: test get with overwrite=never - aws_s3: - bucket: "{{ bucket_name }}" - mode: get - dest: "{{ tmpdir.path }}/download.txt" - object: delete.txt - overwrite: never - <<: *aws_connection_info - retries: 3 - delay: 3 - register: result - - - assert: - that: - - result is not changed - - - name: test get with overwrite=different and modified file - aws_s3: - bucket: "{{ bucket_name }}" - mode: get - dest: "{{ tmpdir.path }}/download.txt" - object: delete.txt - overwrite: different - <<: *aws_connection_info - retries: 3 - delay: 3 - register: result - - - assert: - that: - - result is changed - - - name: test get with overwrite=always - aws_s3: - bucket: "{{ bucket_name }}" - mode: get - dest: "{{ tmpdir.path }}/download.txt" - object: delete.txt - overwrite: always - <<: *aws_connection_info - retries: 3 - delay: 3 - register: result - - - assert: - that: - - result is changed - - - name: test geturl of the object - aws_s3: - bucket: "{{ bucket_name }}" - mode: geturl - object: delete.txt - <<: *aws_connection_info - retries: 3 - delay: 3 - register: result - until: result is changed - - - assert: - that: - - "'Download url:' in result.msg" - - result is changed - - - name: test getstr of the object - aws_s3: - bucket: "{{ bucket_name }}" - mode: getstr - object: delete.txt - <<: *aws_connection_info - retries: 3 - delay: 3 - register: result - - - assert: - that: - - result.msg == "GET operation complete" - - result.contents == content - - - name: test list to get all objects in the bucket - aws_s3: - bucket: "{{ bucket_name }}" - mode: list - <<: *aws_connection_info - retries: 3 - delay: 3 - register: result - - - assert: - that: - - "'delete.txt' in result.s3_keys" - - result.msg == "LIST operation complete" - - - name: test delobj to just delete an object in the bucket - aws_s3: - bucket: "{{ bucket_name }}" - mode: delobj - object: delete.txt - <<: *aws_connection_info - retries: 3 - delay: 3 - register: result - - - assert: - that: - - "'Object deleted from bucket' in result.msg" - - result is changed - - - name: test putting an encrypted object in the bucket - aws_s3: - bucket: "{{ bucket_name }}" - mode: put - src: "{{ tmpdir.path }}/upload.txt" - encrypt: yes - object: delete_encrypt.txt - <<: *aws_connection_info - retries: 3 - delay: 3 - register: result - - - assert: - that: - - result is changed - - result.msg == "PUT operation complete" - - - name: test get encrypted object - aws_s3: - bucket: "{{ bucket_name }}" - mode: get - dest: "{{ tmpdir.path }}/download_encrypted.txt" - object: delete_encrypt.txt - <<: *aws_connection_info - retries: 3 - delay: 3 - register: result - until: "result.msg == 'GET operation complete'" - - - name: stat the file so we can compare the checksums - stat: - path: "{{ tmpdir.path }}/download_encrypted.txt" - get_checksum: yes - register: download_file - - - assert: - that: - - upload_file.stat.checksum == download_file.stat.checksum - - - name: delete encrypted file - aws_s3: - bucket: "{{ bucket_name }}" - mode: delobj - object: delete_encrypt.txt - <<: *aws_connection_info - retries: 3 - delay: 3 - - - name: test putting an aws:kms encrypted object in the bucket - aws_s3: - bucket: "{{ bucket_name }}" - mode: put - src: "{{ tmpdir.path }}/upload.txt" - encrypt: yes - encryption_mode: aws:kms - object: delete_encrypt_kms.txt - <<: *aws_connection_info - retries: 3 - delay: 3 - register: result - - - assert: - that: - - result is changed - - result.msg == "PUT operation complete" - - - name: test get KMS encrypted object - aws_s3: - bucket: "{{ bucket_name }}" - mode: get - dest: "{{ tmpdir.path }}/download_kms.txt" - object: delete_encrypt_kms.txt - <<: *aws_connection_info - retries: 3 - delay: 3 - register: result - until: "result.msg == 'GET operation complete'" - - - name: get the stat of the file so we can compare the checksums - stat: - path: "{{ tmpdir.path }}/download_kms.txt" - get_checksum: yes - register: download_file - - - assert: - that: - - upload_file.stat.checksum == download_file.stat.checksum - - # FIXME - could use a test that checks uploaded file is *actually* aws:kms encrypted - - - name: delete KMS encrypted file - aws_s3: - bucket: "{{ bucket_name }}" - mode: delobj - object: delete_encrypt_kms.txt - <<: *aws_connection_info - retries: 3 - delay: 3 - - # FIXME: could use a test that checks non standard KMS key - # but that would require ability to create and remove such keys. - # PRs exist for that, but propose deferring until after merge. - - - name: test creation of empty path - aws_s3: - bucket: "{{ bucket_name }}" - mode: create - object: foo/bar/baz/ - <<: *aws_connection_info - retries: 3 - delay: 3 - register: result - - - assert: - that: - - "'Virtual directory foo/bar/baz/ created' in result.msg" - - result is changed - - - name: test deletion of empty path - aws_s3: - bucket: "{{ bucket_name }}" - mode: delobj - object: foo/bar/baz/ - <<: *aws_connection_info - retries: 3 - delay: 3 - - - name: test delete bucket - aws_s3: - bucket: "{{ bucket_name }}" - mode: delete - <<: *aws_connection_info - register: result - retries: 3 - delay: 3 - until: result is changed - - - assert: - that: - - result is changed - - - name: test create a bucket with a dot in the name - aws_s3: - bucket: "{{ bucket_name + '.bucket' }}" - mode: create - <<: *aws_connection_info - register: result - - - assert: - that: - - result is changed - - - name: test delete a bucket with a dot in the name - aws_s3: - bucket: "{{ bucket_name + '.bucket' }}" - mode: delete - <<: *aws_connection_info - register: result - - - assert: - that: - - result is changed - - - name: test delete a nonexistent bucket - aws_s3: - bucket: "{{ bucket_name + '.bucket' }}" - mode: delete - <<: *aws_connection_info - register: result - - - assert: - that: - - result is not changed - - - name: make tempfile 4 GB for OSX - command: - _raw_params: "dd if=/dev/zero of={{ tmpdir.path }}/largefile bs=1m count=4096" - when: ansible_distribution == 'MacOSX' - - - name: make tempfile 4 GB for linux - command: - _raw_params: "dd if=/dev/zero of={{ tmpdir.path }}/largefile bs=1M count=4096" - when: ansible_system == 'Linux' - - - name: test multipart download - platform specific - block: - - name: make a bucket to upload the file - aws_s3: - bucket: "{{ bucket_name }}" - mode: create - <<: *aws_connection_info - - - name: upload the file to the bucket - aws_s3: - bucket: "{{ bucket_name }}" - mode: put - src: "{{ tmpdir.path }}/largefile" - object: multipart.txt - <<: *aws_connection_info - - - name: download file once - aws_s3: - bucket: "{{ bucket_name }}" - mode: get - dest: "{{ tmpdir.path }}/download.txt" - object: multipart.txt - overwrite: different - <<: *aws_connection_info - retries: 3 - delay: 3 - until: "result.msg == 'GET operation complete'" - register: result - - - assert: - that: - - result is changed - - - name: download file again - aws_s3: - bucket: "{{ bucket_name }}" - mode: get - dest: "{{ tmpdir.path }}/download.txt" - object: multipart.txt - overwrite: different - <<: *aws_connection_info - register: result - - - assert: - that: - - result is not changed - when: ansible_system == 'Linux' or ansible_distribution == 'MacOSX' - - always: - - name: remove uploaded files - aws_s3: - bucket: "{{ bucket_name }}" - mode: delobj - object: "{{ item }}" - <<: *aws_connection_info - loop: - - hello.txt - - delete.txt - - delete_encrypt.txt - - delete_encrypt_kms.txt - ignore_errors: yes - - - name: delete temporary files - file: - state: absent - path: "{{ tmpdir.path }}" - ignore_errors: yes - - - name: delete the bucket - aws_s3: - bucket: "{{ bucket_name }}" - mode: delete - <<: *aws_connection_info - ignore_errors: yes diff --git a/test/integration/targets/cloudformation/aliases b/test/integration/targets/cloudformation/aliases deleted file mode 100644 index 55555be789..0000000000 --- a/test/integration/targets/cloudformation/aliases +++ /dev/null @@ -1,3 +0,0 @@ -cloud/aws -shippable/aws/group2 -cloudformation_info diff --git a/test/integration/targets/cloudformation/defaults/main.yml b/test/integration/targets/cloudformation/defaults/main.yml deleted file mode 100644 index aaf0ca7e61..0000000000 --- a/test/integration/targets/cloudformation/defaults/main.yml +++ /dev/null @@ -1,8 +0,0 @@ -stack_name: "{{ resource_prefix }}" - -vpc_name: '{{ resource_prefix }}-vpc' -vpc_seed: '{{ resource_prefix }}' -vpc_cidr: '10.{{ 256 | random(seed=vpc_seed) }}.0.0/16' -subnet_cidr: '10.{{ 256 | random(seed=vpc_seed) }}.32.0/24' - -ec2_ami_name: 'amzn2-ami-hvm-2.*-x86_64-gp2' diff --git a/test/integration/targets/cloudformation/files/cf_template.json b/test/integration/targets/cloudformation/files/cf_template.json deleted file mode 100644 index ff4c5693b0..0000000000 --- a/test/integration/targets/cloudformation/files/cf_template.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "AWSTemplateFormatVersion" : "2010-09-09", - - "Description" : "Create an Amazon EC2 instance.", - - "Parameters" : { - "InstanceType" : { - "Description" : "EC2 instance type", - "Type" : "String", - "Default" : "t3.nano", - "AllowedValues" : [ "t3.micro", "t3.nano"] - }, - "ImageId" : { - "Type" : "String" - }, - "SubnetId" : { - "Type" : "String" - } - }, - - "Resources" : { - "EC2Instance" : { - "Type" : "AWS::EC2::Instance", - "Properties" : { - "InstanceType" : { "Ref" : "InstanceType" }, - "ImageId" : { "Ref" : "ImageId" }, - "SubnetId": { "Ref" : "SubnetId" } - } - } - }, - - "Outputs" : { - "InstanceId" : { - "Value" : { "Ref" : "EC2Instance" } - } - } -} diff --git a/test/integration/targets/cloudformation/tasks/main.yml b/test/integration/targets/cloudformation/tasks/main.yml deleted file mode 100644 index 9b89722b20..0000000000 --- a/test/integration/targets/cloudformation/tasks/main.yml +++ /dev/null @@ -1,463 +0,0 @@ ---- - -- module_defaults: - group/aws: - aws_access_key: '{{ aws_access_key | default(omit) }}' - aws_secret_key: '{{ aws_secret_key | default(omit) }}' - security_token: '{{ security_token | default(omit) }}' - region: '{{ aws_region | default(omit) }}' - - block: - - # ==== Env setup ========================================================== - - name: list available AZs - aws_az_info: - register: region_azs - - - name: pick an AZ for testing - set_fact: - availability_zone: "{{ region_azs.availability_zones[0].zone_name }}" - - - name: Create a test VPC - ec2_vpc_net: - name: "{{ vpc_name }}" - cidr_block: "{{ vpc_cidr }}" - tags: - Name: Cloudformation testing - register: testing_vpc - - - name: Create a test subnet - ec2_vpc_subnet: - vpc_id: "{{ testing_vpc.vpc.id }}" - cidr: "{{ subnet_cidr }}" - az: "{{ availability_zone }}" - register: testing_subnet - - - name: Find AMI to use - ec2_ami_info: - owners: 'amazon' - filters: - name: '{{ ec2_ami_name }}' - register: ec2_amis - - - name: Set fact with latest AMI - vars: - latest_ami: '{{ ec2_amis.images | sort(attribute="creation_date") | last }}' - set_fact: - ec2_ami_image: '{{ latest_ami.image_id }}' - - # ==== Cloudformation tests =============================================== - - # 1. Basic stack creation (check mode, actual run and idempotency) - # 2. Tags - # 3. cloudformation_info tests (basic + all_facts) - # 4. termination_protection - # 5. create_changeset + changeset_name - - # There is still scope to add tests for - - # 1. capabilities - # 2. stack_policy - # 3. on_create_failure (covered in unit tests) - # 4. Passing in a role - # 5. nested stacks? - - - - name: create a cloudformation stack (check mode) - cloudformation: - stack_name: "{{ stack_name }}" - template_body: "{{ lookup('file','cf_template.json') }}" - template_parameters: - InstanceType: "t3.nano" - ImageId: "{{ ec2_ami_image }}" - SubnetId: "{{ testing_subnet.subnet.id }}" - tags: - Stack: "{{ stack_name }}" - test: "{{ resource_prefix }}" - register: cf_stack - check_mode: yes - - - name: check task return attributes - assert: - that: - - cf_stack.changed - - "'msg' in cf_stack and 'New stack would be created' in cf_stack.msg" - - - name: create a cloudformation stack - cloudformation: - stack_name: "{{ stack_name }}" - template_body: "{{ lookup('file','cf_template.json') }}" - template_parameters: - InstanceType: "t3.nano" - ImageId: "{{ ec2_ami_image }}" - SubnetId: "{{ testing_subnet.subnet.id }}" - tags: - Stack: "{{ stack_name }}" - test: "{{ resource_prefix }}" - register: cf_stack - - - name: check task return attributes - assert: - that: - - cf_stack.changed - - "'events' in cf_stack" - - "'output' in cf_stack and 'Stack CREATE complete' in cf_stack.output" - - "'stack_outputs' in cf_stack and 'InstanceId' in cf_stack.stack_outputs" - - "'stack_resources' in cf_stack" - - - name: create a cloudformation stack (check mode) (idempotent) - cloudformation: - stack_name: "{{ stack_name }}" - template_body: "{{ lookup('file','cf_template.json') }}" - template_parameters: - InstanceType: "t3.nano" - ImageId: "{{ ec2_ami_image }}" - SubnetId: "{{ testing_subnet.subnet.id }}" - tags: - Stack: "{{ stack_name }}" - test: "{{ resource_prefix }}" - register: cf_stack - check_mode: yes - - - name: check task return attributes - assert: - that: - - not cf_stack.changed - - - name: create a cloudformation stack (idempotent) - cloudformation: - stack_name: "{{ stack_name }}" - template_body: "{{ lookup('file','cf_template.json') }}" - template_parameters: - InstanceType: "t3.nano" - ImageId: "{{ ec2_ami_image }}" - SubnetId: "{{ testing_subnet.subnet.id }}" - tags: - Stack: "{{ stack_name }}" - test: "{{ resource_prefix }}" - register: cf_stack - - - name: check task return attributes - assert: - that: - - not cf_stack.changed - - "'output' in cf_stack and 'Stack is already up-to-date.' in cf_stack.output" - - "'stack_outputs' in cf_stack and 'InstanceId' in cf_stack.stack_outputs" - - "'stack_resources' in cf_stack" - - - name: get stack details - cloudformation_info: - stack_name: "{{ stack_name }}" - register: stack_info - - - name: assert stack info - assert: - that: - - "'cloudformation' in stack_info" - - "stack_info.cloudformation | length == 1" - - "stack_name in stack_info.cloudformation" - - "'stack_description' in stack_info.cloudformation[stack_name]" - - "'stack_outputs' in stack_info.cloudformation[stack_name]" - - "'stack_parameters' in stack_info.cloudformation[stack_name]" - - "'stack_tags' in stack_info.cloudformation[stack_name]" - - "stack_info.cloudformation[stack_name].stack_tags.Stack == stack_name" - - - name: get stack details (checkmode) - cloudformation_info: - stack_name: "{{ stack_name }}" - register: stack_info - check_mode: yes - - - name: assert stack info - assert: - that: - - "'cloudformation' in stack_info" - - "stack_info.cloudformation | length == 1" - - "stack_name in stack_info.cloudformation" - - "'stack_description' in stack_info.cloudformation[stack_name]" - - "'stack_outputs' in stack_info.cloudformation[stack_name]" - - "'stack_parameters' in stack_info.cloudformation[stack_name]" - - "'stack_tags' in stack_info.cloudformation[stack_name]" - - "stack_info.cloudformation[stack_name].stack_tags.Stack == stack_name" - - - name: get stack details (all_facts) - cloudformation_info: - stack_name: "{{ stack_name }}" - all_facts: yes - register: stack_info - - - name: assert stack info - assert: - that: - - "'stack_events' in stack_info.cloudformation[stack_name]" - - "'stack_policy' in stack_info.cloudformation[stack_name]" - - "'stack_resource_list' in stack_info.cloudformation[stack_name]" - - "'stack_resources' in stack_info.cloudformation[stack_name]" - - "'stack_template' in stack_info.cloudformation[stack_name]" - - - name: get stack details (all_facts) (checkmode) - cloudformation_info: - stack_name: "{{ stack_name }}" - all_facts: yes - register: stack_info - check_mode: yes - - - name: assert stack info - assert: - that: - - "'stack_events' in stack_info.cloudformation[stack_name]" - - "'stack_policy' in stack_info.cloudformation[stack_name]" - - "'stack_resource_list' in stack_info.cloudformation[stack_name]" - - "'stack_resources' in stack_info.cloudformation[stack_name]" - - "'stack_template' in stack_info.cloudformation[stack_name]" - - # ==== Cloudformation tests (create changeset) ============================ - - # try to create a changeset by changing instance type - - name: create a changeset - cloudformation: - stack_name: "{{ stack_name }}" - create_changeset: yes - changeset_name: "test-changeset" - template_body: "{{ lookup('file','cf_template.json') }}" - template_parameters: - InstanceType: "t3.micro" - ImageId: "{{ ec2_ami_image }}" - SubnetId: "{{ testing_subnet.subnet.id }}" - tags: - Stack: "{{ stack_name }}" - test: "{{ resource_prefix }}" - register: create_changeset_result - - - name: assert changeset created - assert: - that: - - "create_changeset_result.changed" - - "'change_set_id' in create_changeset_result" - - "'Stack CREATE_CHANGESET complete' in create_changeset_result.output" - - - name: get stack details with changesets - cloudformation_info: - stack_name: "{{ stack_name }}" - stack_change_sets: True - register: stack_info - - - name: assert changesets in info - assert: - that: - - "'stack_change_sets' in stack_info.cloudformation[stack_name]" - - - name: get stack details with changesets (checkmode) - cloudformation_info: - stack_name: "{{ stack_name }}" - stack_change_sets: True - register: stack_info - check_mode: yes - - - name: assert changesets in info - assert: - that: - - "'stack_change_sets' in stack_info.cloudformation[stack_name]" - - # try to create an empty changeset by passing in unchanged template - - name: create a changeset - cloudformation: - stack_name: "{{ stack_name }}" - create_changeset: yes - template_body: "{{ lookup('file','cf_template.json') }}" - template_parameters: - InstanceType: "t3.nano" - ImageId: "{{ ec2_ami_image }}" - SubnetId: "{{ testing_subnet.subnet.id }}" - tags: - Stack: "{{ stack_name }}" - test: "{{ resource_prefix }}" - register: create_changeset_result - - - name: assert changeset created - assert: - that: - - "not create_changeset_result.changed" - - "'The created Change Set did not contain any changes to this stack and was deleted.' in create_changeset_result.output" - - # ==== Cloudformation tests (termination_protection) ====================== - - - name: set termination protection to true - cloudformation: - stack_name: "{{ stack_name }}" - termination_protection: yes - template_body: "{{ lookup('file','cf_template.json') }}" - template_parameters: - InstanceType: "t3.nano" - ImageId: "{{ ec2_ami_image }}" - SubnetId: "{{ testing_subnet.subnet.id }}" - tags: - Stack: "{{ stack_name }}" - test: "{{ resource_prefix }}" - register: cf_stack - -# This fails - #65592 -# - name: check task return attributes -# assert: -# that: -# - cf_stack.changed - - - name: get stack details - cloudformation_info: - stack_name: "{{ stack_name }}" - register: stack_info - - - name: assert stack info - assert: - that: - - "stack_info.cloudformation[stack_name].stack_description.enable_termination_protection" - - - name: get stack details (checkmode) - cloudformation_info: - stack_name: "{{ stack_name }}" - register: stack_info - check_mode: yes - - - name: assert stack info - assert: - that: - - "stack_info.cloudformation[stack_name].stack_description.enable_termination_protection" - - - name: set termination protection to false - cloudformation: - stack_name: "{{ stack_name }}" - termination_protection: no - template_body: "{{ lookup('file','cf_template.json') }}" - template_parameters: - InstanceType: "t3.nano" - ImageId: "{{ ec2_ami_image }}" - SubnetId: "{{ testing_subnet.subnet.id }}" - tags: - Stack: "{{ stack_name }}" - test: "{{ resource_prefix }}" - register: cf_stack - -# This fails - #65592 -# - name: check task return attributes -# assert: -# that: -# - cf_stack.changed - - - name: get stack details - cloudformation_info: - stack_name: "{{ stack_name }}" - register: stack_info - - - name: assert stack info - assert: - that: - - "not stack_info.cloudformation[stack_name].stack_description.enable_termination_protection" - - - name: get stack details (checkmode) - cloudformation_info: - stack_name: "{{ stack_name }}" - register: stack_info - check_mode: yes - - - name: assert stack info - assert: - that: - - "not stack_info.cloudformation[stack_name].stack_description.enable_termination_protection" - - # ==== Cloudformation tests (delete stack tests) ========================== - - - name: delete cloudformation stack (check mode) - cloudformation: - stack_name: "{{ stack_name }}" - state: absent - check_mode: yes - register: cf_stack - - - name: check task return attributes - assert: - that: - - cf_stack.changed - - "'msg' in cf_stack and 'Stack would be deleted' in cf_stack.msg" - - - name: delete cloudformation stack - cloudformation: - stack_name: "{{ stack_name }}" - state: absent - register: cf_stack - - - name: check task return attributes - assert: - that: - - cf_stack.changed - - "'output' in cf_stack and 'Stack Deleted' in cf_stack.output" - - - name: delete cloudformation stack (check mode) (idempotent) - cloudformation: - stack_name: "{{ stack_name }}" - state: absent - check_mode: yes - register: cf_stack - - - name: check task return attributes - assert: - that: - - not cf_stack.changed - - "'msg' in cf_stack" - - >- - "Stack doesn't exist" in cf_stack.msg - - - name: delete cloudformation stack (idempotent) - cloudformation: - stack_name: "{{ stack_name }}" - state: absent - register: cf_stack - - - name: check task return attributes - assert: - that: - - not cf_stack.changed - - "'output' in cf_stack and 'Stack not found.' in cf_stack.output" - - - name: get stack details - cloudformation_info: - stack_name: "{{ stack_name }}" - register: stack_info - - - name: assert stack info - assert: - that: - - "not stack_info.cloudformation" - - - name: get stack details (checkmode) - cloudformation_info: - stack_name: "{{ stack_name }}" - register: stack_info - check_mode: yes - - - name: assert stack info - assert: - that: - - "not stack_info.cloudformation" - - # ==== Cleanup ============================================================ - - always: - - - name: delete stack - cloudformation: - stack_name: "{{ stack_name }}" - state: absent - ignore_errors: yes - - - name: Delete test subnet - ec2_vpc_subnet: - vpc_id: "{{ testing_vpc.vpc.id }}" - cidr: "{{ subnet_cidr }}" - state: absent - ignore_errors: yes - - - name: Delete test VPC - ec2_vpc_net: - name: "{{ vpc_name }}" - cidr_block: "{{ vpc_cidr }}" - state: absent - ignore_errors: yes diff --git a/test/integration/targets/ec2_ami/aliases b/test/integration/targets/ec2_ami/aliases deleted file mode 100644 index 0e61c5bb7b..0000000000 --- a/test/integration/targets/ec2_ami/aliases +++ /dev/null @@ -1,4 +0,0 @@ -cloud/aws -shippable/aws/group2 -unstable -ec2_ami_info diff --git a/test/integration/targets/ec2_ami/defaults/main.yml b/test/integration/targets/ec2_ami/defaults/main.yml deleted file mode 100644 index 86665321a2..0000000000 --- a/test/integration/targets/ec2_ami/defaults/main.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -# defaults file for test_ec2_ami -ec2_ami_name: '{{resource_prefix}}' -ec2_ami_description: 'Created by ansible integration tests' -# image for Amazon Linux AMI 2017.03.1 (HVM), SSD Volume Type -ec2_ami_image: - us-east-1: ami-4fffc834 - us-east-2: ami-ea87a78f diff --git a/test/integration/targets/ec2_ami/meta/main.yml b/test/integration/targets/ec2_ami/meta/main.yml deleted file mode 100644 index 1f64f1169a..0000000000 --- a/test/integration/targets/ec2_ami/meta/main.yml +++ /dev/null @@ -1,3 +0,0 @@ -dependencies: - - prepare_tests - - setup_ec2 diff --git a/test/integration/targets/ec2_ami/tasks/main.yml b/test/integration/targets/ec2_ami/tasks/main.yml deleted file mode 100644 index 2cb15b5da1..0000000000 --- a/test/integration/targets/ec2_ami/tasks/main.yml +++ /dev/null @@ -1,462 +0,0 @@ ---- -# tasks file for test_ec2_ami - -- block: - - # ============================================================ - - # SETUP: vpc, ec2 key pair, subnet, security group, ec2 instance, snapshot - - name: set aws_connection_info fact - set_fact: - aws_connection_info: &aws_connection_info - aws_region: '{{aws_region}}' - aws_access_key: '{{aws_access_key}}' - aws_secret_key: '{{aws_secret_key}}' - security_token: '{{security_token}}' - no_log: yes - - - name: create a VPC to work in - ec2_vpc_net: - cidr_block: 10.0.0.0/24 - state: present - name: '{{ ec2_ami_name }}_setup' - resource_tags: - Name: '{{ ec2_ami_name }}_setup' - <<: *aws_connection_info - register: setup_vpc - - - name: create a key pair to use for creating an ec2 instance - ec2_key: - name: '{{ ec2_ami_name }}_setup' - state: present - <<: *aws_connection_info - register: setup_key - - - name: create a subnet to use for creating an ec2 instance - ec2_vpc_subnet: - az: '{{ ec2_region }}a' - tags: '{{ ec2_ami_name }}_setup' - vpc_id: '{{ setup_vpc.vpc.id }}' - cidr: 10.0.0.0/24 - state: present - resource_tags: - Name: '{{ ec2_ami_name }}_setup' - <<: *aws_connection_info - register: setup_subnet - - - name: create a security group to use for creating an ec2 instance - ec2_group: - name: '{{ ec2_ami_name }}_setup' - description: 'created by Ansible integration tests' - state: present - vpc_id: '{{ setup_vpc.vpc.id }}' - <<: *aws_connection_info - register: setup_sg - - - name: provision ec2 instance to create an image - ec2: - key_name: '{{ setup_key.key.name }}' - instance_type: t2.micro - state: present - image: '{{ ec2_region_images[ec2_region] }}' - wait: yes - instance_tags: - '{{ec2_ami_name}}_instance_setup': 'integration_tests' - group_id: '{{ setup_sg.group_id }}' - vpc_subnet_id: '{{ setup_subnet.subnet.id }}' - <<: *aws_connection_info - register: setup_instance - - - name: take a snapshot of the instance to create an image - ec2_snapshot: - instance_id: '{{ setup_instance.instance_ids[0] }}' - device_name: /dev/xvda - state: present - <<: *aws_connection_info - register: setup_snapshot - - # ============================================================ - - - name: test clean failure if not providing image_id or name with state=present - ec2_ami: - instance_id: '{{ setup_instance.instance_ids[0] }}' - state: present - description: '{{ ec2_ami_description }}' - tags: - Name: '{{ ec2_ami_name }}_ami' - wait: yes - root_device_name: /dev/xvda - <<: *aws_connection_info - register: result - ignore_errors: yes - - - name: assert error message is helpful - assert: - that: - - result.failed - - "result.msg == 'one of the following is required: name, image_id'" - - # ============================================================ - - - name: create an image from the instance - ec2_ami: - instance_id: '{{ setup_instance.instance_ids[0] }}' - state: present - name: '{{ ec2_ami_name }}_ami' - description: '{{ ec2_ami_description }}' - tags: - Name: '{{ ec2_ami_name }}_ami' - wait: yes - root_device_name: /dev/xvda - <<: *aws_connection_info - register: result - - - name: set image id fact for deletion later - set_fact: - ec2_ami_image_id: "{{ result.image_id }}" - - - name: assert that image has been created - assert: - that: - - "result.changed" - - "result.image_id.startswith('ami-')" - - "'Name' in result.tags and result.tags.Name == ec2_ami_name + '_ami'" - - # ============================================================ - - - name: gather facts about the image created - ec2_ami_info: - image_ids: '{{ ec2_ami_image_id }}' - <<: *aws_connection_info - register: ami_facts_result - ignore_errors: true - - - name: assert that the right image was found - assert: - that: - - "ami_facts_result.images[0].image_id == ec2_ami_image_id" - - # ============================================================ - - - name: delete the image - ec2_ami: - instance_id: '{{ setup_instance.instance_ids[0] }}' - state: absent - delete_snapshot: yes - name: '{{ ec2_ami_name }}_ami' - description: '{{ ec2_ami_description }}' - image_id: '{{ result.image_id }}' - tags: - Name: '{{ ec2_ami_name }}_ami' - wait: yes - <<: *aws_connection_info - ignore_errors: true - register: result - - - name: assert that the image has been deleted - assert: - that: - - "result.changed" - - "'image_id' not in result" - - "result.snapshots_deleted" - - # ============================================================ - - - name: test removing an ami if no image ID is provided (expected failed=true) - ec2_ami: - state: absent - <<: *aws_connection_info - register: result - ignore_errors: yes - - - name: assert that an image ID is required - assert: - that: - - "result.failed" - - "result.msg == 'state is absent but all of the following are missing: image_id'" - - # ============================================================ - - - name: create an image from the snapshot - ec2_ami: - name: '{{ ec2_ami_name }}_ami' - description: '{{ ec2_ami_description }}' - state: present - launch_permissions: - user_ids: [] - tags: - Name: '{{ ec2_ami_name }}_ami' - root_device_name: /dev/xvda - device_mapping: - - device_name: /dev/xvda - volume_type: gp2 - size: 8 - delete_on_termination: true - snapshot_id: '{{ setup_snapshot.snapshot_id }}' - <<: *aws_connection_info - register: result - ignore_errors: true - - - name: set image id fact for deletion later - set_fact: - ec2_ami_image_id: "{{ result.image_id }}" - ec2_ami_snapshot: "{{ result.block_device_mapping['/dev/xvda'].snapshot_id }}" - - - name: assert a new ami has been created - assert: - that: - - "result.changed" - - "result.image_id.startswith('ami-')" - - # ============================================================ - - - name: test default launch permissions idempotence - ec2_ami: - description: '{{ ec2_ami_description }}' - state: present - name: '{{ ec2_ami_name }}_ami' - tags: - Name: '{{ ec2_ami_name }}_ami' - root_device_name: /dev/xvda - image_id: '{{ result.image_id }}' - launch_permissions: - user_ids: [] - device_mapping: - - device_name: /dev/xvda - volume_type: gp2 - size: 8 - delete_on_termination: true - snapshot_id: '{{ setup_snapshot.snapshot_id }}' - <<: *aws_connection_info - register: result - - - name: assert a new ami has not been created - assert: - that: - - "not result.changed" - - "result.image_id.startswith('ami-')" - - # ============================================================ - - - name: add a tag to the AMI - ec2_ami: - state: present - description: '{{ ec2_ami_description }}' - image_id: '{{ result.image_id }}' - name: '{{ ec2_ami_name }}_ami' - tags: - New: Tag - <<: *aws_connection_info - register: result - - - name: assert a tag was added - assert: - that: - - "'Name' in result.tags and result.tags.Name == ec2_ami_name + '_ami'" - - "'New' in result.tags and result.tags.New == 'Tag'" - - - name: use purge_tags to remove a tag from the AMI - ec2_ami: - state: present - description: '{{ ec2_ami_description }}' - image_id: '{{ result.image_id }}' - name: '{{ ec2_ami_name }}_ami' - tags: - New: Tag - purge_tags: yes - <<: *aws_connection_info - register: result - - - name: assert a tag was removed - assert: - that: - - "'Name' not in result.tags" - - "'New' in result.tags and result.tags.New == 'Tag'" - - # ============================================================ - - - name: update AMI launch permissions - ec2_ami: - state: present - image_id: '{{ result.image_id }}' - description: '{{ ec2_ami_description }}' - tags: - Name: '{{ ec2_ami_name }}_ami' - launch_permissions: - group_names: ['all'] - <<: *aws_connection_info - register: result - - - name: assert launch permissions were updated - assert: - that: - - "result.changed" - - # ============================================================ - - - name: modify the AMI description - ec2_ami: - state: present - image_id: '{{ result.image_id }}' - name: '{{ ec2_ami_name }}_ami' - description: '{{ ec2_ami_description }}CHANGED' - tags: - Name: '{{ ec2_ami_name }}_ami' - launch_permissions: - group_names: ['all'] - <<: *aws_connection_info - register: result - - - name: assert the description changed - assert: - that: - - "result.changed" - - # ============================================================ - - - name: remove public launch permissions - ec2_ami: - state: present - image_id: '{{ result.image_id }}' - name: '{{ ec2_ami_name }}_ami' - tags: - Name: '{{ ec2_ami_name }}_ami' - launch_permissions: - group_names: [] - <<: *aws_connection_info - register: result - - - name: assert launch permissions were updated - assert: - that: - - "result.changed" - - # ============================================================ - - - name: delete ami without deleting the snapshot (default is not to delete) - ec2_ami: - instance_id: '{{ setup_instance.instance_ids[0] }}' - state: absent - name: '{{ ec2_ami_name }}_ami' - image_id: '{{ ec2_ami_image_id }}' - tags: - Name: '{{ ec2_ami_name }}_ami' - wait: yes - <<: *aws_connection_info - ignore_errors: true - register: result - - - name: assert that the image has been deleted - assert: - that: - - "result.changed" - - "'image_id' not in result" - - - name: ensure the snapshot still exists - ec2_snapshot_info: - snapshot_ids: - - '{{ ec2_ami_snapshot }}' - <<: *aws_connection_info - register: snapshot_result - - - name: assert the snapshot wasn't deleted - assert: - that: - - "snapshot_result.snapshots[0].snapshot_id == ec2_ami_snapshot" - - - name: delete ami for a second time - ec2_ami: - instance_id: '{{ setup_instance.instance_ids[0] }}' - state: absent - name: '{{ ec2_ami_name }}_ami' - image_id: '{{ ec2_ami_image_id }}' - tags: - Name: '{{ ec2_ami_name }}_ami' - wait: yes - <<: *aws_connection_info - register: result - - - name: assert that image does not exist - assert: - that: - - not result.changed - - not result.failed - - - # ============================================================ - - always: - - # ============================================================ - - # TEAR DOWN: snapshot, ec2 instance, ec2 key pair, security group, vpc - - name: Announce teardown start - debug: - msg: "***** TESTING COMPLETE. COMMENCE TEARDOWN *****" - - - name: delete ami - ec2_ami: - state: absent - image_id: "{{ ec2_ami_image_id }}" - name: '{{ ec2_ami_name }}_ami' - wait: yes - <<: *aws_connection_info - ignore_errors: yes - - - name: remove setup snapshot of ec2 instance - ec2_snapshot: - state: absent - snapshot_id: '{{ setup_snapshot.snapshot_id }}' - <<: *aws_connection_info - ignore_errors: yes - - - name: remove setup ec2 instance - ec2: - instance_type: t2.micro - instance_ids: '{{ setup_instance.instance_ids }}' - state: absent - wait: yes - instance_tags: - '{{ec2_ami_name}}_instance_setup': 'integration_tests' - group_id: '{{ setup_sg.group_id }}' - vpc_subnet_id: '{{ setup_subnet.subnet.id }}' - <<: *aws_connection_info - ignore_errors: yes - - - name: remove setup keypair - ec2_key: - name: '{{ec2_ami_name}}_setup' - state: absent - <<: *aws_connection_info - ignore_errors: yes - - - name: remove setup security group - ec2_group: - name: '{{ ec2_ami_name }}_setup' - description: 'created by Ansible integration tests' - state: absent - vpc_id: '{{ setup_vpc.vpc.id }}' - <<: *aws_connection_info - ignore_errors: yes - - - name: remove setup subnet - ec2_vpc_subnet: - az: '{{ ec2_region }}a' - tags: '{{ec2_ami_name}}_setup' - vpc_id: '{{ setup_vpc.vpc.id }}' - cidr: 10.0.0.0/24 - state: absent - resource_tags: - Name: '{{ ec2_ami_name }}_setup' - <<: *aws_connection_info - ignore_errors: yes - - - name: remove setup VPC - ec2_vpc_net: - cidr_block: 10.0.0.0/24 - state: absent - name: '{{ ec2_ami_name }}_setup' - resource_tags: - Name: '{{ ec2_ami_name }}_setup' - <<: *aws_connection_info - ignore_errors: yes diff --git a/test/integration/targets/ec2_ami/vars/main.yml b/test/integration/targets/ec2_ami/vars/main.yml deleted file mode 100644 index dac1fda2e9..0000000000 --- a/test/integration/targets/ec2_ami/vars/main.yml +++ /dev/null @@ -1,20 +0,0 @@ ---- -# vars file for test_ec2_ami - -# based on Amazon Linux AMI 2017.09.0 (HVM), SSD Volume Type -ec2_region_images: - us-east-1: ami-8c1be5f6 - us-east-2: ami-c5062ba0 - us-west-1: ami-02eada62 - us-west-2: ami-e689729e - ca-central-1: ami-fd55ec99 - eu-west-1: ami-acd005d5 - eu-central-1: ami-c7ee5ca8 - eu-west-2: ami-1a7f6d7e - ap-southeast-1: ami-0797ea64 - ap-southeast-2: ami-8536d6e7 - ap-northeast-2: ami-9bec36f5 - ap-northeast-1: ami-2a69be4c - ap-south-1: ami-4fc58420 - sa-east-1: ami-f1344b9d - cn-north-1: ami-fba67596 diff --git a/test/integration/targets/ec2_elb_lb/aliases b/test/integration/targets/ec2_elb_lb/aliases deleted file mode 100644 index 6e3860bee2..0000000000 --- a/test/integration/targets/ec2_elb_lb/aliases +++ /dev/null @@ -1,2 +0,0 @@ -cloud/aws -shippable/aws/group2 diff --git a/test/integration/targets/ec2_elb_lb/defaults/main.yml b/test/integration/targets/ec2_elb_lb/defaults/main.yml deleted file mode 100644 index 76164523d4..0000000000 --- a/test/integration/targets/ec2_elb_lb/defaults/main.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -# defaults file for test_ec2_eip -tag_prefix: '{{resource_prefix}}' diff --git a/test/integration/targets/ec2_elb_lb/meta/main.yml b/test/integration/targets/ec2_elb_lb/meta/main.yml deleted file mode 100644 index 1f64f1169a..0000000000 --- a/test/integration/targets/ec2_elb_lb/meta/main.yml +++ /dev/null @@ -1,3 +0,0 @@ -dependencies: - - prepare_tests - - setup_ec2 diff --git a/test/integration/targets/ec2_elb_lb/tasks/main.yml b/test/integration/targets/ec2_elb_lb/tasks/main.yml deleted file mode 100644 index 4f25493023..0000000000 --- a/test/integration/targets/ec2_elb_lb/tasks/main.yml +++ /dev/null @@ -1,425 +0,0 @@ ---- -# __Test Info__ -# Create a self signed cert and upload it to AWS -# http://www.akadia.com/services/ssh_test_certificate.html -# http://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/ssl-server-cert.html - -# __Test Outline__ -# -# __ec2_elb_lb__ -# create test elb with listeners and certificate -# change AZ's -# change listeners -# remove listeners -# remove elb - -# __ec2-common__ -# test environment variable EC2_REGION -# test with no parameters -# test with only instance_id -# test invalid region parameter -# test valid region parameter -# test invalid ec2_url parameter -# test valid ec2_url parameter -# test credentials from environment -# test credential parameters - -- block: - - # ============================================================ - # create test elb with listeners, certificate, and health check - - - name: Create ELB - ec2_elb_lb: - name: "{{ tag_prefix }}" - region: "{{ ec2_region }}" - ec2_access_key: "{{ ec2_access_key }}" - ec2_secret_key: "{{ ec2_secret_key }}" - security_token: "{{ security_token }}" - state: present - zones: - - "{{ ec2_region }}a" - - "{{ ec2_region }}b" - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - - protocol: http - load_balancer_port: 8080 - instance_port: 8080 - health_check: - ping_protocol: http - ping_port: 80 - ping_path: "/index.html" - response_timeout: 5 - interval: 30 - unhealthy_threshold: 2 - healthy_threshold: 10 - register: info - - - assert: - that: - - 'info.changed' - - 'info.elb.status == "created"' - - '"{{ ec2_region }}a" in info.elb.zones' - - '"{{ ec2_region }}b" in info.elb.zones' - - 'info.elb.health_check.healthy_threshold == 10' - - 'info.elb.health_check.interval == 30' - - 'info.elb.health_check.target == "HTTP:80/index.html"' - - 'info.elb.health_check.timeout == 5' - - 'info.elb.health_check.unhealthy_threshold == 2' - - '[80, 80, "HTTP", "HTTP"] in info.elb.listeners' - - '[8080, 8080, "HTTP", "HTTP"] in info.elb.listeners' - - # ============================================================ - - # check ports, would be cool, but we are at the mercy of AWS - # to start things in a timely manner - - #- name: check to make sure 80 is listening - # wait_for: host={{ info.elb.dns_name }} port=80 timeout=600 - # register: result - - #- name: assert can connect to port# - # assert: 'result.state == "started"' - - #- name: check to make sure 443 is listening - # wait_for: host={{ info.elb.dns_name }} port=443 timeout=600 - # register: result - - #- name: assert can connect to port# - # assert: 'result.state == "started"' - - # ============================================================ - - # Change AZ's - - - name: Change AZ's - ec2_elb_lb: - name: "{{ tag_prefix }}" - region: "{{ ec2_region }}" - ec2_access_key: "{{ ec2_access_key }}" - ec2_secret_key: "{{ ec2_secret_key }}" - security_token: "{{ security_token }}" - state: present - zones: - - "{{ ec2_region }}c" - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - purge_zones: yes - health_check: - ping_protocol: http - ping_port: 80 - ping_path: "/index.html" - response_timeout: 5 - interval: 30 - unhealthy_threshold: 2 - healthy_threshold: 10 - register: info - - - - - assert: - that: - - 'info.elb.status == "ok"' - - 'info.changed' - - 'info.elb.zones[0] == "{{ ec2_region }}c"' - - # ============================================================ - - # Update AZ's - - - name: Update AZ's - ec2_elb_lb: - name: "{{ tag_prefix }}" - region: "{{ ec2_region }}" - ec2_access_key: "{{ ec2_access_key }}" - ec2_secret_key: "{{ ec2_secret_key }}" - security_token: "{{ security_token }}" - state: present - zones: - - "{{ ec2_region }}a" - - "{{ ec2_region }}b" - - "{{ ec2_region }}c" - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - purge_zones: yes - register: info - - - assert: - that: - - 'info.changed' - - 'info.elb.status == "ok"' - - '"{{ ec2_region }}a" in info.elb.zones' - - '"{{ ec2_region }}b" in info.elb.zones' - - '"{{ ec2_region }}c" in info.elb.zones' - - - # ============================================================ - - # Purge Listeners - - - name: Purge Listeners - ec2_elb_lb: - name: "{{ tag_prefix }}" - region: "{{ ec2_region }}" - ec2_access_key: "{{ ec2_access_key }}" - ec2_secret_key: "{{ ec2_secret_key }}" - security_token: "{{ security_token }}" - state: present - zones: - - "{{ ec2_region }}a" - - "{{ ec2_region }}b" - - "{{ ec2_region }}c" - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 81 - purge_listeners: yes - register: info - - - assert: - that: - - 'info.elb.status == "ok"' - - 'info.changed' - - '[80, 81, "HTTP", "HTTP"] in info.elb.listeners' - - 'info.elb.listeners|length == 1' - - - - # ============================================================ - - # add Listeners - - - name: Add Listeners - ec2_elb_lb: - name: "{{ tag_prefix }}" - region: "{{ ec2_region }}" - ec2_access_key: "{{ ec2_access_key }}" - ec2_secret_key: "{{ ec2_secret_key }}" - security_token: "{{ security_token }}" - state: present - zones: - - "{{ ec2_region }}a" - - "{{ ec2_region }}b" - - "{{ ec2_region }}c" - listeners: - - protocol: http - load_balancer_port: 8081 - instance_port: 8081 - purge_listeners: no - register: info - - - assert: - that: - - 'info.elb.status == "ok"' - - 'info.changed' - - '[80, 81, "HTTP", "HTTP"] in info.elb.listeners' - - '[8081, 8081, "HTTP", "HTTP"] in info.elb.listeners' - - 'info.elb.listeners|length == 2' - - - # ============================================================ - - - name: test with no parameters - ec2_elb_lb: - register: result - ignore_errors: true - - - name: assert failure when called with no parameters - assert: - that: - - 'result.failed' - - 'result.msg.startswith("missing required arguments: ")' - - - - # ============================================================ - - name: test with only name - ec2_elb_lb: - name="{{ tag_prefix }}" - register: result - ignore_errors: true - - - name: assert failure when called with only name - assert: - that: - - 'result.failed' - - 'result.msg == "missing required arguments: state"' - - - # ============================================================ - - name: test invalid region parameter - ec2_elb_lb: - name: "{{ tag_prefix }}" - region: 'asdf querty 1234' - state: present - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - zones: - - "{{ ec2_region }}a" - - "{{ ec2_region }}b" - - "{{ ec2_region }}c" - register: result - ignore_errors: true - - - name: assert invalid region parameter - assert: - that: - - 'result.failed' - - 'result.msg.startswith("Region asdf querty 1234 does not seem to be available ")' - - - # ============================================================ - - name: test valid region parameter - ec2_elb_lb: - name: "{{ tag_prefix }}" - region: "{{ ec2_region }}" - state: present - zones: - - "{{ ec2_region }}a" - - "{{ ec2_region }}b" - - "{{ ec2_region }}c" - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - - register: result - ignore_errors: true - - - name: assert valid region parameter - assert: - that: - - 'result.failed' - - 'result.msg.startswith("No handler was ready to authenticate.")' - - - # ============================================================ - - - name: test invalid ec2_url parameter - ec2_elb_lb: - name: "{{ tag_prefix }}" - region: "{{ ec2_region }}" - state: present - zones: - - "{{ ec2_region }}a" - - "{{ ec2_region }}b" - - "{{ ec2_region }}c" - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - environment: - EC2_URL: bogus.example.com - register: result - ignore_errors: true - - - name: assert invalid ec2_url parameter - assert: - that: - - 'result.failed' - - 'result.msg.startswith("No handler was ready to authenticate.")' - - - # ============================================================ - - name: test valid ec2_url parameter - ec2_elb_lb: - name: "{{ tag_prefix }}" - region: "{{ ec2_region }}" - state: present - zones: - - "{{ ec2_region }}a" - - "{{ ec2_region }}b" - - "{{ ec2_region }}c" - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - environment: - EC2_URL: '{{ec2_url}}' - register: result - ignore_errors: true - - - name: assert valid ec2_url parameter - assert: - that: - - 'result.failed' - - 'result.msg.startswith("No handler was ready to authenticate.")' - - - # ============================================================ - - name: test credentials from environment - ec2_elb_lb: - name: "{{ tag_prefix }}" - region: "{{ ec2_region }}" - state: present - zones: - - "{{ ec2_region }}a" - - "{{ ec2_region }}b" - - "{{ ec2_region }}c" - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - environment: - EC2_ACCESS_KEY: bogus_access_key - EC2_SECRET_KEY: bogus_secret_key - register: result - ignore_errors: true - - - name: assert credentials from environment - assert: - that: - - 'result.failed' - - '"InvalidClientTokenId" in result.exception' - - - # ============================================================ - - name: test credential parameters - ec2_elb_lb: - name: "{{ tag_prefix }}" - region: "{{ ec2_region }}" - state: present - zones: - - "{{ ec2_region }}a" - - "{{ ec2_region }}b" - - "{{ ec2_region }}c" - listeners: - - protocol: http - load_balancer_port: 80 - instance_port: 80 - register: result - ignore_errors: true - - - name: assert credential parameters - assert: - that: - - 'result.failed' - - '"No handler was ready to authenticate. 1 handlers were checked." in result.msg' - - always: - - # ============================================================ - - name: remove the test load balancer completely - ec2_elb_lb: - name: "{{ tag_prefix }}" - region: "{{ ec2_region }}" - state: absent - ec2_access_key: "{{ ec2_access_key }}" - ec2_secret_key: "{{ ec2_secret_key }}" - security_token: "{{ security_token }}" - register: result - - - name: assert the load balancer was removed - assert: - that: - - 'result.changed' - - 'result.elb.name == "{{tag_prefix}}"' - - 'result.elb.status == "deleted"' diff --git a/test/integration/targets/ec2_elb_lb/vars/main.yml b/test/integration/targets/ec2_elb_lb/vars/main.yml deleted file mode 100644 index 79194af1ef..0000000000 --- a/test/integration/targets/ec2_elb_lb/vars/main.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -# vars file for test_ec2_elb_lb diff --git a/test/integration/targets/ec2_group/aliases b/test/integration/targets/ec2_group/aliases deleted file mode 100644 index 5e7a8d3877..0000000000 --- a/test/integration/targets/ec2_group/aliases +++ /dev/null @@ -1,3 +0,0 @@ -cloud/aws -shippable/aws/group2 -unstable diff --git a/test/integration/targets/ec2_group/defaults/main.yml b/test/integration/targets/ec2_group/defaults/main.yml deleted file mode 100644 index a48051eac7..0000000000 --- a/test/integration/targets/ec2_group/defaults/main.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -# defaults file for test_ec2_group -ec2_group_name: '{{resource_prefix}}' -ec2_group_description: 'Created by ansible integration tests' diff --git a/test/integration/targets/ec2_group/meta/main.yml b/test/integration/targets/ec2_group/meta/main.yml deleted file mode 100644 index 1f64f1169a..0000000000 --- a/test/integration/targets/ec2_group/meta/main.yml +++ /dev/null @@ -1,3 +0,0 @@ -dependencies: - - prepare_tests - - setup_ec2 diff --git a/test/integration/targets/ec2_group/tasks/credential_tests.yml b/test/integration/targets/ec2_group/tasks/credential_tests.yml deleted file mode 100644 index 1957eaae18..0000000000 --- a/test/integration/targets/ec2_group/tasks/credential_tests.yml +++ /dev/null @@ -1,161 +0,0 @@ ---- -# 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 -# - -# - include: ../../setup_ec2/tasks/common.yml module_name: ec2_group - -- block: - # ============================================================ - - name: test failure with no parameters - ec2_group: - register: result - ignore_errors: true - - - name: assert failure with no parameters - assert: - that: - - 'result.failed' - - 'result.msg == "one of the following is required: name, group_id"' - - # ============================================================ - - name: test failure with only name - ec2_group: - name: '{{ec2_group_name}}' - register: result - ignore_errors: true - - - name: assert failure with only name - assert: - that: - - 'result.failed' - - 'result.msg == "Must provide description when state is present."' - - # ============================================================ - - name: test failure with only description - ec2_group: - description: '{{ec2_group_description}}' - register: result - ignore_errors: true - - - name: assert failure with only description - assert: - that: - - 'result.failed' - - 'result.msg == "one of the following is required: name, group_id"' - - # ============================================================ - - name: test failure with empty description (AWS API requires non-empty string desc) - ec2_group: - name: '{{ec2_group_name}}' - description: '' - region: '{{ec2_region}}' - register: result - ignore_errors: true - - - name: assert failure with empty description - assert: - that: - - 'result.failed' - - 'result.msg == "Must provide description when state is present."' - - # ============================================================ - - name: test valid region parameter - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - region: '{{ec2_region}}' - register: result - ignore_errors: true - - - name: assert valid region parameter - assert: - that: - - 'result.failed' - - '"Unable to locate credentials" in result.msg' - - # ============================================================ - - name: test environment variable EC2_REGION - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - environment: - EC2_REGION: '{{ec2_region}}' - register: result - ignore_errors: true - - - name: assert environment variable EC2_REGION - assert: - that: - - 'result.failed' - - '"Unable to locate credentials" in result.msg' - - # ============================================================ - - name: test invalid ec2_url parameter - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - environment: - EC2_URL: bogus.example.com - register: result - ignore_errors: true - - - name: assert invalid ec2_url parameter - assert: - that: - - 'result.failed' - - 'result.msg.startswith("The ec2_group module requires a region")' - - # ============================================================ - - name: test valid ec2_url parameter - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - environment: - EC2_URL: '{{ec2_url}}' - register: result - ignore_errors: true - - - name: assert valid ec2_url parameter - assert: - that: - - 'result.failed' - - 'result.msg.startswith("The ec2_group module requires a region")' - - # ============================================================ - - name: test credentials from environment - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - environment: - EC2_REGION: '{{ec2_region}}' - EC2_ACCESS_KEY: bogus_access_key - EC2_SECRET_KEY: bogus_secret_key - register: result - ignore_errors: true - - - name: assert ec2_group with valid ec2_url - assert: - that: - - 'result.failed' - - '"validate the provided access credentials" in result.msg' - - # ============================================================ - - name: test credential parameters - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - ec2_region: '{{ec2_region}}' - ec2_access_key: 'bogus_access_key' - ec2_secret_key: 'bogus_secret_key' - register: result - ignore_errors: true - - - name: assert credential parameters - assert: - that: - - 'result.failed' - - '"validate the provided access credentials" in result.msg' diff --git a/test/integration/targets/ec2_group/tasks/data_validation.yml b/test/integration/targets/ec2_group/tasks/data_validation.yml deleted file mode 100644 index 9c37e64713..0000000000 --- a/test/integration/targets/ec2_group/tasks/data_validation.yml +++ /dev/null @@ -1,44 +0,0 @@ ---- -- block: - - name: set up aws connection info - set_fact: - aws_connection_info: &aws_connection_info - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" - security_token: "{{ security_token }}" - region: "{{ aws_region }}" - no_log: yes - - name: Create a group with only the default rule - ec2_group: - name: '{{ec2_group_name}}-input-tests' - vpc_id: '{{ vpc_result.vpc.id }}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - - - name: Run through some common weird port specs - ec2_group: - name: '{{ec2_group_name}}-input-tests' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - rules: - - "{{ item }}" - with_items: - - proto: tcp - from_port: "8182" - to_port: 8182 - cidr_ipv6: "64:ff9b::/96" - rule_desc: Mixed string and non-string ports - - proto: tcp - ports: - - "9000" - - 9001 - - 9002-9005 - cidr_ip: "1.2.3.0/24" - always: - - name: tidy up input testing group - ec2_group: - name: '{{ec2_group_name}}-input-tests' - vpc_id: '{{ vpc_result.vpc.id }}' - state: absent - <<: *aws_connection_info - ignore_errors: yes diff --git a/test/integration/targets/ec2_group/tasks/diff_mode.yml b/test/integration/targets/ec2_group/tasks/diff_mode.yml deleted file mode 100644 index c4bf13bc14..0000000000 --- a/test/integration/targets/ec2_group/tasks/diff_mode.yml +++ /dev/null @@ -1,184 +0,0 @@ ---- - - name: set up aws connection info - set_fact: - aws_connection_info: &aws_connection_info - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" - security_token: "{{ security_token }}" - region: "{{ aws_region }}" - no_log: yes - - # ============================================================ - - - name: create a group with a rule (CHECK MODE + DIFF) - ec2_group: - name: '{{ ec2_group_name }}' - description: '{{ ec2_group_description }}' - state: present - rules: - - proto: tcp - from_port: 80 - to_port: 80 - cidr_ip: 0.0.0.0/0 - rules_egress: - - proto: all - cidr_ip: 0.0.0.0/0 - <<: *aws_connection_info - register: check_mode_result - check_mode: true - diff: true - - - assert: - that: - - check_mode_result.changed - - - name: create a group with a rule (DIFF) - ec2_group: - name: '{{ ec2_group_name }}' - description: '{{ ec2_group_description }}' - state: present - rules: - - proto: tcp - from_port: 80 - to_port: 80 - cidr_ip: 0.0.0.0/0 - rules_egress: - - proto: all - cidr_ip: 0.0.0.0/0 - <<: *aws_connection_info - register: result - diff: true - - - assert: - that: - - result.changed - - result.diff.0.after.ip_permissions == check_mode_result.diff.0.after.ip_permissions - - result.diff.0.after.ip_permissions_egress == check_mode_result.diff.0.after.ip_permissions_egress - - - name: add rules to make sorting occur (CHECK MODE + DIFF) - ec2_group: - name: '{{ ec2_group_name }}' - description: '{{ ec2_group_description }}' - state: present - rules: - - proto: tcp - from_port: 80 - to_port: 80 - cidr_ip: 0.0.0.0/0 - - proto: tcp - from_port: 22 - to_port: 22 - cidr_ip: 20.0.0.0/8 - - proto: tcp - from_port: 22 - to_port: 22 - cidr_ip: 10.0.0.0/8 - rules_egress: - - proto: all - cidr_ip: 0.0.0.0/0 - <<: *aws_connection_info - register: check_mode_result - check_mode: true - diff: true - - - assert: - that: - - check_mode_result.changed - - - name: add rules in a different order to test sorting consistency (DIFF) - ec2_group: - name: '{{ ec2_group_name }}' - description: '{{ ec2_group_description }}' - state: present - rules: - - proto: tcp - from_port: 22 - to_port: 22 - cidr_ip: 20.0.0.0/8 - - proto: tcp - from_port: 80 - to_port: 80 - cidr_ip: 0.0.0.0/0 - - proto: tcp - from_port: 22 - to_port: 22 - cidr_ip: 10.0.0.0/8 - rules_egress: - - proto: all - cidr_ip: 0.0.0.0/0 - <<: *aws_connection_info - register: result - diff: true - - - assert: - that: - - result.changed - - result.diff.0.after.ip_permissions == check_mode_result.diff.0.after.ip_permissions - - result.diff.0.after.ip_permissions_egress == check_mode_result.diff.0.after.ip_permissions_egress - - - name: purge rules (CHECK MODE + DIFF) - ec2_group: - name: '{{ ec2_group_name }}' - description: '{{ ec2_group_description }}' - state: present - rules: - - proto: tcp - from_port: 80 - to_port: 80 - cidr_ip: 0.0.0.0/0 - rules_egress: [] - <<: *aws_connection_info - register: check_mode_result - check_mode: true - diff: true - - - assert: - that: - - check_mode_result.changed - - - name: purge rules (DIFF) - ec2_group: - name: '{{ ec2_group_name }}' - description: '{{ ec2_group_description }}' - state: present - rules: - - proto: tcp - from_port: 80 - to_port: 80 - cidr_ip: 0.0.0.0/0 - rules_egress: [] - <<: *aws_connection_info - register: result - diff: true - - - assert: - that: - - result.changed - - result.diff.0.after.ip_permissions == check_mode_result.diff.0.after.ip_permissions - - result.diff.0.after.ip_permissions_egress == check_mode_result.diff.0.after.ip_permissions_egress - - - name: delete the security group (CHECK MODE + DIFF) - ec2_group: - name: '{{ ec2_group_name }}' - state: absent - <<: *aws_connection_info - register: check_mode_result - diff: true - check_mode: true - - - assert: - that: - - check_mode_result.changed - - - name: delete the security group (DIFF) - ec2_group: - name: '{{ ec2_group_name }}' - state: absent - <<: *aws_connection_info - register: result - diff: true - - - assert: - that: - - result.changed - - not result.diff.0.after and not check_mode_result.diff.0.after diff --git a/test/integration/targets/ec2_group/tasks/ec2_classic.yml b/test/integration/targets/ec2_group/tasks/ec2_classic.yml deleted file mode 100644 index 9019af95d4..0000000000 --- a/test/integration/targets/ec2_group/tasks/ec2_classic.yml +++ /dev/null @@ -1,88 +0,0 @@ -- module_defaults: - group/aws: - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" - security_token: "{{ security_token }}" - region: "{{ aws_region }}" - block: - - name: Get available AZs - aws_az_facts: - aws_access_key: "{{ aws_connection_info['aws_access_key'] }}" - aws_secret_key: "{{ aws_connection_info['aws_secret_key'] }}" - filters: - region-name: "{{ aws_connection_info['region'] }}" - register: az_facts - - - name: Create a classic ELB with classic networking - ec2_elb_lb: - name: "{{ resource_prefix }}-elb" - state: present - zones: - - "{{ az_facts['availability_zones'][0]['zone_name'] }}" - - "{{ az_facts['availability_zones'][1]['zone_name'] }}" - listeners: - - protocol: http # options are http, https, ssl, tcp - load_balancer_port: 80 - instance_port: 80 - proxy_protocol: True - register: classic_elb - - - name: Assert the elb was created - assert: - that: - - classic_elb.changed - - - name: Create a security group with a classic elb-sg rule - ec2_group: - name: "{{ resource_prefix }}-sg-a" - description: "EC2 classic test security group" - rules: - - proto: tcp - ports: 80 - group_id: amazon-elb/amazon-elb-sg - state: present - register: classic_sg - - - name: Assert the SG was created - assert: - that: - - classic_sg.changed - - "{{ classic_sg.ip_permissions | length }} == 1" - - - set_fact: - elb_sg_id: "{{ classic_sg.ip_permissions[0].user_id_group_pairs[0].user_id }}/{{ classic_sg.ip_permissions[0].user_id_group_pairs[0].group_id }}/{{ classic_sg.ip_permissions[0].user_id_group_pairs[0].group_name }}" - - - name: Update the security group - ec2_group: - name: "{{ resource_prefix }}-sg-a" - description: "EC2 classic test security group" - rules: - - proto: tcp - ports: 8080 - group_id: "{{ elb_sg_id }}" - - proto: tcp - ports: - - 80 - cidr_ip: 0.0.0.0/0 - state: present - register: updated_classic_sg - - - - name: Assert the SG was updated - assert: - that: - - updated_classic_sg.changed - - "{{ updated_classic_sg.ip_permissions | length }} == 2" - - "{{ classic_sg.ip_permissions[0]}} not in {{ updated_classic_sg.ip_permissions }}" - - # =========================================== - always: - - name: Terminate classic ELB - ec2_elb_lb: - name: "{{ resource_prefix }}-classic-elb" - state: absent - - - name: Delete security group - ec2_group: - name: "{{ resource_prefix }}-sg-a" - state: absent diff --git a/test/integration/targets/ec2_group/tasks/egress_tests.yml b/test/integration/targets/ec2_group/tasks/egress_tests.yml deleted file mode 100644 index aafb16ec80..0000000000 --- a/test/integration/targets/ec2_group/tasks/egress_tests.yml +++ /dev/null @@ -1,198 +0,0 @@ ---- -- block: - - name: set up aws connection info - set_fact: - aws_connection_info: &aws_connection_info - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" - security_token: "{{ security_token }}" - region: "{{ aws_region }}" - no_log: yes - - - - name: Create a group with only the default rule - ec2_group: - name: '{{ec2_group_name}}-egress-tests' - vpc_id: '{{ vpc_result.vpc.id }}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - state: present - register: result - - - name: assert default rule is in place (expected changed=true) - assert: - that: - - result is changed - - result.ip_permissions|length == 0 - - result.ip_permissions_egress|length == 1 - - result.ip_permissions_egress[0].ip_ranges[0].cidr_ip == '0.0.0.0/0' - - - name: Create a group with only the default rule - ec2_group: - name: '{{ec2_group_name}}-egress-tests' - vpc_id: '{{ vpc_result.vpc.id }}' - description: '{{ec2_group_description}}' - purge_rules_egress: false - <<: *aws_connection_info - state: present - register: result - - - name: assert default rule is not purged (expected changed=false) - assert: - that: - - result is not changed - - result.ip_permissions|length == 0 - - result.ip_permissions_egress|length == 1 - - result.ip_permissions_egress[0].ip_ranges[0].cidr_ip == '0.0.0.0/0' - - - name: Pass empty egress rules without purging, should leave default rule in place - ec2_group: - name: '{{ec2_group_name}}-egress-tests' - description: '{{ec2_group_description}}' - vpc_id: '{{ vpc_result.vpc.id }}' - purge_rules_egress: false - rules_egress: [] - <<: *aws_connection_info - state: present - register: result - - - name: assert default rule is not purged (expected changed=false) - assert: - that: - - result is not changed - - result.ip_permissions|length == 0 - - result.ip_permissions_egress|length == 1 - - result.ip_permissions_egress[0].ip_ranges[0].cidr_ip == '0.0.0.0/0' - - - name: Purge rules, including the default - ec2_group: - name: '{{ec2_group_name}}-egress-tests' - description: '{{ec2_group_description}}' - vpc_id: '{{ vpc_result.vpc.id }}' - purge_rules_egress: true - rules_egress: [] - <<: *aws_connection_info - state: present - register: result - - - name: assert default rule is not purged (expected changed=false) - assert: - that: - - result is changed - - result.ip_permissions|length == 0 - - result.ip_permissions_egress|length == 0 - - - name: Add a custom egress rule - ec2_group: - name: '{{ec2_group_name}}-egress-tests' - description: '{{ec2_group_description}}' - vpc_id: '{{ vpc_result.vpc.id }}' - rules_egress: - - proto: tcp - ports: - - 1212 - cidr_ip: 1.2.1.2/32 - <<: *aws_connection_info - state: present - register: result - - - name: assert first rule is here - assert: - that: - - result.ip_permissions_egress|length == 1 - - - name: Add a second custom egress rule - ec2_group: - name: '{{ec2_group_name}}-egress-tests' - description: '{{ec2_group_description}}' - purge_rules_egress: false - vpc_id: '{{ vpc_result.vpc.id }}' - rules_egress: - - proto: tcp - ports: - - 2323 - cidr_ip: 2.3.2.3/32 - <<: *aws_connection_info - state: present - register: result - - - name: assert the first rule is not purged - assert: - that: - - result.ip_permissions_egress|length == 2 - - - name: Purge the second rule (CHECK MODE) (DIFF MODE) - ec2_group: - name: '{{ec2_group_name}}-egress-tests' - description: '{{ec2_group_description}}' - vpc_id: '{{ vpc_result.vpc.id }}' - rules_egress: - - proto: tcp - ports: - - 1212 - cidr_ip: 1.2.1.2/32 - <<: *aws_connection_info - state: present - register: result - check_mode: True - diff: True - - - name: assert first rule will be left - assert: - that: - - result.changed - - result.diff.0.after.ip_permissions_egress|length == 1 - - result.diff.0.after.ip_permissions_egress[0].ip_ranges[0].cidr_ip == '1.2.1.2/32' - - - name: Purge the second rule - ec2_group: - name: '{{ec2_group_name}}-egress-tests' - description: '{{ec2_group_description}}' - vpc_id: '{{ vpc_result.vpc.id }}' - rules_egress: - - proto: tcp - ports: - - 1212 - cidr_ip: 1.2.1.2/32 - <<: *aws_connection_info - state: present - register: result - - - name: assert first rule is here - assert: - that: - - result.ip_permissions_egress|length == 1 - - result.ip_permissions_egress[0].ip_ranges[0].cidr_ip == '1.2.1.2/32' - - - name: add a rule for all TCP ports - ec2_group: - name: '{{ec2_group_name}}-egress-tests' - description: '{{ec2_group_description}}' - rules_egress: - - proto: tcp - ports: 0-65535 - cidr_ip: 0.0.0.0/0 - <<: *aws_connection_info - state: present - vpc_id: '{{ vpc_result.vpc.id }}' - register: result - - - name: Re-add the default rule - ec2_group: - name: '{{ec2_group_name}}-egress-tests' - description: '{{ec2_group_description}}' - rules_egress: - - proto: -1 - cidr_ip: 0.0.0.0/0 - <<: *aws_connection_info - state: present - vpc_id: '{{ vpc_result.vpc.id }}' - register: result - always: - - name: tidy up egress rule test security group - ec2_group: - name: '{{ec2_group_name}}-egress-tests' - state: absent - vpc_id: '{{ vpc_result.vpc.id }}' - <<: *aws_connection_info - ignore_errors: yes diff --git a/test/integration/targets/ec2_group/tasks/ipv6_default_tests.yml b/test/integration/targets/ec2_group/tasks/ipv6_default_tests.yml deleted file mode 100644 index eadaf30f6b..0000000000 --- a/test/integration/targets/ec2_group/tasks/ipv6_default_tests.yml +++ /dev/null @@ -1,103 +0,0 @@ ---- -- name: set up aws connection info - set_fact: - aws_connection_info: &aws_connection_info - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" - security_token: "{{ security_token }}" - region: "{{ aws_region }}" - no_log: yes -# ============================================================ -- name: test state=present for ipv6 (expected changed=true) (CHECK MODE) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - state: present - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ipv6: "64:ff9b::/96" - check_mode: true - register: result - -- name: assert state=present (expected changed=true) - assert: - that: - - 'result.changed' - -# ============================================================ -- name: test state=present for ipv6 (expected changed=true) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - state: present - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ipv6: "64:ff9b::/96" - register: result - -- name: assert state=present (expected changed=true) - assert: - that: - - 'result.changed' - - 'result.group_id.startswith("sg-")' - -# ============================================================ -- name: test rules_egress state=present for ipv6 (expected changed=true) (CHECK MODE) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - state: present - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ipv6: "64:ff9b::/96" - rules_egress: - - proto: "tcp" - from_port: 8181 - to_port: 8181 - cidr_ipv6: "64:ff9b::/96" - check_mode: true - register: result - -- name: assert state=present (expected changed=true) - assert: - that: - - 'result.changed' - -# ============================================================ -- name: test rules_egress state=present for ipv6 (expected changed=true) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - state: present - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ipv6: "64:ff9b::/96" - rules_egress: - - proto: "tcp" - from_port: 8181 - to_port: 8181 - cidr_ipv6: "64:ff9b::/96" - register: result - -- name: assert state=present (expected changed=true) - assert: - that: - - 'result.changed' - - 'result.group_id.startswith("sg-")' -- name: delete it - ec2_group: - name: '{{ec2_group_name}}' - <<: *aws_connection_info - state: absent diff --git a/test/integration/targets/ec2_group/tasks/main.yml b/test/integration/targets/ec2_group/tasks/main.yml deleted file mode 100644 index 9b558656cd..0000000000 --- a/test/integration/targets/ec2_group/tasks/main.yml +++ /dev/null @@ -1,1536 +0,0 @@ ---- -# 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 -# - -# - include: ../../setup_ec2/tasks/common.yml module_name: ec2_group - -- include: ./credential_tests.yml -# ============================================================ -# EC2 Classic tests can only be run on a pre-2013 AWS account with supported-platforms=EC2 -# Ansible CI does NOT have classic EC2 support; these tests are provided as-is for the -# community and can be run if you have access to a classic account. To check if your account -# has support for EC2 Classic you can use the `aws_account_attribute` plugin. - -- name: determine if this is an EC2 Classic account - set_fact: - has_ec2_classic: "{{ lookup('aws_account_attribute', - attribute='has-ec2-classic', - region=aws_region, - aws_access_key=aws_access_key, - aws_secret_key=aws_secret_key, - aws_security_token=security_token, - wantlist=True) }}" -# ============================================================ -- -- name: set up aws connection info - set_fact: - aws_connection_info: &aws_connection_info - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" - security_token: "{{ security_token }}" - region: "{{ aws_region }}" - no_log: yes - -# ============================================================ -- name: Run EC2 Classic accounts if account type is EC2 - include: ./ec2_classic.yml - when: has_ec2_classic - -# ============================================================ -# Other tests depend on attribute='default-vpc', ie no vpc_id is set. This is -# incompatible with EC2 classic accounts, so these tests can only be run in a -# VPC-type account. See "Q. I really want a default VPC for my existing EC2 -# account. Is that possible?" in https://aws.amazon.com/vpc/faqs/#Default_VPCs -- name: Run all other tests if account type is VPC - module_defaults: - group/aws: - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" - security_token: "{{ security_token }}" - region: "{{ aws_region }}" - block: - - name: determine if there is a default VPC - set_fact: - defaultvpc: "{{ lookup('aws_account_attribute', - attribute='default-vpc', - region=aws_region, - aws_access_key=aws_access_key, - aws_secret_key=aws_secret_key, - aws_security_token=security_token) }}" - register: default_vpc - - - name: create a VPC - ec2_vpc_net: - name: "{{ resource_prefix }}-vpc" - state: present - cidr_block: "10.232.232.128/26" - <<: *aws_connection_info - tags: - Name: "{{ resource_prefix }}-vpc" - Description: "Created by ansible-test" - register: vpc_result - #TODO(ryansb): Update CI for VPC peering permissions - #- include: ./multi_account.yml - - include: ./diff_mode.yml - - include: ./numeric_protos.yml - - include: ./rule_group_create.yml - - include: ./egress_tests.yml - - include: ./data_validation.yml - - include: ./multi_nested_target.yml - - # ============================================================ - - name: test state=absent (CHECK MODE) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - state: absent - check_mode: true - register: result - - - name: assert no changes would be made - assert: - that: - - not result.changed - - # =========================================================== - - name: test state=absent - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - state: absent - register: result - - # ============================================================ - - name: test state=present (expected changed=true) (CHECK MODE) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - state: present - check_mode: true - register: result - - - name: assert state=present (expected changed=true) - assert: - that: - - 'result.changed' - - # ============================================================ - - name: test state=present (expected changed=true) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - state: present - register: result - - - name: assert state=present (expected changed=true) - assert: - that: - - 'result.changed' - - 'result.group_id.startswith("sg-")' - - # ============================================================ - - name: test state=present different description (expected changed=false) (CHECK MODE) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}CHANGED' - <<: *aws_connection_info - state: present - check_mode: true - register: result - - - name: assert state=present (expected changed=false) - assert: - that: - - 'not result.changed' - - # ============================================================ - - name: test state=present different description (expected changed=false) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}CHANGED' - <<: *aws_connection_info - state: present - ignore_errors: true - register: result - - - name: assert state=present (expected changed=false) - assert: - that: - - 'not result.changed' - - 'result.group_id.startswith("sg-")' - - # ============================================================ - - name: test state=present (expected changed=false) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - state: present - register: result - - - name: assert state=present (expected changed=false) - assert: - that: - - 'not result.changed' - - 'result.group_id.startswith("sg-")' - - # ============================================================ - - name: tests IPv6 with the default VPC - include: ./ipv6_default_tests.yml - when: default_vpc - - - name: test IPv6 with a specified VPC - block: - - # ============================================================ - - name: test state=present (expected changed=true) (CHECK MODE) - ec2_group: - name: '{{ ec2_group_name }}-2' - description: '{{ ec2_group_description }}-2' - state: present - vpc_id: '{{ vpc_result.vpc.id }}' - <<: *aws_connection_info - check_mode: true - register: result - - - name: assert state=present (expected changed=true) - assert: - that: - - 'result.changed' - - # ============================================================ - - name: test state=present (expected changed=true) - ec2_group: - name: '{{ ec2_group_name }}-2' - description: '{{ ec2_group_description }}-2' - state: present - vpc_id: '{{ vpc_result.vpc.id }}' - <<: *aws_connection_info - register: result - - - name: assert state=present (expected changed=true) - assert: - that: - - 'result.changed' - - 'result.group_id.startswith("sg-")' - - # ============================================================ - - name: test state=present for ipv6 (expected changed=true) (CHECK MODE) - ec2_group: - name: '{{ ec2_group_name }}-2' - description: '{{ ec2_group_description }}-2' - state: present - vpc_id: '{{ vpc_result.vpc.id }}' - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ipv6: "64:ff9b::/96" - <<: *aws_connection_info - check_mode: true - register: result - - - name: assert state=present (expected changed=true) - assert: - that: - - 'result.changed' - - # ============================================================ - - name: test state=present for ipv6 (expected changed=true) - ec2_group: - name: '{{ ec2_group_name }}-2' - description: '{{ ec2_group_description }}-2' - state: present - vpc_id: '{{ vpc_result.vpc.id }}' - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ipv6: "64:ff9b::/96" - <<: *aws_connection_info - register: result - - - name: assert state=present (expected changed=true) - assert: - that: - - 'result.changed' - - 'result.group_id.startswith("sg-")' - - # ============================================================ - - name: test state=present for ipv6 (expected changed=false) (CHECK MODE) - ec2_group: - name: '{{ ec2_group_name }}-2' - description: '{{ ec2_group_description }}-2' - state: present - vpc_id: '{{ vpc_result.vpc.id }}' - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ipv6: "64:ff9b::/96" - <<: *aws_connection_info - check_mode: true - register: result - - - name: assert nothing changed - assert: - that: - - 'not result.changed' - - # ============================================================ - - name: test state=present for ipv6 (expected changed=false) - ec2_group: - name: '{{ ec2_group_name }}-2' - description: '{{ ec2_group_description }}-2' - state: present - vpc_id: '{{ vpc_result.vpc.id }}' - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ipv6: "64:ff9b::/96" - <<: *aws_connection_info - register: result - - - name: assert nothing changed - assert: - that: - - 'not result.changed' - - # ============================================================ - - name: test rules_egress state=present for ipv6 (expected changed=true) (CHECK MODE) - ec2_group: - name: '{{ ec2_group_name }}-2' - description: '{{ ec2_group_description }}-2' - state: present - vpc_id: '{{ vpc_result.vpc.id }}' - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ipv6: "64:ff9b::/96" - rules_egress: - - proto: "tcp" - from_port: 8181 - to_port: 8181 - cidr_ipv6: "64:ff9b::/96" - <<: *aws_connection_info - check_mode: true - diff: true - register: result - - - name: assert state=present (expected changed=true) - assert: - that: - - 'result.changed' - - 'result.diff.0.before.ip_permissions == result.diff.0.after.ip_permissions' - - 'result.diff.0.before.ip_permissions_egress != result.diff.0.after.ip_permissions_egress' - - # ============================================================ - - name: test rules_egress state=present for ipv6 (expected changed=true) - ec2_group: - name: '{{ ec2_group_name }}-2' - description: '{{ ec2_group_description }}-2' - state: present - vpc_id: '{{ vpc_result.vpc.id }}' - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ipv6: "64:ff9b::/96" - rules_egress: - - proto: "tcp" - from_port: 8181 - to_port: 8181 - cidr_ipv6: "64:ff9b::/96" - <<: *aws_connection_info - register: result - - - name: assert state=present (expected changed=true) - assert: - that: - - 'result.changed' - - 'result.group_id.startswith("sg-")' - - # ============================================================ - - name: test state=absent (expected changed=true) (CHECK MODE) - ec2_group: - name: '{{ ec2_group_name }}-2' - description: '{{ ec2_group_description }}-2' - state: absent - vpc_id: '{{ vpc_result.vpc.id }}' - <<: *aws_connection_info - check_mode: true - diff: true - register: result - - - name: assert group was removed - assert: - that: - - 'result.changed' - - 'not result.diff.0.after' - - # ============================================================ - - name: test state=absent (expected changed=true) - ec2_group: - name: '{{ ec2_group_name }}-2' - description: '{{ ec2_group_description }}-2' - state: absent - vpc_id: '{{ vpc_result.vpc.id }}' - <<: *aws_connection_info - register: result - - - name: assert group was removed - assert: - that: - - 'result.changed' - - # ============================================================ - - name: test state=present for ipv4 (expected changed=true) (CHECK MODE) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ip: "1.1.1.1/32" - check_mode: true - register: result - - - name: assert state=present (expected changed=true) - assert: - that: - - 'result.changed' - - # ============================================================ - - name: test state=present for ipv4 (expected changed=true) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ip: "1.1.1.1/32" - register: result - - - name: assert state=present (expected changed=true) - assert: - that: - - 'result.changed' - - 'result.group_id.startswith("sg-")' - - 'result.ip_permissions|length == 1' - - 'result.ip_permissions_egress|length == 1' - - # ============================================================ - - name: add same rule to the existing group (expected changed=false) (CHECK MODE) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - state: present - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ip: "1.1.1.1/32" - check_mode: true - diff: true - register: check_result - - - assert: - that: - - not check_result.changed - - check_result.diff.0.before.ip_permissions.0 == check_result.diff.0.after.ip_permissions.0 - - # ============================================================ - - name: add same rule to the existing group (expected changed=false) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - state: present - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ip: "1.1.1.1/32" - register: result - - - name: assert state=present (expected changed=false) - assert: - that: - - 'not result.changed' - - 'result.group_id.startswith("sg-")' - - - name: assert state=present (expected changed=false) - assert: - that: - - 'not check_result.changed' - - # ============================================================ - - name: add a rule that auto creates another security group (CHECK MODE) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - state: present - purge_rules: no - rules: - - proto: "tcp" - group_name: "{{ resource_prefix }} - Another security group" - group_desc: Another security group - ports: 7171 - check_mode: true - register: result - - - name: check that there are now two rules - assert: - that: - - result.changed - - # ============================================================ - - name: add a rule that auto creates another security group - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - state: present - purge_rules: no - rules: - - proto: "tcp" - group_name: "{{ resource_prefix }} - Another security group" - group_desc: Another security group - ports: 7171 - register: result - - - name: check that there are now two rules - assert: - that: - - result.changed - - result.ip_permissions|length == 2 - - result.ip_permissions[0].user_id_group_pairs or - result.ip_permissions[1].user_id_group_pairs - - 'result.ip_permissions_egress[0].ip_protocol == "-1"' - - # ============================================================ - - name: test ip rules convert port numbers from string to int (expected changed=true) (CHECK MODE) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - state: present - rules: - - proto: "tcp" - from_port: "8183" - to_port: "8183" - cidr_ip: "1.1.1.1/32" - rules_egress: - - proto: "tcp" - from_port: "8184" - to_port: "8184" - cidr_ip: "1.1.1.1/32" - check_mode: true - register: result - - - name: assert state=present (expected changed=true) - assert: - that: - - 'result.changed' - - # ============================================================ - - name: test ip rules convert port numbers from string to int (expected changed=true) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - state: present - rules: - - proto: "tcp" - from_port: "8183" - to_port: "8183" - cidr_ip: "1.1.1.1/32" - rules_egress: - - proto: "tcp" - from_port: "8184" - to_port: "8184" - cidr_ip: "1.1.1.1/32" - register: result - - - name: assert state=present (expected changed=true) - assert: - that: - - 'result.changed' - - 'result.group_id.startswith("sg-")' - - 'result.ip_permissions|length == 1' - - 'result.ip_permissions_egress[0].ip_protocol == "tcp"' - - - # ============================================================ - - name: test group rules convert port numbers from string to int (expected changed=true) (CHECK MODE) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - state: present - rules: - - proto: "tcp" - from_port: "8185" - to_port: "8185" - group_id: "{{result.group_id}}" - rules_egress: - - proto: "tcp" - from_port: "8186" - to_port: "8186" - group_id: "{{result.group_id}}" - check_mode: true - register: result - - - name: assert state=present (expected changed=true) - assert: - that: - - 'result.changed' - - # ============================================================ - - name: test group rules convert port numbers from string to int (expected changed=true) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - state: present - rules: - - proto: "tcp" - from_port: "8185" - to_port: "8185" - group_id: "{{result.group_id}}" - rules_egress: - - proto: "tcp" - from_port: "8186" - to_port: "8186" - group_id: "{{result.group_id}}" - register: result - - - name: assert state=present (expected changed=true) - assert: - that: - - 'result.changed' - - 'result.group_id.startswith("sg-")' - - # ============================================================ - - name: test adding a range of ports and ports given as strings (expected changed=true) (CHECK MODE) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - state: present - # set purge_rules to false so we don't get a false positive from previously added rules - purge_rules: false - rules: - - proto: "tcp" - ports: - - 8183-8190 - - '8192' - cidr_ip: 1.1.1.1/32 - check_mode: true - register: result - - - name: assert state=present (expected changed=true) - assert: - that: - - 'result.changed' - - # ============================================================ - - name: test adding a range of ports and ports given as strings (expected changed=true) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - state: present - # set purge_rules to false so we don't get a false positive from previously added rules - purge_rules: false - rules: - - proto: "tcp" - ports: - - 8183-8190 - - '8192' - cidr_ip: 1.1.1.1/32 - register: result - - - name: assert state=present (expected changed=true) - assert: - that: - - 'result.changed' - - 'result.group_id.startswith("sg-")' - - # ============================================================ - - name: test adding a rule with a IPv4 CIDR with host bits set (expected changed=true) (CHECK MODE) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - state: present - # set purge_rules to false so we don't get a false positive from previously added rules - purge_rules: false - rules: - - proto: "tcp" - ports: - - 8195 - cidr_ip: 10.0.0.1/8 - check_mode: true - register: result - - - name: assert state=present (expected changed=true) - assert: - that: - - 'result.changed' - - # ============================================================ - - name: test adding a rule with a IPv4 CIDR with host bits set (expected changed=true) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - state: present - # set purge_rules to false so we don't get a false positive from previously added rules - purge_rules: false - rules: - - proto: "tcp" - ports: - - 8195 - cidr_ip: 10.0.0.1/8 - register: result - - - name: assert state=present (expected changed=true) - assert: - that: - - 'result.changed' - - 'result.group_id.startswith("sg-")' - - # ============================================================ - - name: test adding the same rule with a IPv4 CIDR with host bits set (expected changed=false) (CHECK MODE) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - state: present - # set purge_rules to false so we don't get a false positive from previously added rules - purge_rules: false - rules: - - proto: "tcp" - ports: - - 8195 - cidr_ip: 10.0.0.1/8 - check_mode: true - register: check_result - - # ============================================================ - - name: test adding the same rule with a IPv4 CIDR with host bits set (expected changed=false and a warning) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - state: present - # set purge_rules to false so we don't get a false positive from previously added rules - purge_rules: false - rules: - - proto: "tcp" - ports: - - 8195 - cidr_ip: 10.0.0.1/8 - register: result - - - name: assert state=present (expected changed=false and a warning) - assert: - that: - - 'not check_result.changed' - - - name: assert state=present (expected changed=false and a warning) - assert: - that: - # No way to assert for warnings? - - 'not result.changed' - - 'result.group_id.startswith("sg-")' - - # ============================================================ - - name: test using the default VPC - block: - - - name: test adding a rule with a IPv6 CIDR with host bits set (expected changed=true) (CHECK MODE) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - state: present - # set purge_rules to false so we don't get a false positive from previously added rules - purge_rules: false - rules: - - proto: "tcp" - ports: - - 8196 - cidr_ipv6: '2001:db00::1/24' - check_mode: true - register: result - - - name: assert state=present (expected changed=true) - assert: - that: - - 'result.changed' - - # ============================================================ - - name: test adding a rule with a IPv6 CIDR with host bits set (expected changed=true) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - state: present - # set purge_rules to false so we don't get a false positive from previously added rules - purge_rules: false - rules: - - proto: "tcp" - ports: - - 8196 - cidr_ipv6: '2001:db00::1/24' - register: result - - - name: assert state=present (expected changed=true) - assert: - that: - - 'result.changed' - - 'result.group_id.startswith("sg-")' - - # ============================================================ - - - name: test adding a rule again with a IPv6 CIDR with host bits set (expected changed=false and a warning) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - state: present - # set purge_rules to false so we don't get a false positive from previously added rules - purge_rules: false - rules: - - proto: "tcp" - ports: - - 8196 - cidr_ipv6: '2001:db00::1/24' - register: result - - - name: assert state=present (expected changed=false and a warning) - assert: - that: - # No way to assert for warnings? - - 'not result.changed' - - 'result.group_id.startswith("sg-")' - - when: default_vpc - - # ============================================================ - - name: test state=absent (expected changed=true) (CHECK MODE) - ec2_group: - name: '{{ec2_group_name}}' - state: absent - <<: *aws_connection_info - check_mode: true - register: result - - - name: assert state=absent (expected changed=true) - assert: - that: - - 'result.changed' - - # ============================================================ - - name: test state=absent (expected changed=true) - ec2_group: - name: '{{ec2_group_name}}' - state: absent - <<: *aws_connection_info - register: result - - - name: assert state=absent (expected changed=true) - assert: - that: - - 'result.changed' - - 'not result.group_id' - - # ============================================================ - - name: create security group in the VPC (CHECK MODE) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - vpc_id: '{{ vpc_result.vpc.id }}' - state: present - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ip: "1.1.1.1/32" - check_mode: true - register: result - - - name: assert state=present (expected changed=true) - assert: - that: - - 'result.changed' - - # ============================================================ - - name: create security group in the VPC - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - vpc_id: '{{ vpc_result.vpc.id }}' - state: present - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ip: "1.1.1.1/32" - register: result - - - name: assert state=present (expected changed=true) - assert: - that: - - 'result.changed' - - 'result.vpc_id == vpc_result.vpc.id' - - 'result.group_id.startswith("sg-")' - - # ============================================================ - - name: test adding tags (expected changed=true) (CHECK MODE) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - vpc_id: '{{ vpc_result.vpc.id }}' - state: present - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ip: "1.1.1.1/32" - tags: - tag1: test1 - tag2: test2 - check_mode: true - diff: true - register: result - - - name: assert that tags were added (expected changed=true) - assert: - that: - - 'result.changed' - - 'not result.diff.0.before.tags' - - 'result.diff.0.after.tags.tag1 == "test1"' - - 'result.diff.0.after.tags.tag2 == "test2"' - - # ============================================================ - - name: test adding tags (expected changed=true) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - vpc_id: '{{ vpc_result.vpc.id }}' - state: present - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ip: "1.1.1.1/32" - tags: - tag1: test1 - tag2: test2 - register: result - - - name: assert that tags were added (expected changed=true) - assert: - that: - - 'result.changed' - - 'result.tags == {"tag1": "test1", "tag2": "test2"}' - - # ============================================================ - - name: test that tags are present (expected changed=False) (CHECK MODE) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - vpc_id: '{{ vpc_result.vpc.id }}' - state: present - purge_rules_egress: false - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ip: "1.1.1.1/32" - tags: - tag1: test1 - tag2: test2 - check_mode: true - register: result - - - name: assert that tags were not changed (expected changed=False) - assert: - that: - - 'not result.changed' - - # ============================================================ - - name: test that tags are present (expected changed=False) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - vpc_id: '{{ vpc_result.vpc.id }}' - state: present - purge_rules_egress: false - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ip: "1.1.1.1/32" - tags: - tag1: test1 - tag2: test2 - register: result - - - name: assert that tags were not changed (expected changed=False) - assert: - that: - - 'not result.changed' - - 'result.tags == {"tag1": "test1", "tag2": "test2"}' - - # ============================================================ - - name: test purging tags (expected changed=True) (CHECK MODE) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - vpc_id: '{{ vpc_result.vpc.id }}' - state: present - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ip: "1.1.1.1/32" - tags: - tag1: test1 - check_mode: true - register: result - - - name: assert that tag2 was removed (expected changed=true) - assert: - that: - - 'result.changed' - - # ============================================================ - - name: test purging tags (expected changed=True) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - vpc_id: '{{ vpc_result.vpc.id }}' - state: present - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ip: "1.1.1.1/32" - tags: - tag1: test1 - register: result - - - name: assert that tag2 was removed (expected changed=true) - assert: - that: - - 'result.changed' - - 'result.tags == {"tag1": "test1"}' - - # ============================================================ - - - name: assert that tags are left as-is if not specified (expected changed=False) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - vpc_id: '{{ vpc_result.vpc.id }}' - state: present - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ip: "1.1.1.1/32" - register: result - - - name: assert that the tags stayed the same (expected changed=false) - assert: - that: - - 'not result.changed' - - 'result.tags == {"tag1": "test1"}' - - # ============================================================ - - - name: test purging all tags (expected changed=True) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - vpc_id: '{{ vpc_result.vpc.id }}' - state: present - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ip: "1.1.1.1/32" - tags: {} - register: result - - - name: assert that tag1 was removed (expected changed=true) - assert: - that: - - 'result.changed' - - 'not result.tags' - - # ============================================================ - - name: test adding a rule and egress rule descriptions (expected changed=true) (CHECK MODE) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - vpc_id: '{{ vpc_result.vpc.id }}' - # purge the other rules so assertions work for the subsequent tests for rule descriptions - purge_rules_egress: true - purge_rules: true - state: present - rules: - - proto: "tcp" - ports: - - 8281 - cidr_ipv6: 1001:d00::/24 - rule_desc: ipv6 rule desc 1 - rules_egress: - - proto: "tcp" - ports: - - 8282 - cidr_ip: 2.2.2.2/32 - rule_desc: egress rule desc 1 - check_mode: true - register: result - - - name: assert that rule descriptions are created (expected changed=true) - # Only assert this if rule description is defined as the botocore version may < 1.7.2. - # It's still helpful to have these tests run on older versions since it verifies backwards - # compatibility with this feature. - assert: - that: - - 'result.changed' - when: result.ip_permissions_egress[0].ip_ranges[0].description is defined - - - name: if an older version of botocore is installed changes should still have changed due to purged rules (expected changed=true) - assert: - that: - - 'result.changed' - when: result.ip_permissions_egress[0].ip_ranges[0].description is undefined - - # ========================================================================================= - - name: add rules without descriptions ready for adding descriptions to existing rules - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - vpc_id: '{{ vpc_result.vpc.id }}' - # purge the other rules so assertions work for the subsequent tests for rule descriptions - purge_rules_egress: true - purge_rules: true - state: present - rules: - - proto: "tcp" - ports: - - 8281 - cidr_ipv6: 1001:d00::/24 - rules_egress: - - proto: "tcp" - ports: - - 8282 - cidr_ip: 2.2.2.2/32 - register: result - - # ============================================================ - - name: test adding a rule and egress rule descriptions (expected changed=true) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - vpc_id: '{{ vpc_result.vpc.id }}' - # purge the other rules so assertions work for the subsequent tests for rule descriptions - purge_rules_egress: true - purge_rules: true - state: present - rules: - - proto: "tcp" - ports: - - 8281 - cidr_ipv6: 1001:d00::/24 - rule_desc: ipv6 rule desc 1 - rules_egress: - - proto: "tcp" - ports: - - 8282 - cidr_ip: 2.2.2.2/32 - rule_desc: egress rule desc 1 - register: result - - - name: assert that rule descriptions are created (expected changed=true) - # Only assert this if rule description is defined as the botocore version may < 1.7.2. - # It's still helpful to have these tests run on older versions since it verifies backwards - # compatibility with this feature. - assert: - that: - - 'result.changed' - - 'result.ip_permissions[0].ipv6_ranges[0].description == "ipv6 rule desc 1"' - - 'result.ip_permissions_egress[0].ip_ranges[0].description == "egress rule desc 1"' - when: result.ip_permissions_egress[0].ip_ranges[0].description is defined - - - name: if an older version of botocore is installed changes should still have changed due to purged rules (expected changed=true) - assert: - that: - - 'result.changed' - when: result.ip_permissions_egress[0].ip_ranges[0].description is undefined - - # ============================================================ - - name: test modifying rule and egress rule descriptions (expected changed=true) (CHECK MODE) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - vpc_id: '{{ vpc_result.vpc.id }}' - purge_rules_egress: false - purge_rules: false - state: present - rules: - - proto: "tcp" - ports: - - 8281 - cidr_ipv6: 1001:d00::/24 - rule_desc: ipv6 rule desc 2 - rules_egress: - - proto: "tcp" - ports: - - 8282 - cidr_ip: 2.2.2.2/32 - rule_desc: egress rule desc 2 - check_mode: true - register: result - - - name: assert that rule descriptions were modified (expected changed=true) - # Only assert this if rule description is defined as the botocore version may < 1.7.2. - # It's still helpful to have these tests run on older versions since it verifies backwards - # compatibility with this feature. - assert: - that: - - 'result.ip_permissions | length > 0' - - 'result.changed' - when: result.ip_permissions_egress[0].ip_ranges[0].description is defined - - - name: if an older version of botocore is installed everything should stay the same (expected changed=false) - assert: - that: - - 'not result.changed' - when: result.ip_permissions_egress[0].ip_ranges[0].description is undefined and result.ip_permissions_egress[1].ip_ranges[0].description is undefined - - # ============================================================ - - name: test modifying rule and egress rule descriptions (expected changed=true) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - vpc_id: '{{ vpc_result.vpc.id }}' - purge_rules_egress: false - purge_rules: false - state: present - rules: - - proto: "tcp" - ports: - - 8281 - cidr_ipv6: 1001:d00::/24 - rule_desc: ipv6 rule desc 2 - rules_egress: - - proto: "tcp" - ports: - - 8282 - cidr_ip: 2.2.2.2/32 - rule_desc: egress rule desc 2 - register: result - - - name: assert that rule descriptions were modified (expected changed=true) - # Only assert this if rule description is defined as the botocore version may < 1.7.2. - # It's still helpful to have these tests run on older versions since it verifies backwards - # compatibility with this feature. - assert: - that: - - 'result.changed' - - 'result.ip_permissions[0].ipv6_ranges[0].description == "ipv6 rule desc 2"' - - 'result.ip_permissions_egress[0].ip_ranges[0].description == "egress rule desc 2"' - when: result.ip_permissions_egress[0].ip_ranges[0].description is defined - - - name: if an older version of botocore is installed everything should stay the same (expected changed=false) - assert: - that: - - 'not result.changed' - when: result.ip_permissions_egress[0].ip_ranges[0].description is undefined - - # ============================================================ - - - name: test creating rule in default vpc with egress rule (expected changed=true) - ec2_group: - name: '{{ec2_group_name}}-default-vpc' - description: '{{ec2_group_description}} default VPC' - <<: *aws_connection_info - purge_rules_egress: true - state: present - rules: - - proto: "tcp" - ports: - - 8281 - cidr_ip: 1.1.1.1/24 - rule_desc: ipv4 rule desc - rules_egress: - - proto: "tcp" - ports: - - 8282 - cidr_ip: 2.2.2.2/32 - rule_desc: egress rule desc 2 - register: result - - - name: assert that rule descriptions were modified (expected changed=true) - # Only assert this if rule description is defined as the botocore version may < 1.7.2. - # It's still helpful to have these tests run on older versions since it verifies backwards - # compatibility with this feature. - assert: - that: - - 'result.changed' - - 'result.ip_permissions_egress|length == 1' - - # ============================================================ - - name: test that keeping the same rule descriptions (expected changed=false) (CHECK MODE) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - vpc_id: '{{ vpc_result.vpc.id }}' - purge_rules_egress: false - purge_rules: false - state: present - rules: - - proto: "tcp" - ports: - - 8281 - cidr_ipv6: 1001:d00::/24 - rule_desc: ipv6 rule desc 2 - rules_egress: - - proto: "tcp" - ports: - - 8282 - cidr_ip: 2.2.2.2/32 - rule_desc: egress rule desc 2 - check_mode: true - register: result - - - name: assert that rule descriptions stayed the same (expected changed=false) - # Only assert this if rule description is defined as the botocore version may < 1.7.2. - # It's still helpful to have these tests run on older versions since it verifies backwards - # compatibility with this feature. - assert: - that: - - 'not result.changed' - when: result.ip_permissions_egress[0].ip_ranges[0].description is defined - - - name: if an older version of botocore is installed everything should stay the same (expected changed=false) - assert: - that: - - 'not result.changed' - when: result.ip_permissions_egress[0].ip_ranges[0].description is undefined - - # ============================================================ - - name: test that keeping the same rule descriptions (expected changed=false) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - vpc_id: '{{ vpc_result.vpc.id }}' - purge_rules_egress: false - purge_rules: false - state: present - rules: - - proto: "tcp" - ports: - - 8281 - cidr_ipv6: 1001:d00::/24 - rule_desc: ipv6 rule desc 2 - rules_egress: - - proto: "tcp" - ports: - - 8282 - cidr_ip: 2.2.2.2/32 - rule_desc: egress rule desc 2 - register: result - - - name: assert that rule descriptions stayed the same (expected changed=false) - # Only assert this if rule description is defined as the botocore version may < 1.7.2. - # It's still helpful to have these tests run on older versions since it verifies backwards - # compatibility with this feature. - assert: - that: - - 'not result.changed' - - 'result.ip_permissions[0].ipv6_ranges[0].description == "ipv6 rule desc 2"' - - 'result.ip_permissions_egress[0].ip_ranges[0].description == "egress rule desc 2"' - when: result.ip_permissions_egress[0].ip_ranges[0].description is defined - - - name: if an older version of botocore is installed everything should stay the same (expected changed=false) - assert: - that: - - 'not result.changed' - when: result.ip_permissions_egress[0].ip_ranges[0].description is undefined - - # ============================================================ - - name: test removing rule descriptions (expected changed=true) (CHECK MODE) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - vpc_id: '{{ vpc_result.vpc.id }}' - purge_rules_egress: false - purge_rules: false - state: present - rules: - - proto: "tcp" - ports: - - 8281 - cidr_ipv6: 1001:d00::/24 - rule_desc: - rules_egress: - - proto: "tcp" - ports: - - 8282 - cidr_ip: 2.2.2.2/32 - rule_desc: - check_mode: true - register: result - - - name: assert that rule descriptions were removed (expected changed=true) - # Only assert this if rule description is defined as the botocore version may < 1.7.2. - # It's still helpful to have these tests run on older versions since it verifies backwards - # compatibility with this feature. - assert: - that: - - 'result.changed' - when: result.ip_permissions_egress[0].ip_ranges[0].description is defined - - - name: if an older version of botocore is installed everything should stay the same (expected changed=false) - assert: - that: - - 'not result.changed' - when: result.ip_permissions_egress[0].ip_ranges[0].description is undefined - - # ============================================================ - - name: test removing rule descriptions (expected changed=true) - ec2_group: - name: '{{ec2_group_name}}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - vpc_id: '{{ vpc_result.vpc.id }}' - purge_rules_egress: false - purge_rules: false - state: present - rules: - - proto: "tcp" - ports: - - 8281 - cidr_ipv6: 1001:d00::/24 - rule_desc: - rules_egress: - - proto: "tcp" - ports: - - 8282 - cidr_ip: 2.2.2.2/32 - rule_desc: - register: result - ignore_errors: true - - - name: assert that rule descriptions were removed (expected changed=true with newer botocore) - # Only assert this if rule description is defined as the botocore version may < 1.7.2. - # It's still helpful to have these tests run on older versions since it verifies backwards - # compatibility with this feature. - assert: - that: - - 'result.ip_permissions[0].ipv6_ranges[0].description is undefined' - - 'result.ip_permissions_egress[0].ip_ranges[0].description is undefined' - when: result is changed - - - name: if an older version of botocore is installed everything should stay the same (expected changed=false) - assert: - that: - - 'not result.changed' - when: result.failed - - # ============================================================ - - - name: test state=absent (expected changed=true) - ec2_group: - name: '{{ec2_group_name}}' - state: absent - <<: *aws_connection_info - register: result - - - name: assert state=absent (expected changed=true) - assert: - that: - - 'result.changed' - - 'not result.group_id' - when: not has_ec2_classic - - always: - # ============================================================ - - name: tidy up security group - ec2_group: - name: '{{ec2_group_name}}' - state: absent - <<: *aws_connection_info - ignore_errors: yes - - - name: tidy up security group for IPv6 EC2-Classic tests - ec2_group: - name: '{{ ec2_group_name }}-2' - state: absent - <<: *aws_connection_info - ignore_errors: yes - - - name: tidy up default VPC security group - ec2_group: - name: '{{ec2_group_name}}-default-vpc' - state: absent - <<: *aws_connection_info - ignore_errors: yes - - - name: tidy up automatically created SG - ec2_group: - name: "{{ resource_prefix }} - Another security group" - state: absent - <<: *aws_connection_info - ignore_errors: yes - - - name: tidy up VPC - ec2_vpc_net: - name: "{{ resource_prefix }}-vpc" - state: absent - cidr_block: "10.232.232.128/26" - <<: *aws_connection_info - ignore_errors: yes diff --git a/test/integration/targets/ec2_group/tasks/multi_account.yml b/test/integration/targets/ec2_group/tasks/multi_account.yml deleted file mode 100644 index d557938350..0000000000 --- a/test/integration/targets/ec2_group/tasks/multi_account.yml +++ /dev/null @@ -1,124 +0,0 @@ -- block: - - aws_caller_info: - register: caller_facts - - name: create a VPC - ec2_vpc_net: - name: "{{ resource_prefix }}-vpc-2" - state: present - cidr_block: "10.232.233.128/26" - tags: - Description: "Created by ansible-test" - register: vpc_result_2 - - name: Peer the secondary-VPC to the main VPC - ec2_vpc_peer: - vpc_id: '{{ vpc_result_2.vpc.id }}' - peer_vpc_id: '{{ vpc_result.vpc.id }}' - peer_owner_id: '{{ caller_facts.account }}' - peer_region: '{{ aws_region }}' - register: peer_origin - - name: Accept the secondary-VPC peering connection in the main VPC - ec2_vpc_peer: - peer_vpc_id: '{{ vpc_result_2.vpc.id }}' - vpc_id: '{{ vpc_result.vpc.id }}' - state: accept - peering_id: '{{ peer_origin.peering_id }}' - peer_owner_id: '{{ caller_facts.account }}' - peer_region: '{{ aws_region }}' - - name: Create group in second VPC - ec2_group: - name: '{{ ec2_group_name }}-external' - description: '{{ ec2_group_description }}' - vpc_id: '{{ vpc_result_2.vpc.id }}' - state: present - rules: - - proto: "tcp" - cidr_ip: 0.0.0.0/0 - ports: - - 80 - rule_desc: 'http whoo' - register: external - - name: Create group in internal VPC - ec2_group: - name: '{{ ec2_group_name }}-internal' - description: '{{ ec2_group_description }}' - vpc_id: '{{ vpc_result.vpc.id }}' - state: present - rules: - - proto: "tcp" - group_id: '{{ caller_facts.account }}/{{ external.group_id }}/{{ ec2_group_name }}-external' - ports: - - 80 - - name: Re-make same rule, expecting changed=false in internal VPC - ec2_group: - name: '{{ ec2_group_name }}-internal' - description: '{{ ec2_group_description }}' - vpc_id: '{{ vpc_result.vpc.id }}' - state: present - rules: - - proto: "tcp" - group_id: '{{ caller_facts.account }}/{{ external.group_id }}/{{ ec2_group_name }}-external' - ports: - - 80 - register: out - - assert: - that: - - out is not changed - - name: Try again with a bad group_id group in internal VPC - ec2_group: - name: '{{ ec2_group_name }}-internal' - description: '{{ ec2_group_description }}' - vpc_id: '{{ vpc_result.vpc.id }}' - state: present - rules: - - proto: "tcp" - group_id: '{{ external.group_id }}/{{ caller_facts.account }}/{{ ec2_group_name }}-external' - ports: - - 80 - register: out - ignore_errors: true - - assert: - that: - - out is failed - always: - - pause: seconds=5 - - name: Delete secondary-VPC side of peer - ec2_vpc_peer: - vpc_id: '{{ vpc_result_2.vpc.id }}' - peer_vpc_id: '{{ vpc_result.vpc.id }}' - peering_id: '{{ peer_origin.peering_id }}' - state: absent - peer_owner_id: '{{ caller_facts.account }}' - peer_region: '{{ aws_region }}' - ignore_errors: yes - - name: Delete main-VPC side of peer - ec2_vpc_peer: - peer_vpc_id: '{{ vpc_result_2.vpc.id }}' - vpc_id: '{{ vpc_result.vpc.id }}' - state: absent - peering_id: '{{ peer_origin.peering_id }}' - peer_owner_id: '{{ caller_facts.account }}' - peer_region: '{{ aws_region }}' - ignore_errors: yes - - name: Clean up group in second VPC - ec2_group: - name: '{{ ec2_group_name }}-external' - description: '{{ ec2_group_description }}' - state: absent - vpc_id: '{{ vpc_result_2.vpc.id }}' - ignore_errors: yes - - name: Clean up group in second VPC - ec2_group: - name: '{{ ec2_group_name }}-internal' - description: '{{ ec2_group_description }}' - state: absent - vpc_id: '{{ vpc_result.vpc.id }}' - ignore_errors: yes - - name: tidy up VPC - ec2_vpc_net: - name: "{{ resource_prefix }}-vpc-2" - state: absent - cidr_block: "10.232.233.128/26" - ignore_errors: yes - register: removed - retries: 10 - until: removed is not failed diff --git a/test/integration/targets/ec2_group/tasks/multi_nested_target.yml b/test/integration/targets/ec2_group/tasks/multi_nested_target.yml deleted file mode 100644 index 876f2a30a3..0000000000 --- a/test/integration/targets/ec2_group/tasks/multi_nested_target.yml +++ /dev/null @@ -1,230 +0,0 @@ ---- - - name: set up aws connection info - set_fact: - aws_connection_info: &aws_connection_info - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" - security_token: "{{ security_token }}" - region: "{{ aws_region }}" - no_log: yes - - # ============================================================ - - - name: test state=present for multiple ipv6 and ipv4 targets (expected changed=true) (CHECK MODE) - ec2_group: - name: '{{ ec2_group_name }}' - description: '{{ ec2_group_description }}' - state: present - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ipv6: - - "64:ff9b::/96" - - ["2620::/32"] - - proto: "tcp" - ports: 5665 - cidr_ip: - - 172.16.1.0/24 - - 172.16.17.0/24 - - ["10.0.0.0/24", "20.0.0.0/24"] - <<: *aws_connection_info - check_mode: true - register: result - - - name: assert state=present (expected changed=true) - assert: - that: - - 'result.changed' - - - name: test state=present for multiple ipv6 and ipv4 targets (expected changed=true) - ec2_group: - name: '{{ ec2_group_name }}' - description: '{{ ec2_group_description }}' - state: present - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ipv6: - - "64:ff9b::/96" - - ["2620::/32"] - - proto: "tcp" - ports: 5665 - cidr_ip: - - 172.16.1.0/24 - - 172.16.17.0/24 - - ["10.0.0.0/24", "20.0.0.0/24"] - <<: *aws_connection_info - register: result - - - name: assert state=present (expected changed=true) - assert: - that: - - 'result.changed' - - 'result.ip_permissions | length == 2' - - 'result.ip_permissions[0].ip_ranges | length == 4 or result.ip_permissions[1].ip_ranges | length == 4' - - 'result.ip_permissions[0].ipv6_ranges | length == 2 or result.ip_permissions[1].ipv6_ranges | length == 2' - - - name: test state=present for multiple ipv6 and ipv4 targets (expected changed=false) (CHECK MODE) - ec2_group: - name: '{{ ec2_group_name }}' - description: '{{ ec2_group_description }}' - state: present - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ipv6: - - "64:ff9b::/96" - - ["2620::/32"] - - proto: "tcp" - ports: 5665 - cidr_ip: - - 172.16.1.0/24 - - 172.16.17.0/24 - - ["10.0.0.0/24", "20.0.0.0/24"] - <<: *aws_connection_info - check_mode: true - register: result - - - name: assert state=present (expected changed=true) - assert: - that: - - 'not result.changed' - - - name: test state=present for multiple ipv6 and ipv4 targets (expected changed=false) - ec2_group: - name: '{{ ec2_group_name }}' - description: '{{ ec2_group_description }}' - state: present - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ipv6: - - "64:ff9b::/96" - - ["2620::/32"] - - proto: "tcp" - ports: 5665 - cidr_ip: - - 172.16.1.0/24 - - 172.16.17.0/24 - - ["10.0.0.0/24", "20.0.0.0/24"] - <<: *aws_connection_info - register: result - - - name: assert state=present (expected changed=true) - assert: - that: - - 'not result.changed' - - - name: test state=present purging a nested ipv4 target (expected changed=true) (CHECK MODE) - ec2_group: - name: '{{ ec2_group_name }}' - description: '{{ ec2_group_description }}' - state: present - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ipv6: - - "64:ff9b::/96" - - ["2620::/32"] - - proto: "tcp" - ports: 5665 - cidr_ip: - - 172.16.1.0/24 - - 172.16.17.0/24 - - ["10.0.0.0/24"] - <<: *aws_connection_info - check_mode: true - register: result - - - assert: - that: - - result.changed - - - name: test state=present purging a nested ipv4 target (expected changed=true) - ec2_group: - name: '{{ ec2_group_name }}' - description: '{{ ec2_group_description }}' - state: present - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ipv6: - - "64:ff9b::/96" - - ["2620::/32"] - - proto: "tcp" - ports: 5665 - cidr_ip: - - 172.16.1.0/24 - - 172.16.17.0/24 - - ["10.0.0.0/24"] - <<: *aws_connection_info - register: result - - - assert: - that: - - result.changed - - 'result.ip_permissions[0].ip_ranges | length == 3 or result.ip_permissions[1].ip_ranges | length == 3' - - 'result.ip_permissions[0].ipv6_ranges | length == 2 or result.ip_permissions[1].ipv6_ranges | length == 2' - - - name: test state=present with both associated ipv6 targets nested (expected changed=false) - ec2_group: - name: '{{ ec2_group_name }}' - description: '{{ ec2_group_description }}' - state: present - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ipv6: - - ["2620::/32", "64:ff9b::/96"] - - proto: "tcp" - ports: 5665 - cidr_ip: - - 172.16.1.0/24 - - 172.16.17.0/24 - - ["10.0.0.0/24"] - <<: *aws_connection_info - register: result - - - assert: - that: - - not result.changed - - - name: test state=present add another nested ipv6 target (expected changed=true) - ec2_group: - name: '{{ ec2_group_name }}' - description: '{{ ec2_group_description }}' - state: present - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - cidr_ipv6: - - ["2620::/32", "64:ff9b::/96"] - - ["2001:DB8:A0B:12F0::1/64"] - - proto: "tcp" - ports: 5665 - cidr_ip: - - 172.16.1.0/24 - - 172.16.17.0/24 - - ["10.0.0.0/24"] - <<: *aws_connection_info - register: result - - - assert: - that: - - result.changed - - 'result.ip_permissions[0].ip_ranges | length == 3 or result.ip_permissions[1].ip_ranges | length == 3' - - 'result.ip_permissions[0].ipv6_ranges | length == 3 or result.ip_permissions[1].ipv6_ranges | length == 3' - - - name: delete it - ec2_group: - name: '{{ ec2_group_name }}' - state: absent - <<: *aws_connection_info diff --git a/test/integration/targets/ec2_group/tasks/numeric_protos.yml b/test/integration/targets/ec2_group/tasks/numeric_protos.yml deleted file mode 100644 index ba4f7e90dc..0000000000 --- a/test/integration/targets/ec2_group/tasks/numeric_protos.yml +++ /dev/null @@ -1,71 +0,0 @@ ---- -- block: - - name: set up aws connection info - set_fact: - group_tmp_name: '{{ec2_group_name}}-numbered-protos' - aws_connection_info: &aws_connection_info - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" - security_token: "{{ security_token }}" - region: "{{ aws_region }}" - no_log: yes - - - name: Create a group with numbered protocol (GRE) - ec2_group: - name: '{{ group_tmp_name }}' - vpc_id: '{{ vpc_result.vpc.id }}' - description: '{{ ec2_group_description }}' - rules: - - proto: 47 - to_port: -1 - from_port: -1 - cidr_ip: 0.0.0.0/0 - <<: *aws_connection_info - state: present - register: result - - - name: Create a group with a quoted proto - ec2_group: - name: '{{ group_tmp_name }}' - vpc_id: '{{ vpc_result.vpc.id }}' - description: '{{ ec2_group_description }}' - rules: - - proto: '47' - to_port: -1 - from_port: -1 - cidr_ip: 0.0.0.0/0 - <<: *aws_connection_info - state: present - register: result - - assert: - that: - - result is not changed - - name: Add a tag with a numeric value - ec2_group: - name: '{{ group_tmp_name }}' - vpc_id: '{{ vpc_result.vpc.id }}' - description: '{{ ec2_group_description }}' - tags: - foo: 1 - <<: *aws_connection_info - - name: Read a tag with a numeric value - ec2_group: - name: '{{ group_tmp_name }}' - vpc_id: '{{ vpc_result.vpc.id }}' - description: '{{ ec2_group_description }}' - tags: - foo: 1 - <<: *aws_connection_info - register: result - - assert: - that: - - result is not changed - - always: - - name: tidy up egress rule test security group - ec2_group: - name: '{{group_tmp_name}}' - state: absent - vpc_id: '{{ vpc_result.vpc.id }}' - <<: *aws_connection_info - ignore_errors: yes diff --git a/test/integration/targets/ec2_group/tasks/rule_group_create.yml b/test/integration/targets/ec2_group/tasks/rule_group_create.yml deleted file mode 100644 index 465bdc569f..0000000000 --- a/test/integration/targets/ec2_group/tasks/rule_group_create.yml +++ /dev/null @@ -1,132 +0,0 @@ ---- -- block: - - name: set up aws connection info - set_fact: - aws_connection_info: &aws_connection_info - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" - security_token: "{{ security_token }}" - region: "{{ aws_region }}" - no_log: yes - - - name: Create a group with self-referring rule - ec2_group: - name: '{{ec2_group_name}}-auto-create-1' - vpc_id: '{{ vpc_result.vpc.id }}' - description: '{{ec2_group_description}}' - rules: - - proto: "tcp" - from_port: 8000 - to_port: 8100 - group_name: '{{ec2_group_name}}-auto-create-1' - <<: *aws_connection_info - state: present - register: result - - - name: Create a second group rule - ec2_group: - name: '{{ec2_group_name}}-auto-create-2' - vpc_id: '{{ vpc_result.vpc.id }}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - state: present - - - name: Create a series of rules with a recently created group as target - ec2_group: - name: '{{ec2_group_name}}-auto-create-1' - vpc_id: '{{ vpc_result.vpc.id }}' - description: '{{ec2_group_description}}' - purge_rules: false - rules: - - proto: "tcp" - from_port: "{{ item }}" - to_port: "{{ item }}" - group_name: '{{ec2_group_name}}-auto-create-2' - <<: *aws_connection_info - state: present - register: result - with_items: - - 20 - - 40 - - 60 - - 80 - - - name: Create a group with only the default rule - ec2_group: - name: '{{ec2_group_name}}-auto-create-1' - vpc_id: '{{ vpc_result.vpc.id }}' - description: '{{ec2_group_description}}' - rules: - - proto: "tcp" - from_port: 8182 - to_port: 8182 - group_name: '{{ec2_group_name}}-auto-create-3' - <<: *aws_connection_info - state: present - register: result - ignore_errors: true - - - name: assert you can't create a new group from a rule target with no description - assert: - that: - - result is failed - - - name: Create a group with a target of a separate group - ec2_group: - name: '{{ec2_group_name}}-auto-create-1' - vpc_id: '{{ vpc_result.vpc.id }}' - description: '{{ec2_group_description}}' - rules: - - proto: tcp - ports: - - 22 - - 80 - group_name: '{{ec2_group_name}}-auto-create-3' - group_desc: '{{ec2_group_description}}' - <<: *aws_connection_info - state: present - register: result - - - name: Create a 4th group - ec2_group: - name: '{{ec2_group_name}}-auto-create-4' - vpc_id: '{{ vpc_result.vpc.id }}' - description: '{{ec2_group_description}}' - <<: *aws_connection_info - state: present - rules: - - proto: tcp - ports: - - 22 - cidr_ip: 0.0.0.0/0 - - - name: use recently created group in a rule - ec2_group: - name: '{{ec2_group_name}}-auto-create-5' - vpc_id: '{{ vpc_result.vpc.id }}' - description: '{{ec2_group_description}}' - rules: - - proto: tcp - ports: - - 443 - group_name: '{{ec2_group_name}}-auto-create-4' - <<: *aws_connection_info - state: present - - always: - - name: tidy up egress rule test security group - ec2_group: - name: '{{ec2_group_name}}-auto-create-{{ item }}' - state: absent - vpc_id: '{{ vpc_result.vpc.id }}' - <<: *aws_connection_info - ignore_errors: yes - with_items: [5, 4, 3, 2, 1] - - name: tidy up egress rule test security group - ec2_group: - name: '{{ec2_group_name}}-auto-create-{{ item }}' - state: absent - vpc_id: '{{ vpc_result.vpc.id }}' - <<: *aws_connection_info - ignore_errors: yes - with_items: [1, 2, 3, 4, 5] diff --git a/test/integration/targets/ec2_key/aliases b/test/integration/targets/ec2_key/aliases deleted file mode 100644 index 6e3860bee2..0000000000 --- a/test/integration/targets/ec2_key/aliases +++ /dev/null @@ -1,2 +0,0 @@ -cloud/aws -shippable/aws/group2 diff --git a/test/integration/targets/ec2_key/defaults/main.yml b/test/integration/targets/ec2_key/defaults/main.yml deleted file mode 100644 index df0082d999..0000000000 --- a/test/integration/targets/ec2_key/defaults/main.yml +++ /dev/null @@ -1,3 +0,0 @@ ---- -# defaults file for test_ec2_key -ec2_key_name: '{{resource_prefix}}' diff --git a/test/integration/targets/ec2_key/meta/main.yml b/test/integration/targets/ec2_key/meta/main.yml deleted file mode 100644 index 45f0cedf8e..0000000000 --- a/test/integration/targets/ec2_key/meta/main.yml +++ /dev/null @@ -1,4 +0,0 @@ -dependencies: - - prepare_tests - - setup_sshkey - - setup_ec2 diff --git a/test/integration/targets/ec2_key/tasks/main.yml b/test/integration/targets/ec2_key/tasks/main.yml deleted file mode 100644 index c39bc5385d..0000000000 --- a/test/integration/targets/ec2_key/tasks/main.yml +++ /dev/null @@ -1,164 +0,0 @@ ---- -# 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 -# -# TODO - name: test 'validate_certs' parameter -# TODO - name: test creating key pair with another_key_material with force=yes -# ============================================================ -# - include: ../../setup_ec2/tasks/common.yml module_name=ec2_key - -- block: - - # ============================================================ - - name: test with no parameters - ec2_key: - register: result - ignore_errors: true - - - name: assert failure when called with no parameters - assert: - that: - - 'result.failed' - - 'result.msg == "missing required arguments: name"' - - # ============================================================ - - name: test removing a non-existent key pair - ec2_key: - name='{{ec2_key_name}}' - ec2_region={{ec2_region}} - ec2_access_key={{ec2_access_key}} - ec2_secret_key={{ec2_secret_key}} - security_token={{security_token}} - state=absent - register: result - - - name: assert removing a non-existent key pair - assert: - that: - - 'not result.changed' - - # ============================================================ - - name: test creating a new key pair - ec2_key: - name='{{ec2_key_name}}' - ec2_region={{ec2_region}} - ec2_access_key={{ec2_access_key}} - ec2_secret_key={{ec2_secret_key}} - security_token={{security_token}} - state=present - register: result - - - name: assert creating a new key pair - assert: - that: - - 'result.changed' - - '"key" in result' - - '"name" in result.key' - - '"fingerprint" in result.key' - - '"private_key" in result.key' - - 'result.key.name == "{{ec2_key_name}}"' - - # ============================================================ - - name: test removing an existent key - ec2_key: - name='{{ec2_key_name}}' - state=absent - environment: - EC2_REGION: '{{ec2_region}}' - EC2_ACCESS_KEY: '{{ec2_access_key}}' - EC2_SECRET_KEY: '{{ec2_secret_key}}' - EC2_SECURITY_TOKEN: '{{security_token|default("")}}' - register: result - - - name: assert removing an existent key - assert: - that: - - 'result.changed' - - '"key" in result' - - 'result.key == None' - - # ============================================================ - - name: test state=present with key_material - ec2_key: - name='{{ec2_key_name}}' - key_material='{{key_material}}' - state=present - environment: - EC2_REGION: '{{ec2_region}}' - EC2_ACCESS_KEY: '{{ec2_access_key}}' - EC2_SECRET_KEY: '{{ec2_secret_key}}' - EC2_SECURITY_TOKEN: '{{security_token|default("")}}' - register: result - - - name: assert state=present with key_material - assert: - that: - - 'result.changed == True' - - '"key" in result' - - '"name" in result.key' - - '"fingerprint" in result.key' - - '"private_key" not in result.key' - - 'result.key.name == "{{ec2_key_name}}"' - - 'result.key.fingerprint == "{{fingerprint}}"' - - # ============================================================ - - - name: test force=no with another_key_material (expect changed=false) - ec2_key: - name: '{{ ec2_key_name }}' - ec2_region: '{{ ec2_region }}' - ec2_access_key: '{{ ec2_access_key }}' - ec2_secret_key: '{{ ec2_secret_key }}' - security_token: '{{ security_token }}' - key_material: '{{ another_key_material }}' - force: no - register: result - - - name: assert force=no with another_key_material (expect changed=false) - assert: - that: - - 'not result.changed' - - 'result.key.fingerprint == "{{ fingerprint }}"' - - # ============================================================ - - - name: test updating a key pair using another_key_material (expect changed=True) - ec2_key: - name: '{{ ec2_key_name }}' - ec2_region: '{{ ec2_region }}' - ec2_access_key: '{{ ec2_access_key }}' - ec2_secret_key: '{{ ec2_secret_key }}' - security_token: '{{ security_token }}' - key_material: '{{ another_key_material }}' - register: result - - - name: assert updating a key pair using another_key_material (expect changed=True) - assert: - that: - - 'result.changed' - - 'result.key.fingerprint != "{{ fingerprint }}"' - - # ============================================================ - - always: - - # ============================================================ - - name: test state=absent (expect changed=true) - ec2_key: - name='{{ec2_key_name}}' - ec2_region='{{ec2_region}}' - ec2_access_key='{{ec2_access_key}}' - ec2_secret_key='{{ec2_secret_key}}' - security_token='{{security_token}}' - state=absent - register: result - - - name: assert state=absent with key_material (expect changed=true) - assert: - that: - - 'result.changed' - - '"key" in result' - - 'result.key == None' diff --git a/test/integration/targets/ec2_metadata_facts/aliases b/test/integration/targets/ec2_metadata_facts/aliases deleted file mode 100644 index f9961d9a2f..0000000000 --- a/test/integration/targets/ec2_metadata_facts/aliases +++ /dev/null @@ -1,3 +0,0 @@ -cloud/aws -shippable/aws/group2 -shippable/aws/smoketest diff --git a/test/integration/targets/ec2_metadata_facts/defaults/main.yml b/test/integration/targets/ec2_metadata_facts/defaults/main.yml deleted file mode 100644 index c25743914a..0000000000 --- a/test/integration/targets/ec2_metadata_facts/defaults/main.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -# defaults file for test_ec2_facts diff --git a/test/integration/targets/ec2_metadata_facts/meta/main.yml b/test/integration/targets/ec2_metadata_facts/meta/main.yml deleted file mode 100644 index 1f64f1169a..0000000000 --- a/test/integration/targets/ec2_metadata_facts/meta/main.yml +++ /dev/null @@ -1,3 +0,0 @@ -dependencies: - - prepare_tests - - setup_ec2 diff --git a/test/integration/targets/ec2_metadata_facts/tasks/main.yml b/test/integration/targets/ec2_metadata_facts/tasks/main.yml deleted file mode 100644 index 8ea9fcf1f8..0000000000 --- a/test/integration/targets/ec2_metadata_facts/tasks/main.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -# tasks file for test_ec2_facts diff --git a/test/integration/targets/ec2_metadata_facts/vars/main.yml b/test/integration/targets/ec2_metadata_facts/vars/main.yml deleted file mode 100644 index bb8f6c1875..0000000000 --- a/test/integration/targets/ec2_metadata_facts/vars/main.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -# vars file for test_ec2_facts diff --git a/test/integration/targets/ec2_snapshot/aliases b/test/integration/targets/ec2_snapshot/aliases deleted file mode 100644 index 1dcb36b283..0000000000 --- a/test/integration/targets/ec2_snapshot/aliases +++ /dev/null @@ -1,3 +0,0 @@ -cloud/aws -shippable/aws/group4 -ec2_snapshot_info diff --git a/test/integration/targets/ec2_snapshot/defaults/main.yml b/test/integration/targets/ec2_snapshot/defaults/main.yml deleted file mode 100644 index dc1f0f703d..0000000000 --- a/test/integration/targets/ec2_snapshot/defaults/main.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -# defaults file for ec2_snapshot diff --git a/test/integration/targets/ec2_snapshot/tasks/main.yml b/test/integration/targets/ec2_snapshot/tasks/main.yml deleted file mode 100644 index b8cdec3045..0000000000 --- a/test/integration/targets/ec2_snapshot/tasks/main.yml +++ /dev/null @@ -1,256 +0,0 @@ ---- -# Tests for EC2 Snapshot -# -# Tests ec2_snapshot: -# - Snapshot creation -# - Create with last_snapshot_min_age -# - Snapshot deletion -# -# Tests ec2_snapshot_info: -# - Listing snapshots for filter: tag -# -# Possible Bugs: -# - check_mode not supported -# -- name: Integration testing for ec2_snapshot - module_defaults: - group/aws: - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" - security_token: "{{ security_token | default(omit) }}" - region: "{{ aws_region }}" - - block: - - ec2_ami_info: - owners: amazon - filters: - architecture: x86_64 - virtualization-type: hvm - root-device-type: ebs - name: "amzn-ami-hvm*" - register: amis - - - name: Setup an instance for testing - ec2_instance: - name: '{{ resource_prefix }}' - instance_type: t2.nano - image_id: "{{ (amis.images | sort(attribute='creation_date') | last).image_id }}" - wait: yes - volumes: - - device_name: /dev/xvda - ebs: - volume_size: 8 - delete_on_termination: true - register: instance - - - set_fact: - volume_id: '{{ instance.instances[0].block_device_mappings[0].ebs.volume_id }}' - instance_id: '{{ instance.instances[0].instance_id }}' - device_name: '{{ instance.instances[0].block_device_mappings[0].device_name }}' - -# JR: Check mode not supported -# - name: Take snapshot (check mode) -# ec2_snapshot: -# instance_id: '{{ instance_id }}' -# check_mode: true -# snapshot_tags: -# Test: '{{ resource_prefix }}' -# register: result -# - assert: -# that: -# - result is changed - - - name: Take snapshot of volume - ec2_snapshot: - volume_id: '{{ volume_id }}' - register: result - - # The Name tag is created automatically as the instance_name; ie the resource_prefix - - name: Get info about snapshots - ec2_snapshot_info: - filters: - "tag:Name": '{{ resource_prefix }}' - register: info_result - - - assert: - that: - - result is changed - - info_result.snapshots| length == 1 - - info_result.snapshots[0].snapshot_id == result.snapshot_id - - info_result.snapshots[0].volume_id == result.volume_id - - info_result.snapshots[0].volume_size == result.volume_size - - info_result.snapshots[0].tags == result.tags - -# JR: Check mode not supported -# - name: Take snapshot if most recent >1hr (False) (check mode) -# ec2_snapshot: -# volume_id: '{{ volume_id }}' -# snapshot_tags: -# Name: '{{ resource_prefix }}' -# last_snapshot_min_age: 60 -# check_mode: true -# register: result -# - assert: -# that: -# - result is not changed - - - name: Take snapshot if most recent >1hr (False) - ec2_snapshot: - volume_id: '{{ volume_id }}' - last_snapshot_min_age: 60 - register: result - - - name: Get info about snapshots - ec2_snapshot_info: - filters: - "tag:Name": '{{ resource_prefix }}' - register: info_result - - - assert: - that: - - result is not changed - - info_result.snapshots| length == 1 - - - name: Pause so we can do a last_snapshot_min_age test - pause: - minutes: 1 - -# JR: Check mode not supported -# - name: Take snapshot if most recent >1min (True) (check mode) -# ec2_snapshot: -# volume_id: '{{ volume_id }}' -# snapshot_tags: -# Name: '{{ resource_prefix }}' -# last_snapshot_min_age: 1 -# check_mode: true -# register: result -# - assert: -# that: -# - result is changed - - - name: Take snapshot if most recent >1min (True) - ec2_snapshot: - volume_id: '{{ volume_id }}' - last_snapshot_min_age: 1 - register: result - - - name: Get info about snapshots - ec2_snapshot_info: - filters: - "tag:Name": '{{ resource_prefix }}' - register: info_result - - - assert: - that: - - result is changed - - info_result.snapshots| length == 2 - - '"{{ result.snapshot_id }}" in "{{ info_result| json_query("snapshots[].snapshot_id") }}"' - -# JR: Check mode not supported -# - name: Take snapshot with a tag (check mode) -# ec2_snapshot: -# volume_id: '{{ volume_id }}' -# snapshot_tags: -# MyTag: '{{ resource_prefix }}' -# register: result -# - assert: -# that: -# - result is changed - - # Wait at least 15 seconds between concurrent volume snapshots. - - name: Prevent SnapshotCreationPerVolumeRateExceeded errors - pause: - seconds: 15 - - - name: Take snapshot and tag it - ec2_snapshot: - volume_id: '{{ volume_id }}' - snapshot_tags: - MyTag: '{{ resource_prefix }}' - register: tagged_result - - - name: Get info about snapshots by tag - ec2_snapshot_info: - filters: - "tag:MyTag": '{{ resource_prefix }}' - register: tag_info_result - - - set_fact: - tagged_snapshot_id: '{{ tag_info_result.snapshots[0].snapshot_id }}' - - - assert: - that: - - tagged_result is changed - - tagged_result.tags| length == 2 - - tag_info_result.snapshots| length == 1 - - tagged_result.tags.MyTag == "{{ resource_prefix }}" - - '"{{ tagged_result.snapshot_id }}" == "{{ tagged_snapshot_id }}"' - - - name: Get info about all snapshots for this test - ec2_snapshot_info: - filters: - "tag:Name": '{{ resource_prefix }}' - register: info_result - - - assert: - that: - - info_result.snapshots| length == 3 - - - name: Delete the tagged snapshot - ec2_snapshot: - state: absent - snapshot_id: '{{ tagged_snapshot_id }}' - - - name: Get info about all snapshots for this test - ec2_snapshot_info: - filters: - "tag:Name": '{{ resource_prefix }}' - register: info_result - - - assert: - that: - - info_result.snapshots| length == 2 - - '"{{ tagged_snapshot_id }}" not in "{{ info_result| json_query("snapshots[].snapshot_id") }}"' - - - name: Delete snapshots - ec2_snapshot: - state: absent - snapshot_id: '{{ item.snapshot_id }}' - with_items: '{{ info_result.snapshots }}' - - - name: Get info about all snapshots for this test - ec2_snapshot_info: - filters: - "tag:Name": '{{ resource_prefix }}' - register: info_result - - - assert: - that: - - info_result.snapshots| length == 0 - - always: - - - name: Snapshots to delete - ec2_snapshot_info: - filters: - "tag:Name": '{{ resource_prefix }}' - register: tagged_snapshots - - - name: Delete tagged snapshots - ec2_snapshot: - state: absent - snapshot_id: '{{ item.snapshot_id }}' - with_items: '{{ tagged_snapshots.snapshots }}' - ignore_errors: true - - - name: Delete instance - ec2_instance: - instance_ids: '{{ instance_id }}' - state: absent - ignore_errors: true - - - name: Delete volume - ec2_vol: - id: '{{ volume_id }}' - state: absent - ignore_errors: true
\ No newline at end of file diff --git a/test/integration/targets/ec2_tag/aliases b/test/integration/targets/ec2_tag/aliases deleted file mode 100644 index be56eee894..0000000000 --- a/test/integration/targets/ec2_tag/aliases +++ /dev/null @@ -1,3 +0,0 @@ -cloud/aws -shippable/aws/group2 -ec2_tag_info diff --git a/test/integration/targets/ec2_tag/defaults/main.yml b/test/integration/targets/ec2_tag/defaults/main.yml deleted file mode 100644 index 6aa39c7360..0000000000 --- a/test/integration/targets/ec2_tag/defaults/main.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -# defaults file for test_ec2_tag diff --git a/test/integration/targets/ec2_tag/meta/main.yml b/test/integration/targets/ec2_tag/meta/main.yml deleted file mode 100644 index 1f64f1169a..0000000000 --- a/test/integration/targets/ec2_tag/meta/main.yml +++ /dev/null @@ -1,3 +0,0 @@ -dependencies: - - prepare_tests - - setup_ec2 diff --git a/test/integration/targets/ec2_tag/tasks/main.yml b/test/integration/targets/ec2_tag/tasks/main.yml deleted file mode 100644 index 7e8cd8d128..0000000000 --- a/test/integration/targets/ec2_tag/tasks/main.yml +++ /dev/null @@ -1,144 +0,0 @@ ---- -# tasks file for test_ec2_tag -- name: Set up AWS connection info - module_defaults: - group/aws: - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" - security_token: "{{ security_token | default(omit) }}" - region: "{{ aws_region }}" - block: - - name: Create an EC2 volume so we have something to tag - ec2_vol: - name: "{{ resource_prefix }} ec2_tag volume" - volume_size: 1 - state: present - zone: "{{ aws_region }}a" - register: volume - - - name: List the tags on the volume (ec2_tag) - ec2_tag: - resource: "{{ volume.volume_id }}" - state: list - register: result - - name: List the tags on the volume (ec2_tag_info) - ec2_tag_info: - resource: "{{ volume.volume_id }}" - register: result_info - - - assert: - that: - - result.tags | length == 1 - - result.tags.Name == '{{ resource_prefix }} ec2_tag volume' - - result_info.tags | length == 1 - - result_info.tags.Name == '{{ resource_prefix }} ec2_tag volume' - - - name: Set some new tags on the volume - ec2_tag: - resource: "{{ volume.volume_id }}" - state: present - tags: - foo: foo - bar: baz - baz: also baz - register: result - - name: List the new tags on the volume - ec2_tag_info: - resource: "{{ volume.volume_id }}" - register: result_info - - - assert: - that: - - result is changed - - result.tags | length == 4 - - result.added_tags | length == 3 - - result.tags.Name == '{{ resource_prefix }} ec2_tag volume' - - result.tags.foo == 'foo' - - result.tags.bar == 'baz' - - result.tags.baz == 'also baz' - - result_info.tags | length == 4 - - result_info.tags.Name == '{{ resource_prefix }} ec2_tag volume' - - result_info.tags.foo == 'foo' - - result_info.tags.bar == 'baz' - - result_info.tags.baz == 'also baz' - - - name: Remove a tag by name - ec2_tag: - resource: "{{ volume.volume_id }}" - state: absent - tags: - baz: - register: result - - - assert: - that: - - result is changed - - result.removed_tags | length == 1 - - "'baz' in result.removed_tags" - - - name: Don't remove a tag - ec2_tag: - resource: "{{ volume.volume_id }}" - state: absent - tags: - foo: baz - register: result - - - assert: - that: - - result is not changed - - - name: Remove a tag - ec2_tag: - resource: "{{ volume.volume_id }}" - state: absent - tags: - foo: foo - register: result - - - assert: - that: - - result is changed - - result.tags | length == 2 - - "'added_tags' not in result" - - result.removed_tags | length == 1 - - result.tags.Name == '{{ resource_prefix }} ec2_tag volume' - - result.tags.bar == 'baz' - - - name: Set an exclusive tag - ec2_tag: - resource: "{{ volume.volume_id }}" - purge_tags: true - tags: - baz: quux - register: result - - - assert: - that: - - result is changed - - result.tags | length == 1 - - result.added_tags | length == 1 - - result.removed_tags | length == 2 - - result.tags.baz == 'quux' - - - name: Remove all tags - ec2_tag: - resource: "{{ volume.volume_id }}" - purge_tags: true - tags: {} - register: result - - - assert: - that: - - result is changed - - result.tags | length == 0 - - always: - - name: Remove the volume - ec2_vol: - id: "{{ volume.volume_id }}" - state: absent - register: result - until: result is not failed - ignore_errors: yes - retries: 10 diff --git a/test/integration/targets/ec2_tag/vars/main.yml b/test/integration/targets/ec2_tag/vars/main.yml deleted file mode 100644 index c2d0654aef..0000000000 --- a/test/integration/targets/ec2_tag/vars/main.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -# vars file for test_ec2_tag diff --git a/test/integration/targets/ec2_vol/aliases b/test/integration/targets/ec2_vol/aliases deleted file mode 100644 index 157ce0c9d4..0000000000 --- a/test/integration/targets/ec2_vol/aliases +++ /dev/null @@ -1,2 +0,0 @@ -cloud/aws -shippable/aws/group3 diff --git a/test/integration/targets/ec2_vol/defaults/main.yml b/test/integration/targets/ec2_vol/defaults/main.yml deleted file mode 100644 index eb2594bc99..0000000000 --- a/test/integration/targets/ec2_vol/defaults/main.yml +++ /dev/null @@ -1,5 +0,0 @@ -vpc_name: '{{ resource_prefix }}-vpc' -vpc_seed: '{{ resource_prefix }}' -vpc_cidr: '10.{{ 256 | random(seed=vpc_seed) }}.0.0/16' -subnet_cidr: '10.{{ 256 | random(seed=vpc_seed) }}.32.0/24' -ec2_ami_name: 'amzn2-ami-hvm-2.*-x86_64-gp2'
\ No newline at end of file diff --git a/test/integration/targets/ec2_vol/tasks/main.yml b/test/integration/targets/ec2_vol/tasks/main.yml deleted file mode 100644 index aa81248e30..0000000000 --- a/test/integration/targets/ec2_vol/tasks/main.yml +++ /dev/null @@ -1,373 +0,0 @@ ---- - -- module_defaults: - group/aws: - aws_access_key: '{{ aws_access_key | default(omit) }}' - aws_secret_key: '{{ aws_secret_key | default(omit) }}' - security_token: '{{ security_token | default(omit) }}' - region: '{{ aws_region | default(omit) }}' - - block: - - # ==== Env setup ========================================================== - - name: list available AZs - aws_az_info: - register: region_azs - - - name: pick an AZ for testing - set_fact: - availability_zone: "{{ region_azs.availability_zones[0].zone_name }}" - - - name: Create a test VPC - ec2_vpc_net: - name: "{{ vpc_name }}" - cidr_block: "{{ vpc_cidr }}" - tags: - Name: ec2_vol testing - ResourcePrefix: "{{ resource_prefix }}" - register: testing_vpc - - - name: Create a test subnet - ec2_vpc_subnet: - vpc_id: "{{ testing_vpc.vpc.id }}" - cidr: "{{ subnet_cidr }}" - tags: - Name: ec2_vol testing - ResourcePrefix: "{{ resource_prefix }}" - az: '{{ availability_zone }}' - register: testing_subnet - - - name: Find AMI to use - ec2_ami_info: - owners: 'amazon' - filters: - name: '{{ ec2_ami_name }}' - register: ec2_amis - - - name: Set fact with latest AMI - vars: - latest_ami: '{{ ec2_amis.images | sort(attribute="creation_date") | last }}' - set_fact: - ec2_ami_image: '{{ latest_ami.image_id }}' - - # ==== ec2_vol tests =============================================== - - - name: create a volume (validate module defaults) - ec2_vol: - volume_size: 1 - zone: "{{ availability_zone }}" - tags: - ResourcePrefix: "{{ resource_prefix }}" - register: volume1 - - - name: check task return attributes - assert: - that: - - volume1.changed - - "'volume' in volume1" - - "'volume_id' in volume1" - - "'volume_type' in volume1" - - "'device' in volume1" - - "volume1.volume.status == 'available'" - - "volume1.volume_type == 'standard'" - - "'attachment_set' in volume1.volume and 'instance_id' in volume1.volume.attachment_set and not volume1.volume.attachment_set.instance_id" - - "not volume1.volume.encrypted" - - # no idempotency check needed here - - - name: create another volume (override module defaults) - ec2_vol: - encrypted: yes - volume_size: 4 - volume_type: io1 - iops: 101 - name: "{{ resource_prefix }}" - tags: - ResourcePrefix: "{{ resource_prefix }}" - zone: "{{ availability_zone }}" - register: volume2 - - - name: check task return attributes - assert: - that: - - volume2.changed - - "'volume' in volume2" - - "'volume_id' in volume2" - - "'volume_type' in volume2" - - "'device' in volume2" - - "volume2.volume.status == 'available'" - - "volume2.volume_type == 'io1'" - - "volume2.volume.iops == 101" - - "volume2.volume.size == 4" - - "volume2.volume.encrypted" - - - name: create another volume (override module defaults) (idempotent) - ec2_vol: - encrypted: yes - volume_size: 4 - volume_type: io1 - iops: 101 - name: "{{ resource_prefix }}" - tags: - ResourcePrefix: "{{ resource_prefix }}" - zone: "{{ availability_zone }}" - register: volume2_idem - - - name: check task return attributes - assert: - that: - - not volume2_idem.changed - - - name: create snapshot from volume - ec2_snapshot: - volume_id: "{{ volume1.volume_id }}" - description: "Resource Prefix - {{ resource_prefix }}" - snapshot_tags: - ResourcePrefix: "{{ resource_prefix }}" - register: vol1_snapshot - - - name: check task return attributes - assert: - that: - - vol1_snapshot.changed - - - name: create a volume from a snapshot - ec2_vol: - snapshot: "{{ vol1_snapshot.snapshot_id }}" - encrypted: yes - volume_type: gp2 - volume_size: 1 - zone: "{{ availability_zone }}" - tags: - ResourcePrefix: "{{ resource_prefix }}" - register: volume3 - - - name: check task return attributes - assert: - that: - - volume3.changed - - "volume3.volume.snapshot_id == vol1_snapshot.snapshot_id" - - - name: create an ec2 instance - ec2_instance: - name: "{{ resource_prefix }}" - vpc_subnet_id: "{{ testing_subnet.subnet.id }}" - instance_type: t3.nano - image_id: "{{ ec2_ami_image }}" - tags: - ResourcePrefix: "{{ resource_prefix }}" - register: test_instance - - - name: check task return attributes - assert: - that: - - test_instance.changed - - - name: attach existing volume to an instance - ec2_vol: - id: "{{ volume1.volume_id }}" - instance: "{{ test_instance.instance_ids[0] }}" - device_name: /dev/sdg - delete_on_termination: no - register: vol_attach_result - - - name: check task return attributes - assert: - that: - - "vol_attach_result.changed" - - "'device' in vol_attach_result and vol_attach_result.device == '/dev/sdg'" - - "'volume' in vol_attach_result" - - "vol_attach_result.volume.attachment_set.status == 'attached'" - - "vol_attach_result.volume.attachment_set.instance_id == test_instance.instance_ids[0]" - - "vol_attach_result.volume.attachment_set.device == '/dev/sdg'" - -# Failing -# - "vol_attach_result.volume.attachment_set.deleteOnTermination" - - - name: attach existing volume to an instance (idempotent) - ec2_vol: - id: "{{ volume1.volume_id }}" - instance: "{{ test_instance.instance_ids[0] }}" - device_name: /dev/sdg - delete_on_termination: no - register: vol_attach_result - - - name: check task return attributes - assert: - that: - - "not vol_attach_result.changed" - - - name: attach a new volume to an instance - ec2_vol: - instance: "{{ test_instance.instance_ids[0] }}" - device_name: /dev/sdh - volume_size: 1 - volume_type: gp2 - tags: - ResourcePrefix: "{{ resource_prefix }}" - register: new_vol_attach_result - - - name: check task return attributes - assert: - that: - - "new_vol_attach_result.changed" - - "'device' in new_vol_attach_result and new_vol_attach_result.device == '/dev/sdh'" - - "'volume' in new_vol_attach_result" - - "new_vol_attach_result.volume.attachment_set.status == 'attached'" - - "new_vol_attach_result.volume.attachment_set.instance_id == test_instance.instance_ids[0]" - - "new_vol_attach_result.volume.attachment_set.device == '/dev/sdh'" - - - name: attach a new volume to an instance (idempotent) - ec2_vol: - instance: "{{ test_instance.instance_ids[0] }}" - device_name: /dev/sdh - volume_size: 1 - volume_type: gp2 - tags: - ResourcePrefix: "{{ resource_prefix }}" - register: new_vol_attach_result_idem - - - name: check task return attributes - assert: - that: - - "not new_vol_attach_result_idem.changed" - - "'Volume mapping for /dev/sdh already exists' in new_vol_attach_result_idem.msg" - - - name: create a volume from a snapshot and attach to the instance - ec2_vol: - instance: "{{ test_instance.instance_ids[0] }}" - device_name: /dev/sdi - snapshot: "{{ vol1_snapshot.snapshot_id }}" - tags: - ResourcePrefix: "{{ resource_prefix }}" - register: attach_new_vol_from_snapshot_result - - - name: check task return attributes - assert: - that: - - "attach_new_vol_from_snapshot_result.changed" - - "'device' in attach_new_vol_from_snapshot_result and attach_new_vol_from_snapshot_result.device == '/dev/sdi'" - - "'volume' in attach_new_vol_from_snapshot_result" - - "attach_new_vol_from_snapshot_result.volume.attachment_set.status == 'attached'" - - "attach_new_vol_from_snapshot_result.volume.attachment_set.instance_id == test_instance.instance_ids[0]" - - - name: list volumes attached to instance - ec2_vol: - instance: "{{ test_instance.instance_ids[0] }}" - state: list - register: inst_vols - - - name: check task return attributes - assert: - that: - - "not inst_vols.changed" - - "'volumes' in inst_vols" - - "inst_vols.volumes | length == 4" - - - name: get info on ebs volumes - ec2_vol_info: - register: ec2_vol_info - - - name: check task return attributes - assert: - that: - - "not ec2_vol_info.failed" - - - name: get info on ebs volumes - ec2_vol_info: - filters: - attachment.instance-id: "{{ test_instance.instance_ids[0] }}" - register: ec2_vol_info - - - name: check task return attributes - assert: - that: - - "{{ ec2_vol_info.volumes | length == 4 }}" - - - name: detach volume from the instance - ec2_vol: - id: "{{ new_vol_attach_result.volume_id }}" - instance: "" - register: new_vol_attach_result - - - name: check task return attributes - assert: - that: - - "new_vol_attach_result.changed" - - "new_vol_attach_result.volume.status == 'available'" - - - name: detach volume from the instance (idempotent) - ec2_vol: - id: "{{ new_vol_attach_result.volume_id }}" - instance: "" - register: new_vol_attach_result_idem - - - name: check task return attributes - assert: - that: - - "not new_vol_attach_result_idem.changed" - - - name: delete volume - ec2_vol: - id: "{{ volume2.volume_id }}" - state: absent - register: delete_volume_result - - - name: check task return attributes - assert: - that: - - "delete_volume_result.changed" - - - name: delete volume (idempotent) - ec2_vol: - id: "{{ volume2.volume_id }}" - state: absent - register: delete_volume_result_idem - - - name: check task return attributes - assert: - that: - - "not delete_volume_result_idem.changed" - - # ==== Cleanup ============================================================ - - always: - - - name: delete test instance - ec2_instance: - instance_ids: - - "{{ test_instance.instance_ids[0] }}" - state: terminated - ignore_errors: yes - - - name: delete volumes - ec2_vol: - id: "{{ item.volume_id }}" - state: absent - ignore_errors: yes - with_items: - - "{{ volume1 }}" - - "{{ volume2 }}" - - "{{ volume3 }}" - - "{{ new_vol_attach_result }}" - - "{{ attach_new_vol_from_snapshot_result }}" - - - name: delete snapshot - ec2_snapshot: - snapshot_id: "{{ vol1_snapshot.snapshot_id }}" - state: absent - ignore_errors: yes - - - name: delete test subnet - ec2_vpc_subnet: - vpc_id: "{{ testing_vpc.vpc.id }}" - cidr: "{{ subnet_cidr }}" - state: absent - ignore_errors: yes - - - name: delete test VPC - ec2_vpc_net: - name: "{{ vpc_name }}" - cidr_block: "{{ vpc_cidr }}" - state: absent - ignore_errors: yes diff --git a/test/integration/targets/ec2_vol_info/aliases b/test/integration/targets/ec2_vol_info/aliases deleted file mode 100644 index 157ce0c9d4..0000000000 --- a/test/integration/targets/ec2_vol_info/aliases +++ /dev/null @@ -1,2 +0,0 @@ -cloud/aws -shippable/aws/group3 diff --git a/test/integration/targets/ec2_vol_info/meta/main.yml b/test/integration/targets/ec2_vol_info/meta/main.yml deleted file mode 100644 index 1f64f1169a..0000000000 --- a/test/integration/targets/ec2_vol_info/meta/main.yml +++ /dev/null @@ -1,3 +0,0 @@ -dependencies: - - prepare_tests - - setup_ec2 diff --git a/test/integration/targets/ec2_vol_info/tasks/main.yml b/test/integration/targets/ec2_vol_info/tasks/main.yml deleted file mode 100644 index 9bde030761..0000000000 --- a/test/integration/targets/ec2_vol_info/tasks/main.yml +++ /dev/null @@ -1,123 +0,0 @@ ---- -# tasks file for test_ec2_vol_info -- name: Set up AWS connection info - set_fact: - aws_connection_info: &aws_connection_info - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" - security_token: "{{ security_token }}" - region: "{{ aws_region }}" - no_log: true - -- block: - - ec2_ami_info: - owners: amazon - <<: *aws_connection_info - filters: - architecture: x86_64 - virtualization-type: hvm - root-device-type: ebs - name: "amzn-ami-hvm*" - register: amis - - - name: Create test instance - ec2_instance: - name: "{{ resource_prefix }}_ansible_ec2_vol_info_test" - instance_type: t2.nano - image_id: "{{ (amis.images | sort(attribute='creation_date') | last).image_id }}" - wait: yes - tags: - Environment: test - <<: *aws_connection_info - register: instance - - - name: Ensure there's only one matching instance - assert: - that: - - "instance.instance_ids|length == 1" - - "instance.instances|length == 1" - - - name: Create test volume - ec2_vol: - instance: "{{ instance.instance_ids[0] }}" - volume_size: 4 - name: "{{ resource_prefix }}_ansible_ec2_vol_info_test.db" - device_name: /dev/xvdf - iops: 100 - tags: - Tag Name with Space-and-dash: Tag Value with Space-and-dash - <<: *aws_connection_info - delete_on_termination: yes - register: volume - - - name: Gather volume info without any filters - ec2_vol_info: - <<: *aws_connection_info - register: volume_facts_wo_filters - check_mode: no - - - name: Check if facts are returned without filters - assert: - that: - - "volume_facts_wo_filters.volumes is defined" - - - name: Gather volume info - ec2_vol_info: - <<: *aws_connection_info - filters: - "tag:Name": "{{ resource_prefix }}_ansible_ec2_vol_info_test.db" - register: volume_facts - check_mode: no - - - name: Format check - assert: - that: - - "volume_facts.volumes|length == 1" - - "v.attachment_set.attach_time is defined" - - "v.attachment_set.device is defined and v.attachment_set.device == volume.device" - - "v.attachment_set.instance_id is defined and v.attachment_set.instance_id == instance.instance_ids[0]" - - "v.attachment_set.status is defined and v.attachment_set.status == 'attached'" - - "v.create_time is defined" - - "v.encrypted is defined and v.encrypted == false" - - "v.id is defined and v.id == volume.volume_id" - - "v.iops is defined and v.iops == 100" - - "v.region is defined and v.region == aws_region" - - "v.size is defined and v.size == 4" - - "v.snapshot_id is defined and v.snapshot_id == ''" - - "v.status is defined and v.status == 'in-use'" - - "v.tags.Name is defined and v.tags.Name == resource_prefix + '_ansible_ec2_vol_info_test.db'" - - "v.tags['Tag Name with Space-and-dash'] == 'Tag Value with Space-and-dash'" - - "v.type is defined and v.type == 'io1'" - - "v.zone is defined and v.zone == instance.instances[0].placement.availability_zone" - vars: - v: "{{ volume_facts.volumes[0] }}" - - - name: New format check - assert: - that: - - "v.attachment_set.delete_on_termination is defined" - vars: - v: "{{ volume_facts.volumes[0] }}" - when: ansible_version.full is version('2.7', '>=') - - always: - - name: Remove the instance - ec2_instance: - state: absent - filters: - "tag:Name": "{{ resource_prefix }}_ansible_ec2_vol_info_test" - <<: *aws_connection_info - register: result - until: result is not failed - ignore_errors: yes - retries: 10 - - - name: Remove the volume - ec2_vol: - id: "{{ volume.volume_id }}" - state: absent - <<: *aws_connection_info - register: result - until: result is not failed - ignore_errors: yes - retries: 10 diff --git a/test/integration/targets/ec2_vpc_net/aliases b/test/integration/targets/ec2_vpc_net/aliases deleted file mode 100644 index fb765ef767..0000000000 --- a/test/integration/targets/ec2_vpc_net/aliases +++ /dev/null @@ -1,3 +0,0 @@ -ec2_vpc_net_info -cloud/aws -shippable/aws/group1 diff --git a/test/integration/targets/ec2_vpc_net/defaults/main.yml b/test/integration/targets/ec2_vpc_net/defaults/main.yml deleted file mode 100644 index 3289b27835..0000000000 --- a/test/integration/targets/ec2_vpc_net/defaults/main.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -# defaults file for ec2_vpc_net -vpc_cidr: '10.{{ 256 | random(seed=resource_prefix) }}.0.0/24' -vpc_cidr_a: '10.{{ 256 | random(seed=resource_prefix) }}.1.0/24' -vpc_cidr_b: '10.{{ 256 | random(seed=resource_prefix) }}.2.0/24' diff --git a/test/integration/targets/ec2_vpc_net/meta/main.yml b/test/integration/targets/ec2_vpc_net/meta/main.yml deleted file mode 100644 index 1f64f1169a..0000000000 --- a/test/integration/targets/ec2_vpc_net/meta/main.yml +++ /dev/null @@ -1,3 +0,0 @@ -dependencies: - - prepare_tests - - setup_ec2 diff --git a/test/integration/targets/ec2_vpc_net/tasks/main.yml b/test/integration/targets/ec2_vpc_net/tasks/main.yml deleted file mode 100644 index c0e5e1b45f..0000000000 --- a/test/integration/targets/ec2_vpc_net/tasks/main.yml +++ /dev/null @@ -1,1306 +0,0 @@ ---- -- name: Setup AWS Environment - module_defaults: - group/aws: - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" - security_token: "{{ security_token | default(omit) }}" - region: "{{ aws_region }}" - - block: - - # ============================================================ - - - name: Get the current caller identity facts - aws_caller_info: - register: caller_facts - - - name: run the module without parameters - ec2_vpc_net: - ignore_errors: yes - register: result - - - name: assert failure - assert: - that: - - result is failed - - result.msg.startswith("missing required arguments") - - # ============================================================ - - - name: attempt to create a VPC without providing connnection information - module_defaults: { group/aws: {} } - ec2_vpc_net: - state: present - cidr_block: "{{ vpc_cidr }}" - name: "{{ resource_prefix }}" - region: us-east-1 - ignore_errors: yes - register: result - - - name: assert connection failure - assert: - that: - - result is failed - - '"Unable to locate credentials" in result.msg' - - # ============================================================ - - - name: Fetch existing VPC info - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - name: Check no-one is using the Prefix before we start - assert: - that: - - vpc_info.vpcs | length == 0 - - - name: test check mode creating a VPC - ec2_vpc_net: - state: present - cidr_block: "{{ vpc_cidr }}" - name: "{{ resource_prefix }}" - check_mode: true - register: result - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - - name: check for a change - assert: - that: - - result is changed - - vpc_info.vpcs | length == 0 - - # ============================================================ - - - name: create a VPC - ec2_vpc_net: - state: present - cidr_block: "{{ vpc_cidr }}" - name: "{{ resource_prefix }}" - ipv6_cidr: True - register: result - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - - name: assert the VPC was created successfully - assert: - that: - - result is successful - - result is changed - - vpc_info.vpcs | length == 1 - - - name: assert the output - assert: - that: - - '"cidr_block" in result.vpc' - - result.vpc.cidr_block == vpc_cidr - - result.vpc.cidr_block_association_set | length == 1 - - result.vpc.cidr_block_association_set[0].association_id.startswith("vpc-cidr-assoc-") - - result.vpc.cidr_block_association_set[0].cidr_block == vpc_cidr - - result.vpc.cidr_block_association_set[0].cidr_block_state.state in ["associated", "associating"] - - '"classic_link_enabled" in result.vpc' - - result.vpc.dhcp_options_id.startswith("dopt-") - - result.vpc.id.startswith("vpc-") - - '"instance_tenancy" in result.vpc' - - result.vpc.ipv6_cidr_block_association_set | length == 1 - - result.vpc.ipv6_cidr_block_association_set[0].association_id.startswith("vpc-cidr-assoc-") - - result.vpc.ipv6_cidr_block_association_set[0].ipv6_cidr_block | ipv6 - - result.vpc.ipv6_cidr_block_association_set[0].ipv6_cidr_block_state.state in ["associated", "associating"] - - '"is_default" in result.vpc' - - '"state" in result.vpc' - - result.vpc.tags.keys() | length == 1 - - result.vpc.tags.Name == resource_prefix - - - name: set the first VPC's details as facts for comparison and cleanup - set_fact: - vpc_1_result: "{{ result }}" - vpc_1: "{{ result.vpc.id }}" - vpc_1_ipv6_cidr: "{{ result.vpc.ipv6_cidr_block_association_set.0.ipv6_cidr_block }}" - default_dhcp_options_id: "{{ result.vpc.dhcp_options_id }}" - - - name: create a VPC (retry) - ec2_vpc_net: - state: present - cidr_block: "{{ vpc_cidr }}" - name: "{{ resource_prefix }}" - ipv6_cidr: True - register: result - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - - name: assert nothing changed - assert: - that: - - result is successful - - result is not changed - - vpc_info.vpcs | length == 1 - - '"cidr_block" in result.vpc' - - result.vpc.cidr_block == vpc_cidr - - result.vpc.cidr_block_association_set | length == 1 - - result.vpc.cidr_block_association_set[0].association_id.startswith("vpc-cidr-assoc-") - - result.vpc.cidr_block_association_set[0].cidr_block == vpc_cidr - - result.vpc.cidr_block_association_set[0].cidr_block_state.state in ["associated", "associating"] - - '"classic_link_enabled" in result.vpc' - - result.vpc.dhcp_options_id.startswith("dopt-") - - result.vpc.id.startswith("vpc-") - - '"instance_tenancy" in result.vpc' - - result.vpc.ipv6_cidr_block_association_set | length == 1 - - result.vpc.ipv6_cidr_block_association_set[0].association_id.startswith("vpc-cidr-assoc-") - - result.vpc.ipv6_cidr_block_association_set[0].ipv6_cidr_block | ipv6 - - result.vpc.ipv6_cidr_block_association_set[0].ipv6_cidr_block_state.state in ["associated", "associating"] - - '"is_default" in result.vpc' - - '"state" in result.vpc' - - result.vpc.tags.keys() | length == 1 - - result.vpc.tags.Name == resource_prefix - - result.vpc.id == vpc_1 - - # ============================================================ - - - name: VPC info (no filters) - ec2_vpc_net_info: - register: vpc_info - - - name: Test that our new VPC shows up in the results - assert: - that: - - vpc_1 in ( vpc_info | json_query("vpcs[].vpc_id") | list ) - - - name: VPC info (Simple tag filter) - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - - name: Test vpc_info results - assert: - that: - - vpc_info.vpcs[0].cidr_block == vpc_cidr - - vpc_info.vpcs[0].cidr_block_association_set | length == 1 - - vpc_info.vpcs[0].cidr_block_association_set[0].association_id == result.vpc.cidr_block_association_set[0].association_id - - vpc_info.vpcs[0].cidr_block_association_set[0].cidr_block == result.vpc.cidr_block_association_set[0].cidr_block - - vpc_info.vpcs[0].cidr_block_association_set[0].cidr_block_state.state in ["associated", "associating"] - - '"classic_link_dns_supported" in vpc_info.vpcs[0]' - - '"classic_link_enabled" in vpc_info.vpcs[0]' - - vpc_info.vpcs[0].dhcp_options_id == result.vpc.dhcp_options_id - - ( vpc_info.vpcs[0].enable_dns_hostnames | bool ) == True - - ( vpc_info.vpcs[0].enable_dns_support | bool ) == True - - vpc_info.vpcs[0].id == result.vpc.id - - '"instance_tenancy" in vpc_info.vpcs[0]' - - vpc_info.vpcs[0].ipv6_cidr_block_association_set | length == 1 - - vpc_info.vpcs[0].ipv6_cidr_block_association_set[0].association_id == result.vpc.ipv6_cidr_block_association_set[0].association_id - - vpc_info.vpcs[0].ipv6_cidr_block_association_set[0].ipv6_cidr_block == result.vpc.ipv6_cidr_block_association_set[0].ipv6_cidr_block - - vpc_info.vpcs[0].ipv6_cidr_block_association_set[0].ipv6_cidr_block_state.state in ["associated", "associating"] - - '"is_default" in vpc_info.vpcs[0]' - - vpc_info.vpcs[0].owner_id == caller_facts.account - - '"state" in vpc_info.vpcs[0]' - - vpc_info.vpcs[0].vpc_id == result.vpc.id - - # ============================================================ - - - name: Try to add IPv6 CIDR when one already exists - ec2_vpc_net: - state: present - cidr_block: "{{ vpc_cidr }}" - name: "{{ resource_prefix }}" - ipv6_cidr: True - register: result - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - - name: Assert no changes made - assert: - that: - - '"Only one IPv6 CIDR is permitted per VPC, {{ result.vpc.id }} already has CIDR {{ vpc_1_ipv6_cidr }}" in result.warnings' - - result is not changed - - vpc_info.vpcs | length == 1 - - # ============================================================ - - - name: test check mode creating an identical VPC (multi_ok) - ec2_vpc_net: - state: present - cidr_block: "{{ vpc_cidr }}" - name: "{{ resource_prefix }}" - ipv6_cidr: True - multi_ok: yes - check_mode: true - register: result - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - - name: assert a change would be made - assert: - that: - - result is changed - - name: assert a change was not actually made - assert: - that: - - vpc_info.vpcs | length == 1 - - # ============================================================ - - - name: create a VPC with a dedicated tenancy using the same CIDR and name - ec2_vpc_net: - state: present - cidr_block: "{{ vpc_cidr }}" - name: "{{ resource_prefix }}" - ipv6_cidr: True - tenancy: dedicated - multi_ok: yes - register: result - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - - name: assert a new VPC was created - assert: - that: - - result is successful - - result is changed - - result.vpc.instance_tenancy == "dedicated" - - result.vpc.id != vpc_1 - - vpc_info.vpcs | length == 2 - - - name: set the second VPC's details as facts for comparison and cleanup - set_fact: - vpc_2_result: "{{ result }}" - vpc_2: "{{ result.vpc.id }}" - - # ============================================================ - - - name: VPC info (Simple VPC-ID filter) - ec2_vpc_net_info: - filters: - "vpc-id": "{{ vpc_2 }}" - register: vpc_info - - - name: Test vpc_info results - assert: - that: - - vpc_info.vpcs[0].cidr_block == vpc_cidr - - vpc_info.vpcs[0].cidr_block_association_set | length == 1 - - vpc_info.vpcs[0].cidr_block_association_set[0].association_id == result.vpc.cidr_block_association_set[0].association_id - - vpc_info.vpcs[0].cidr_block_association_set[0].cidr_block == result.vpc.cidr_block_association_set[0].cidr_block - - vpc_info.vpcs[0].cidr_block_association_set[0].cidr_block_state.state in ["associated", "associating"] - - '"classic_link_dns_supported" in vpc_info.vpcs[0]' - - '"classic_link_enabled" in vpc_info.vpcs[0]' - - vpc_info.vpcs[0].dhcp_options_id == result.vpc.dhcp_options_id - - ( vpc_info.vpcs[0].enable_dns_hostnames | bool ) == True - - ( vpc_info.vpcs[0].enable_dns_support | bool ) == True - - vpc_info.vpcs[0].id == vpc_2 - - '"instance_tenancy" in vpc_info.vpcs[0]' - - vpc_info.vpcs[0].ipv6_cidr_block_association_set | length == 1 - - vpc_info.vpcs[0].ipv6_cidr_block_association_set[0].association_id == result.vpc.ipv6_cidr_block_association_set[0].association_id - - vpc_info.vpcs[0].ipv6_cidr_block_association_set[0].ipv6_cidr_block == result.vpc.ipv6_cidr_block_association_set[0].ipv6_cidr_block - - vpc_info.vpcs[0].ipv6_cidr_block_association_set[0].ipv6_cidr_block_state.state in ["associated", "associating"] - - '"is_default" in vpc_info.vpcs[0]' - - vpc_info.vpcs[0].owner_id == caller_facts.account - - '"state" in vpc_info.vpcs[0]' - - vpc_info.vpcs[0].vpc_id == vpc_2 - - # ============================================================ - - # This will only fail if there are already *2* vpcs otherwise ec2_vpc_net - # assumes you want to update your existing VPC... - - name: attempt to create another VPC with the same CIDR and name without multi_ok - ec2_vpc_net: - state: present - cidr_block: "{{ vpc_cidr }}" - name: "{{ resource_prefix }}" - ipv6_cidr: True - tenancy: dedicated - multi_ok: no - register: new_result - ignore_errors: yes - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - - name: assert failure - assert: - that: - - new_result is failed - - '"If you would like to create the VPC anyway please pass True to the multi_ok param" in new_result.msg' - - vpc_info.vpcs | length == 2 - - # ============================================================ - - # FIXME: right now if there are multiple matching VPCs they cannot be removed, - # as there is no vpc_id option for idempotence. A workaround is to retag the VPC. - - name: remove Name tag on new VPC - ec2_tag: - state: absent - resource: "{{ vpc_2 }}" - tags: - Name: "{{ resource_prefix }}" - - - name: add a unique name tag - ec2_tag: - state: present - resource: "{{ vpc_2 }}" - tags: - Name: "{{ resource_prefix }}-changed" - - - name: delete one of the VPCs - ec2_vpc_net: - state: absent - cidr_block: "{{ vpc_cidr }}" - name: "{{ resource_prefix }}-changed" - register: result - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - - name: assert success - assert: - that: - - result is changed - - not result.vpc - - vpc_info.vpcs | length == 1 - - # ============================================================ - - - name: attempt to delete a VPC that doesn't exist - ec2_vpc_net: - state: absent - cidr_block: "{{ vpc_cidr }}" - name: "{{ resource_prefix }}-changed" - register: result - - - name: assert no changes were made - assert: - that: - - result is not changed - - not result.vpc - - # ============================================================ - - - name: create a DHCP option set to use in next test - ec2_vpc_dhcp_option: - dns_servers: - - 4.4.4.4 - - 8.8.8.8 - tags: - Name: "{{ resource_prefix }}" - register: new_dhcp - - name: assert the DHCP option set was successfully created - assert: - that: - - new_dhcp is changed - - - name: modify the DHCP options set for a VPC (check_mode) - ec2_vpc_net: - state: present - cidr_block: "{{ vpc_cidr }}" - name: "{{ resource_prefix }}" - dhcp_opts_id: "{{ new_dhcp.dhcp_options_id }}" - register: result - check_mode: True - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - - name: assert the DHCP option set changed but didn't update - assert: - that: - - result is changed - - result.vpc.id == vpc_1 - - vpc_info.vpcs | length == 1 - - vpc_info.vpcs[0].dhcp_options_id == default_dhcp_options_id - - - name: modify the DHCP options set for a VPC - ec2_vpc_net: - state: present - cidr_block: "{{ vpc_cidr }}" - name: "{{ resource_prefix }}" - dhcp_opts_id: "{{ new_dhcp.dhcp_options_id }}" - register: result - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - - name: assert the DHCP option set changed - assert: - that: - - result is changed - - result.vpc.id == vpc_1 - - default_dhcp_options_id != result.vpc.dhcp_options_id - - result.vpc.dhcp_options_id == new_dhcp.dhcp_options_id - - vpc_info.vpcs | length == 1 - - vpc_info.vpcs[0].dhcp_options_id == new_dhcp.dhcp_options_id - - - name: modify the DHCP options set for a VPC (retry) - ec2_vpc_net: - state: present - cidr_block: "{{ vpc_cidr }}" - name: "{{ resource_prefix }}" - dhcp_opts_id: "{{ new_dhcp.dhcp_options_id }}" - register: result - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - - name: assert the DHCP option set changed - assert: - that: - - result is not changed - - result.vpc.id == vpc_1 - - result.vpc.dhcp_options_id == new_dhcp.dhcp_options_id - - vpc_info.vpcs | length == 1 - - vpc_info.vpcs[0].dhcp_options_id == new_dhcp.dhcp_options_id - - # ============================================================ - - # XXX #62677 - #- name: disable dns_hostnames (check mode) - # ec2_vpc_net: - # state: present - # cidr_block: "{{ vpc_cidr }}" - # name: "{{ resource_prefix }}" - # dns_hostnames: False - # register: result - # check_mode: True - #- ec2_vpc_net_info: - # filters: - # "tag:Name": "{{ resource_prefix }}" - # register: vpc_info - - #- name: assert changed was set but not made - # assert: - # that: - # - result is successful - # - result is changed - # - vpc_info.vpcs | length == 1 - # - vpc_info.vpcs[0].enable_dns_hostnames | bool == True - # - vpc_info.vpcs[0].enable_dns_support | bool == True - - - name: disable dns_hostnames - ec2_vpc_net: - state: present - cidr_block: "{{ vpc_cidr }}" - name: "{{ resource_prefix }}" - dns_hostnames: False - register: result - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - - name: assert a change was made - assert: - that: - - result is successful - - result is changed - - result.vpc.id == vpc_1 - - vpc_info.vpcs | length == 1 - - vpc_info.vpcs[0].enable_dns_hostnames | bool == False - - vpc_info.vpcs[0].enable_dns_support | bool == True - - - name: disable dns_hostnames (retry) - ec2_vpc_net: - state: present - cidr_block: "{{ vpc_cidr }}" - name: "{{ resource_prefix }}" - dns_hostnames: False - register: result - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - - name: assert a change was made - assert: - that: - - result is successful - - result is not changed - - result.vpc.id == vpc_1 - - vpc_info.vpcs | length == 1 - - vpc_info.vpcs[0].enable_dns_hostnames | bool == False - - vpc_info.vpcs[0].enable_dns_support | bool == True - - # XXX #62677 - #- name: disable dns_support (check mode) - # ec2_vpc_net: - # state: present - # cidr_block: "{{ vpc_cidr }}" - # name: "{{ resource_prefix }}" - # dns_hostnames: False - # dns_support: False - # check_mode: True - # register: result - #- ec2_vpc_net_info: - # filters: - # "tag:Name": "{{ resource_prefix }}" - # register: vpc_info - - #- name: assert changed was set but not made - # assert: - # that: - # - result is successful - # - result is changed - # - result.vpc.id == vpc_1 - # - vpc_info.vpcs | length == 1 - # - vpc_info.vpcs[0].enable_dns_hostnames | bool == False - # - vpc_info.vpcs[0].enable_dns_support | bool == True - - - name: disable dns_support - ec2_vpc_net: - state: present - cidr_block: "{{ vpc_cidr }}" - name: "{{ resource_prefix }}" - dns_hostnames: False - dns_support: False - register: result - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - - name: assert a change was made - assert: - that: - - result is successful - - result is changed - - result.vpc.id == vpc_1 - - vpc_info.vpcs | length == 1 - - vpc_info.vpcs[0].enable_dns_hostnames | bool == False - - vpc_info.vpcs[0].enable_dns_support | bool == False - - - name: disable dns_support (retry) - ec2_vpc_net: - state: present - cidr_block: "{{ vpc_cidr }}" - name: "{{ resource_prefix }}" - dns_hostnames: False - dns_support: False - register: result - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - - name: assert a change was not made - assert: - that: - - result is successful - - result is not changed - - result.vpc.id == vpc_1 - - vpc_info.vpcs | length == 1 - - vpc_info.vpcs[0].enable_dns_hostnames | bool == False - - vpc_info.vpcs[0].enable_dns_support | bool == False - - # XXX #62677 - #- name: re-enable dns_support (check mode) - # ec2_vpc_net: - # state: present - # cidr_block: "{{ vpc_cidr }}" - # name: "{{ resource_prefix }}" - # register: result - # check_mode: True - #- ec2_vpc_net_info: - # filters: - # "tag:Name": "{{ resource_prefix }}" - # register: vpc_info - - #- name: assert a change was made - # assert: - # that: - # - result is successful - # - result is changed - # - result.vpc.id == vpc_1 - # - vpc_info.vpcs | length == 1 - # - vpc_info.vpcs[0].enable_dns_hostnames | bool == True - # - vpc_info.vpcs[0].enable_dns_support | bool == True - - - name: re-enable dns_support - ec2_vpc_net: - state: present - cidr_block: "{{ vpc_cidr }}" - name: "{{ resource_prefix }}" - register: result - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - - name: assert a change was made - assert: - that: - - result is successful - - result is changed - - result.vpc.id == vpc_1 - - vpc_info.vpcs | length == 1 - - vpc_info.vpcs[0].enable_dns_hostnames | bool == True - - vpc_info.vpcs[0].enable_dns_support | bool == True - - - name: re-enable dns_support (retry) - ec2_vpc_net: - state: present - cidr_block: "{{ vpc_cidr }}" - name: "{{ resource_prefix }}" - register: result - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - - name: assert a change was not made - assert: - that: - - result is successful - - result is not changed - - result.vpc.id == vpc_1 - - vpc_info.vpcs | length == 1 - - vpc_info.vpcs[0].enable_dns_hostnames | bool == True - - vpc_info.vpcs[0].enable_dns_support | bool == True - - # ============================================================ - - - name: modify tags (check mode) - ec2_vpc_net: - state: present - cidr_block: "{{ vpc_cidr }}" - name: "{{ resource_prefix }}" - tags: - Ansible: Test - check_mode: true - register: result - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - - name: assert the VPC has Name but not Ansible tag - assert: - that: - - result is successful - - result is changed - - result.vpc.id == vpc_1 - - result.vpc.tags | length == 1 - - result.vpc.tags.Name == resource_prefix - - vpc_info.vpcs | length == 1 - - vpc_info.vpcs[0].tags | length == 1 - - vpc_info.vpcs[0].tags.Name == resource_prefix - - - name: modify tags - ec2_vpc_net: - state: present - cidr_block: "{{ vpc_cidr }}" - name: "{{ resource_prefix }}" - tags: - Ansible: Test - register: result - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - - name: assert the VPC has Name and Ansible tags - assert: - that: - - result is successful - - result is changed - - result.vpc.id == vpc_1 - - result.vpc.tags | length == 2 - - result.vpc.tags.Ansible == "Test" - - result.vpc.tags.Name == resource_prefix - - vpc_info.vpcs | length == 1 - - vpc_info.vpcs[0].tags | length == 2 - - vpc_info.vpcs[0].tags.Ansible == "Test" - - vpc_info.vpcs[0].tags.Name == resource_prefix - - - name: modify tags (no change) - ec2_vpc_net: - state: present - cidr_block: "{{ vpc_cidr }}" - name: "{{ resource_prefix }}" - dns_support: True - dns_hostnames: True - tags: - Ansible: Test - register: result - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - - name: assert the VPC has Name and Ansible tags - assert: - that: - - result is successful - - result is not changed - - result.vpc.id == vpc_1 - - result.vpc.tags|length == 2 - - result.vpc.tags.Ansible == "Test" - - result.vpc.tags.Name == resource_prefix - - vpc_info.vpcs | length == 1 - - vpc_info.vpcs[0].tags|length == 2 - - vpc_info.vpcs[0].tags.Ansible == "Test" - - vpc_info.vpcs[0].tags.Name == resource_prefix - - # ============================================================ - - # #62678 - #- name: modify CIDR (check mode) - # ec2_vpc_net: - # state: present - # cidr_block: - # - "{{ vpc_cidr }}" - # - "{{ vpc_cidr_a }}" - # name: "{{ resource_prefix }}" - # check_mode: true - # register: result - #- ec2_vpc_net_info: - # filters: - # "tag:Name": "{{ resource_prefix }}" - # register: vpc_info - - #- name: Check the CIDRs weren't changed - # assert: - # that: - # - result is successful - # - result is changed - # - result.vpc.id == vpc_1 - # - vpc_info.vpcs | length == 1 - # - vpc_info.vpcs[0].cidr_block == vpc_cidr - # - vpc_cidr in (result.vpc | json_query("cidr_block_association_set[*].cidr_block") | list) - # - vpc_cidr_a not in (result.vpc | json_query("cidr_block_association_set[*].cidr_block") | list) - # - vpc_cidr_b not in (result.vpc | json_query("cidr_block_association_set[*].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] | json_query("cidr_block_association_set[*].cidr_block") | list) - # - vpc_cidr_a not in (vpc_info.vpcs[0] | json_query("cidr_block_association_set[*].cidr_block") | list) - # - vpc_cidr_b not in (result.vpc | json_query("cidr_block_association_set[*].cidr_block") | list) - - - name: modify CIDR - ec2_vpc_net: - state: present - cidr_block: - - "{{ vpc_cidr }}" - - "{{ vpc_cidr_a }}" - name: "{{ resource_prefix }}" - register: result - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - - name: assert the CIDRs changed - assert: - that: - - result is successful - - result is changed - - result.vpc.id == vpc_1 - - vpc_info.vpcs | length == 1 - - result.vpc.cidr_block == vpc_cidr - - vpc_info.vpcs[0].cidr_block == vpc_cidr - - result.vpc.cidr_block_association_set | length == 2 - - result.vpc.cidr_block_association_set[0].association_id.startswith("vpc-cidr-assoc-") - - 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 | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (result.vpc | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b not in (result.vpc | json_query("cidr_block_association_set[*].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] | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (vpc_info.vpcs[0] | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b not in (vpc_info.vpcs[0] | json_query("cidr_block_association_set[*].cidr_block") | list) - - - name: modify CIDR (no change) - ec2_vpc_net: - state: present - cidr_block: - - "{{ vpc_cidr }}" - - "{{ vpc_cidr_a }}" - name: "{{ resource_prefix }}" - register: result - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - - name: assert the CIDRs didn't change - assert: - that: - - result is successful - - result is not changed - - result.vpc.id == vpc_1 - - vpc_info.vpcs | length == 1 - - result.vpc.cidr_block == vpc_cidr - - vpc_info.vpcs[0].cidr_block == vpc_cidr - - result.vpc.cidr_block_association_set | length == 2 - - result.vpc.cidr_block_association_set[0].association_id.startswith("vpc-cidr-assoc-") - - 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 | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (result.vpc | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b not in (result.vpc | json_query("cidr_block_association_set[*].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] | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (vpc_info.vpcs[0] | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b not in (vpc_info.vpcs[0] | json_query("cidr_block_association_set[*].cidr_block") | list) - - # #62678 - #- name: modify CIDR - no purge (check mode) - # ec2_vpc_net: - # state: present - # cidr_block: - # - "{{ vpc_cidr }}" - # - "{{ vpc_cidr_b }}" - # name: "{{ resource_prefix }}" - # check_mode: true - # register: result - #- ec2_vpc_net_info: - # filters: - # "tag:Name": "{{ resource_prefix }}" - # register: vpc_info - - #- name: Check the CIDRs weren't changed - # assert: - # that: - # - result is successful - # - result is changed - # - vpc_info.vpcs | length == 1 - # - vpc_info.vpcs[0].cidr_block == vpc_cidr - # - vpc_cidr in (result.vpc | json_query("cidr_block_association_set[*].cidr_block") | list) - # - vpc_cidr_a in (result.vpc | json_query("cidr_block_association_set[*].cidr_block") | list) - # - vpc_cidr_b not in (result.vpc | json_query("cidr_block_association_set[*].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] | json_query("cidr_block_association_set[*].cidr_block") | list) - # - vpc_cidr_a in (vpc_info.vpcs[0] | json_query("cidr_block_association_set[*].cidr_block") | list) - # - vpc_cidr_b not in (vpc_info.vpcs[0] | json_query("cidr_block_association_set[*].cidr_block") | list) - - - name: modify CIDR - no purge - ec2_vpc_net: - state: present - cidr_block: - - "{{ vpc_cidr }}" - - "{{ vpc_cidr_b }}" - name: "{{ resource_prefix }}" - register: result - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - - name: assert the CIDRs changed - assert: - that: - - result is successful - - result is changed - - result.vpc.id == vpc_1 - - vpc_info.vpcs | length == 1 - - result.vpc.cidr_block == vpc_cidr - - vpc_info.vpcs[0].cidr_block == vpc_cidr - - result.vpc.cidr_block_association_set | length == 3 - - result.vpc.cidr_block_association_set[0].association_id.startswith("vpc-cidr-assoc-") - - result.vpc.cidr_block_association_set[1].association_id.startswith("vpc-cidr-assoc-") - - result.vpc.cidr_block_association_set[2].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"] - - result.vpc.cidr_block_association_set[2].cidr_block_state.state in ["associated", "associating"] - - vpc_cidr in (result.vpc | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (result.vpc | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b in (result.vpc | json_query("cidr_block_association_set[*].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-") - - vpc_info.vpcs[0].cidr_block_association_set[2].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_info.vpcs[0].cidr_block_association_set[2].cidr_block_state.state in ["associated", "associating"] - - vpc_cidr in (vpc_info.vpcs[0] | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (vpc_info.vpcs[0] | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b in (vpc_info.vpcs[0] | json_query("cidr_block_association_set[*].cidr_block") | list) - - - name: modify CIDR - no purge (no change) - ec2_vpc_net: - state: present - cidr_block: - - "{{ vpc_cidr }}" - - "{{ vpc_cidr_b }}" - name: "{{ resource_prefix }}" - register: result - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - - name: assert the CIDRs didn't change - assert: - that: - - result is successful - - result is not changed - - vpc_info.vpcs | length == 1 - - result.vpc.cidr_block == vpc_cidr - - vpc_info.vpcs[0].cidr_block == vpc_cidr - - result.vpc.cidr_block_association_set | length == 3 - - result.vpc.cidr_block_association_set[0].association_id.startswith("vpc-cidr-assoc-") - - result.vpc.cidr_block_association_set[1].association_id.startswith("vpc-cidr-assoc-") - - result.vpc.cidr_block_association_set[2].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"] - - result.vpc.cidr_block_association_set[2].cidr_block_state.state in ["associated", "associating"] - - vpc_cidr in (result.vpc | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (result.vpc | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b in (result.vpc | json_query("cidr_block_association_set[*].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-") - - vpc_info.vpcs[0].cidr_block_association_set[2].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_info.vpcs[0].cidr_block_association_set[2].cidr_block_state.state in ["associated", "associating"] - - vpc_cidr in (vpc_info.vpcs[0] | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (vpc_info.vpcs[0] | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b in (vpc_info.vpcs[0] | json_query("cidr_block_association_set[*].cidr_block") | list) - - - name: modify CIDR - no purge (no change - list all - check mode) - ec2_vpc_net: - state: present - cidr_block: - - "{{ vpc_cidr }}" - - "{{ vpc_cidr_a }}" - - "{{ vpc_cidr_b }}" - name: "{{ resource_prefix }}" - register: result - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - - name: assert the CIDRs didn't change - assert: - that: - - result is successful - - result is not changed - - vpc_info.vpcs | length == 1 - - result.vpc.cidr_block == vpc_cidr - - vpc_info.vpcs[0].cidr_block == vpc_cidr - - result.vpc.cidr_block_association_set | length == 3 - - result.vpc.cidr_block_association_set[0].association_id.startswith("vpc-cidr-assoc-") - - result.vpc.cidr_block_association_set[1].association_id.startswith("vpc-cidr-assoc-") - - result.vpc.cidr_block_association_set[2].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"] - - result.vpc.cidr_block_association_set[2].cidr_block_state.state in ["associated", "associating"] - - vpc_cidr in (result.vpc | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (result.vpc | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b in (result.vpc | json_query("cidr_block_association_set[*].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-") - - vpc_info.vpcs[0].cidr_block_association_set[2].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_info.vpcs[0].cidr_block_association_set[2].cidr_block_state.state in ["associated", "associating"] - - vpc_cidr in (vpc_info.vpcs[0] | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (vpc_info.vpcs[0] | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b in (vpc_info.vpcs[0] | json_query("cidr_block_association_set[*].cidr_block") | list) - - - name: modify CIDR - no purge (no change - list all) - ec2_vpc_net: - state: present - cidr_block: - - "{{ vpc_cidr }}" - - "{{ vpc_cidr_a }}" - - "{{ vpc_cidr_b }}" - name: "{{ resource_prefix }}" - register: result - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - - name: assert the CIDRs didn't change - assert: - that: - - result is successful - - result is not changed - - vpc_info.vpcs | length == 1 - - result.vpc.cidr_block == vpc_cidr - - vpc_info.vpcs[0].cidr_block == vpc_cidr - - result.vpc.cidr_block_association_set | length == 3 - - result.vpc.cidr_block_association_set[0].association_id.startswith("vpc-cidr-assoc-") - - result.vpc.cidr_block_association_set[1].association_id.startswith("vpc-cidr-assoc-") - - result.vpc.cidr_block_association_set[2].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"] - - result.vpc.cidr_block_association_set[2].cidr_block_state.state in ["associated", "associating"] - - vpc_cidr in (result.vpc | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (result.vpc | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b in (result.vpc | json_query("cidr_block_association_set[*].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-") - - vpc_info.vpcs[0].cidr_block_association_set[2].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_info.vpcs[0].cidr_block_association_set[2].cidr_block_state.state in ["associated", "associating"] - - vpc_cidr in (vpc_info.vpcs[0] | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (vpc_info.vpcs[0] | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b in (vpc_info.vpcs[0] | json_query("cidr_block_association_set[*].cidr_block") | list) - - - name: modify CIDR - no purge (no change - different order - check mode) - ec2_vpc_net: - state: present - cidr_block: - - "{{ vpc_cidr }}" - - "{{ vpc_cidr_b }}" - - "{{ vpc_cidr_a }}" - name: "{{ resource_prefix }}" - register: result - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - - name: assert the CIDRs didn't change - assert: - that: - - result is successful - - result is not changed - - vpc_info.vpcs | length == 1 - - result.vpc.cidr_block == vpc_cidr - - vpc_info.vpcs[0].cidr_block == vpc_cidr - - result.vpc.cidr_block_association_set | length == 3 - - result.vpc.cidr_block_association_set[0].association_id.startswith("vpc-cidr-assoc-") - - result.vpc.cidr_block_association_set[1].association_id.startswith("vpc-cidr-assoc-") - - result.vpc.cidr_block_association_set[2].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"] - - result.vpc.cidr_block_association_set[2].cidr_block_state.state in ["associated", "associating"] - - vpc_cidr in (result.vpc | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (result.vpc | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b in (result.vpc | json_query("cidr_block_association_set[*].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-") - - vpc_info.vpcs[0].cidr_block_association_set[2].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_info.vpcs[0].cidr_block_association_set[2].cidr_block_state.state in ["associated", "associating"] - - vpc_cidr in (vpc_info.vpcs[0] | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (vpc_info.vpcs[0] | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b in (vpc_info.vpcs[0] | json_query("cidr_block_association_set[*].cidr_block") | list) - - - name: modify CIDR - no purge (no change - different order) - ec2_vpc_net: - state: present - cidr_block: - - "{{ vpc_cidr }}" - - "{{ vpc_cidr_b }}" - - "{{ vpc_cidr_a }}" - name: "{{ resource_prefix }}" - register: result - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - register: vpc_info - - - name: assert the CIDRs didn't change - assert: - that: - - result is successful - - result is not changed - - vpc_info.vpcs | length == 1 - - result.vpc.cidr_block == vpc_cidr - - vpc_info.vpcs[0].cidr_block == vpc_cidr - - result.vpc.cidr_block_association_set | length == 3 - - result.vpc.cidr_block_association_set[0].association_id.startswith("vpc-cidr-assoc-") - - result.vpc.cidr_block_association_set[1].association_id.startswith("vpc-cidr-assoc-") - - result.vpc.cidr_block_association_set[2].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"] - - result.vpc.cidr_block_association_set[2].cidr_block_state.state in ["associated", "associating"] - - vpc_cidr in (result.vpc | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (result.vpc | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b in (result.vpc | json_query("cidr_block_association_set[*].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-") - - vpc_info.vpcs[0].cidr_block_association_set[2].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_info.vpcs[0].cidr_block_association_set[2].cidr_block_state.state in ["associated", "associating"] - - vpc_cidr in (vpc_info.vpcs[0] | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_a in (vpc_info.vpcs[0] | json_query("cidr_block_association_set[*].cidr_block") | list) - - vpc_cidr_b in (vpc_info.vpcs[0] | json_query("cidr_block_association_set[*].cidr_block") | list) - - # #62678 - #- name: modify CIDR - purge (check mode) - # ec2_vpc_net: - # state: present - # cidr_block: - # - "{{ vpc_cidr }}" - # - "{{ vpc_cidr_b }}" - # name: "{{ resource_prefix }}" - # purge_cidrs: yes - # check_mode: true - # register: result - #- ec2_vpc_net_info: - # filters: - # "tag:Name": "{{ resource_prefix }}" - # register: vpc_info - - #- name: Check the CIDRs weren't changed - # assert: - # that: - # - result is successful - # - result is changed - # - vpc_info.vpcs | length == 1 - # - vpc_info.vpcs[0].cidr_block == vpc_cidr - # - 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-") - # - vpc_info.vpcs[0].cidr_block_association_set[2].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_info.vpcs[0].cidr_block_association_set[2].cidr_block_state.state in ["associated", "associating"] - # - vpc_cidr in (vpc_info.vpcs[0] | json_query("cidr_block_association_set[*].cidr_block") | list) - # - vpc_cidr_a in (vpc_info.vpcs[0] | json_query("cidr_block_association_set[*].cidr_block") | list) - # - vpc_cidr_b in (vpc_info.vpcs[0] | json_query("cidr_block_association_set[*].cidr_block") | list) - - - name: modify CIDR - purge - ec2_vpc_net: - state: present - cidr_block: - - "{{ vpc_cidr }}" - - "{{ vpc_cidr_b }}" - name: "{{ resource_prefix }}" - purge_cidrs: yes - register: result - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - 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 - - result is changed - - result.vpc.id == vpc_1 - - vpc_info.vpcs | length == 1 - - result.vpc.cidr_block == vpc_cidr - - vpc_info.vpcs[0].cidr_block == vpc_cidr - - result.vpc | json_query(cidr_query) | list | length == 2 - - vpc_cidr in (result.vpc | json_query(cidr_query) | list) - - vpc_cidr_a not in (result.vpc | json_query(cidr_query) | list) - - vpc_cidr_b in (result.vpc | json_query(cidr_query) | list) - - vpc_info.vpcs[0] | json_query(cidr_query) | list | length == 2 - - vpc_cidr in (vpc_info.vpcs[0] | json_query(cidr_query) | list) - - vpc_cidr_a not in (vpc_info.vpcs[0] | json_query(cidr_query) | list) - - vpc_cidr_b in (vpc_info.vpcs[0] | json_query(cidr_query) | list) - - - name: modify CIDR - purge (no change) - ec2_vpc_net: - state: present - cidr_block: - - "{{ vpc_cidr }}" - - "{{ vpc_cidr_b }}" - name: "{{ resource_prefix }}" - purge_cidrs: yes - register: result - - ec2_vpc_net_info: - filters: - "tag:Name": "{{ resource_prefix }}" - 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 - - result is not changed - - result.vpc.id == vpc_1 - - vpc_info.vpcs | length == 1 - - result.vpc.cidr_block == vpc_cidr - - vpc_info.vpcs[0].cidr_block == vpc_cidr - - result.vpc | json_query(cidr_query) | list | length == 2 - - vpc_cidr in (result.vpc | json_query(cidr_query) | list) - - vpc_cidr_a not in (result.vpc | json_query(cidr_query) | list) - - vpc_cidr_b in (result.vpc | json_query(cidr_query) | list) - - vpc_info.vpcs[0] | json_query(cidr_query) | list | length == 2 - - vpc_cidr in (vpc_info.vpcs[0] | json_query(cidr_query) | list) - - vpc_cidr_a not in (vpc_info.vpcs[0] | json_query(cidr_query) | list) - - vpc_cidr_b in (vpc_info.vpcs[0] | json_query(cidr_query) | list) - - # ============================================================ - - - name: test check mode to delete a VPC - ec2_vpc_net: - cidr_block: "{{ vpc_cidr }}" - name: "{{ resource_prefix }}" - state: absent - check_mode: true - register: result - - - name: assert that a change would have been made - assert: - that: - - result is changed - - # ============================================================ - - always: - - - name: replace the DHCP options set so the new one can be deleted - ec2_vpc_net: - cidr_block: "{{ vpc_cidr }}" - name: "{{ resource_prefix }}" - state: present - multi_ok: no - dhcp_opts_id: "{{ default_dhcp_options_id }}" - ignore_errors: true - - - name: remove the DHCP option set - ec2_vpc_dhcp_option: - dhcp_options_id: "{{ new_dhcp.dhcp_options_id }}" - state: absent - ignore_errors: true - - - name: remove the VPC - ec2_vpc_net: - cidr_block: "{{ vpc_cidr }}" - name: "{{ resource_prefix }}" - state: absent - ignore_errors: true diff --git a/test/integration/targets/ec2_vpc_subnet/aliases b/test/integration/targets/ec2_vpc_subnet/aliases deleted file mode 100644 index 5e7a8d3877..0000000000 --- a/test/integration/targets/ec2_vpc_subnet/aliases +++ /dev/null @@ -1,3 +0,0 @@ -cloud/aws -shippable/aws/group2 -unstable diff --git a/test/integration/targets/ec2_vpc_subnet/defaults/main.yml b/test/integration/targets/ec2_vpc_subnet/defaults/main.yml deleted file mode 100644 index 9c529aff02..0000000000 --- a/test/integration/targets/ec2_vpc_subnet/defaults/main.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -# defaults file for ec2_vpc_subnet -ec2_vpc_subnet_name: '{{resource_prefix}}' -ec2_vpc_subnet_description: 'Created by ansible integration tests' diff --git a/test/integration/targets/ec2_vpc_subnet/meta/main.yml b/test/integration/targets/ec2_vpc_subnet/meta/main.yml deleted file mode 100644 index 1f64f1169a..0000000000 --- a/test/integration/targets/ec2_vpc_subnet/meta/main.yml +++ /dev/null @@ -1,3 +0,0 @@ -dependencies: - - prepare_tests - - setup_ec2 diff --git a/test/integration/targets/ec2_vpc_subnet/tasks/main.yml b/test/integration/targets/ec2_vpc_subnet/tasks/main.yml deleted file mode 100644 index fa79901db0..0000000000 --- a/test/integration/targets/ec2_vpc_subnet/tasks/main.yml +++ /dev/null @@ -1,618 +0,0 @@ ---- -- module_defaults: - group/aws: - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" - security_token: "{{ security_token | default(omit) }}" - region: "{{ aws_region }}" - block: - - - name: list available AZs - aws_az_info: - register: region_azs - - - name: pick an AZ for testing - set_fact: - subnet_az: "{{ region_azs.availability_zones[0].zone_name }}" - - # ============================================================ - - name: create a VPC - ec2_vpc_net: - name: "{{ resource_prefix }}-vpc" - state: present - cidr_block: "10.232.232.128/26" - ipv6_cidr: True - tags: - Name: "{{ resource_prefix }}-vpc" - Description: "Created by ansible-test" - register: vpc_result - - - set_fact: - vpc_ipv6_cidr: "{{ vpc_result.vpc.ipv6_cidr_block_association_set[0].ipv6_cidr_block }}" - - # ============================================================ - - name: create subnet (expected changed=true) (CHECK MODE) - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - az: "{{ subnet_az }}" - vpc_id: "{{ vpc_result.vpc.id }}" - tags: - Name: '{{ec2_vpc_subnet_name}}' - Description: '{{ec2_vpc_subnet_description}}' - state: present - check_mode: true - register: vpc_subnet_create - - - name: assert creation would happen - assert: - that: - - vpc_subnet_create is changed - - - name: create subnet (expected changed=true) - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - az: "{{ subnet_az }}" - vpc_id: "{{ vpc_result.vpc.id }}" - tags: - Name: '{{ec2_vpc_subnet_name}}' - Description: '{{ec2_vpc_subnet_description}}' - state: present - register: vpc_subnet_create - - - name: assert creation happened (expected changed=true) - assert: - that: - - 'vpc_subnet_create' - - 'vpc_subnet_create.subnet.id.startswith("subnet-")' - - '"Name" in vpc_subnet_create.subnet.tags and vpc_subnet_create.subnet.tags["Name"] == ec2_vpc_subnet_name' - - '"Description" in vpc_subnet_create.subnet.tags and vpc_subnet_create.subnet.tags["Description"] == ec2_vpc_subnet_description' - # ============================================================ - - name: recreate subnet (expected changed=false) (CHECK MODE) - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - az: "{{ subnet_az }}" - vpc_id: "{{ vpc_result.vpc.id }}" - tags: - Name: '{{ec2_vpc_subnet_name}}' - Description: '{{ec2_vpc_subnet_description}}' - state: present - check_mode: true - register: vpc_subnet_recreate - - - name: assert recreation changed nothing (expected changed=false) - assert: - that: - - vpc_subnet_recreate is not changed - - - name: recreate subnet (expected changed=false) - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - az: "{{ subnet_az }}" - vpc_id: "{{ vpc_result.vpc.id }}" - tags: - Name: '{{ec2_vpc_subnet_name}}' - Description: '{{ec2_vpc_subnet_description}}' - state: present - register: vpc_subnet_recreate - - - name: assert recreation changed nothing (expected changed=false) - assert: - that: - - vpc_subnet_recreate is not changed - - 'vpc_subnet_recreate.subnet == vpc_subnet_create.subnet' - - # ============================================================ - - name: update subnet so instances launched in it are assigned an IP (CHECK MODE) - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - az: "{{ subnet_az }}" - vpc_id: "{{ vpc_result.vpc.id }}" - tags: - Name: '{{ec2_vpc_subnet_name}}' - Description: '{{ec2_vpc_subnet_description}}' - state: present - map_public: true - check_mode: true - register: vpc_subnet_modify - - - name: assert subnet changed - assert: - that: - - vpc_subnet_modify is changed - - - name: update subnet so instances launched in it are assigned an IP - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - az: "{{ subnet_az }}" - vpc_id: "{{ vpc_result.vpc.id }}" - tags: - Name: '{{ec2_vpc_subnet_name}}' - Description: '{{ec2_vpc_subnet_description}}' - state: present - map_public: true - register: vpc_subnet_modify - - - name: assert subnet changed - assert: - that: - - vpc_subnet_modify is changed - - vpc_subnet_modify.subnet.map_public_ip_on_launch - - # ============================================================ - - name: add invalid ipv6 block to subnet (expected failed) - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - az: "{{ subnet_az }}" - vpc_id: "{{ vpc_result.vpc.id }}" - ipv6_cidr: 2001:db8::/64 - tags: - Name: '{{ec2_vpc_subnet_name}}' - Description: '{{ec2_vpc_subnet_description}}' - state: present - register: vpc_subnet_ipv6_failed - ignore_errors: yes - - - name: assert failure happened (expected failed) - assert: - that: - - vpc_subnet_ipv6_failed is failed - - "'Couldn\\'t associate ipv6 cidr' in vpc_subnet_ipv6_failed.msg" - - # ============================================================ - - name: add a tag (expected changed=true) (CHECK MODE) - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - az: "{{ subnet_az }}" - vpc_id: "{{ vpc_result.vpc.id }}" - tags: - Name: '{{ec2_vpc_subnet_name}}' - Description: '{{ec2_vpc_subnet_description}}' - AnotherTag: SomeValue - state: present - check_mode: true - register: vpc_subnet_add_a_tag - - - name: assert tag addition happened (expected changed=true) - assert: - that: - - vpc_subnet_add_a_tag is changed - - - name: add a tag (expected changed=true) - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - az: "{{ subnet_az }}" - vpc_id: "{{ vpc_result.vpc.id }}" - tags: - Name: '{{ec2_vpc_subnet_name}}' - Description: '{{ec2_vpc_subnet_description}}' - AnotherTag: SomeValue - state: present - register: vpc_subnet_add_a_tag - - - name: assert tag addition happened (expected changed=true) - assert: - that: - - vpc_subnet_add_a_tag is changed - - '"Name" in vpc_subnet_add_a_tag.subnet.tags and vpc_subnet_add_a_tag.subnet.tags["Name"] == ec2_vpc_subnet_name' - - '"Description" in vpc_subnet_add_a_tag.subnet.tags and vpc_subnet_add_a_tag.subnet.tags["Description"] == ec2_vpc_subnet_description' - - '"AnotherTag" in vpc_subnet_add_a_tag.subnet.tags and vpc_subnet_add_a_tag.subnet.tags["AnotherTag"] == "SomeValue"' - - # ============================================================ - - name: remove tags with default purge_tags=true (expected changed=true) (CHECK MODE) - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - az: "{{ subnet_az }}" - vpc_id: "{{ vpc_result.vpc.id }}" - tags: - AnotherTag: SomeValue - state: present - check_mode: true - register: vpc_subnet_remove_tags - - - name: assert tag removal happened (expected changed=true) - assert: - that: - - vpc_subnet_remove_tags is changed - - - name: remove tags with default purge_tags=true (expected changed=true) - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - az: "{{ subnet_az }}" - vpc_id: "{{ vpc_result.vpc.id }}" - tags: - AnotherTag: SomeValue - state: present - register: vpc_subnet_remove_tags - - - name: assert tag removal happened (expected changed=true) - assert: - that: - - vpc_subnet_remove_tags is changed - - '"Name" not in vpc_subnet_remove_tags.subnet.tags' - - '"Description" not in vpc_subnet_remove_tags.subnet.tags' - - '"AnotherTag" in vpc_subnet_remove_tags.subnet.tags and vpc_subnet_remove_tags.subnet.tags["AnotherTag"] == "SomeValue"' - - # ============================================================ - - name: change tags with purge_tags=false (expected changed=true) (CHECK MODE) - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - az: "{{ subnet_az }}" - vpc_id: "{{ vpc_result.vpc.id }}" - tags: - Name: '{{ec2_vpc_subnet_name}}' - Description: '{{ec2_vpc_subnet_description}}' - state: present - purge_tags: false - check_mode: true - register: vpc_subnet_change_tags - - - name: assert tag addition happened (expected changed=true) - assert: - that: - - vpc_subnet_change_tags is changed - - - name: change tags with purge_tags=false (expected changed=true) - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - az: "{{ subnet_az }}" - vpc_id: "{{ vpc_result.vpc.id }}" - tags: - Name: '{{ec2_vpc_subnet_name}}' - Description: '{{ec2_vpc_subnet_description}}' - state: present - purge_tags: false - register: vpc_subnet_change_tags - - - name: assert tag addition happened (expected changed=true) - assert: - that: - - vpc_subnet_change_tags is changed - - '"Name" in vpc_subnet_change_tags.subnet.tags and vpc_subnet_change_tags.subnet.tags["Name"] == ec2_vpc_subnet_name' - - '"Description" in vpc_subnet_change_tags.subnet.tags and vpc_subnet_change_tags.subnet.tags["Description"] == ec2_vpc_subnet_description' - - '"AnotherTag" in vpc_subnet_change_tags.subnet.tags and vpc_subnet_change_tags.subnet.tags["AnotherTag"] == "SomeValue"' - - # ============================================================ - - name: test state=absent (expected changed=true) (CHECK MODE) - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - vpc_id: "{{ vpc_result.vpc.id }}" - state: absent - check_mode: true - register: result - - - name: assert state=absent (expected changed=true) - assert: - that: - - result is changed - - - name: test state=absent (expected changed=true) - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - vpc_id: "{{ vpc_result.vpc.id }}" - state: absent - register: result - - - name: assert state=absent (expected changed=true) - assert: - that: - - result is changed - - # ============================================================ - - name: test state=absent (expected changed=false) (CHECK MODE) - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - vpc_id: "{{ vpc_result.vpc.id }}" - state: absent - check_mode: true - register: result - - - name: assert state=absent (expected changed=false) - assert: - that: - - result is not changed - - - name: test state=absent (expected changed=false) - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - vpc_id: "{{ vpc_result.vpc.id }}" - state: absent - register: result - - - name: assert state=absent (expected changed=false) - assert: - that: - - result is not changed - - # ============================================================ - - name: create subnet without AZ (CHECK MODE) - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - vpc_id: "{{ vpc_result.vpc.id }}" - state: present - check_mode: true - register: subnet_without_az - - - name: check that subnet without AZ works fine - assert: - that: - - subnet_without_az is changed - - - name: create subnet without AZ - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - vpc_id: "{{ vpc_result.vpc.id }}" - state: present - register: subnet_without_az - - - name: check that subnet without AZ works fine - assert: - that: - - subnet_without_az is changed - - # ============================================================ - - name: remove subnet without AZ (CHECK MODE) - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - vpc_id: "{{ vpc_result.vpc.id }}" - state: absent - check_mode: true - register: result - - - name: assert state=absent (expected changed=true) - assert: - that: - - result is changed - - - name: remove subnet without AZ - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - vpc_id: "{{ vpc_result.vpc.id }}" - state: absent - register: result - - - name: assert state=absent (expected changed=true) - assert: - that: - - result is changed - - - # ============================================================ - - name: create subnet with IPv6 (expected changed=true) (CHECK MODE) - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - vpc_id: "{{ vpc_result.vpc.id }}" - ipv6_cidr: "{{ vpc_ipv6_cidr | regex_replace('::/56', '::/64') }}" - assign_instances_ipv6: true - state: present - tags: - Name: '{{ec2_vpc_subnet_name}}' - Description: '{{ec2_vpc_subnet_description}}' - check_mode: true - register: vpc_subnet_ipv6_create - - - name: assert creation with IPv6 happened (expected changed=true) - assert: - that: - - vpc_subnet_ipv6_create is changed - - - name: create subnet with IPv6 (expected changed=true) - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - vpc_id: "{{ vpc_result.vpc.id }}" - ipv6_cidr: "{{ vpc_ipv6_cidr | regex_replace('::/56', '::/64') }}" - assign_instances_ipv6: true - state: present - tags: - Name: '{{ec2_vpc_subnet_name}}' - Description: '{{ec2_vpc_subnet_description}}' - register: vpc_subnet_ipv6_create - - - name: assert creation with IPv6 happened (expected changed=true) - assert: - that: - - vpc_subnet_ipv6_create is changed - - 'vpc_subnet_ipv6_create.subnet.id.startswith("subnet-")' - - "vpc_subnet_ipv6_create.subnet.ipv6_cidr_block == '{{ vpc_ipv6_cidr | regex_replace('::/56', '::/64') }}'" - - '"Name" in vpc_subnet_ipv6_create.subnet.tags and vpc_subnet_ipv6_create.subnet.tags["Name"] == ec2_vpc_subnet_name' - - '"Description" in vpc_subnet_ipv6_create.subnet.tags and vpc_subnet_ipv6_create.subnet.tags["Description"] == ec2_vpc_subnet_description' - - 'vpc_subnet_ipv6_create.subnet.assign_ipv6_address_on_creation' - - # ============================================================ - - name: recreate subnet (expected changed=false) (CHECK MODE) - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - vpc_id: "{{ vpc_result.vpc.id }}" - ipv6_cidr: "{{ vpc_ipv6_cidr | regex_replace('::/56', '::/64') }}" - assign_instances_ipv6: true - state: present - tags: - Name: '{{ec2_vpc_subnet_name}}' - Description: '{{ec2_vpc_subnet_description}}' - check_mode: true - register: vpc_subnet_ipv6_recreate - - - name: assert recreation changed nothing (expected changed=false) - assert: - that: - - vpc_subnet_ipv6_recreate is not changed - - - name: recreate subnet (expected changed=false) - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - vpc_id: "{{ vpc_result.vpc.id }}" - ipv6_cidr: "{{ vpc_ipv6_cidr | regex_replace('::/56', '::/64') }}" - assign_instances_ipv6: true - state: present - tags: - Name: '{{ec2_vpc_subnet_name}}' - Description: '{{ec2_vpc_subnet_description}}' - register: vpc_subnet_ipv6_recreate - - - name: assert recreation changed nothing (expected changed=false) - assert: - that: - - vpc_subnet_ipv6_recreate is not changed - - 'vpc_subnet_ipv6_recreate.subnet == vpc_subnet_ipv6_create.subnet' - - # ============================================================ - - name: change subnet ipv6 attribute (expected changed=true) (CHECK MODE) - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - vpc_id: "{{ vpc_result.vpc.id }}" - ipv6_cidr: "{{ vpc_ipv6_cidr | regex_replace('::/56', '::/64') }}" - assign_instances_ipv6: false - state: present - purge_tags: false - check_mode: true - register: vpc_change_attribute - - - name: assert assign_instances_ipv6 attribute changed (expected changed=true) - assert: - that: - - vpc_change_attribute is changed - - - name: change subnet ipv6 attribute (expected changed=true) - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - vpc_id: "{{ vpc_result.vpc.id }}" - ipv6_cidr: "{{ vpc_ipv6_cidr | regex_replace('::/56', '::/64') }}" - assign_instances_ipv6: false - state: present - purge_tags: false - register: vpc_change_attribute - - - name: assert assign_instances_ipv6 attribute changed (expected changed=true) - assert: - that: - - vpc_change_attribute is changed - - 'not vpc_change_attribute.subnet.assign_ipv6_address_on_creation' - - # ============================================================ - - name: add second subnet with duplicate ipv6 cidr (expected failure) - ec2_vpc_subnet: - cidr: "10.232.232.144/28" - vpc_id: "{{ vpc_result.vpc.id }}" - ipv6_cidr: "{{ vpc_ipv6_cidr | regex_replace('::/56', '::/64') }}" - state: present - purge_tags: false - register: vpc_add_duplicate_ipv6 - ignore_errors: true - - - name: assert graceful failure (expected failed) - assert: - that: - - vpc_add_duplicate_ipv6 is failed - - "'The IPv6 CIDR \\'{{ vpc_ipv6_cidr | regex_replace('::/56', '::/64') }}\\' conflicts with another subnet' in vpc_add_duplicate_ipv6.msg" - - # ============================================================ - - name: remove subnet ipv6 cidr (expected changed=true) (CHECK MODE) - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - vpc_id: "{{ vpc_result.vpc.id }}" - state: present - purge_tags: false - check_mode: true - register: vpc_remove_ipv6_cidr - - - name: assert subnet ipv6 cidr removed (expected changed=true) - assert: - that: - - vpc_remove_ipv6_cidr is changed - - - name: remove subnet ipv6 cidr (expected changed=true) - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - vpc_id: "{{ vpc_result.vpc.id }}" - state: present - purge_tags: false - register: vpc_remove_ipv6_cidr - - - name: assert subnet ipv6 cidr removed (expected changed=true) - assert: - that: - - vpc_remove_ipv6_cidr is changed - - "vpc_remove_ipv6_cidr.subnet.ipv6_cidr_block == ''" - - 'not vpc_remove_ipv6_cidr.subnet.assign_ipv6_address_on_creation' - - # ============================================================ - - name: test adding a tag that looks like a boolean to the subnet (CHECK MODE) - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - vpc_id: "{{ vpc_result.vpc.id }}" - state: present - purge_tags: false - tags: - looks_like_boolean: true - check_mode: true - register: vpc_subnet_info - - - name: assert a tag was added - assert: - that: - - vpc_subnet_info is changed - - - name: test adding a tag that looks like a boolean to the subnet - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - vpc_id: "{{ vpc_result.vpc.id }}" - state: present - purge_tags: false - tags: - looks_like_boolean: true - register: vpc_subnet_info - - - name: assert a tag was added - assert: - that: - - vpc_subnet_info is changed - - 'vpc_subnet_info.subnet.tags.looks_like_boolean == "True"' - - # ============================================================ - - name: test idempotence adding a tag that looks like a boolean (CHECK MODE) - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - vpc_id: "{{ vpc_result.vpc.id }}" - state: present - purge_tags: false - tags: - looks_like_boolean: true - check_mode: true - register: vpc_subnet_info - - - name: assert tags haven't changed - assert: - that: - - vpc_subnet_info is not changed - - - name: test idempotence adding a tag that looks like a boolean - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - vpc_id: "{{ vpc_result.vpc.id }}" - state: present - purge_tags: false - tags: - looks_like_boolean: true - register: vpc_subnet_info - - - name: assert tags haven't changed - assert: - that: - - vpc_subnet_info is not changed - - always: - - ################################################ - # TEARDOWN STARTS HERE - ################################################ - - - name: tidy up subnet - ec2_vpc_subnet: - cidr: "10.232.232.128/28" - vpc_id: "{{ vpc_result.vpc.id }}" - state: absent - - - name: tidy up VPC - ec2_vpc_net: - name: "{{ resource_prefix }}-vpc" - state: absent - cidr_block: "10.232.232.128/26" diff --git a/test/integration/targets/inventory_aws_ec2/aliases b/test/integration/targets/inventory_aws_ec2/aliases deleted file mode 100644 index a112c3d1bb..0000000000 --- a/test/integration/targets/inventory_aws_ec2/aliases +++ /dev/null @@ -1,2 +0,0 @@ -cloud/aws -shippable/aws/group1 diff --git a/test/integration/targets/inventory_aws_ec2/playbooks/create_inventory_config.yml b/test/integration/targets/inventory_aws_ec2/playbooks/create_inventory_config.yml deleted file mode 100644 index 8680c38d01..0000000000 --- a/test/integration/targets/inventory_aws_ec2/playbooks/create_inventory_config.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -- hosts: 127.0.0.1 - connection: local - gather_facts: no - vars: - template_name: "../templates/{{ template | default('inventory.yml') }}" - tasks: - - name: write inventory config file - copy: - dest: ../test.aws_ec2.yml - content: "{{ lookup('template', template_name) }}" diff --git a/test/integration/targets/inventory_aws_ec2/playbooks/empty_inventory_config.yml b/test/integration/targets/inventory_aws_ec2/playbooks/empty_inventory_config.yml deleted file mode 100644 index f67fff1a93..0000000000 --- a/test/integration/targets/inventory_aws_ec2/playbooks/empty_inventory_config.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -- hosts: 127.0.0.1 - connection: local - gather_facts: no - tasks: - - name: write inventory config file - copy: - dest: ../test.aws_ec2.yml - content: "" diff --git a/test/integration/targets/inventory_aws_ec2/playbooks/populate_cache.yml b/test/integration/targets/inventory_aws_ec2/playbooks/populate_cache.yml deleted file mode 100644 index 07b0eec4c5..0000000000 --- a/test/integration/targets/inventory_aws_ec2/playbooks/populate_cache.yml +++ /dev/null @@ -1,64 +0,0 @@ ---- -- hosts: 127.0.0.1 - connection: local - gather_facts: no - environment: "{{ ansible_test.environment }}" - tasks: - - - block: - - # Create VPC, subnet, security group, and find image_id to create instance - - - include_tasks: setup.yml - - - name: assert group was populated with inventory but is empty - assert: - that: - - "'aws_ec2' in groups" - - "not groups.aws_ec2" - - # Create new host, add it to inventory and then terminate it without updating the cache - - - name: set connection information for all tasks - set_fact: - aws_connection_info: &aws_connection_info - aws_access_key: '{{ aws_access_key }}' - aws_secret_key: '{{ aws_secret_key }}' - security_token: '{{ security_token }}' - region: '{{ aws_region }}' - no_log: yes - - - name: create a new host - ec2: - image: '{{ image_id }}' - exact_count: 1 - count_tag: - Name: '{{ resource_prefix }}' - instance_tags: - Name: '{{ resource_prefix }}' - instance_type: t2.micro - wait: yes - group_id: '{{ sg_id }}' - vpc_subnet_id: '{{ subnet_id }}' - <<: *aws_connection_info - register: setup_instance - - - meta: refresh_inventory - - always: - - - name: remove setup ec2 instance - ec2: - instance_type: t2.micro - instance_ids: '{{ setup_instance.instance_ids }}' - state: absent - wait: yes - instance_tags: - Name: '{{ resource_prefix }}' - group_id: '{{ sg_id }}' - vpc_subnet_id: '{{ subnet_id }}' - <<: *aws_connection_info - ignore_errors: yes - when: setup_instance is defined - - - include_tasks: tear_down.yml diff --git a/test/integration/targets/inventory_aws_ec2/playbooks/setup.yml b/test/integration/targets/inventory_aws_ec2/playbooks/setup.yml deleted file mode 100644 index 8a9b88937f..0000000000 --- a/test/integration/targets/inventory_aws_ec2/playbooks/setup.yml +++ /dev/null @@ -1,62 +0,0 @@ -- name: set connection information for all tasks - set_fact: - aws_connection_info: &aws_connection_info - aws_access_key: '{{ aws_access_key }}' - aws_secret_key: '{{ aws_secret_key }}' - security_token: '{{ security_token }}' - region: '{{ aws_region }}' - no_log: yes - -- name: get image ID to create an instance - ec2_ami_info: - filters: - architecture: x86_64 - owner-id: '125523088429' - virtualization-type: hvm - root-device-type: ebs - name: 'Fedora-Atomic-27*' - <<: *aws_connection_info - register: fedora_images - -- set_fact: - image_id: '{{ fedora_images.images.0.image_id }}' - -- name: create a VPC to work in - ec2_vpc_net: - cidr_block: 10.10.0.0/24 - state: present - name: '{{ resource_prefix }}_setup' - resource_tags: - Name: '{{ resource_prefix }}_setup' - <<: *aws_connection_info - register: setup_vpc - -- set_fact: - vpc_id: '{{ setup_vpc.vpc.id }}' - -- name: create a subnet to use for creating an ec2 instance - ec2_vpc_subnet: - az: '{{ aws_region }}a' - tags: '{{ resource_prefix }}_setup' - vpc_id: '{{ setup_vpc.vpc.id }}' - cidr: 10.10.0.0/24 - state: present - resource_tags: - Name: '{{ resource_prefix }}_setup' - <<: *aws_connection_info - register: setup_subnet - -- set_fact: - subnet_id: '{{ setup_subnet.subnet.id }}' - -- name: create a security group to use for creating an ec2 instance - ec2_group: - name: '{{ resource_prefix }}_setup' - description: 'created by Ansible integration tests' - state: present - vpc_id: '{{ setup_vpc.vpc.id }}' - <<: *aws_connection_info - register: setup_sg - -- set_fact: - sg_id: '{{ setup_sg.group_id }}' diff --git a/test/integration/targets/inventory_aws_ec2/playbooks/tear_down.yml b/test/integration/targets/inventory_aws_ec2/playbooks/tear_down.yml deleted file mode 100644 index 4c8240e46d..0000000000 --- a/test/integration/targets/inventory_aws_ec2/playbooks/tear_down.yml +++ /dev/null @@ -1,39 +0,0 @@ -- name: set connection information for all tasks - set_fact: - aws_connection_info: &aws_connection_info - aws_access_key: '{{ aws_access_key }}' - aws_secret_key: '{{ aws_secret_key }}' - security_token: '{{ security_token }}' - region: '{{ aws_region }}' - no_log: yes - -- name: remove setup security group - ec2_group: - name: '{{ resource_prefix }}_setup' - description: 'created by Ansible integration tests' - state: absent - vpc_id: '{{ vpc_id }}' - <<: *aws_connection_info - ignore_errors: yes - -- name: remove setup subnet - ec2_vpc_subnet: - az: '{{ aws_region }}a' - tags: '{{ resource_prefix }}_setup' - vpc_id: '{{ vpc_id }}' - cidr: 10.10.0.0/24 - state: absent - resource_tags: - Name: '{{ resource_prefix }}_setup' - <<: *aws_connection_info - ignore_errors: yes - -- name: remove setup VPC - ec2_vpc_net: - cidr_block: 10.10.0.0/24 - state: absent - name: '{{ resource_prefix }}_setup' - resource_tags: - Name: '{{ resource_prefix }}_setup' - <<: *aws_connection_info - ignore_errors: yes diff --git a/test/integration/targets/inventory_aws_ec2/playbooks/test_invalid_aws_ec2_inventory_config.yml b/test/integration/targets/inventory_aws_ec2/playbooks/test_invalid_aws_ec2_inventory_config.yml deleted file mode 100644 index cc1b9a5a5e..0000000000 --- a/test/integration/targets/inventory_aws_ec2/playbooks/test_invalid_aws_ec2_inventory_config.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -- hosts: 127.0.0.1 - connection: local - gather_facts: no - tasks: - - name: assert inventory was not populated by aws_ec2 inventory plugin - assert: - that: - - "'aws_ec2' not in groups" diff --git a/test/integration/targets/inventory_aws_ec2/playbooks/test_inventory_cache.yml b/test/integration/targets/inventory_aws_ec2/playbooks/test_inventory_cache.yml deleted file mode 100644 index d83cb0bfe6..0000000000 --- a/test/integration/targets/inventory_aws_ec2/playbooks/test_inventory_cache.yml +++ /dev/null @@ -1,18 +0,0 @@ ---- -- hosts: 127.0.0.1 - connection: local - gather_facts: no - tasks: - - name: assert cache was used to populate inventory - assert: - that: - - "'aws_ec2' in groups" - - "groups.aws_ec2 | length == 1" - - - meta: refresh_inventory - - - name: assert refresh_inventory updated the cache - assert: - that: - - "'aws_ec2' in groups" - - "not groups.aws_ec2" diff --git a/test/integration/targets/inventory_aws_ec2/playbooks/test_populating_inventory.yml b/test/integration/targets/inventory_aws_ec2/playbooks/test_populating_inventory.yml deleted file mode 100644 index 73a67db065..0000000000 --- a/test/integration/targets/inventory_aws_ec2/playbooks/test_populating_inventory.yml +++ /dev/null @@ -1,91 +0,0 @@ ---- -- hosts: 127.0.0.1 - connection: local - gather_facts: no - environment: "{{ ansible_test.environment }}" - tasks: - - - block: - - # Create VPC, subnet, security group, and find image_id to create instance - - - include_tasks: setup.yml - - - name: assert group was populated with inventory but is empty - assert: - that: - - "'aws_ec2' in groups" - - "not groups.aws_ec2" - - # Create new host, refresh inventory, remove host, refresh inventory - - - name: set connection information for all tasks - set_fact: - aws_connection_info: &aws_connection_info - aws_access_key: '{{ aws_access_key }}' - aws_secret_key: '{{ aws_secret_key }}' - security_token: '{{ security_token }}' - region: '{{ aws_region }}' - no_log: yes - - - name: create a new host - ec2: - image: '{{ image_id }}' - exact_count: 1 - count_tag: - Name: '{{ resource_prefix }}' - instance_tags: - Name: '{{ resource_prefix }}' - instance_type: t2.micro - wait: yes - group_id: '{{ sg_id }}' - vpc_subnet_id: '{{ subnet_id }}' - <<: *aws_connection_info - register: setup_instance - - - meta: refresh_inventory - - - name: assert group was populated with inventory and is no longer empty - assert: - that: - - "'aws_ec2' in groups" - - "groups.aws_ec2 | length == 1" - - "groups.aws_ec2.0 == '{{ resource_prefix }}'" - - - name: remove setup ec2 instance - ec2: - instance_type: t2.micro - instance_ids: '{{ setup_instance.instance_ids }}' - state: absent - wait: yes - instance_tags: - Name: '{{ resource_prefix }}' - group_id: '{{ sg_id }}' - vpc_subnet_id: '{{ subnet_id }}' - <<: *aws_connection_info - - - meta: refresh_inventory - - - name: assert group was populated with inventory but is empty - assert: - that: - - "'aws_ec2' in groups" - - "not groups.aws_ec2" - - always: - - - name: remove setup ec2 instance - ec2: - instance_type: t2.micro - instance_ids: '{{ setup_instance.instance_ids }}' - state: absent - wait: yes - instance_tags: - Name: '{{ resource_prefix }}' - group_id: '{{ sg_id }}' - vpc_subnet_id: '{{ subnet_id }}' - <<: *aws_connection_info - ignore_errors: yes - when: setup_instance is defined - - - include_tasks: tear_down.yml diff --git a/test/integration/targets/inventory_aws_ec2/playbooks/test_populating_inventory_with_constructed.yml b/test/integration/targets/inventory_aws_ec2/playbooks/test_populating_inventory_with_constructed.yml deleted file mode 100644 index fdeeeeff42..0000000000 --- a/test/integration/targets/inventory_aws_ec2/playbooks/test_populating_inventory_with_constructed.yml +++ /dev/null @@ -1,79 +0,0 @@ ---- -- hosts: 127.0.0.1 - connection: local - gather_facts: no - environment: "{{ ansible_test.environment }}" - tasks: - - - block: - - # Create VPC, subnet, security group, and find image_id to create instance - - - include_tasks: setup.yml - - # Create new host, refresh inventory - - - name: set connection information for all tasks - set_fact: - aws_connection_info: &aws_connection_info - aws_access_key: '{{ aws_access_key }}' - aws_secret_key: '{{ aws_secret_key }}' - security_token: '{{ security_token }}' - region: '{{ aws_region }}' - no_log: yes - - - name: create a new host - ec2: - image: '{{ image_id }}' - exact_count: 1 - count_tag: - Name: '{{ resource_prefix }}' - instance_tags: - Name: '{{ resource_prefix }}' - tag1: value1 - tag2: value2 - instance_type: t2.micro - wait: yes - group_id: '{{ sg_id }}' - vpc_subnet_id: '{{ subnet_id }}' - <<: *aws_connection_info - register: setup_instance - - - meta: refresh_inventory - - - name: register the keyed sg group name - set_fact: - sg_group_name: "security_groups_{{ sg_id | replace('-', '_') }}" - - - name: register one of the keyed tag groups name - set_fact: - tag_group_name: "tag_Name_{{ resource_prefix | replace('-', '_') }}" - - - name: assert the keyed groups and groups from constructed config were added to inventory and composite var added to hostvars - assert: - that: - # There are 9 groups: all, ungrouped, aws_ec2, sg keyed group, 3 tag keyed group (one per tag), arch keyed group, constructed group - - "groups | length == 9" - - "groups[tag_group_name] | length == 1" - - "groups[sg_group_name] | length == 1" - - "groups.arch_x86_64 | length == 1" - - "groups.tag_with_name_key | length == 1" - - vars.hostvars[groups.aws_ec2.0]['test_compose_var_sum'] == 'value1value2' - - always: - - - name: remove setup ec2 instance - ec2: - instance_type: t2.micro - instance_ids: '{{ setup_instance.instance_ids }}' - state: absent - wait: yes - instance_tags: - Name: '{{ resource_prefix }}' - group_id: "{{ sg_id }}" - vpc_subnet_id: "{{ subnet_id }}" - <<: *aws_connection_info - ignore_errors: yes - when: setup_instance is defined - - - include_tasks: tear_down.yml diff --git a/test/integration/targets/inventory_aws_ec2/playbooks/test_refresh_inventory.yml b/test/integration/targets/inventory_aws_ec2/playbooks/test_refresh_inventory.yml deleted file mode 100644 index 6b46599b5b..0000000000 --- a/test/integration/targets/inventory_aws_ec2/playbooks/test_refresh_inventory.yml +++ /dev/null @@ -1,74 +0,0 @@ -- name: test updating inventory - block: - - name: assert group was populated with inventory but is empty - assert: - that: - - "'aws_ec2' in groups" - - "not groups.aws_ec2" - - - name: set connection information for all tasks - set_fact: - aws_connection_info: &aws_connection_info - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" - security_token: "{{ security_token }}" - region: "{{ aws_region }}" - no_log: yes - - - name: create a new host - ec2: - image: "{{ images[aws_region] }}" - exact_count: 1 - count_tag: - Name: '{{ resource_prefix }}' - instance_tags: - Name: '{{ resource_prefix }}' - instance_type: t2.micro - wait: yes - group_id: '{{ setup_sg.group_id }}' - vpc_subnet_id: '{{ setup_subnet.subnet.id }}' - <<: *aws_connection_info - register: setup_instance - - - meta: refresh_inventory - - - name: assert group was populated with inventory and is no longer empty - assert: - that: - - "'aws_ec2' in groups" - - "groups.aws_ec2 | length == 1" - - "groups.aws_ec2.0 == '{{ resource_prefix }}'" - - - name: remove setup ec2 instance - ec2: - instance_type: t2.micro - instance_ids: '{{ setup_instance.instance_ids }}' - state: absent - wait: yes - instance_tags: - Name: '{{ resource_prefix }}' - group_id: '{{ setup_sg.group_id }}' - vpc_subnet_id: '{{ setup_subnet.subnet.id }}' - <<: *aws_connection_info - - - meta: refresh_inventory - - - name: assert group was populated with inventory but is empty - assert: - that: - - "'aws_ec2' in groups" - - "not groups.aws_ec2" - - always: - - name: remove setup ec2 instance - ec2: - instance_type: t2.micro - instance_ids: '{{ setup_instance.instance_ids }}' - state: absent - wait: yes - instance_tags: - Name: '{{ resource_prefix }}' - group_id: '{{ setup_sg.group_id }}' - vpc_subnet_id: '{{ setup_subnet.subnet.id }}' - <<: *aws_connection_info - ignore_errors: yes diff --git a/test/integration/targets/inventory_aws_ec2/runme.sh b/test/integration/targets/inventory_aws_ec2/runme.sh deleted file mode 100755 index 916f7e8f7a..0000000000 --- a/test/integration/targets/inventory_aws_ec2/runme.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash - -set -eux - -# ensure test config is empty -ansible-playbook playbooks/empty_inventory_config.yml "$@" - -export ANSIBLE_INVENTORY_ENABLED=aws_ec2 - -# test with default inventory file -ansible-playbook playbooks/test_invalid_aws_ec2_inventory_config.yml "$@" - -export ANSIBLE_INVENTORY=test.aws_ec2.yml - -# test empty inventory config -ansible-playbook playbooks/test_invalid_aws_ec2_inventory_config.yml "$@" - -# generate inventory config and test using it -ansible-playbook playbooks/create_inventory_config.yml "$@" -ansible-playbook playbooks/test_populating_inventory.yml "$@" - -# generate inventory config with caching and test using it -ansible-playbook playbooks/create_inventory_config.yml -e "template='inventory_with_cache.yml'" "$@" -ansible-playbook playbooks/populate_cache.yml "$@" -ansible-playbook playbooks/test_inventory_cache.yml "$@" - -# remove inventory cache -rm -r aws_ec2_cache_dir/ - -# generate inventory config with constructed features and test using it -ansible-playbook playbooks/create_inventory_config.yml -e "template='inventory_with_constructed.yml'" "$@" -ansible-playbook playbooks/test_populating_inventory_with_constructed.yml "$@" - -# cleanup inventory config -ansible-playbook playbooks/empty_inventory_config.yml "$@" diff --git a/test/integration/targets/inventory_aws_ec2/templates/inventory.yml b/test/integration/targets/inventory_aws_ec2/templates/inventory.yml deleted file mode 100644 index 942edb309b..0000000000 --- a/test/integration/targets/inventory_aws_ec2/templates/inventory.yml +++ /dev/null @@ -1,12 +0,0 @@ -plugin: aws_ec2 -aws_access_key_id: '{{ aws_access_key }}' -aws_secret_access_key: '{{ aws_secret_key }}' -aws_security_token: '{{ security_token }}' -regions: - - '{{ aws_region }}' -filters: - tag:Name: - - '{{ resource_prefix }}' -hostnames: - - tag:Name - - dns-name diff --git a/test/integration/targets/inventory_aws_ec2/templates/inventory_with_cache.yml b/test/integration/targets/inventory_aws_ec2/templates/inventory_with_cache.yml deleted file mode 100644 index e35bf9010b..0000000000 --- a/test/integration/targets/inventory_aws_ec2/templates/inventory_with_cache.yml +++ /dev/null @@ -1,12 +0,0 @@ -plugin: aws_ec2 -cache: True -cache_plugin: jsonfile -cache_connection: aws_ec2_cache_dir -aws_access_key_id: '{{ aws_access_key }}' -aws_secret_access_key: '{{ aws_secret_key }}' -aws_security_token: '{{ security_token }}' -regions: - - '{{ aws_region }}' -filters: - tag:Name: - - '{{ resource_prefix }}' diff --git a/test/integration/targets/inventory_aws_ec2/templates/inventory_with_constructed.yml b/test/integration/targets/inventory_aws_ec2/templates/inventory_with_constructed.yml deleted file mode 100644 index 6befb4e339..0000000000 --- a/test/integration/targets/inventory_aws_ec2/templates/inventory_with_constructed.yml +++ /dev/null @@ -1,20 +0,0 @@ -plugin: aws_ec2 -aws_access_key_id: '{{ aws_access_key }}' -aws_secret_access_key: '{{ aws_secret_key }}' -aws_security_token: '{{ security_token }}' -regions: - - '{{ aws_region }}' -filters: - tag:Name: - - '{{ resource_prefix }}' -keyed_groups: - - key: 'security_groups|json_query("[].group_id")' - prefix: 'security_groups' - - key: 'tags' - prefix: 'tag' - - prefix: 'arch' - key: "architecture" -compose: - test_compose_var_sum: tags.tag1 + tags.tag2 -groups: - tag_with_name_key: "'Name' in (tags | list)" diff --git a/test/integration/targets/inventory_aws_ec2/test.aws_ec2.yml b/test/integration/targets/inventory_aws_ec2/test.aws_ec2.yml deleted file mode 100644 index e69de29bb2..0000000000 --- a/test/integration/targets/inventory_aws_ec2/test.aws_ec2.yml +++ /dev/null diff --git a/test/integration/targets/inventory_aws_rds/aliases b/test/integration/targets/inventory_aws_rds/aliases deleted file mode 100644 index 5692719518..0000000000 --- a/test/integration/targets/inventory_aws_rds/aliases +++ /dev/null @@ -1,2 +0,0 @@ -cloud/aws -unsupported diff --git a/test/integration/targets/inventory_aws_rds/playbooks/create_inventory_config.yml b/test/integration/targets/inventory_aws_rds/playbooks/create_inventory_config.yml deleted file mode 100644 index f0a9030a0f..0000000000 --- a/test/integration/targets/inventory_aws_rds/playbooks/create_inventory_config.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -- hosts: 127.0.0.1 - connection: local - gather_facts: no - vars: - template_name: "../templates/{{ template | default('inventory.j2') }}" - tasks: - - name: write inventory config file - copy: - dest: ../test.aws_rds.yml - content: "{{ lookup('template', template_name) }}" diff --git a/test/integration/targets/inventory_aws_rds/playbooks/empty_inventory_config.yml b/test/integration/targets/inventory_aws_rds/playbooks/empty_inventory_config.yml deleted file mode 100644 index d7e2cda3a7..0000000000 --- a/test/integration/targets/inventory_aws_rds/playbooks/empty_inventory_config.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -- hosts: 127.0.0.1 - connection: local - gather_facts: no - tasks: - - name: write inventory config file - copy: - dest: ../test.aws_rds.yml - content: "" diff --git a/test/integration/targets/inventory_aws_rds/playbooks/populate_cache.yml b/test/integration/targets/inventory_aws_rds/playbooks/populate_cache.yml deleted file mode 100644 index bd7dc6b494..0000000000 --- a/test/integration/targets/inventory_aws_rds/playbooks/populate_cache.yml +++ /dev/null @@ -1,54 +0,0 @@ ---- -- hosts: 127.0.0.1 - connection: local - gather_facts: no - environment: "{{ ansible_test.environment }}" - 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 }}' - block: - - set_fact: - instance_id: '{{ resource_prefix }}-mariadb' - - - name: assert group was populated with inventory but is empty - assert: - that: - - "'aws_rds' in groups" - - "not groups.aws_rds" - - # Create new host, add it to inventory and then terminate it without updating the cache - - - name: create minimal mariadb instance in default VPC and default subnet group - rds_instance: - state: present - engine: mariadb - db_instance_class: db.t2.micro - allocated_storage: 20 - instance_id: '{{ instance_id }}' - master_username: 'ansibletestuser' - master_user_password: 'password-{{ resource_prefix | regex_findall(".{8}$") | first }}' - tags: - workload_type: other - register: setup_instance - - - meta: refresh_inventory - - - assert: - that: - - groups.aws_rds - - always: - - - name: remove mariadb instance - rds_instance: - state: absent - engine: mariadb - skip_final_snapshot: yes - instance_id: '{{ instance_id }}' - ignore_errors: yes - when: setup_instance is defined diff --git a/test/integration/targets/inventory_aws_rds/playbooks/test_invalid_aws_rds_inventory_config.yml b/test/integration/targets/inventory_aws_rds/playbooks/test_invalid_aws_rds_inventory_config.yml deleted file mode 100644 index 499513570b..0000000000 --- a/test/integration/targets/inventory_aws_rds/playbooks/test_invalid_aws_rds_inventory_config.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -- hosts: 127.0.0.1 - connection: local - gather_facts: no - tasks: - - name: assert inventory was not populated by aws_rds inventory plugin - assert: - that: - - "'aws_rds' not in groups" diff --git a/test/integration/targets/inventory_aws_rds/playbooks/test_inventory_cache.yml b/test/integration/targets/inventory_aws_rds/playbooks/test_inventory_cache.yml deleted file mode 100644 index 7eadbad853..0000000000 --- a/test/integration/targets/inventory_aws_rds/playbooks/test_inventory_cache.yml +++ /dev/null @@ -1,18 +0,0 @@ ---- -- hosts: 127.0.0.1 - connection: local - gather_facts: no - tasks: - - name: assert cache was used to populate inventory - assert: - that: - - "'aws_rds' in groups" - - "groups.aws_rds | length == 1" - - - meta: refresh_inventory - - - name: assert refresh_inventory updated the cache - assert: - that: - - "'aws_rds' in groups" - - "not groups.aws_rds" diff --git a/test/integration/targets/inventory_aws_rds/playbooks/test_populating_inventory.yml b/test/integration/targets/inventory_aws_rds/playbooks/test_populating_inventory.yml deleted file mode 100644 index d79f2a01a9..0000000000 --- a/test/integration/targets/inventory_aws_rds/playbooks/test_populating_inventory.yml +++ /dev/null @@ -1,74 +0,0 @@ ---- -- hosts: 127.0.0.1 - connection: local - gather_facts: no - environment: "{{ ansible_test.environment }}" - 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 }}' - block: - - - set_fact: - instance_id: "{{ resource_prefix }}-mariadb" - - - debug: var=groups - - name: assert group was populated with inventory but is empty - assert: - that: - - "'aws_rds' in groups" - - "not groups.aws_rds" - - # Create new host, refresh inventory, remove host, refresh inventory - - - name: create minimal mariadb instance in default VPC and default subnet group - rds_instance: - state: present - engine: mariadb - db_instance_class: db.t2.micro - allocated_storage: 20 - instance_id: '{{ instance_id }}' - master_username: 'ansibletestuser' - master_user_password: 'password-{{ resource_prefix | regex_findall(".{8}$") | first }}' - tags: - workload_type: other - register: setup_instance - - - meta: refresh_inventory - - - name: assert group was populated with inventory and is no longer empty - assert: - that: - - "'aws_rds' in groups" - - "groups.aws_rds | length == 1" - - "groups.aws_rds.0 == '{{ instance_id }}'" - - - name: remove mariadb instance - rds_instance: - state: absent - engine: mariadb - skip_final_snapshot: yes - instance_id: '{{ instance_id }}' - - - meta: refresh_inventory - - - name: assert group was populated with inventory but is empty - assert: - that: - - "'aws_rds' in groups" - - "not groups.aws_rds" - - always: - - - name: remove mariadb instance - rds_instance: - state: absent - engine: mariadb - skip_final_snapshot: yes - instance_id: '{{ instance_id }}' - ignore_errors: yes - when: setup_instance is defined diff --git a/test/integration/targets/inventory_aws_rds/playbooks/test_populating_inventory_with_constructed.yml b/test/integration/targets/inventory_aws_rds/playbooks/test_populating_inventory_with_constructed.yml deleted file mode 100644 index c6ddb57340..0000000000 --- a/test/integration/targets/inventory_aws_rds/playbooks/test_populating_inventory_with_constructed.yml +++ /dev/null @@ -1,62 +0,0 @@ ---- -- hosts: 127.0.0.1 - connection: local - gather_facts: no - environment: "{{ ansible_test.environment }}" - 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 }}' - block: - - - set_fact: - instance_id: "{{ resource_prefix }}-mariadb" - - - name: create minimal mariadb instance in default VPC and default subnet group - rds_instance: - state: present - engine: mariadb - db_instance_class: db.t2.micro - allocated_storage: 20 - instance_id: '{{ resource_prefix }}-mariadb' - master_username: 'ansibletestuser' - master_user_password: 'password-{{ resource_prefix | regex_findall(".{8}$") | first }}' - tags: - workload_type: other - register: setup_instance - - - meta: refresh_inventory - - debug: var=groups - - - name: 'generate expected group name based off the db parameter groups' - vars: - parameter_group_name: '{{ setup_instance.db_parameter_groups[0].db_parameter_group_name }}' - set_fact: - parameter_group_key: 'rds_parameter_group_{{ parameter_group_name | replace(".", "_") }}' - - - name: assert the keyed groups from constructed config were added to inventory - assert: - that: - # There are 6 groups: all, ungrouped, aws_rds, tag keyed group, engine keyed group, parameter group keyed group - - "groups | length == 6" - - '"all" in groups' - - '"ungrouped" in groups' - - '"aws_rds" in groups' - - '"tag_workload_type_other" in groups' - - '"rds_mariadb" in groups' - - 'parameter_group_key in groups' - - always: - - - name: remove mariadb instance - rds_instance: - state: absent - engine: mariadb - skip_final_snapshot: yes - instance_id: '{{ instance_id }}' - ignore_errors: yes - when: setup_instance is defined diff --git a/test/integration/targets/inventory_aws_rds/playbooks/test_refresh_inventory.yml b/test/integration/targets/inventory_aws_rds/playbooks/test_refresh_inventory.yml deleted file mode 100644 index 565803800c..0000000000 --- a/test/integration/targets/inventory_aws_rds/playbooks/test_refresh_inventory.yml +++ /dev/null @@ -1,64 +0,0 @@ -- name: test updating inventory - module_defaults: - group/aws: - aws_access_key: '{{ aws_access_key }}' - aws_secret_key: '{{ aws_secret_key }}' - security_token: '{{ security_token | default(omit) }}' - region: '{{ aws_region }}' - block: - - set_fact: - instance_id: "{{ resource_prefix }}update" - - - name: assert group was populated with inventory but is empty - assert: - that: - - "'aws_rds' in groups" - - "not groups.aws_rds" - - - name: create minimal mariadb instance in default VPC and default subnet group - rds_instance: - state: present - engine: mariadb - db_instance_class: db.t2.micro - allocated_storage: 20 - instance_id: 'rds-mariadb-{{ resource_prefix }}' - master_username: 'ansibletestuser' - master_user_password: 'password-{{ resource_prefix | regex_findall(".{8}$") | first }}' - tags: - workload_type: other - register: setup_instance - - - meta: refresh_inventory - - - name: assert group was populated with inventory and is no longer empty - assert: - that: - - "'aws_rds' in groups" - - "groups.aws_rds | length == 1" - - "groups.aws_rds.0 == '{{ resource_prefix }}'" - - - name: remove mariadb instance - rds_instance: - state: absent - engine: mariadb - skip_final_snapshot: yes - instance_id: ansible-rds-mariadb-example - - - meta: refresh_inventory - - - name: assert group was populated with inventory but is empty - assert: - that: - - "'aws_rds' in groups" - - "not groups.aws_rds" - - always: - - - name: remove mariadb instance - rds_instance: - state: absent - engine: mariadb - skip_final_snapshot: yes - instance_id: ansible-rds-mariadb-example - ignore_errors: yes - when: setup_instance is defined diff --git a/test/integration/targets/inventory_aws_rds/runme.sh b/test/integration/targets/inventory_aws_rds/runme.sh deleted file mode 100755 index d759349e76..0000000000 --- a/test/integration/targets/inventory_aws_rds/runme.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash - -set -eux - -# ensure test config is empty -ansible-playbook playbooks/empty_inventory_config.yml "$@" - -export ANSIBLE_INVENTORY_ENABLED=aws_rds - -# test with default inventory file -ansible-playbook playbooks/test_invalid_aws_rds_inventory_config.yml "$@" - -export ANSIBLE_INVENTORY=test.aws_rds.yml - -# test empty inventory config -ansible-playbook playbooks/test_invalid_aws_rds_inventory_config.yml "$@" - -# generate inventory config and test using it -ansible-playbook playbooks/create_inventory_config.yml "$@" -ansible-playbook playbooks/test_populating_inventory.yml "$@" - -# generate inventory config with caching and test using it -ansible-playbook playbooks/create_inventory_config.yml -e "template='inventory_with_cache.j2'" "$@" -ansible-playbook playbooks/populate_cache.yml "$@" -ansible-playbook playbooks/test_inventory_cache.yml "$@" - -# remove inventory cache -rm -r aws_rds_cache_dir/ - -# generate inventory config with constructed features and test using it -ansible-playbook playbooks/create_inventory_config.yml -e "template='inventory_with_constructed.j2'" "$@" -ansible-playbook playbooks/test_populating_inventory_with_constructed.yml "$@" - -# cleanup inventory config -ansible-playbook playbooks/empty_inventory_config.yml "$@" diff --git a/test/integration/targets/inventory_aws_rds/templates/inventory.j2 b/test/integration/targets/inventory_aws_rds/templates/inventory.j2 deleted file mode 100644 index 3d9df9affc..0000000000 --- a/test/integration/targets/inventory_aws_rds/templates/inventory.j2 +++ /dev/null @@ -1,10 +0,0 @@ -plugin: aws_rds -aws_access_key_id: '{{ aws_access_key }}' -aws_secret_access_key: '{{ aws_secret_key }}' -{% if security_token | default(false) %} -aws_security_token: '{{ security_token }}' -{% endif %} -regions: - - '{{ aws_region }}' -filters: - db-instance-id: "{{ resource_prefix }}-mariadb" diff --git a/test/integration/targets/inventory_aws_rds/templates/inventory_with_cache.j2 b/test/integration/targets/inventory_aws_rds/templates/inventory_with_cache.j2 deleted file mode 100644 index ba227e3082..0000000000 --- a/test/integration/targets/inventory_aws_rds/templates/inventory_with_cache.j2 +++ /dev/null @@ -1,13 +0,0 @@ -plugin: aws_rds -cache: True -cache_plugin: jsonfile -cache_connection: aws_rds_cache_dir -aws_access_key_id: '{{ aws_access_key }}' -aws_secret_access_key: '{{ aws_secret_key }}' -{% if security_token | default(false) %} -aws_security_token: '{{ security_token }}' -{% endif %} -regions: - - '{{ aws_region }}' -filters: - db-instance-id: "{{ resource_prefix }}-mariadb" diff --git a/test/integration/targets/inventory_aws_rds/templates/inventory_with_constructed.j2 b/test/integration/targets/inventory_aws_rds/templates/inventory_with_constructed.j2 deleted file mode 100644 index 7239497478..0000000000 --- a/test/integration/targets/inventory_aws_rds/templates/inventory_with_constructed.j2 +++ /dev/null @@ -1,17 +0,0 @@ -plugin: aws_rds -aws_access_key_id: '{{ aws_access_key }}' -aws_secret_access_key: '{{ aws_secret_key }}' -{% if security_token | default(false) %} -aws_security_token: '{{ security_token }}' -{% endif %} -regions: - - '{{ aws_region }}' -keyed_groups: - - key: 'db_parameter_groups|json_query("[].db_parameter_group_name")' - prefix: rds_parameter_group - - key: tags - prefix: tag - - key: engine - prefix: rds -filters: - db-instance-id: "{{ resource_prefix }}-mariadb" diff --git a/test/integration/targets/inventory_aws_rds/test.aws_rds.yml b/test/integration/targets/inventory_aws_rds/test.aws_rds.yml deleted file mode 100644 index e69de29bb2..0000000000 --- a/test/integration/targets/inventory_aws_rds/test.aws_rds.yml +++ /dev/null diff --git a/test/integration/targets/s3_bucket/aliases b/test/integration/targets/s3_bucket/aliases deleted file mode 100644 index a112c3d1bb..0000000000 --- a/test/integration/targets/s3_bucket/aliases +++ /dev/null @@ -1,2 +0,0 @@ -cloud/aws -shippable/aws/group1 diff --git a/test/integration/targets/s3_bucket/inventory b/test/integration/targets/s3_bucket/inventory deleted file mode 100644 index 2968f764cf..0000000000 --- a/test/integration/targets/s3_bucket/inventory +++ /dev/null @@ -1,12 +0,0 @@ -[tests] -missing -simple -complex -dotted -tags -encryption_kms -encryption_sse - -[all:vars] -ansible_connection=local -ansible_python_interpreter="{{ ansible_playbook_python }}" diff --git a/test/integration/targets/s3_bucket/main.yml b/test/integration/targets/s3_bucket/main.yml deleted file mode 100644 index 22fc0d64f7..0000000000 --- a/test/integration/targets/s3_bucket/main.yml +++ /dev/null @@ -1,12 +0,0 @@ ---- -# 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/s3_bucket/tasks/ - -# VPC should get cleaned up once all hosts have run -- hosts: all - gather_facts: no - strategy: free - #serial: 10 - roles: - - s3_bucket diff --git a/test/integration/targets/s3_bucket/meta/main.yml b/test/integration/targets/s3_bucket/meta/main.yml deleted file mode 100644 index 38b31be072..0000000000 --- a/test/integration/targets/s3_bucket/meta/main.yml +++ /dev/null @@ -1,4 +0,0 @@ -dependencies: - - prepare_tests - - setup_ec2 - - setup_remote_tmp_dir diff --git a/test/integration/targets/s3_bucket/roles/s3_bucket/defaults/main.yml b/test/integration/targets/s3_bucket/roles/s3_bucket/defaults/main.yml deleted file mode 100644 index b4fd58adfc..0000000000 --- a/test/integration/targets/s3_bucket/roles/s3_bucket/defaults/main.yml +++ /dev/null @@ -1,2 +0,0 @@ ---- -bucket_name: '{{ resource_prefix }}-{{ inventory_hostname | regex_replace("_","-") }}' diff --git a/test/integration/targets/s3_bucket/roles/s3_bucket/meta/main.yml b/test/integration/targets/s3_bucket/roles/s3_bucket/meta/main.yml deleted file mode 100644 index 38b31be072..0000000000 --- a/test/integration/targets/s3_bucket/roles/s3_bucket/meta/main.yml +++ /dev/null @@ -1,4 +0,0 @@ -dependencies: - - prepare_tests - - setup_ec2 - - setup_remote_tmp_dir diff --git a/test/integration/targets/s3_bucket/roles/s3_bucket/tasks/complex.yml b/test/integration/targets/s3_bucket/roles/s3_bucket/tasks/complex.yml deleted file mode 100644 index 41a03a4a55..0000000000 --- a/test/integration/targets/s3_bucket/roles/s3_bucket/tasks/complex.yml +++ /dev/null @@ -1,146 +0,0 @@ ---- -- block: - - name: 'Create more complex s3_bucket' - s3_bucket: - name: '{{ bucket_name }}' - state: present - policy: "{{ lookup('template','policy.json') }}" - requester_pays: yes - versioning: yes - tags: - example: tag1 - another: tag2 - register: output - - - assert: - that: - - output is changed - - output.name == '{{ bucket_name }}' - - output.requester_pays - - output.versioning.MfaDelete == 'Disabled' - - output.versioning.Versioning == 'Enabled' - - output.tags.example == 'tag1' - - output.tags.another == 'tag2' - - output.policy.Statement[0].Action == 's3:GetObject' - - output.policy.Statement[0].Effect == 'Allow' - - output.policy.Statement[0].Principal == '*' - - output.policy.Statement[0].Resource == 'arn:aws:s3:::{{ bucket_name }}/*' - - output.policy.Statement[0].Sid == 'AddPerm' - - # ============================================================ - - - name: 'Pause to help with s3 bucket eventual consistency' - wait_for: - timeout: 10 - delegate_to: localhost - - - name: 'Try to update the same complex s3_bucket' - s3_bucket: - name: '{{ bucket_name }}' - state: present - policy: "{{ lookup('template','policy.json') }}" - requester_pays: yes - versioning: yes - tags: - example: tag1 - another: tag2 - register: output - - - assert: - that: - - output is not changed - - output.name == '{{ bucket_name }}' - - output.requester_pays - - output.versioning.MfaDelete == 'Disabled' - - output.versioning.Versioning == 'Enabled' - - output.tags.example == 'tag1' - - output.tags.another == 'tag2' - - output.policy.Statement[0].Action == 's3:GetObject' - - output.policy.Statement[0].Effect == 'Allow' - - output.policy.Statement[0].Principal == '*' - - output.policy.Statement[0].Resource == 'arn:aws:s3:::{{ bucket_name }}/*' - - output.policy.Statement[0].Sid == 'AddPerm' - - # ============================================================ - - name: 'Update bucket policy on complex bucket' - s3_bucket: - name: '{{ bucket_name }}' - state: present - policy: "{{ lookup('template','policy-updated.json') }}" - requester_pays: yes - versioning: yes - tags: - example: tag1 - another: tag2 - register: output - - - assert: - that: - - output is changed - - output.policy.Statement[0].Action == 's3:GetObject' - - output.policy.Statement[0].Effect == 'Deny' - - output.policy.Statement[0].Principal == '*' - - output.policy.Statement[0].Resource == 'arn:aws:s3:::{{ bucket_name }}/*' - - output.policy.Statement[0].Sid == 'AddPerm' - - # ============================================================ - - - name: 'Pause to help with s3 bucket eventual consistency' - wait_for: - timeout: 10 - delegate_to: localhost - - - name: Update attributes for s3_bucket - s3_bucket: - name: '{{ bucket_name }}' - state: present - policy: "{{ lookup('template','policy.json') }}" - requester_pays: no - versioning: no - tags: - example: tag1-udpated - another: tag2 - register: output - - - assert: - that: - - output is changed - - output.name == '{{ bucket_name }}' - - not output.requester_pays - - output.versioning.MfaDelete == 'Disabled' - - output.versioning.Versioning in ['Suspended', 'Disabled'] - - output.tags.example == 'tag1-udpated' - - output.tags.another == 'tag2' - - output.policy.Statement[0].Action == 's3:GetObject' - - output.policy.Statement[0].Effect == 'Allow' - - output.policy.Statement[0].Principal == '*' - - output.policy.Statement[0].Resource == 'arn:aws:s3:::{{ bucket_name }}/*' - - output.policy.Statement[0].Sid == 'AddPerm' - - - name: 'Delete complex test bucket' - s3_bucket: - name: '{{ bucket_name }}' - state: absent - register: output - - - assert: - that: - - output is changed - - - name: 'Re-delete complex test bucket' - s3_bucket: - name: '{{ bucket_name }}' - state: absent - register: output - - - assert: - that: - - output is not changed - - # ============================================================ - always: - - name: 'Ensure all buckets are deleted' - s3_bucket: - name: '{{ bucket_name }}' - state: absent - ignore_errors: yes diff --git a/test/integration/targets/s3_bucket/roles/s3_bucket/tasks/dotted.yml b/test/integration/targets/s3_bucket/roles/s3_bucket/tasks/dotted.yml deleted file mode 100644 index 7d4e0ae9ea..0000000000 --- a/test/integration/targets/s3_bucket/roles/s3_bucket/tasks/dotted.yml +++ /dev/null @@ -1,54 +0,0 @@ ---- -- block: - - name: 'Ensure bucket_name contains a .' - set_fact: - bucket_name: '{{ bucket_name }}.something' - - # ============================================================ - # - - name: 'Create bucket with dot in name' - s3_bucket: - name: '{{ bucket_name }}' - state: present - register: output - - - assert: - that: - - output is changed - - output.name == '{{ bucket_name }}' - - - # ============================================================ - - - name: 'Pause to help with s3 bucket eventual consistency' - wait_for: - timeout: 10 - delegate_to: localhost - - - name: 'Delete s3_bucket with dot in name' - s3_bucket: - name: '{{ bucket_name }}' - state: absent - register: output - - - assert: - that: - - output is changed - - - name: 'Re-delete s3_bucket with dot in name' - s3_bucket: - name: '{{ bucket_name }}' - state: absent - register: output - - - assert: - that: - - output is not changed - - # ============================================================ - always: - - name: 'Ensure all buckets are deleted' - s3_bucket: - name: '{{ bucket_name }}' - state: absent - ignore_errors: yes diff --git a/test/integration/targets/s3_bucket/roles/s3_bucket/tasks/encryption_kms.yml b/test/integration/targets/s3_bucket/roles/s3_bucket/tasks/encryption_kms.yml deleted file mode 100644 index 869dd40236..0000000000 --- a/test/integration/targets/s3_bucket/roles/s3_bucket/tasks/encryption_kms.yml +++ /dev/null @@ -1,88 +0,0 @@ ---- -- module_defaults: - group/aws: - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" - security_token: "{{ security_token | default(omit) }}" - region: "{{ aws_region }}" - block: - - # ============================================================ - - - name: 'Create a simple bucket' - s3_bucket: - name: '{{ bucket_name }}' - state: present - register: output - - - name: 'Enable aws:kms encryption with KMS master key' - s3_bucket: - name: '{{ bucket_name }}' - state: present - encryption: "aws:kms" - register: output - - - assert: - that: - - output.changed - - output.encryption - - output.encryption.SSEAlgorithm == 'aws:kms' - - - name: 'Re-enable aws:kms encryption with KMS master key (idempotent)' - s3_bucket: - name: '{{ bucket_name }}' - state: present - encryption: "aws:kms" - register: output - - - assert: - that: - - not output.changed - - output.encryption - - output.encryption.SSEAlgorithm == 'aws:kms' - - # ============================================================ - - - name: Disable encryption from bucket - s3_bucket: - name: '{{ bucket_name }}' - state: present - encryption: "none" - register: output - - - assert: - that: - - output.changed - - not output.encryption - - - name: Disable encryption from bucket - s3_bucket: - name: '{{ bucket_name }}' - state: present - encryption: "none" - register: output - - - assert: - that: - - output is not changed - - not output.encryption - - # ============================================================ - - - name: Delete encryption test s3 bucket - s3_bucket: - name: '{{ bucket_name }}' - state: absent - register: output - - - assert: - that: - - output.changed - - # ============================================================ - always: - - name: Ensure all buckets are deleted - s3_bucket: - name: '{{ bucket_name }}' - state: absent - ignore_errors: yes diff --git a/test/integration/targets/s3_bucket/roles/s3_bucket/tasks/encryption_sse.yml b/test/integration/targets/s3_bucket/roles/s3_bucket/tasks/encryption_sse.yml deleted file mode 100644 index 699e8ae410..0000000000 --- a/test/integration/targets/s3_bucket/roles/s3_bucket/tasks/encryption_sse.yml +++ /dev/null @@ -1,88 +0,0 @@ ---- -- module_defaults: - group/aws: - aws_access_key: "{{ aws_access_key }}" - aws_secret_key: "{{ aws_secret_key }}" - security_token: "{{ security_token | default(omit) }}" - region: "{{ aws_region }}" - block: - - # ============================================================ - - - name: 'Create a simple bucket' - s3_bucket: - name: '{{ bucket_name }}' - state: present - register: output - - - name: 'Enable AES256 encryption' - s3_bucket: - name: '{{ bucket_name }}' - state: present - encryption: 'AES256' - register: output - - - assert: - that: - - output.changed - - output.encryption - - output.encryption.SSEAlgorithm == 'AES256' - - - name: 'Re-enable AES256 encryption (idempotency)' - s3_bucket: - name: '{{ bucket_name }}' - state: present - encryption: 'AES256' - register: output - - - assert: - that: - - not output.changed - - output.encryption - - output.encryption.SSEAlgorithm == 'AES256' - - # ============================================================ - - - name: Disable encryption from bucket - s3_bucket: - name: '{{ bucket_name }}' - state: present - encryption: "none" - register: output - - - assert: - that: - - output.changed - - not output.encryption - - - name: Disable encryption from bucket - s3_bucket: - name: '{{ bucket_name }}' - state: present - encryption: "none" - register: output - - - assert: - that: - - output is not changed - - not output.encryption - - # ============================================================ - - - name: Delete encryption test s3 bucket - s3_bucket: - name: '{{ bucket_name }}' - state: absent - register: output - - - assert: - that: - - output.changed - - # ============================================================ - always: - - name: Ensure all buckets are deleted - s3_bucket: - name: '{{ bucket_name }}' - state: absent - ignore_errors: yes diff --git a/test/integration/targets/s3_bucket/roles/s3_bucket/tasks/main.yml b/test/integration/targets/s3_bucket/roles/s3_bucket/tasks/main.yml deleted file mode 100644 index 8eba03ba1a..0000000000 --- a/test/integration/targets/s3_bucket/roles/s3_bucket/tasks/main.yml +++ /dev/null @@ -1,20 +0,0 @@ ---- -# 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_roles/tasks/ -# -# ############################################################################### - -- 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 }}" - block: - - debug: - msg: "{{ inventory_hostname }} start: {{ lookup('pipe','date') }}" - - include_tasks: '{{ inventory_hostname }}.yml' - - debug: - msg: "{{ inventory_hostname }} finish: {{ lookup('pipe','date') }}" diff --git a/test/integration/targets/s3_bucket/roles/s3_bucket/tasks/missing.yml b/test/integration/targets/s3_bucket/roles/s3_bucket/tasks/missing.yml deleted file mode 100644 index 4d827680ee..0000000000 --- a/test/integration/targets/s3_bucket/roles/s3_bucket/tasks/missing.yml +++ /dev/null @@ -1,26 +0,0 @@ ---- -- name: 'Attempt to delete non-existent buckets' - block: - # ============================================================ - # - # While in theory the 'simple' test case covers this there are - # ways in which eventual-consistency could catch us out. - # - - name: 'Delete non-existstent s3_bucket (never created)' - s3_bucket: - name: '{{ bucket_name }}' - state: absent - register: output - - - assert: - that: - - output is success - - output is not changed - - # ============================================================ - always: - - name: 'Ensure all buckets are deleted' - s3_bucket: - name: '{{ bucket_name }}' - state: absent - ignore_errors: yes diff --git a/test/integration/targets/s3_bucket/roles/s3_bucket/tasks/simple.yml b/test/integration/targets/s3_bucket/roles/s3_bucket/tasks/simple.yml deleted file mode 100644 index 3c39c5b4cb..0000000000 --- a/test/integration/targets/s3_bucket/roles/s3_bucket/tasks/simple.yml +++ /dev/null @@ -1,64 +0,0 @@ ---- -- name: 'Run simple tests' - block: - # Note: s3_bucket doesn't support check_mode - - # ============================================================ - - name: 'Create a simple s3_bucket' - s3_bucket: - name: '{{ bucket_name }}' - state: present - register: output - - - assert: - that: - - output is success - - output is changed - - output.name == '{{ bucket_name }}' - - not output.requester_pays - - # ============================================================ - - name: 'Try to update the simple bucket with the same values' - s3_bucket: - name: '{{ bucket_name }}' - state: present - register: output - - - assert: - that: - - output is success - - output is not changed - - output.name == '{{ bucket_name }}' - - not output.requester_pays - - # ============================================================ - - name: 'Delete the simple s3_bucket' - s3_bucket: - name: '{{ bucket_name }}' - state: absent - register: output - - - assert: - that: - - output is success - - output is changed - - # ============================================================ - - name: 'Re-delete the simple s3_bucket (idemoptency)' - s3_bucket: - name: '{{ bucket_name }}' - state: absent - register: output - - - assert: - that: - - output is success - - output is not changed - - # ============================================================ - always: - - name: 'Ensure all buckets are deleted' - s3_bucket: - name: '{{ bucket_name }}' - state: absent - ignore_errors: yes diff --git a/test/integration/targets/s3_bucket/roles/s3_bucket/tasks/tags.yml b/test/integration/targets/s3_bucket/roles/s3_bucket/tasks/tags.yml deleted file mode 100644 index 437dd2ca5f..0000000000 --- a/test/integration/targets/s3_bucket/roles/s3_bucket/tasks/tags.yml +++ /dev/null @@ -1,256 +0,0 @@ ---- -- name: 'Run tagging tests' - block: - - # ============================================================ - - name: 'Create simple s3_bucket for testing tagging' - s3_bucket: - name: '{{ bucket_name }}' - state: present - register: output - - - assert: - that: - - output.changed - - output.name == '{{ bucket_name }}' - - # ============================================================ - - - name: 'Add tags to s3 bucket' - s3_bucket: - name: '{{ bucket_name }}' - state: present - tags: - example: tag1 - another: tag2 - register: output - - - assert: - that: - - output.changed - - output.name == '{{ bucket_name }}' - - output.tags.example == 'tag1' - - output.tags.another == 'tag2' - - - name: 'Re-Add tags to s3 bucket' - s3_bucket: - name: '{{ bucket_name }}' - state: present - tags: - example: tag1 - another: tag2 - register: output - - - assert: - that: - - output is not changed - - output.name == '{{ bucket_name }}' - - output.tags.example == 'tag1' - - output.tags.another == 'tag2' - - # ============================================================ - - - name: Remove a tag from an s3_bucket - s3_bucket: - name: '{{ bucket_name }}' - state: present - tags: - example: tag1 - register: output - - - assert: - that: - - output.changed - - output.name == '{{ bucket_name }}' - - output.tags.example == 'tag1' - - "'another' not in output.tags" - - - name: Re-remove the tag from an s3_bucket - s3_bucket: - name: '{{ bucket_name }}' - state: present - tags: - example: tag1 - register: output - - - assert: - that: - - output is not changed - - output.name == '{{ bucket_name }}' - - output.tags.example == 'tag1' - - "'another' not in output.tags" - - ## ============================================================ - - #- name: 'Pause to help with s3 bucket eventual consistency' - # wait_for: - # timeout: 10 - # delegate_to: localhost - - ## ============================================================ - - - name: 'Add a tag for s3_bucket with purge_tags False' - s3_bucket: - name: '{{ bucket_name }}' - state: present - purge_tags: no - tags: - anewtag: here - register: output - - - assert: - that: - - output.changed - - output.name == '{{ bucket_name }}' - - output.tags.example == 'tag1' - - output.tags.anewtag == 'here' - - - name: 'Re-add a tag for s3_bucket with purge_tags False' - s3_bucket: - name: '{{ bucket_name }}' - state: present - purge_tags: no - tags: - anewtag: here - register: output - - - assert: - that: - - output is not changed - - output.name == '{{ bucket_name }}' - - output.tags.example == 'tag1' - - output.tags.anewtag == 'here' - - ## ============================================================ - - #- name: 'Pause to help with s3 bucket eventual consistency' - # wait_for: - # timeout: 10 - # delegate_to: localhost - - ## ============================================================ - - - name: Update a tag for s3_bucket with purge_tags False - s3_bucket: - name: '{{ bucket_name }}' - state: present - purge_tags: no - tags: - anewtag: next - register: output - - - assert: - that: - - output.changed - - output.name == '{{ bucket_name }}' - - output.tags.example == 'tag1' - - output.tags.anewtag == 'next' - - - name: Re-update a tag for s3_bucket with purge_tags False - s3_bucket: - name: '{{ bucket_name }}' - state: present - purge_tags: no - tags: - anewtag: next - register: output - - - assert: - that: - - output is not changed - - output.name == '{{ bucket_name }}' - - output.tags.example == 'tag1' - - output.tags.anewtag == 'next' - - ## ============================================================ - - #- name: 'Pause to help with s3 bucket eventual consistency' - # wait_for: - # timeout: 10 - # delegate_to: localhost - - ## ============================================================ - - - name: Pass empty tags dict for s3_bucket with purge_tags False - s3_bucket: - name: '{{ bucket_name }}' - state: present - purge_tags: no - tags: {} - register: output - - - assert: - that: - - output is not changed - - output.name == '{{ bucket_name }}' - - output.tags.example == 'tag1' - - output.tags.anewtag == 'next' - - ## ============================================================ - - #- name: 'Pause to help with s3 bucket eventual consistency' - # wait_for: - # timeout: 10 - # delegate_to: localhost - - ## ============================================================ - - - name: Do not specify any tag to ensure previous tags are not removed - s3_bucket: - name: '{{ bucket_name }}' - state: present - register: output - - - assert: - that: - - not output.changed - - output.name == '{{ bucket_name }}' - - output.tags.example == 'tag1' - - # ============================================================ - - - name: Remove all tags - s3_bucket: - name: '{{ bucket_name }}' - state: present - tags: {} - register: output - - - assert: - that: - - output.changed - - output.name == '{{ bucket_name }}' - - output.tags == {} - - - name: Re-remove all tags - s3_bucket: - name: '{{ bucket_name }}' - state: present - tags: {} - register: output - - - assert: - that: - - output is not changed - - output.name == '{{ bucket_name }}' - - output.tags == {} - - # ============================================================ - - - name: Delete bucket - s3_bucket: - name: '{{ bucket_name }}' - state: absent - register: output - - - assert: - that: - - output.changed - - # ============================================================ - always: - - name: Ensure all buckets are deleted - s3_bucket: - name: '{{ bucket_name }}' - state: absent - ignore_errors: yes diff --git a/test/integration/targets/s3_bucket/roles/s3_bucket/templates/policy-updated.json b/test/integration/targets/s3_bucket/roles/s3_bucket/templates/policy-updated.json deleted file mode 100644 index 5775c5eb2c..0000000000 --- a/test/integration/targets/s3_bucket/roles/s3_bucket/templates/policy-updated.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "Version":"2012-10-17", - "Statement":[ - { - "Sid":"AddPerm", - "Effect":"Deny", - "Principal": "*", - "Action":["s3:GetObject"], - "Resource":["arn:aws:s3:::{{bucket_name}}/*"] - } - ] -} diff --git a/test/integration/targets/s3_bucket/roles/s3_bucket/templates/policy.json b/test/integration/targets/s3_bucket/roles/s3_bucket/templates/policy.json deleted file mode 100644 index a2720aed60..0000000000 --- a/test/integration/targets/s3_bucket/roles/s3_bucket/templates/policy.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "Version":"2012-10-17", - "Statement":[ - { - "Sid":"AddPerm", - "Effect":"Allow", - "Principal": "*", - "Action":["s3:GetObject"], - "Resource":["arn:aws:s3:::{{bucket_name}}/*"] - } - ] -} diff --git a/test/integration/targets/s3_bucket/runme.sh b/test/integration/targets/s3_bucket/runme.sh deleted file mode 100755 index aa324772bb..0000000000 --- a/test/integration/targets/s3_bucket/runme.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/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/test/sanity/ignore.txt b/test/sanity/ignore.txt index e6bf7868ed..3448786ddd 100644 --- a/test/sanity/ignore.txt +++ b/test/sanity/ignore.txt @@ -234,35 +234,6 @@ lib/ansible/module_utils/urls.py replace-urlopen lib/ansible/module_utils/yumdnf.py future-import-boilerplate lib/ansible/module_utils/yumdnf.py metaclass-boilerplate lib/ansible/modules/cloud/amazon/aws_netapp_cvs_FileSystems.py validate-modules:parameter-list-no-elements -lib/ansible/modules/cloud/amazon/aws_s3.py validate-modules:doc-elements-mismatch -lib/ansible/modules/cloud/amazon/aws_s3.py validate-modules:parameter-list-no-elements -lib/ansible/modules/cloud/amazon/cloudformation.py validate-modules:doc-elements-mismatch -lib/ansible/modules/cloud/amazon/cloudformation.py validate-modules:parameter-list-no-elements -lib/ansible/modules/cloud/amazon/ec2.py validate-modules:doc-elements-mismatch -lib/ansible/modules/cloud/amazon/ec2.py validate-modules:parameter-list-no-elements -lib/ansible/modules/cloud/amazon/ec2_ami.py validate-modules:doc-elements-mismatch -lib/ansible/modules/cloud/amazon/ec2_ami.py validate-modules:parameter-list-no-elements -lib/ansible/modules/cloud/amazon/ec2_ami_info.py validate-modules:doc-elements-mismatch -lib/ansible/modules/cloud/amazon/ec2_ami_info.py validate-modules:parameter-list-no-elements -lib/ansible/modules/cloud/amazon/ec2_elb_lb.py validate-modules:parameter-list-no-elements -lib/ansible/modules/cloud/amazon/ec2_eni.py validate-modules:doc-elements-mismatch -lib/ansible/modules/cloud/amazon/ec2_eni.py validate-modules:parameter-list-no-elements -lib/ansible/modules/cloud/amazon/ec2_group.py validate-modules:doc-elements-mismatch -lib/ansible/modules/cloud/amazon/ec2_group.py validate-modules:parameter-list-no-elements -lib/ansible/modules/cloud/amazon/ec2_snapshot_info.py validate-modules:doc-elements-mismatch -lib/ansible/modules/cloud/amazon/ec2_snapshot_info.py validate-modules:parameter-list-no-elements -lib/ansible/modules/cloud/amazon/ec2_tag.py validate-modules:parameter-state-invalid-choice -lib/ansible/modules/cloud/amazon/ec2_vol.py validate-modules:parameter-state-invalid-choice -lib/ansible/modules/cloud/amazon/ec2_vpc_dhcp_option.py validate-modules:doc-elements-mismatch -lib/ansible/modules/cloud/amazon/ec2_vpc_dhcp_option.py validate-modules:parameter-list-no-elements -lib/ansible/modules/cloud/amazon/ec2_vpc_dhcp_option_info.py validate-modules:doc-elements-mismatch -lib/ansible/modules/cloud/amazon/ec2_vpc_dhcp_option_info.py validate-modules:parameter-list-no-elements -lib/ansible/modules/cloud/amazon/ec2_vpc_net.py validate-modules:doc-elements-mismatch -lib/ansible/modules/cloud/amazon/ec2_vpc_net.py validate-modules:parameter-list-no-elements -lib/ansible/modules/cloud/amazon/ec2_vpc_net_info.py validate-modules:doc-elements-mismatch -lib/ansible/modules/cloud/amazon/ec2_vpc_net_info.py validate-modules:parameter-list-no-elements -lib/ansible/modules/cloud/amazon/ec2_vpc_subnet_info.py validate-modules:doc-elements-mismatch -lib/ansible/modules/cloud/amazon/ec2_vpc_subnet_info.py validate-modules:parameter-list-no-elements lib/ansible/modules/cloud/azure/azure_rm_acs.py validate-modules:parameter-list-no-elements lib/ansible/modules/cloud/azure/azure_rm_acs.py validate-modules:parameter-type-not-in-doc lib/ansible/modules/cloud/azure/azure_rm_acs.py validate-modules:required_if-requirements-unknown @@ -4013,12 +3984,6 @@ lib/ansible/plugins/action/vyos.py action-plugin-docs # base class for deprecate lib/ansible/plugins/cache/base.py ansible-doc!skip # not a plugin, but a stub for backwards compatibility lib/ansible/plugins/doc_fragments/asa.py future-import-boilerplate lib/ansible/plugins/doc_fragments/asa.py metaclass-boilerplate -lib/ansible/plugins/doc_fragments/aws.py future-import-boilerplate -lib/ansible/plugins/doc_fragments/aws.py metaclass-boilerplate -lib/ansible/plugins/doc_fragments/aws_credentials.py future-import-boilerplate -lib/ansible/plugins/doc_fragments/aws_credentials.py metaclass-boilerplate -lib/ansible/plugins/doc_fragments/aws_region.py future-import-boilerplate -lib/ansible/plugins/doc_fragments/aws_region.py metaclass-boilerplate lib/ansible/plugins/doc_fragments/azure.py future-import-boilerplate lib/ansible/plugins/doc_fragments/azure.py metaclass-boilerplate lib/ansible/plugins/doc_fragments/azure_tags.py future-import-boilerplate @@ -4037,8 +4002,6 @@ lib/ansible/plugins/doc_fragments/dellos6.py future-import-boilerplate lib/ansible/plugins/doc_fragments/dellos6.py metaclass-boilerplate lib/ansible/plugins/doc_fragments/dellos9.py future-import-boilerplate lib/ansible/plugins/doc_fragments/dellos9.py metaclass-boilerplate -lib/ansible/plugins/doc_fragments/ec2.py future-import-boilerplate -lib/ansible/plugins/doc_fragments/ec2.py metaclass-boilerplate lib/ansible/plugins/doc_fragments/eos.py future-import-boilerplate lib/ansible/plugins/doc_fragments/eos.py metaclass-boilerplate lib/ansible/plugins/doc_fragments/f5.py future-import-boilerplate @@ -4314,7 +4277,6 @@ test/units/mock/path.py future-import-boilerplate test/units/mock/path.py metaclass-boilerplate test/units/mock/yaml_helper.py future-import-boilerplate test/units/mock/yaml_helper.py metaclass-boilerplate -test/units/module_utils/aws/test_aws_module.py metaclass-boilerplate test/units/module_utils/basic/test__symbolic_mode_to_octal.py future-import-boilerplate test/units/module_utils/basic/test_deprecate_warn.py future-import-boilerplate test/units/module_utils/basic/test_deprecate_warn.py metaclass-boilerplate diff --git a/test/units/module_utils/aws/test_aws_module.py b/test/units/module_utils/aws/test_aws_module.py deleted file mode 100644 index 425282cc54..0000000000 --- a/test/units/module_utils/aws/test_aws_module.py +++ /dev/null @@ -1,139 +0,0 @@ -# -*- coding: utf-8 -*- -# (c) 2017, Michael De La Rue -# -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see <http://www.gnu.org/licenses/>. - -# Make coding more python3-ish -from __future__ import (absolute_import, division, print_function) -from pytest import importorskip -import unittest -from ansible.module_utils import basic -from ansible.module_utils.aws.core import AnsibleAWSModule -from ansible.module_utils._text import to_bytes -from units.compat.mock import Mock, patch -import json - -importorskip("boto3") -botocore = importorskip("botocore") - - -class AWSModuleTestCase(unittest.TestCase): - - basic._ANSIBLE_ARGS = to_bytes(json.dumps({'ANSIBLE_MODULE_ARGS': {'_ansible_tmpdir': '/tmp/ansible-abc'}})) - - def test_create_aws_module_should_set_up_params(self): - m = AnsibleAWSModule(argument_spec=dict( - win_string_arg=dict(type='list', default=['win']) - )) - m_noretry_no_customargs = AnsibleAWSModule( - auto_retry=False, default_args=False, - argument_spec=dict( - success_string_arg=dict(type='list', default=['success']) - ) - ) - assert m, "module wasn't true!!" - assert m_noretry_no_customargs, "module wasn't true!!" - - m_params = m.params - m_no_defs_params = m_noretry_no_customargs.params - assert 'region' in m_params - assert 'win' in m_params["win_string_arg"] - assert 'success' in m_no_defs_params["success_string_arg"] - assert 'aws_secret_key' not in m_no_defs_params - - -class ErrorReportingTestcase(unittest.TestCase): - - def test_botocore_exception_reports_nicely_via_fail_json_aws(self): - - basic._ANSIBLE_ARGS = to_bytes(json.dumps({'ANSIBLE_MODULE_ARGS': {'_ansible_tmpdir': '/tmp/ansible-abc'}})) - module = AnsibleAWSModule(argument_spec=dict( - fail_mode=dict(type='list', default=['success']) - )) - - fail_json_double = Mock() - err_msg = {'Error': {'Code': 'FakeClass.FakeError'}} - with patch.object(basic.AnsibleModule, 'fail_json', fail_json_double): - try: - raise botocore.exceptions.ClientError(err_msg, 'Could not find you') - except Exception as e: - print("exception is " + str(e)) - module.fail_json_aws(e, msg="Fake failure for testing boto exception messages") - - assert(len(fail_json_double.mock_calls) > - 0), "failed to call fail_json when should have" - assert(len(fail_json_double.mock_calls) < - 2), "called fail_json multiple times when once would do" - assert("test_botocore_exception_reports_nicely" - in fail_json_double.mock_calls[0][2]["exception"]), \ - "exception traceback doesn't include correct function, fail call was actually: " \ - + str(fail_json_double.mock_calls[0]) - - assert("Fake failure for testing boto exception messages:" - in fail_json_double.mock_calls[0][2]["msg"]), \ - "error message doesn't include the local message; was: " \ - + str(fail_json_double.mock_calls[0]) - assert("Could not find you" in fail_json_double.mock_calls[0][2]["msg"]), \ - "error message doesn't include the botocore exception message; was: " \ - + str(fail_json_double.mock_calls[0]) - try: - fail_json_double.mock_calls[0][2]["error"] - except KeyError: - raise Exception("error was missing; call was: " + str(fail_json_double.mock_calls[0])) - assert("FakeClass.FakeError" == fail_json_double.mock_calls[0][2]["error"]["code"]), \ - "Failed to find error/code; was: " + str(fail_json_double.mock_calls[0]) - - def test_botocore_exception_without_response_reports_nicely_via_fail_json_aws(self): - basic._ANSIBLE_ARGS = to_bytes(json.dumps({'ANSIBLE_MODULE_ARGS': {'_ansible_tmpdir': '/tmp/ansible-abc'}})) - module = AnsibleAWSModule(argument_spec=dict( - fail_mode=dict(type='list', default=['success']) - )) - - fail_json_double = Mock() - err_msg = None - with patch.object(basic.AnsibleModule, 'fail_json', fail_json_double): - try: - raise botocore.exceptions.ClientError(err_msg, 'Could not find you') - except Exception as e: - print("exception is " + str(e)) - module.fail_json_aws(e, msg="Fake failure for testing boto exception messages") - - assert(len(fail_json_double.mock_calls) > 0), "failed to call fail_json when should have" - assert(len(fail_json_double.mock_calls) < 2), "called fail_json multiple times" - - assert("test_botocore_exception_without_response_reports_nicely_via_fail_json_aws" - in fail_json_double.mock_calls[0][2]["exception"]), \ - "exception traceback doesn't include correct function, fail call was actually: " \ - + str(fail_json_double.mock_calls[0]) - - assert("Fake failure for testing boto exception messages" - in fail_json_double.mock_calls[0][2]["msg"]), \ - "error message doesn't include the local message; was: " \ - + str(fail_json_double.mock_calls[0]) - - # I would have thought this should work, however the botocore exception comes back with - # "argument of type 'NoneType' is not iterable" so it's probably not really designed - # to handle "None" as an error response. - # - # assert("Could not find you" in fail_json_double.mock_calls[0][2]["msg"]), \ - # "error message doesn't include the botocore exception message; was: " \ - # + str(fail_json_double.mock_calls[0]) - - -# TODO: -# - an exception without a message -# - plain boto exception -# - socket errors and other standard things. diff --git a/test/units/module_utils/ec2/test_aws.py b/test/units/module_utils/ec2/test_aws.py deleted file mode 100644 index 7c66442264..0000000000 --- a/test/units/module_utils/ec2/test_aws.py +++ /dev/null @@ -1,101 +0,0 @@ -# -*- coding: utf-8 -*- -# (c) 2015, Allen Sanabria <asanabria@linuxdynasty.org> -# -# This file is part of Ansible -# 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 - -try: - import boto3 - import botocore - HAS_BOTO3 = True -except Exception: - HAS_BOTO3 = False - -import pytest - -from units.compat import unittest -from ansible.module_utils.ec2 import AWSRetry - -if not HAS_BOTO3: - pytestmark = pytest.mark.skip("test_aws.py requires the python modules 'boto3' and 'botocore'") - - -class RetryTestCase(unittest.TestCase): - - def test_no_failures(self): - self.counter = 0 - - @AWSRetry.backoff(tries=2, delay=0.1) - def no_failures(): - self.counter += 1 - - r = no_failures() - self.assertEqual(self.counter, 1) - - def test_extend_boto3_failures(self): - self.counter = 0 - err_msg = {'Error': {'Code': 'MalformedPolicyDocument'}} - - @AWSRetry.backoff(tries=2, delay=0.1, catch_extra_error_codes=['MalformedPolicyDocument']) - def extend_failures(): - self.counter += 1 - if self.counter < 2: - raise botocore.exceptions.ClientError(err_msg, 'You did something wrong.') - else: - return 'success' - - r = extend_failures() - self.assertEqual(r, 'success') - self.assertEqual(self.counter, 2) - - def test_retry_once(self): - self.counter = 0 - err_msg = {'Error': {'Code': 'InternalFailure'}} - - @AWSRetry.backoff(tries=2, delay=0.1) - def retry_once(): - self.counter += 1 - if self.counter < 2: - raise botocore.exceptions.ClientError(err_msg, 'Something went wrong!') - else: - return 'success' - - r = retry_once() - self.assertEqual(r, 'success') - self.assertEqual(self.counter, 2) - - def test_reached_limit(self): - self.counter = 0 - err_msg = {'Error': {'Code': 'RequestLimitExceeded'}} - - @AWSRetry.backoff(tries=4, delay=0.1) - def fail(): - self.counter += 1 - raise botocore.exceptions.ClientError(err_msg, 'toooo fast!!') - - # with self.assertRaises(botocore.exceptions.ClientError): - try: - fail() - except Exception as e: - self.assertEqual(e.response['Error']['Code'], 'RequestLimitExceeded') - self.assertEqual(self.counter, 4) - - def test_unexpected_exception_does_not_retry(self): - self.counter = 0 - err_msg = {'Error': {'Code': 'AuthFailure'}} - - @AWSRetry.backoff(tries=4, delay=0.1) - def raise_unexpected_error(): - self.counter += 1 - raise botocore.exceptions.ClientError(err_msg, 'unexpected error') - - # with self.assertRaises(botocore.exceptions.ClientError): - try: - raise_unexpected_error() - except Exception as e: - self.assertEqual(e.response['Error']['Code'], 'AuthFailure') - - self.assertEqual(self.counter, 1) diff --git a/test/units/module_utils/test_ec2.py b/test/units/module_utils/test_ec2.py deleted file mode 100644 index dc748276e2..0000000000 --- a/test/units/module_utils/test_ec2.py +++ /dev/null @@ -1,234 +0,0 @@ -# (c) 2017 Red Hat Inc. -# -# This file is part of Ansible -# 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 - -import unittest - -from ansible.module_utils.ec2 import map_complex_type, compare_policies - - -class Ec2Utils(unittest.TestCase): - - def setUp(self): - # A pair of simple IAM Trust relationships using bools, the first a - # native bool the second a quoted string - self.bool_policy_bool = { - 'Version': '2012-10-17', - 'Statement': [ - { - "Action": "sts:AssumeRole", - "Condition": { - "Bool": {"aws:MultiFactorAuthPresent": True} - }, - "Effect": "Allow", - "Principal": {"AWS": "arn:aws:iam::XXXXXXXXXXXX:root"}, - "Sid": "AssumeRoleWithBoolean" - } - ] - } - - self.bool_policy_string = { - 'Version': '2012-10-17', - 'Statement': [ - { - "Action": "sts:AssumeRole", - "Condition": { - "Bool": {"aws:MultiFactorAuthPresent": "true"} - }, - "Effect": "Allow", - "Principal": {"AWS": "arn:aws:iam::XXXXXXXXXXXX:root"}, - "Sid": "AssumeRoleWithBoolean" - } - ] - } - - # A pair of simple bucket policies using numbers, the first a - # native int the second a quoted string - self.numeric_policy_number = { - 'Version': '2012-10-17', - 'Statement': [ - { - "Action": "s3:ListBucket", - "Condition": { - "NumericLessThanEquals": {"s3:max-keys": 15} - }, - "Effect": "Allow", - "Resource": "arn:aws:s3:::examplebucket", - "Sid": "s3ListBucketWithNumericLimit" - } - ] - } - - self.numeric_policy_string = { - 'Version': '2012-10-17', - 'Statement': [ - { - "Action": "s3:ListBucket", - "Condition": { - "NumericLessThanEquals": {"s3:max-keys": "15"} - }, - "Effect": "Allow", - "Resource": "arn:aws:s3:::examplebucket", - "Sid": "s3ListBucketWithNumericLimit" - } - ] - } - - self.small_policy_one = { - 'Version': '2012-10-17', - 'Statement': [ - { - 'Action': 's3:PutObjectAcl', - 'Sid': 'AddCannedAcl2', - 'Resource': 'arn:aws:s3:::test_policy/*', - 'Effect': 'Allow', - 'Principal': {'AWS': ['arn:aws:iam::XXXXXXXXXXXX:user/username1', 'arn:aws:iam::XXXXXXXXXXXX:user/username2']} - } - ] - } - - # The same as small_policy_one, except the single resource is in a list and the contents of Statement are jumbled - self.small_policy_two = { - 'Version': '2012-10-17', - 'Statement': [ - { - 'Effect': 'Allow', - 'Action': 's3:PutObjectAcl', - 'Principal': {'AWS': ['arn:aws:iam::XXXXXXXXXXXX:user/username1', 'arn:aws:iam::XXXXXXXXXXXX:user/username2']}, - 'Resource': ['arn:aws:s3:::test_policy/*'], - 'Sid': 'AddCannedAcl2' - } - ] - } - - self.larger_policy_one = { - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "Test", - "Effect": "Allow", - "Principal": { - "AWS": [ - "arn:aws:iam::XXXXXXXXXXXX:user/testuser1", - "arn:aws:iam::XXXXXXXXXXXX:user/testuser2" - ] - }, - "Action": "s3:PutObjectAcl", - "Resource": "arn:aws:s3:::test_policy/*" - }, - { - "Effect": "Allow", - "Principal": { - "AWS": "arn:aws:iam::XXXXXXXXXXXX:user/testuser2" - }, - "Action": [ - "s3:PutObject", - "s3:PutObjectAcl" - ], - "Resource": "arn:aws:s3:::test_policy/*" - } - ] - } - - # The same as larger_policy_one, except having a list of length 1 and jumbled contents - self.larger_policy_two = { - "Version": "2012-10-17", - "Statement": [ - { - "Principal": { - "AWS": ["arn:aws:iam::XXXXXXXXXXXX:user/testuser2"] - }, - "Effect": "Allow", - "Resource": "arn:aws:s3:::test_policy/*", - "Action": [ - "s3:PutObject", - "s3:PutObjectAcl" - ] - }, - { - "Action": "s3:PutObjectAcl", - "Principal": { - "AWS": [ - "arn:aws:iam::XXXXXXXXXXXX:user/testuser1", - "arn:aws:iam::XXXXXXXXXXXX:user/testuser2" - ] - }, - "Sid": "Test", - "Resource": "arn:aws:s3:::test_policy/*", - "Effect": "Allow" - } - ] - } - - # Different than larger_policy_two: a different principal is given - self.larger_policy_three = { - "Version": "2012-10-17", - "Statement": [ - { - "Principal": { - "AWS": ["arn:aws:iam::XXXXXXXXXXXX:user/testuser2"] - }, - "Effect": "Allow", - "Resource": "arn:aws:s3:::test_policy/*", - "Action": [ - "s3:PutObject", - "s3:PutObjectAcl"] - }, - { - "Action": "s3:PutObjectAcl", - "Principal": { - "AWS": [ - "arn:aws:iam::XXXXXXXXXXXX:user/testuser1", - "arn:aws:iam::XXXXXXXXXXXX:user/testuser3" - ] - }, - "Sid": "Test", - "Resource": "arn:aws:s3:::test_policy/*", - "Effect": "Allow" - } - ] - } - - def test_map_complex_type_over_dict(self): - complex_type = {'minimum_healthy_percent': "75", 'maximum_percent': "150"} - type_map = {'minimum_healthy_percent': 'int', 'maximum_percent': 'int'} - complex_type_mapped = map_complex_type(complex_type, type_map) - complex_type_expected = {'minimum_healthy_percent': 75, 'maximum_percent': 150} - self.assertEqual(complex_type_mapped, complex_type_expected) - - def test_compare_small_policies_without_differences(self): - """ Testing two small policies which are identical except for: - * The contents of the statement are in different orders - * The second policy contains a list of length one whereas in the first it is a string - """ - self.assertFalse(compare_policies(self.small_policy_one, self.small_policy_two)) - - def test_compare_large_policies_without_differences(self): - """ Testing two larger policies which are identical except for: - * The statements are in different orders - * The contents of the statements are also in different orders - * The second contains a list of length one for the Principal whereas in the first it is a string - """ - self.assertFalse(compare_policies(self.larger_policy_one, self.larger_policy_two)) - - def test_compare_larger_policies_with_difference(self): - """ Testing two larger policies which are identical except for: - * one different principal - """ - self.assertTrue(compare_policies(self.larger_policy_two, self.larger_policy_three)) - - def test_compare_smaller_policy_with_larger(self): - """ Testing two policies of different sizes """ - self.assertTrue(compare_policies(self.larger_policy_one, self.small_policy_one)) - - def test_compare_boolean_policy_bool_and_string_are_equal(self): - """ Testing two policies one using a quoted boolean, the other a bool """ - self.assertFalse(compare_policies(self.bool_policy_string, self.bool_policy_bool)) - - def test_compare_numeric_policy_number_and_string_are_equal(self): - """ Testing two policies one using a quoted number, the other an int """ - self.assertFalse(compare_policies(self.numeric_policy_string, self.numeric_policy_number)) diff --git a/test/units/modules/cloud/amazon/test_aws_s3.py b/test/units/modules/cloud/amazon/test_aws_s3.py deleted file mode 100644 index a752c67fcb..0000000000 --- a/test/units/modules/cloud/amazon/test_aws_s3.py +++ /dev/null @@ -1,38 +0,0 @@ -# Make coding more python3-ish -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import pytest - -import unittest - -try: - import ansible.modules.cloud.amazon.aws_s3 as s3 -except ImportError: - pytestmark = pytest.mark.skip("This test requires the s3 Python libraries") - -from ansible.module_utils.six.moves.urllib.parse import urlparse - -boto3 = pytest.importorskip("boto3") - - -class TestUrlparse(unittest.TestCase): - - def test_urlparse(self): - actual = urlparse("http://test.com/here") - self.assertEqual("http", actual.scheme) - self.assertEqual("test.com", actual.netloc) - self.assertEqual("/here", actual.path) - - def test_is_fakes3(self): - actual = s3.is_fakes3("fakes3://bla.blubb") - self.assertEqual(True, actual) - - def test_get_s3_connection(self): - aws_connect_kwargs = dict(aws_access_key_id="access_key", - aws_secret_access_key="secret_key") - location = None - rgw = True - s3_url = "http://bla.blubb" - actual = s3.get_s3_connection(None, aws_connect_kwargs, location, rgw, s3_url) - self.assertEqual(bool("bla.blubb" in str(actual._endpoint)), True) diff --git a/test/units/modules/cloud/amazon/test_cloudformation.py b/test/units/modules/cloud/amazon/test_cloudformation.py deleted file mode 100644 index fe99a8510a..0000000000 --- a/test/units/modules/cloud/amazon/test_cloudformation.py +++ /dev/null @@ -1,205 +0,0 @@ -# (c) 2017 Red Hat Inc. -# -# This file is part of Ansible -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -# Make coding more python3-ish -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import pytest - -from units.utils.amazon_placebo_fixtures import placeboify, maybe_sleep -from ansible.modules.cloud.amazon import cloudformation as cfn_module - -basic_yaml_tpl = """ ---- -AWSTemplateFormatVersion: '2010-09-09' -Description: 'Basic template that creates an S3 bucket' -Resources: - MyBucket: - Type: "AWS::S3::Bucket" -Outputs: - TheName: - Value: - !Ref MyBucket -""" - -bad_json_tpl = """{ - "AWSTemplateFormatVersion": "2010-09-09", - "Description": "Broken template, no comma here ->" - "Resources": { - "MyBucket": { - "Type": "AWS::S3::Bucket" - } - } -}""" - -failing_yaml_tpl = """ ---- -AWSTemplateFormatVersion: 2010-09-09 -Resources: - ECRRepo: - Type: AWS::ECR::Repository - Properties: - RepositoryPolicyText: - Version: 3000-10-17 # <--- invalid version - Statement: - - Effect: Allow - Action: - - 'ecr:*' - Principal: - AWS: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:root -""" - -default_events_limit = 10 - - -class FakeModule(object): - def __init__(self, **kwargs): - self.params = kwargs - - def fail_json(self, *args, **kwargs): - self.exit_args = args - self.exit_kwargs = kwargs - raise Exception('FAIL') - - def exit_json(self, *args, **kwargs): - self.exit_args = args - self.exit_kwargs = kwargs - raise Exception('EXIT') - - -def test_invalid_template_json(placeboify): - connection = placeboify.client('cloudformation') - params = { - 'StackName': 'ansible-test-wrong-json', - 'TemplateBody': bad_json_tpl, - } - m = FakeModule(disable_rollback=False) - with pytest.raises(Exception) as exc_info: - cfn_module.create_stack(m, params, connection, default_events_limit) - pytest.fail('Expected malformed JSON to have caused the call to fail') - - assert exc_info.match('FAIL') - assert "ValidationError" in m.exit_kwargs['msg'] - - -def test_client_request_token_s3_stack(maybe_sleep, placeboify): - connection = placeboify.client('cloudformation') - params = { - 'StackName': 'ansible-test-client-request-token-yaml', - 'TemplateBody': basic_yaml_tpl, - 'ClientRequestToken': '3faf3fb5-b289-41fc-b940-44151828f6cf', - } - m = FakeModule(disable_rollback=False) - result = cfn_module.create_stack(m, params, connection, default_events_limit) - assert result['changed'] - assert len(result['events']) > 1 - # require that the final recorded stack state was CREATE_COMPLETE - # events are retrieved newest-first, so 0 is the latest - assert 'CREATE_COMPLETE' in result['events'][0] - connection.delete_stack(StackName='ansible-test-client-request-token-yaml') - - -def test_basic_s3_stack(maybe_sleep, placeboify): - connection = placeboify.client('cloudformation') - params = { - 'StackName': 'ansible-test-basic-yaml', - 'TemplateBody': basic_yaml_tpl - } - m = FakeModule(disable_rollback=False) - result = cfn_module.create_stack(m, params, connection, default_events_limit) - assert result['changed'] - assert len(result['events']) > 1 - # require that the final recorded stack state was CREATE_COMPLETE - # events are retrieved newest-first, so 0 is the latest - assert 'CREATE_COMPLETE' in result['events'][0] - connection.delete_stack(StackName='ansible-test-basic-yaml') - - -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) - 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 - - -def test_missing_template_body(): - m = FakeModule() - with pytest.raises(Exception) as exc_info: - cfn_module.create_stack( - module=m, - stack_params={}, - cfn=None, - events_limit=default_events_limit - ) - pytest.fail('Expected module to have failed with no template') - - assert exc_info.match('FAIL') - assert not m.exit_args - assert "Either 'template', 'template_body' or 'template_url' is required when the stack does not exist." == m.exit_kwargs['msg'] - - -def test_on_create_failure_delete(maybe_sleep, placeboify): - m = FakeModule( - on_create_failure='DELETE', - disable_rollback=False, - ) - connection = placeboify.client('cloudformation') - params = { - 'StackName': 'ansible-test-on-create-failure-delete', - 'TemplateBody': failing_yaml_tpl - } - result = cfn_module.create_stack(m, params, connection, default_events_limit) - assert result['changed'] - assert result['failed'] - assert len(result['events']) > 1 - # require that the final recorded stack state was DELETE_COMPLETE - # events are retrieved newest-first, so 0 is the latest - assert 'DELETE_COMPLETE' in result['events'][0] - - -def test_on_create_failure_rollback(maybe_sleep, placeboify): - m = FakeModule( - on_create_failure='ROLLBACK', - disable_rollback=False, - ) - connection = placeboify.client('cloudformation') - params = { - 'StackName': 'ansible-test-on-create-failure-rollback', - 'TemplateBody': failing_yaml_tpl - } - result = cfn_module.create_stack(m, params, connection, default_events_limit) - assert result['changed'] - assert result['failed'] - assert len(result['events']) > 1 - # require that the final recorded stack state was ROLLBACK_COMPLETE - # events are retrieved newest-first, so 0 is the latest - assert 'ROLLBACK_COMPLETE' in result['events'][0] - connection.delete_stack(StackName=params['StackName']) - - -def test_on_create_failure_do_nothing(maybe_sleep, placeboify): - m = FakeModule( - on_create_failure='DO_NOTHING', - disable_rollback=False, - ) - connection = placeboify.client('cloudformation') - params = { - 'StackName': 'ansible-test-on-create-failure-do-nothing', - 'TemplateBody': failing_yaml_tpl - } - result = cfn_module.create_stack(m, params, connection, default_events_limit) - assert result['changed'] - assert result['failed'] - assert len(result['events']) > 1 - # require that the final recorded stack state was CREATE_FAILED - # events are retrieved newest-first, so 0 is the latest - assert 'CREATE_FAILED' in result['events'][0] - connection.delete_stack(StackName=params['StackName']) diff --git a/test/units/modules/cloud/amazon/test_ec2_group.py b/test/units/modules/cloud/amazon/test_ec2_group.py deleted file mode 100644 index 14f597f69d..0000000000 --- a/test/units/modules/cloud/amazon/test_ec2_group.py +++ /dev/null @@ -1,83 +0,0 @@ -# Make coding more python3-ish -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -from ansible.modules.cloud.amazon import ec2_group as group_module - - -def test_from_permission(): - internal_http = { - u'FromPort': 80, - u'IpProtocol': 'tcp', - u'IpRanges': [ - { - u'CidrIp': '10.0.0.0/8', - u'Description': 'Foo Bar Baz' - }, - ], - u'Ipv6Ranges': [ - {u'CidrIpv6': 'fe80::94cc:8aff:fef6:9cc/64'}, - ], - u'PrefixListIds': [], - u'ToPort': 80, - u'UserIdGroupPairs': [], - } - perms = list(group_module.rule_from_group_permission(internal_http)) - assert len(perms) == 2 - assert perms[0].target == '10.0.0.0/8' - assert perms[0].target_type == 'ipv4' - assert perms[0].description == 'Foo Bar Baz' - assert perms[1].target == 'fe80::94cc:8aff:fef6:9cc/64' - - global_egress = { - 'IpProtocol': '-1', - 'IpRanges': [{'CidrIp': '0.0.0.0/0'}], - 'Ipv6Ranges': [], - 'PrefixListIds': [], - 'UserIdGroupPairs': [] - } - perms = list(group_module.rule_from_group_permission(global_egress)) - assert len(perms) == 1 - assert perms[0].target == '0.0.0.0/0' - assert perms[0].port_range == (None, None) - - internal_prefix_http = { - u'FromPort': 80, - u'IpProtocol': 'tcp', - u'PrefixListIds': [ - {'PrefixListId': 'p-1234'} - ], - u'ToPort': 80, - u'UserIdGroupPairs': [], - } - perms = list(group_module.rule_from_group_permission(internal_prefix_http)) - assert len(perms) == 1 - assert perms[0].target == 'p-1234' - - -def test_rule_to_permission(): - tests = [ - group_module.Rule((22, 22), 'udp', 'sg-1234567890', 'group', None), - group_module.Rule((1, 65535), 'tcp', '0.0.0.0/0', 'ipv4', "All TCP from everywhere"), - group_module.Rule((443, 443), 'tcp', 'ip-123456', 'ip_prefix', "Traffic to privatelink IPs"), - group_module.Rule((443, 443), 'tcp', 'feed:dead:::beef/64', 'ipv6', None), - ] - for test in tests: - perm = group_module.to_permission(test) - assert perm['FromPort'], perm['ToPort'] == test.port_range - assert perm['IpProtocol'] == test.protocol - - -def test_validate_ip(): - class Warner(object): - def warn(self, msg): - return - ips = [ - ('1.1.1.1/24', '1.1.1.0/24'), - ('192.168.56.101/16', '192.168.0.0/16'), - # Don't modify IPv6 CIDRs, AWS supports /128 and device ranges - ('1203:8fe0:fe80:b897:8990:8a7c:99bf:323d/128', '1203:8fe0:fe80:b897:8990:8a7c:99bf:323d/128'), - ] - - for ip, net in ips: - assert group_module.validate_ip(Warner(), ip) == net diff --git a/test/units/plugins/inventory/test_aws_ec2.py b/test/units/plugins/inventory/test_aws_ec2.py deleted file mode 100644 index 06137a3ec2..0000000000 --- a/test/units/plugins/inventory/test_aws_ec2.py +++ /dev/null @@ -1,183 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright 2017 Sloane Hertel <shertel@redhat.com> -# -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see <http://www.gnu.org/licenses/>. - -# Make coding more python3-ish -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import pytest -import datetime - -# Just to test that we have the prerequisite for InventoryModule and instance_data_filter_to_boto_attr -boto3 = pytest.importorskip('boto3') -botocore = pytest.importorskip('botocore') - -from ansible.errors import AnsibleError -from ansible.plugins.inventory.aws_ec2 import InventoryModule, instance_data_filter_to_boto_attr - -instances = { - u'Instances': [ - {u'Monitoring': {u'State': 'disabled'}, - u'PublicDnsName': 'ec2-12-345-67-890.compute-1.amazonaws.com', - u'State': {u'Code': 16, u'Name': 'running'}, - u'EbsOptimized': False, - u'LaunchTime': datetime.datetime(2017, 10, 31, 12, 59, 25), - u'PublicIpAddress': '12.345.67.890', - u'PrivateIpAddress': '098.76.54.321', - u'ProductCodes': [], - u'VpcId': 'vpc-12345678', - u'StateTransitionReason': '', - u'InstanceId': 'i-00000000000000000', - u'EnaSupport': True, - u'ImageId': 'ami-12345678', - u'PrivateDnsName': 'ip-098-76-54-321.ec2.internal', - u'KeyName': 'testkey', - u'SecurityGroups': [{u'GroupName': 'default', u'GroupId': 'sg-12345678'}], - u'ClientToken': '', - u'SubnetId': 'subnet-12345678', - u'InstanceType': 't2.micro', - u'NetworkInterfaces': [ - {u'Status': 'in-use', - u'MacAddress': '12:a0:50:42:3d:a4', - u'SourceDestCheck': True, - u'VpcId': 'vpc-12345678', - u'Description': '', - u'NetworkInterfaceId': 'eni-12345678', - u'PrivateIpAddresses': [ - {u'PrivateDnsName': 'ip-098-76-54-321.ec2.internal', - u'PrivateIpAddress': '098.76.54.321', - u'Primary': True, - u'Association': - {u'PublicIp': '12.345.67.890', - u'PublicDnsName': 'ec2-12-345-67-890.compute-1.amazonaws.com', - u'IpOwnerId': 'amazon'}}], - u'PrivateDnsName': 'ip-098-76-54-321.ec2.internal', - u'Attachment': - {u'Status': 'attached', - u'DeviceIndex': 0, - u'DeleteOnTermination': True, - u'AttachmentId': 'eni-attach-12345678', - u'AttachTime': datetime.datetime(2017, 10, 31, 12, 59, 25)}, - u'Groups': [ - {u'GroupName': 'default', - u'GroupId': 'sg-12345678'}], - u'Ipv6Addresses': [], - u'OwnerId': '123456789000', - u'PrivateIpAddress': '098.76.54.321', - u'SubnetId': 'subnet-12345678', - u'Association': - {u'PublicIp': '12.345.67.890', - u'PublicDnsName': 'ec2-12-345-67-890.compute-1.amazonaws.com', - u'IpOwnerId': 'amazon'}}], - u'SourceDestCheck': True, - u'Placement': - {u'Tenancy': 'default', - u'GroupName': '', - u'AvailabilityZone': 'us-east-1c'}, - u'Hypervisor': 'xen', - u'BlockDeviceMappings': [ - {u'DeviceName': '/dev/xvda', - u'Ebs': - {u'Status': 'attached', - u'DeleteOnTermination': True, - u'VolumeId': 'vol-01234567890000000', - u'AttachTime': datetime.datetime(2017, 10, 31, 12, 59, 26)}}], - u'Architecture': 'x86_64', - u'RootDeviceType': 'ebs', - u'RootDeviceName': '/dev/xvda', - u'VirtualizationType': 'hvm', - u'Tags': [{u'Value': 'test', u'Key': 'ansible'}, {u'Value': 'aws_ec2', u'Key': 'name'}], - u'AmiLaunchIndex': 0}], - u'ReservationId': 'r-01234567890000000', - u'Groups': [], - u'OwnerId': '123456789000' -} - - -@pytest.fixture(scope="module") -def inventory(): - return InventoryModule() - - -def test_compile_values(inventory): - found_value = instances['Instances'][0] - chain_of_keys = instance_data_filter_to_boto_attr['instance.group-id'] - for attr in chain_of_keys: - found_value = inventory._compile_values(found_value, attr) - assert found_value == "sg-12345678" - - -def test_get_boto_attr_chain(inventory): - instance = instances['Instances'][0] - assert inventory._get_boto_attr_chain('network-interface.addresses.private-ip-address', instance) == "098.76.54.321" - - -def test_boto3_conn(inventory): - inventory._options = {"aws_profile": "first_precedence", - "aws_access_key": "test_access_key", - "aws_secret_key": "test_secret_key", - "aws_security_token": "test_security_token", - "iam_role_arn": None} - inventory._set_credentials() - with pytest.raises(AnsibleError) as error_message: - for connection, region in inventory._boto3_conn(regions=['us-east-1']): - assert "Insufficient credentials found" in error_message - - -def test_get_hostname_default(inventory): - instance = instances['Instances'][0] - assert inventory._get_hostname(instance, hostnames=None) == "ec2-12-345-67-890.compute-1.amazonaws.com" - - -def test_get_hostname(inventory): - hostnames = ['ip-address', 'dns-name'] - instance = instances['Instances'][0] - assert inventory._get_hostname(instance, hostnames) == "12.345.67.890" - - -def test_set_credentials(inventory): - inventory._options = {'aws_access_key': 'test_access_key', - 'aws_secret_key': 'test_secret_key', - 'aws_security_token': 'test_security_token', - 'aws_profile': 'test_profile', - 'iam_role_arn': 'arn:aws:iam::112233445566:role/test-role'} - inventory._set_credentials() - - assert inventory.boto_profile == "test_profile" - assert inventory.aws_access_key_id == "test_access_key" - assert inventory.aws_secret_access_key == "test_secret_key" - assert inventory.aws_security_token == "test_security_token" - assert inventory.iam_role_arn == "arn:aws:iam::112233445566:role/test-role" - - -def test_insufficient_credentials(inventory): - inventory._options = { - 'aws_access_key': None, - 'aws_secret_key': None, - 'aws_security_token': None, - 'aws_profile': None, - 'iam_role_arn': None - } - with pytest.raises(AnsibleError) as error_message: - inventory._set_credentials() - assert "Insufficient credentials found" in error_message - - -def test_verify_file_bad_config(inventory): - assert inventory.verify_file('not_aws_config.yml') is False diff --git a/test/units/plugins/lookup/fixtures/avi.json b/test/units/plugins/lookup/fixtures/avi.json deleted file mode 100644 index ae89ca689c..0000000000 --- a/test/units/plugins/lookup/fixtures/avi.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "mock_single_obj": { - "_last_modified": "", - "cloud_ref": "https://192.0.2.132/api/cloud/cloud-4d063be1-99c2-44cf-8b28-977bd970524c", - "dhcp_enabled": true, - "exclude_discovered_subnets": false, - "name": "PG-123", - "synced_from_se": true, - "tenant_ref": "https://192.0.2.132/api/tenant/admin", - "url": "https://192.0.2.132/api/network/dvportgroup-2084-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", - "uuid": "dvportgroup-2084-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", - "vcenter_dvs": true, - "vimgrnw_ref": "https://192.0.2.132/api/vimgrnwruntime/dvportgroup-2084-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", - "vrf_context_ref": "https://192.0.2.132/api/vrfcontext/vrfcontext-31f1b55f-319c-44eb-862f-69d79ffdf295" - }, - "mock_multiple_obj": { - "results": [ - { - "_last_modified": "", - "cloud_ref": "https://192.0.2.132/api/cloud/cloud-4d063be1-99c2-44cf-8b28-977bd970524c", - "dhcp_enabled": true, - "exclude_discovered_subnets": false, - "name": "J-PG-0682", - "synced_from_se": true, - "tenant_ref": "https://192.0.2.132/api/tenant/admin", - "url": "https://192.0.2.132/api/network/dvportgroup-2084-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", - "uuid": "dvportgroup-2084-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", - "vcenter_dvs": true, - "vimgrnw_ref": "https://192.0.2.132/api/vimgrnwruntime/dvportgroup-2084-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", - "vrf_context_ref": "https://192.0.2.132/api/vrfcontext/vrfcontext-31f1b55f-319c-44eb-862f-69d79ffdf295" - }, - { - "_last_modified": "", - "cloud_ref": "https://192.0.2.132/api/cloud/cloud-4d063be1-99c2-44cf-8b28-977bd970524c", - "dhcp_enabled": true, - "exclude_discovered_subnets": false, - "name": "J-PG-0231", - "synced_from_se": true, - "tenant_ref": "https://192.0.2.132/api/tenant/admin", - "url": "https://192.0.2.132/api/network/dvportgroup-1627-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", - "uuid": "dvportgroup-1627-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", - "vcenter_dvs": true, - "vimgrnw_ref": "https://192.0.2.132/api/vimgrnwruntime/dvportgroup-1627-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", - "vrf_context_ref": "https://192.0.2.132/api/vrfcontext/vrfcontext-31f1b55f-319c-44eb-862f-69d79ffdf295" - }, - { - "_last_modified": "", - "cloud_ref": "https://192.0.2.132/api/cloud/cloud-4d063be1-99c2-44cf-8b28-977bd970524c", - "dhcp_enabled": true, - "exclude_discovered_subnets": false, - "name": "J-PG-0535", - "synced_from_se": true, - "tenant_ref": "https://192.0.2.132/api/tenant/admin", - "url": "https://192.0.2.132/api/network/dvportgroup-1934-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", - "uuid": "dvportgroup-1934-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", - "vcenter_dvs": true, - "vimgrnw_ref": "https://192.0.2.132/api/vimgrnwruntime/dvportgroup-1934-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", - "vrf_context_ref": "https://192.0.2.132/api/vrfcontext/vrfcontext-31f1b55f-319c-44eb-862f-69d79ffdf295" - }, - { - "_last_modified": "", - "cloud_ref": "https://192.0.2.132/api/cloud/cloud-4d063be1-99c2-44cf-8b28-977bd970524c", - "dhcp_enabled": true, - "exclude_discovered_subnets": false, - "name": "J-PG-0094", - "synced_from_se": true, - "tenant_ref": "https://192.0.2.132/api/tenant/admin", - "url": "https://192.0.2.132/api/network/dvportgroup-1458-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", - "uuid": "dvportgroup-1458-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", - "vcenter_dvs": true, - "vimgrnw_ref": "https://192.0.2.132/api/vimgrnwruntime/dvportgroup-1458-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", - "vrf_context_ref": "https://192.0.2.132/api/vrfcontext/vrfcontext-31f1b55f-319c-44eb-862f-69d79ffdf295" - }, - { - "_last_modified": "", - "cloud_ref": "https://192.0.2.132/api/cloud/cloud-4d063be1-99c2-44cf-8b28-977bd970524c", - "dhcp_enabled": true, - "exclude_discovered_subnets": false, - "name": "J-PG-0437", - "synced_from_se": true, - "tenant_ref": "https://192.0.2.132/api/tenant/admin", - "url": "https://192.0.2.132/api/network/dvportgroup-1836-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", - "uuid": "dvportgroup-1836-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", - "vcenter_dvs": true, - "vimgrnw_ref": "https://192.0.2.132/api/vimgrnwruntime/dvportgroup-1836-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", - "vrf_context_ref": "https://192.0.2.132/api/vrfcontext/vrfcontext-31f1b55f-319c-44eb-862f-69d79ffdf295" - }, - { - "_last_modified": "", - "cloud_ref": "https://192.0.2.132/api/cloud/cloud-4d063be1-99c2-44cf-8b28-977bd970524c", - "dhcp_enabled": true, - "exclude_discovered_subnets": false, - "name": "J-PG-0673", - "synced_from_se": true, - "tenant_ref": "https://192.0.2.132/api/tenant/admin", - "url": "https://192.0.2.132/api/network/dvportgroup-2075-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", - "uuid": "dvportgroup-2075-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", - "vcenter_dvs": true, - "vimgrnw_ref": "https://192.0.2.132/api/vimgrnwruntime/dvportgroup-2075-cloud-4d063be1-99c2-44cf-8b28-977bd970524c", - "vrf_context_ref": "https://192.0.2.132/api/vrfcontext/vrfcontext-31f1b55f-319c-44eb-862f-69d79ffdf295" - } - ] - } -} diff --git a/test/units/plugins/lookup/test_aws_secret.py b/test/units/plugins/lookup/test_aws_secret.py deleted file mode 100644 index ae7734501c..0000000000 --- a/test/units/plugins/lookup/test_aws_secret.py +++ /dev/null @@ -1,90 +0,0 @@ -# (c) 2019 Robert Williams -# -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see <http://www.gnu.org/licenses/>. - -# Make coding more python3-ish -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import pytest -import datetime - -from ansible.errors import AnsibleError - -from ansible.plugins.loader import lookup_loader - -try: - import boto3 - from botocore.exceptions import ClientError -except ImportError: - pytestmark = pytest.mark.skip("This test requires the boto3 and botocore Python libraries") - - -@pytest.fixture -def dummy_credentials(): - dummy_credentials = {} - dummy_credentials['boto_profile'] = None - dummy_credentials['aws_secret_key'] = "notasecret" - dummy_credentials['aws_access_key'] = "notakey" - dummy_credentials['aws_security_token'] = None - dummy_credentials['region'] = 'eu-west-1' - return dummy_credentials - - -def test_lookup_variable(mocker, dummy_credentials): - dateutil_tz = pytest.importorskip("dateutil.tz") - simple_variable_success_response = { - 'Name': 'secret', - 'VersionId': 'cafe8168-e6ce-4e59-8830-5b143faf6c52', - 'SecretString': '{"secret":"simplesecret"}', - 'VersionStages': ['AWSCURRENT'], - 'CreatedDate': datetime.datetime(2019, 4, 4, 11, 41, 0, 878000, tzinfo=dateutil_tz.tzlocal()), - 'ResponseMetadata': { - 'RequestId': '21099462-597c-490a-800f-8b7a41e5151c', - 'HTTPStatusCode': 200, - 'HTTPHeaders': { - 'date': 'Thu, 04 Apr 2019 10:43:12 GMT', - 'content-type': 'application/x-amz-json-1.1', - 'content-length': '252', - 'connection': 'keep-alive', - 'x-amzn-requestid': '21099462-597c-490a-800f-8b7a41e5151c' - }, - 'RetryAttempts': 0 - } - } - lookup = lookup_loader.get('aws_secret') - boto3_double = mocker.MagicMock() - boto3_double.Session.return_value.client.return_value.get_secret_value.return_value = simple_variable_success_response - boto3_client_double = boto3_double.Session.return_value.client - - mocker.patch.object(boto3, 'session', boto3_double) - retval = lookup.run(["simple_variable"], None, **dummy_credentials) - assert(retval[0] == '{"secret":"simplesecret"}') - boto3_client_double.assert_called_with('secretsmanager', 'eu-west-1', aws_access_key_id='notakey', - aws_secret_access_key="notasecret", aws_session_token=None) - - -error_response = {'Error': {'Code': 'ResourceNotFoundException', 'Message': 'Fake Testing Error'}} -operation_name = 'FakeOperation' - - -def test_warn_denied_variable(mocker, dummy_credentials): - boto3_double = mocker.MagicMock() - boto3_double.Session.return_value.client.return_value.get_secret_value.side_effect = ClientError(error_response, operation_name) - - with pytest.raises(AnsibleError): - mocker.patch.object(boto3, 'session', boto3_double) - lookup_loader.get('aws_secret').run(["denied_variable"], None, **dummy_credentials) diff --git a/test/units/plugins/lookup/test_aws_ssm.py b/test/units/plugins/lookup/test_aws_ssm.py deleted file mode 100644 index 811ccfb489..0000000000 --- a/test/units/plugins/lookup/test_aws_ssm.py +++ /dev/null @@ -1,166 +0,0 @@ -# -# (c) 2017 Michael De La Rue -# -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see <http://www.gnu.org/licenses/>. - -# Make coding more python3-ish -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -import pytest -from copy import copy - -from ansible.errors import AnsibleError - -from ansible.plugins.lookup import aws_ssm - -try: - import boto3 - from botocore.exceptions import ClientError -except ImportError: - pytestmark = pytest.mark.skip("This test requires the boto3 and botocore Python libraries") - -simple_variable_success_response = { - 'Parameters': [ - { - 'Name': 'simple_variable', - 'Type': 'String', - 'Value': 'simplevalue', - 'Version': 1 - } - ], - 'InvalidParameters': [], - 'ResponseMetadata': { - 'RequestId': '12121212-3434-5656-7878-9a9a9a9a9a9a', - 'HTTPStatusCode': 200, - 'HTTPHeaders': { - 'x-amzn-requestid': '12121212-3434-5656-7878-9a9a9a9a9a9a', - 'content-type': 'application/x-amz-json-1.1', - 'content-length': '116', - 'date': 'Tue, 23 Jan 2018 11:04:27 GMT' - }, - 'RetryAttempts': 0 - } -} - -path_success_response = copy(simple_variable_success_response) -path_success_response['Parameters'] = [ - {'Name': '/testpath/too', 'Type': 'String', 'Value': 'simple_value_too', 'Version': 1}, - {'Name': '/testpath/won', 'Type': 'String', 'Value': 'simple_value_won', 'Version': 1} -] - -missing_variable_response = copy(simple_variable_success_response) -missing_variable_response['Parameters'] = [] -missing_variable_response['InvalidParameters'] = ['missing_variable'] - -some_missing_variable_response = copy(simple_variable_success_response) -some_missing_variable_response['Parameters'] = [ - {'Name': 'simple', 'Type': 'String', 'Value': 'simple_value', 'Version': 1}, - {'Name': '/testpath/won', 'Type': 'String', 'Value': 'simple_value_won', 'Version': 1} -] -some_missing_variable_response['InvalidParameters'] = ['missing_variable'] - - -dummy_credentials = {} -dummy_credentials['boto_profile'] = None -dummy_credentials['aws_secret_key'] = "notasecret" -dummy_credentials['aws_access_key'] = "notakey" -dummy_credentials['aws_security_token'] = None -dummy_credentials['region'] = 'eu-west-1' - - -def test_lookup_variable(mocker): - lookup = aws_ssm.LookupModule() - lookup._load_name = "aws_ssm" - - boto3_double = mocker.MagicMock() - boto3_double.Session.return_value.client.return_value.get_parameters.return_value = simple_variable_success_response - boto3_client_double = boto3_double.Session.return_value.client - - mocker.patch.object(boto3, 'session', boto3_double) - retval = lookup.run(["simple_variable"], {}, **dummy_credentials) - assert(retval[0] == "simplevalue") - boto3_client_double.assert_called_with('ssm', 'eu-west-1', aws_access_key_id='notakey', - aws_secret_access_key="notasecret", aws_session_token=None) - - -def test_path_lookup_variable(mocker): - lookup = aws_ssm.LookupModule() - lookup._load_name = "aws_ssm" - - boto3_double = mocker.MagicMock() - get_path_fn = boto3_double.Session.return_value.client.return_value.get_parameters_by_path - get_path_fn.return_value = path_success_response - boto3_client_double = boto3_double.Session.return_value.client - - mocker.patch.object(boto3, 'session', boto3_double) - args = copy(dummy_credentials) - args["bypath"] = 'true' - retval = lookup.run(["/testpath"], {}, **args) - assert(retval[0]["/testpath/won"] == "simple_value_won") - assert(retval[0]["/testpath/too"] == "simple_value_too") - boto3_client_double.assert_called_with('ssm', 'eu-west-1', aws_access_key_id='notakey', - aws_secret_access_key="notasecret", aws_session_token=None) - get_path_fn.assert_called_with(Path="/testpath", Recursive=False, WithDecryption=True) - - -def test_return_none_for_missing_variable(mocker): - """ - during jinja2 templates, we can't shouldn't normally raise exceptions since this blocks the ability to use defaults. - - for this reason we return ```None``` for missing variables - """ - lookup = aws_ssm.LookupModule() - lookup._load_name = "aws_ssm" - - boto3_double = mocker.MagicMock() - boto3_double.Session.return_value.client.return_value.get_parameters.return_value = missing_variable_response - - mocker.patch.object(boto3, 'session', boto3_double) - retval = lookup.run(["missing_variable"], {}, **dummy_credentials) - assert(retval[0] is None) - - -def test_match_retvals_to_call_params_even_with_some_missing_variables(mocker): - """ - If we get a complex list of variables with some missing and some not, we still have to return a - list which matches with the original variable list. - """ - lookup = aws_ssm.LookupModule() - lookup._load_name = "aws_ssm" - - boto3_double = mocker.MagicMock() - boto3_double.Session.return_value.client.return_value.get_parameters.return_value = some_missing_variable_response - - mocker.patch.object(boto3, 'session', boto3_double) - retval = lookup.run(["simple", "missing_variable", "/testpath/won", "simple"], {}, **dummy_credentials) - assert(retval == ["simple_value", None, "simple_value_won", "simple_value"]) - - -error_response = {'Error': {'Code': 'ResourceNotFoundException', 'Message': 'Fake Testing Error'}} -operation_name = 'FakeOperation' - - -def test_warn_denied_variable(mocker): - lookup = aws_ssm.LookupModule() - lookup._load_name = "aws_ssm" - - boto3_double = mocker.MagicMock() - boto3_double.Session.return_value.client.return_value.get_parameters.side_effect = ClientError(error_response, operation_name) - - with pytest.raises(AnsibleError): - mocker.patch.object(boto3, 'session', boto3_double) - lookup.run(["denied_variable"], {}, **dummy_credentials) |