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