1"""
2Manage Lambda Functions
3=======================
4
5.. versionadded:: 2016.3.0
6
7Create and destroy Lambda Functions. Be aware that this interacts with Amazon's services,
8and so may incur charges.
9
10:depends:
11    - boto
12    - boto3
13
14The dependencies listed above can be installed via package or pip.
15
16This module accepts explicit vpc credentials but can also utilize
17IAM roles assigned to the instance through Instance Profiles. Dynamic
18credentials are then automatically obtained from AWS API and no further
19configuration is necessary. More information available `here
20<http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html>`_.
21
22If IAM roles are not used you need to specify them either in a pillar file or
23in the minion's config file:
24
25.. code-block:: yaml
26
27    vpc.keyid: GKTADJGHEIQSXMKKRBJ08H
28    vpc.key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
29
30It's also possible to specify ``key``, ``keyid`` and ``region`` via a profile,
31either passed in as a dict, or as a string to pull from pillars or minion
32config:
33
34.. code-block:: yaml
35
36    myprofile:
37        keyid: GKTADJGHEIQSXMKKRBJ08H
38        key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
39            region: us-east-1
40
41.. code-block:: yaml
42
43    Ensure function exists:
44        boto_lambda.function_present:
45            - FunctionName: myfunction
46            - Runtime: python2.7
47            - Role: iam_role_name
48            - Handler: entry_function
49            - ZipFile: code.zip
50            - S3Bucket: bucketname
51            - S3Key: keyname
52            - S3ObjectVersion: version
53            - Description: "My Lambda Function"
54            - Timeout: 3
55            - MemorySize: 128
56            - region: us-east-1
57            - keyid: GKTADJGHEIQSXMKKRBJ08H
58            - key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
59
60"""
61
62
63import hashlib
64import logging
65import os
66
67import salt.utils.data
68import salt.utils.dictupdate as dictupdate
69import salt.utils.files
70import salt.utils.json
71from salt.exceptions import SaltInvocationError
72
73log = logging.getLogger(__name__)
74
75
76def __virtual__():
77    """
78    Only load if boto is available.
79    """
80    if "boto_lambda.function_exists" in __salt__:
81        return "boto_lambda"
82    return (False, "boto_lambda module could not be loaded")
83
84
85def function_present(
86    name,
87    FunctionName,
88    Runtime,
89    Role,
90    Handler,
91    ZipFile=None,
92    S3Bucket=None,
93    S3Key=None,
94    S3ObjectVersion=None,
95    Description="",
96    Timeout=3,
97    MemorySize=128,
98    Permissions=None,
99    RoleRetries=5,
100    region=None,
101    key=None,
102    keyid=None,
103    profile=None,
104    VpcConfig=None,
105    Environment=None,
106):
107    """
108    Ensure function exists.
109
110    name
111        The name of the state definition
112
113    FunctionName
114        Name of the Function.
115
116    Runtime
117        The Runtime environment for the function. One of
118        'nodejs', 'java8', or 'python2.7'
119
120    Role
121        The name or ARN of the IAM role that the function assumes when it executes your
122        function to access any other AWS resources.
123
124    Handler
125        The function within your code that Lambda calls to begin execution. For Node.js it is the
126        module-name.*export* value in your function. For Java, it can be package.classname::handler or
127        package.class-name.
128
129    ZipFile
130        A path to a .zip file containing your deployment package. If this is
131        specified, S3Bucket and S3Key must not be specified.
132
133    S3Bucket
134        Amazon S3 bucket name where the .zip file containing your package is
135        stored. If this is specified, S3Key must be specified and ZipFile must
136        NOT be specified.
137
138    S3Key
139        The Amazon S3 object (the deployment package) key name you want to
140        upload. If this is specified, S3Key must be specified and ZipFile must
141        NOT be specified.
142
143    S3ObjectVersion
144        The version of S3 object to use. Optional, should only be specified if
145        S3Bucket and S3Key are specified.
146
147    Description
148        A short, user-defined function description. Lambda does not use this value. Assign a meaningful
149        description as you see fit.
150
151    Timeout
152        The function execution time at which Lambda should terminate this function. Because the execution
153        time has cost implications, we recommend you set this value based on your expected execution time.
154        The default is 3 seconds.
155
156    MemorySize
157        The amount of memory, in MB, your function is given. Lambda uses this memory size to infer
158        the amount of CPU and memory allocated to your function. Your function use-case determines your
159        CPU and memory requirements. For example, a database operation might need less memory compared
160        to an image processing function. The default value is 128 MB. The value must be a multiple of
161        64 MB.
162
163    VpcConfig
164        If your Lambda function accesses resources in a VPC, you must provide this parameter
165        identifying the list of security group IDs/Names and subnet IDs/Name.  These must all belong
166        to the same VPC.  This is a dict of the form:
167
168        .. code-block:: yaml
169
170            VpcConfig:
171              SecurityGroupNames:
172                - mysecgroup1
173                - mysecgroup2
174              SecurityGroupIds:
175                - sg-abcdef1234
176              SubnetNames:
177                - mysubnet1
178              SubnetIds:
179                - subnet-1234abcd
180                - subnet-abcd1234
181
182        If VpcConfig is provided at all, you MUST pass at least one security group and one subnet.
183
184    Permissions
185        A list of permission definitions to be added to the function's policy
186
187    RoleRetries
188        IAM Roles may take some time to propagate to all regions once created.
189        During that time function creation may fail; this state will
190        atuomatically retry this number of times. The default is 5.
191
192    Environment
193        The parent object that contains your environment's configuration
194        settings.  This is a dictionary of the form:
195
196        .. code-block:: python
197
198            {
199                'Variables': {
200                    'VariableName': 'VariableValue'
201                }
202            }
203
204        .. versionadded:: 2017.7.0
205
206    region
207        Region to connect to.
208
209    key
210        Secret key to be used.
211
212    keyid
213        Access key to be used.
214
215    profile
216        A dict with region, key and keyid, or a pillar key (string) that
217        contains a dict with region, key and keyid.
218    """
219    ret = {"name": FunctionName, "result": True, "comment": "", "changes": {}}
220
221    if Permissions is not None:
222        if isinstance(Permissions, str):
223            Permissions = salt.utils.json.loads(Permissions)
224        required_keys = {"Action", "Principal"}
225        optional_keys = {"SourceArn", "SourceAccount", "Qualifier"}
226        for sid, permission in Permissions.items():
227            keyset = set(permission.keys())
228            if not keyset.issuperset(required_keys):
229                raise SaltInvocationError(
230                    "{} are required for each permission specification".format(
231                        ", ".join(required_keys)
232                    )
233                )
234            keyset = keyset - required_keys
235            keyset = keyset - optional_keys
236            if bool(keyset):
237                raise SaltInvocationError(
238                    "Invalid permission value {}".format(", ".join(keyset))
239                )
240
241    r = __salt__["boto_lambda.function_exists"](
242        FunctionName=FunctionName, region=region, key=key, keyid=keyid, profile=profile
243    )
244
245    if "error" in r:
246        ret["result"] = False
247        ret["comment"] = "Failed to create function: {}.".format(r["error"]["message"])
248        return ret
249
250    if not r.get("exists"):
251        if __opts__["test"]:
252            ret["comment"] = "Function {} is set to be created.".format(FunctionName)
253            ret["result"] = None
254            return ret
255        r = __salt__["boto_lambda.create_function"](
256            FunctionName=FunctionName,
257            Runtime=Runtime,
258            Role=Role,
259            Handler=Handler,
260            ZipFile=ZipFile,
261            S3Bucket=S3Bucket,
262            S3Key=S3Key,
263            S3ObjectVersion=S3ObjectVersion,
264            Description=Description,
265            Timeout=Timeout,
266            MemorySize=MemorySize,
267            VpcConfig=VpcConfig,
268            Environment=Environment,
269            WaitForRole=True,
270            RoleRetries=RoleRetries,
271            region=region,
272            key=key,
273            keyid=keyid,
274            profile=profile,
275        )
276        if not r.get("created"):
277            ret["result"] = False
278            ret["comment"] = "Failed to create function: {}.".format(
279                r["error"]["message"]
280            )
281            return ret
282
283        if Permissions:
284            for sid, permission in Permissions.items():
285                r = __salt__["boto_lambda.add_permission"](
286                    FunctionName=FunctionName,
287                    StatementId=sid,
288                    region=region,
289                    key=key,
290                    keyid=keyid,
291                    profile=profile,
292                    **permission
293                )
294                if not r.get("updated"):
295                    ret["result"] = False
296                    ret["comment"] = "Failed to create function: {}.".format(
297                        r["error"]["message"]
298                    )
299
300        _describe = __salt__["boto_lambda.describe_function"](
301            FunctionName, region=region, key=key, keyid=keyid, profile=profile
302        )
303        _describe["function"]["Permissions"] = __salt__["boto_lambda.get_permissions"](
304            FunctionName, region=region, key=key, keyid=keyid, profile=profile
305        )["permissions"]
306        ret["changes"]["old"] = {"function": None}
307        ret["changes"]["new"] = _describe
308        ret["comment"] = "Function {} created.".format(FunctionName)
309        return ret
310
311    ret["comment"] = os.linesep.join(
312        [ret["comment"], "Function {} is present.".format(FunctionName)]
313    )
314    ret["changes"] = {}
315    # function exists, ensure config matches
316    _ret = _function_config_present(
317        FunctionName,
318        Role,
319        Handler,
320        Description,
321        Timeout,
322        MemorySize,
323        VpcConfig,
324        Environment,
325        region,
326        key,
327        keyid,
328        profile,
329        RoleRetries,
330    )
331    if not _ret.get("result"):
332        ret["result"] = _ret.get("result", False)
333        ret["comment"] = _ret["comment"]
334        ret["changes"] = {}
335        return ret
336    ret["changes"] = dictupdate.update(ret["changes"], _ret["changes"])
337    ret["comment"] = " ".join([ret["comment"], _ret["comment"]])
338    _ret = _function_code_present(
339        FunctionName,
340        ZipFile,
341        S3Bucket,
342        S3Key,
343        S3ObjectVersion,
344        region,
345        key,
346        keyid,
347        profile,
348    )
349    if not _ret.get("result"):
350        ret["result"] = _ret.get("result", False)
351        ret["comment"] = _ret["comment"]
352        ret["changes"] = {}
353        return ret
354    ret["changes"] = dictupdate.update(ret["changes"], _ret["changes"])
355    ret["comment"] = " ".join([ret["comment"], _ret["comment"]])
356    _ret = _function_permissions_present(
357        FunctionName, Permissions, region, key, keyid, profile
358    )
359    if not _ret.get("result"):
360        ret["result"] = _ret.get("result", False)
361        ret["comment"] = _ret["comment"]
362        ret["changes"] = {}
363        return ret
364    ret["changes"] = dictupdate.update(ret["changes"], _ret["changes"])
365    ret["comment"] = " ".join([ret["comment"], _ret["comment"]])
366    return ret
367
368
369def _get_role_arn(name, region=None, key=None, keyid=None, profile=None):
370    if name.startswith("arn:aws:iam:"):
371        return name
372
373    account_id = __salt__["boto_iam.get_account_id"](
374        region=region, key=key, keyid=keyid, profile=profile
375    )
376    return "arn:aws:iam::{}:role/{}".format(account_id, name)
377
378
379def _resolve_vpcconfig(conf, region=None, key=None, keyid=None, profile=None):
380    if isinstance(conf, str):
381        conf = salt.utils.json.loads(conf)
382    if not conf:
383        # if the conf is None, we should explicitly set the VpcConfig to
384        # {'SubnetIds': [], 'SecurityGroupIds': []} to take the lambda out of
385        # the VPC it was in
386        return {"SubnetIds": [], "SecurityGroupIds": []}
387    if not isinstance(conf, dict):
388        raise SaltInvocationError("VpcConfig must be a dict.")
389    sns = [
390        __salt__["boto_vpc.get_resource_id"](
391            "subnet", s, region=region, key=key, keyid=keyid, profile=profile
392        ).get("id")
393        for s in conf.pop("SubnetNames", [])
394    ]
395    sgs = [
396        __salt__["boto_secgroup.get_group_id"](
397            s, region=region, key=key, keyid=keyid, profile=profile
398        )
399        for s in conf.pop("SecurityGroupNames", [])
400    ]
401    conf.setdefault("SubnetIds", []).extend(sns)
402    conf.setdefault("SecurityGroupIds", []).extend(sgs)
403    return conf
404
405
406def _function_config_present(
407    FunctionName,
408    Role,
409    Handler,
410    Description,
411    Timeout,
412    MemorySize,
413    VpcConfig,
414    Environment,
415    region,
416    key,
417    keyid,
418    profile,
419    RoleRetries,
420):
421    ret = {"result": True, "comment": "", "changes": {}}
422    func = __salt__["boto_lambda.describe_function"](
423        FunctionName, region=region, key=key, keyid=keyid, profile=profile
424    )["function"]
425    need_update = False
426    options = {
427        "Role": _get_role_arn(Role, region, key, keyid, profile),
428        "Handler": Handler,
429        "Description": Description,
430        "Timeout": Timeout,
431        "MemorySize": MemorySize,
432    }
433
434    for key, val in options.items():
435        if func[key] != val:
436            need_update = True
437            ret["changes"].setdefault("old", {})[key] = func[key]
438            ret["changes"].setdefault("new", {})[key] = val
439    # VpcConfig returns the extra value 'VpcId' so do a special compare
440    oldval = func.get("VpcConfig")
441    if oldval is not None:
442        oldval.pop("VpcId", None)
443    fixed_VpcConfig = _resolve_vpcconfig(VpcConfig, region, key, keyid, profile)
444    if __utils__["boto3.ordered"](oldval) != __utils__["boto3.ordered"](
445        fixed_VpcConfig
446    ):
447        need_update = True
448        ret["changes"].setdefault("new", {})["VpcConfig"] = fixed_VpcConfig
449        ret["changes"].setdefault("old", {})["VpcConfig"] = func.get("VpcConfig")
450
451    if Environment is not None:
452        if func.get("Environment") != Environment:
453            need_update = True
454            ret["changes"].setdefault("new", {})["Environment"] = Environment
455            ret["changes"].setdefault("old", {})["Environment"] = func.get(
456                "Environment"
457            )
458
459    if need_update:
460        ret["comment"] = os.linesep.join(
461            [ret["comment"], "Function config to be modified"]
462        )
463        if __opts__["test"]:
464            ret["comment"] = "Function {} set to be modified.".format(FunctionName)
465            ret["result"] = None
466            return ret
467        _r = __salt__["boto_lambda.update_function_config"](
468            FunctionName=FunctionName,
469            Role=Role,
470            Handler=Handler,
471            Description=Description,
472            Timeout=Timeout,
473            MemorySize=MemorySize,
474            VpcConfig=fixed_VpcConfig,
475            Environment=Environment,
476            region=region,
477            key=key,
478            keyid=keyid,
479            profile=profile,
480            WaitForRole=True,
481            RoleRetries=RoleRetries,
482        )
483        if not _r.get("updated"):
484            ret["result"] = False
485            ret["comment"] = "Failed to update function: {}.".format(
486                _r["error"]["message"]
487            )
488            ret["changes"] = {}
489    return ret
490
491
492def _function_code_present(
493    FunctionName, ZipFile, S3Bucket, S3Key, S3ObjectVersion, region, key, keyid, profile
494):
495    ret = {"result": True, "comment": "", "changes": {}}
496    func = __salt__["boto_lambda.describe_function"](
497        FunctionName, region=region, key=key, keyid=keyid, profile=profile
498    )["function"]
499    update = False
500    if ZipFile:
501        if "://" in ZipFile:  # Looks like a remote URL to me...
502            dlZipFile = __salt__["cp.cache_file"](path=ZipFile)
503            if dlZipFile is False:
504                ret["result"] = False
505                ret["comment"] = "Failed to cache ZipFile `{}`.".format(ZipFile)
506                return ret
507            ZipFile = dlZipFile
508        size = os.path.getsize(ZipFile)
509        if size == func["CodeSize"]:
510            sha = hashlib.sha256()
511            with salt.utils.files.fopen(ZipFile, "rb") as f:
512                sha.update(f.read())
513            hashed = sha.digest().encode("base64").strip()
514            if hashed != func["CodeSha256"]:
515                update = True
516        else:
517            update = True
518    else:
519        # No way to judge whether the item in the s3 bucket is current without
520        # downloading it. Cheaper to just request an update every time, and still
521        # idempotent
522        update = True
523    if update:
524        if __opts__["test"]:
525            ret["comment"] = "Function {} set to be modified.".format(FunctionName)
526            ret["result"] = None
527            return ret
528        ret["changes"]["old"] = {
529            "CodeSha256": func["CodeSha256"],
530            "CodeSize": func["CodeSize"],
531        }
532        func = __salt__["boto_lambda.update_function_code"](
533            FunctionName,
534            ZipFile,
535            S3Bucket,
536            S3Key,
537            S3ObjectVersion,
538            region=region,
539            key=key,
540            keyid=keyid,
541            profile=profile,
542        )
543        if not func.get("updated"):
544            ret["result"] = False
545            ret["comment"] = "Failed to update function: {}.".format(
546                func["error"]["message"]
547            )
548            ret["changes"] = {}
549            return ret
550        func = func["function"]
551        if (
552            func["CodeSha256"] != ret["changes"]["old"]["CodeSha256"]
553            or func["CodeSize"] != ret["changes"]["old"]["CodeSize"]
554        ):
555            ret["comment"] = os.linesep.join(
556                [ret["comment"], "Function code to be modified"]
557            )
558            ret["changes"]["new"] = {
559                "CodeSha256": func["CodeSha256"],
560                "CodeSize": func["CodeSize"],
561            }
562        else:
563            del ret["changes"]["old"]
564    return ret
565
566
567def _function_permissions_present(
568    FunctionName, Permissions, region, key, keyid, profile
569):
570    ret = {"result": True, "comment": "", "changes": {}}
571    curr_permissions = __salt__["boto_lambda.get_permissions"](
572        FunctionName, region=region, key=key, keyid=keyid, profile=profile
573    ).get("permissions")
574    if curr_permissions is None:
575        curr_permissions = {}
576    need_update = False
577    diffs = salt.utils.data.compare_dicts(curr_permissions, Permissions or {})
578    if bool(diffs):
579        ret["comment"] = os.linesep.join(
580            [ret["comment"], "Function permissions to be modified"]
581        )
582        if __opts__["test"]:
583            ret["comment"] = "Function {} set to be modified.".format(FunctionName)
584            ret["result"] = None
585            return ret
586        for sid, diff in diffs.items():
587            if diff.get("old", "") != "":
588                # There's a permssion that needs to be removed
589                _r = __salt__["boto_lambda.remove_permission"](
590                    FunctionName=FunctionName,
591                    StatementId=sid,
592                    region=region,
593                    key=key,
594                    keyid=keyid,
595                    profile=profile,
596                )
597                ret["changes"].setdefault("new", {}).setdefault("Permissions", {})[
598                    sid
599                ] = {}
600                ret["changes"].setdefault("old", {}).setdefault("Permissions", {})[
601                    sid
602                ] = diff["old"]
603            if diff.get("new", "") != "":
604                # New permission information needs to be added
605                _r = __salt__["boto_lambda.add_permission"](
606                    FunctionName=FunctionName,
607                    StatementId=sid,
608                    region=region,
609                    key=key,
610                    keyid=keyid,
611                    profile=profile,
612                    **diff["new"]
613                )
614                ret["changes"].setdefault("new", {}).setdefault("Permissions", {})[
615                    sid
616                ] = diff["new"]
617                oldperms = (
618                    ret["changes"].setdefault("old", {}).setdefault("Permissions", {})
619                )
620                if sid not in oldperms:
621                    oldperms[sid] = {}
622            if not _r.get("updated"):
623                ret["result"] = False
624                ret["comment"] = "Failed to update function: {}.".format(
625                    _r["error"]["message"]
626                )
627                ret["changes"] = {}
628    return ret
629
630
631def function_absent(
632    name, FunctionName, region=None, key=None, keyid=None, profile=None
633):
634    """
635    Ensure function with passed properties is absent.
636
637    name
638        The name of the state definition.
639
640    FunctionName
641        Name of the function.
642
643    region
644        Region to connect to.
645
646    key
647        Secret key to be used.
648
649    keyid
650        Access key to be used.
651
652    profile
653        A dict with region, key and keyid, or a pillar key (string) that
654        contains a dict with region, key and keyid.
655    """
656
657    ret = {"name": FunctionName, "result": True, "comment": "", "changes": {}}
658
659    r = __salt__["boto_lambda.function_exists"](
660        FunctionName, region=region, key=key, keyid=keyid, profile=profile
661    )
662    if "error" in r:
663        ret["result"] = False
664        ret["comment"] = "Failed to delete function: {}.".format(r["error"]["message"])
665        return ret
666
667    if r and not r["exists"]:
668        ret["comment"] = "Function {} does not exist.".format(FunctionName)
669        return ret
670
671    if __opts__["test"]:
672        ret["comment"] = "Function {} is set to be removed.".format(FunctionName)
673        ret["result"] = None
674        return ret
675    r = __salt__["boto_lambda.delete_function"](
676        FunctionName, region=region, key=key, keyid=keyid, profile=profile
677    )
678    if not r["deleted"]:
679        ret["result"] = False
680        ret["comment"] = "Failed to delete function: {}.".format(r["error"]["message"])
681        return ret
682    ret["changes"]["old"] = {"function": FunctionName}
683    ret["changes"]["new"] = {"function": None}
684    ret["comment"] = "Function {} deleted.".format(FunctionName)
685    return ret
686
687
688def alias_present(
689    name,
690    FunctionName,
691    Name,
692    FunctionVersion,
693    Description="",
694    region=None,
695    key=None,
696    keyid=None,
697    profile=None,
698):
699    """
700    Ensure alias exists.
701
702    name
703        The name of the state definition.
704
705    FunctionName
706        Name of the function for which you want to create an alias.
707
708    Name
709        The name of the alias to be created.
710
711    FunctionVersion
712        Function version for which you are creating the alias.
713
714    Description
715        A short, user-defined function description. Lambda does not use this value. Assign a meaningful
716        description as you see fit.
717
718    region
719        Region to connect to.
720
721    key
722        Secret key to be used.
723
724    keyid
725        Access key to be used.
726
727    profile
728        A dict with region, key and keyid, or a pillar key (string) that
729        contains a dict with region, key and keyid.
730    """
731    ret = {"name": Name, "result": True, "comment": "", "changes": {}}
732
733    r = __salt__["boto_lambda.alias_exists"](
734        FunctionName=FunctionName,
735        Name=Name,
736        region=region,
737        key=key,
738        keyid=keyid,
739        profile=profile,
740    )
741
742    if "error" in r:
743        ret["result"] = False
744        ret["comment"] = "Failed to create alias: {}.".format(r["error"]["message"])
745        return ret
746
747    if not r.get("exists"):
748        if __opts__["test"]:
749            ret["comment"] = "Alias {} is set to be created.".format(Name)
750            ret["result"] = None
751            return ret
752        r = __salt__["boto_lambda.create_alias"](
753            FunctionName,
754            Name,
755            FunctionVersion,
756            Description,
757            region,
758            key,
759            keyid,
760            profile,
761        )
762        if not r.get("created"):
763            ret["result"] = False
764            ret["comment"] = "Failed to create alias: {}.".format(r["error"]["message"])
765            return ret
766        _describe = __salt__["boto_lambda.describe_alias"](
767            FunctionName, Name, region=region, key=key, keyid=keyid, profile=profile
768        )
769        ret["changes"]["old"] = {"alias": None}
770        ret["changes"]["new"] = _describe
771        ret["comment"] = "Alias {} created.".format(Name)
772        return ret
773
774    ret["comment"] = os.linesep.join(
775        [ret["comment"], "Alias {} is present.".format(Name)]
776    )
777    ret["changes"] = {}
778    _describe = __salt__["boto_lambda.describe_alias"](
779        FunctionName, Name, region=region, key=key, keyid=keyid, profile=profile
780    )["alias"]
781
782    need_update = False
783    options = {"FunctionVersion": FunctionVersion, "Description": Description}
784
785    for key, val in options.items():
786        if _describe[key] != val:
787            need_update = True
788            ret["changes"].setdefault("old", {})[key] = _describe[key]
789            ret["changes"].setdefault("new", {})[key] = val
790    if need_update:
791        ret["comment"] = os.linesep.join(
792            [ret["comment"], "Alias config to be modified"]
793        )
794        if __opts__["test"]:
795            ret["comment"] = "Alias {} set to be modified.".format(Name)
796            ret["result"] = None
797            return ret
798        _r = __salt__["boto_lambda.update_alias"](
799            FunctionName=FunctionName,
800            Name=Name,
801            FunctionVersion=FunctionVersion,
802            Description=Description,
803            region=region,
804            key=key,
805            keyid=keyid,
806            profile=profile,
807        )
808        if not _r.get("updated"):
809            ret["result"] = False
810            ret["comment"] = "Failed to update alias: {}.".format(
811                _r["error"]["message"]
812            )
813            ret["changes"] = {}
814    return ret
815
816
817def alias_absent(
818    name, FunctionName, Name, region=None, key=None, keyid=None, profile=None
819):
820    """
821    Ensure alias with passed properties is absent.
822
823    name
824        The name of the state definition.
825
826    FunctionName
827        Name of the function.
828
829    Name
830        Name of the alias.
831
832    region
833        Region to connect to.
834
835    key
836        Secret key to be used.
837
838    keyid
839        Access key to be used.
840
841    profile
842        A dict with region, key and keyid, or a pillar key (string) that
843        contains a dict with region, key and keyid.
844    """
845
846    ret = {"name": Name, "result": True, "comment": "", "changes": {}}
847
848    r = __salt__["boto_lambda.alias_exists"](
849        FunctionName, Name, region=region, key=key, keyid=keyid, profile=profile
850    )
851    if "error" in r:
852        ret["result"] = False
853        ret["comment"] = "Failed to delete alias: {}.".format(r["error"]["message"])
854        return ret
855
856    if r and not r["exists"]:
857        ret["comment"] = "Alias {} does not exist.".format(Name)
858        return ret
859
860    if __opts__["test"]:
861        ret["comment"] = "Alias {} is set to be removed.".format(Name)
862        ret["result"] = None
863        return ret
864    r = __salt__["boto_lambda.delete_alias"](
865        FunctionName, Name, region=region, key=key, keyid=keyid, profile=profile
866    )
867    if not r["deleted"]:
868        ret["result"] = False
869        ret["comment"] = "Failed to delete alias: {}.".format(r["error"]["message"])
870        return ret
871    ret["changes"]["old"] = {"alias": Name}
872    ret["changes"]["new"] = {"alias": None}
873    ret["comment"] = "Alias {} deleted.".format(Name)
874    return ret
875
876
877def _get_function_arn(name, region=None, key=None, keyid=None, profile=None):
878    if name.startswith("arn:aws:lambda:"):
879        return name
880
881    account_id = __salt__["boto_iam.get_account_id"](
882        region=region, key=key, keyid=keyid, profile=profile
883    )
884    if profile and "region" in profile:
885        region = profile["region"]
886    if region is None:
887        region = "us-east-1"
888    return "arn:aws:lambda:{}:{}:function:{}".format(region, account_id, name)
889
890
891def event_source_mapping_present(
892    name,
893    EventSourceArn,
894    FunctionName,
895    StartingPosition,
896    Enabled=True,
897    BatchSize=100,
898    region=None,
899    key=None,
900    keyid=None,
901    profile=None,
902):
903    """
904    Ensure event source mapping exists.
905
906    name
907        The name of the state definition.
908
909    EventSourceArn
910        The Amazon Resource Name (ARN) of the Amazon Kinesis or the Amazon
911        DynamoDB stream that is the event source.
912
913    FunctionName
914        The Lambda function to invoke when AWS Lambda detects an event on the
915        stream.
916
917        You can specify an unqualified function name (for example, "Thumbnail")
918        or you can specify Amazon Resource Name (ARN) of the function (for
919        example, "arn:aws:lambda:us-west-2:account-id:function:ThumbNail"). AWS
920        Lambda also allows you to specify only the account ID qualifier (for
921        example, "account-id:Thumbnail"). Note that the length constraint
922        applies only to the ARN. If you specify only the function name, it is
923        limited to 64 character in length.
924
925    StartingPosition
926        The position in the stream where AWS Lambda should start reading.
927        (TRIM_HORIZON | LATEST)
928
929    Enabled
930        Indicates whether AWS Lambda should begin polling the event source. By
931        default, Enabled is true.
932
933    BatchSize
934        The largest number of records that AWS Lambda will retrieve from your
935        event source at the time of invoking your function. Your function
936        receives an event with all the retrieved records. The default is 100
937        records.
938
939    region
940        Region to connect to.
941
942    key
943        Secret key to be used.
944
945    keyid
946        Access key to be used.
947
948    profile
949        A dict with region, key and keyid, or a pillar key (string) that
950        contains a dict with region, key and keyid.
951    """
952    ret = {"name": None, "result": True, "comment": "", "changes": {}}
953
954    r = __salt__["boto_lambda.event_source_mapping_exists"](
955        EventSourceArn=EventSourceArn,
956        FunctionName=FunctionName,
957        region=region,
958        key=key,
959        keyid=keyid,
960        profile=profile,
961    )
962
963    if "error" in r:
964        ret["result"] = False
965        ret["comment"] = "Failed to create event source mapping: {}.".format(
966            r["error"]["message"]
967        )
968        return ret
969
970    if not r.get("exists"):
971        if __opts__["test"]:
972            ret["comment"] = "Event source mapping {} is set to be created.".format(
973                FunctionName
974            )
975            ret["result"] = None
976            return ret
977        r = __salt__["boto_lambda.create_event_source_mapping"](
978            EventSourceArn=EventSourceArn,
979            FunctionName=FunctionName,
980            StartingPosition=StartingPosition,
981            Enabled=Enabled,
982            BatchSize=BatchSize,
983            region=region,
984            key=key,
985            keyid=keyid,
986            profile=profile,
987        )
988        if not r.get("created"):
989            ret["result"] = False
990            ret["comment"] = "Failed to create event source mapping: {}.".format(
991                r["error"]["message"]
992            )
993            return ret
994        _describe = __salt__["boto_lambda.describe_event_source_mapping"](
995            EventSourceArn=EventSourceArn,
996            FunctionName=FunctionName,
997            region=region,
998            key=key,
999            keyid=keyid,
1000            profile=profile,
1001        )
1002        ret["name"] = _describe["event_source_mapping"]["UUID"]
1003        ret["changes"]["old"] = {"event_source_mapping": None}
1004        ret["changes"]["new"] = _describe
1005        ret["comment"] = "Event source mapping {} created.".format(ret["name"])
1006        return ret
1007
1008    ret["comment"] = os.linesep.join(
1009        [ret["comment"], "Event source mapping is present."]
1010    )
1011    ret["changes"] = {}
1012    _describe = __salt__["boto_lambda.describe_event_source_mapping"](
1013        EventSourceArn=EventSourceArn,
1014        FunctionName=FunctionName,
1015        region=region,
1016        key=key,
1017        keyid=keyid,
1018        profile=profile,
1019    )["event_source_mapping"]
1020
1021    need_update = False
1022    options = {"BatchSize": BatchSize}
1023
1024    for key, val in options.items():
1025        if _describe[key] != val:
1026            need_update = True
1027            ret["changes"].setdefault("old", {})[key] = _describe[key]
1028            ret["changes"].setdefault("new", {})[key] = val
1029    # verify FunctionName against FunctionArn
1030    function_arn = _get_function_arn(
1031        FunctionName, region=region, key=key, keyid=keyid, profile=profile
1032    )
1033    if _describe["FunctionArn"] != function_arn:
1034        need_update = True
1035        ret["changes"].setdefault("new", {})["FunctionArn"] = function_arn
1036        ret["changes"].setdefault("old", {})["FunctionArn"] = _describe["FunctionArn"]
1037    # TODO check for 'Enabled', since it doesn't directly map to a specific
1038    # state
1039    if need_update:
1040        ret["comment"] = os.linesep.join(
1041            [ret["comment"], "Event source mapping to be modified"]
1042        )
1043        if __opts__["test"]:
1044            ret["comment"] = "Event source mapping {} set to be modified.".format(
1045                _describe["UUID"]
1046            )
1047            ret["result"] = None
1048            return ret
1049        _r = __salt__["boto_lambda.update_event_source_mapping"](
1050            UUID=_describe["UUID"],
1051            FunctionName=FunctionName,
1052            Enabled=Enabled,
1053            BatchSize=BatchSize,
1054            region=region,
1055            key=key,
1056            keyid=keyid,
1057            profile=profile,
1058        )
1059        if not _r.get("updated"):
1060            ret["result"] = False
1061            ret["comment"] = "Failed to update mapping: {}.".format(
1062                _r["error"]["message"]
1063            )
1064            ret["changes"] = {}
1065    return ret
1066
1067
1068def event_source_mapping_absent(
1069    name, EventSourceArn, FunctionName, region=None, key=None, keyid=None, profile=None
1070):
1071    """
1072    Ensure event source mapping with passed properties is absent.
1073
1074    name
1075        The name of the state definition.
1076
1077    EventSourceArn
1078        ARN of the event source.
1079
1080    FunctionName
1081        Name of the lambda function.
1082
1083    region
1084        Region to connect to.
1085
1086    key
1087        Secret key to be used.
1088
1089    keyid
1090        Access key to be used.
1091
1092    profile
1093        A dict with region, key and keyid, or a pillar key (string) that
1094        contains a dict with region, key and keyid.
1095    """
1096
1097    ret = {"name": None, "result": True, "comment": "", "changes": {}}
1098
1099    desc = __salt__["boto_lambda.describe_event_source_mapping"](
1100        EventSourceArn=EventSourceArn,
1101        FunctionName=FunctionName,
1102        region=region,
1103        key=key,
1104        keyid=keyid,
1105        profile=profile,
1106    )
1107    if "error" in desc:
1108        ret["result"] = False
1109        ret["comment"] = "Failed to delete event source mapping: {}.".format(
1110            desc["error"]["message"]
1111        )
1112        return ret
1113
1114    if not desc.get("event_source_mapping"):
1115        ret["comment"] = "Event source mapping does not exist."
1116        return ret
1117
1118    ret["name"] = desc["event_source_mapping"]["UUID"]
1119    if __opts__["test"]:
1120        ret["comment"] = "Event source mapping is set to be removed."
1121        ret["result"] = None
1122        return ret
1123    r = __salt__["boto_lambda.delete_event_source_mapping"](
1124        EventSourceArn=EventSourceArn,
1125        FunctionName=FunctionName,
1126        region=region,
1127        key=key,
1128        keyid=keyid,
1129        profile=profile,
1130    )
1131    if not r["deleted"]:
1132        ret["result"] = False
1133        ret["comment"] = "Failed to delete event source mapping: {}.".format(
1134            r["error"]["message"]
1135        )
1136        return ret
1137    ret["changes"]["old"] = desc
1138    ret["changes"]["new"] = {"event_source_mapping": None}
1139    ret["comment"] = "Event source mapping deleted."
1140    return ret
1141