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