1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3# This file is part of Ansible
4#
5# Ansible is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9#
10# Ansible is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License
16# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
17
18ANSIBLE_METADATA = {'metadata_version': '1.1',
19                    'status': ['preview'],
20                    'supported_by': 'community'}
21
22
23DOCUMENTATION = '''
24---
25module: ec2_ami_copy
26short_description: copies AMI between AWS regions, return new image id
27description:
28    - Copies AMI from a source region to a destination region. B(Since version 2.3 this module depends on boto3.)
29version_added: "2.0"
30options:
31  source_region:
32    description:
33      - The source region the AMI should be copied from.
34    required: true
35  source_image_id:
36    description:
37      - The ID of the AMI in source region that should be copied.
38    required: true
39  name:
40    description:
41      - The name of the new AMI to copy. (As of 2.3 the default is 'default', in prior versions it was 'null'.)
42    default: "default"
43  description:
44    description:
45      - An optional human-readable string describing the contents and purpose of the new AMI.
46  encrypted:
47    description:
48      - Whether or not the destination snapshots of the copied AMI should be encrypted.
49    version_added: "2.2"
50    type: bool
51  kms_key_id:
52    description:
53      - KMS key id used to encrypt image. If not specified, uses default EBS Customer Master Key (CMK) for your account.
54    version_added: "2.2"
55  wait:
56    description:
57      - Wait for the copied AMI to be in state 'available' before returning.
58    type: bool
59    default: 'no'
60  wait_timeout:
61    description:
62      - How long before wait gives up, in seconds. Prior to 2.3 the default was 1200.
63      - From 2.3-2.5 this option was deprecated in favor of boto3 waiter defaults.
64        This was reenabled in 2.6 to allow timeouts greater than 10 minutes.
65    default: 600
66  tags:
67    description:
68      - A hash/dictionary of tags to add to the new copied AMI; '{"key":"value"}' and '{"key":"value","key":"value"}'
69  tag_equality:
70    description:
71      - Whether to use tags if the source AMI already exists in the target region. If this is set, and all tags match
72        in an existing AMI, the AMI will not be copied again.
73    default: false
74    type: bool
75    version_added: 2.6
76author:
77- Amir Moulavi (@amir343) <amir.moulavi@gmail.com>
78- Tim C (@defunctio) <defunct@defunct.io>
79extends_documentation_fragment:
80    - aws
81    - ec2
82requirements:
83    - boto3
84'''
85
86EXAMPLES = '''
87# Basic AMI Copy
88- ec2_ami_copy:
89    source_region: us-east-1
90    region: eu-west-1
91    source_image_id: ami-xxxxxxx
92
93# AMI copy wait until available
94- ec2_ami_copy:
95    source_region: us-east-1
96    region: eu-west-1
97    source_image_id: ami-xxxxxxx
98    wait: yes
99    wait_timeout: 1200  # Default timeout is 600
100  register: image_id
101
102# Named AMI copy
103- ec2_ami_copy:
104    source_region: us-east-1
105    region: eu-west-1
106    source_image_id: ami-xxxxxxx
107    name: My-Awesome-AMI
108    description: latest patch
109
110# Tagged AMI copy (will not copy the same AMI twice)
111- ec2_ami_copy:
112    source_region: us-east-1
113    region: eu-west-1
114    source_image_id: ami-xxxxxxx
115    tags:
116        Name: My-Super-AMI
117        Patch: 1.2.3
118    tag_equality: yes
119
120# Encrypted AMI copy
121- ec2_ami_copy:
122    source_region: us-east-1
123    region: eu-west-1
124    source_image_id: ami-xxxxxxx
125    encrypted: yes
126
127# Encrypted AMI copy with specified key
128- ec2_ami_copy:
129    source_region: us-east-1
130    region: eu-west-1
131    source_image_id: ami-xxxxxxx
132    encrypted: yes
133    kms_key_id: arn:aws:kms:us-east-1:XXXXXXXXXXXX:key/746de6ea-50a4-4bcb-8fbc-e3b29f2d367b
134'''
135
136RETURN = '''
137image_id:
138  description: AMI ID of the copied AMI
139  returned: always
140  type: str
141  sample: ami-e689729e
142'''
143
144from ansible.module_utils.aws.core import AnsibleAWSModule
145from ansible.module_utils.ec2 import ec2_argument_spec
146from ansible.module_utils.ec2 import camel_dict_to_snake_dict, ansible_dict_to_boto3_tag_list
147from ansible.module_utils._text import to_native
148
149try:
150    from botocore.exceptions import ClientError, NoCredentialsError, WaiterError, BotoCoreError
151    HAS_BOTO3 = True
152except ImportError:
153    HAS_BOTO3 = False
154
155
156def copy_image(module, ec2):
157    """
158    Copies an AMI
159
160    module : AnsibleModule object
161    ec2: ec2 connection object
162    """
163
164    image = None
165    changed = False
166    tags = module.params.get('tags')
167
168    params = {'SourceRegion': module.params.get('source_region'),
169              'SourceImageId': module.params.get('source_image_id'),
170              'Name': module.params.get('name'),
171              'Description': module.params.get('description'),
172              'Encrypted': module.params.get('encrypted'),
173              }
174    if module.params.get('kms_key_id'):
175        params['KmsKeyId'] = module.params.get('kms_key_id')
176
177    try:
178        if module.params.get('tag_equality'):
179            filters = [{'Name': 'tag:%s' % k, 'Values': [v]} for (k, v) in module.params.get('tags').items()]
180            filters.append(dict(Name='state', Values=['available', 'pending']))
181            images = ec2.describe_images(Filters=filters)
182            if len(images['Images']) > 0:
183                image = images['Images'][0]
184        if not image:
185            image = ec2.copy_image(**params)
186            image_id = image['ImageId']
187            if tags:
188                ec2.create_tags(Resources=[image_id],
189                                Tags=ansible_dict_to_boto3_tag_list(tags))
190            changed = True
191
192        if module.params.get('wait'):
193            delay = 15
194            max_attempts = module.params.get('wait_timeout') // delay
195            image_id = image.get('ImageId')
196            ec2.get_waiter('image_available').wait(
197                ImageIds=[image_id],
198                WaiterConfig={'Delay': delay, 'MaxAttempts': max_attempts}
199            )
200
201        module.exit_json(changed=changed, **camel_dict_to_snake_dict(image))
202    except WaiterError as e:
203        module.fail_json_aws(e, msg='An error occurred waiting for the image to become available')
204    except (ClientError, BotoCoreError) as e:
205        module.fail_json_aws(e, msg="Could not copy AMI")
206    except Exception as e:
207        module.fail_json(msg='Unhandled exception. (%s)' % to_native(e))
208
209
210def main():
211    argument_spec = ec2_argument_spec()
212    argument_spec.update(dict(
213        source_region=dict(required=True),
214        source_image_id=dict(required=True),
215        name=dict(default='default'),
216        description=dict(default=''),
217        encrypted=dict(type='bool', default=False, required=False),
218        kms_key_id=dict(type='str', required=False),
219        wait=dict(type='bool', default=False),
220        wait_timeout=dict(type='int', default=600),
221        tags=dict(type='dict')),
222        tag_equality=dict(type='bool', default=False))
223
224    module = AnsibleAWSModule(argument_spec=argument_spec)
225    # TODO: Check botocore version
226    ec2 = module.client('ec2')
227    copy_image(module, ec2)
228
229
230if __name__ == '__main__':
231    main()
232