"Fossies" - the Fresh Open Source Software Archive

Member "salt-3002.2/salt/states/boto_ec2.py" (18 Nov 2020, 73676 Bytes) of package /linux/misc/salt-3002.2.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "boto_ec2.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 3002.1_vs_3002.2.

    1 """
    2 Manage EC2
    3 
    4 .. versionadded:: 2015.8.0
    5 
    6 This module provides an interface to the Elastic Compute Cloud (EC2) service
    7 from AWS.
    8 
    9 The below code creates a key pair:
   10 
   11 .. code-block:: yaml
   12 
   13     create-key-pair:
   14       boto_ec2.key_present:
   15         - name: mykeypair
   16         - save_private: /root/
   17         - region: eu-west-1
   18         - keyid: GKTADJGHEIQSXMKKRBJ08H
   19         - key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
   20 
   21 .. code-block:: yaml
   22 
   23     import-key-pair:
   24        boto_ec2.key_present:
   25         - name: mykeypair
   26         - upload_public: 'ssh-rsa AAAA'
   27         - keyid: GKTADJGHEIQSXMKKRBJ08H
   28         - key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
   29 
   30 You can also use salt:// in order to define the public key.
   31 
   32 .. code-block:: yaml
   33 
   34     import-key-pair:
   35        boto_ec2.key_present:
   36         - name: mykeypair
   37         - upload_public: salt://mybase/public_key.pub
   38         - keyid: GKTADJGHEIQSXMKKRBJ08H
   39         - key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
   40 
   41 The below code deletes a key pair:
   42 
   43 .. code-block:: yaml
   44 
   45     delete-key-pair:
   46       boto_ec2.key_absent:
   47         - name: mykeypair
   48         - region: eu-west-1
   49         - keyid: GKTADJGHEIQSXMKKRBJ08H
   50         - key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
   51 """
   52 
   53 
   54 import logging
   55 from time import sleep, time
   56 
   57 import salt.utils.data
   58 import salt.utils.dictupdate as dictupdate
   59 from salt.exceptions import CommandExecutionError, SaltInvocationError
   60 from salt.ext.six.moves import range
   61 
   62 log = logging.getLogger(__name__)
   63 
   64 
   65 def __virtual__():
   66     """
   67     Only load if boto is available.
   68     """
   69     if "boto_ec2.get_key" in __salt__:
   70         return "boto_ec2"
   71     return (False, "boto_ec2 module could not be loaded")
   72 
   73 
   74 def key_present(
   75     name,
   76     save_private=None,
   77     upload_public=None,
   78     region=None,
   79     key=None,
   80     keyid=None,
   81     profile=None,
   82 ):
   83     """
   84     Ensure key pair is present.
   85     """
   86     ret = {"name": name, "result": True, "comment": "", "changes": {}}
   87     exists = __salt__["boto_ec2.get_key"](name, region, key, keyid, profile)
   88     log.debug("exists is %s", exists)
   89     if upload_public is not None and "salt://" in upload_public:
   90         try:
   91             upload_public = __salt__["cp.get_file_str"](upload_public)
   92         except OSError as e:
   93             log.debug(e)
   94             ret["comment"] = "File {} not found.".format(upload_public)
   95             ret["result"] = False
   96             return ret
   97     if not exists:
   98         if __opts__["test"]:
   99             ret["comment"] = "The key {} is set to be created.".format(name)
  100             ret["result"] = None
  101             return ret
  102         if save_private and not upload_public:
  103             created = __salt__["boto_ec2.create_key"](
  104                 name, save_private, region, key, keyid, profile
  105             )
  106             if created:
  107                 ret["result"] = True
  108                 ret["comment"] = "The key {} is created.".format(name)
  109                 ret["changes"]["new"] = created
  110             else:
  111                 ret["result"] = False
  112                 ret["comment"] = "Could not create key {} ".format(name)
  113         elif not save_private and upload_public:
  114             imported = __salt__["boto_ec2.import_key"](
  115                 name, upload_public, region, key, keyid, profile
  116             )
  117             if imported:
  118                 ret["result"] = True
  119                 ret["comment"] = "The key {} is created.".format(name)
  120                 ret["changes"]["old"] = None
  121                 ret["changes"]["new"] = imported
  122             else:
  123                 ret["result"] = False
  124                 ret["comment"] = "Could not create key {} ".format(name)
  125         else:
  126             ret["result"] = False
  127             ret["comment"] = "You can either upload or download a private key "
  128     else:
  129         ret["result"] = True
  130         ret["comment"] = "The key name {} already exists".format(name)
  131     return ret
  132 
  133 
  134 def key_absent(name, region=None, key=None, keyid=None, profile=None):
  135     """
  136     Deletes a key pair
  137     """
  138     ret = {"name": name, "result": True, "comment": "", "changes": {}}
  139     exists = __salt__["boto_ec2.get_key"](name, region, key, keyid, profile)
  140     if exists:
  141         if __opts__["test"]:
  142             ret["comment"] = "The key {} is set to be deleted.".format(name)
  143             ret["result"] = None
  144             return ret
  145         deleted = __salt__["boto_ec2.delete_key"](name, region, key, keyid, profile)
  146         log.debug("exists is %s", deleted)
  147         if deleted:
  148             ret["result"] = True
  149             ret["comment"] = "The key {} is deleted.".format(name)
  150             ret["changes"]["old"] = name
  151         else:
  152             ret["result"] = False
  153             ret["comment"] = "Could not delete key {} ".format(name)
  154     else:
  155         ret["result"] = True
  156         ret["comment"] = "The key name {} does not exist".format(name)
  157     return ret
  158 
  159 
  160 def eni_present(
  161     name,
  162     subnet_id=None,
  163     subnet_name=None,
  164     private_ip_address=None,
  165     description=None,
  166     groups=None,
  167     source_dest_check=True,
  168     allocate_eip=None,
  169     arecords=None,
  170     region=None,
  171     key=None,
  172     keyid=None,
  173     profile=None,
  174 ):
  175     """
  176     Ensure the EC2 ENI exists.
  177 
  178     .. versionadded:: 2016.3.0
  179 
  180     name
  181         Name tag associated with the ENI.
  182 
  183     subnet_id
  184         The VPC subnet ID the ENI will exist within.
  185 
  186     subnet_name
  187         The VPC subnet name the ENI will exist within.
  188 
  189     private_ip_address
  190         The private ip address to use for this ENI. If this is not specified
  191         AWS will automatically assign a private IP address to the ENI. Must be
  192         specified at creation time; will be ignored afterward.
  193 
  194     description
  195         Description of the key.
  196 
  197     groups
  198         A list of security groups to apply to the ENI.
  199 
  200     source_dest_check
  201         Boolean specifying whether source/destination checking is enabled on
  202         the ENI.
  203 
  204     allocate_eip
  205         allocate and associate an EIP to the ENI. Could be 'standard' to
  206         allocate Elastic IP to EC2 region or 'vpc' to get it for a
  207         particular VPC
  208 
  209         .. versionchanged:: 2016.11.0
  210 
  211     arecords
  212         A list of arecord dicts with attributes needed for the DNS add_record state.
  213         By default the boto_route53.add_record state will be used, which requires: name, zone, ttl, and identifier.
  214         See the boto_route53 state for information about these attributes.
  215         Other DNS modules can be called by specifying the provider keyword.
  216         By default, the private ENI IP address will be used, set 'public: True' in the arecord dict to use the ENI's public IP address
  217 
  218         .. versionadded:: 2016.3.0
  219 
  220     region
  221         Region to connect to.
  222 
  223     key
  224         Secret key to be used.
  225 
  226     keyid
  227         Access key to be used.
  228 
  229     profile
  230         A dict with region, key and keyid, or a pillar key (string)
  231         that contains a dict with region, key and keyid.
  232     """
  233     if not salt.utils.data.exactly_one((subnet_id, subnet_name)):
  234         raise SaltInvocationError(
  235             "One (but not both) of subnet_id or " "subnet_name must be provided."
  236         )
  237     if not groups:
  238         raise SaltInvocationError("groups is a required argument.")
  239     if not isinstance(groups, list):
  240         raise SaltInvocationError("groups must be a list.")
  241     if not isinstance(source_dest_check, bool):
  242         raise SaltInvocationError("source_dest_check must be a bool.")
  243     ret = {"name": name, "result": True, "comment": "", "changes": {}}
  244     r = __salt__["boto_ec2.get_network_interface"](
  245         name=name, region=region, key=key, keyid=keyid, profile=profile
  246     )
  247     if "error" in r:
  248         ret["result"] = False
  249         ret["comment"] = "Error when attempting to find eni: {}.".format(
  250             r["error"]["message"]
  251         )
  252         return ret
  253     if not r["result"]:
  254         if __opts__["test"]:
  255             ret["comment"] = "ENI is set to be created."
  256             if allocate_eip:
  257                 ret["comment"] = " ".join(
  258                     [
  259                         ret["comment"],
  260                         "An EIP is set to be allocated/assocaited to the ENI.",
  261                     ]
  262                 )
  263             if arecords:
  264                 ret["comment"] = " ".join(
  265                     [ret["comment"], "A records are set to be created."]
  266                 )
  267             ret["result"] = None
  268             return ret
  269         result_create = __salt__["boto_ec2.create_network_interface"](
  270             name,
  271             subnet_id=subnet_id,
  272             subnet_name=subnet_name,
  273             private_ip_address=private_ip_address,
  274             description=description,
  275             groups=groups,
  276             region=region,
  277             key=key,
  278             keyid=keyid,
  279             profile=profile,
  280         )
  281         if "error" in result_create:
  282             ret["result"] = False
  283             ret["comment"] = "Failed to create ENI: {}".format(
  284                 result_create["error"]["message"]
  285             )
  286             return ret
  287         r["result"] = result_create["result"]
  288         ret["comment"] = "Created ENI {}".format(name)
  289         ret["changes"]["id"] = r["result"]["id"]
  290     else:
  291         _ret = _eni_attribute(
  292             r["result"], "description", description, region, key, keyid, profile
  293         )
  294         ret["changes"] = dictupdate.update(ret["changes"], _ret["changes"])
  295         ret["comment"] = _ret["comment"]
  296         if not _ret["result"]:
  297             ret["result"] = _ret["result"]
  298             if ret["result"] is False:
  299                 return ret
  300         _ret = _eni_groups(r["result"], groups, region, key, keyid, profile)
  301         ret["changes"] = dictupdate.update(ret["changes"], _ret["changes"])
  302         ret["comment"] = " ".join([ret["comment"], _ret["comment"]])
  303         if not _ret["result"]:
  304             ret["result"] = _ret["result"]
  305             if ret["result"] is False:
  306                 return ret
  307     # Actions that need to occur whether creating or updating
  308     _ret = _eni_attribute(
  309         r["result"], "source_dest_check", source_dest_check, region, key, keyid, profile
  310     )
  311     ret["changes"] = dictupdate.update(ret["changes"], _ret["changes"])
  312     ret["comment"] = " ".join([ret["comment"], _ret["comment"]])
  313     if not _ret["result"]:
  314         ret["result"] = _ret["result"]
  315         return ret
  316     if allocate_eip:
  317         if "allocationId" not in r["result"]:
  318             if __opts__["test"]:
  319                 ret["comment"] = " ".join(
  320                     [
  321                         ret["comment"],
  322                         "An EIP is set to be allocated and assocaited to the ENI.",
  323                     ]
  324                 )
  325             else:
  326                 domain = "vpc" if allocate_eip == "vpc" else None
  327                 eip_alloc = __salt__["boto_ec2.allocate_eip_address"](
  328                     domain=domain, region=region, key=key, keyid=keyid, profile=profile
  329                 )
  330                 if eip_alloc:
  331                     _ret = __salt__["boto_ec2.associate_eip_address"](
  332                         instance_id=None,
  333                         instance_name=None,
  334                         public_ip=None,
  335                         allocation_id=eip_alloc["allocation_id"],
  336                         network_interface_id=r["result"]["id"],
  337                         private_ip_address=None,
  338                         allow_reassociation=False,
  339                         region=region,
  340                         key=key,
  341                         keyid=keyid,
  342                         profile=profile,
  343                     )
  344                     if not _ret:
  345                         _ret = __salt__["boto_ec2.release_eip_address"](
  346                             public_ip=None,
  347                             allocation_id=eip_alloc["allocation_id"],
  348                             region=region,
  349                             key=key,
  350                             keyid=keyid,
  351                             profile=profile,
  352                         )
  353                         ret["result"] = False
  354                         msg = "Failed to assocaite the allocated EIP address with the ENI.  The EIP {}".format(
  355                             "was successfully released."
  356                             if _ret
  357                             else "was NOT RELEASED."
  358                         )
  359                         ret["comment"] = " ".join([ret["comment"], msg])
  360                         return ret
  361                 else:
  362                     ret["result"] = False
  363                     ret["comment"] = " ".join(
  364                         [ret["comment"], "Failed to allocate an EIP address"]
  365                     )
  366                     return ret
  367         else:
  368             ret["comment"] = " ".join(
  369                 [ret["comment"], "An EIP is already allocated/assocaited to the ENI"]
  370             )
  371     if arecords:
  372         for arecord in arecords:
  373             if "name" not in arecord:
  374                 msg = 'The arecord must contain a "name" property.'
  375                 raise SaltInvocationError(msg)
  376             log.debug("processing arecord %s", arecord)
  377             _ret = None
  378             dns_provider = "boto_route53"
  379             arecord["record_type"] = "A"
  380             public_ip_arecord = False
  381             if "public" in arecord:
  382                 public_ip_arecord = arecord.pop("public")
  383             if public_ip_arecord:
  384                 if "publicIp" in r["result"]:
  385                     arecord["value"] = r["result"]["publicIp"]
  386                 elif "public_ip" in eip_alloc:
  387                     arecord["value"] = eip_alloc["public_ip"]
  388                 else:
  389                     msg = "Unable to add an A record for the public IP address, a public IP address does not seem to be allocated to this ENI."
  390                     raise CommandExecutionError(msg)
  391             else:
  392                 arecord["value"] = r["result"]["private_ip_address"]
  393             if "provider" in arecord:
  394                 dns_provider = arecord.pop("provider")
  395             if dns_provider == "boto_route53":
  396                 if "profile" not in arecord:
  397                     arecord["profile"] = profile
  398                 if "key" not in arecord:
  399                     arecord["key"] = key
  400                 if "keyid" not in arecord:
  401                     arecord["keyid"] = keyid
  402                 if "region" not in arecord:
  403                     arecord["region"] = region
  404             _ret = __states__[".".join([dns_provider, "present"])](**arecord)
  405             log.debug("ret from dns_provider.present = %s", _ret)
  406             ret["changes"] = dictupdate.update(ret["changes"], _ret["changes"])
  407             ret["comment"] = " ".join([ret["comment"], _ret["comment"]])
  408             if not _ret["result"]:
  409                 ret["result"] = _ret["result"]
  410                 if ret["result"] is False:
  411                     return ret
  412     return ret
  413 
  414 
  415 def _eni_attribute(metadata, attr, value, region, key, keyid, profile):
  416     ret = {"result": True, "comment": "", "changes": {}}
  417     if metadata[attr] == value:
  418         return ret
  419     if __opts__["test"]:
  420         ret["comment"] = "ENI set to have {} updated.".format(attr)
  421         ret["result"] = None
  422         return ret
  423     result_update = __salt__["boto_ec2.modify_network_interface_attribute"](
  424         network_interface_id=metadata["id"],
  425         attr=attr,
  426         value=value,
  427         region=region,
  428         key=key,
  429         keyid=keyid,
  430         profile=profile,
  431     )
  432     if "error" in result_update:
  433         msg = "Failed to update ENI {0}: {1}."
  434         ret["result"] = False
  435         ret["comment"] = msg.format(attr, result_update["error"]["message"])
  436     else:
  437         ret["comment"] = "Updated ENI {}.".format(attr)
  438         ret["changes"][attr] = {"old": metadata[attr], "new": value}
  439     return ret
  440 
  441 
  442 def _eni_groups(metadata, groups, region, key, keyid, profile):
  443     ret = {"result": True, "comment": "", "changes": {}}
  444     group_ids = [g["id"] for g in metadata["groups"]]
  445     group_ids.sort()
  446     _groups = __salt__["boto_secgroup.convert_to_group_ids"](
  447         groups,
  448         vpc_id=metadata["vpc_id"],
  449         region=region,
  450         key=key,
  451         keyid=keyid,
  452         profile=profile,
  453     )
  454     if not _groups:
  455         ret["comment"] = "Could not find secgroup ids for provided groups."
  456         ret["result"] = False
  457     _groups.sort()
  458     if group_ids == _groups:
  459         return ret
  460     if __opts__["test"]:
  461         ret["comment"] = "ENI set to have groups updated."
  462         ret["result"] = None
  463         return ret
  464     result_update = __salt__["boto_ec2.modify_network_interface_attribute"](
  465         network_interface_id=metadata["id"],
  466         attr="groups",
  467         value=_groups,
  468         region=region,
  469         key=key,
  470         keyid=keyid,
  471         profile=profile,
  472     )
  473     if "error" in result_update:
  474         msg = "Failed to update ENI groups: {1}."
  475         ret["result"] = False
  476         ret["comment"] = msg.format(result_update["error"]["message"])
  477     else:
  478         ret["comment"] = "Updated ENI groups."
  479         ret["changes"]["groups"] = {"old": group_ids, "new": _groups}
  480     return ret
  481 
  482 
  483 def eni_absent(
  484     name, release_eip=False, region=None, key=None, keyid=None, profile=None
  485 ):
  486     """
  487     Ensure the EC2 ENI is absent.
  488 
  489     .. versionadded:: 2016.3.0
  490 
  491     name
  492         Name tag associated with the ENI.
  493 
  494     release_eip
  495         True/False - release any EIP associated with the ENI
  496 
  497     region
  498         Region to connect to.
  499 
  500     key
  501         Secret key to be used.
  502 
  503     keyid
  504         Access key to be used.
  505 
  506     profile
  507         A dict with region, key and keyid, or a pillar key (string)
  508         that contains a dict with region, key and keyid.
  509     """
  510     ret = {"name": name, "result": True, "comment": "", "changes": {}}
  511     r = __salt__["boto_ec2.get_network_interface"](
  512         name=name, region=region, key=key, keyid=keyid, profile=profile
  513     )
  514     if "error" in r:
  515         ret["result"] = False
  516         ret["comment"] = "Error when attempting to find eni: {}.".format(
  517             r["error"]["message"]
  518         )
  519         return ret
  520     if not r["result"]:
  521         if __opts__["test"]:
  522             ret["comment"] = "ENI is set to be deleted."
  523             ret["result"] = None
  524             return ret
  525     else:
  526         if __opts__["test"]:
  527             ret["comment"] = "ENI is set to be deleted."
  528             if release_eip and "allocationId" in r["result"]:
  529                 ret["comment"] = " ".join(
  530                     [ret["comment"], "Allocated/associated EIP is set to be released"]
  531                 )
  532             ret["result"] = None
  533             return ret
  534         if "id" in r["result"]["attachment"]:
  535             result_detach = __salt__["boto_ec2.detach_network_interface"](
  536                 name=name,
  537                 force=True,
  538                 region=region,
  539                 key=key,
  540                 keyid=keyid,
  541                 profile=profile,
  542             )
  543             if "error" in result_detach:
  544                 ret["result"] = False
  545                 ret["comment"] = "Failed to detach ENI: {}".format(
  546                     result_detach["error"]["message"]
  547                 )
  548                 return ret
  549             # TODO: Ensure the detach occurs before continuing
  550         result_delete = __salt__["boto_ec2.delete_network_interface"](
  551             name=name, region=region, key=key, keyid=keyid, profile=profile
  552         )
  553         if "error" in result_delete:
  554             ret["result"] = False
  555             ret["comment"] = "Failed to delete ENI: {}".format(
  556                 result_delete["error"]["message"]
  557             )
  558             return ret
  559         ret["comment"] = "Deleted ENI {}".format(name)
  560         ret["changes"]["id"] = None
  561         if release_eip and "allocationId" in r["result"]:
  562             _ret = __salt__["boto_ec2.release_eip_address"](
  563                 public_ip=None,
  564                 allocation_id=r["result"]["allocationId"],
  565                 region=region,
  566                 key=key,
  567                 keyid=keyid,
  568                 profile=profile,
  569             )
  570             if not _ret:
  571                 ret["comment"] = " ".join(
  572                     [ret["comment"], "Failed to release EIP allocated to the ENI."]
  573                 )
  574                 ret["result"] = False
  575                 return ret
  576             else:
  577                 ret["comment"] = " ".join([ret["comment"], "EIP released."])
  578                 ret["changes"]["eip released"] = True
  579     return ret
  580 
  581 
  582 def snapshot_created(
  583     name,
  584     ami_name,
  585     instance_name,
  586     wait_until_available=True,
  587     wait_timeout_seconds=300,
  588     **kwargs
  589 ):
  590     """
  591     Create a snapshot from the given instance
  592 
  593     .. versionadded:: 2016.3.0
  594     """
  595     ret = {"name": name, "result": True, "comment": "", "changes": {}}
  596 
  597     if not __salt__["boto_ec2.create_image"](
  598         ami_name=ami_name, instance_name=instance_name, **kwargs
  599     ):
  600         ret["comment"] = "Failed to create new AMI {ami_name}".format(ami_name=ami_name)
  601         ret["result"] = False
  602         return ret
  603 
  604     ret["comment"] = "Created new AMI {ami_name}".format(ami_name=ami_name)
  605     ret["changes"]["new"] = {ami_name: ami_name}
  606     if not wait_until_available:
  607         return ret
  608 
  609     starttime = time()
  610     while True:
  611         images = __salt__["boto_ec2.find_images"](
  612             ami_name=ami_name, return_objs=True, **kwargs
  613         )
  614         if images and images[0].state == "available":
  615             break
  616         if time() - starttime > wait_timeout_seconds:
  617             if images:
  618                 ret["comment"] = "AMI still in state {state} after timeout".format(
  619                     state=images[0].state
  620                 )
  621             else:
  622                 ret[
  623                     "comment"
  624                 ] = "AMI with name {ami_name} not found after timeout.".format(
  625                     ami_name=ami_name
  626                 )
  627             ret["result"] = False
  628             return ret
  629         sleep(5)
  630 
  631     return ret
  632 
  633 
  634 def instance_present(
  635     name,
  636     instance_name=None,
  637     instance_id=None,
  638     image_id=None,
  639     image_name=None,
  640     tags=None,
  641     key_name=None,
  642     security_groups=None,
  643     user_data=None,
  644     instance_type=None,
  645     placement=None,
  646     kernel_id=None,
  647     ramdisk_id=None,
  648     vpc_id=None,
  649     vpc_name=None,
  650     monitoring_enabled=None,
  651     subnet_id=None,
  652     subnet_name=None,
  653     private_ip_address=None,
  654     block_device_map=None,
  655     disable_api_termination=None,
  656     instance_initiated_shutdown_behavior=None,
  657     placement_group=None,
  658     client_token=None,
  659     security_group_ids=None,
  660     security_group_names=None,
  661     additional_info=None,
  662     tenancy=None,
  663     instance_profile_arn=None,
  664     instance_profile_name=None,
  665     ebs_optimized=None,
  666     network_interfaces=None,
  667     network_interface_name=None,
  668     network_interface_id=None,
  669     attributes=None,
  670     target_state=None,
  671     public_ip=None,
  672     allocation_id=None,
  673     allocate_eip=False,
  674     region=None,
  675     key=None,
  676     keyid=None,
  677     profile=None,
  678 ):
  679     ### TODO - implement 'target_state={running, stopped}'
  680     """
  681     Ensure an EC2 instance is running with the given attributes and state.
  682 
  683     name
  684         (string) - The name of the state definition.  Recommended that this
  685         match the instance_name attribute (generally the FQDN of the instance).
  686     instance_name
  687         (string) - The name of the instance, generally its FQDN.  Exclusive with
  688         'instance_id'.
  689     instance_id
  690         (string) - The ID of the instance (if known).  Exclusive with
  691         'instance_name'.
  692     image_id
  693         (string) – The ID of the AMI image to run.
  694     image_name
  695         (string) – The name of the AMI image to run.
  696     tags
  697         (dict) - Tags to apply to the instance.
  698     key_name
  699         (string) – The name of the key pair with which to launch instances.
  700     security_groups
  701         (list of strings) – The names of the EC2 classic security groups with
  702         which to associate instances
  703     user_data
  704         (string) – The Base64-encoded MIME user data to be made available to the
  705         instance(s) in this reservation.
  706     instance_type
  707         (string) – The EC2 instance size/type.  Note that only certain types are
  708         compatible with HVM based AMIs.
  709     placement
  710         (string) – The Availability Zone to launch the instance into.
  711     kernel_id
  712         (string) – The ID of the kernel with which to launch the instances.
  713     ramdisk_id
  714         (string) – The ID of the RAM disk with which to launch the instances.
  715     vpc_id
  716         (string) - The ID of a VPC to attach the instance to.
  717     vpc_name
  718         (string) - The name of a VPC to attach the instance to.
  719     monitoring_enabled
  720         (bool) – Enable detailed CloudWatch monitoring on the instance.
  721     subnet_id
  722         (string) – The ID of the subnet within which to launch the instances for
  723         VPC.
  724     subnet_name
  725         (string) – The name of the subnet within which to launch the instances
  726         for VPC.
  727     private_ip_address
  728         (string) – If you’re using VPC, you can optionally use this parameter to
  729         assign the instance a specific available IP address from the subnet
  730         (e.g., 10.0.0.25).
  731     block_device_map
  732         (boto.ec2.blockdevicemapping.BlockDeviceMapping) – A BlockDeviceMapping
  733         data structure describing the EBS volumes associated with the Image.
  734     disable_api_termination
  735         (bool) – If True, the instances will be locked and will not be able to
  736         be terminated via the API.
  737     instance_initiated_shutdown_behavior
  738         (string) – Specifies whether the instance stops or terminates on
  739         instance-initiated shutdown. Valid values are:
  740 
  741         - 'stop'
  742         - 'terminate'
  743 
  744     placement_group
  745         (string) – If specified, this is the name of the placement group in
  746         which the instance(s) will be launched.
  747     client_token
  748         (string) – Unique, case-sensitive identifier you provide to ensure
  749         idempotency of the request. Maximum 64 ASCII characters.
  750     security_group_ids
  751         (list of strings) – The IDs of the VPC security groups with which to
  752         associate instances.
  753     security_group_names
  754         (list of strings) – The names of the VPC security groups with which to
  755         associate instances.
  756     additional_info
  757         (string) – Specifies additional information to make available to the
  758         instance(s).
  759     tenancy
  760         (string) – The tenancy of the instance you want to launch. An instance
  761         with a tenancy of ‘dedicated’ runs on single-tenant hardware and can
  762         only be launched into a VPC. Valid values are:”default” or “dedicated”.
  763         NOTE: To use dedicated tenancy you MUST specify a VPC subnet-ID as well.
  764     instance_profile_arn
  765         (string) – The Amazon resource name (ARN) of the IAM Instance Profile
  766         (IIP) to associate with the instances.
  767     instance_profile_name
  768         (string) – The name of the IAM Instance Profile (IIP) to associate with
  769         the instances.
  770     ebs_optimized
  771         (bool) – Whether the instance is optimized for EBS I/O. This
  772         optimization provides dedicated throughput to Amazon EBS and a tuned
  773         configuration stack to provide optimal EBS I/O performance. This
  774         optimization isn’t available with all instance types.
  775     network_interfaces
  776         (boto.ec2.networkinterface.NetworkInterfaceCollection) – A
  777         NetworkInterfaceCollection data structure containing the ENI
  778         specifications for the instance.
  779     network_interface_name
  780          (string) - The name of Elastic Network Interface to attach
  781 
  782         .. versionadded:: 2016.11.0
  783 
  784     network_interface_id
  785          (string) - The id of Elastic Network Interface to attach
  786 
  787         .. versionadded:: 2016.11.0
  788 
  789     attributes
  790         (dict) - Instance attributes and value to be applied to the instance.
  791         Available options are:
  792 
  793         - instanceType - A valid instance type (m1.small)
  794         - kernel - Kernel ID (None)
  795         - ramdisk - Ramdisk ID (None)
  796         - userData - Base64 encoded String (None)
  797         - disableApiTermination - Boolean (true)
  798         - instanceInitiatedShutdownBehavior - stop|terminate
  799         - blockDeviceMapping - List of strings - ie: [‘/dev/sda=false’]
  800         - sourceDestCheck - Boolean (true)
  801         - groupSet - Set of Security Groups or IDs
  802         - ebsOptimized - Boolean (false)
  803         - sriovNetSupport - String - ie: ‘simple’
  804 
  805     target_state
  806         (string) - The desired target state of the instance.  Available options
  807         are:
  808 
  809         - running
  810         - stopped
  811 
  812         Note that this option is currently UNIMPLEMENTED.
  813     public_ip:
  814         (string) - The IP of a previously allocated EIP address, which will be
  815         attached to the instance.  EC2 Classic instances ONLY - for VCP pass in
  816         an allocation_id instead.
  817     allocation_id:
  818         (string) - The ID of a previously allocated EIP address, which will be
  819         attached to the instance.  VPC instances ONLY - for Classic pass in
  820         a public_ip instead.
  821     allocate_eip:
  822         (bool) - Allocate and attach an EIP on-the-fly for this instance.  Note
  823         you'll want to release this address when terminating the instance,
  824         either manually or via the 'release_eip' flag to 'instance_absent'.
  825     region
  826         (string) - Region to connect to.
  827     key
  828         (string) - Secret key to be used.
  829     keyid
  830         (string) - Access key to be used.
  831     profile
  832         (variable) - A dict with region, key and keyid, or a pillar key (string)
  833         that contains a dict with region, key and keyid.
  834 
  835     .. versionadded:: 2016.3.0
  836     """
  837     ret = {"name": name, "result": True, "comment": "", "changes": {}}
  838     _create = False
  839     running_states = ("pending", "rebooting", "running", "stopping", "stopped")
  840     changed_attrs = {}
  841 
  842     if not salt.utils.data.exactly_one((image_id, image_name)):
  843         raise SaltInvocationError(
  844             "Exactly one of image_id OR " "image_name must be provided."
  845         )
  846     if (public_ip or allocation_id or allocate_eip) and not salt.utils.data.exactly_one(
  847         (public_ip, allocation_id, allocate_eip)
  848     ):
  849         raise SaltInvocationError(
  850             "At most one of public_ip, allocation_id OR "
  851             "allocate_eip may be provided."
  852         )
  853 
  854     if instance_id:
  855         exists = __salt__["boto_ec2.exists"](
  856             instance_id=instance_id,
  857             region=region,
  858             key=key,
  859             keyid=keyid,
  860             profile=profile,
  861             in_states=running_states,
  862         )
  863         if not exists:
  864             _create = True
  865     else:
  866         instances = __salt__["boto_ec2.find_instances"](
  867             name=instance_name if instance_name else name,
  868             region=region,
  869             key=key,
  870             keyid=keyid,
  871             profile=profile,
  872             in_states=running_states,
  873         )
  874         if not instances:
  875             _create = True
  876         elif len(instances) > 1:
  877             log.debug(
  878                 "Multiple instances matching criteria found - cannot determine a singular instance-id"
  879             )
  880             instance_id = None  # No way to know, we'll just have to bail later....
  881         else:
  882             instance_id = instances[0]
  883 
  884     if _create:
  885         if __opts__["test"]:
  886             ret["comment"] = "The instance {} is set to be created.".format(name)
  887             ret["result"] = None
  888             return ret
  889         if image_name:
  890             args = {
  891                 "ami_name": image_name,
  892                 "region": region,
  893                 "key": key,
  894                 "keyid": keyid,
  895                 "profile": profile,
  896             }
  897             image_ids = __salt__["boto_ec2.find_images"](**args)
  898             if image_ids:
  899                 image_id = image_ids[0]
  900             else:
  901                 image_id = image_name
  902         r = __salt__["boto_ec2.run"](
  903             image_id,
  904             instance_name if instance_name else name,
  905             tags=tags,
  906             key_name=key_name,
  907             security_groups=security_groups,
  908             user_data=user_data,
  909             instance_type=instance_type,
  910             placement=placement,
  911             kernel_id=kernel_id,
  912             ramdisk_id=ramdisk_id,
  913             vpc_id=vpc_id,
  914             vpc_name=vpc_name,
  915             monitoring_enabled=monitoring_enabled,
  916             subnet_id=subnet_id,
  917             subnet_name=subnet_name,
  918             private_ip_address=private_ip_address,
  919             block_device_map=block_device_map,
  920             disable_api_termination=disable_api_termination,
  921             instance_initiated_shutdown_behavior=instance_initiated_shutdown_behavior,
  922             placement_group=placement_group,
  923             client_token=client_token,
  924             security_group_ids=security_group_ids,
  925             security_group_names=security_group_names,
  926             additional_info=additional_info,
  927             tenancy=tenancy,
  928             instance_profile_arn=instance_profile_arn,
  929             instance_profile_name=instance_profile_name,
  930             ebs_optimized=ebs_optimized,
  931             network_interfaces=network_interfaces,
  932             network_interface_name=network_interface_name,
  933             network_interface_id=network_interface_id,
  934             region=region,
  935             key=key,
  936             keyid=keyid,
  937             profile=profile,
  938         )
  939         if not r or "instance_id" not in r:
  940             ret["result"] = False
  941             ret["comment"] = "Failed to create instance {}.".format(
  942                 instance_name if instance_name else name
  943             )
  944             return ret
  945 
  946         instance_id = r["instance_id"]
  947         ret["changes"] = {"old": {}, "new": {}}
  948         ret["changes"]["old"]["instance_id"] = None
  949         ret["changes"]["new"]["instance_id"] = instance_id
  950 
  951         # To avoid issues we only allocate new EIPs at instance creation.
  952         # This might miss situations where an instance is initially created
  953         # created without and one is added later, but the alternative is the
  954         # risk of EIPs allocated at every state run.
  955         if allocate_eip:
  956             if __opts__["test"]:
  957                 ret["comment"] = "New EIP would be allocated."
  958                 ret["result"] = None
  959                 return ret
  960             domain = "vpc" if vpc_id or vpc_name else None
  961             r = __salt__["boto_ec2.allocate_eip_address"](
  962                 domain=domain, region=region, key=key, keyid=keyid, profile=profile
  963             )
  964             if not r:
  965                 ret["result"] = False
  966                 ret["comment"] = "Failed to allocate new EIP."
  967                 return ret
  968             allocation_id = r["allocation_id"]
  969             log.info("New EIP with address %s allocated.", r["public_ip"])
  970         else:
  971             log.info("EIP not requested.")
  972 
  973     if public_ip or allocation_id:
  974         # This can take a bit to show up, give it a chance to...
  975         tries = 10
  976         secs = 3
  977         for t in range(tries):
  978             r = __salt__["boto_ec2.get_eip_address_info"](
  979                 addresses=public_ip,
  980                 allocation_ids=allocation_id,
  981                 region=region,
  982                 key=key,
  983                 keyid=keyid,
  984                 profile=profile,
  985             )
  986             if r:
  987                 break
  988             else:
  989                 log.info(
  990                     "Waiting up to %s secs for new EIP %s to become available",
  991                     tries * secs,
  992                     public_ip or allocation_id,
  993                 )
  994                 time.sleep(secs)
  995         if not r:
  996             ret["result"] = False
  997             ret["comment"] = "Failed to lookup EIP {}.".format(
  998                 public_ip or allocation_id
  999             )
 1000             return ret
 1001         ip = r[0]["public_ip"]
 1002         if r[0].get("instance_id"):
 1003             if r[0]["instance_id"] != instance_id:
 1004                 ret["result"] = False
 1005                 ret["comment"] = (
 1006                     "EIP {} is already associated with instance "
 1007                     "{}.".format(
 1008                         public_ip if public_ip else allocation_id, r[0]["instance_id"]
 1009                     )
 1010                 )
 1011                 return ret
 1012         else:
 1013             if __opts__["test"]:
 1014                 ret["comment"] = "Instance {} to be updated.".format(name)
 1015                 ret["result"] = None
 1016                 return ret
 1017             r = __salt__["boto_ec2.associate_eip_address"](
 1018                 instance_id=instance_id,
 1019                 public_ip=public_ip,
 1020                 allocation_id=allocation_id,
 1021                 region=region,
 1022                 key=key,
 1023                 keyid=keyid,
 1024                 profile=profile,
 1025             )
 1026             if r:
 1027                 if "new" not in ret["changes"]:
 1028                     ret["changes"]["new"] = {}
 1029                 ret["changes"]["new"]["public_ip"] = ip
 1030             else:
 1031                 ret["result"] = False
 1032                 ret["comment"] = "Failed to attach EIP to instance {}.".format(
 1033                     instance_name if instance_name else name
 1034                 )
 1035                 return ret
 1036 
 1037     if attributes:
 1038         for k, v in attributes.items():
 1039             curr = __salt__["boto_ec2.get_attribute"](
 1040                 k,
 1041                 instance_id=instance_id,
 1042                 region=region,
 1043                 key=key,
 1044                 keyid=keyid,
 1045                 profile=profile,
 1046             )
 1047             curr = {} if not isinstance(curr, dict) else curr
 1048             if curr.get(k) == v:
 1049                 continue
 1050             else:
 1051                 if __opts__["test"]:
 1052                     changed_attrs[
 1053                         k
 1054                     ] = "The instance attribute {} is set to be changed from '{}' to '{}'.".format(
 1055                         k, curr.get(k), v
 1056                     )
 1057                     continue
 1058                 try:
 1059                     r = __salt__["boto_ec2.set_attribute"](
 1060                         attribute=k,
 1061                         attribute_value=v,
 1062                         instance_id=instance_id,
 1063                         region=region,
 1064                         key=key,
 1065                         keyid=keyid,
 1066                         profile=profile,
 1067                     )
 1068                 except SaltInvocationError as e:
 1069                     ret["result"] = False
 1070                     ret[
 1071                         "comment"
 1072                     ] = "Failed to set attribute {} to {} on instance {}.".format(
 1073                         k, v, instance_name
 1074                     )
 1075                     return ret
 1076                 ret["changes"] = (
 1077                     ret["changes"] if ret["changes"] else {"old": {}, "new": {}}
 1078                 )
 1079                 ret["changes"]["old"][k] = curr.get(k)
 1080                 ret["changes"]["new"][k] = v
 1081 
 1082     if __opts__["test"]:
 1083         if changed_attrs:
 1084             ret["changes"]["new"] = changed_attrs
 1085             ret["result"] = None
 1086         else:
 1087             ret["comment"] = "Instance {} is in the correct state".format(
 1088                 instance_name if instance_name else name
 1089             )
 1090             ret["result"] = True
 1091 
 1092     if tags and instance_id is not None:
 1093         tags = dict(tags)
 1094         curr_tags = dict(
 1095             __salt__["boto_ec2.get_all_tags"](
 1096                 filters={"resource-id": instance_id},
 1097                 region=region,
 1098                 key=key,
 1099                 keyid=keyid,
 1100                 profile=profile,
 1101             ).get(instance_id, {})
 1102         )
 1103         current = set(curr_tags.keys())
 1104         desired = set(tags.keys())
 1105         remove = list(
 1106             current - desired
 1107         )  # Boto explicitly requires a list here and can't cope with a set...
 1108         add = {t: tags[t] for t in desired - current}
 1109         replace = {t: tags[t] for t in tags if tags.get(t) != curr_tags.get(t)}
 1110         # Tag keys are unique despite the bizarre semantics uses which make it LOOK like they could be duplicative.
 1111         add.update(replace)
 1112         if add or remove:
 1113             if __opts__["test"]:
 1114                 ret["changes"]["old"] = (
 1115                     ret["changes"]["old"] if "old" in ret["changes"] else {}
 1116                 )
 1117                 ret["changes"]["new"] = (
 1118                     ret["changes"]["new"] if "new" in ret["changes"] else {}
 1119                 )
 1120                 ret["changes"]["old"]["tags"] = curr_tags
 1121                 ret["changes"]["new"]["tags"] = tags
 1122                 ret["comment"] += "  Tags would be updated on instance {}.".format(
 1123                     instance_name if instance_name else name
 1124                 )
 1125             else:
 1126                 if remove:
 1127                     if not __salt__["boto_ec2.delete_tags"](
 1128                         resource_ids=instance_id,
 1129                         tags=remove,
 1130                         region=region,
 1131                         key=key,
 1132                         keyid=keyid,
 1133                         profile=profile,
 1134                     ):
 1135                         msg = "Error while deleting tags on instance {}".format(
 1136                             instance_name if instance_name else name
 1137                         )
 1138                         log.error(msg)
 1139                         ret["comment"] += "  " + msg
 1140                         ret["result"] = False
 1141                         return ret
 1142                 if add:
 1143                     if not __salt__["boto_ec2.create_tags"](
 1144                         resource_ids=instance_id,
 1145                         tags=add,
 1146                         region=region,
 1147                         key=key,
 1148                         keyid=keyid,
 1149                         profile=profile,
 1150                     ):
 1151                         msg = "Error while creating tags on instance {}".format(
 1152                             instance_name if instance_name else name
 1153                         )
 1154                         log.error(msg)
 1155                         ret["comment"] += "  " + msg
 1156                         ret["result"] = False
 1157                         return ret
 1158                 ret["changes"]["old"] = (
 1159                     ret["changes"]["old"] if "old" in ret["changes"] else {}
 1160                 )
 1161                 ret["changes"]["new"] = (
 1162                     ret["changes"]["new"] if "new" in ret["changes"] else {}
 1163                 )
 1164                 ret["changes"]["old"]["tags"] = curr_tags
 1165                 ret["changes"]["new"]["tags"] = tags
 1166 
 1167     return ret
 1168 
 1169 
 1170 def instance_absent(
 1171     name,
 1172     instance_name=None,
 1173     instance_id=None,
 1174     release_eip=False,
 1175     region=None,
 1176     key=None,
 1177     keyid=None,
 1178     profile=None,
 1179     filters=None,
 1180 ):
 1181     """
 1182     Ensure an EC2 instance does not exist (is stopped and removed).
 1183 
 1184     .. versionchanged:: 2016.11.0
 1185 
 1186     name
 1187         (string) - The name of the state definition.
 1188     instance_name
 1189         (string) - The name of the instance.
 1190     instance_id
 1191         (string) - The ID of the instance.
 1192     release_eip
 1193         (bool)   - Release any associated EIPs during termination.
 1194     region
 1195         (string) - Region to connect to.
 1196     key
 1197         (string) - Secret key to be used.
 1198     keyid
 1199         (string) - Access key to be used.
 1200     profile
 1201         (variable) - A dict with region, key and keyid, or a pillar key (string)
 1202         that contains a dict with region, key and keyid.
 1203     filters
 1204         (dict) - A dict of additional filters to use in matching the instance to
 1205         delete.
 1206 
 1207     YAML example fragment:
 1208 
 1209     .. code-block:: yaml
 1210 
 1211         - filters:
 1212             vpc-id: vpc-abcdef12
 1213     """
 1214     ### TODO - Implement 'force' option??  Would automagically turn off
 1215     ###        'disableApiTermination', as needed, before trying to delete.
 1216     ret = {"name": name, "result": True, "comment": "", "changes": {}}
 1217     running_states = ("pending", "rebooting", "running", "stopping", "stopped")
 1218 
 1219     if not instance_id:
 1220         try:
 1221             instance_id = __salt__["boto_ec2.get_id"](
 1222                 name=instance_name if instance_name else name,
 1223                 region=region,
 1224                 key=key,
 1225                 keyid=keyid,
 1226                 profile=profile,
 1227                 in_states=running_states,
 1228                 filters=filters,
 1229             )
 1230         except CommandExecutionError as e:
 1231             ret["result"] = None
 1232             ret["comment"] = (
 1233                 "Couldn't determine current status of instance "
 1234                 "{}.".format(instance_name or name)
 1235             )
 1236             return ret
 1237 
 1238     instances = __salt__["boto_ec2.find_instances"](
 1239         instance_id=instance_id,
 1240         region=region,
 1241         key=key,
 1242         keyid=keyid,
 1243         profile=profile,
 1244         return_objs=True,
 1245         filters=filters,
 1246     )
 1247     if not instances:
 1248         ret["result"] = True
 1249         ret["comment"] = "Instance {} is already gone.".format(instance_id)
 1250         return ret
 1251     instance = instances[0]
 1252 
 1253     ### Honor 'disableApiTermination' - if you want to override it, first use set_attribute() to turn it off
 1254     no_can_do = __salt__["boto_ec2.get_attribute"](
 1255         "disableApiTermination",
 1256         instance_id=instance_id,
 1257         region=region,
 1258         key=key,
 1259         keyid=keyid,
 1260         profile=profile,
 1261     )
 1262     if no_can_do.get("disableApiTermination") is True:
 1263         ret["result"] = False
 1264         ret["comment"] = "Termination of instance {} via the API is disabled.".format(
 1265             instance_id
 1266         )
 1267         return ret
 1268 
 1269     if __opts__["test"]:
 1270         ret["comment"] = "The instance {} is set to be deleted.".format(name)
 1271         ret["result"] = None
 1272         return ret
 1273 
 1274     r = __salt__["boto_ec2.terminate"](
 1275         instance_id=instance_id,
 1276         name=instance_name,
 1277         region=region,
 1278         key=key,
 1279         keyid=keyid,
 1280         profile=profile,
 1281     )
 1282     if not r:
 1283         ret["result"] = False
 1284         ret["comment"] = "Failed to terminate instance {}.".format(instance_id)
 1285         return ret
 1286 
 1287     ret["changes"]["old"] = {"instance_id": instance_id}
 1288     ret["changes"]["new"] = None
 1289 
 1290     if release_eip:
 1291         ip = getattr(instance, "ip_address", None)
 1292         if ip:
 1293             base_args = {
 1294                 "region": region,
 1295                 "key": key,
 1296                 "keyid": keyid,
 1297                 "profile": profile,
 1298             }
 1299             public_ip = None
 1300             alloc_id = None
 1301             assoc_id = None
 1302             if getattr(instance, "vpc_id", None):
 1303                 r = __salt__["boto_ec2.get_eip_address_info"](addresses=ip, **base_args)
 1304                 if r and "allocation_id" in r[0]:
 1305                     alloc_id = r[0]["allocation_id"]
 1306                     assoc_id = r[0].get("association_id")
 1307                 else:
 1308                     # I /believe/ this situation is impossible but let's hedge our bets...
 1309                     ret["result"] = False
 1310                     ret[
 1311                         "comment"
 1312                     ] = "Can't determine AllocationId for address {}.".format(ip)
 1313                     return ret
 1314             else:
 1315                 public_ip = instance.ip_address
 1316 
 1317             if assoc_id:
 1318                 # Race here - sometimes the terminate above will already have dropped this
 1319                 if not __salt__["boto_ec2.disassociate_eip_address"](
 1320                     association_id=assoc_id, **base_args
 1321                 ):
 1322                     log.warning("Failed to disassociate EIP %s.", ip)
 1323 
 1324             if __salt__["boto_ec2.release_eip_address"](
 1325                 allocation_id=alloc_id, public_ip=public_ip, **base_args
 1326             ):
 1327                 log.info("Released EIP address %s", public_ip or r[0]["public_ip"])
 1328                 ret["changes"]["old"]["public_ip"] = public_ip or r[0]["public_ip"]
 1329             else:
 1330                 ret["result"] = False
 1331                 ret["comment"] = "Failed to release EIP {}.".format(ip)
 1332                 return ret
 1333 
 1334     return ret
 1335 
 1336 
 1337 def volume_absent(
 1338     name,
 1339     volume_name=None,
 1340     volume_id=None,
 1341     instance_name=None,
 1342     instance_id=None,
 1343     device=None,
 1344     region=None,
 1345     key=None,
 1346     keyid=None,
 1347     profile=None,
 1348 ):
 1349     """
 1350     Ensure the EC2 volume is detached and absent.
 1351 
 1352     .. versionadded:: 2016.11.0
 1353 
 1354     name
 1355         State definition name.
 1356 
 1357     volume_name
 1358         Name tag associated with the volume.  For safety, if this matches more than
 1359         one volume, the state will refuse to apply.
 1360 
 1361     volume_id
 1362         Resource ID of the volume.
 1363 
 1364     instance_name
 1365         Only remove volume if it is attached to instance with this Name tag.
 1366         Exclusive with 'instance_id'.  Requires 'device'.
 1367 
 1368     instance_id
 1369         Only remove volume if it is attached to this instance.
 1370         Exclusive with 'instance_name'.  Requires 'device'.
 1371 
 1372     device
 1373         Match by device rather than ID.  Requires one of 'instance_name' or
 1374         'instance_id'.
 1375 
 1376     region
 1377         Region to connect to.
 1378 
 1379     key
 1380         Secret key to be used.
 1381 
 1382     keyid
 1383         Access key to be used.
 1384 
 1385     profile
 1386         A dict with region, key and keyid, or a pillar key (string)
 1387         that contains a dict with region, key and keyid.
 1388 
 1389     """
 1390 
 1391     ret = {"name": name, "result": True, "comment": "", "changes": {}}
 1392     filters = {}
 1393     running_states = ("pending", "rebooting", "running", "stopping", "stopped")
 1394 
 1395     if not salt.utils.data.exactly_one(
 1396         (volume_name, volume_id, instance_name, instance_id)
 1397     ):
 1398         raise SaltInvocationError(
 1399             "Exactly one of 'volume_name', 'volume_id', "
 1400             "'instance_name', or 'instance_id' must be provided."
 1401         )
 1402     if (instance_name or instance_id) and not device:
 1403         raise SaltInvocationError(
 1404             "Parameter 'device' is required when either "
 1405             "'instance_name' or 'instance_id' is specified."
 1406         )
 1407     if volume_id:
 1408         filters.update({"volume-id": volume_id})
 1409     if volume_name:
 1410         filters.update({"tag:Name": volume_name})
 1411     if instance_name:
 1412         instance_id = __salt__["boto_ec2.get_id"](
 1413             name=instance_name,
 1414             region=region,
 1415             key=key,
 1416             keyid=keyid,
 1417             profile=profile,
 1418             in_states=running_states,
 1419         )
 1420         if not instance_id:
 1421             ret["comment"] = (
 1422                 "Instance with Name {} not found.  Assuming "
 1423                 "associated volumes gone.".format(instance_name)
 1424             )
 1425             return ret
 1426     if instance_id:
 1427         filters.update({"attachment.instance-id": instance_id})
 1428     if device:
 1429         filters.update({"attachment.device": device})
 1430 
 1431     args = {"region": region, "key": key, "keyid": keyid, "profile": profile}
 1432 
 1433     vols = __salt__["boto_ec2.get_all_volumes"](filters=filters, **args)
 1434     if len(vols) < 1:
 1435         ret["comment"] = "Volume matching criteria not found, assuming already absent"
 1436         return ret
 1437     if len(vols) > 1:
 1438         msg = "More than one volume matched criteria, can't continue in state {}".format(
 1439             name
 1440         )
 1441         log.error(msg)
 1442         ret["comment"] = msg
 1443         ret["result"] = False
 1444         return ret
 1445     vol = vols[0]
 1446     log.info("Matched Volume ID %s", vol)
 1447 
 1448     if __opts__["test"]:
 1449         ret["comment"] = "The volume {} is set to be deleted.".format(vol)
 1450         ret["result"] = None
 1451         return ret
 1452     if __salt__["boto_ec2.delete_volume"](volume_id=vol, force=True, **args):
 1453         ret["comment"] = "Volume {} deleted.".format(vol)
 1454         ret["changes"] = {"old": {"volume_id": vol}, "new": {"volume_id": None}}
 1455     else:
 1456         ret["comment"] = "Error deleting volume {}.".format(vol)
 1457         ret["result"] = False
 1458     return ret
 1459 
 1460 
 1461 def volumes_tagged(
 1462     name, tag_maps, authoritative=False, region=None, key=None, keyid=None, profile=None
 1463 ):
 1464     """
 1465     Ensure EC2 volume(s) matching the given filters have the defined tags.
 1466 
 1467     .. versionadded:: 2016.11.0
 1468 
 1469     name
 1470         State definition name.
 1471 
 1472     tag_maps
 1473         List of dicts of filters and tags, where 'filters' is a dict suitable for passing
 1474         to the 'filters' argument of boto_ec2.get_all_volumes(), and 'tags' is a dict of
 1475         tags to be set on volumes as matched by the given filters.  The filter syntax is
 1476         extended to permit passing either a list of volume_ids or an instance_name (with
 1477         instance_name being the Name tag of the instance to which the desired volumes are
 1478         mapped).  Each mapping in the list is applied separately, so multiple sets of
 1479         volumes can be all tagged differently with one call to this function.
 1480 
 1481     YAML example fragment:
 1482 
 1483     .. code-block:: yaml
 1484 
 1485         - filters:
 1486             attachment.instance_id: i-abcdef12
 1487           tags:
 1488             Name: dev-int-abcdef12.aws-foo.com
 1489         - filters:
 1490             attachment.device: /dev/sdf
 1491           tags:
 1492             ManagedSnapshots: true
 1493             BillingGroup: bubba.hotep@aws-foo.com
 1494         - filters:
 1495             instance_name: prd-foo-01.aws-foo.com
 1496           tags:
 1497             Name: prd-foo-01.aws-foo.com
 1498             BillingGroup: infra-team@aws-foo.com
 1499         - filters:
 1500             volume_ids: [ vol-12345689, vol-abcdef12 ]
 1501           tags:
 1502             BillingGroup: infra-team@aws-foo.com
 1503 
 1504     authoritative
 1505         Should un-declared tags currently set on matched volumes be deleted?  Boolean.
 1506 
 1507     region
 1508         Region to connect to.
 1509 
 1510     key
 1511         Secret key to be used.
 1512 
 1513     keyid
 1514         Access key to be used.
 1515 
 1516     profile
 1517         A dict with region, key and keyid, or a pillar key (string)
 1518         that contains a dict with region, key and keyid.
 1519 
 1520     """
 1521 
 1522     ret = {"name": name, "result": True, "comment": "", "changes": {}}
 1523     args = {
 1524         "tag_maps": tag_maps,
 1525         "authoritative": authoritative,
 1526         "region": region,
 1527         "key": key,
 1528         "keyid": keyid,
 1529         "profile": profile,
 1530     }
 1531 
 1532     if __opts__["test"]:
 1533         args["dry_run"] = True
 1534         r = __salt__["boto_ec2.set_volumes_tags"](**args)
 1535         if r["success"]:
 1536             if r.get("changes"):
 1537                 ret["comment"] = "Tags would be updated."
 1538                 ret["changes"] = r["changes"]
 1539                 ret["result"] = None
 1540         else:
 1541             ret["comment"] = "Error validating requested volume tags."
 1542             ret["result"] = False
 1543         return ret
 1544     r = __salt__["boto_ec2.set_volumes_tags"](**args)
 1545     if r["success"]:
 1546         if r.get("changes"):
 1547             ret["comment"] = "Tags applied."
 1548             ret["changes"] = r["changes"]
 1549     else:
 1550         ret["comment"] = "Error updating requested volume tags."
 1551         ret["result"] = False
 1552     return ret
 1553 
 1554 
 1555 def volume_present(
 1556     name,
 1557     volume_name=None,
 1558     volume_id=None,
 1559     instance_name=None,
 1560     instance_id=None,
 1561     device=None,
 1562     size=None,
 1563     snapshot_id=None,
 1564     volume_type=None,
 1565     iops=None,
 1566     encrypted=False,
 1567     kms_key_id=None,
 1568     region=None,
 1569     key=None,
 1570     keyid=None,
 1571     profile=None,
 1572 ):
 1573     """
 1574     Ensure the EC2 volume is present and attached.
 1575 
 1576     ..
 1577 
 1578     name
 1579         State definition name.
 1580 
 1581     volume_name
 1582         The Name tag value for the volume. If no volume with that matching name tag is found,
 1583         a new volume will be created. If multiple volumes are matched, the state will fail.
 1584 
 1585     volume_id
 1586         Resource ID of the volume. Exclusive with 'volume_name'.
 1587 
 1588     instance_name
 1589         Attach volume to instance with this Name tag.
 1590         Exclusive with 'instance_id'.
 1591 
 1592     instance_id
 1593         Attach volume to instance with this ID.
 1594         Exclusive with 'instance_name'.
 1595 
 1596     device
 1597         The device on the instance through which the volume is exposed (e.g. /dev/sdh)
 1598 
 1599     size
 1600         The size of the new volume, in GiB. If you're creating the volume from a snapshot
 1601         and don't specify a volume size, the default is the snapshot size. Optionally specified
 1602         at volume creation time; will be ignored afterward. Requires 'volume_name'.
 1603 
 1604     snapshot_id
 1605         The snapshot ID from which the new Volume will be created. Optionally specified
 1606         at volume creation time; will be ignored afterward. Requires 'volume_name'.
 1607 
 1608     volume_type
 1609         The type of the volume. Optionally specified at volume creation time; will be ignored afterward.
 1610         Requires 'volume_name'.
 1611         Valid volume types for AWS can be found here:
 1612         http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html
 1613 
 1614     iops
 1615         The provisioned IOPS you want to associate with this volume. Optionally specified
 1616         at volume creation time; will be ignored afterward. Requires 'volume_name'.
 1617 
 1618     encrypted
 1619         Specifies whether the volume should be encrypted. Optionally specified
 1620         at volume creation time; will be ignored afterward. Requires 'volume_name'.
 1621 
 1622     kms_key_id
 1623         If encrypted is True, this KMS Key ID may be specified to encrypt volume with this key.
 1624         Optionally specified at volume creation time; will be ignored afterward.
 1625         Requires 'volume_name'.
 1626         e.g.: arn:aws:kms:us-east-1:012345678910:key/abcd1234-a123-456a-a12b-a123b4cd56ef
 1627 
 1628     region
 1629         Region to connect to.
 1630 
 1631     key
 1632         Secret key to be used.
 1633 
 1634     keyid
 1635         Access key to be used.
 1636 
 1637     profile
 1638         A dict with region, key and keyid, or a pillar key (string)
 1639         that contains a dict with region, key and keyid.
 1640 
 1641     """
 1642 
 1643     ret = {"name": name, "result": True, "comment": "", "changes": {}}
 1644     old_dict = {}
 1645     new_dict = {}
 1646     running_states = ("running", "stopped")
 1647 
 1648     if not salt.utils.data.exactly_one((volume_name, volume_id)):
 1649         raise SaltInvocationError(
 1650             "Exactly one of 'volume_name', 'volume_id', " " must be provided."
 1651         )
 1652     if not salt.utils.data.exactly_one((instance_name, instance_id)):
 1653         raise SaltInvocationError(
 1654             "Exactly one of 'instance_name', or 'instance_id'" " must be provided."
 1655         )
 1656     if device is None:
 1657         raise SaltInvocationError("Parameter 'device' is required.")
 1658     args = {"region": region, "key": key, "keyid": keyid, "profile": profile}
 1659     if instance_name:
 1660         instance_id = __salt__["boto_ec2.get_id"](
 1661             name=instance_name, in_states=running_states, **args
 1662         )
 1663         if not instance_id:
 1664             raise SaltInvocationError(
 1665                 "Instance with Name {} not found.".format(instance_name)
 1666             )
 1667 
 1668     instances = __salt__["boto_ec2.find_instances"](
 1669         instance_id=instance_id, return_objs=True, **args
 1670     )
 1671     instance = instances[0]
 1672     if volume_name:
 1673         filters = {}
 1674         filters.update({"tag:Name": volume_name})
 1675         vols = __salt__["boto_ec2.get_all_volumes"](filters=filters, **args)
 1676         if len(vols) > 1:
 1677             msg = "More than one volume matched volume name {}, can't continue in state {}".format(
 1678                 volume_name, name
 1679             )
 1680             raise SaltInvocationError(msg)
 1681         if len(vols) < 1:
 1682             if __opts__["test"]:
 1683                 ret["comment"] = (
 1684                     "The volume with name {} is set to be created and attached"
 1685                     " on {}({}).".format(volume_name, instance_id, device)
 1686                 )
 1687                 ret["result"] = None
 1688                 return ret
 1689             _rt = __salt__["boto_ec2.create_volume"](
 1690                 zone_name=instance.placement,
 1691                 size=size,
 1692                 snapshot_id=snapshot_id,
 1693                 volume_type=volume_type,
 1694                 iops=iops,
 1695                 encrypted=encrypted,
 1696                 kms_key_id=kms_key_id,
 1697                 wait_for_creation=True,
 1698                 **args
 1699             )
 1700             if "result" in _rt:
 1701                 volume_id = _rt["result"]
 1702             else:
 1703                 raise SaltInvocationError(
 1704                     "Error creating volume with name {}.".format(volume_name)
 1705                 )
 1706             _rt = __salt__["boto_ec2.set_volumes_tags"](
 1707                 tag_maps=[
 1708                     {
 1709                         "filters": {"volume_ids": [volume_id]},
 1710                         "tags": {"Name": volume_name},
 1711                     }
 1712                 ],
 1713                 **args
 1714             )
 1715             if _rt["success"] is False:
 1716                 raise SaltInvocationError(
 1717                     "Error updating requested volume "
 1718                     "{} with name {}. {}".format(volume_id, volume_name, _rt["comment"])
 1719                 )
 1720             old_dict["volume_id"] = None
 1721             new_dict["volume_id"] = volume_id
 1722         else:
 1723             volume_id = vols[0]
 1724     vols = __salt__["boto_ec2.get_all_volumes"](
 1725         volume_ids=[volume_id], return_objs=True, **args
 1726     )
 1727     if len(vols) < 1:
 1728         raise SaltInvocationError("Volume {} do not exist".format(volume_id))
 1729     vol = vols[0]
 1730     if vol.zone != instance.placement:
 1731         raise SaltInvocationError(
 1732             ("Volume {} in {} cannot attach to instance" " {} in {}.").format(
 1733                 volume_id, vol.zone, instance_id, instance.placement
 1734             )
 1735         )
 1736     attach_data = vol.attach_data
 1737     if attach_data is not None and attach_data.instance_id is not None:
 1738         if instance_id == attach_data.instance_id and device == attach_data.device:
 1739             ret["comment"] = "The volume {} is attached on {}({}).".format(
 1740                 volume_id, instance_id, device
 1741             )
 1742             return ret
 1743         else:
 1744             if __opts__["test"]:
 1745                 ret["comment"] = (
 1746                     "The volume {} is set to be detached"
 1747                     " from {}({} and attached on {}({})."
 1748                 ).format(
 1749                     attach_data.instance_id,
 1750                     attach_data.devic,
 1751                     volume_id,
 1752                     instance_id,
 1753                     device,
 1754                 )
 1755                 ret["result"] = None
 1756                 return ret
 1757             if __salt__["boto_ec2.detach_volume"](
 1758                 volume_id=volume_id, wait_for_detachement=True, **args
 1759             ):
 1760                 ret["comment"] = "Volume {} is detached from {}({}).".format(
 1761                     volume_id, attach_data.instance_id, attach_data.device
 1762                 )
 1763                 old_dict["instance_id"] = attach_data.instance_id
 1764                 old_dict["device"] = attach_data.device
 1765             else:
 1766                 raise SaltInvocationError(
 1767                     (
 1768                         "The volume {} is already attached on instance {}({})."
 1769                         " Failed to detach"
 1770                     ).format(volume_id, attach_data.instance_id, attach_data.device)
 1771                 )
 1772     else:
 1773         old_dict["instance_id"] = instance_id
 1774         old_dict["device"] = None
 1775     if __opts__["test"]:
 1776         ret["comment"] = "The volume {} is set to be attached on {}({}).".format(
 1777             volume_id, instance_id, device
 1778         )
 1779         ret["result"] = None
 1780         return ret
 1781     if __salt__["boto_ec2.attach_volume"](
 1782         volume_id=volume_id, instance_id=instance_id, device=device, **args
 1783     ):
 1784         ret["comment"] = " ".join(
 1785             [
 1786                 ret["comment"],
 1787                 "Volume {} is attached on {}({}).".format(
 1788                     volume_id, instance_id, device
 1789                 ),
 1790             ]
 1791         )
 1792         new_dict["instance_id"] = instance_id
 1793         new_dict["device"] = device
 1794         ret["changes"] = {"old": old_dict, "new": new_dict}
 1795     else:
 1796         ret["comment"] = "Error attaching volume {} to instance {}({}).".format(
 1797             volume_id, instance_id, device
 1798         )
 1799         ret["result"] = False
 1800     return ret
 1801 
 1802 
 1803 def private_ips_present(
 1804     name,
 1805     network_interface_name=None,
 1806     network_interface_id=None,
 1807     private_ip_addresses=None,
 1808     allow_reassignment=False,
 1809     region=None,
 1810     key=None,
 1811     keyid=None,
 1812     profile=None,
 1813 ):
 1814     """
 1815     Ensure an ENI has secondary private ip addresses associated with it
 1816 
 1817     name
 1818         (String) - State definition name
 1819     network_interface_id
 1820         (String) - The EC2 network interface id, example eni-123456789
 1821     private_ip_addresses
 1822         (List or String) - The secondary private ip address(es) that should be present on the ENI.
 1823     allow_reassignment
 1824         (Boolean) - If true, will reassign a secondary private ip address associated with another
 1825         ENI. If false, state will fail if the secondary private ip address is associated with
 1826         another ENI.
 1827     region
 1828         (string) - Region to connect to.
 1829     key
 1830         (string) - Secret key to be used.
 1831     keyid
 1832         (string) - Access key to be used.
 1833     profile
 1834         (variable) - A dict with region, key and keyid, or a pillar key (string) that contains a
 1835         dict with region, key and keyid.
 1836     """
 1837 
 1838     if not salt.utils.data.exactly_one((network_interface_name, network_interface_id)):
 1839         raise SaltInvocationError(
 1840             "Exactly one of 'network_interface_name', "
 1841             "'network_interface_id' must be provided"
 1842         )
 1843 
 1844     if not private_ip_addresses:
 1845         raise SaltInvocationError(
 1846             "You must provide the private_ip_addresses to associate with the " "ENI"
 1847         )
 1848 
 1849     ret = {
 1850         "name": name,
 1851         "result": True,
 1852         "comment": "",
 1853         "changes": {"old": [], "new": []},
 1854     }
 1855 
 1856     get_eni_args = {
 1857         "name": network_interface_name,
 1858         "network_interface_id": network_interface_id,
 1859         "region": region,
 1860         "key": key,
 1861         "keyid": keyid,
 1862         "profile": profile,
 1863     }
 1864 
 1865     eni = __salt__["boto_ec2.get_network_interface"](**get_eni_args)
 1866 
 1867     # Check if there are any new secondary private ips to add to the eni
 1868     if eni and eni.get("result", {}).get("private_ip_addresses"):
 1869         for eni_pip in eni["result"]["private_ip_addresses"]:
 1870             ret["changes"]["old"].append(eni_pip["private_ip_address"])
 1871 
 1872     ips_to_add = []
 1873     for private_ip in private_ip_addresses:
 1874         if private_ip not in ret["changes"]["old"]:
 1875             ips_to_add.append(private_ip)
 1876 
 1877     if ips_to_add:
 1878         if not __opts__["test"]:
 1879             # Assign secondary private ips to ENI
 1880             assign_ips_args = {
 1881                 "network_interface_id": network_interface_id,
 1882                 "private_ip_addresses": ips_to_add,
 1883                 "allow_reassignment": allow_reassignment,
 1884                 "region": region,
 1885                 "key": key,
 1886                 "keyid": keyid,
 1887                 "profile": profile,
 1888             }
 1889 
 1890             __salt__["boto_ec2.assign_private_ip_addresses"](**assign_ips_args)
 1891 
 1892             # Verify secondary private ips were properly assigned to ENI
 1893             eni = __salt__["boto_ec2.get_network_interface"](**get_eni_args)
 1894             if eni and eni.get("result", {}).get("private_ip_addresses", None):
 1895                 for eni_pip in eni["result"]["private_ip_addresses"]:
 1896                     ret["changes"]["new"].append(eni_pip["private_ip_address"])
 1897 
 1898             ips_not_added = []
 1899             for private_ip in private_ip_addresses:
 1900                 if private_ip not in ret["changes"]["new"]:
 1901                     ips_not_added.append(private_ip)
 1902 
 1903             # Display results
 1904             if ips_not_added:
 1905                 ret["result"] = False
 1906                 ret["comment"] = (
 1907                     "ips on eni: {}\n"
 1908                     "attempted to add: {}\n"
 1909                     "could not add the following ips: {}\n".format(
 1910                         "\n\t- " + "\n\t- ".join(ret["changes"]["new"]),
 1911                         "\n\t- " + "\n\t- ".join(ips_to_add),
 1912                         "\n\t- " + "\n\t- ".join(ips_not_added),
 1913                     )
 1914                 )
 1915             else:
 1916                 ret["comment"] = "added ips: {}".format(
 1917                     "\n\t- " + "\n\t- ".join(ips_to_add)
 1918                 )
 1919 
 1920             # Verify there were changes
 1921             if ret["changes"]["old"] == ret["changes"]["new"]:
 1922                 ret["changes"] = {}
 1923 
 1924         else:
 1925             # Testing mode, show that there were ips to add
 1926             ret["comment"] = "ips on eni: {}\n" "ips that would be added: {}\n".format(
 1927                 "\n\t- " + "\n\t- ".join(ret["changes"]["old"]),
 1928                 "\n\t- " + "\n\t- ".join(ips_to_add),
 1929             )
 1930             ret["changes"] = {}
 1931             ret["result"] = None
 1932 
 1933     else:
 1934         ret["comment"] = "ips on eni: {}".format(
 1935             "\n\t- " + "\n\t- ".join(ret["changes"]["old"])
 1936         )
 1937 
 1938         # there were no changes since we did not attempt to remove ips
 1939         ret["changes"] = {}
 1940 
 1941     return ret
 1942 
 1943 
 1944 def private_ips_absent(
 1945     name,
 1946     network_interface_name=None,
 1947     network_interface_id=None,
 1948     private_ip_addresses=None,
 1949     region=None,
 1950     key=None,
 1951     keyid=None,
 1952     profile=None,
 1953 ):
 1954 
 1955     """
 1956     Ensure an ENI does not have secondary private ip addresses associated with it
 1957 
 1958     name
 1959         (String) - State definition name
 1960     network_interface_id
 1961         (String) - The EC2 network interface id, example eni-123456789
 1962     private_ip_addresses
 1963         (List or String) - The secondary private ip address(es) that should be absent on the ENI.
 1964     region
 1965         (string) - Region to connect to.
 1966     key
 1967         (string) - Secret key to be used.
 1968     keyid
 1969         (string) - Access key to be used.
 1970     profile
 1971         (variable) - A dict with region, key and keyid, or a pillar key (string) that contains a
 1972         dict with region, key and keyid.
 1973     """
 1974 
 1975     if not salt.utils.data.exactly_one((network_interface_name, network_interface_id)):
 1976         raise SaltInvocationError(
 1977             "Exactly one of 'network_interface_name', "
 1978             "'network_interface_id' must be provided"
 1979         )
 1980 
 1981     if not private_ip_addresses:
 1982         raise SaltInvocationError(
 1983             "You must provide the private_ip_addresses to unassociate with " "the ENI"
 1984         )
 1985     if not isinstance(private_ip_addresses, list):
 1986         private_ip_addresses = [private_ip_addresses]
 1987 
 1988     ret = {
 1989         "name": name,
 1990         "result": True,
 1991         "comment": "",
 1992         "changes": {"new": [], "old": []},
 1993     }
 1994 
 1995     get_eni_args = {
 1996         "name": network_interface_name,
 1997         "network_interface_id": network_interface_id,
 1998         "region": region,
 1999         "key": key,
 2000         "keyid": keyid,
 2001         "profile": profile,
 2002     }
 2003 
 2004     eni = __salt__["boto_ec2.get_network_interface"](**get_eni_args)
 2005 
 2006     # Check if there are any old private ips to remove from the eni
 2007     primary_private_ip = None
 2008     if eni and eni.get("result", {}).get("private_ip_addresses"):
 2009         for eni_pip in eni["result"]["private_ip_addresses"]:
 2010             ret["changes"]["old"].append(eni_pip["private_ip_address"])
 2011             if eni_pip["primary"]:
 2012                 primary_private_ip = eni_pip["private_ip_address"]
 2013 
 2014     ips_to_remove = []
 2015     for private_ip in private_ip_addresses:
 2016         if private_ip in ret["changes"]["old"]:
 2017             ips_to_remove.append(private_ip)
 2018         if private_ip == primary_private_ip:
 2019             ret["result"] = False
 2020             ret["comment"] = (
 2021                 "You cannot unassign the primary private ip address ({}) on an "
 2022                 "eni\n"
 2023                 "ips on eni: {}\n"
 2024                 "attempted to remove: {}\n".format(
 2025                     primary_private_ip,
 2026                     "\n\t- " + "\n\t- ".join(ret["changes"]["old"]),
 2027                     "\n\t- " + "\n\t- ".join(private_ip_addresses),
 2028                 )
 2029             )
 2030             ret["changes"] = {}
 2031             return ret
 2032 
 2033     if ips_to_remove:
 2034         if not __opts__["test"]:
 2035             # Unassign secondary private ips to ENI
 2036             assign_ips_args = {
 2037                 "network_interface_id": network_interface_id,
 2038                 "private_ip_addresses": ips_to_remove,
 2039                 "region": region,
 2040                 "key": key,
 2041                 "keyid": keyid,
 2042                 "profile": profile,
 2043             }
 2044 
 2045             __salt__["boto_ec2.unassign_private_ip_addresses"](**assign_ips_args)
 2046 
 2047             # Verify secondary private ips were properly unassigned from ENI
 2048             eni = __salt__["boto_ec2.get_network_interface"](**get_eni_args)
 2049             if eni and eni.get("result", {}).get("private_ip_addresses", None):
 2050                 for eni_pip in eni["result"]["private_ip_addresses"]:
 2051                     ret["changes"]["new"].append(eni_pip["private_ip_address"])
 2052             ips_not_removed = []
 2053             for private_ip in private_ip_addresses:
 2054                 if private_ip in ret["changes"]["new"]:
 2055                     ips_not_removed.append(private_ip)
 2056 
 2057             if ips_not_removed:
 2058                 ret["result"] = False
 2059                 ret["comment"] = (
 2060                     "ips on eni: {}\n"
 2061                     "attempted to remove: {}\n"
 2062                     "could not remove the following ips: {}\n".format(
 2063                         "\n\t- " + "\n\t- ".join(ret["changes"]["new"]),
 2064                         "\n\t- " + "\n\t- ".join(ips_to_remove),
 2065                         "\n\t- " + "\n\t- ".join(ips_not_removed),
 2066                     )
 2067                 )
 2068             else:
 2069                 ret["comment"] = "removed ips: {}".format(
 2070                     "\n\t- " + "\n\t- ".join(ips_to_remove)
 2071                 )
 2072 
 2073             # Verify there were changes
 2074             if ret["changes"]["old"] == ret["changes"]["new"]:
 2075                 ret["changes"] = {}
 2076 
 2077         else:
 2078             # Testing mode, show that there were ips to remove
 2079             ret["comment"] = (
 2080                 "ips on eni: {}\n"
 2081                 "ips that would be removed: {}\n".format(
 2082                     "\n\t- " + "\n\t- ".join(ret["changes"]["old"]),
 2083                     "\n\t- " + "\n\t- ".join(ips_to_remove),
 2084                 )
 2085             )
 2086             ret["changes"] = {}
 2087             ret["result"] = None
 2088 
 2089     else:
 2090         ret["comment"] = "ips on network interface: {}".format(
 2091             "\n\t- " + "\n\t- ".join(ret["changes"]["old"])
 2092         )
 2093 
 2094         # there were no changes since we did not attempt to remove ips
 2095         ret["changes"] = {}
 2096 
 2097     return ret