text: #!/usr/bin/python
  # Copyright (c) 2017 Ansible Project
  # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
  from __future__ import (absolute_import, division, print_function)
  
  ANSIBLE_METADATA = {'metadata_version': '1.1',
                      'status': ['preview'],
                      'supported_by': 'community'}
  DOCUMENTATION = '''
  ---
  module: spotinst_aws_elastigroup
  short_description: Create, update or delete Spotinst AWS Elastigroups
  author: Spotinst (@talzur)
  description:
    - Can create, update, or delete Spotinst AWS Elastigroups
      Launch configuration is part of the elastigroup configuration,
      so no additional modules are necessary for handling the launch configuration.
      You will have to have a credentials file in this location - <home>/.spotinst/credentials
      The credentials file must contain a row that looks like this
      token = <YOUR TOKEN>
      Full documentation available at https://help.spotinst.com/hc/en-us/articles/115003530285-Ansible-
  requirements:
    - python >= 2.7
    - spotinst_sdk >= 1.0.38
  options:
  
    credentials_path:
      description:
        - (String) Optional parameter that allows to set a non-default credentials path.
         Default is ~/.spotinst/credentials
  
    account_id:
      description:
        - (String) Optional parameter that allows to set an account-id inside the module configuration
         By default this is retrieved from the credentials path
  
    availability_vs_cost:
      choices:
        - availabilityOriented
        - costOriented
        - balanced
      description:
        - (String) The strategy orientation.
      required: true
  
    availability_zones:
      description:
        - (List of Objects) a list of hash/dictionaries of Availability Zones that are configured in the elastigroup;
          '[{"key":"value", "key":"value"}]';
          keys allowed are
          name (String),
          subnet_id (String),
          placement_group_name (String),
      required: true
  
    block_device_mappings:
      description:
        - (List of Objects) a list of hash/dictionaries of Block Device Mappings for elastigroup instances;
          You can specify virtual devices and EBS volumes.;
          '[{"key":"value", "key":"value"}]';
          keys allowed are
          device_name (List of Strings),
          virtual_name (String),
          no_device (String),
          ebs (Object, expects the following keys-
          delete_on_termination(Boolean),
          encrypted(Boolean),
          iops (Integer),
          snapshot_id(Integer),
          volume_type(String),
          volume_size(Integer))
  
    chef:
      description:
        - (Object) The Chef integration configuration.;
          Expects the following keys - chef_server (String),
          organization (String),
          user (String),
          pem_key (String),
          chef_version (String)
  
    draining_timeout:
      description:
        - (Integer) Time for instance to be drained from incoming requests and deregistered from ELB before termination.
  
    ebs_optimized:
      description:
        - (Boolean) Enable EBS optimization for supported instances which are not enabled by default.;
          Note - additional charges will be applied.
      type: bool
  
    ebs_volume_pool:
      description:
        - (List of Objects) a list of hash/dictionaries of EBS devices to reattach to the elastigroup when available;
          '[{"key":"value", "key":"value"}]';
          keys allowed are -
          volume_ids (List of Strings),
          device_name (String)
  
    ecs:
      description:
        - (Object) The ECS integration configuration.;
          Expects the following key -
          cluster_name (String)
  
  
    elastic_ips:
      description:
        - (List of Strings) List of ElasticIps Allocation Ids (Example C(eipalloc-9d4e16f8)) to associate to the group instances
  
    fallback_to_od:
      description:
        - (Boolean) In case of no spots available, Elastigroup will launch an On-demand instance instead
      type: bool
    health_check_grace_period:
      description:
        - (Integer) The amount of time, in seconds, after the instance has launched to start and check its health.
      default: 300
  
    health_check_unhealthy_duration_before_replacement:
      description:
        - (Integer) Minimal mount of time instance should be unhealthy for us to consider it unhealthy.
  
    health_check_type:
      choices:
        - ELB
        - HCS
        - TARGET_GROUP
        - MLB
        - EC2
      description:
        - (String) The service to use for the health check.
  
    iam_role_name:
      description:
        - (String) The instance profile iamRole name
        - Only use iam_role_arn, or iam_role_name
  
    iam_role_arn:
      description:
        - (String) The instance profile iamRole arn
        - Only use iam_role_arn, or iam_role_name
  
    id:
      description:
        - (String) The group id if it already exists and you want to update, or delete it.
          This will not work unless the uniqueness_by field is set to id.
          When this is set, and the uniqueness_by field is set, the group will either be updated or deleted, but not created.
  
    ignore_changes:
      choices:
        - image_id
        - target
      description:
        - (List of Strings) list of fields on which changes should be ignored when updating
  
    image_id:
      description:
        - (String) The image Id used to launch the instance.;
          In case of conflict between Instance type and image type, an error will be returned
      required: true
  
    key_pair:
      description:
        - (String) Specify a Key Pair to attach to the instances
      required: true
  
    kubernetes:
      description:
        - (Object) The Kubernetes integration configuration.
          Expects the following keys -
          api_server (String),
          token (String)
  
    lifetime_period:
      description:
        - (String) lifetime period
  
    load_balancers:
      description:
        - (List of Strings) List of classic ELB names
  
    max_size:
      description:
        - (Integer) The upper limit number of instances that you can scale up to
      required: true
  
    mesosphere:
      description:
        - (Object) The Mesosphere integration configuration.
          Expects the following key -
          api_server (String)
  
    min_size:
      description:
        - (Integer) The lower limit number of instances that you can scale down to
      required: true
  
    monitoring:
      description:
        - (Boolean) Describes whether instance Enhanced Monitoring is enabled
      required: true
  
    name:
      description:
        - (String) Unique name for elastigroup to be created, updated or deleted
      required: true
  
    network_interfaces:
      description:
        - (List of Objects) a list of hash/dictionaries of network interfaces to add to the elastigroup;
          '[{"key":"value", "key":"value"}]';
          keys allowed are -
          description (String),
          device_index (Integer),
          secondary_private_ip_address_count (Integer),
          associate_public_ip_address (Boolean),
          delete_on_termination (Boolean),
          groups (List of Strings),
          network_interface_id (String),
          private_ip_address (String),
          subnet_id (String),
          associate_ipv6_address (Boolean),
          private_ip_addresses (List of Objects, Keys are privateIpAddress (String, required) and primary (Boolean))
  
    on_demand_count:
      description:
        - (Integer) Required if risk is not set
        - Number of on demand instances to launch. All other instances will be spot instances.;
          Either set this parameter or the risk parameter
  
    on_demand_instance_type:
      description:
        - (String) On-demand instance type that will be provisioned
      required: true
  
    opsworks:
      description:
        - (Object) The elastigroup OpsWorks integration configration.;
          Expects the following key -
          layer_id (String)
  
    persistence:
      description:
        - (Object) The Stateful elastigroup configration.;
          Accepts the following keys -
          should_persist_root_device (Boolean),
          should_persist_block_devices (Boolean),
          should_persist_private_ip (Boolean)
  
    product:
      choices:
        - Linux/UNIX
        - SUSE Linux
        - Windows
        - Linux/UNIX (Amazon VPC)
        - SUSE Linux (Amazon VPC)
        - Windows
      description:
        - (String) Operation system type._
      required: true
  
    rancher:
      description:
        - (Object) The Rancher integration configuration.;
          Expects the following keys -
          version (String),
          access_key (String),
          secret_key (String),
          master_host (String)
  
    right_scale:
      description:
        - (Object) The Rightscale integration configuration.;
          Expects the following keys -
          account_id (String),
          refresh_token (String)
  
    risk:
      description:
        - (Integer) required if on demand is not set. The percentage of Spot instances to launch (0 - 100).
  
    roll_config:
      description:
        - (Object) Roll configuration.;
          If you would like the group to roll after updating, please use this feature.
          Accepts the following keys -
          batch_size_percentage(Integer, Required),
          grace_period - (Integer, Required),
          health_check_type(String, Optional)
  
    scheduled_tasks:
      description:
        - (List of Objects) a list of hash/dictionaries of scheduled tasks to configure in the elastigroup;
          '[{"key":"value", "key":"value"}]';
          keys allowed are -
          adjustment (Integer),
          scale_target_capacity (Integer),
          scale_min_capacity (Integer),
          scale_max_capacity (Integer),
          adjustment_percentage (Integer),
          batch_size_percentage (Integer),
          cron_expression (String),
          frequency (String),
          grace_period (Integer),
          task_type (String, required),
          is_enabled (Boolean)
  
    security_group_ids:
      description:
        - (List of Strings) One or more security group IDs. ;
          In case of update it will override the existing Security Group with the new given array
      required: true
  
    shutdown_script:
      description:
        - (String) The Base64-encoded shutdown script that executes prior to instance termination.
          Encode before setting.
  
    signals:
      description:
        - (List of Objects) a list of hash/dictionaries of signals to configure in the elastigroup;
          keys allowed are -
          name (String, required),
          timeout (Integer)
  
    spin_up_time:
      description:
        - (Integer) spin up time, in seconds, for the instance
  
    spot_instance_types:
      description:
        - (List of Strings) Spot instance type that will be provisioned.
      required: true
  
    state:
      choices:
        - present
        - absent
      description:
        - (String) create or delete the elastigroup
  
    tags:
      description:
        - (List of tagKey:tagValue paris) a list of tags to configure in the elastigroup. Please specify list of keys and values (key colon value);
  
    target:
      description:
        - (Integer) The number of instances to launch
      required: true
  
    target_group_arns:
      description:
        - (List of Strings) List of target group arns instances should be registered to
  
    tenancy:
      choices:
        - default
        - dedicated
      description:
        - (String) dedicated vs shared tenancy
  
    terminate_at_end_of_billing_hour:
      description:
        - (Boolean) terminate at the end of billing hour
      type: bool
    unit:
      choices:
        - instance
        - weight
      description:
        - (String) The capacity unit to launch instances by.
      required: true
  
    up_scaling_policies:
      description:
        - (List of Objects) a list of hash/dictionaries of scaling policies to configure in the elastigroup;
          '[{"key":"value", "key":"value"}]';
          keys allowed are -
          policy_name (String, required),
          namespace (String, required),
          metric_name (String, required),
          dimensions (List of Objects, Keys allowed are name (String, required) and value (String)),
          statistic (String, required)
          evaluation_periods (String, required),
          period (String, required),
          threshold (String, required),
          cooldown (String, required),
          unit (String, required),
          operator (String, required),
          action_type (String, required),
          adjustment (String),
          min_target_capacity (String),
          target (String),
          maximum (String),
          minimum (String)
  
  
    down_scaling_policies:
      description:
        - (List of Objects) a list of hash/dictionaries of scaling policies to configure in the elastigroup;
          '[{"key":"value", "key":"value"}]';
          keys allowed are -
          policy_name (String, required),
          namespace (String, required),
          metric_name (String, required),
          dimensions ((List of Objects), Keys allowed are name (String, required) and value (String)),
          statistic (String, required),
          evaluation_periods (String, required),
          period (String, required),
          threshold (String, required),
          cooldown (String, required),
          unit (String, required),
          operator (String, required),
          action_type (String, required),
          adjustment (String),
          max_target_capacity (String),
          target (String),
          maximum (String),
          minimum (String)
  
    target_tracking_policies:
      description:
        - (List of Objects) a list of hash/dictionaries of target tracking policies to configure in the elastigroup;
          '[{"key":"value", "key":"value"}]';
          keys allowed are -
          policy_name (String, required),
          namespace (String, required),
          source (String, required),
          metric_name (String, required),
          statistic (String, required),
          unit (String, required),
          cooldown (String, required),
          target (String, required)
  
    uniqueness_by:
      choices:
        - id
        - name
      description:
        - (String) If your group names are not unique, you may use this feature to update or delete a specific group.
          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.
  
  
    user_data:
      description:
        - (String) Base64-encoded MIME user data. Encode before setting the value.
  
  
    utilize_reserved_instances:
      description:
        - (Boolean) In case of any available Reserved Instances,
           Elastigroup will utilize your reservations before purchasing Spot instances.
      type: bool
  
    wait_for_instances:
      description:
        - (Boolean) Whether or not the elastigroup creation / update actions should wait for the instances to spin
      type: bool
  
    wait_timeout:
      description:
        - (Integer) How long the module should wait for instances before failing the action.;
          Only works if wait_for_instances is True.
  
  '''
  EXAMPLES = '''
  # Basic configuration YAML example
  
  - hosts: localhost
    tasks:
      - name: create elastigroup
        spotinst_aws_elastigroup:
            state: present
            risk: 100
            availability_vs_cost: balanced
            availability_zones:
              - name: us-west-2a
                subnet_id: subnet-2b68a15c
            image_id: ami-f173cc91
            key_pair: spotinst-oregon
            max_size: 15
            min_size: 0
            target: 0
            unit: instance
            monitoring: True
            name: ansible-group
            on_demand_instance_type: c3.large
            product: Linux/UNIX
            load_balancers:
              - test-lb-1
            security_group_ids:
              - sg-8f4b8fe9
            spot_instance_types:
              - c3.large
            do_not_update:
              - image_id
              - target
        register: result
      - debug: var=result
  
  # In this example, we create an elastigroup and wait 600 seconds to retrieve the instances, and use their private ips
  
  - hosts: localhost
    tasks:
      - name: create elastigroup
        spotinst_aws_elastigroup:
            state: present
            account_id: act-1a9dd2b
            risk: 100
            availability_vs_cost: balanced
            availability_zones:
              - name: us-west-2a
                subnet_id: subnet-2b68a15c
            tags:
              - Environment: someEnvValue
              - OtherTagKey: otherValue
            image_id: ami-f173cc91
            key_pair: spotinst-oregon
            max_size: 5
            min_size: 0
            target: 0
            unit: instance
            monitoring: True
            name: ansible-group-tal
            on_demand_instance_type: c3.large
            product: Linux/UNIX
            security_group_ids:
              - sg-8f4b8fe9
            block_device_mappings:
              - device_name: '/dev/sda1'
                ebs:
                  volume_size: 100
                  volume_type: gp2
            spot_instance_types:
              - c3.large
            do_not_update:
              - image_id
            wait_for_instances: True
            wait_timeout: 600
        register: result
  
      - name: Store private ips to file
        shell: echo {{ item.private_ip }}\\n >> list-of-private-ips
        with_items: "{{ result.instances }}"
      - debug: var=result
  
  # In this example, we create an elastigroup with multiple block device mappings, tags, and also an account id
  # In organizations with more than one account, it is required to specify an account_id
  
  - hosts: localhost
    tasks:
      - name: create elastigroup
        spotinst_aws_elastigroup:
            state: present
            account_id: act-1a9dd2b
            risk: 100
            availability_vs_cost: balanced
            availability_zones:
              - name: us-west-2a
                subnet_id: subnet-2b68a15c
            tags:
              - Environment: someEnvValue
              - OtherTagKey: otherValue
            image_id: ami-f173cc91
            key_pair: spotinst-oregon
            max_size: 5
            min_size: 0
            target: 0
            unit: instance
            monitoring: True
            name: ansible-group-tal
            on_demand_instance_type: c3.large
            product: Linux/UNIX
            security_group_ids:
              - sg-8f4b8fe9
            block_device_mappings:
              - device_name: '/dev/xvda'
                ebs:
                  volume_size: 60
                  volume_type: gp2
              - device_name: '/dev/xvdb'
                ebs:
                  volume_size: 120
                  volume_type: gp2
            spot_instance_types:
              - c3.large
            do_not_update:
              - image_id
            wait_for_instances: True
            wait_timeout: 600
        register: result
  
      - name: Store private ips to file
        shell: echo {{ item.private_ip }}\\n >> list-of-private-ips
        with_items: "{{ result.instances }}"
      - debug: var=result
  
  # In this example we have set up block device mapping with ephemeral devices
  
  - hosts: localhost
    tasks:
      - name: create elastigroup
        spotinst_aws_elastigroup:
            state: present
            risk: 100
            availability_vs_cost: balanced
            availability_zones:
              - name: us-west-2a
                subnet_id: subnet-2b68a15c
            image_id: ami-f173cc91
            key_pair: spotinst-oregon
            max_size: 15
            min_size: 0
            target: 0
            unit: instance
            block_device_mappings:
              - device_name: '/dev/xvda'
                virtual_name: ephemeral0
              - device_name: '/dev/xvdb/'
                virtual_name: ephemeral1
            monitoring: True
            name: ansible-group
            on_demand_instance_type: c3.large
            product: Linux/UNIX
            load_balancers:
              - test-lb-1
            security_group_ids:
              - sg-8f4b8fe9
            spot_instance_types:
              - c3.large
            do_not_update:
              - image_id
              - target
        register: result
      - debug: var=result
  
  # In this example we create a basic group configuration with a network interface defined.
  # Each network interface must have a device index
  
  - hosts: localhost
    tasks:
      - name: create elastigroup
        spotinst_aws_elastigroup:
            state: present
            risk: 100
            availability_vs_cost: balanced
            network_interfaces:
              - associate_public_ip_address: true
                device_index: 0
            availability_zones:
              - name: us-west-2a
                subnet_id: subnet-2b68a15c
            image_id: ami-f173cc91
            key_pair: spotinst-oregon
            max_size: 15
            min_size: 0
            target: 0
            unit: instance
            monitoring: True
            name: ansible-group
            on_demand_instance_type: c3.large
            product: Linux/UNIX
            load_balancers:
              - test-lb-1
            security_group_ids:
              - sg-8f4b8fe9
            spot_instance_types:
              - c3.large
            do_not_update:
              - image_id
              - target
        register: result
      - debug: var=result
  
  
  # In this example we create a basic group configuration with a target tracking scaling policy defined
  
  - hosts: localhost
    tasks:
      - name: create elastigroup
        spotinst_aws_elastigroup:
            account_id: act-92d45673
            state: present
            risk: 100
            availability_vs_cost: balanced
            availability_zones:
              - name: us-west-2a
                subnet_id: subnet-79da021e
            image_id: ami-f173cc91
            fallback_to_od: true
            tags:
              - Creator: ValueOfCreatorTag
              - Environment: ValueOfEnvironmentTag
            key_pair: spotinst-labs-oregon
            max_size: 10
            min_size: 0
            target: 2
            unit: instance
            monitoring: True
            name: ansible-group-1
            on_demand_instance_type: c3.large
            product: Linux/UNIX
            security_group_ids:
              - sg-46cdc13d
            spot_instance_types:
              - c3.large
            target_tracking_policies:
              - policy_name: target-tracking-1
                namespace: AWS/EC2
                metric_name: CPUUtilization
                statistic: average
                unit: percent
                target: 50
                cooldown: 120
            do_not_update:
              - image_id
        register: result
      - debug: var=result
  
  '''
  RETURN = '''
  ---
  instances:
      description: List of active elastigroup instances and their details.
      returned: success
      type: dict
      sample: [
           {
              "spotInstanceRequestId": "sir-regs25zp",
              "instanceId": "i-09640ad8678234c",
              "instanceType": "m4.large",
              "product": "Linux/UNIX",
              "availabilityZone": "us-west-2b",
              "privateIp": "180.0.2.244",
              "createdAt": "2017-07-17T12:46:18.000Z",
              "status": "fulfilled"
          }
      ]
  group_id:
      description: Created / Updated group's ID.
      returned: success
      type: str
      sample: "sig-12345"
  
  '''
  
  HAS_SPOTINST_SDK = False
  __metaclass__ = type
  
  import os
  import time
  from ansible.module_utils.basic import AnsibleModule
  
  try:
      import spotinst_sdk as spotinst
      from spotinst_sdk import SpotinstClientException
  
      HAS_SPOTINST_SDK = True
  
  except ImportError:
      pass
  
  eni_fields = ('description',
                'device_index',
                'secondary_private_ip_address_count',
                'associate_public_ip_address',
                'delete_on_termination',
                'groups',
                'network_interface_id',
                'private_ip_address',
                'subnet_id',
                'associate_ipv6_address')
  
  private_ip_fields = ('private_ip_address',
                       'primary')
  
  capacity_fields = (dict(ansible_field_name='min_size',
                          spotinst_field_name='minimum'),
                     dict(ansible_field_name='max_size',
                          spotinst_field_name='maximum'),
                     'target',
                     'unit')
  
  lspec_fields = ('user_data',
                  'key_pair',
                  'tenancy',
                  'shutdown_script',
                  'monitoring',
                  'ebs_optimized',
                  'image_id',
                  'health_check_type',
                  'health_check_grace_period',
                  'health_check_unhealthy_duration_before_replacement',
                  'security_group_ids')
  
  iam_fields = (dict(ansible_field_name='iam_role_name',
                     spotinst_field_name='name'),
                dict(ansible_field_name='iam_role_arn',
                     spotinst_field_name='arn'))
  
  scheduled_task_fields = ('adjustment',
                           'adjustment_percentage',
                           'batch_size_percentage',
                           'cron_expression',
                           'frequency',
                           'grace_period',
                           'task_type',
                           'is_enabled',
                           'scale_target_capacity',
                           'scale_min_capacity',
                           'scale_max_capacity')
  
  scaling_policy_fields = ('policy_name',
                           'namespace',
                           'metric_name',
                           'dimensions',
                           'statistic',
                           'evaluation_periods',
                           'period',
                           'threshold',
                           'cooldown',
                           'unit',
                           'operator')
  
  tracking_policy_fields = ('policy_name',
                            'namespace',
                            'source',
                            'metric_name',
                            'statistic',
                            'unit',
                            'cooldown',
                            'target',
                            'threshold')
  
  action_fields = (dict(ansible_field_name='action_type',
                        spotinst_field_name='type'),
                   'adjustment',
                   'min_target_capacity',
                   'max_target_capacity',
                   'target',
                   'minimum',
                   'maximum')
  
  signal_fields = ('name',
                   'timeout')
  
  multai_lb_fields = ('balancer_id',
                      'project_id',
                      'target_set_id',
                      'az_awareness',
                      'auto_weight')
  
  persistence_fields = ('should_persist_root_device',
                        'should_persist_block_devices',
                        'should_persist_private_ip')
  
  strategy_fields = ('risk',
                     'utilize_reserved_instances',
                     'fallback_to_od',
                     'on_demand_count',
                     'availability_vs_cost',
                     'draining_timeout',
                     'spin_up_time',
                     'lifetime_period')
  
  ebs_fields = ('delete_on_termination',
                'encrypted',
                'iops',
                'snapshot_id',
                'volume_type',
                'volume_size')
  
  bdm_fields = ('device_name',
                'virtual_name',
                'no_device')
  
  kubernetes_fields = ('api_server',
                       'token')
  
  right_scale_fields = ('account_id',
                        'refresh_token')
  
  rancher_fields = ('access_key',
                    'secret_key',
                    'master_host',
                    'version')
  
  chef_fields = ('chef_server',
                 'organization',
                 'user',
                 'pem_key',
                 'chef_version')
  
  az_fields = ('name',
               'subnet_id',
               'placement_group_name')
  
  opsworks_fields = ('layer_id',)
  
  scaling_strategy_fields = ('terminate_at_end_of_billing_hour',)
  
  mesosphere_fields = ('api_server',)
  
  ecs_fields = ('cluster_name',)
  
  multai_fields = ('multai_token',)
  
  
  def handle_elastigroup(client, module):
      has_changed = False
      should_create = False
      group_id = None
      message = 'None'
  
      name = module.params.get('name')
      state = module.params.get('state')
      uniqueness_by = module.params.get('uniqueness_by')
      external_group_id = module.params.get('id')
  
      if uniqueness_by == 'id':
          if external_group_id is None:
              should_create = True
          else:
              should_create = False
              group_id = external_group_id
      else:
          groups = client.get_elastigroups()
          should_create, group_id = find_group_with_same_name(groups, name)
  
      if should_create is True:
          if state == 'present':
              eg = expand_elastigroup(module, is_update=False)
              module.debug(str(" [INFO] " + message + "\n"))
              group = client.create_elastigroup(group=eg)
              group_id = group['id']
              message = 'Created group Successfully.'
              has_changed = True
  
          elif state == 'absent':
              message = 'Cannot delete non-existent group.'
              has_changed = False
      else:
          eg = expand_elastigroup(module, is_update=True)
  
          if state == 'present':
              group = client.update_elastigroup(group_update=eg, group_id=group_id)
              message = 'Updated group successfully.'
  
              try:
                  roll_config = module.params.get('roll_config')
                  if roll_config:
                      eg_roll = spotinst.aws_elastigroup.Roll(
                          batch_size_percentage=roll_config.get('batch_size_percentage'),
                          grace_period=roll_config.get('grace_period'),
                          health_check_type=roll_config.get('health_check_type')
                      )
                      roll_response = client.roll_group(group_roll=eg_roll, group_id=group_id)
                      message = 'Updated and started rolling the group successfully.'
  
              except SpotinstClientException as exc:
                  message = 'Updated group successfully, but failed to perform roll. Error:' + str(exc)
              has_changed = True
  
          elif state == 'absent':
              try:
                  client.delete_elastigroup(group_id=group_id)
              except SpotinstClientException as exc:
                  if "GROUP_DOESNT_EXIST" in exc.message:
                      pass
                  else:
                      module.fail_json(msg="Error while attempting to delete group : " + exc.message)
  
              message = 'Deleted group successfully.'
              has_changed = True
  
      return group_id, message, has_changed
  
  
  def retrieve_group_instances(client, module, group_id):
      wait_timeout = module.params.get('wait_timeout')
      wait_for_instances = module.params.get('wait_for_instances')
  
      health_check_type = module.params.get('health_check_type')
  
      if wait_timeout is None:
          wait_timeout = 300
  
      wait_timeout = time.time() + wait_timeout
      target = module.params.get('target')
      state = module.params.get('state')
      instances = list()
  
      if state == 'present' and group_id is not None and wait_for_instances is True:
  
          is_amount_fulfilled = False
          while is_amount_fulfilled is False and wait_timeout > time.time():
              instances = list()
              amount_of_fulfilled_instances = 0
  
              if health_check_type is not None:
                  healthy_instances = client.get_instance_healthiness(group_id=group_id)
  
                  for healthy_instance in healthy_instances:
                      if(healthy_instance.get('healthStatus') == 'HEALTHY'):
                          amount_of_fulfilled_instances += 1
                          instances.append(healthy_instance)
  
              else:
                  active_instances = client.get_elastigroup_active_instances(group_id=group_id)
  
                  for active_instance in active_instances:
                      if active_instance.get('private_ip') is not None:
                          amount_of_fulfilled_instances += 1
                          instances.append(active_instance)
  
              if amount_of_fulfilled_instances >= target:
                  is_amount_fulfilled = True
  
              time.sleep(10)
  
      return instances
  
  
  def find_group_with_same_name(groups, name):
      for group in groups:
          if group['name'] == name:
              return False, group.get('id')
  
      return True, None
  
  
  def expand_elastigroup(module, is_update):
      do_not_update = module.params['do_not_update']
      name = module.params.get('name')
  
      eg = spotinst.aws_elastigroup.Elastigroup()
      description = module.params.get('description')
  
      if name is not None:
          eg.name = name
      if description is not None:
          eg.description = description
  
      # Capacity
      expand_capacity(eg, module, is_update, do_not_update)
      # Strategy
      expand_strategy(eg, module)
      # Scaling
      expand_scaling(eg, module)
      # Third party integrations
      expand_integrations(eg, module)
      # Compute
      expand_compute(eg, module, is_update, do_not_update)
      # Multai
      expand_multai(eg, module)
      # Scheduling
      expand_scheduled_tasks(eg, module)
  
      return eg
  
  
  def expand_compute(eg, module, is_update, do_not_update):
      elastic_ips = module.params['elastic_ips']
      on_demand_instance_type = module.params.get('on_demand_instance_type')
      spot_instance_types = module.params['spot_instance_types']
      ebs_volume_pool = module.params['ebs_volume_pool']
      availability_zones_list = module.params['availability_zones']
      product = module.params.get('product')
  
      eg_compute = spotinst.aws_elastigroup.Compute()
  
      if product is not None:
          # Only put product on group creation
          if is_update is not True:
              eg_compute.product = product
  
      if elastic_ips is not None:
          eg_compute.elastic_ips = elastic_ips
  
      if on_demand_instance_type or spot_instance_types is not None:
          eg_instance_types = spotinst.aws_elastigroup.InstanceTypes()
  
          if on_demand_instance_type is not None:
              eg_instance_types.spot = spot_instance_types
          if spot_instance_types is not None:
              eg_instance_types.ondemand = on_demand_instance_type
  
          if eg_instance_types.spot is not None or eg_instance_types.ondemand is not None:
              eg_compute.instance_types = eg_instance_types
  
      expand_ebs_volume_pool(eg_compute, ebs_volume_pool)
  
      eg_compute.availability_zones = expand_list(availability_zones_list, az_fields, 'AvailabilityZone')
  
      expand_launch_spec(eg_compute, module, is_update, do_not_update)
  
      eg.compute = eg_compute
  
  
  def expand_ebs_volume_pool(eg_compute, ebs_volumes_list):
      if ebs_volumes_list is not None:
          eg_volumes = []
  
          for volume in ebs_volumes_list:
              eg_volume = spotinst.aws_elastigroup.EbsVolume()
  
              if volume.get('device_name') is not None:
                  eg_volume.device_name = volume.get('device_name')
              if volume.get('volume_ids') is not None:
                  eg_volume.volume_ids = volume.get('volume_ids')
  
              if eg_volume.device_name is not None:
                  eg_volumes.append(eg_volume)
  
          if len(eg_volumes) > 0:
              eg_compute.ebs_volume_pool = eg_volumes
  
  
  def expand_launch_spec(eg_compute, module, is_update, do_not_update):
      eg_launch_spec = expand_fields(lspec_fields, module.params, 'LaunchSpecification')
  
      if module.params['iam_role_arn'] is not None or module.params['iam_role_name'] is not None:
          eg_launch_spec.iam_role = expand_fields(iam_fields, module.params, 'IamRole')
  
      tags = module.params['tags']
      load_balancers = module.params['load_balancers']
      target_group_arns = module.params['target_group_arns']
      block_device_mappings = module.params['block_device_mappings']
      network_interfaces = module.params['network_interfaces']
  
      if is_update is True:
          if 'image_id' in do_not_update:
              delattr(eg_launch_spec, 'image_id')
  
      expand_tags(eg_launch_spec, tags)
  
      expand_load_balancers(eg_launch_spec, load_balancers, target_group_arns)
  
      expand_block_device_mappings(eg_launch_spec, block_device_mappings)
  
      expand_network_interfaces(eg_launch_spec, network_interfaces)
  
      eg_compute.launch_specification = eg_launch_spec
  
  
  def expand_integrations(eg, module):
      rancher = module.params.get('rancher')
      mesosphere = module.params.get('mesosphere')
      ecs = module.params.get('ecs')
      kubernetes = module.params.get('kubernetes')
      right_scale = module.params.get('right_scale')
      opsworks = module.params.get('opsworks')
      chef = module.params.get('chef')
  
      integration_exists = False
  
      eg_integrations = spotinst.aws_elastigroup.ThirdPartyIntegrations()
  
      if mesosphere is not None:
          eg_integrations.mesosphere = expand_fields(mesosphere_fields, mesosphere, 'Mesosphere')
          integration_exists = True
  
      if ecs is not None:
          eg_integrations.ecs = expand_fields(ecs_fields, ecs, 'EcsConfiguration')
          integration_exists = True
  
      if kubernetes is not None:
          eg_integrations.kubernetes = expand_fields(kubernetes_fields, kubernetes, 'KubernetesConfiguration')
          integration_exists = True
  
      if right_scale is not None:
          eg_integrations.right_scale = expand_fields(right_scale_fields, right_scale, 'RightScaleConfiguration')
          integration_exists = True
  
      if opsworks is not None:
          eg_integrations.opsworks = expand_fields(opsworks_fields, opsworks, 'OpsWorksConfiguration')
          integration_exists = True
  
      if rancher is not None:
          eg_integrations.rancher = expand_fields(rancher_fields, rancher, 'Rancher')
          integration_exists = True
  
      if chef is not None:
          eg_integrations.chef = expand_fields(chef_fields, chef, 'ChefConfiguration')
          integration_exists = True
  
      if integration_exists:
          eg.third_parties_integration = eg_integrations
  
  
  def expand_capacity(eg, module, is_update, do_not_update):
      eg_capacity = expand_fields(capacity_fields, module.params, 'Capacity')
  
      if is_update is True:
          delattr(eg_capacity, 'unit')
  
          if 'target' in do_not_update:
              delattr(eg_capacity, 'target')
  
      eg.capacity = eg_capacity
  
  
  def expand_strategy(eg, module):
      persistence = module.params.get('persistence')
      signals = module.params.get('signals')
  
      eg_strategy = expand_fields(strategy_fields, module.params, 'Strategy')
  
      terminate_at_end_of_billing_hour = module.params.get('terminate_at_end_of_billing_hour')
  
      if terminate_at_end_of_billing_hour is not None:
          eg_strategy.eg_scaling_strategy = expand_fields(scaling_strategy_fields,
                                                          module.params, 'ScalingStrategy')
  
      if persistence is not None:
          eg_strategy.persistence = expand_fields(persistence_fields, persistence, 'Persistence')
  
      if signals is not None:
          eg_signals = expand_list(signals, signal_fields, 'Signal')
  
          if len(eg_signals) > 0:
              eg_strategy.signals = eg_signals
  
      eg.strategy = eg_strategy
  
  
  def expand_multai(eg, module):
      multai_load_balancers = module.params.get('multai_load_balancers')
  
      eg_multai = expand_fields(multai_fields, module.params, 'Multai')
  
      if multai_load_balancers is not None:
          eg_multai_load_balancers = expand_list(multai_load_balancers, multai_lb_fields, 'MultaiLoadBalancer')
  
          if len(eg_multai_load_balancers) > 0:
              eg_multai.balancers = eg_multai_load_balancers
              eg.multai = eg_multai
  
  
  def expand_scheduled_tasks(eg, module):
      scheduled_tasks = module.params.get('scheduled_tasks')
  
      if scheduled_tasks is not None:
          eg_scheduling = spotinst.aws_elastigroup.Scheduling()
  
          eg_tasks = expand_list(scheduled_tasks, scheduled_task_fields, 'ScheduledTask')
  
          if len(eg_tasks) > 0:
              eg_scheduling.tasks = eg_tasks
              eg.scheduling = eg_scheduling
  
  
  def expand_load_balancers(eg_launchspec, load_balancers, target_group_arns):
      if load_balancers is not None or target_group_arns is not None:
          eg_load_balancers_config = spotinst.aws_elastigroup.LoadBalancersConfig()
          eg_total_lbs = []
  
          if load_balancers is not None:
              for elb_name in load_balancers:
                  eg_elb = spotinst.aws_elastigroup.LoadBalancer()
                  if elb_name is not None:
                      eg_elb.name = elb_name
                      eg_elb.type = 'CLASSIC'
                      eg_total_lbs.append(eg_elb)
  
          if target_group_arns is not None:
              for target_arn in target_group_arns:
                  eg_elb = spotinst.aws_elastigroup.LoadBalancer()
                  if target_arn is not None:
                      eg_elb.arn = target_arn
                      eg_elb.type = 'TARGET_GROUP'
                      eg_total_lbs.append(eg_elb)
  
          if len(eg_total_lbs) > 0:
              eg_load_balancers_config.load_balancers = eg_total_lbs
              eg_launchspec.load_balancers_config = eg_load_balancers_config
  
  
  def expand_tags(eg_launchspec, tags):
      if tags is not None:
          eg_tags = []
  
          for tag in tags:
              eg_tag = spotinst.aws_elastigroup.Tag()
              if tag.keys():
                  eg_tag.tag_key = tag.keys()[0]
              if tag.values():
                  eg_tag.tag_value = tag.values()[0]
  
              eg_tags.append(eg_tag)
  
          if len(eg_tags) > 0:
              eg_launchspec.tags = eg_tags
  
  
  def expand_block_device_mappings(eg_launchspec, bdms):
      if bdms is not None:
          eg_bdms = []
  
          for bdm in bdms:
              eg_bdm = expand_fields(bdm_fields, bdm, 'BlockDeviceMapping')
  
              if bdm.get('ebs') is not None:
                  eg_bdm.ebs = expand_fields(ebs_fields, bdm.get('ebs'), 'EBS')
  
              eg_bdms.append(eg_bdm)
  
          if len(eg_bdms) > 0:
              eg_launchspec.block_device_mappings = eg_bdms
  
  
  def expand_network_interfaces(eg_launchspec, enis):
      if enis is not None:
          eg_enis = []
  
          for eni in enis:
              eg_eni = expand_fields(eni_fields, eni, 'NetworkInterface')
  
              eg_pias = expand_list(eni.get('private_ip_addresses'), private_ip_fields, 'PrivateIpAddress')
  
              if eg_pias is not None:
                  eg_eni.private_ip_addresses = eg_pias
  
              eg_enis.append(eg_eni)
  
          if len(eg_enis) > 0:
              eg_launchspec.network_interfaces = eg_enis
  
  
  def expand_scaling(eg, module):
      up_scaling_policies = module.params['up_scaling_policies']
      down_scaling_policies = module.params['down_scaling_policies']
      target_tracking_policies = module.params['target_tracking_policies']
  
      eg_scaling = spotinst.aws_elastigroup.Scaling()
  
      if up_scaling_policies is not None:
          eg_up_scaling_policies = expand_scaling_policies(up_scaling_policies)
          if len(eg_up_scaling_policies) > 0:
              eg_scaling.up = eg_up_scaling_policies
  
      if down_scaling_policies is not None:
          eg_down_scaling_policies = expand_scaling_policies(down_scaling_policies)
          if len(eg_down_scaling_policies) > 0:
              eg_scaling.down = eg_down_scaling_policies
  
      if target_tracking_policies is not None:
          eg_target_tracking_policies = expand_target_tracking_policies(target_tracking_policies)
          if len(eg_target_tracking_policies) > 0:
              eg_scaling.target = eg_target_tracking_policies
  
      if eg_scaling.down is not None or eg_scaling.up is not None or eg_scaling.target is not None:
          eg.scaling = eg_scaling
  
  
  def expand_list(items, fields, class_name):
      if items is not None:
          new_objects_list = []
          for item in items:
              new_obj = expand_fields(fields, item, class_name)
              new_objects_list.append(new_obj)
  
          return new_objects_list
  
  
  def expand_fields(fields, item, class_name):
      class_ = getattr(spotinst.aws_elastigroup, class_name)
      new_obj = class_()
  
      # Handle primitive fields
      if item is not None:
          for field in fields:
              if isinstance(field, dict):
                  ansible_field_name = field['ansible_field_name']
                  spotinst_field_name = field['spotinst_field_name']
              else:
                  ansible_field_name = field
                  spotinst_field_name = field
              if item.get(ansible_field_name) is not None:
                  setattr(new_obj, spotinst_field_name, item.get(ansible_field_name))
  
      return new_obj
  
  
  def expand_scaling_policies(scaling_policies):
      eg_scaling_policies = []
  
      for policy in scaling_policies:
          eg_policy = expand_fields(scaling_policy_fields, policy, 'ScalingPolicy')
          eg_policy.action = expand_fields(action_fields, policy, 'ScalingPolicyAction')
          eg_scaling_policies.append(eg_policy)
  
      return eg_scaling_policies
  
  
  def expand_target_tracking_policies(tracking_policies):
      eg_tracking_policies = []
  
      for policy in tracking_policies:
          eg_policy = expand_fields(tracking_policy_fields, policy, 'TargetTrackingPolicy')
          eg_tracking_policies.append(eg_policy)
  
      return eg_tracking_policies
  
  
  def main():
      fields = dict(
          account_id=dict(type='str'),
          availability_vs_cost=dict(type='str', required=True),
          availability_zones=dict(type='list', required=True),
          block_device_mappings=dict(type='list'),
          chef=dict(type='dict'),
          credentials_path=dict(type='path', default="~/.spotinst/credentials"),
          do_not_update=dict(default=[], type='list'),
          down_scaling_policies=dict(type='list'),
          draining_timeout=dict(type='int'),
          ebs_optimized=dict(type='bool'),
          ebs_volume_pool=dict(type='list'),
          ecs=dict(type='dict'),
          elastic_beanstalk=dict(type='dict'),
          elastic_ips=dict(type='list'),
          fallback_to_od=dict(type='bool'),
          id=dict(type='str'),
          health_check_grace_period=dict(type='int'),
          health_check_type=dict(type='str'),
          health_check_unhealthy_duration_before_replacement=dict(type='int'),
          iam_role_arn=dict(type='str'),
          iam_role_name=dict(type='str'),
          image_id=dict(type='str', required=True),
          key_pair=dict(type='str'),
          kubernetes=dict(type='dict'),
          lifetime_period=dict(type='int'),
          load_balancers=dict(type='list'),
          max_size=dict(type='int', required=True),
          mesosphere=dict(type='dict'),
          min_size=dict(type='int', required=True),
          monitoring=dict(type='str'),
          multai_load_balancers=dict(type='list'),
          multai_token=dict(type='str'),
          name=dict(type='str', required=True),
          network_interfaces=dict(type='list'),
          on_demand_count=dict(type='int'),
          on_demand_instance_type=dict(type='str'),
          opsworks=dict(type='dict'),
          persistence=dict(type='dict'),
          product=dict(type='str', required=True),
          rancher=dict(type='dict'),
          right_scale=dict(type='dict'),
          risk=dict(type='int'),
          roll_config=dict(type='dict'),
          scheduled_tasks=dict(type='list'),
          security_group_ids=dict(type='list', required=True),
          shutdown_script=dict(type='str'),
          signals=dict(type='list'),
          spin_up_time=dict(type='int'),
          spot_instance_types=dict(type='list', required=True),
          state=dict(default='present', choices=['present', 'absent']),
          tags=dict(type='list'),
          target=dict(type='int', required=True),
          target_group_arns=dict(type='list'),
          tenancy=dict(type='str'),
          terminate_at_end_of_billing_hour=dict(type='bool'),
          token=dict(type='str'),
          unit=dict(type='str'),
          user_data=dict(type='str'),
          utilize_reserved_instances=dict(type='bool'),
          uniqueness_by=dict(default='name', choices=['name', 'id']),
          up_scaling_policies=dict(type='list'),
          target_tracking_policies=dict(type='list'),
          wait_for_instances=dict(type='bool', default=False),
          wait_timeout=dict(type='int')
      )
  
      module = AnsibleModule(argument_spec=fields)
  
      if not HAS_SPOTINST_SDK:
          module.fail_json(msg="the Spotinst SDK library is required. (pip install spotinst_sdk)")
  
      # Retrieve creds file variables
      creds_file_loaded_vars = dict()
  
      credentials_path = module.params.get('credentials_path')
  
      try:
          with open(credentials_path, "r") as creds:
              for line in creds:
                  eq_index = line.find('=')
                  var_name = line[:eq_index].strip()
                  string_value = line[eq_index + 1:].strip()
                  creds_file_loaded_vars[var_name] = string_value
      except IOError:
          pass
      # End of creds file retrieval
  
      token = module.params.get('token')
      if not token:
          token = os.environ.get('SPOTINST_TOKEN')
      if not token:
          token = creds_file_loaded_vars.get("token")
  
      account = module.params.get('account_id')
      if not account:
          account = os.environ.get('SPOTINST_ACCOUNT_ID') or os.environ.get('ACCOUNT')
      if not account:
          account = creds_file_loaded_vars.get("account")
  
      client = spotinst.SpotinstClient(auth_token=token, print_output=False)
  
      if account is not None:
          client = spotinst.SpotinstClient(auth_token=token, print_output=False, account_id=account)
  
      group_id, message, has_changed = handle_elastigroup(client=client, module=module)
  
      instances = retrieve_group_instances(client=client, module=module, group_id=group_id)
  
      module.exit_json(changed=has_changed, group_id=group_id, message=message, instances=instances)
  
  
  if __name__ == '__main__':
      main()
  