docs: def instance_present(name, instance_name=None, instance_id=None, image_id=None,
                       image_name=None, tags=None, key_name=None,
                       security_groups=None, user_data=None, instance_type=None,
                       placement=None, kernel_id=None, ramdisk_id=None,
                       vpc_id=None, vpc_name=None, monitoring_enabled=None,
                       subnet_id=None, subnet_name=None, private_ip_address=None,
                       block_device_map=None, disable_api_termination=None,
                       instance_initiated_shutdown_behavior=None,
                       placement_group=None, client_token=None,
                       security_group_ids=None, security_group_names=None,
                       additional_info=None, tenancy=None,
                       instance_profile_arn=None, instance_profile_name=None,
                       ebs_optimized=None, network_interfaces=None,
                       network_interface_name=None,
                       network_interface_id=None,
                       attributes=None, target_state=None, public_ip=None,
                       allocation_id=None, allocate_eip=False, region=None,
                       key=None, keyid=None, profile=None):
      ### TODO - implement 'target_state={running, stopped}'
      '''
      Ensure an EC2 instance is running with the given attributes and state.
  
      name
          (string) - The name of the state definition.  Recommended that this
          match the instance_name attribute (generally the FQDN of the instance).
      instance_name
          (string) - The name of the instance, generally its FQDN.  Exclusive with
          'instance_id'.
      instance_id
          (string) - The ID of the instance (if known).  Exclusive with
          'instance_name'.
      image_id
          (string) – The ID of the AMI image to run.
      image_name
          (string) – The name of the AMI image to run.
      tags
          (dict) - Tags to apply to the instance.
      key_name
          (string) – The name of the key pair with which to launch instances.
      security_groups
          (list of strings) – The names of the EC2 classic security groups with
          which to associate instances
      user_data
          (string) – The Base64-encoded MIME user data to be made available to the
          instance(s) in this reservation.
      instance_type
          (string) – The EC2 instance size/type.  Note that only certain types are
          compatible with HVM based AMIs.
      placement
          (string) – The Availability Zone to launch the instance into.
      kernel_id
          (string) – The ID of the kernel with which to launch the instances.
      ramdisk_id
          (string) – The ID of the RAM disk with which to launch the instances.
      vpc_id
          (string) - The ID of a VPC to attach the instance to.
      vpc_name
          (string) - The name of a VPC to attach the instance to.
      monitoring_enabled
          (bool) – Enable detailed CloudWatch monitoring on the instance.
      subnet_id
          (string) – The ID of the subnet within which to launch the instances for
          VPC.
      subnet_name
          (string) – The name of the subnet within which to launch the instances
          for VPC.
      private_ip_address
          (string) – If you’re using VPC, you can optionally use this parameter to
          assign the instance a specific available IP address from the subnet
          (e.g., 10.0.0.25).
      block_device_map
          (boto.ec2.blockdevicemapping.BlockDeviceMapping) – A BlockDeviceMapping
          data structure describing the EBS volumes associated with the Image.
      disable_api_termination
          (bool) – If True, the instances will be locked and will not be able to
          be terminated via the API.
      instance_initiated_shutdown_behavior
          (string) – Specifies whether the instance stops or terminates on
          instance-initiated shutdown. Valid values are:
  
          - 'stop'
          - 'terminate'
  
      placement_group
          (string) – If specified, this is the name of the placement group in
          which the instance(s) will be launched.
      client_token
          (string) – Unique, case-sensitive identifier you provide to ensure
          idempotency of the request. Maximum 64 ASCII characters.
      security_group_ids
          (list of strings) – The IDs of the VPC security groups with which to
          associate instances.
      security_group_names
          (list of strings) – The names of the VPC security groups with which to
          associate instances.
      additional_info
          (string) – Specifies additional information to make available to the
          instance(s).
      tenancy
          (string) – The tenancy of the instance you want to launch. An instance
          with a tenancy of ‘dedicated’ runs on single-tenant hardware and can
          only be launched into a VPC. Valid values are:”default” or “dedicated”.
          NOTE: To use dedicated tenancy you MUST specify a VPC subnet-ID as well.
      instance_profile_arn
          (string) – The Amazon resource name (ARN) of the IAM Instance Profile
          (IIP) to associate with the instances.
      instance_profile_name
          (string) – The name of the IAM Instance Profile (IIP) to associate with
          the instances.
      ebs_optimized
          (bool) – Whether the instance is optimized for EBS I/O. This
          optimization provides dedicated throughput to Amazon EBS and a tuned
          configuration stack to provide optimal EBS I/O performance. This
          optimization isn’t available with all instance types.
      network_interfaces
          (boto.ec2.networkinterface.NetworkInterfaceCollection) – A
          NetworkInterfaceCollection data structure containing the ENI
          specifications for the instance.
      network_interface_name
           (string) - The name of Elastic Network Interface to attach
  
          .. versionadded:: 2016.11.0
  
      network_interface_id
           (string) - The id of Elastic Network Interface to attach
  
          .. versionadded:: 2016.11.0
  
      attributes
          (dict) - Instance attributes and value to be applied to the instance.
          Available options are:
  
          - instanceType - A valid instance type (m1.small)
          - kernel - Kernel ID (None)
          - ramdisk - Ramdisk ID (None)
          - userData - Base64 encoded String (None)
          - disableApiTermination - Boolean (true)
          - instanceInitiatedShutdownBehavior - stop|terminate
          - blockDeviceMapping - List of strings - ie: [‘/dev/sda=false’]
          - sourceDestCheck - Boolean (true)
          - groupSet - Set of Security Groups or IDs
          - ebsOptimized - Boolean (false)
          - sriovNetSupport - String - ie: ‘simple’
  
      target_state
          (string) - The desired target state of the instance.  Available options
          are:
  
          - running
          - stopped
  
          Note that this option is currently UNIMPLEMENTED.
      public_ip:
          (string) - The IP of a previously allocated EIP address, which will be
          attached to the instance.  EC2 Classic instances ONLY - for VCP pass in
          an allocation_id instead.
      allocation_id:
          (string) - The ID of a previously allocated EIP address, which will be
          attached to the instance.  VPC instances ONLY - for Classic pass in
          a public_ip instead.
      allocate_eip:
          (bool) - Allocate and attach an EIP on-the-fly for this instance.  Note
          you'll want to releaase this address when terminating the instance,
          either manually or via the 'release_eip' flag to 'instance_absent'.
      region
          (string) - Region to connect to.
      key
          (string) - Secret key to be used.
      keyid
          (string) - Access key to be used.
      profile
          (variable) - A dict with region, key and keyid, or a pillar key (string)
          that contains a dict with region, key and keyid.
  
      .. versionadded:: 2016.3.0
      '''
      ret = {'name': name,
             'result': True,
             'comment': '',
             'changes': {}
            }
      _create = False
      running_states = ('pending', 'rebooting', 'running', 'stopping', 'stopped')
      changed_attrs = {}
  
      if not salt.utils.data.exactly_one((image_id, image_name)):
          raise SaltInvocationError('Exactly one of image_id OR '
                                    'image_name must be provided.')
      if (public_ip or allocation_id or allocate_eip) and not salt.utils.data.exactly_one((public_ip, allocation_id, allocate_eip)):
          raise SaltInvocationError('At most one of public_ip, allocation_id OR '
                                    'allocate_eip may be provided.')
  
      if instance_id:
          exists = __salt__['boto_ec2.exists'](instance_id=instance_id, region=region, key=key,
                                               keyid=keyid, profile=profile, in_states=running_states)
          if not exists:
              _create = True
      else:
          instances = __salt__['boto_ec2.find_instances'](name=instance_name if instance_name else name,
                                                          region=region, key=key, keyid=keyid, profile=profile,
                                                          in_states=running_states)
          if not instances:
              _create = True
          elif len(instances) > 1:
              log.debug('Multiple instances matching criteria found - cannot determine a singular instance-id')
              instance_id = None  # No way to know, we'll just have to bail later....
          else:
              instance_id = instances[0]
  
      if _create:
          if __opts__['test']:
              ret['comment'] = 'The instance {0} is set to be created.'.format(name)
              ret['result'] = None
              return ret
          if image_name:
              args = {'ami_name': image_name, 'region': region, 'key': key,
                      'keyid': keyid, 'profile': profile}
              image_ids = __salt__['boto_ec2.find_images'](**args)
              if image_ids:
                  image_id = image_ids[0]
              else:
                  image_id = image_name
          r = __salt__['boto_ec2.run'](image_id, instance_name if instance_name else name,
                                       tags=tags, key_name=key_name,
                                       security_groups=security_groups, user_data=user_data,
                                       instance_type=instance_type, placement=placement,
                                       kernel_id=kernel_id, ramdisk_id=ramdisk_id, vpc_id=vpc_id,
                                       vpc_name=vpc_name, monitoring_enabled=monitoring_enabled,
                                       subnet_id=subnet_id, subnet_name=subnet_name,
                                       private_ip_address=private_ip_address,
                                       block_device_map=block_device_map,
                                       disable_api_termination=disable_api_termination,
                                       instance_initiated_shutdown_behavior=instance_initiated_shutdown_behavior,
                                       placement_group=placement_group, client_token=client_token,
                                       security_group_ids=security_group_ids,
                                       security_group_names=security_group_names,
                                       additional_info=additional_info, tenancy=tenancy,
                                       instance_profile_arn=instance_profile_arn,
                                       instance_profile_name=instance_profile_name,
                                       ebs_optimized=ebs_optimized, network_interfaces=network_interfaces,
                                       network_interface_name=network_interface_name,
                                       network_interface_id=network_interface_id,
                                       region=region, key=key, keyid=keyid, profile=profile)
          if not r or 'instance_id' not in r:
              ret['result'] = False
              ret['comment'] = 'Failed to create instance {0}.'.format(instance_name if instance_name else name)
              return ret
  
          instance_id = r['instance_id']
          ret['changes'] = {'old': {}, 'new': {}}
          ret['changes']['old']['instance_id'] = None
          ret['changes']['new']['instance_id'] = instance_id
  
          # To avoid issues we only allocate new EIPs at instance creation.
          # This might miss situations where an instance is initially created
          # created without and one is added later, but the alternative is the
          # risk of EIPs allocated at every state run.
          if allocate_eip:
              if __opts__['test']:
                  ret['comment'] = 'New EIP would be allocated.'
                  ret['result'] = None
                  return ret
              domain = 'vpc' if vpc_id or vpc_name else None
              r = __salt__['boto_ec2.allocate_eip_address'](
                      domain=domain, region=region, key=key, keyid=keyid,
                      profile=profile)
              if not r:
                  ret['result'] = False
                  ret['comment'] = 'Failed to allocate new EIP.'
                  return ret
              allocation_id = r['allocation_id']
              log.info("New EIP with address %s allocated.", r['public_ip'])
          else:
              log.info("EIP not requested.")
  
      if public_ip or allocation_id:
          # This can take a bit to show up, give it a chance to...
          tries = 10
          secs = 3
          for t in range(tries):
              r = __salt__['boto_ec2.get_eip_address_info'](
                      addresses=public_ip, allocation_ids=allocation_id,
                      region=region, key=key, keyid=keyid, profile=profile)
              if r:
                  break
              else:
                  log.info(
                      'Waiting up to %s secs for new EIP %s to become available',
                      tries * secs, public_ip or allocation_id
                  )
                  time.sleep(secs)
          if not r:
              ret['result'] = False
              ret['comment'] = 'Failed to lookup EIP {0}.'.format(public_ip or allocation_id)
              return ret
          ip = r[0]['public_ip']
          if r[0].get('instance_id'):
              if r[0]['instance_id'] != instance_id:
                  ret['result'] = False
                  ret['comment'] = ('EIP {0} is already associated with instance '
                                    '{1}.'.format(public_ip if public_ip else
                                    allocation_id, r[0]['instance_id']))
                  return ret
          else:
              if __opts__['test']:
                  ret['comment'] = 'Instance {0} to be updated.'.format(name)
                  ret['result'] = None
                  return ret
              r = __salt__['boto_ec2.associate_eip_address'](
                      instance_id=instance_id, public_ip=public_ip,
                      allocation_id=allocation_id, region=region, key=key,
                      keyid=keyid, profile=profile)
              if r:
                  if 'new' not in ret['changes']:
                      ret['changes']['new'] = {}
                  ret['changes']['new']['public_ip'] = ip
              else:
                  ret['result'] = False
                  ret['comment'] = 'Failed to attach EIP to instance {0}.'.format(
                          instance_name if instance_name else name)
                  return ret
  
      if attributes:
          for k, v in six.iteritems(attributes):
              curr = __salt__['boto_ec2.get_attribute'](k, instance_id=instance_id, region=region, key=key,
                                                        keyid=keyid, profile=profile)
              curr = {} if not isinstance(curr, dict) else curr
              if curr.get(k) == v:
                  continue
              else:
                  if __opts__['test']:
                      changed_attrs[k] = 'The instance attribute {0} is set to be changed from \'{1}\' to \'{2}\'.'.format(
                                         k, curr.get(k), v)
                      continue
                  try:
                      r = __salt__['boto_ec2.set_attribute'](attribute=k, attribute_value=v,
                                                             instance_id=instance_id, region=region,
                                                             key=key, keyid=keyid, profile=profile)
                  except SaltInvocationError as e:
                      ret['result'] = False
                      ret['comment'] = 'Failed to set attribute {0} to {1} on instance {2}.'.format(k, v, instance_name)
                      return ret
                  ret['changes'] = ret['changes'] if ret['changes'] else {'old': {}, 'new': {}}
                  ret['changes']['old'][k] = curr.get(k)
                  ret['changes']['new'][k] = v
  
      if __opts__['test']:
          if changed_attrs:
              ret['changes']['new'] = changed_attrs
              ret['result'] = None
          else:
              ret['comment'] = 'Instance {0} is in the correct state'.format(instance_name if instance_name else name)
              ret['result'] = True
  
      if tags and instance_id is not None:
          tags = dict(tags)
          curr_tags = dict(__salt__['boto_ec2.get_all_tags'](filters={'resource-id': instance_id},
                  region=region, key=key, keyid=keyid, profile=profile).get(instance_id, {}))
          current = set(curr_tags.keys())
          desired = set(tags.keys())
          remove = list(current - desired)  # Boto explicitly requires a list here and can't cope with a set...
          add = dict([(t, tags[t]) for t in desired - current])
          replace = dict([(t, tags[t]) for t in tags if tags.get(t) != curr_tags.get(t)])
          # Tag keys are unique despite the bizarre semantics uses which make it LOOK like they could be duplicative.
          add.update(replace)
          if add or remove:
              if __opts__['test']:
                  ret['changes']['old'] = ret['changes']['old'] if 'old' in ret['changes'] else {}
                  ret['changes']['new'] = ret['changes']['new'] if 'new' in ret['changes'] else {}
                  ret['changes']['old']['tags'] = curr_tags
                  ret['changes']['new']['tags'] = tags
                  ret['comment'] += '  Tags would be updated on instance {0}.'.format(instance_name if
                                                                                      instance_name else name)
              else:
                  if remove:
                      if not __salt__['boto_ec2.delete_tags'](resource_ids=instance_id, tags=remove,
                                                              region=region, key=key, keyid=keyid,
                                                              profile=profile):
                          msg = "Error while deleting tags on instance {0}".format(instance_name if
                                                                                  instance_name else name)
                          log.error(msg)
                          ret['comment'] += '  ' + msg
                          ret['result'] = False
                          return ret
                  if add:
                      if not __salt__['boto_ec2.create_tags'](resource_ids=instance_id, tags=add,
                                                              region=region, key=key, keyid=keyid,
                                                              profile=profile):
                          msg = "Error while creating tags on instance {0}".format(instance_name if
                                                                                  instance_name else name)
                          log.error(msg)
                          ret['comment'] += '  ' + msg
                          ret['result'] = False
                          return ret
                  ret['changes']['old'] = ret['changes']['old'] if 'old' in ret['changes'] else {}
                  ret['changes']['new'] = ret['changes']['new'] if 'new' in ret['changes'] else {}
                  ret['changes']['old']['tags'] = curr_tags
                  ret['changes']['new']['tags'] = tags
  
      return ret