1#!/usr/local/bin/python3.8
2# This file is part of Ansible
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
8ANSIBLE_METADATA = {'metadata_version': '1.1',
9                    'status': ['stableinterface'],
10                    'supported_by': 'core'}
11
12
13DOCUMENTATION = '''
14---
15module: ec2
16short_description: create, terminate, start or stop an instance in ec2
17description:
18    - Creates or terminates ec2 instances.
19    - >
20      Note: This module uses the older boto Python module to interact with the EC2 API.
21      M(ec2) will still receive bug fixes, but no new features.
22      Consider using the M(ec2_instance) module instead.
23      If M(ec2_instance) does not support a feature you need that is available in M(ec2), please
24      file a feature request.
25version_added: "0.9"
26options:
27  key_name:
28    description:
29      - Key pair to use on the instance.
30      - The SSH key must already exist in AWS in order to use this argument.
31      - Keys can be created / deleted using the M(ec2_key) module.
32    aliases: ['keypair']
33    type: str
34  id:
35    version_added: "1.1"
36    description:
37      - Identifier for this instance or set of instances, so that the module will be idempotent with respect to EC2 instances.
38      - 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.
39      - For details, see the description of client token at U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Run_Instance_Idempotency.html).
40    type: str
41  group:
42    description:
43      - Security group (or list of groups) to use with the instance.
44    aliases: [ 'groups' ]
45    type: list
46    elements: str
47  group_id:
48    version_added: "1.1"
49    description:
50      - Security group id (or list of ids) to use with the instance.
51    type: list
52    elements: str
53  zone:
54    version_added: "1.2"
55    description:
56      - AWS availability zone in which to launch the instance.
57    aliases: [ 'aws_zone', 'ec2_zone' ]
58    type: str
59  instance_type:
60    description:
61      - Instance type to use for the instance, see U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-types.html).
62      - Required when creating a new instance.
63    type: str
64    aliases: ['type']
65  tenancy:
66    version_added: "1.9"
67    description:
68      - An instance with a tenancy of C(dedicated) runs on single-tenant hardware and can only be launched into a VPC.
69      - Note that to use dedicated tenancy you MUST specify a I(vpc_subnet_id) as well.
70      - Dedicated tenancy is not available for EC2 "micro" instances.
71    default: default
72    choices: [ "default", "dedicated" ]
73    type: str
74  spot_price:
75    version_added: "1.5"
76    description:
77      - Maximum spot price to bid. If not set, a regular on-demand instance is requested.
78      - A spot request is made with this maximum bid. When it is filled, the instance is started.
79    type: str
80  spot_type:
81    version_added: "2.0"
82    description:
83      - The type of spot request.
84      - After being interrupted a C(persistent) spot instance will be started once there is capacity to fill the request again.
85    default: "one-time"
86    choices: [ "one-time", "persistent" ]
87    type: str
88  image:
89    description:
90       - I(ami) ID to use for the instance.
91       - Required when I(state=present).
92    type: str
93  kernel:
94    description:
95      - Kernel eki to use for the instance.
96    type: str
97  ramdisk:
98    description:
99      - Ramdisk eri to use for the instance.
100    type: str
101  wait:
102    description:
103      - Wait for the instance to reach its desired state before returning.
104      - Does not wait for SSH, see the 'wait_for_connection' example for details.
105    type: bool
106    default: false
107  wait_timeout:
108    description:
109      - How long before wait gives up, in seconds.
110    default: 300
111    type: int
112  spot_wait_timeout:
113    version_added: "1.5"
114    description:
115      - How long to wait for the spot instance request to be fulfilled. Affects 'Request valid until' for setting spot request lifespan.
116    default: 600
117    type: int
118  count:
119    description:
120      - Number of instances to launch.
121    default: 1
122    type: int
123  monitoring:
124    version_added: "1.1"
125    description:
126      - Enable detailed monitoring (CloudWatch) for instance.
127    type: bool
128    default: false
129  user_data:
130    version_added: "0.9"
131    description:
132      - Opaque blob of data which is made available to the EC2 instance.
133    type: str
134  instance_tags:
135    version_added: "1.0"
136    description:
137      - 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"}'.
138    type: dict
139  placement_group:
140    version_added: "1.3"
141    description:
142      - Placement group for the instance when using EC2 Clustered Compute.
143    type: str
144  vpc_subnet_id:
145    version_added: "1.1"
146    description:
147      - the subnet ID in which to launch the instance (VPC).
148    type: str
149  assign_public_ip:
150    version_added: "1.5"
151    description:
152      - When provisioning within vpc, assign a public IP address. Boto library must be 2.13.0+.
153    type: bool
154  private_ip:
155    version_added: "1.2"
156    description:
157      - The private ip address to assign the instance (from the vpc subnet).
158    type: str
159  instance_profile_name:
160    version_added: "1.3"
161    description:
162      - 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+.
163    type: str
164  instance_ids:
165    version_added: "1.3"
166    description:
167      - "list of instance ids, currently used for states: absent, running, stopped"
168    aliases: ['instance_id']
169    type: list
170    elements: str
171  source_dest_check:
172    version_added: "1.6"
173    description:
174      - Enable or Disable the Source/Destination checks (for NAT instances and Virtual Routers).
175        When initially creating an instance the EC2 API defaults this to C(True).
176    type: bool
177  termination_protection:
178    version_added: "2.0"
179    description:
180      - Enable or Disable the Termination Protection.
181    type: bool
182    default: false
183  instance_initiated_shutdown_behavior:
184    version_added: "2.2"
185    description:
186    - Set whether AWS will Stop or Terminate an instance on shutdown. This parameter is ignored when using instance-store.
187      images (which require termination on shutdown).
188    default: 'stop'
189    choices: [ "stop", "terminate" ]
190    type: str
191  state:
192    version_added: "1.3"
193    description:
194      - Create, terminate, start, stop or restart instances. The state 'restarted' was added in Ansible 2.2.
195      - When I(state=absent), I(instance_ids) is required.
196      - When I(state=running), I(state=stopped) or I(state=restarted) then either I(instance_ids) or I(instance_tags) is required.
197    default: 'present'
198    choices: ['absent', 'present', 'restarted', 'running', 'stopped']
199    type: str
200  volumes:
201    version_added: "1.5"
202    description:
203      - A list of hash/dictionaries of volumes to add to the new instance.
204    type: list
205    elements: dict
206    suboptions:
207      device_name:
208        type: str
209        required: true
210        description:
211          - A name for the device (For example C(/dev/sda)).
212      delete_on_termination:
213        type: bool
214        default: false
215        description:
216          - Whether the volume should be automatically deleted when the instance is terminated.
217      ephemeral:
218        type: str
219        description:
220          - Whether the volume should be ephemeral.
221          - Data on ephemeral volumes is lost when the instance is stopped.
222          - Mutually exclusive with the I(snapshot) parameter.
223      encrypted:
224        type: bool
225        default: false
226        description:
227          - Whether the volume should be encrypted using the 'aws/ebs' KMS CMK.
228      snapshot:
229        type: str
230        description:
231          - The ID of an EBS snapshot to copy when creating the volume.
232          - Mutually exclusive with the I(ephemeral) parameter.
233      volume_type:
234        type: str
235        description:
236          - The type of volume to create.
237          - See U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html) for more information on the available volume types.
238      volume_size:
239        type: int
240        description:
241          - The size of the volume (in GiB).
242      iops:
243        type: int
244        description:
245          - The number of IOPS per second to provision for the volume.
246          - Required when I(volume_type=io1).
247  ebs_optimized:
248    version_added: "1.6"
249    description:
250      - Whether instance is using optimized EBS volumes, see U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSOptimized.html).
251    default: false
252    type: bool
253  exact_count:
254    version_added: "1.5"
255    description:
256      - An integer value which indicates how many instances that match the 'count_tag' parameter should be running.
257        Instances are either created or terminated based on this value.
258    type: int
259  count_tag:
260    version_added: "1.5"
261    description:
262      - Used with I(exact_count) to determine how many nodes based on a specific tag criteria should be running.
263        This can be expressed in multiple ways and is shown in the EXAMPLES section.  For instance, one can request 25 servers
264        that are tagged with "class=webserver". The specified tag must already exist or be passed in as the I(instance_tags) option.
265    type: raw
266  network_interfaces:
267    version_added: "2.0"
268    description:
269      - A list of existing network interfaces to attach to the instance at launch. When specifying existing network interfaces,
270        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
271        for creating a new network interface at launch.)
272    aliases: ['network_interface']
273    type: list
274    elements: str
275  spot_launch_group:
276    version_added: "2.1"
277    description:
278      - Launch group for spot requests, see U(https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/how-spot-instances-work.html#spot-launch-group).
279    type: str
280author:
281    - "Tim Gerla (@tgerla)"
282    - "Lester Wade (@lwade)"
283    - "Seth Vidal (@skvidal)"
284extends_documentation_fragment:
285    - aws
286    - ec2
287'''
288
289EXAMPLES = '''
290# Note: These examples do not set authentication details, see the AWS Guide for details.
291
292# Basic provisioning example
293- ec2:
294    key_name: mykey
295    instance_type: t2.micro
296    image: ami-123456
297    wait: yes
298    group: webserver
299    count: 3
300    vpc_subnet_id: subnet-29e63245
301    assign_public_ip: yes
302
303# Advanced example with tagging and CloudWatch
304- ec2:
305    key_name: mykey
306    group: databases
307    instance_type: t2.micro
308    image: ami-123456
309    wait: yes
310    wait_timeout: 500
311    count: 5
312    instance_tags:
313       db: postgres
314    monitoring: yes
315    vpc_subnet_id: subnet-29e63245
316    assign_public_ip: yes
317
318# Single instance with additional IOPS volume from snapshot and volume delete on termination
319- ec2:
320    key_name: mykey
321    group: webserver
322    instance_type: c3.medium
323    image: ami-123456
324    wait: yes
325    wait_timeout: 500
326    volumes:
327      - device_name: /dev/sdb
328        snapshot: snap-abcdef12
329        volume_type: io1
330        iops: 1000
331        volume_size: 100
332        delete_on_termination: true
333    monitoring: yes
334    vpc_subnet_id: subnet-29e63245
335    assign_public_ip: yes
336
337# Single instance with ssd gp2 root volume
338- ec2:
339    key_name: mykey
340    group: webserver
341    instance_type: c3.medium
342    image: ami-123456
343    wait: yes
344    wait_timeout: 500
345    volumes:
346      - device_name: /dev/xvda
347        volume_type: gp2
348        volume_size: 8
349    vpc_subnet_id: subnet-29e63245
350    assign_public_ip: yes
351    count_tag:
352      Name: dbserver
353    exact_count: 1
354
355# Multiple groups example
356- ec2:
357    key_name: mykey
358    group: ['databases', 'internal-services', 'sshable', 'and-so-forth']
359    instance_type: m1.large
360    image: ami-6e649707
361    wait: yes
362    wait_timeout: 500
363    count: 5
364    instance_tags:
365        db: postgres
366    monitoring: yes
367    vpc_subnet_id: subnet-29e63245
368    assign_public_ip: yes
369
370# Multiple instances with additional volume from snapshot
371- ec2:
372    key_name: mykey
373    group: webserver
374    instance_type: m1.large
375    image: ami-6e649707
376    wait: yes
377    wait_timeout: 500
378    count: 5
379    volumes:
380    - device_name: /dev/sdb
381      snapshot: snap-abcdef12
382      volume_size: 10
383    monitoring: yes
384    vpc_subnet_id: subnet-29e63245
385    assign_public_ip: yes
386
387# Dedicated tenancy example
388- local_action:
389    module: ec2
390    assign_public_ip: yes
391    group_id: sg-1dc53f72
392    key_name: mykey
393    image: ami-6e649707
394    instance_type: m1.small
395    tenancy: dedicated
396    vpc_subnet_id: subnet-29e63245
397    wait: yes
398
399# Spot instance example
400- ec2:
401    spot_price: 0.24
402    spot_wait_timeout: 600
403    keypair: mykey
404    group_id: sg-1dc53f72
405    instance_type: m1.small
406    image: ami-6e649707
407    wait: yes
408    vpc_subnet_id: subnet-29e63245
409    assign_public_ip: yes
410    spot_launch_group: report_generators
411    instance_initiated_shutdown_behavior: terminate
412
413# Examples using pre-existing network interfaces
414- ec2:
415    key_name: mykey
416    instance_type: t2.small
417    image: ami-f005ba11
418    network_interface: eni-deadbeef
419
420- ec2:
421    key_name: mykey
422    instance_type: t2.small
423    image: ami-f005ba11
424    network_interfaces: ['eni-deadbeef', 'eni-5ca1ab1e']
425
426# Launch instances, runs some tasks
427# and then terminate them
428
429- name: Create a sandbox instance
430  hosts: localhost
431  gather_facts: False
432  vars:
433    keypair: my_keypair
434    instance_type: m1.small
435    security_group: my_securitygroup
436    image: my_ami_id
437    region: us-east-1
438  tasks:
439    - name: Launch instance
440      ec2:
441         key_name: "{{ keypair }}"
442         group: "{{ security_group }}"
443         instance_type: "{{ instance_type }}"
444         image: "{{ image }}"
445         wait: true
446         region: "{{ region }}"
447         vpc_subnet_id: subnet-29e63245
448         assign_public_ip: yes
449      register: ec2
450
451    - name: Add new instance to host group
452      add_host:
453        hostname: "{{ item.public_ip }}"
454        groupname: launched
455      loop: "{{ ec2.instances }}"
456
457    - name: Wait for SSH to come up
458      delegate_to: "{{ item.public_dns_name }}"
459      wait_for_connection:
460        delay: 60
461        timeout: 320
462      loop: "{{ ec2.instances }}"
463
464- name: Configure instance(s)
465  hosts: launched
466  become: True
467  gather_facts: True
468  roles:
469    - my_awesome_role
470    - my_awesome_test
471
472- name: Terminate instances
473  hosts: localhost
474  tasks:
475    - name: Terminate instances that were previously launched
476      ec2:
477        state: 'absent'
478        instance_ids: '{{ ec2.instance_ids }}'
479
480# Start a few existing instances, run some tasks
481# and stop the instances
482
483- name: Start sandbox instances
484  hosts: localhost
485  gather_facts: false
486  vars:
487    instance_ids:
488      - 'i-xxxxxx'
489      - 'i-xxxxxx'
490      - 'i-xxxxxx'
491    region: us-east-1
492  tasks:
493    - name: Start the sandbox instances
494      ec2:
495        instance_ids: '{{ instance_ids }}'
496        region: '{{ region }}'
497        state: running
498        wait: True
499        vpc_subnet_id: subnet-29e63245
500        assign_public_ip: yes
501  roles:
502    - do_neat_stuff
503    - do_more_neat_stuff
504
505- name: Stop sandbox instances
506  hosts: localhost
507  gather_facts: false
508  vars:
509    instance_ids:
510      - 'i-xxxxxx'
511      - 'i-xxxxxx'
512      - 'i-xxxxxx'
513    region: us-east-1
514  tasks:
515    - name: Stop the sandbox instances
516      ec2:
517        instance_ids: '{{ instance_ids }}'
518        region: '{{ region }}'
519        state: stopped
520        wait: True
521        vpc_subnet_id: subnet-29e63245
522        assign_public_ip: yes
523
524#
525# Start stopped instances specified by tag
526#
527- local_action:
528    module: ec2
529    instance_tags:
530        Name: ExtraPower
531    state: running
532
533#
534# Restart instances specified by tag
535#
536- local_action:
537    module: ec2
538    instance_tags:
539        Name: ExtraPower
540    state: restarted
541
542#
543# Enforce that 5 instances with a tag "foo" are running
544# (Highly recommended!)
545#
546
547- ec2:
548    key_name: mykey
549    instance_type: c1.medium
550    image: ami-40603AD1
551    wait: yes
552    group: webserver
553    instance_tags:
554        foo: bar
555    exact_count: 5
556    count_tag: foo
557    vpc_subnet_id: subnet-29e63245
558    assign_public_ip: yes
559
560#
561# Enforce that 5 running instances named "database" with a "dbtype" of "postgres"
562#
563
564- ec2:
565    key_name: mykey
566    instance_type: c1.medium
567    image: ami-40603AD1
568    wait: yes
569    group: webserver
570    instance_tags:
571        Name: database
572        dbtype: postgres
573    exact_count: 5
574    count_tag:
575        Name: database
576        dbtype: postgres
577    vpc_subnet_id: subnet-29e63245
578    assign_public_ip: yes
579
580#
581# count_tag complex argument examples
582#
583
584    # instances with tag foo
585- ec2:
586    count_tag:
587        foo:
588
589    # instances with tag foo=bar
590- ec2:
591    count_tag:
592        foo: bar
593
594    # instances with tags foo=bar & baz
595- ec2:
596    count_tag:
597        foo: bar
598        baz:
599
600    # instances with tags foo & bar & baz=bang
601- ec2:
602    count_tag:
603        - foo
604        - bar
605        - baz: bang
606
607'''
608
609import time
610import datetime
611import traceback
612from ast import literal_eval
613from distutils.version import LooseVersion
614
615from ansible.module_utils.basic import AnsibleModule
616from ansible.module_utils.ec2 import get_aws_connection_info, ec2_argument_spec, ec2_connect
617from ansible.module_utils.six import get_function_code, string_types
618from ansible.module_utils._text import to_bytes, to_text
619
620try:
621    import boto.ec2
622    from boto.ec2.blockdevicemapping import BlockDeviceType, BlockDeviceMapping
623    from boto.exception import EC2ResponseError
624    from boto import connect_ec2_endpoint
625    from boto import connect_vpc
626    HAS_BOTO = True
627except ImportError:
628    HAS_BOTO = False
629
630
631def find_running_instances_by_count_tag(module, ec2, vpc, count_tag, zone=None):
632
633    # get reservations for instances that match tag(s) and are in the desired state
634    state = module.params.get('state')
635    if state not in ['running', 'stopped']:
636        state = None
637    reservations = get_reservations(module, ec2, vpc, tags=count_tag, state=state, zone=zone)
638
639    instances = []
640    for res in reservations:
641        if hasattr(res, 'instances'):
642            for inst in res.instances:
643                if inst.state == 'terminated' or inst.state == 'shutting-down':
644                    continue
645                instances.append(inst)
646
647    return reservations, instances
648
649
650def _set_none_to_blank(dictionary):
651    result = dictionary
652    for k in result:
653        if isinstance(result[k], dict):
654            result[k] = _set_none_to_blank(result[k])
655        elif not result[k]:
656            result[k] = ""
657    return result
658
659
660def get_reservations(module, ec2, vpc, tags=None, state=None, zone=None):
661    # TODO: filters do not work with tags that have underscores
662    filters = dict()
663
664    vpc_subnet_id = module.params.get('vpc_subnet_id')
665    vpc_id = None
666    if vpc_subnet_id:
667        filters.update({"subnet-id": vpc_subnet_id})
668        if vpc:
669            vpc_id = vpc.get_all_subnets(subnet_ids=[vpc_subnet_id])[0].vpc_id
670
671    if vpc_id:
672        filters.update({"vpc-id": vpc_id})
673
674    if tags is not None:
675
676        if isinstance(tags, str):
677            try:
678                tags = literal_eval(tags)
679            except Exception:
680                pass
681
682        # if not a string type, convert and make sure it's a text string
683        if isinstance(tags, int):
684            tags = to_text(tags)
685
686        # if string, we only care that a tag of that name exists
687        if isinstance(tags, str):
688            filters.update({"tag-key": tags})
689
690        # if list, append each item to filters
691        if isinstance(tags, list):
692            for x in tags:
693                if isinstance(x, dict):
694                    x = _set_none_to_blank(x)
695                    filters.update(dict(("tag:" + tn, tv) for (tn, tv) in x.items()))
696                else:
697                    filters.update({"tag-key": x})
698
699        # if dict, add the key and value to the filter
700        if isinstance(tags, dict):
701            tags = _set_none_to_blank(tags)
702            filters.update(dict(("tag:" + tn, tv) for (tn, tv) in tags.items()))
703
704        # lets check to see if the filters dict is empty, if so then stop
705        if not filters:
706            module.fail_json(msg="Filters based on tag is empty => tags: %s" % (tags))
707
708    if state:
709        # http://stackoverflow.com/questions/437511/what-are-the-valid-instancestates-for-the-amazon-ec2-api
710        filters.update({'instance-state-name': state})
711
712    if zone:
713        filters.update({'availability-zone': zone})
714
715    if module.params.get('id'):
716        filters['client-token'] = module.params['id']
717
718    results = ec2.get_all_instances(filters=filters)
719
720    return results
721
722
723def get_instance_info(inst):
724    """
725    Retrieves instance information from an instance
726    ID and returns it as a dictionary
727    """
728    instance_info = {'id': inst.id,
729                     'ami_launch_index': inst.ami_launch_index,
730                     'private_ip': inst.private_ip_address,
731                     'private_dns_name': inst.private_dns_name,
732                     'public_ip': inst.ip_address,
733                     'dns_name': inst.dns_name,
734                     'public_dns_name': inst.public_dns_name,
735                     'state_code': inst.state_code,
736                     'architecture': inst.architecture,
737                     'image_id': inst.image_id,
738                     'key_name': inst.key_name,
739                     'placement': inst.placement,
740                     'region': inst.placement[:-1],
741                     'kernel': inst.kernel,
742                     'ramdisk': inst.ramdisk,
743                     'launch_time': inst.launch_time,
744                     'instance_type': inst.instance_type,
745                     'root_device_type': inst.root_device_type,
746                     'root_device_name': inst.root_device_name,
747                     'state': inst.state,
748                     'hypervisor': inst.hypervisor,
749                     'tags': inst.tags,
750                     'groups': dict((group.id, group.name) for group in inst.groups),
751                     }
752    try:
753        instance_info['virtualization_type'] = getattr(inst, 'virtualization_type')
754    except AttributeError:
755        instance_info['virtualization_type'] = None
756
757    try:
758        instance_info['ebs_optimized'] = getattr(inst, 'ebs_optimized')
759    except AttributeError:
760        instance_info['ebs_optimized'] = False
761
762    try:
763        bdm_dict = {}
764        bdm = getattr(inst, 'block_device_mapping')
765        for device_name in bdm.keys():
766            bdm_dict[device_name] = {
767                'status': bdm[device_name].status,
768                'volume_id': bdm[device_name].volume_id,
769                'delete_on_termination': bdm[device_name].delete_on_termination
770            }
771        instance_info['block_device_mapping'] = bdm_dict
772    except AttributeError:
773        instance_info['block_device_mapping'] = False
774
775    try:
776        instance_info['tenancy'] = getattr(inst, 'placement_tenancy')
777    except AttributeError:
778        instance_info['tenancy'] = 'default'
779
780    return instance_info
781
782
783def boto_supports_associate_public_ip_address(ec2):
784    """
785    Check if Boto library has associate_public_ip_address in the NetworkInterfaceSpecification
786    class. Added in Boto 2.13.0
787
788    ec2: authenticated ec2 connection object
789
790    Returns:
791        True if Boto library accepts associate_public_ip_address argument, else false
792    """
793
794    try:
795        network_interface = boto.ec2.networkinterface.NetworkInterfaceSpecification()
796        getattr(network_interface, "associate_public_ip_address")
797        return True
798    except AttributeError:
799        return False
800
801
802def boto_supports_profile_name_arg(ec2):
803    """
804    Check if Boto library has instance_profile_name argument. instance_profile_name has been added in Boto 2.5.0
805
806    ec2: authenticated ec2 connection object
807
808    Returns:
809        True if Boto library accept instance_profile_name argument, else false
810    """
811    run_instances_method = getattr(ec2, 'run_instances')
812    return 'instance_profile_name' in get_function_code(run_instances_method).co_varnames
813
814
815def boto_supports_volume_encryption():
816    """
817    Check if Boto library supports encryption of EBS volumes (added in 2.29.0)
818
819    Returns:
820        True if boto library has the named param as an argument on the request_spot_instances method, else False
821    """
822    return hasattr(boto, 'Version') and LooseVersion(boto.Version) >= LooseVersion('2.29.0')
823
824
825def create_block_device(module, ec2, volume):
826    # Not aware of a way to determine this programatically
827    # http://aws.amazon.com/about-aws/whats-new/2013/10/09/ebs-provisioned-iops-maximum-iops-gb-ratio-increased-to-30-1/
828    MAX_IOPS_TO_SIZE_RATIO = 30
829
830    volume_type = volume.get('volume_type')
831
832    if 'snapshot' not in volume and 'ephemeral' not in volume:
833        if 'volume_size' not in volume:
834            module.fail_json(msg='Size must be specified when creating a new volume or modifying the root volume')
835    if 'snapshot' in volume:
836        if volume_type == 'io1' and 'iops' not in volume:
837            module.fail_json(msg='io1 volumes must have an iops value set')
838        if 'iops' in volume:
839            snapshot = ec2.get_all_snapshots(snapshot_ids=[volume['snapshot']])[0]
840            size = volume.get('volume_size', snapshot.volume_size)
841            if int(volume['iops']) > MAX_IOPS_TO_SIZE_RATIO * size:
842                module.fail_json(msg='IOPS must be at most %d times greater than size' % MAX_IOPS_TO_SIZE_RATIO)
843    if 'ephemeral' in volume:
844        if 'snapshot' in volume:
845            module.fail_json(msg='Cannot set both ephemeral and snapshot')
846    if boto_supports_volume_encryption():
847        return BlockDeviceType(snapshot_id=volume.get('snapshot'),
848                               ephemeral_name=volume.get('ephemeral'),
849                               size=volume.get('volume_size'),
850                               volume_type=volume_type,
851                               delete_on_termination=volume.get('delete_on_termination', False),
852                               iops=volume.get('iops'),
853                               encrypted=volume.get('encrypted', None))
854    else:
855        return BlockDeviceType(snapshot_id=volume.get('snapshot'),
856                               ephemeral_name=volume.get('ephemeral'),
857                               size=volume.get('volume_size'),
858                               volume_type=volume_type,
859                               delete_on_termination=volume.get('delete_on_termination', False),
860                               iops=volume.get('iops'))
861
862
863def boto_supports_param_in_spot_request(ec2, param):
864    """
865    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.
866
867    ec2: authenticated ec2 connection object
868
869    Returns:
870        True if boto library has the named param as an argument on the request_spot_instances method, else False
871    """
872    method = getattr(ec2, 'request_spot_instances')
873    return param in get_function_code(method).co_varnames
874
875
876def await_spot_requests(module, ec2, spot_requests, count):
877    """
878    Wait for a group of spot requests to be fulfilled, or fail.
879
880    module: Ansible module object
881    ec2: authenticated ec2 connection object
882    spot_requests: boto.ec2.spotinstancerequest.SpotInstanceRequest object returned by ec2.request_spot_instances
883    count: Total number of instances to be created by the spot requests
884
885    Returns:
886        list of instance ID's created by the spot request(s)
887    """
888    spot_wait_timeout = int(module.params.get('spot_wait_timeout'))
889    wait_complete = time.time() + spot_wait_timeout
890
891    spot_req_inst_ids = dict()
892    while time.time() < wait_complete:
893        reqs = ec2.get_all_spot_instance_requests()
894        for sirb in spot_requests:
895            if sirb.id in spot_req_inst_ids:
896                continue
897            for sir in reqs:
898                if sir.id != sirb.id:
899                    continue  # this is not our spot instance
900                if sir.instance_id is not None:
901                    spot_req_inst_ids[sirb.id] = sir.instance_id
902                elif sir.state == 'open':
903                    continue  # still waiting, nothing to do here
904                elif sir.state == 'active':
905                    continue  # Instance is created already, nothing to do here
906                elif sir.state == 'failed':
907                    module.fail_json(msg="Spot instance request %s failed with status %s and fault %s:%s" % (
908                        sir.id, sir.status.code, sir.fault.code, sir.fault.message))
909                elif sir.state == 'cancelled':
910                    module.fail_json(msg="Spot instance request %s was cancelled before it could be fulfilled." % sir.id)
911                elif sir.state == 'closed':
912                    # instance is terminating or marked for termination
913                    # this may be intentional on the part of the operator,
914                    # or it may have been terminated by AWS due to capacity,
915                    # price, or group constraints in this case, we'll fail
916                    # the module if the reason for the state is anything
917                    # other than termination by user. Codes are documented at
918                    # https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-bid-status.html
919                    if sir.status.code == 'instance-terminated-by-user':
920                        # do nothing, since the user likely did this on purpose
921                        pass
922                    else:
923                        spot_msg = "Spot instance request %s was closed by AWS with the status %s and fault %s:%s"
924                        module.fail_json(msg=spot_msg % (sir.id, sir.status.code, sir.fault.code, sir.fault.message))
925
926        if len(spot_req_inst_ids) < count:
927            time.sleep(5)
928        else:
929            return list(spot_req_inst_ids.values())
930    module.fail_json(msg="wait for spot requests timeout on %s" % time.asctime())
931
932
933def enforce_count(module, ec2, vpc):
934
935    exact_count = module.params.get('exact_count')
936    count_tag = module.params.get('count_tag')
937    zone = module.params.get('zone')
938
939    # fail here if the exact count was specified without filtering
940    # on a tag, as this may lead to a undesired removal of instances
941    if exact_count and count_tag is None:
942        module.fail_json(msg="you must use the 'count_tag' option with exact_count")
943
944    reservations, instances = find_running_instances_by_count_tag(module, ec2, vpc, count_tag, zone)
945
946    changed = None
947    checkmode = False
948    instance_dict_array = []
949    changed_instance_ids = None
950
951    if len(instances) == exact_count:
952        changed = False
953    elif len(instances) < exact_count:
954        changed = True
955        to_create = exact_count - len(instances)
956        if not checkmode:
957            (instance_dict_array, changed_instance_ids, changed) \
958                = create_instances(module, ec2, vpc, override_count=to_create)
959
960            for inst in instance_dict_array:
961                instances.append(inst)
962    elif len(instances) > exact_count:
963        changed = True
964        to_remove = len(instances) - exact_count
965        if not checkmode:
966            all_instance_ids = sorted([x.id for x in instances])
967            remove_ids = all_instance_ids[0:to_remove]
968
969            instances = [x for x in instances if x.id not in remove_ids]
970
971            (changed, instance_dict_array, changed_instance_ids) \
972                = terminate_instances(module, ec2, remove_ids)
973            terminated_list = []
974            for inst in instance_dict_array:
975                inst['state'] = "terminated"
976                terminated_list.append(inst)
977            instance_dict_array = terminated_list
978
979    # ensure all instances are dictionaries
980    all_instances = []
981    for inst in instances:
982
983        if not isinstance(inst, dict):
984            warn_if_public_ip_assignment_changed(module, inst)
985            inst = get_instance_info(inst)
986        all_instances.append(inst)
987
988    return (all_instances, instance_dict_array, changed_instance_ids, changed)
989
990
991def create_instances(module, ec2, vpc, override_count=None):
992    """
993    Creates new instances
994
995    module : AnsibleModule object
996    ec2: authenticated ec2 connection object
997
998    Returns:
999        A list of dictionaries with instance information
1000        about the instances that were launched
1001    """
1002
1003    key_name = module.params.get('key_name')
1004    id = module.params.get('id')
1005    group_name = module.params.get('group')
1006    group_id = module.params.get('group_id')
1007    zone = module.params.get('zone')
1008    instance_type = module.params.get('instance_type')
1009    tenancy = module.params.get('tenancy')
1010    spot_price = module.params.get('spot_price')
1011    spot_type = module.params.get('spot_type')
1012    image = module.params.get('image')
1013    if override_count:
1014        count = override_count
1015    else:
1016        count = module.params.get('count')
1017    monitoring = module.params.get('monitoring')
1018    kernel = module.params.get('kernel')
1019    ramdisk = module.params.get('ramdisk')
1020    wait = module.params.get('wait')
1021    wait_timeout = int(module.params.get('wait_timeout'))
1022    spot_wait_timeout = int(module.params.get('spot_wait_timeout'))
1023    placement_group = module.params.get('placement_group')
1024    user_data = module.params.get('user_data')
1025    instance_tags = module.params.get('instance_tags')
1026    vpc_subnet_id = module.params.get('vpc_subnet_id')
1027    assign_public_ip = module.boolean(module.params.get('assign_public_ip'))
1028    private_ip = module.params.get('private_ip')
1029    instance_profile_name = module.params.get('instance_profile_name')
1030    volumes = module.params.get('volumes')
1031    ebs_optimized = module.params.get('ebs_optimized')
1032    exact_count = module.params.get('exact_count')
1033    count_tag = module.params.get('count_tag')
1034    source_dest_check = module.boolean(module.params.get('source_dest_check'))
1035    termination_protection = module.boolean(module.params.get('termination_protection'))
1036    network_interfaces = module.params.get('network_interfaces')
1037    spot_launch_group = module.params.get('spot_launch_group')
1038    instance_initiated_shutdown_behavior = module.params.get('instance_initiated_shutdown_behavior')
1039
1040    vpc_id = None
1041    if vpc_subnet_id:
1042        if not vpc:
1043            module.fail_json(msg="region must be specified")
1044        else:
1045            vpc_id = vpc.get_all_subnets(subnet_ids=[vpc_subnet_id])[0].vpc_id
1046    else:
1047        vpc_id = None
1048
1049    try:
1050        # Here we try to lookup the group id from the security group name - if group is set.
1051        if group_name:
1052            if vpc_id:
1053                grp_details = ec2.get_all_security_groups(filters={'vpc_id': vpc_id})
1054            else:
1055                grp_details = ec2.get_all_security_groups()
1056            if isinstance(group_name, string_types):
1057                group_name = [group_name]
1058            unmatched = set(group_name).difference(str(grp.name) for grp in grp_details)
1059            if len(unmatched) > 0:
1060                module.fail_json(msg="The following group names are not valid: %s" % ', '.join(unmatched))
1061            group_id = [str(grp.id) for grp in grp_details if str(grp.name) in group_name]
1062        # Now we try to lookup the group id testing if group exists.
1063        elif group_id:
1064            # wrap the group_id in a list if it's not one already
1065            if isinstance(group_id, string_types):
1066                group_id = [group_id]
1067            grp_details = ec2.get_all_security_groups(group_ids=group_id)
1068            group_name = [grp_item.name for grp_item in grp_details]
1069    except boto.exception.NoAuthHandlerFound as e:
1070        module.fail_json(msg=str(e))
1071
1072    # Lookup any instances that much our run id.
1073
1074    running_instances = []
1075    count_remaining = int(count)
1076
1077    if id is not None:
1078        filter_dict = {'client-token': id, 'instance-state-name': 'running'}
1079        previous_reservations = ec2.get_all_instances(None, filter_dict)
1080        for res in previous_reservations:
1081            for prev_instance in res.instances:
1082                running_instances.append(prev_instance)
1083        count_remaining = count_remaining - len(running_instances)
1084
1085    # 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.
1086
1087    if count_remaining == 0:
1088        changed = False
1089    else:
1090        changed = True
1091        try:
1092            params = {'image_id': image,
1093                      'key_name': key_name,
1094                      'monitoring_enabled': monitoring,
1095                      'placement': zone,
1096                      'instance_type': instance_type,
1097                      'kernel_id': kernel,
1098                      'ramdisk_id': ramdisk}
1099            if user_data is not None:
1100                params['user_data'] = to_bytes(user_data, errors='surrogate_or_strict')
1101
1102            if ebs_optimized:
1103                params['ebs_optimized'] = ebs_optimized
1104
1105            # 'tenancy' always has a default value, but it is not a valid parameter for spot instance request
1106            if not spot_price:
1107                params['tenancy'] = tenancy
1108
1109            if boto_supports_profile_name_arg(ec2):
1110                params['instance_profile_name'] = instance_profile_name
1111            else:
1112                if instance_profile_name is not None:
1113                    module.fail_json(
1114                        msg="instance_profile_name parameter requires Boto version 2.5.0 or higher")
1115
1116            if assign_public_ip is not None:
1117                if not boto_supports_associate_public_ip_address(ec2):
1118                    module.fail_json(
1119                        msg="assign_public_ip parameter requires Boto version 2.13.0 or higher.")
1120                elif not vpc_subnet_id:
1121                    module.fail_json(
1122                        msg="assign_public_ip only available with vpc_subnet_id")
1123
1124                else:
1125                    if private_ip:
1126                        interface = boto.ec2.networkinterface.NetworkInterfaceSpecification(
1127                            subnet_id=vpc_subnet_id,
1128                            private_ip_address=private_ip,
1129                            groups=group_id,
1130                            associate_public_ip_address=assign_public_ip)
1131                    else:
1132                        interface = boto.ec2.networkinterface.NetworkInterfaceSpecification(
1133                            subnet_id=vpc_subnet_id,
1134                            groups=group_id,
1135                            associate_public_ip_address=assign_public_ip)
1136                    interfaces = boto.ec2.networkinterface.NetworkInterfaceCollection(interface)
1137                    params['network_interfaces'] = interfaces
1138            else:
1139                if network_interfaces:
1140                    if isinstance(network_interfaces, string_types):
1141                        network_interfaces = [network_interfaces]
1142                    interfaces = []
1143                    for i, network_interface_id in enumerate(network_interfaces):
1144                        interface = boto.ec2.networkinterface.NetworkInterfaceSpecification(
1145                            network_interface_id=network_interface_id,
1146                            device_index=i)
1147                        interfaces.append(interface)
1148                    params['network_interfaces'] = \
1149                        boto.ec2.networkinterface.NetworkInterfaceCollection(*interfaces)
1150                else:
1151                    params['subnet_id'] = vpc_subnet_id
1152                    if vpc_subnet_id:
1153                        params['security_group_ids'] = group_id
1154                    else:
1155                        params['security_groups'] = group_name
1156
1157            if volumes:
1158                bdm = BlockDeviceMapping()
1159                for volume in volumes:
1160                    if 'device_name' not in volume:
1161                        module.fail_json(msg='Device name must be set for volume')
1162                    # Minimum volume size is 1GiB. We'll use volume size explicitly set to 0
1163                    # to be a signal not to create this volume
1164                    if 'volume_size' not in volume or int(volume['volume_size']) > 0:
1165                        bdm[volume['device_name']] = create_block_device(module, ec2, volume)
1166
1167                params['block_device_map'] = bdm
1168
1169            # check to see if we're using spot pricing first before starting instances
1170            if not spot_price:
1171                if assign_public_ip is not None and private_ip:
1172                    params.update(
1173                        dict(
1174                            min_count=count_remaining,
1175                            max_count=count_remaining,
1176                            client_token=id,
1177                            placement_group=placement_group,
1178                        )
1179                    )
1180                else:
1181                    params.update(
1182                        dict(
1183                            min_count=count_remaining,
1184                            max_count=count_remaining,
1185                            client_token=id,
1186                            placement_group=placement_group,
1187                            private_ip_address=private_ip,
1188                        )
1189                    )
1190
1191                # For ordinary (not spot) instances, we can select 'stop'
1192                # (the default) or 'terminate' here.
1193                params['instance_initiated_shutdown_behavior'] = instance_initiated_shutdown_behavior or 'stop'
1194
1195                try:
1196                    res = ec2.run_instances(**params)
1197                except boto.exception.EC2ResponseError as e:
1198                    if (params['instance_initiated_shutdown_behavior'] != 'terminate' and
1199                            "InvalidParameterCombination" == e.error_code):
1200                        params['instance_initiated_shutdown_behavior'] = 'terminate'
1201                        res = ec2.run_instances(**params)
1202                    else:
1203                        raise
1204
1205                instids = [i.id for i in res.instances]
1206                while True:
1207                    try:
1208                        ec2.get_all_instances(instids)
1209                        break
1210                    except boto.exception.EC2ResponseError as e:
1211                        if "<Code>InvalidInstanceID.NotFound</Code>" in str(e):
1212                            # there's a race between start and get an instance
1213                            continue
1214                        else:
1215                            module.fail_json(msg=str(e))
1216
1217                # The instances returned through ec2.run_instances above can be in
1218                # terminated state due to idempotency. See commit 7f11c3d for a complete
1219                # explanation.
1220                terminated_instances = [
1221                    str(instance.id) for instance in res.instances if instance.state == 'terminated'
1222                ]
1223                if terminated_instances:
1224                    module.fail_json(msg="Instances with id(s) %s " % terminated_instances +
1225                                     "were created previously but have since been terminated - " +
1226                                     "use a (possibly different) 'instanceid' parameter")
1227
1228            else:
1229                if private_ip:
1230                    module.fail_json(
1231                        msg='private_ip only available with on-demand (non-spot) instances')
1232                if boto_supports_param_in_spot_request(ec2, 'placement_group'):
1233                    params['placement_group'] = placement_group
1234                elif placement_group:
1235                    module.fail_json(
1236                        msg="placement_group parameter requires Boto version 2.3.0 or higher.")
1237
1238                # You can't tell spot instances to 'stop'; they will always be
1239                # 'terminate'd. For convenience, we'll ignore the latter value.
1240                if instance_initiated_shutdown_behavior and instance_initiated_shutdown_behavior != 'terminate':
1241                    module.fail_json(
1242                        msg="instance_initiated_shutdown_behavior=stop is not supported for spot instances.")
1243
1244                if spot_launch_group and isinstance(spot_launch_group, string_types):
1245                    params['launch_group'] = spot_launch_group
1246
1247                params.update(dict(
1248                    count=count_remaining,
1249                    type=spot_type,
1250                ))
1251
1252                # Set spot ValidUntil
1253                # ValidUntil -> (timestamp). The end date of the request, in
1254                # UTC format (for example, YYYY -MM -DD T*HH* :MM :SS Z).
1255                utc_valid_until = (
1256                    datetime.datetime.utcnow()
1257                    + datetime.timedelta(seconds=spot_wait_timeout))
1258                params['valid_until'] = utc_valid_until.strftime('%Y-%m-%dT%H:%M:%S.000Z')
1259
1260                res = ec2.request_spot_instances(spot_price, **params)
1261
1262                # Now we have to do the intermediate waiting
1263                if wait:
1264                    instids = await_spot_requests(module, ec2, res, count)
1265                else:
1266                    instids = []
1267        except boto.exception.BotoServerError as e:
1268            module.fail_json(msg="Instance creation failed => %s: %s" % (e.error_code, e.error_message))
1269
1270        # wait here until the instances are up
1271        num_running = 0
1272        wait_timeout = time.time() + wait_timeout
1273        res_list = ()
1274        while wait_timeout > time.time() and num_running < len(instids):
1275            try:
1276                res_list = ec2.get_all_instances(instids)
1277            except boto.exception.BotoServerError as e:
1278                if e.error_code == 'InvalidInstanceID.NotFound':
1279                    time.sleep(1)
1280                    continue
1281                else:
1282                    raise
1283
1284            num_running = 0
1285            for res in res_list:
1286                num_running += len([i for i in res.instances if i.state == 'running'])
1287            if len(res_list) <= 0:
1288                # got a bad response of some sort, possibly due to
1289                # stale/cached data. Wait a second and then try again
1290                time.sleep(1)
1291                continue
1292            if wait and num_running < len(instids):
1293                time.sleep(5)
1294            else:
1295                break
1296
1297        if wait and wait_timeout <= time.time():
1298            # waiting took too long
1299            module.fail_json(msg="wait for instances running timeout on %s" % time.asctime())
1300
1301        # We do this after the loop ends so that we end up with one list
1302        for res in res_list:
1303            running_instances.extend(res.instances)
1304
1305        # Enabled by default by AWS
1306        if source_dest_check is False:
1307            for inst in res.instances:
1308                inst.modify_attribute('sourceDestCheck', False)
1309
1310        # Disabled by default by AWS
1311        if termination_protection is True:
1312            for inst in res.instances:
1313                inst.modify_attribute('disableApiTermination', True)
1314
1315        # Leave this as late as possible to try and avoid InvalidInstanceID.NotFound
1316        if instance_tags and instids:
1317            try:
1318                ec2.create_tags(instids, instance_tags)
1319            except boto.exception.EC2ResponseError as e:
1320                module.fail_json(msg="Instance tagging failed => %s: %s" % (e.error_code, e.error_message))
1321
1322    instance_dict_array = []
1323    created_instance_ids = []
1324    for inst in running_instances:
1325        inst.update()
1326        d = get_instance_info(inst)
1327        created_instance_ids.append(inst.id)
1328        instance_dict_array.append(d)
1329
1330    return (instance_dict_array, created_instance_ids, changed)
1331
1332
1333def terminate_instances(module, ec2, instance_ids):
1334    """
1335    Terminates a list of instances
1336
1337    module: Ansible module object
1338    ec2: authenticated ec2 connection object
1339    termination_list: a list of instances to terminate in the form of
1340      [ {id: <inst-id>}, ..]
1341
1342    Returns a dictionary of instance information
1343    about the instances terminated.
1344
1345    If the instance to be terminated is running
1346    "changed" will be set to False.
1347
1348    """
1349
1350    # Whether to wait for termination to complete before returning
1351    wait = module.params.get('wait')
1352    wait_timeout = int(module.params.get('wait_timeout'))
1353
1354    changed = False
1355    instance_dict_array = []
1356
1357    if not isinstance(instance_ids, list) or len(instance_ids) < 1:
1358        module.fail_json(msg='instance_ids should be a list of instances, aborting')
1359
1360    terminated_instance_ids = []
1361    for res in ec2.get_all_instances(instance_ids):
1362        for inst in res.instances:
1363            if inst.state == 'running' or inst.state == 'stopped':
1364                terminated_instance_ids.append(inst.id)
1365                instance_dict_array.append(get_instance_info(inst))
1366                try:
1367                    ec2.terminate_instances([inst.id])
1368                except EC2ResponseError as e:
1369                    module.fail_json(msg='Unable to terminate instance {0}, error: {1}'.format(inst.id, e))
1370                changed = True
1371
1372    # wait here until the instances are 'terminated'
1373    if wait:
1374        num_terminated = 0
1375        wait_timeout = time.time() + wait_timeout
1376        while wait_timeout > time.time() and num_terminated < len(terminated_instance_ids):
1377            response = ec2.get_all_instances(instance_ids=terminated_instance_ids,
1378                                             filters={'instance-state-name': 'terminated'})
1379            try:
1380                num_terminated = sum([len(res.instances) for res in response])
1381            except Exception as e:
1382                # got a bad response of some sort, possibly due to
1383                # stale/cached data. Wait a second and then try again
1384                time.sleep(1)
1385                continue
1386
1387            if num_terminated < len(terminated_instance_ids):
1388                time.sleep(5)
1389
1390        # waiting took too long
1391        if wait_timeout < time.time() and num_terminated < len(terminated_instance_ids):
1392            module.fail_json(msg="wait for instance termination timeout on %s" % time.asctime())
1393        # Lets get the current state of the instances after terminating - issue600
1394        instance_dict_array = []
1395        for res in ec2.get_all_instances(instance_ids=terminated_instance_ids, filters={'instance-state-name': 'terminated'}):
1396            for inst in res.instances:
1397                instance_dict_array.append(get_instance_info(inst))
1398
1399    return (changed, instance_dict_array, terminated_instance_ids)
1400
1401
1402def startstop_instances(module, ec2, instance_ids, state, instance_tags):
1403    """
1404    Starts or stops a list of existing instances
1405
1406    module: Ansible module object
1407    ec2: authenticated ec2 connection object
1408    instance_ids: The list of instances to start in the form of
1409      [ {id: <inst-id>}, ..]
1410    instance_tags: A dict of tag keys and values in the form of
1411      {key: value, ... }
1412    state: Intended state ("running" or "stopped")
1413
1414    Returns a dictionary of instance information
1415    about the instances started/stopped.
1416
1417    If the instance was not able to change state,
1418    "changed" will be set to False.
1419
1420    Note that if instance_ids and instance_tags are both non-empty,
1421    this method will process the intersection of the two
1422    """
1423
1424    wait = module.params.get('wait')
1425    wait_timeout = int(module.params.get('wait_timeout'))
1426    group_id = module.params.get('group_id')
1427    group_name = module.params.get('group')
1428    changed = False
1429    instance_dict_array = []
1430
1431    if not isinstance(instance_ids, list) or len(instance_ids) < 1:
1432        # Fail unless the user defined instance tags
1433        if not instance_tags:
1434            module.fail_json(msg='instance_ids should be a list of instances, aborting')
1435
1436    # To make an EC2 tag filter, we need to prepend 'tag:' to each key.
1437    # An empty filter does no filtering, so it's safe to pass it to the
1438    # get_all_instances method even if the user did not specify instance_tags
1439    filters = {}
1440    if instance_tags:
1441        for key, value in instance_tags.items():
1442            filters["tag:" + key] = value
1443
1444    if module.params.get('id'):
1445        filters['client-token'] = module.params['id']
1446    # Check that our instances are not in the state we want to take
1447
1448    # Check (and eventually change) instances attributes and instances state
1449    existing_instances_array = []
1450    for res in ec2.get_all_instances(instance_ids, filters=filters):
1451        for inst in res.instances:
1452
1453            warn_if_public_ip_assignment_changed(module, inst)
1454
1455            changed = (check_source_dest_attr(module, inst, ec2) or
1456                       check_termination_protection(module, inst) or changed)
1457
1458            # Check security groups and if we're using ec2-vpc; ec2-classic security groups may not be modified
1459            if inst.vpc_id and group_name:
1460                grp_details = ec2.get_all_security_groups(filters={'vpc_id': inst.vpc_id})
1461                if isinstance(group_name, string_types):
1462                    group_name = [group_name]
1463                unmatched = set(group_name) - set(to_text(grp.name) for grp in grp_details)
1464                if unmatched:
1465                    module.fail_json(msg="The following group names are not valid: %s" % ', '.join(unmatched))
1466                group_ids = [to_text(grp.id) for grp in grp_details if to_text(grp.name) in group_name]
1467            elif inst.vpc_id and group_id:
1468                if isinstance(group_id, string_types):
1469                    group_id = [group_id]
1470                grp_details = ec2.get_all_security_groups(group_ids=group_id)
1471                group_ids = [grp_item.id for grp_item in grp_details]
1472            if inst.vpc_id and (group_name or group_id):
1473                if set(sg.id for sg in inst.groups) != set(group_ids):
1474                    changed = inst.modify_attribute('groupSet', group_ids)
1475
1476            # Check instance state
1477            if inst.state != state:
1478                instance_dict_array.append(get_instance_info(inst))
1479                try:
1480                    if state == 'running':
1481                        inst.start()
1482                    else:
1483                        inst.stop()
1484                except EC2ResponseError as e:
1485                    module.fail_json(msg='Unable to change state for instance {0}, error: {1}'.format(inst.id, e))
1486                changed = True
1487            existing_instances_array.append(inst.id)
1488
1489    instance_ids = list(set(existing_instances_array + (instance_ids or [])))
1490    # Wait for all the instances to finish starting or stopping
1491    wait_timeout = time.time() + wait_timeout
1492    while wait and wait_timeout > time.time():
1493        instance_dict_array = []
1494        matched_instances = []
1495        for res in ec2.get_all_instances(instance_ids):
1496            for i in res.instances:
1497                if i.state == state:
1498                    instance_dict_array.append(get_instance_info(i))
1499                    matched_instances.append(i)
1500        if len(matched_instances) < len(instance_ids):
1501            time.sleep(5)
1502        else:
1503            break
1504
1505    if wait and wait_timeout <= time.time():
1506        # waiting took too long
1507        module.fail_json(msg="wait for instances running timeout on %s" % time.asctime())
1508
1509    return (changed, instance_dict_array, instance_ids)
1510
1511
1512def restart_instances(module, ec2, instance_ids, state, instance_tags):
1513    """
1514    Restarts a list of existing instances
1515
1516    module: Ansible module object
1517    ec2: authenticated ec2 connection object
1518    instance_ids: The list of instances to start in the form of
1519      [ {id: <inst-id>}, ..]
1520    instance_tags: A dict of tag keys and values in the form of
1521      {key: value, ... }
1522    state: Intended state ("restarted")
1523
1524    Returns a dictionary of instance information
1525    about the instances.
1526
1527    If the instance was not able to change state,
1528    "changed" will be set to False.
1529
1530    Wait will not apply here as this is a OS level operation.
1531
1532    Note that if instance_ids and instance_tags are both non-empty,
1533    this method will process the intersection of the two.
1534    """
1535
1536    changed = False
1537    instance_dict_array = []
1538
1539    if not isinstance(instance_ids, list) or len(instance_ids) < 1:
1540        # Fail unless the user defined instance tags
1541        if not instance_tags:
1542            module.fail_json(msg='instance_ids should be a list of instances, aborting')
1543
1544    # To make an EC2 tag filter, we need to prepend 'tag:' to each key.
1545    # An empty filter does no filtering, so it's safe to pass it to the
1546    # get_all_instances method even if the user did not specify instance_tags
1547    filters = {}
1548    if instance_tags:
1549        for key, value in instance_tags.items():
1550            filters["tag:" + key] = value
1551    if module.params.get('id'):
1552        filters['client-token'] = module.params['id']
1553
1554    # Check that our instances are not in the state we want to take
1555
1556    # Check (and eventually change) instances attributes and instances state
1557    for res in ec2.get_all_instances(instance_ids, filters=filters):
1558        for inst in res.instances:
1559
1560            warn_if_public_ip_assignment_changed(module, inst)
1561
1562            changed = (check_source_dest_attr(module, inst, ec2) or
1563                       check_termination_protection(module, inst) or changed)
1564
1565            # Check instance state
1566            if inst.state != state:
1567                instance_dict_array.append(get_instance_info(inst))
1568                try:
1569                    inst.reboot()
1570                except EC2ResponseError as e:
1571                    module.fail_json(msg='Unable to change state for instance {0}, error: {1}'.format(inst.id, e))
1572                changed = True
1573
1574    return (changed, instance_dict_array, instance_ids)
1575
1576
1577def check_termination_protection(module, inst):
1578    """
1579    Check the instance disableApiTermination attribute.
1580
1581    module: Ansible module object
1582    inst: EC2 instance object
1583
1584    returns: True if state changed None otherwise
1585    """
1586
1587    termination_protection = module.params.get('termination_protection')
1588
1589    if (inst.get_attribute('disableApiTermination')['disableApiTermination'] != termination_protection and termination_protection is not None):
1590        inst.modify_attribute('disableApiTermination', termination_protection)
1591        return True
1592
1593
1594def check_source_dest_attr(module, inst, ec2):
1595    """
1596    Check the instance sourceDestCheck attribute.
1597
1598    module: Ansible module object
1599    inst: EC2 instance object
1600
1601    returns: True if state changed None otherwise
1602    """
1603
1604    source_dest_check = module.params.get('source_dest_check')
1605
1606    if source_dest_check is not None:
1607        try:
1608            if inst.vpc_id is not None and inst.get_attribute('sourceDestCheck')['sourceDestCheck'] != source_dest_check:
1609                inst.modify_attribute('sourceDestCheck', source_dest_check)
1610                return True
1611        except boto.exception.EC2ResponseError as exc:
1612            # instances with more than one Elastic Network Interface will
1613            # fail, because they have the sourceDestCheck attribute defined
1614            # per-interface
1615            if exc.code == 'InvalidInstanceID':
1616                for interface in inst.interfaces:
1617                    if interface.source_dest_check != source_dest_check:
1618                        ec2.modify_network_interface_attribute(interface.id, "sourceDestCheck", source_dest_check)
1619                        return True
1620            else:
1621                module.fail_json(msg='Failed to handle source_dest_check state for instance {0}, error: {1}'.format(inst.id, exc),
1622                                 exception=traceback.format_exc())
1623
1624
1625def warn_if_public_ip_assignment_changed(module, instance):
1626    # This is a non-modifiable attribute.
1627    assign_public_ip = module.params.get('assign_public_ip')
1628
1629    # Check that public ip assignment is the same and warn if not
1630    public_dns_name = getattr(instance, 'public_dns_name', None)
1631    if (assign_public_ip or public_dns_name) and (not public_dns_name or assign_public_ip is False):
1632        module.warn("Unable to modify public ip assignment to {0} for instance {1}. "
1633                    "Whether or not to assign a public IP is determined during instance creation.".format(assign_public_ip, instance.id))
1634
1635
1636def main():
1637    argument_spec = ec2_argument_spec()
1638    argument_spec.update(
1639        dict(
1640            key_name=dict(aliases=['keypair']),
1641            id=dict(),
1642            group=dict(type='list', aliases=['groups']),
1643            group_id=dict(type='list'),
1644            zone=dict(aliases=['aws_zone', 'ec2_zone']),
1645            instance_type=dict(aliases=['type']),
1646            spot_price=dict(),
1647            spot_type=dict(default='one-time', choices=["one-time", "persistent"]),
1648            spot_launch_group=dict(),
1649            image=dict(),
1650            kernel=dict(),
1651            count=dict(type='int', default='1'),
1652            monitoring=dict(type='bool', default=False),
1653            ramdisk=dict(),
1654            wait=dict(type='bool', default=False),
1655            wait_timeout=dict(type='int', default=300),
1656            spot_wait_timeout=dict(type='int', default=600),
1657            placement_group=dict(),
1658            user_data=dict(),
1659            instance_tags=dict(type='dict'),
1660            vpc_subnet_id=dict(),
1661            assign_public_ip=dict(type='bool'),
1662            private_ip=dict(),
1663            instance_profile_name=dict(),
1664            instance_ids=dict(type='list', aliases=['instance_id']),
1665            source_dest_check=dict(type='bool', default=None),
1666            termination_protection=dict(type='bool', default=None),
1667            state=dict(default='present', choices=['present', 'absent', 'running', 'restarted', 'stopped']),
1668            instance_initiated_shutdown_behavior=dict(default='stop', choices=['stop', 'terminate']),
1669            exact_count=dict(type='int', default=None),
1670            count_tag=dict(type='raw'),
1671            volumes=dict(type='list'),
1672            ebs_optimized=dict(type='bool', default=False),
1673            tenancy=dict(default='default', choices=['default', 'dedicated']),
1674            network_interfaces=dict(type='list', aliases=['network_interface'])
1675        )
1676    )
1677
1678    module = AnsibleModule(
1679        argument_spec=argument_spec,
1680        mutually_exclusive=[
1681            # Can be uncommented when we finish the deprecation cycle.
1682            # ['group', 'group_id'],
1683            ['exact_count', 'count'],
1684            ['exact_count', 'state'],
1685            ['exact_count', 'instance_ids'],
1686            ['network_interfaces', 'assign_public_ip'],
1687            ['network_interfaces', 'group'],
1688            ['network_interfaces', 'group_id'],
1689            ['network_interfaces', 'private_ip'],
1690            ['network_interfaces', 'vpc_subnet_id'],
1691        ],
1692    )
1693
1694    if module.params.get('group') and module.params.get('group_id'):
1695        module.deprecate(
1696            msg='Support for passing both group and group_id has been deprecated. '
1697            'Currently group_id is ignored, in future passing both will result in an error',
1698            version='2.14', collection_name='ansible.builtin')
1699
1700    if not HAS_BOTO:
1701        module.fail_json(msg='boto required for this module')
1702
1703    try:
1704        region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module)
1705        if module.params.get('region') or not module.params.get('ec2_url'):
1706            ec2 = ec2_connect(module)
1707        elif module.params.get('ec2_url'):
1708            ec2 = connect_ec2_endpoint(ec2_url, **aws_connect_kwargs)
1709
1710        if 'region' not in aws_connect_kwargs:
1711            aws_connect_kwargs['region'] = ec2.region
1712
1713        vpc = connect_vpc(**aws_connect_kwargs)
1714    except boto.exception.NoAuthHandlerFound as e:
1715        module.fail_json(msg="Failed to get connection: %s" % e.message, exception=traceback.format_exc())
1716
1717    tagged_instances = []
1718
1719    state = module.params['state']
1720
1721    if state == 'absent':
1722        instance_ids = module.params['instance_ids']
1723        if not instance_ids:
1724            module.fail_json(msg='instance_ids list is required for absent state')
1725
1726        (changed, instance_dict_array, new_instance_ids) = terminate_instances(module, ec2, instance_ids)
1727
1728    elif state in ('running', 'stopped'):
1729        instance_ids = module.params.get('instance_ids')
1730        instance_tags = module.params.get('instance_tags')
1731        if not (isinstance(instance_ids, list) or isinstance(instance_tags, dict)):
1732            module.fail_json(msg='running list needs to be a list of instances or set of tags to run: %s' % instance_ids)
1733
1734        (changed, instance_dict_array, new_instance_ids) = startstop_instances(module, ec2, instance_ids, state, instance_tags)
1735
1736    elif state in ('restarted'):
1737        instance_ids = module.params.get('instance_ids')
1738        instance_tags = module.params.get('instance_tags')
1739        if not (isinstance(instance_ids, list) or isinstance(instance_tags, dict)):
1740            module.fail_json(msg='running list needs to be a list of instances or set of tags to run: %s' % instance_ids)
1741
1742        (changed, instance_dict_array, new_instance_ids) = restart_instances(module, ec2, instance_ids, state, instance_tags)
1743
1744    elif state == 'present':
1745        # Changed is always set to true when provisioning new instances
1746        if not module.params.get('image'):
1747            module.fail_json(msg='image parameter is required for new instance')
1748
1749        if module.params.get('exact_count') is None:
1750            (instance_dict_array, new_instance_ids, changed) = create_instances(module, ec2, vpc)
1751        else:
1752            (tagged_instances, instance_dict_array, new_instance_ids, changed) = enforce_count(module, ec2, vpc)
1753
1754    # Always return instances in the same order
1755    if new_instance_ids:
1756        new_instance_ids.sort()
1757    if instance_dict_array:
1758        instance_dict_array.sort(key=lambda x: x['id'])
1759    if tagged_instances:
1760        tagged_instances.sort(key=lambda x: x['id'])
1761
1762    module.exit_json(changed=changed, instance_ids=new_instance_ids, instances=instance_dict_array, tagged_instances=tagged_instances)
1763
1764
1765if __name__ == '__main__':
1766    main()
1767