1#!/usr/bin/python
2# Copyright: Ansible Project
3# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
4
5from __future__ import absolute_import, division, print_function
6__metaclass__ = type
7
8
9ANSIBLE_METADATA = {'metadata_version': '1.1',
10                    'status': ['preview'],
11                    'supported_by': 'community'}
12
13DOCUMENTATION = '''
14---
15module: ec2_instance
16short_description: Create & manage EC2 instances
17description:
18    - Create and manage AWS EC2 instance
19version_added: "2.5"
20author:
21  - Ryan Scott Brown (@ryansb)
22requirements: [ "boto3", "botocore" ]
23options:
24  instance_ids:
25    description:
26      - If you specify one or more instance IDs, only instances that have the specified IDs are returned.
27  state:
28    description:
29      - Goal state for the instances.
30    choices: [present, terminated, running, started, stopped, restarted, rebooted, absent]
31    default: present
32  wait:
33    description:
34      - Whether or not to wait for the desired state (use wait_timeout to customize this).
35    default: true
36    type: bool
37  wait_timeout:
38    description:
39      - How long to wait (in seconds) for the instance to finish booting/terminating.
40    default: 600
41  instance_type:
42    description:
43      - Instance type to use for the instance, see U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-types.html)
44        Only required when instance is not already present.
45    default: t2.micro
46  user_data:
47    description:
48      - Opaque blob of data which is made available to the ec2 instance
49  tower_callback:
50    description:
51      - Preconfigured user-data to enable an instance to perform a Tower callback (Linux only).
52      - Mutually exclusive with I(user_data).
53      - For Windows instances, to enable remote access via Ansible set I(tower_callback.windows) to true, and optionally set an admin password.
54      - If using 'windows' and 'set_password', callback to Tower will not be performed but the instance will be ready to receive winrm connections from Ansible.
55    suboptions:
56      tower_address:
57        description:
58        - IP address or DNS name of Tower server. Must be accessible via this address from the VPC that this instance will be launched in.
59      job_template_id:
60        description:
61        - Either the integer ID of the Tower Job Template, or the name (name supported only for Tower 3.2+).
62      host_config_key:
63        description:
64        - Host configuration secret key generated by the Tower job template.
65  tags:
66    description:
67      - A hash/dictionary of tags to add to the new instance or to add/remove from an existing one.
68  purge_tags:
69    description:
70      - Delete any tags not specified in the task that are on the instance.
71        This means you have to specify all the desired tags on each task affecting an instance.
72    default: false
73    type: bool
74  image:
75    description:
76      - An image to use for the instance. The M(ec2_ami_info) module may be used to retrieve images.
77        One of I(image) or I(image_id) are required when instance is not already present.
78      - Complex object containing I(image.id), I(image.ramdisk), and I(image.kernel).
79      - I(image.id) is the AMI ID.
80      - I(image.ramdisk) overrides the AMI's default ramdisk ID.
81      - I(image.kernel) is a string AKI to override the AMI kernel.
82  image_id:
83    description:
84       - I(ami) ID to use for the instance. One of I(image) or I(image_id) are required when instance is not already present.
85       - This is an alias for I(image.id).
86  security_groups:
87    description:
88      - A list of security group IDs or names (strings). Mutually exclusive with I(security_group).
89  security_group:
90    description:
91      - A security group ID or name. Mutually exclusive with I(security_groups).
92  name:
93    description:
94      - The Name tag for the instance.
95  vpc_subnet_id:
96    description:
97      - The subnet ID in which to launch the instance (VPC)
98        If none is provided, ec2_instance will chose the default zone of the default VPC.
99    aliases: ['subnet_id']
100  network:
101    description:
102      - Either a dictionary containing the key 'interfaces' corresponding to a list of network interface IDs or
103        containing specifications for a single network interface.
104      - If specifications for a single network are given, accepted keys are assign_public_ip (bool),
105        private_ip_address (str), ipv6_addresses (list), source_dest_check (bool), description (str),
106        delete_on_termination (bool), device_index (int), groups (list of security group IDs),
107        private_ip_addresses (list), subnet_id (str).
108      - I(network.interfaces) should be a list of ENI IDs (strings) or a list of objects containing the key I(id).
109      - Use the ec2_eni to create ENIs with special settings.
110  volumes:
111    description:
112    - A list of block device mappings, by default this will always use the AMI root device so the volumes option is primarily for adding more storage.
113    - A mapping contains the (optional) keys device_name, virtual_name, ebs.volume_type, ebs.volume_size, ebs.kms_key_id,
114      ebs.iops, and ebs.delete_on_termination.
115    - For more information about each parameter, see U(https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_BlockDeviceMapping.html).
116  launch_template:
117    description:
118      - The EC2 launch template to base instance configuration on.
119      - I(launch_template.id) the ID or the launch template (optional if name is specified).
120      - I(launch_template.name) the pretty name of the launch template (optional if id is specified).
121      - I(launch_template.version) the specific version of the launch template to use. If unspecified, the template default is chosen.
122  key_name:
123    description:
124    - Name of the SSH access key to assign to the instance - must exist in the region the instance is created.
125  availability_zone:
126    description:
127    - Specify an availability zone to use the default subnet it. Useful if not specifying the I(vpc_subnet_id) parameter.
128    - If no subnet, ENI, or availability zone is provided, the default subnet in the default VPC will be used in the first AZ (alphabetically sorted).
129  instance_initiated_shutdown_behavior:
130    description:
131      - Whether to stop or terminate an instance upon shutdown.
132    choices: ['stop', 'terminate']
133  tenancy:
134    description:
135      - What type of tenancy to allow an instance to use. Default is shared tenancy. Dedicated tenancy will incur additional charges.
136    choices: ['dedicated', 'default']
137  termination_protection:
138    description:
139      - Whether to enable termination protection.
140        This module will not terminate an instance with termination protection active, it must be turned off first.
141    type: bool
142  cpu_credit_specification:
143    description:
144      - For T2 series instances, choose whether to allow increased charges to buy CPU credits if the default pool is depleted.
145      - Choose I(unlimited) to enable buying additional CPU credits.
146    choices: [unlimited, standard]
147  cpu_options:
148    description:
149      - Reduce the number of vCPU exposed to the instance.
150      - Those parameters can only be set at instance launch. The two suboptions threads_per_core and core_count are mandatory.
151      - See U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-optimize-cpu.html) for combinations available.
152      - Requires botocore >= 1.10.16
153    version_added: 2.7
154    suboptions:
155      threads_per_core:
156        description:
157        - Select the number of threads per core to enable. Disable or Enable Intel HT.
158        choices: [1, 2]
159        required: true
160      core_count:
161        description:
162        - Set the number of core to enable.
163        required: true
164  detailed_monitoring:
165    description:
166      - Whether to allow detailed cloudwatch metrics to be collected, enabling more detailed alerting.
167    type: bool
168  ebs_optimized:
169    description:
170      - Whether instance is should use optimized EBS volumes, see U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSOptimized.html).
171    type: bool
172  filters:
173    description:
174      - A dict of filters to apply when deciding whether existing instances match and should be altered. Each dict item
175        consists of a filter key and a filter value. See
176        U(https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html).
177        for possible filters. Filter names and values are case sensitive.
178        By default, instances are filtered for counting by their "Name" tag, base AMI, state (running, by default), and
179        subnet ID. Any queryable filter can be used. Good candidates are specific tags, SSH keys, or security groups.
180    default: {"tag:Name": "<provided-Name-attribute>", "subnet-id": "<provided-or-default subnet>"}
181  instance_role:
182    description:
183      - The ARN or name of an EC2-enabled instance role to be used. If a name is not provided in arn format
184        then the ListInstanceProfiles permission must also be granted.
185        U(https://docs.aws.amazon.com/IAM/latest/APIReference/API_ListInstanceProfiles.html) If no full ARN is provided,
186        the role with a matching name will be used from the active AWS account.
187  placement_group:
188    description:
189      - The placement group that needs to be assigned to the instance
190    version_added: 2.8
191
192extends_documentation_fragment:
193    - aws
194    - ec2
195'''
196
197EXAMPLES = '''
198# Note: These examples do not set authentication details, see the AWS Guide for details.
199
200# Terminate every running instance in a region. Use with EXTREME caution.
201- ec2_instance:
202    state: absent
203    filters:
204      instance-state-name: running
205
206# restart a particular instance by its ID
207- ec2_instance:
208    state: restarted
209    instance_ids:
210      - i-12345678
211
212# start an instance with a public IP address
213- ec2_instance:
214    name: "public-compute-instance"
215    key_name: "prod-ssh-key"
216    vpc_subnet_id: subnet-5ca1ab1e
217    instance_type: c5.large
218    security_group: default
219    network:
220      assign_public_ip: true
221    image_id: ami-123456
222    tags:
223      Environment: Testing
224
225# start an instance and Add EBS
226- ec2_instance:
227    name: "public-withebs-instance"
228    vpc_subnet_id: subnet-5ca1ab1e
229    instance_type: t2.micro
230    key_name: "prod-ssh-key"
231    security_group: default
232    volumes:
233      - device_name: /dev/sda1
234        ebs:
235          volume_size: 16
236          delete_on_termination: true
237
238# start an instance with a cpu_options
239- ec2_instance:
240    name: "public-cpuoption-instance"
241    vpc_subnet_id: subnet-5ca1ab1e
242    tags:
243      Environment: Testing
244    instance_type: c4.large
245    volumes:
246    - device_name: /dev/sda1
247      ebs:
248        delete_on_termination: true
249    cpu_options:
250        core_count: 1
251        threads_per_core: 1
252
253# start an instance and have it begin a Tower callback on boot
254- ec2_instance:
255    name: "tower-callback-test"
256    key_name: "prod-ssh-key"
257    vpc_subnet_id: subnet-5ca1ab1e
258    security_group: default
259    tower_callback:
260      # IP or hostname of tower server
261      tower_address: 1.2.3.4
262      job_template_id: 876
263      host_config_key: '[secret config key goes here]'
264    network:
265      assign_public_ip: true
266    image_id: ami-123456
267    cpu_credit_specification: unlimited
268    tags:
269      SomeThing: "A value"
270
271# start an instance with ENI (An existing ENI ID is required)
272- ec2_instance:
273    name: "public-eni-instance"
274    key_name: "prod-ssh-key"
275    vpc_subnet_id: subnet-5ca1ab1e
276    network:
277      interfaces:
278        - id: "eni-12345"
279    tags:
280      Env: "eni_on"
281    volumes:
282    - device_name: /dev/sda1
283      ebs:
284        delete_on_termination: true
285    instance_type: t2.micro
286    image_id: ami-123456
287
288# add second ENI interface
289- ec2_instance:
290    name: "public-eni-instance"
291    network:
292      interfaces:
293        - id: "eni-12345"
294        - id: "eni-67890"
295    image_id: ami-123456
296    tags:
297      Env: "eni_on"
298    instance_type: t2.micro
299'''
300
301RETURN = '''
302instances:
303    description: a list of ec2 instances
304    returned: when wait == true
305    type: complex
306    contains:
307        ami_launch_index:
308            description: The AMI launch index, which can be used to find this instance in the launch group.
309            returned: always
310            type: int
311            sample: 0
312        architecture:
313            description: The architecture of the image
314            returned: always
315            type: str
316            sample: x86_64
317        block_device_mappings:
318            description: Any block device mapping entries for the instance.
319            returned: always
320            type: complex
321            contains:
322                device_name:
323                    description: The device name exposed to the instance (for example, /dev/sdh or xvdh).
324                    returned: always
325                    type: str
326                    sample: /dev/sdh
327                ebs:
328                    description: Parameters used to automatically set up EBS volumes when the instance is launched.
329                    returned: always
330                    type: complex
331                    contains:
332                        attach_time:
333                            description: The time stamp when the attachment initiated.
334                            returned: always
335                            type: str
336                            sample: "2017-03-23T22:51:24+00:00"
337                        delete_on_termination:
338                            description: Indicates whether the volume is deleted on instance termination.
339                            returned: always
340                            type: bool
341                            sample: true
342                        status:
343                            description: The attachment state.
344                            returned: always
345                            type: str
346                            sample: attached
347                        volume_id:
348                            description: The ID of the EBS volume
349                            returned: always
350                            type: str
351                            sample: vol-12345678
352        client_token:
353            description: The idempotency token you provided when you launched the instance, if applicable.
354            returned: always
355            type: str
356            sample: mytoken
357        ebs_optimized:
358            description: Indicates whether the instance is optimized for EBS I/O.
359            returned: always
360            type: bool
361            sample: false
362        hypervisor:
363            description: The hypervisor type of the instance.
364            returned: always
365            type: str
366            sample: xen
367        iam_instance_profile:
368            description: The IAM instance profile associated with the instance, if applicable.
369            returned: always
370            type: complex
371            contains:
372                arn:
373                    description: The Amazon Resource Name (ARN) of the instance profile.
374                    returned: always
375                    type: str
376                    sample: "arn:aws:iam::000012345678:instance-profile/myprofile"
377                id:
378                    description: The ID of the instance profile
379                    returned: always
380                    type: str
381                    sample: JFJ397FDG400FG9FD1N
382        image_id:
383            description: The ID of the AMI used to launch the instance.
384            returned: always
385            type: str
386            sample: ami-0011223344
387        instance_id:
388            description: The ID of the instance.
389            returned: always
390            type: str
391            sample: i-012345678
392        instance_type:
393            description: The instance type size of the running instance.
394            returned: always
395            type: str
396            sample: t2.micro
397        key_name:
398            description: The name of the key pair, if this instance was launched with an associated key pair.
399            returned: always
400            type: str
401            sample: my-key
402        launch_time:
403            description: The time the instance was launched.
404            returned: always
405            type: str
406            sample: "2017-03-23T22:51:24+00:00"
407        monitoring:
408            description: The monitoring for the instance.
409            returned: always
410            type: complex
411            contains:
412                state:
413                    description: Indicates whether detailed monitoring is enabled. Otherwise, basic monitoring is enabled.
414                    returned: always
415                    type: str
416                    sample: disabled
417        network_interfaces:
418            description: One or more network interfaces for the instance.
419            returned: always
420            type: complex
421            contains:
422                association:
423                    description: The association information for an Elastic IPv4 associated with the network interface.
424                    returned: always
425                    type: complex
426                    contains:
427                        ip_owner_id:
428                            description: The ID of the owner of the Elastic IP address.
429                            returned: always
430                            type: str
431                            sample: amazon
432                        public_dns_name:
433                            description: The public DNS name.
434                            returned: always
435                            type: str
436                            sample: ""
437                        public_ip:
438                            description: The public IP address or Elastic IP address bound to the network interface.
439                            returned: always
440                            type: str
441                            sample: 1.2.3.4
442                attachment:
443                    description: The network interface attachment.
444                    returned: always
445                    type: complex
446                    contains:
447                        attach_time:
448                            description: The time stamp when the attachment initiated.
449                            returned: always
450                            type: str
451                            sample: "2017-03-23T22:51:24+00:00"
452                        attachment_id:
453                            description: The ID of the network interface attachment.
454                            returned: always
455                            type: str
456                            sample: eni-attach-3aff3f
457                        delete_on_termination:
458                            description: Indicates whether the network interface is deleted when the instance is terminated.
459                            returned: always
460                            type: bool
461                            sample: true
462                        device_index:
463                            description: The index of the device on the instance for the network interface attachment.
464                            returned: always
465                            type: int
466                            sample: 0
467                        status:
468                            description: The attachment state.
469                            returned: always
470                            type: str
471                            sample: attached
472                description:
473                    description: The description.
474                    returned: always
475                    type: str
476                    sample: My interface
477                groups:
478                    description: One or more security groups.
479                    returned: always
480                    type: list of complex
481                    contains:
482                        group_id:
483                            description: The ID of the security group.
484                            returned: always
485                            type: str
486                            sample: sg-abcdef12
487                        group_name:
488                            description: The name of the security group.
489                            returned: always
490                            type: str
491                            sample: mygroup
492                ipv6_addresses:
493                    description: One or more IPv6 addresses associated with the network interface.
494                    returned: always
495                    type: complex
496                    contains:
497                        - ipv6_address:
498                              description: The IPv6 address.
499                              returned: always
500                              type: str
501                              sample: "2001:0db8:85a3:0000:0000:8a2e:0370:7334"
502                mac_address:
503                    description: The MAC address.
504                    returned: always
505                    type: str
506                    sample: "00:11:22:33:44:55"
507                network_interface_id:
508                    description: The ID of the network interface.
509                    returned: always
510                    type: str
511                    sample: eni-01234567
512                owner_id:
513                    description: The AWS account ID of the owner of the network interface.
514                    returned: always
515                    type: str
516                    sample: 01234567890
517                private_ip_address:
518                    description: The IPv4 address of the network interface within the subnet.
519                    returned: always
520                    type: str
521                    sample: 10.0.0.1
522                private_ip_addresses:
523                    description: The private IPv4 addresses associated with the network interface.
524                    returned: always
525                    type: list of complex
526                    contains:
527                        association:
528                            description: The association information for an Elastic IP address (IPv4) associated with the network interface.
529                            returned: always
530                            type: complex
531                            contains:
532                                ip_owner_id:
533                                    description: The ID of the owner of the Elastic IP address.
534                                    returned: always
535                                    type: str
536                                    sample: amazon
537                                public_dns_name:
538                                    description: The public DNS name.
539                                    returned: always
540                                    type: str
541                                    sample: ""
542                                public_ip:
543                                    description: The public IP address or Elastic IP address bound to the network interface.
544                                    returned: always
545                                    type: str
546                                    sample: 1.2.3.4
547                        primary:
548                            description: Indicates whether this IPv4 address is the primary private IP address of the network interface.
549                            returned: always
550                            type: bool
551                            sample: true
552                        private_ip_address:
553                            description: The private IPv4 address of the network interface.
554                            returned: always
555                            type: str
556                            sample: 10.0.0.1
557                source_dest_check:
558                    description: Indicates whether source/destination checking is enabled.
559                    returned: always
560                    type: bool
561                    sample: true
562                status:
563                    description: The status of the network interface.
564                    returned: always
565                    type: str
566                    sample: in-use
567                subnet_id:
568                    description: The ID of the subnet for the network interface.
569                    returned: always
570                    type: str
571                    sample: subnet-0123456
572                vpc_id:
573                    description: The ID of the VPC for the network interface.
574                    returned: always
575                    type: str
576                    sample: vpc-0123456
577        placement:
578            description: The location where the instance launched, if applicable.
579            returned: always
580            type: complex
581            contains:
582                availability_zone:
583                    description: The Availability Zone of the instance.
584                    returned: always
585                    type: str
586                    sample: ap-southeast-2a
587                group_name:
588                    description: The name of the placement group the instance is in (for cluster compute instances).
589                    returned: always
590                    type: str
591                    sample: ""
592                tenancy:
593                    description: The tenancy of the instance (if the instance is running in a VPC).
594                    returned: always
595                    type: str
596                    sample: default
597        private_dns_name:
598            description: The private DNS name.
599            returned: always
600            type: str
601            sample: ip-10-0-0-1.ap-southeast-2.compute.internal
602        private_ip_address:
603            description: The IPv4 address of the network interface within the subnet.
604            returned: always
605            type: str
606            sample: 10.0.0.1
607        product_codes:
608            description: One or more product codes.
609            returned: always
610            type: list of complex
611            contains:
612                product_code_id:
613                    description: The product code.
614                    returned: always
615                    type: str
616                    sample: aw0evgkw8ef3n2498gndfgasdfsd5cce
617                product_code_type:
618                    description: The type of product code.
619                    returned: always
620                    type: str
621                    sample: marketplace
622        public_dns_name:
623            description: The public DNS name assigned to the instance.
624            returned: always
625            type: str
626            sample:
627        public_ip_address:
628            description: The public IPv4 address assigned to the instance
629            returned: always
630            type: str
631            sample: 52.0.0.1
632        root_device_name:
633            description: The device name of the root device
634            returned: always
635            type: str
636            sample: /dev/sda1
637        root_device_type:
638            description: The type of root device used by the AMI.
639            returned: always
640            type: str
641            sample: ebs
642        security_groups:
643            description: One or more security groups for the instance.
644            returned: always
645            type: list of complex
646            contains:
647                group_id:
648                    description: The ID of the security group.
649                    returned: always
650                    type: str
651                    sample: sg-0123456
652                group_name:
653                    description: The name of the security group.
654                    returned: always
655                    type: str
656                    sample: my-security-group
657        network.source_dest_check:
658            description: Indicates whether source/destination checking is enabled.
659            returned: always
660            type: bool
661            sample: true
662        state:
663            description: The current state of the instance.
664            returned: always
665            type: complex
666            contains:
667                code:
668                    description: The low byte represents the state.
669                    returned: always
670                    type: int
671                    sample: 16
672                name:
673                    description: The name of the state.
674                    returned: always
675                    type: str
676                    sample: running
677        state_transition_reason:
678            description: The reason for the most recent state transition.
679            returned: always
680            type: str
681            sample:
682        subnet_id:
683            description: The ID of the subnet in which the instance is running.
684            returned: always
685            type: str
686            sample: subnet-00abcdef
687        tags:
688            description: Any tags assigned to the instance.
689            returned: always
690            type: dict
691            sample:
692        virtualization_type:
693            description: The type of virtualization of the AMI.
694            returned: always
695            type: str
696            sample: hvm
697        vpc_id:
698            description: The ID of the VPC the instance is in.
699            returned: always
700            type: dict
701            sample: vpc-0011223344
702'''
703
704import re
705import uuid
706import string
707import textwrap
708import time
709from collections import namedtuple
710
711try:
712    import boto3
713    import botocore.exceptions
714except ImportError:
715    pass
716
717from ansible.module_utils.six import text_type, string_types
718from ansible.module_utils.six.moves.urllib import parse as urlparse
719from ansible.module_utils._text import to_bytes, to_native
720import ansible.module_utils.ec2 as ec2_utils
721from ansible.module_utils.ec2 import (boto3_conn,
722                                      ec2_argument_spec,
723                                      get_aws_connection_info,
724                                      AWSRetry,
725                                      ansible_dict_to_boto3_filter_list,
726                                      compare_aws_tags,
727                                      boto3_tag_list_to_ansible_dict,
728                                      ansible_dict_to_boto3_tag_list,
729                                      camel_dict_to_snake_dict)
730
731from ansible.module_utils.aws.core import AnsibleAWSModule
732
733module = None
734
735
736def tower_callback_script(tower_conf, windows=False, passwd=None):
737    script_url = 'https://raw.githubusercontent.com/ansible/ansible/devel/examples/scripts/ConfigureRemotingForAnsible.ps1'
738    if windows and passwd is not None:
739        script_tpl = """<powershell>
740        $admin = [adsi]("WinNT://./administrator, user")
741        $admin.PSBase.Invoke("SetPassword", "{PASS}")
742        Invoke-Expression ((New-Object System.Net.Webclient).DownloadString('{SCRIPT}'))
743        </powershell>
744        """
745        return to_native(textwrap.dedent(script_tpl).format(PASS=passwd, SCRIPT=script_url))
746    elif windows and passwd is None:
747        script_tpl = """<powershell>
748        $admin = [adsi]("WinNT://./administrator, user")
749        Invoke-Expression ((New-Object System.Net.Webclient).DownloadString('{SCRIPT}'))
750        </powershell>
751        """
752        return to_native(textwrap.dedent(script_tpl).format(PASS=passwd, SCRIPT=script_url))
753    elif not windows:
754        for p in ['tower_address', 'job_template_id', 'host_config_key']:
755            if p not in tower_conf:
756                module.fail_json(msg="Incomplete tower_callback configuration. tower_callback.{0} not set.".format(p))
757
758        if isinstance(tower_conf['job_template_id'], string_types):
759            tower_conf['job_template_id'] = urlparse.quote(tower_conf['job_template_id'])
760        tpl = string.Template(textwrap.dedent("""#!/bin/bash
761        set -x
762
763        retry_attempts=10
764        attempt=0
765        while [[ $attempt -lt $retry_attempts ]]
766        do
767          status_code=`curl --max-time 10 -v -k -s -i \
768            --data "host_config_key=${host_config_key}" \
769            'https://${tower_address}/api/v2/job_templates/${template_id}/callback/' \
770            | head -n 1 \
771            | awk '{print $2}'`
772          if [[ $status_code == 404 ]]
773            then
774            status_code=`curl --max-time 10 -v -k -s -i \
775              --data "host_config_key=${host_config_key}" \
776              'https://${tower_address}/api/v1/job_templates/${template_id}/callback/' \
777              | head -n 1 \
778              | awk '{print $2}'`
779            # fall back to using V1 API for Tower 3.1 and below, since v2 API will always 404
780          fi
781          if [[ $status_code == 201 ]]
782            then
783            exit 0
784          fi
785          attempt=$(( attempt + 1 ))
786          echo "$${status_code} received... retrying in 1 minute. (Attempt $${attempt})"
787          sleep 60
788        done
789        exit 1
790        """))
791        return tpl.safe_substitute(tower_address=tower_conf['tower_address'],
792                                   template_id=tower_conf['job_template_id'],
793                                   host_config_key=tower_conf['host_config_key'])
794    raise NotImplementedError("Only windows with remote-prep or non-windows with tower job callback supported so far.")
795
796
797@AWSRetry.jittered_backoff()
798def manage_tags(match, new_tags, purge_tags, ec2):
799    changed = False
800    old_tags = boto3_tag_list_to_ansible_dict(match['Tags'])
801    tags_to_set, tags_to_delete = compare_aws_tags(
802        old_tags, new_tags,
803        purge_tags=purge_tags,
804    )
805    if tags_to_set:
806        ec2.create_tags(
807            Resources=[match['InstanceId']],
808            Tags=ansible_dict_to_boto3_tag_list(tags_to_set))
809        changed |= True
810    if tags_to_delete:
811        delete_with_current_values = dict((k, old_tags.get(k)) for k in tags_to_delete)
812        ec2.delete_tags(
813            Resources=[match['InstanceId']],
814            Tags=ansible_dict_to_boto3_tag_list(delete_with_current_values))
815        changed |= True
816    return changed
817
818
819def build_volume_spec(params):
820    volumes = params.get('volumes') or []
821    for volume in volumes:
822        if 'ebs' in volume:
823            for int_value in ['volume_size', 'iops']:
824                if int_value in volume['ebs']:
825                    volume['ebs'][int_value] = int(volume['ebs'][int_value])
826    return [ec2_utils.snake_dict_to_camel_dict(v, capitalize_first=True) for v in volumes]
827
828
829def add_or_update_instance_profile(instance, desired_profile_name):
830    instance_profile_setting = instance.get('IamInstanceProfile')
831    if instance_profile_setting and desired_profile_name:
832        if desired_profile_name in (instance_profile_setting.get('Name'), instance_profile_setting.get('Arn')):
833            # great, the profile we asked for is what's there
834            return False
835        else:
836            desired_arn = determine_iam_role(desired_profile_name)
837            if instance_profile_setting.get('Arn') == desired_arn:
838                return False
839        # update association
840        ec2 = module.client('ec2')
841        try:
842            association = ec2.describe_iam_instance_profile_associations(Filters=[{'Name': 'instance-id', 'Values': [instance['InstanceId']]}])
843        except botocore.exceptions.ClientError as e:
844            # check for InvalidAssociationID.NotFound
845            module.fail_json_aws(e, "Could not find instance profile association")
846        try:
847            resp = ec2.replace_iam_instance_profile_association(
848                AssociationId=association['IamInstanceProfileAssociations'][0]['AssociationId'],
849                IamInstanceProfile={'Arn': determine_iam_role(desired_profile_name)}
850            )
851            return True
852        except botocore.exceptions.ClientError as e:
853            module.fail_json_aws(e, "Could not associate instance profile")
854
855    if not instance_profile_setting and desired_profile_name:
856        # create association
857        ec2 = module.client('ec2')
858        try:
859            resp = ec2.associate_iam_instance_profile(
860                IamInstanceProfile={'Arn': determine_iam_role(desired_profile_name)},
861                InstanceId=instance['InstanceId']
862            )
863            return True
864        except botocore.exceptions.ClientError as e:
865            module.fail_json_aws(e, "Could not associate new instance profile")
866
867    return False
868
869
870def build_network_spec(params, ec2=None):
871    """
872    Returns list of interfaces [complex]
873    Interface type: {
874        'AssociatePublicIpAddress': True|False,
875        'DeleteOnTermination': True|False,
876        'Description': 'string',
877        'DeviceIndex': 123,
878        'Groups': [
879            'string',
880        ],
881        'Ipv6AddressCount': 123,
882        'Ipv6Addresses': [
883            {
884                'Ipv6Address': 'string'
885            },
886        ],
887        'NetworkInterfaceId': 'string',
888        'PrivateIpAddress': 'string',
889        'PrivateIpAddresses': [
890            {
891                'Primary': True|False,
892                'PrivateIpAddress': 'string'
893            },
894        ],
895        'SecondaryPrivateIpAddressCount': 123,
896        'SubnetId': 'string'
897    },
898    """
899    if ec2 is None:
900        ec2 = module.client('ec2')
901
902    interfaces = []
903    network = params.get('network') or {}
904    if not network.get('interfaces'):
905        # they only specified one interface
906        spec = {
907            'DeviceIndex': 0,
908        }
909        if network.get('assign_public_ip') is not None:
910            spec['AssociatePublicIpAddress'] = network['assign_public_ip']
911
912        if params.get('vpc_subnet_id'):
913            spec['SubnetId'] = params['vpc_subnet_id']
914        else:
915            default_vpc = get_default_vpc(ec2)
916            if default_vpc is None:
917                raise module.fail_json(
918                    msg="No default subnet could be found - you must include a VPC subnet ID (vpc_subnet_id parameter) to create an instance")
919            else:
920                sub = get_default_subnet(ec2, default_vpc)
921                spec['SubnetId'] = sub['SubnetId']
922
923        if network.get('private_ip_address'):
924            spec['PrivateIpAddress'] = network['private_ip_address']
925
926        if params.get('security_group') or params.get('security_groups'):
927            groups = discover_security_groups(
928                group=params.get('security_group'),
929                groups=params.get('security_groups'),
930                subnet_id=spec['SubnetId'],
931                ec2=ec2
932            )
933            spec['Groups'] = [g['GroupId'] for g in groups]
934        if network.get('description') is not None:
935            spec['Description'] = network['description']
936        # TODO more special snowflake network things
937
938        return [spec]
939
940    # handle list of `network.interfaces` options
941    for idx, interface_params in enumerate(network.get('interfaces', [])):
942        spec = {
943            'DeviceIndex': idx,
944        }
945
946        if isinstance(interface_params, string_types):
947            # naive case where user gave
948            # network_interfaces: [eni-1234, eni-4567, ....]
949            # put into normal data structure so we don't dupe code
950            interface_params = {'id': interface_params}
951
952        if interface_params.get('id') is not None:
953            # if an ID is provided, we don't want to set any other parameters.
954            spec['NetworkInterfaceId'] = interface_params['id']
955            interfaces.append(spec)
956            continue
957
958        spec['DeleteOnTermination'] = interface_params.get('delete_on_termination', True)
959
960        if interface_params.get('ipv6_addresses'):
961            spec['Ipv6Addresses'] = [{'Ipv6Address': a} for a in interface_params.get('ipv6_addresses', [])]
962
963        if interface_params.get('private_ip_address'):
964            spec['PrivateIpAddress'] = interface_params.get('private_ip_address')
965
966        if interface_params.get('description'):
967            spec['Description'] = interface_params.get('description')
968
969        if interface_params.get('subnet_id', params.get('vpc_subnet_id')):
970            spec['SubnetId'] = interface_params.get('subnet_id', params.get('vpc_subnet_id'))
971        elif not spec.get('SubnetId') and not interface_params['id']:
972            # TODO grab a subnet from default VPC
973            raise ValueError('Failed to assign subnet to interface {0}'.format(interface_params))
974
975        interfaces.append(spec)
976    return interfaces
977
978
979def warn_if_public_ip_assignment_changed(instance):
980    # This is a non-modifiable attribute.
981    assign_public_ip = (module.params.get('network') or {}).get('assign_public_ip')
982    if assign_public_ip is None:
983        return
984
985    # Check that public ip assignment is the same and warn if not
986    public_dns_name = instance.get('PublicDnsName')
987    if (public_dns_name and not assign_public_ip) or (assign_public_ip and not public_dns_name):
988        module.warn(
989            "Unable to modify public ip assignment to {0} for instance {1}. "
990            "Whether or not to assign a public IP is determined during instance creation.".format(
991                assign_public_ip, instance['InstanceId']))
992
993
994def warn_if_cpu_options_changed(instance):
995    # This is a non-modifiable attribute.
996    cpu_options = module.params.get('cpu_options')
997    if cpu_options is None:
998        return
999
1000    # Check that the CpuOptions set are the same and warn if not
1001    core_count_curr = instance['CpuOptions'].get('CoreCount')
1002    core_count = cpu_options.get('core_count')
1003    threads_per_core_curr = instance['CpuOptions'].get('ThreadsPerCore')
1004    threads_per_core = cpu_options.get('threads_per_core')
1005    if core_count_curr != core_count:
1006        module.warn(
1007            "Unable to modify core_count from {0} to {1}. "
1008            "Assigning a number of core is determinted during instance creation".format(
1009                core_count_curr, core_count))
1010
1011    if threads_per_core_curr != threads_per_core:
1012        module.warn(
1013            "Unable to modify threads_per_core from {0} to {1}. "
1014            "Assigning a number of threads per core is determined during instance creation.".format(
1015                threads_per_core_curr, threads_per_core))
1016
1017
1018def discover_security_groups(group, groups, parent_vpc_id=None, subnet_id=None, ec2=None):
1019    if ec2 is None:
1020        ec2 = module.client('ec2')
1021
1022    if subnet_id is not None:
1023        try:
1024            sub = ec2.describe_subnets(SubnetIds=[subnet_id])
1025        except botocore.exceptions.ClientError as e:
1026            if e.response['Error']['Code'] == 'InvalidGroup.NotFound':
1027                module.fail_json(
1028                    "Could not find subnet {0} to associate security groups. Please check the vpc_subnet_id and security_groups parameters.".format(
1029                        subnet_id
1030                    )
1031                )
1032            module.fail_json_aws(e, msg="Error while searching for subnet {0} parent VPC.".format(subnet_id))
1033        except botocore.exceptions.BotoCoreError as e:
1034            module.fail_json_aws(e, msg="Error while searching for subnet {0} parent VPC.".format(subnet_id))
1035        parent_vpc_id = sub['Subnets'][0]['VpcId']
1036
1037    vpc = {
1038        'Name': 'vpc-id',
1039        'Values': [parent_vpc_id]
1040    }
1041
1042    # because filter lists are AND in the security groups API,
1043    # make two separate requests for groups by ID and by name
1044    id_filters = [vpc]
1045    name_filters = [vpc]
1046
1047    if group:
1048        name_filters.append(
1049            dict(
1050                Name='group-name',
1051                Values=[group]
1052            )
1053        )
1054        if group.startswith('sg-'):
1055            id_filters.append(
1056                dict(
1057                    Name='group-id',
1058                    Values=[group]
1059                )
1060            )
1061    if groups:
1062        name_filters.append(
1063            dict(
1064                Name='group-name',
1065                Values=groups
1066            )
1067        )
1068        if [g for g in groups if g.startswith('sg-')]:
1069            id_filters.append(
1070                dict(
1071                    Name='group-id',
1072                    Values=[g for g in groups if g.startswith('sg-')]
1073                )
1074            )
1075
1076    found_groups = []
1077    for f_set in (id_filters, name_filters):
1078        if len(f_set) > 1:
1079            found_groups.extend(ec2.get_paginator(
1080                'describe_security_groups'
1081            ).paginate(
1082                Filters=f_set
1083            ).search('SecurityGroups[]'))
1084    return list(dict((g['GroupId'], g) for g in found_groups).values())
1085
1086
1087def build_top_level_options(params):
1088    spec = {}
1089    if params.get('image_id'):
1090        spec['ImageId'] = params['image_id']
1091    elif isinstance(params.get('image'), dict):
1092        image = params.get('image', {})
1093        spec['ImageId'] = image.get('id')
1094        if 'ramdisk' in image:
1095            spec['RamdiskId'] = image['ramdisk']
1096        if 'kernel' in image:
1097            spec['KernelId'] = image['kernel']
1098    if not spec.get('ImageId') and not params.get('launch_template'):
1099        module.fail_json(msg="You must include an image_id or image.id parameter to create an instance, or use a launch_template.")
1100
1101    if params.get('key_name') is not None:
1102        spec['KeyName'] = params.get('key_name')
1103    if params.get('user_data') is not None:
1104        spec['UserData'] = to_native(params.get('user_data'))
1105    elif params.get('tower_callback') is not None:
1106        spec['UserData'] = tower_callback_script(
1107            tower_conf=params.get('tower_callback'),
1108            windows=params.get('tower_callback').get('windows', False),
1109            passwd=params.get('tower_callback').get('set_password'),
1110        )
1111
1112    if params.get('launch_template') is not None:
1113        spec['LaunchTemplate'] = {}
1114        if not params.get('launch_template').get('id') or params.get('launch_template').get('name'):
1115            module.fail_json(msg="Could not create instance with launch template. Either launch_template.name or launch_template.id parameters are required")
1116
1117        if params.get('launch_template').get('id') is not None:
1118            spec['LaunchTemplate']['LaunchTemplateId'] = params.get('launch_template').get('id')
1119        if params.get('launch_template').get('name') is not None:
1120            spec['LaunchTemplate']['LaunchTemplateName'] = params.get('launch_template').get('name')
1121        if params.get('launch_template').get('version') is not None:
1122            spec['LaunchTemplate']['Version'] = to_native(params.get('launch_template').get('version'))
1123
1124    if params.get('detailed_monitoring', False):
1125        spec['Monitoring'] = {'Enabled': True}
1126    if params.get('cpu_credit_specification') is not None:
1127        spec['CreditSpecification'] = {'CpuCredits': params.get('cpu_credit_specification')}
1128    if params.get('tenancy') is not None:
1129        spec['Placement'] = {'Tenancy': params.get('tenancy')}
1130    if params.get('placement_group'):
1131        spec.setdefault('Placement', {'GroupName': str(params.get('placement_group'))})
1132    if params.get('ebs_optimized') is not None:
1133        spec['EbsOptimized'] = params.get('ebs_optimized')
1134    if params.get('instance_initiated_shutdown_behavior'):
1135        spec['InstanceInitiatedShutdownBehavior'] = params.get('instance_initiated_shutdown_behavior')
1136    if params.get('termination_protection') is not None:
1137        spec['DisableApiTermination'] = params.get('termination_protection')
1138    if params.get('cpu_options') is not None:
1139        spec['CpuOptions'] = {}
1140        spec['CpuOptions']['ThreadsPerCore'] = params.get('cpu_options').get('threads_per_core')
1141        spec['CpuOptions']['CoreCount'] = params.get('cpu_options').get('core_count')
1142    return spec
1143
1144
1145def build_instance_tags(params, propagate_tags_to_volumes=True):
1146    tags = params.get('tags', {})
1147    if params.get('name') is not None:
1148        if tags is None:
1149            tags = {}
1150        tags['Name'] = params.get('name')
1151    return [
1152        {
1153            'ResourceType': 'volume',
1154            'Tags': ansible_dict_to_boto3_tag_list(tags),
1155        },
1156        {
1157            'ResourceType': 'instance',
1158            'Tags': ansible_dict_to_boto3_tag_list(tags),
1159        },
1160    ]
1161
1162
1163def build_run_instance_spec(params, ec2=None):
1164    if ec2 is None:
1165        ec2 = module.client('ec2')
1166
1167    spec = dict(
1168        ClientToken=uuid.uuid4().hex,
1169        MaxCount=1,
1170        MinCount=1,
1171    )
1172    # network parameters
1173    spec['NetworkInterfaces'] = build_network_spec(params, ec2)
1174    spec['BlockDeviceMappings'] = build_volume_spec(params)
1175    spec.update(**build_top_level_options(params))
1176    spec['TagSpecifications'] = build_instance_tags(params)
1177
1178    # IAM profile
1179    if params.get('instance_role'):
1180        spec['IamInstanceProfile'] = dict(Arn=determine_iam_role(params.get('instance_role')))
1181
1182    spec['InstanceType'] = params['instance_type']
1183    return spec
1184
1185
1186def await_instances(ids, state='OK'):
1187    if not module.params.get('wait', True):
1188        # the user asked not to wait for anything
1189        return
1190
1191    if module.check_mode:
1192        # In check mode, there is no change even if you wait.
1193        return
1194
1195    state_opts = {
1196        'OK': 'instance_status_ok',
1197        'STOPPED': 'instance_stopped',
1198        'TERMINATED': 'instance_terminated',
1199        'EXISTS': 'instance_exists',
1200        'RUNNING': 'instance_running',
1201    }
1202    if state not in state_opts:
1203        module.fail_json(msg="Cannot wait for state {0}, invalid state".format(state))
1204    waiter = module.client('ec2').get_waiter(state_opts[state])
1205    try:
1206        waiter.wait(
1207            InstanceIds=ids,
1208            WaiterConfig={
1209                'Delay': 15,
1210                'MaxAttempts': module.params.get('wait_timeout', 600) // 15,
1211            }
1212        )
1213    except botocore.exceptions.WaiterConfigError as e:
1214        module.fail_json(msg="{0}. Error waiting for instances {1} to reach state {2}".format(
1215            to_native(e), ', '.join(ids), state))
1216    except botocore.exceptions.WaiterError as e:
1217        module.warn("Instances {0} took too long to reach state {1}. {2}".format(
1218            ', '.join(ids), state, to_native(e)))
1219
1220
1221def diff_instance_and_params(instance, params, ec2=None, skip=None):
1222    """boto3 instance obj, module params"""
1223    if ec2 is None:
1224        ec2 = module.client('ec2')
1225
1226    if skip is None:
1227        skip = []
1228
1229    changes_to_apply = []
1230    id_ = instance['InstanceId']
1231
1232    ParamMapper = namedtuple('ParamMapper', ['param_key', 'instance_key', 'attribute_name', 'add_value'])
1233
1234    def value_wrapper(v):
1235        return {'Value': v}
1236
1237    param_mappings = [
1238        ParamMapper('ebs_optimized', 'EbsOptimized', 'ebsOptimized', value_wrapper),
1239        ParamMapper('termination_protection', 'DisableApiTermination', 'disableApiTermination', value_wrapper),
1240        # user data is an immutable property
1241        # ParamMapper('user_data', 'UserData', 'userData', value_wrapper),
1242    ]
1243
1244    for mapping in param_mappings:
1245        if params.get(mapping.param_key) is not None and mapping.instance_key not in skip:
1246            value = AWSRetry.jittered_backoff()(ec2.describe_instance_attribute)(Attribute=mapping.attribute_name, InstanceId=id_)
1247            if params.get(mapping.param_key) is not None and value[mapping.instance_key]['Value'] != params.get(mapping.param_key):
1248                arguments = dict(
1249                    InstanceId=instance['InstanceId'],
1250                    # Attribute=mapping.attribute_name,
1251                )
1252                arguments[mapping.instance_key] = mapping.add_value(params.get(mapping.param_key))
1253                changes_to_apply.append(arguments)
1254
1255    if (params.get('network') or {}).get('source_dest_check') is not None:
1256        # network.source_dest_check is nested, so needs to be treated separately
1257        check = bool(params.get('network').get('source_dest_check'))
1258        if instance['SourceDestCheck'] != check:
1259            changes_to_apply.append(dict(
1260                InstanceId=instance['InstanceId'],
1261                SourceDestCheck={'Value': check},
1262            ))
1263
1264    return changes_to_apply
1265
1266
1267def change_network_attachments(instance, params, ec2):
1268    if (params.get('network') or {}).get('interfaces') is not None:
1269        new_ids = []
1270        for inty in params.get('network').get('interfaces'):
1271            if isinstance(inty, dict) and 'id' in inty:
1272                new_ids.append(inty['id'])
1273            elif isinstance(inty, string_types):
1274                new_ids.append(inty)
1275        # network.interfaces can create the need to attach new interfaces
1276        old_ids = [inty['NetworkInterfaceId'] for inty in instance['NetworkInterfaces']]
1277        to_attach = set(new_ids) - set(old_ids)
1278        for eni_id in to_attach:
1279            ec2.attach_network_interface(
1280                DeviceIndex=new_ids.index(eni_id),
1281                InstanceId=instance['InstanceId'],
1282                NetworkInterfaceId=eni_id,
1283            )
1284        return bool(len(to_attach))
1285    return False
1286
1287
1288def find_instances(ec2, ids=None, filters=None):
1289    paginator = ec2.get_paginator('describe_instances')
1290    if ids:
1291        return list(paginator.paginate(
1292            InstanceIds=ids,
1293        ).search('Reservations[].Instances[]'))
1294    elif filters is None:
1295        module.fail_json(msg="No filters provided when they were required")
1296    elif filters is not None:
1297        for key in list(filters.keys()):
1298            if not key.startswith("tag:"):
1299                filters[key.replace("_", "-")] = filters.pop(key)
1300        return list(paginator.paginate(
1301            Filters=ansible_dict_to_boto3_filter_list(filters)
1302        ).search('Reservations[].Instances[]'))
1303    return []
1304
1305
1306@AWSRetry.jittered_backoff()
1307def get_default_vpc(ec2):
1308    vpcs = ec2.describe_vpcs(Filters=ansible_dict_to_boto3_filter_list({'isDefault': 'true'}))
1309    if len(vpcs.get('Vpcs', [])):
1310        return vpcs.get('Vpcs')[0]
1311    return None
1312
1313
1314@AWSRetry.jittered_backoff()
1315def get_default_subnet(ec2, vpc, availability_zone=None):
1316    subnets = ec2.describe_subnets(
1317        Filters=ansible_dict_to_boto3_filter_list({
1318            'vpc-id': vpc['VpcId'],
1319            'state': 'available',
1320            'default-for-az': 'true',
1321        })
1322    )
1323    if len(subnets.get('Subnets', [])):
1324        if availability_zone is not None:
1325            subs_by_az = dict((subnet['AvailabilityZone'], subnet) for subnet in subnets.get('Subnets'))
1326            if availability_zone in subs_by_az:
1327                return subs_by_az[availability_zone]
1328
1329        # to have a deterministic sorting order, we sort by AZ so we'll always pick the `a` subnet first
1330        # there can only be one default-for-az subnet per AZ, so the AZ key is always unique in this list
1331        by_az = sorted(subnets.get('Subnets'), key=lambda s: s['AvailabilityZone'])
1332        return by_az[0]
1333    return None
1334
1335
1336def ensure_instance_state(state, ec2=None):
1337    if ec2 is None:
1338        module.client('ec2')
1339    if state in ('running', 'started'):
1340        changed, failed, instances, failure_reason = change_instance_state(filters=module.params.get('filters'), desired_state='RUNNING')
1341
1342        if failed:
1343            module.fail_json(
1344                msg="Unable to start instances: {0}".format(failure_reason),
1345                reboot_success=list(changed),
1346                reboot_failed=failed)
1347
1348        module.exit_json(
1349            msg='Instances started',
1350            reboot_success=list(changed),
1351            changed=bool(len(changed)),
1352            reboot_failed=[],
1353            instances=[pretty_instance(i) for i in instances],
1354        )
1355    elif state in ('restarted', 'rebooted'):
1356        changed, failed, instances, failure_reason = change_instance_state(
1357            filters=module.params.get('filters'),
1358            desired_state='STOPPED')
1359        changed, failed, instances, failure_reason = change_instance_state(
1360            filters=module.params.get('filters'),
1361            desired_state='RUNNING')
1362
1363        if failed:
1364            module.fail_json(
1365                msg="Unable to restart instances: {0}".format(failure_reason),
1366                reboot_success=list(changed),
1367                reboot_failed=failed)
1368
1369        module.exit_json(
1370            msg='Instances restarted',
1371            reboot_success=list(changed),
1372            changed=bool(len(changed)),
1373            reboot_failed=[],
1374            instances=[pretty_instance(i) for i in instances],
1375        )
1376    elif state in ('stopped',):
1377        changed, failed, instances, failure_reason = change_instance_state(
1378            filters=module.params.get('filters'),
1379            desired_state='STOPPED')
1380
1381        if failed:
1382            module.fail_json(
1383                msg="Unable to stop instances: {0}".format(failure_reason),
1384                stop_success=list(changed),
1385                stop_failed=failed)
1386
1387        module.exit_json(
1388            msg='Instances stopped',
1389            stop_success=list(changed),
1390            changed=bool(len(changed)),
1391            stop_failed=[],
1392            instances=[pretty_instance(i) for i in instances],
1393        )
1394    elif state in ('absent', 'terminated'):
1395        terminated, terminate_failed, instances, failure_reason = change_instance_state(
1396            filters=module.params.get('filters'),
1397            desired_state='TERMINATED')
1398
1399        if terminate_failed:
1400            module.fail_json(
1401                msg="Unable to terminate instances: {0}".format(failure_reason),
1402                terminate_success=list(terminated),
1403                terminate_failed=terminate_failed)
1404        module.exit_json(
1405            msg='Instances terminated',
1406            terminate_success=list(terminated),
1407            changed=bool(len(terminated)),
1408            terminate_failed=[],
1409            instances=[pretty_instance(i) for i in instances],
1410        )
1411
1412
1413@AWSRetry.jittered_backoff()
1414def change_instance_state(filters, desired_state, ec2=None):
1415    """Takes STOPPED/RUNNING/TERMINATED"""
1416    if ec2 is None:
1417        ec2 = module.client('ec2')
1418
1419    changed = set()
1420    instances = find_instances(ec2, filters=filters)
1421    to_change = set(i['InstanceId'] for i in instances if i['State']['Name'].upper() != desired_state)
1422    unchanged = set()
1423    failure_reason = ""
1424
1425    for inst in instances:
1426        try:
1427            if desired_state == 'TERMINATED':
1428                if module.check_mode:
1429                    changed.add(inst['InstanceId'])
1430                    continue
1431
1432                # TODO use a client-token to prevent double-sends of these start/stop/terminate commands
1433                # https://docs.aws.amazon.com/AWSEC2/latest/APIReference/Run_Instance_Idempotency.html
1434                resp = ec2.terminate_instances(InstanceIds=[inst['InstanceId']])
1435                [changed.add(i['InstanceId']) for i in resp['TerminatingInstances']]
1436            if desired_state == 'STOPPED':
1437                if inst['State']['Name'] in ('stopping', 'stopped'):
1438                    unchanged.add(inst['InstanceId'])
1439                    continue
1440
1441                if module.check_mode:
1442                    changed.add(inst['InstanceId'])
1443                    continue
1444
1445                resp = ec2.stop_instances(InstanceIds=[inst['InstanceId']])
1446                [changed.add(i['InstanceId']) for i in resp['StoppingInstances']]
1447            if desired_state == 'RUNNING':
1448                if module.check_mode:
1449                    changed.add(inst['InstanceId'])
1450                    continue
1451
1452                resp = ec2.start_instances(InstanceIds=[inst['InstanceId']])
1453                [changed.add(i['InstanceId']) for i in resp['StartingInstances']]
1454        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
1455            try:
1456                failure_reason = to_native(e.message)
1457            except AttributeError:
1458                failure_reason = to_native(e)
1459
1460    if changed:
1461        await_instances(ids=list(changed) + list(unchanged), state=desired_state)
1462
1463    change_failed = list(to_change - changed)
1464    instances = find_instances(ec2, ids=list(i['InstanceId'] for i in instances))
1465    return changed, change_failed, instances, failure_reason
1466
1467
1468def pretty_instance(i):
1469    instance = camel_dict_to_snake_dict(i, ignore_list=['Tags'])
1470    instance['tags'] = boto3_tag_list_to_ansible_dict(i['Tags'])
1471    return instance
1472
1473
1474def determine_iam_role(name_or_arn):
1475    if re.match(r'^arn:aws:iam::\d+:instance-profile/[\w+=/,.@-]+$', name_or_arn):
1476        return name_or_arn
1477    iam = module.client('iam', retry_decorator=AWSRetry.jittered_backoff())
1478    try:
1479        role = iam.get_instance_profile(InstanceProfileName=name_or_arn, aws_retry=True)
1480        return role['InstanceProfile']['Arn']
1481    except botocore.exceptions.ClientError as e:
1482        if e.response['Error']['Code'] == 'NoSuchEntity':
1483            module.fail_json_aws(e, msg="Could not find instance_role {0}".format(name_or_arn))
1484        module.fail_json_aws(e, msg="An error occurred while searching for instance_role {0}. Please try supplying the full ARN.".format(name_or_arn))
1485
1486
1487def handle_existing(existing_matches, changed, ec2, state):
1488    if state in ('running', 'started') and [i for i in existing_matches if i['State']['Name'] != 'running']:
1489        ins_changed, failed, instances, failure_reason = change_instance_state(filters=module.params.get('filters'), desired_state='RUNNING')
1490        if failed:
1491            module.fail_json(msg="Couldn't start instances: {0}. Failure reason: {1}".format(instances, failure_reason))
1492        module.exit_json(
1493            changed=bool(len(ins_changed)) or changed,
1494            instances=[pretty_instance(i) for i in instances],
1495            instance_ids=[i['InstanceId'] for i in instances],
1496        )
1497    changes = diff_instance_and_params(existing_matches[0], module.params)
1498    for c in changes:
1499        AWSRetry.jittered_backoff()(ec2.modify_instance_attribute)(**c)
1500    changed |= bool(changes)
1501    changed |= add_or_update_instance_profile(existing_matches[0], module.params.get('instance_role'))
1502    changed |= change_network_attachments(existing_matches[0], module.params, ec2)
1503    altered = find_instances(ec2, ids=[i['InstanceId'] for i in existing_matches])
1504    module.exit_json(
1505        changed=bool(len(changes)) or changed,
1506        instances=[pretty_instance(i) for i in altered],
1507        instance_ids=[i['InstanceId'] for i in altered],
1508        changes=changes,
1509    )
1510
1511
1512def ensure_present(existing_matches, changed, ec2, state):
1513    if len(existing_matches):
1514        try:
1515            handle_existing(existing_matches, changed, ec2, state)
1516        except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
1517            module.fail_json_aws(
1518                e, msg="Failed to handle existing instances {0}".format(', '.join([i['InstanceId'] for i in existing_matches])),
1519                # instances=[pretty_instance(i) for i in existing_matches],
1520                # instance_ids=[i['InstanceId'] for i in existing_matches],
1521            )
1522    try:
1523        instance_spec = build_run_instance_spec(module.params)
1524        # If check mode is enabled,suspend 'ensure function'.
1525        if module.check_mode:
1526            module.exit_json(
1527                changed=True,
1528                spec=instance_spec,
1529            )
1530        instance_response = run_instances(ec2, **instance_spec)
1531        instances = instance_response['Instances']
1532        instance_ids = [i['InstanceId'] for i in instances]
1533
1534        for ins in instances:
1535            changes = diff_instance_and_params(ins, module.params, skip=['UserData', 'EbsOptimized'])
1536            for c in changes:
1537                try:
1538                    AWSRetry.jittered_backoff()(ec2.modify_instance_attribute)(**c)
1539                except botocore.exceptions.ClientError as e:
1540                    module.fail_json_aws(e, msg="Could not apply change {0} to new instance.".format(str(c)))
1541
1542        if not module.params.get('wait'):
1543            module.exit_json(
1544                changed=True,
1545                instance_ids=instance_ids,
1546                spec=instance_spec,
1547            )
1548        await_instances(instance_ids)
1549        instances = ec2.get_paginator('describe_instances').paginate(
1550            InstanceIds=instance_ids
1551        ).search('Reservations[].Instances[]')
1552
1553        module.exit_json(
1554            changed=True,
1555            instances=[pretty_instance(i) for i in instances],
1556            instance_ids=instance_ids,
1557            spec=instance_spec,
1558        )
1559    except (botocore.exceptions.BotoCoreError, botocore.exceptions.ClientError) as e:
1560        module.fail_json_aws(e, msg="Failed to create new EC2 instance")
1561
1562
1563@AWSRetry.jittered_backoff()
1564def run_instances(ec2, **instance_spec):
1565    try:
1566        return ec2.run_instances(**instance_spec)
1567    except botocore.exceptions.ClientError as e:
1568        if e.response['Error']['Code'] == 'InvalidParameterValue' and "Invalid IAM Instance Profile ARN" in e.response['Error']['Message']:
1569            # If the instance profile has just been created, it takes some time to be visible by ec2
1570            # So we wait 10 second and retry the run_instances
1571            time.sleep(10)
1572            return ec2.run_instances(**instance_spec)
1573        else:
1574            raise e
1575
1576
1577def main():
1578    global module
1579    argument_spec = ec2_argument_spec()
1580    argument_spec.update(dict(
1581        state=dict(default='present', choices=['present', 'started', 'running', 'stopped', 'restarted', 'rebooted', 'terminated', 'absent']),
1582        wait=dict(default=True, type='bool'),
1583        wait_timeout=dict(default=600, type='int'),
1584        # count=dict(default=1, type='int'),
1585        image=dict(type='dict'),
1586        image_id=dict(type='str'),
1587        instance_type=dict(default='t2.micro', type='str'),
1588        user_data=dict(type='str'),
1589        tower_callback=dict(type='dict'),
1590        ebs_optimized=dict(type='bool'),
1591        vpc_subnet_id=dict(type='str', aliases=['subnet_id']),
1592        availability_zone=dict(type='str'),
1593        security_groups=dict(default=[], type='list'),
1594        security_group=dict(type='str'),
1595        instance_role=dict(type='str'),
1596        name=dict(type='str'),
1597        tags=dict(type='dict'),
1598        purge_tags=dict(type='bool', default=False),
1599        filters=dict(type='dict', default=None),
1600        launch_template=dict(type='dict'),
1601        key_name=dict(type='str'),
1602        cpu_credit_specification=dict(type='str', choices=['standard', 'unlimited']),
1603        cpu_options=dict(type='dict', options=dict(
1604            core_count=dict(type='int', required=True),
1605            threads_per_core=dict(type='int', choices=[1, 2], required=True)
1606        )),
1607        tenancy=dict(type='str', choices=['dedicated', 'default']),
1608        placement_group=dict(type='str'),
1609        instance_initiated_shutdown_behavior=dict(type='str', choices=['stop', 'terminate']),
1610        termination_protection=dict(type='bool'),
1611        detailed_monitoring=dict(type='bool'),
1612        instance_ids=dict(default=[], type='list'),
1613        network=dict(default=None, type='dict'),
1614        volumes=dict(default=None, type='list'),
1615    ))
1616    # running/present are synonyms
1617    # as are terminated/absent
1618    module = AnsibleAWSModule(
1619        argument_spec=argument_spec,
1620        mutually_exclusive=[
1621            ['security_groups', 'security_group'],
1622            ['availability_zone', 'vpc_subnet_id'],
1623            ['tower_callback', 'user_data'],
1624            ['image_id', 'image'],
1625        ],
1626        supports_check_mode=True
1627    )
1628
1629    if module.params.get('network'):
1630        if module.params.get('network').get('interfaces'):
1631            if module.params.get('security_group'):
1632                module.fail_json(msg="Parameter network.interfaces can't be used with security_group")
1633            if module.params.get('security_groups'):
1634                module.fail_json(msg="Parameter network.interfaces can't be used with security_groups")
1635
1636    state = module.params.get('state')
1637    ec2 = module.client('ec2')
1638    if module.params.get('filters') is None:
1639        filters = {
1640            # all states except shutting-down and terminated
1641            'instance-state-name': ['pending', 'running', 'stopping', 'stopped']
1642        }
1643        if state == 'stopped':
1644            # only need to change instances that aren't already stopped
1645            filters['instance-state-name'] = ['stopping', 'pending', 'running']
1646
1647        if isinstance(module.params.get('instance_ids'), string_types):
1648            filters['instance-id'] = [module.params.get('instance_ids')]
1649        elif isinstance(module.params.get('instance_ids'), list) and len(module.params.get('instance_ids')):
1650            filters['instance-id'] = module.params.get('instance_ids')
1651        else:
1652            if not module.params.get('vpc_subnet_id'):
1653                if module.params.get('network'):
1654                    # grab AZ from one of the ENIs
1655                    ints = module.params.get('network').get('interfaces')
1656                    if ints:
1657                        filters['network-interface.network-interface-id'] = []
1658                        for i in ints:
1659                            if isinstance(i, dict):
1660                                i = i['id']
1661                            filters['network-interface.network-interface-id'].append(i)
1662                else:
1663                    sub = get_default_subnet(ec2, get_default_vpc(ec2), availability_zone=module.params.get('availability_zone'))
1664                    filters['subnet-id'] = sub['SubnetId']
1665            else:
1666                filters['subnet-id'] = [module.params.get('vpc_subnet_id')]
1667
1668            if module.params.get('name'):
1669                filters['tag:Name'] = [module.params.get('name')]
1670
1671            if module.params.get('image_id'):
1672                filters['image-id'] = [module.params.get('image_id')]
1673            elif (module.params.get('image') or {}).get('id'):
1674                filters['image-id'] = [module.params.get('image', {}).get('id')]
1675
1676        module.params['filters'] = filters
1677
1678    if module.params.get('cpu_options') and not module.botocore_at_least('1.10.16'):
1679        module.fail_json(msg="cpu_options is only supported with botocore >= 1.10.16")
1680
1681    existing_matches = find_instances(ec2, filters=module.params.get('filters'))
1682    changed = False
1683
1684    if state not in ('terminated', 'absent') and existing_matches:
1685        for match in existing_matches:
1686            warn_if_public_ip_assignment_changed(match)
1687            warn_if_cpu_options_changed(match)
1688            tags = module.params.get('tags') or {}
1689            name = module.params.get('name')
1690            if name:
1691                tags['Name'] = name
1692            changed |= manage_tags(match, tags, module.params.get('purge_tags', False), ec2)
1693
1694    if state in ('present', 'running', 'started'):
1695        ensure_present(existing_matches=existing_matches, changed=changed, ec2=ec2, state=state)
1696    elif state in ('restarted', 'rebooted', 'stopped', 'absent', 'terminated'):
1697        if existing_matches:
1698            ensure_instance_state(state, ec2)
1699        else:
1700            module.exit_json(
1701                msg='No matching instances found',
1702                changed=False,
1703                instances=[],
1704            )
1705    else:
1706        module.fail_json(msg="We don't handle the state {0}".format(state))
1707
1708
1709if __name__ == '__main__':
1710    main()
1711