1#!/usr/local/bin/python3.8 2# -*- coding: utf-8 -*- 3# Copyright (c) 2017 Ansible Project 4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5from __future__ import (absolute_import, division, print_function) 6 7DOCUMENTATION = ''' 8--- 9module: spotinst_aws_elastigroup 10short_description: Create, update or delete Spotinst AWS Elastigroups 11author: Spotinst (@talzur) 12description: 13 - Can create, update, or delete Spotinst AWS Elastigroups 14 Launch configuration is part of the elastigroup configuration, 15 so no additional modules are necessary for handling the launch configuration. 16 You will have to have a credentials file in this location - <home>/.spotinst/credentials 17 The credentials file must contain a row that looks like this 18 token = <YOUR TOKEN> 19 Full documentation available at https://help.spotinst.com/hc/en-us/articles/115003530285-Ansible- 20requirements: 21 - python >= 2.7 22 - spotinst_sdk >= 1.0.38 23options: 24 25 credentials_path: 26 description: 27 - Optional parameter that allows to set a non-default credentials path. 28 default: ~/.spotinst/credentials 29 type: path 30 31 account_id: 32 description: 33 - Optional parameter that allows to set an account-id inside the module configuration. 34 By default this is retrieved from the credentials path. 35 type: str 36 37 availability_vs_cost: 38 description: 39 - The strategy orientation. 40 - "The choices available are: C(availabilityOriented), C(costOriented), C(balanced)." 41 required: true 42 type: str 43 44 availability_zones: 45 description: 46 - A list of hash/dictionaries of Availability Zones that are configured in the elastigroup; 47 '[{"key":"value", "key":"value"}]'; 48 keys allowed are 49 name (String), 50 subnet_id (String), 51 placement_group_name (String), 52 required: true 53 type: list 54 elements: dict 55 56 block_device_mappings: 57 description: 58 - A list of hash/dictionaries of Block Device Mappings for elastigroup instances; 59 You can specify virtual devices and EBS volumes.; 60 '[{"key":"value", "key":"value"}]'; 61 keys allowed are 62 device_name (List of Strings), 63 virtual_name (String), 64 no_device (String), 65 ebs (Object, expects the following keys- 66 delete_on_termination(Boolean), 67 encrypted(Boolean), 68 iops (Integer), 69 snapshot_id(Integer), 70 volume_type(String), 71 volume_size(Integer)) 72 type: list 73 elements: dict 74 75 chef: 76 description: 77 - The Chef integration configuration.; 78 Expects the following keys - chef_server (String), 79 organization (String), 80 user (String), 81 pem_key (String), 82 chef_version (String) 83 type: dict 84 85 draining_timeout: 86 description: 87 - Time for instance to be drained from incoming requests and deregistered from ELB before termination. 88 type: int 89 90 ebs_optimized: 91 description: 92 - Enable EBS optimization for supported instances which are not enabled by default.; 93 Note - additional charges will be applied. 94 type: bool 95 96 ebs_volume_pool: 97 description: 98 - A list of hash/dictionaries of EBS devices to reattach to the elastigroup when available; 99 '[{"key":"value", "key":"value"}]'; 100 keys allowed are - 101 volume_ids (List of Strings), 102 device_name (String) 103 type: list 104 elements: dict 105 106 ecs: 107 description: 108 - The ECS integration configuration.; 109 Expects the following key - 110 cluster_name (String) 111 type: dict 112 113 elastic_ips: 114 description: 115 - List of ElasticIps Allocation Ids (Example C(eipalloc-9d4e16f8)) to associate to the group instances 116 type: list 117 elements: str 118 119 fallback_to_od: 120 description: 121 - In case of no spots available, Elastigroup will launch an On-demand instance instead 122 type: bool 123 124 health_check_grace_period: 125 description: 126 - The amount of time, in seconds, after the instance has launched to start and check its health. 127 - If not specified, it defaults to C(300). 128 type: int 129 130 health_check_unhealthy_duration_before_replacement: 131 description: 132 - Minimal mount of time instance should be unhealthy for us to consider it unhealthy. 133 type: int 134 135 health_check_type: 136 description: 137 - The service to use for the health check. 138 - "The choices available are: C(ELB), C(HCS), C(TARGET_GROUP), C(MLB), C(EC2)." 139 type: str 140 141 iam_role_name: 142 description: 143 - The instance profile iamRole name 144 - Only use iam_role_arn, or iam_role_name 145 type: str 146 147 iam_role_arn: 148 description: 149 - The instance profile iamRole arn 150 - Only use iam_role_arn, or iam_role_name 151 type: str 152 153 id: 154 description: 155 - The group id if it already exists and you want to update, or delete it. 156 This will not work unless the uniqueness_by field is set to id. 157 When this is set, and the uniqueness_by field is set, the group will either be updated or deleted, but not created. 158 type: str 159 160 image_id: 161 description: 162 - The image Id used to launch the instance.; 163 In case of conflict between Instance type and image type, an error will be returned 164 required: true 165 type: str 166 167 key_pair: 168 description: 169 - Specify a Key Pair to attach to the instances 170 type: str 171 172 kubernetes: 173 description: 174 - The Kubernetes integration configuration. 175 Expects the following keys - 176 api_server (String), 177 token (String) 178 type: dict 179 180 lifetime_period: 181 description: 182 - Lifetime period 183 type: int 184 185 load_balancers: 186 description: 187 - List of classic ELB names 188 type: list 189 elements: str 190 191 max_size: 192 description: 193 - The upper limit number of instances that you can scale up to 194 required: true 195 type: int 196 197 mesosphere: 198 description: 199 - The Mesosphere integration configuration. 200 Expects the following key - 201 api_server (String) 202 type: dict 203 204 min_size: 205 description: 206 - The lower limit number of instances that you can scale down to 207 required: true 208 type: int 209 210 monitoring: 211 description: 212 - Describes whether instance Enhanced Monitoring is enabled 213 type: str 214 215 name: 216 description: 217 - Unique name for elastigroup to be created, updated or deleted 218 required: true 219 type: str 220 221 network_interfaces: 222 description: 223 - A list of hash/dictionaries of network interfaces to add to the elastigroup; 224 '[{"key":"value", "key":"value"}]'; 225 keys allowed are - 226 description (String), 227 device_index (Integer), 228 secondary_private_ip_address_count (Integer), 229 associate_public_ip_address (Boolean), 230 delete_on_termination (Boolean), 231 groups (List of Strings), 232 network_interface_id (String), 233 private_ip_address (String), 234 subnet_id (String), 235 associate_ipv6_address (Boolean), 236 private_ip_addresses (List of Objects, Keys are privateIpAddress (String, required) and primary (Boolean)) 237 type: list 238 elements: dict 239 240 on_demand_count: 241 description: 242 - Required if risk is not set 243 - Number of on demand instances to launch. All other instances will be spot instances.; 244 Either set this parameter or the risk parameter 245 type: int 246 247 on_demand_instance_type: 248 description: 249 - On-demand instance type that will be provisioned 250 type: str 251 252 opsworks: 253 description: 254 - The elastigroup OpsWorks integration configration.; 255 Expects the following key - 256 layer_id (String) 257 type: dict 258 259 persistence: 260 description: 261 - The Stateful elastigroup configration.; 262 Accepts the following keys - 263 should_persist_root_device (Boolean), 264 should_persist_block_devices (Boolean), 265 should_persist_private_ip (Boolean) 266 type: dict 267 268 product: 269 description: 270 - Operation system type. 271 - "Available choices are: C(Linux/UNIX), C(SUSE Linux), C(Windows), C(Linux/UNIX (Amazon VPC)), C(SUSE Linux (Amazon VPC))." 272 required: true 273 type: str 274 275 rancher: 276 description: 277 - The Rancher integration configuration.; 278 Expects the following keys - 279 version (String), 280 access_key (String), 281 secret_key (String), 282 master_host (String) 283 type: dict 284 285 right_scale: 286 description: 287 - The Rightscale integration configuration.; 288 Expects the following keys - 289 account_id (String), 290 refresh_token (String) 291 type: dict 292 293 risk: 294 description: 295 - Required if on demand is not set. The percentage of Spot instances to launch (0 - 100). 296 type: int 297 298 roll_config: 299 description: 300 - Roll configuration.; 301 If you would like the group to roll after updating, please use this feature. 302 Accepts the following keys - 303 batch_size_percentage(Integer, Required), 304 grace_period - (Integer, Required), 305 health_check_type(String, Optional) 306 type: dict 307 308 scheduled_tasks: 309 description: 310 - A list of hash/dictionaries of scheduled tasks to configure in the elastigroup; 311 '[{"key":"value", "key":"value"}]'; 312 keys allowed are - 313 adjustment (Integer), 314 scale_target_capacity (Integer), 315 scale_min_capacity (Integer), 316 scale_max_capacity (Integer), 317 adjustment_percentage (Integer), 318 batch_size_percentage (Integer), 319 cron_expression (String), 320 frequency (String), 321 grace_period (Integer), 322 task_type (String, required), 323 is_enabled (Boolean) 324 type: list 325 elements: dict 326 327 security_group_ids: 328 description: 329 - One or more security group IDs. ; 330 In case of update it will override the existing Security Group with the new given array 331 required: true 332 type: list 333 elements: str 334 335 shutdown_script: 336 description: 337 - The Base64-encoded shutdown script that executes prior to instance termination. 338 Encode before setting. 339 type: str 340 341 signals: 342 description: 343 - A list of hash/dictionaries of signals to configure in the elastigroup; 344 keys allowed are - 345 name (String, required), 346 timeout (Integer) 347 type: list 348 elements: dict 349 350 spin_up_time: 351 description: 352 - Spin up time, in seconds, for the instance 353 type: int 354 355 spot_instance_types: 356 description: 357 - Spot instance type that will be provisioned. 358 required: true 359 type: list 360 elements: str 361 362 state: 363 choices: 364 - present 365 - absent 366 description: 367 - Create or delete the elastigroup 368 default: present 369 type: str 370 371 tags: 372 description: 373 - A list of tags to configure in the elastigroup. Please specify list of keys and values (key colon value); 374 type: list 375 elements: dict 376 377 target: 378 description: 379 - The number of instances to launch 380 required: true 381 type: int 382 383 target_group_arns: 384 description: 385 - List of target group arns instances should be registered to 386 type: list 387 elements: str 388 389 tenancy: 390 description: 391 - Dedicated vs shared tenancy. 392 - "The available choices are: C(default), C(dedicated)." 393 type: str 394 395 terminate_at_end_of_billing_hour: 396 description: 397 - Terminate at the end of billing hour 398 type: bool 399 400 unit: 401 description: 402 - The capacity unit to launch instances by. 403 - "The available choices are: C(instance), C(weight)." 404 type: str 405 406 up_scaling_policies: 407 description: 408 - A list of hash/dictionaries of scaling policies to configure in the elastigroup; 409 '[{"key":"value", "key":"value"}]'; 410 keys allowed are - 411 policy_name (String, required), 412 namespace (String, required), 413 metric_name (String, required), 414 dimensions (List of Objects, Keys allowed are name (String, required) and value (String)), 415 statistic (String, required) 416 evaluation_periods (String, required), 417 period (String, required), 418 threshold (String, required), 419 cooldown (String, required), 420 unit (String, required), 421 operator (String, required), 422 action_type (String, required), 423 adjustment (String), 424 min_target_capacity (String), 425 target (String), 426 maximum (String), 427 minimum (String) 428 type: list 429 elements: dict 430 431 down_scaling_policies: 432 description: 433 - A list of hash/dictionaries of scaling policies to configure in the elastigroup; 434 '[{"key":"value", "key":"value"}]'; 435 keys allowed are - 436 policy_name (String, required), 437 namespace (String, required), 438 metric_name (String, required), 439 dimensions ((List of Objects), Keys allowed are name (String, required) and value (String)), 440 statistic (String, required), 441 evaluation_periods (String, required), 442 period (String, required), 443 threshold (String, required), 444 cooldown (String, required), 445 unit (String, required), 446 operator (String, required), 447 action_type (String, required), 448 adjustment (String), 449 max_target_capacity (String), 450 target (String), 451 maximum (String), 452 minimum (String) 453 type: list 454 elements: dict 455 456 target_tracking_policies: 457 description: 458 - A list of hash/dictionaries of target tracking policies to configure in the elastigroup; 459 '[{"key":"value", "key":"value"}]'; 460 keys allowed are - 461 policy_name (String, required), 462 namespace (String, required), 463 source (String, required), 464 metric_name (String, required), 465 statistic (String, required), 466 unit (String, required), 467 cooldown (String, required), 468 target (String, required) 469 type: list 470 elements: dict 471 472 uniqueness_by: 473 choices: 474 - id 475 - name 476 description: 477 - If your group names are not unique, you may use this feature to update or delete a specific group. 478 Whenever this property is set, you must set a group_id in order to update or delete a group, otherwise a group will be created. 479 default: name 480 type: str 481 482 user_data: 483 description: 484 - Base64-encoded MIME user data. Encode before setting the value. 485 type: str 486 487 utilize_reserved_instances: 488 description: 489 - In case of any available Reserved Instances, 490 Elastigroup will utilize your reservations before purchasing Spot instances. 491 type: bool 492 493 wait_for_instances: 494 description: 495 - Whether or not the elastigroup creation / update actions should wait for the instances to spin 496 type: bool 497 default: false 498 499 wait_timeout: 500 description: 501 - How long the module should wait for instances before failing the action.; 502 Only works if wait_for_instances is True. 503 type: int 504 505''' 506EXAMPLES = ''' 507# Basic configuration YAML example 508 509- hosts: localhost 510 tasks: 511 - name: Create elastigroup 512 community.general.spotinst_aws_elastigroup: 513 state: present 514 risk: 100 515 availability_vs_cost: balanced 516 availability_zones: 517 - name: us-west-2a 518 subnet_id: subnet-2b68a15c 519 image_id: ami-f173cc91 520 key_pair: spotinst-oregon 521 max_size: 15 522 min_size: 0 523 target: 0 524 unit: instance 525 monitoring: True 526 name: ansible-group 527 on_demand_instance_type: c3.large 528 product: Linux/UNIX 529 load_balancers: 530 - test-lb-1 531 security_group_ids: 532 - sg-8f4b8fe9 533 spot_instance_types: 534 - c3.large 535 do_not_update: 536 - image_id 537 - target 538 register: result 539 - ansible.builtin.debug: var=result 540 541# In this example, we create an elastigroup and wait 600 seconds to retrieve the instances, and use their private ips 542 543- hosts: localhost 544 tasks: 545 - name: Create elastigroup 546 community.general.spotinst_aws_elastigroup: 547 state: present 548 account_id: act-1a9dd2b 549 risk: 100 550 availability_vs_cost: balanced 551 availability_zones: 552 - name: us-west-2a 553 subnet_id: subnet-2b68a15c 554 tags: 555 - Environment: someEnvValue 556 - OtherTagKey: otherValue 557 image_id: ami-f173cc91 558 key_pair: spotinst-oregon 559 max_size: 5 560 min_size: 0 561 target: 0 562 unit: instance 563 monitoring: True 564 name: ansible-group-tal 565 on_demand_instance_type: c3.large 566 product: Linux/UNIX 567 security_group_ids: 568 - sg-8f4b8fe9 569 block_device_mappings: 570 - device_name: '/dev/sda1' 571 ebs: 572 volume_size: 100 573 volume_type: gp2 574 spot_instance_types: 575 - c3.large 576 do_not_update: 577 - image_id 578 wait_for_instances: True 579 wait_timeout: 600 580 register: result 581 582 - name: Store private ips to file 583 ansible.builtin.shell: echo {{ item.private_ip }}\\n >> list-of-private-ips 584 with_items: "{{ result.instances }}" 585 - ansible.builtin.debug: var=result 586 587# In this example, we create an elastigroup with multiple block device mappings, tags, and also an account id 588# In organizations with more than one account, it is required to specify an account_id 589 590- hosts: localhost 591 tasks: 592 - name: Create elastigroup 593 community.general.spotinst_aws_elastigroup: 594 state: present 595 account_id: act-1a9dd2b 596 risk: 100 597 availability_vs_cost: balanced 598 availability_zones: 599 - name: us-west-2a 600 subnet_id: subnet-2b68a15c 601 tags: 602 - Environment: someEnvValue 603 - OtherTagKey: otherValue 604 image_id: ami-f173cc91 605 key_pair: spotinst-oregon 606 max_size: 5 607 min_size: 0 608 target: 0 609 unit: instance 610 monitoring: True 611 name: ansible-group-tal 612 on_demand_instance_type: c3.large 613 product: Linux/UNIX 614 security_group_ids: 615 - sg-8f4b8fe9 616 block_device_mappings: 617 - device_name: '/dev/xvda' 618 ebs: 619 volume_size: 60 620 volume_type: gp2 621 - device_name: '/dev/xvdb' 622 ebs: 623 volume_size: 120 624 volume_type: gp2 625 spot_instance_types: 626 - c3.large 627 do_not_update: 628 - image_id 629 wait_for_instances: True 630 wait_timeout: 600 631 register: result 632 633 - name: Store private ips to file 634 ansible.builtin.shell: echo {{ item.private_ip }}\\n >> list-of-private-ips 635 with_items: "{{ result.instances }}" 636 - ansible.builtin.debug: var=result 637 638# In this example we have set up block device mapping with ephemeral devices 639 640- hosts: localhost 641 tasks: 642 - name: Create elastigroup 643 community.general.spotinst_aws_elastigroup: 644 state: present 645 risk: 100 646 availability_vs_cost: balanced 647 availability_zones: 648 - name: us-west-2a 649 subnet_id: subnet-2b68a15c 650 image_id: ami-f173cc91 651 key_pair: spotinst-oregon 652 max_size: 15 653 min_size: 0 654 target: 0 655 unit: instance 656 block_device_mappings: 657 - device_name: '/dev/xvda' 658 virtual_name: ephemeral0 659 - device_name: '/dev/xvdb/' 660 virtual_name: ephemeral1 661 monitoring: True 662 name: ansible-group 663 on_demand_instance_type: c3.large 664 product: Linux/UNIX 665 load_balancers: 666 - test-lb-1 667 security_group_ids: 668 - sg-8f4b8fe9 669 spot_instance_types: 670 - c3.large 671 do_not_update: 672 - image_id 673 - target 674 register: result 675 - ansible.builtin.debug: var=result 676 677# In this example we create a basic group configuration with a network interface defined. 678# Each network interface must have a device index 679 680- hosts: localhost 681 tasks: 682 - name: Create elastigroup 683 community.general.spotinst_aws_elastigroup: 684 state: present 685 risk: 100 686 availability_vs_cost: balanced 687 network_interfaces: 688 - associate_public_ip_address: true 689 device_index: 0 690 availability_zones: 691 - name: us-west-2a 692 subnet_id: subnet-2b68a15c 693 image_id: ami-f173cc91 694 key_pair: spotinst-oregon 695 max_size: 15 696 min_size: 0 697 target: 0 698 unit: instance 699 monitoring: True 700 name: ansible-group 701 on_demand_instance_type: c3.large 702 product: Linux/UNIX 703 load_balancers: 704 - test-lb-1 705 security_group_ids: 706 - sg-8f4b8fe9 707 spot_instance_types: 708 - c3.large 709 do_not_update: 710 - image_id 711 - target 712 register: result 713 - ansible.builtin.debug: var=result 714 715 716# In this example we create a basic group configuration with a target tracking scaling policy defined 717 718- hosts: localhost 719 tasks: 720 - name: Create elastigroup 721 community.general.spotinst_aws_elastigroup: 722 account_id: act-92d45673 723 state: present 724 risk: 100 725 availability_vs_cost: balanced 726 availability_zones: 727 - name: us-west-2a 728 subnet_id: subnet-79da021e 729 image_id: ami-f173cc91 730 fallback_to_od: true 731 tags: 732 - Creator: ValueOfCreatorTag 733 - Environment: ValueOfEnvironmentTag 734 key_pair: spotinst-labs-oregon 735 max_size: 10 736 min_size: 0 737 target: 2 738 unit: instance 739 monitoring: True 740 name: ansible-group-1 741 on_demand_instance_type: c3.large 742 product: Linux/UNIX 743 security_group_ids: 744 - sg-46cdc13d 745 spot_instance_types: 746 - c3.large 747 target_tracking_policies: 748 - policy_name: target-tracking-1 749 namespace: AWS/EC2 750 metric_name: CPUUtilization 751 statistic: average 752 unit: percent 753 target: 50 754 cooldown: 120 755 do_not_update: 756 - image_id 757 register: result 758 - ansible.builtin.debug: var=result 759''' 760 761RETURN = ''' 762--- 763instances: 764 description: List of active elastigroup instances and their details. 765 returned: success 766 type: dict 767 sample: [ 768 { 769 "spotInstanceRequestId": "sir-regs25zp", 770 "instanceId": "i-09640ad8678234c", 771 "instanceType": "m4.large", 772 "product": "Linux/UNIX", 773 "availabilityZone": "us-west-2b", 774 "privateIp": "180.0.2.244", 775 "createdAt": "2017-07-17T12:46:18.000Z", 776 "status": "fulfilled" 777 } 778 ] 779group_id: 780 description: Created / Updated group's ID. 781 returned: success 782 type: str 783 sample: "sig-12345" 784 785''' 786 787HAS_SPOTINST_SDK = False 788__metaclass__ = type 789 790import os 791import time 792from ansible.module_utils.basic import AnsibleModule 793 794try: 795 import spotinst_sdk as spotinst 796 from spotinst_sdk import SpotinstClientException 797 798 HAS_SPOTINST_SDK = True 799 800except ImportError: 801 pass 802 803eni_fields = ('description', 804 'device_index', 805 'secondary_private_ip_address_count', 806 'associate_public_ip_address', 807 'delete_on_termination', 808 'groups', 809 'network_interface_id', 810 'private_ip_address', 811 'subnet_id', 812 'associate_ipv6_address') 813 814private_ip_fields = ('private_ip_address', 815 'primary') 816 817capacity_fields = (dict(ansible_field_name='min_size', 818 spotinst_field_name='minimum'), 819 dict(ansible_field_name='max_size', 820 spotinst_field_name='maximum'), 821 'target', 822 'unit') 823 824lspec_fields = ('user_data', 825 'key_pair', 826 'tenancy', 827 'shutdown_script', 828 'monitoring', 829 'ebs_optimized', 830 'image_id', 831 'health_check_type', 832 'health_check_grace_period', 833 'health_check_unhealthy_duration_before_replacement', 834 'security_group_ids') 835 836iam_fields = (dict(ansible_field_name='iam_role_name', 837 spotinst_field_name='name'), 838 dict(ansible_field_name='iam_role_arn', 839 spotinst_field_name='arn')) 840 841scheduled_task_fields = ('adjustment', 842 'adjustment_percentage', 843 'batch_size_percentage', 844 'cron_expression', 845 'frequency', 846 'grace_period', 847 'task_type', 848 'is_enabled', 849 'scale_target_capacity', 850 'scale_min_capacity', 851 'scale_max_capacity') 852 853scaling_policy_fields = ('policy_name', 854 'namespace', 855 'metric_name', 856 'dimensions', 857 'statistic', 858 'evaluation_periods', 859 'period', 860 'threshold', 861 'cooldown', 862 'unit', 863 'operator') 864 865tracking_policy_fields = ('policy_name', 866 'namespace', 867 'source', 868 'metric_name', 869 'statistic', 870 'unit', 871 'cooldown', 872 'target', 873 'threshold') 874 875action_fields = (dict(ansible_field_name='action_type', 876 spotinst_field_name='type'), 877 'adjustment', 878 'min_target_capacity', 879 'max_target_capacity', 880 'target', 881 'minimum', 882 'maximum') 883 884signal_fields = ('name', 885 'timeout') 886 887multai_lb_fields = ('balancer_id', 888 'project_id', 889 'target_set_id', 890 'az_awareness', 891 'auto_weight') 892 893persistence_fields = ('should_persist_root_device', 894 'should_persist_block_devices', 895 'should_persist_private_ip') 896 897strategy_fields = ('risk', 898 'utilize_reserved_instances', 899 'fallback_to_od', 900 'on_demand_count', 901 'availability_vs_cost', 902 'draining_timeout', 903 'spin_up_time', 904 'lifetime_period') 905 906ebs_fields = ('delete_on_termination', 907 'encrypted', 908 'iops', 909 'snapshot_id', 910 'volume_type', 911 'volume_size') 912 913bdm_fields = ('device_name', 914 'virtual_name', 915 'no_device') 916 917kubernetes_fields = ('api_server', 918 'token') 919 920right_scale_fields = ('account_id', 921 'refresh_token') 922 923rancher_fields = ('access_key', 924 'secret_key', 925 'master_host', 926 'version') 927 928chef_fields = ('chef_server', 929 'organization', 930 'user', 931 'pem_key', 932 'chef_version') 933 934az_fields = ('name', 935 'subnet_id', 936 'placement_group_name') 937 938opsworks_fields = ('layer_id',) 939 940scaling_strategy_fields = ('terminate_at_end_of_billing_hour',) 941 942mesosphere_fields = ('api_server',) 943 944ecs_fields = ('cluster_name',) 945 946multai_fields = ('multai_token',) 947 948 949def handle_elastigroup(client, module): 950 has_changed = False 951 group_id = None 952 message = 'None' 953 954 name = module.params.get('name') 955 state = module.params.get('state') 956 uniqueness_by = module.params.get('uniqueness_by') 957 external_group_id = module.params.get('id') 958 959 if uniqueness_by == 'id': 960 if external_group_id is None: 961 should_create = True 962 else: 963 should_create = False 964 group_id = external_group_id 965 else: 966 groups = client.get_elastigroups() 967 should_create, group_id = find_group_with_same_name(groups, name) 968 969 if should_create is True: 970 if state == 'present': 971 eg = expand_elastigroup(module, is_update=False) 972 module.debug(str(" [INFO] " + message + "\n")) 973 group = client.create_elastigroup(group=eg) 974 group_id = group['id'] 975 message = 'Created group Successfully.' 976 has_changed = True 977 978 elif state == 'absent': 979 message = 'Cannot delete non-existent group.' 980 has_changed = False 981 else: 982 eg = expand_elastigroup(module, is_update=True) 983 984 if state == 'present': 985 group = client.update_elastigroup(group_update=eg, group_id=group_id) 986 message = 'Updated group successfully.' 987 988 try: 989 roll_config = module.params.get('roll_config') 990 if roll_config: 991 eg_roll = spotinst.aws_elastigroup.Roll( 992 batch_size_percentage=roll_config.get('batch_size_percentage'), 993 grace_period=roll_config.get('grace_period'), 994 health_check_type=roll_config.get('health_check_type') 995 ) 996 roll_response = client.roll_group(group_roll=eg_roll, group_id=group_id) 997 message = 'Updated and started rolling the group successfully.' 998 999 except SpotinstClientException as exc: 1000 message = 'Updated group successfully, but failed to perform roll. Error:' + str(exc) 1001 has_changed = True 1002 1003 elif state == 'absent': 1004 try: 1005 client.delete_elastigroup(group_id=group_id) 1006 except SpotinstClientException as exc: 1007 if "GROUP_DOESNT_EXIST" in exc.message: 1008 pass 1009 else: 1010 module.fail_json(msg="Error while attempting to delete group : " + exc.message) 1011 1012 message = 'Deleted group successfully.' 1013 has_changed = True 1014 1015 return group_id, message, has_changed 1016 1017 1018def retrieve_group_instances(client, module, group_id): 1019 wait_timeout = module.params.get('wait_timeout') 1020 wait_for_instances = module.params.get('wait_for_instances') 1021 1022 health_check_type = module.params.get('health_check_type') 1023 1024 if wait_timeout is None: 1025 wait_timeout = 300 1026 1027 wait_timeout = time.time() + wait_timeout 1028 target = module.params.get('target') 1029 state = module.params.get('state') 1030 instances = list() 1031 1032 if state == 'present' and group_id is not None and wait_for_instances is True: 1033 1034 is_amount_fulfilled = False 1035 while is_amount_fulfilled is False and wait_timeout > time.time(): 1036 instances = list() 1037 amount_of_fulfilled_instances = 0 1038 1039 if health_check_type is not None: 1040 healthy_instances = client.get_instance_healthiness(group_id=group_id) 1041 1042 for healthy_instance in healthy_instances: 1043 if healthy_instance.get('healthStatus') == 'HEALTHY': 1044 amount_of_fulfilled_instances += 1 1045 instances.append(healthy_instance) 1046 1047 else: 1048 active_instances = client.get_elastigroup_active_instances(group_id=group_id) 1049 1050 for active_instance in active_instances: 1051 if active_instance.get('private_ip') is not None: 1052 amount_of_fulfilled_instances += 1 1053 instances.append(active_instance) 1054 1055 if amount_of_fulfilled_instances >= target: 1056 is_amount_fulfilled = True 1057 1058 time.sleep(10) 1059 1060 return instances 1061 1062 1063def find_group_with_same_name(groups, name): 1064 for group in groups: 1065 if group['name'] == name: 1066 return False, group.get('id') 1067 1068 return True, None 1069 1070 1071def expand_elastigroup(module, is_update): 1072 do_not_update = module.params['do_not_update'] 1073 name = module.params.get('name') 1074 1075 eg = spotinst.aws_elastigroup.Elastigroup() 1076 description = module.params.get('description') 1077 1078 if name is not None: 1079 eg.name = name 1080 if description is not None: 1081 eg.description = description 1082 1083 # Capacity 1084 expand_capacity(eg, module, is_update, do_not_update) 1085 # Strategy 1086 expand_strategy(eg, module) 1087 # Scaling 1088 expand_scaling(eg, module) 1089 # Third party integrations 1090 expand_integrations(eg, module) 1091 # Compute 1092 expand_compute(eg, module, is_update, do_not_update) 1093 # Multai 1094 expand_multai(eg, module) 1095 # Scheduling 1096 expand_scheduled_tasks(eg, module) 1097 1098 return eg 1099 1100 1101def expand_compute(eg, module, is_update, do_not_update): 1102 elastic_ips = module.params['elastic_ips'] 1103 on_demand_instance_type = module.params.get('on_demand_instance_type') 1104 spot_instance_types = module.params['spot_instance_types'] 1105 ebs_volume_pool = module.params['ebs_volume_pool'] 1106 availability_zones_list = module.params['availability_zones'] 1107 product = module.params.get('product') 1108 1109 eg_compute = spotinst.aws_elastigroup.Compute() 1110 1111 if product is not None: 1112 # Only put product on group creation 1113 if is_update is not True: 1114 eg_compute.product = product 1115 1116 if elastic_ips is not None: 1117 eg_compute.elastic_ips = elastic_ips 1118 1119 if on_demand_instance_type or spot_instance_types is not None: 1120 eg_instance_types = spotinst.aws_elastigroup.InstanceTypes() 1121 1122 if on_demand_instance_type is not None: 1123 eg_instance_types.spot = spot_instance_types 1124 if spot_instance_types is not None: 1125 eg_instance_types.ondemand = on_demand_instance_type 1126 1127 if eg_instance_types.spot is not None or eg_instance_types.ondemand is not None: 1128 eg_compute.instance_types = eg_instance_types 1129 1130 expand_ebs_volume_pool(eg_compute, ebs_volume_pool) 1131 1132 eg_compute.availability_zones = expand_list(availability_zones_list, az_fields, 'AvailabilityZone') 1133 1134 expand_launch_spec(eg_compute, module, is_update, do_not_update) 1135 1136 eg.compute = eg_compute 1137 1138 1139def expand_ebs_volume_pool(eg_compute, ebs_volumes_list): 1140 if ebs_volumes_list is not None: 1141 eg_volumes = [] 1142 1143 for volume in ebs_volumes_list: 1144 eg_volume = spotinst.aws_elastigroup.EbsVolume() 1145 1146 if volume.get('device_name') is not None: 1147 eg_volume.device_name = volume.get('device_name') 1148 if volume.get('volume_ids') is not None: 1149 eg_volume.volume_ids = volume.get('volume_ids') 1150 1151 if eg_volume.device_name is not None: 1152 eg_volumes.append(eg_volume) 1153 1154 if len(eg_volumes) > 0: 1155 eg_compute.ebs_volume_pool = eg_volumes 1156 1157 1158def expand_launch_spec(eg_compute, module, is_update, do_not_update): 1159 eg_launch_spec = expand_fields(lspec_fields, module.params, 'LaunchSpecification') 1160 1161 if module.params['iam_role_arn'] is not None or module.params['iam_role_name'] is not None: 1162 eg_launch_spec.iam_role = expand_fields(iam_fields, module.params, 'IamRole') 1163 1164 tags = module.params['tags'] 1165 load_balancers = module.params['load_balancers'] 1166 target_group_arns = module.params['target_group_arns'] 1167 block_device_mappings = module.params['block_device_mappings'] 1168 network_interfaces = module.params['network_interfaces'] 1169 1170 if is_update is True: 1171 if 'image_id' in do_not_update: 1172 delattr(eg_launch_spec, 'image_id') 1173 1174 expand_tags(eg_launch_spec, tags) 1175 1176 expand_load_balancers(eg_launch_spec, load_balancers, target_group_arns) 1177 1178 expand_block_device_mappings(eg_launch_spec, block_device_mappings) 1179 1180 expand_network_interfaces(eg_launch_spec, network_interfaces) 1181 1182 eg_compute.launch_specification = eg_launch_spec 1183 1184 1185def expand_integrations(eg, module): 1186 rancher = module.params.get('rancher') 1187 mesosphere = module.params.get('mesosphere') 1188 ecs = module.params.get('ecs') 1189 kubernetes = module.params.get('kubernetes') 1190 right_scale = module.params.get('right_scale') 1191 opsworks = module.params.get('opsworks') 1192 chef = module.params.get('chef') 1193 1194 integration_exists = False 1195 1196 eg_integrations = spotinst.aws_elastigroup.ThirdPartyIntegrations() 1197 1198 if mesosphere is not None: 1199 eg_integrations.mesosphere = expand_fields(mesosphere_fields, mesosphere, 'Mesosphere') 1200 integration_exists = True 1201 1202 if ecs is not None: 1203 eg_integrations.ecs = expand_fields(ecs_fields, ecs, 'EcsConfiguration') 1204 integration_exists = True 1205 1206 if kubernetes is not None: 1207 eg_integrations.kubernetes = expand_fields(kubernetes_fields, kubernetes, 'KubernetesConfiguration') 1208 integration_exists = True 1209 1210 if right_scale is not None: 1211 eg_integrations.right_scale = expand_fields(right_scale_fields, right_scale, 'RightScaleConfiguration') 1212 integration_exists = True 1213 1214 if opsworks is not None: 1215 eg_integrations.opsworks = expand_fields(opsworks_fields, opsworks, 'OpsWorksConfiguration') 1216 integration_exists = True 1217 1218 if rancher is not None: 1219 eg_integrations.rancher = expand_fields(rancher_fields, rancher, 'Rancher') 1220 integration_exists = True 1221 1222 if chef is not None: 1223 eg_integrations.chef = expand_fields(chef_fields, chef, 'ChefConfiguration') 1224 integration_exists = True 1225 1226 if integration_exists: 1227 eg.third_parties_integration = eg_integrations 1228 1229 1230def expand_capacity(eg, module, is_update, do_not_update): 1231 eg_capacity = expand_fields(capacity_fields, module.params, 'Capacity') 1232 1233 if is_update is True: 1234 delattr(eg_capacity, 'unit') 1235 1236 if 'target' in do_not_update: 1237 delattr(eg_capacity, 'target') 1238 1239 eg.capacity = eg_capacity 1240 1241 1242def expand_strategy(eg, module): 1243 persistence = module.params.get('persistence') 1244 signals = module.params.get('signals') 1245 1246 eg_strategy = expand_fields(strategy_fields, module.params, 'Strategy') 1247 1248 terminate_at_end_of_billing_hour = module.params.get('terminate_at_end_of_billing_hour') 1249 1250 if terminate_at_end_of_billing_hour is not None: 1251 eg_strategy.eg_scaling_strategy = expand_fields(scaling_strategy_fields, 1252 module.params, 'ScalingStrategy') 1253 1254 if persistence is not None: 1255 eg_strategy.persistence = expand_fields(persistence_fields, persistence, 'Persistence') 1256 1257 if signals is not None: 1258 eg_signals = expand_list(signals, signal_fields, 'Signal') 1259 1260 if len(eg_signals) > 0: 1261 eg_strategy.signals = eg_signals 1262 1263 eg.strategy = eg_strategy 1264 1265 1266def expand_multai(eg, module): 1267 multai_load_balancers = module.params.get('multai_load_balancers') 1268 1269 eg_multai = expand_fields(multai_fields, module.params, 'Multai') 1270 1271 if multai_load_balancers is not None: 1272 eg_multai_load_balancers = expand_list(multai_load_balancers, multai_lb_fields, 'MultaiLoadBalancer') 1273 1274 if len(eg_multai_load_balancers) > 0: 1275 eg_multai.balancers = eg_multai_load_balancers 1276 eg.multai = eg_multai 1277 1278 1279def expand_scheduled_tasks(eg, module): 1280 scheduled_tasks = module.params.get('scheduled_tasks') 1281 1282 if scheduled_tasks is not None: 1283 eg_scheduling = spotinst.aws_elastigroup.Scheduling() 1284 1285 eg_tasks = expand_list(scheduled_tasks, scheduled_task_fields, 'ScheduledTask') 1286 1287 if len(eg_tasks) > 0: 1288 eg_scheduling.tasks = eg_tasks 1289 eg.scheduling = eg_scheduling 1290 1291 1292def expand_load_balancers(eg_launchspec, load_balancers, target_group_arns): 1293 if load_balancers is not None or target_group_arns is not None: 1294 eg_load_balancers_config = spotinst.aws_elastigroup.LoadBalancersConfig() 1295 eg_total_lbs = [] 1296 1297 if load_balancers is not None: 1298 for elb_name in load_balancers: 1299 eg_elb = spotinst.aws_elastigroup.LoadBalancer() 1300 if elb_name is not None: 1301 eg_elb.name = elb_name 1302 eg_elb.type = 'CLASSIC' 1303 eg_total_lbs.append(eg_elb) 1304 1305 if target_group_arns is not None: 1306 for target_arn in target_group_arns: 1307 eg_elb = spotinst.aws_elastigroup.LoadBalancer() 1308 if target_arn is not None: 1309 eg_elb.arn = target_arn 1310 eg_elb.type = 'TARGET_GROUP' 1311 eg_total_lbs.append(eg_elb) 1312 1313 if len(eg_total_lbs) > 0: 1314 eg_load_balancers_config.load_balancers = eg_total_lbs 1315 eg_launchspec.load_balancers_config = eg_load_balancers_config 1316 1317 1318def expand_tags(eg_launchspec, tags): 1319 if tags is not None: 1320 eg_tags = [] 1321 1322 for tag in tags: 1323 eg_tag = spotinst.aws_elastigroup.Tag() 1324 if tag: 1325 eg_tag.tag_key, eg_tag.tag_value = list(tag.items())[0] 1326 1327 eg_tags.append(eg_tag) 1328 1329 if len(eg_tags) > 0: 1330 eg_launchspec.tags = eg_tags 1331 1332 1333def expand_block_device_mappings(eg_launchspec, bdms): 1334 if bdms is not None: 1335 eg_bdms = [] 1336 1337 for bdm in bdms: 1338 eg_bdm = expand_fields(bdm_fields, bdm, 'BlockDeviceMapping') 1339 1340 if bdm.get('ebs') is not None: 1341 eg_bdm.ebs = expand_fields(ebs_fields, bdm.get('ebs'), 'EBS') 1342 1343 eg_bdms.append(eg_bdm) 1344 1345 if len(eg_bdms) > 0: 1346 eg_launchspec.block_device_mappings = eg_bdms 1347 1348 1349def expand_network_interfaces(eg_launchspec, enis): 1350 if enis is not None: 1351 eg_enis = [] 1352 1353 for eni in enis: 1354 eg_eni = expand_fields(eni_fields, eni, 'NetworkInterface') 1355 1356 eg_pias = expand_list(eni.get('private_ip_addresses'), private_ip_fields, 'PrivateIpAddress') 1357 1358 if eg_pias is not None: 1359 eg_eni.private_ip_addresses = eg_pias 1360 1361 eg_enis.append(eg_eni) 1362 1363 if len(eg_enis) > 0: 1364 eg_launchspec.network_interfaces = eg_enis 1365 1366 1367def expand_scaling(eg, module): 1368 up_scaling_policies = module.params['up_scaling_policies'] 1369 down_scaling_policies = module.params['down_scaling_policies'] 1370 target_tracking_policies = module.params['target_tracking_policies'] 1371 1372 eg_scaling = spotinst.aws_elastigroup.Scaling() 1373 1374 if up_scaling_policies is not None: 1375 eg_up_scaling_policies = expand_scaling_policies(up_scaling_policies) 1376 if len(eg_up_scaling_policies) > 0: 1377 eg_scaling.up = eg_up_scaling_policies 1378 1379 if down_scaling_policies is not None: 1380 eg_down_scaling_policies = expand_scaling_policies(down_scaling_policies) 1381 if len(eg_down_scaling_policies) > 0: 1382 eg_scaling.down = eg_down_scaling_policies 1383 1384 if target_tracking_policies is not None: 1385 eg_target_tracking_policies = expand_target_tracking_policies(target_tracking_policies) 1386 if len(eg_target_tracking_policies) > 0: 1387 eg_scaling.target = eg_target_tracking_policies 1388 1389 if eg_scaling.down is not None or eg_scaling.up is not None or eg_scaling.target is not None: 1390 eg.scaling = eg_scaling 1391 1392 1393def expand_list(items, fields, class_name): 1394 if items is not None: 1395 new_objects_list = [] 1396 for item in items: 1397 new_obj = expand_fields(fields, item, class_name) 1398 new_objects_list.append(new_obj) 1399 1400 return new_objects_list 1401 1402 1403def expand_fields(fields, item, class_name): 1404 class_ = getattr(spotinst.aws_elastigroup, class_name) 1405 new_obj = class_() 1406 1407 # Handle primitive fields 1408 if item is not None: 1409 for field in fields: 1410 if isinstance(field, dict): 1411 ansible_field_name = field['ansible_field_name'] 1412 spotinst_field_name = field['spotinst_field_name'] 1413 else: 1414 ansible_field_name = field 1415 spotinst_field_name = field 1416 if item.get(ansible_field_name) is not None: 1417 setattr(new_obj, spotinst_field_name, item.get(ansible_field_name)) 1418 1419 return new_obj 1420 1421 1422def expand_scaling_policies(scaling_policies): 1423 eg_scaling_policies = [] 1424 1425 for policy in scaling_policies: 1426 eg_policy = expand_fields(scaling_policy_fields, policy, 'ScalingPolicy') 1427 eg_policy.action = expand_fields(action_fields, policy, 'ScalingPolicyAction') 1428 eg_scaling_policies.append(eg_policy) 1429 1430 return eg_scaling_policies 1431 1432 1433def expand_target_tracking_policies(tracking_policies): 1434 eg_tracking_policies = [] 1435 1436 for policy in tracking_policies: 1437 eg_policy = expand_fields(tracking_policy_fields, policy, 'TargetTrackingPolicy') 1438 eg_tracking_policies.append(eg_policy) 1439 1440 return eg_tracking_policies 1441 1442 1443def main(): 1444 fields = dict( 1445 account_id=dict(type='str'), 1446 availability_vs_cost=dict(type='str', required=True), 1447 availability_zones=dict(type='list', elements='dict', required=True), 1448 block_device_mappings=dict(type='list', elements='dict'), 1449 chef=dict(type='dict'), 1450 credentials_path=dict(type='path', default="~/.spotinst/credentials"), 1451 do_not_update=dict(default=[], type='list'), 1452 down_scaling_policies=dict(type='list', elements='dict'), 1453 draining_timeout=dict(type='int'), 1454 ebs_optimized=dict(type='bool'), 1455 ebs_volume_pool=dict(type='list', elements='dict'), 1456 ecs=dict(type='dict'), 1457 elastic_beanstalk=dict(type='dict'), 1458 elastic_ips=dict(type='list', elements='str'), 1459 fallback_to_od=dict(type='bool'), 1460 id=dict(type='str'), 1461 health_check_grace_period=dict(type='int'), 1462 health_check_type=dict(type='str'), 1463 health_check_unhealthy_duration_before_replacement=dict(type='int'), 1464 iam_role_arn=dict(type='str'), 1465 iam_role_name=dict(type='str'), 1466 image_id=dict(type='str', required=True), 1467 key_pair=dict(type='str', no_log=False), 1468 kubernetes=dict(type='dict'), 1469 lifetime_period=dict(type='int'), 1470 load_balancers=dict(type='list', elements='str'), 1471 max_size=dict(type='int', required=True), 1472 mesosphere=dict(type='dict'), 1473 min_size=dict(type='int', required=True), 1474 monitoring=dict(type='str'), 1475 multai_load_balancers=dict(type='list'), 1476 multai_token=dict(type='str', no_log=True), 1477 name=dict(type='str', required=True), 1478 network_interfaces=dict(type='list', elements='dict'), 1479 on_demand_count=dict(type='int'), 1480 on_demand_instance_type=dict(type='str'), 1481 opsworks=dict(type='dict'), 1482 persistence=dict(type='dict'), 1483 product=dict(type='str', required=True), 1484 rancher=dict(type='dict'), 1485 right_scale=dict(type='dict'), 1486 risk=dict(type='int'), 1487 roll_config=dict(type='dict'), 1488 scheduled_tasks=dict(type='list', elements='dict'), 1489 security_group_ids=dict(type='list', elements='str', required=True), 1490 shutdown_script=dict(type='str'), 1491 signals=dict(type='list', elements='dict'), 1492 spin_up_time=dict(type='int'), 1493 spot_instance_types=dict(type='list', elements='str', required=True), 1494 state=dict(default='present', choices=['present', 'absent']), 1495 tags=dict(type='list', elements='dict'), 1496 target=dict(type='int', required=True), 1497 target_group_arns=dict(type='list', elements='str'), 1498 tenancy=dict(type='str'), 1499 terminate_at_end_of_billing_hour=dict(type='bool'), 1500 token=dict(type='str', no_log=True), 1501 unit=dict(type='str'), 1502 user_data=dict(type='str'), 1503 utilize_reserved_instances=dict(type='bool'), 1504 uniqueness_by=dict(default='name', choices=['name', 'id']), 1505 up_scaling_policies=dict(type='list', elements='dict'), 1506 target_tracking_policies=dict(type='list', elements='dict'), 1507 wait_for_instances=dict(type='bool', default=False), 1508 wait_timeout=dict(type='int') 1509 ) 1510 1511 module = AnsibleModule(argument_spec=fields) 1512 1513 if not HAS_SPOTINST_SDK: 1514 module.fail_json(msg="the Spotinst SDK library is required. (pip install spotinst_sdk)") 1515 1516 # Retrieve creds file variables 1517 creds_file_loaded_vars = dict() 1518 1519 credentials_path = module.params.get('credentials_path') 1520 1521 try: 1522 with open(credentials_path, "r") as creds: 1523 for line in creds: 1524 eq_index = line.find('=') 1525 var_name = line[:eq_index].strip() 1526 string_value = line[eq_index + 1:].strip() 1527 creds_file_loaded_vars[var_name] = string_value 1528 except IOError: 1529 pass 1530 # End of creds file retrieval 1531 1532 token = module.params.get('token') 1533 if not token: 1534 token = os.environ.get('SPOTINST_TOKEN') 1535 if not token: 1536 token = creds_file_loaded_vars.get("token") 1537 1538 account = module.params.get('account_id') 1539 if not account: 1540 account = os.environ.get('SPOTINST_ACCOUNT_ID') or os.environ.get('ACCOUNT') 1541 if not account: 1542 account = creds_file_loaded_vars.get("account") 1543 1544 client = spotinst.SpotinstClient(auth_token=token, print_output=False) 1545 1546 if account is not None: 1547 client = spotinst.SpotinstClient(auth_token=token, print_output=False, account_id=account) 1548 1549 group_id, message, has_changed = handle_elastigroup(client=client, module=module) 1550 1551 instances = retrieve_group_instances(client=client, module=module, group_id=group_id) 1552 1553 module.exit_json(changed=has_changed, group_id=group_id, message=message, instances=instances) 1554 1555 1556if __name__ == '__main__': 1557 main() 1558