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