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