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