From f242d01a4bfa6802f5c537ca1e69179f378b6fe1 Mon Sep 17 00:00:00 2001 From: aws Date: Tue, 27 Nov 2018 00:10:53 +0000 Subject: [PATCH] Release of Version 1.3.0 --- CHANGELOG.rst | 19 + LICENSE | 402 +++++++++--------- MANIFEST.in | 2 + README.md | 2 - README.rst | 69 +++ examples/BinaryLambdaInvoke/invokee.py | 14 + examples/BinaryLambdaInvoke/invoker.py | 42 ++ examples/HelloWorld/greengrassHelloWorld.py | 56 +++ .../Storyline_MessageLambda/messageLambda.py | 30 ++ .../Storyline_UptimeLambda/uptimeLambda.py | 30 ++ examples/TES/README | 74 ++++ examples/TES/lambda_function.py | 35 ++ examples/TrafficLight/carAggregator.py | 136 ++++++ greengrasssdk/IoTDataPlane.py | 154 +++++++ greengrasssdk/Lambda.py | 135 ++++++ greengrasssdk/SecretsManager.py | 160 +++++++ greengrasssdk/__init__.py | 9 + greengrasssdk/client.py | 16 + greengrasssdk/utils/__init__.py | 0 greengrasssdk/utils/testing.py | 35 ++ setup.cfg | 2 + setup.py | 49 +++ 22 files changed, 1268 insertions(+), 203 deletions(-) create mode 100644 CHANGELOG.rst create mode 100644 MANIFEST.in delete mode 100644 README.md create mode 100644 README.rst create mode 100644 examples/BinaryLambdaInvoke/invokee.py create mode 100644 examples/BinaryLambdaInvoke/invoker.py create mode 100644 examples/HelloWorld/greengrassHelloWorld.py create mode 100644 examples/Storyline_MessageLambda/messageLambda.py create mode 100644 examples/Storyline_UptimeLambda/uptimeLambda.py create mode 100644 examples/TES/README create mode 100644 examples/TES/lambda_function.py create mode 100644 examples/TrafficLight/carAggregator.py create mode 100644 greengrasssdk/IoTDataPlane.py create mode 100644 greengrasssdk/Lambda.py create mode 100644 greengrasssdk/SecretsManager.py create mode 100644 greengrasssdk/__init__.py create mode 100644 greengrasssdk/client.py create mode 100644 greengrasssdk/utils/__init__.py create mode 100644 greengrasssdk/utils/testing.py create mode 100644 setup.cfg create mode 100644 setup.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..ec3221d --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,19 @@ +========= +CHANGELOG +========= + +1.3.0 +====== + +SDK supports SecretsManager client. + + +1.2.0 +====== + +SDK and GGC compatibility check takes place in the background. + + +1.1.0 +====== +Lambda only accepted payload in JSON format. With this update, Invoking or publishing binary payload to a lambda is supported. diff --git a/LICENSE b/LICENSE index 8dada3e..f3b7ffc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,201 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2012-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..5e51007 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include *.txt *.py +recursive-include *.txt *.py \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 0ed1b2a..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# aws-greengrass-core-sdk-python -SDK to use with functions running on Greengrass Core using Python diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..6b71048 --- /dev/null +++ b/README.rst @@ -0,0 +1,69 @@ +Greengrass SDK +===================== + +The AWS Greengrass Core SDK is meant to be used by AWS Lambda functions running on an AWS Greengrass Core. It will enable Lambda functions to invoke other Lambda functions deployed to the Greengrass Core, publish messages to the Greengrass Core and work with the local Shadow service. +You can find the latest, most up to date, documentation at our `doc site `_. + +=============================== +Using AWS Greengrass Core SDK +=============================== + +To use the AWS Greengrass Core SDK, you must first import the AWS Greengrass Core SDK in your Lambda function as you would with any other external libraries. You then need to create a client for 'iot-data' or 'lambda'. Use 'iot-data' if you wish to publish messages to the local AWS Greengrass Core and interact with the local Shadow service. Use 'lambda' if you wish to invoke other Lambda functions deployed to the same AWS Greengrass Core. + +Here is an example for using the 'iot-data' client + +.. code-block:: python + + import greengrasssdk + + # Let's instantiate the iot-data client + client = greengrasssdk.client('iot-data') + + +Now that you have an ``iot-data`` client, you can publish requests. + +.. code-block:: python + + response = client.publish( + topic='someTopic', + payload='some data'.encode() + ) + +Here is an example for using the 'lambda' client. + +.. code-block:: python + + import greengrasssdk + + client = greengrasssdk.client('lambda') + +Now that you have a lambda client, you can publish requests. + +.. code-block:: python + + # Define the payload to pass to the invoked lambda function + msg = json.dumps({ + 'message':"hello" + }) + + # Invoke the lambda function + response = client.invoke( + FunctionName='arn:aws:lambda:::function:', + InvocationType='RequestResponse', + Payload=payload, + Qualifier='2' + ) + +============== +Compatibility +============== + +As new features are added to AWS Greengrass, previous versions of the Greengrass SDK will be incompatible with newer versions of the AWS Greengrass core. The following table lists the compatible SDKs for all GGC releases. + ++-------------+------------------------+ +| GGC Version | Compatible SDK Versions| ++=============+========================+ +| 1.0.x-1.6.x | 1.0.x-1.2.x | ++-------------+------------------------+ +| 1.7.x | 1.0.x-1.3.x | ++-------------+------------------------+ diff --git a/examples/BinaryLambdaInvoke/invokee.py b/examples/BinaryLambdaInvoke/invokee.py new file mode 100644 index 0000000..bfec294 --- /dev/null +++ b/examples/BinaryLambdaInvoke/invokee.py @@ -0,0 +1,14 @@ +# +# Copyright 2010-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +import sys +import logging + +# Setup logging to stdout +logger = logging.getLogger(__name__) +logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) + + +def handler(event, context): + logger.info('Invoked with payload ' + str(event)) + return 'Invoked successfully' diff --git a/examples/BinaryLambdaInvoke/invoker.py b/examples/BinaryLambdaInvoke/invoker.py new file mode 100644 index 0000000..2379d77 --- /dev/null +++ b/examples/BinaryLambdaInvoke/invoker.py @@ -0,0 +1,42 @@ +# +# Copyright 2010-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# + +# This example demonstrates invoking a lambda with 'binary' encoding type. In +# order to run this example, remember to mark your 'invokee' lambda as a binary +# lambda. You can configure this on the lambda configuration page in the +# console. After the lambdas get deployed to your Greengrass Core, you should +# be able to see 'Invoked successfully' returned by 'invokee' lambda. A lambda +# function can support non-json payload, which is a new feature introduced in +# GGC version 1.5. +# +import sys +import base64 +import logging +import json +import greengrasssdk + +# Setup logging to stdout +logger = logging.getLogger(__name__) +logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) + +client = greengrasssdk.client('lambda') + + +def handler(event, context): + client_context = json.dumps({ + 'custom': 'custom text' + }) + + try: + response = client.invoke( + ClientContext=base64.b64encode(bytes(client_context)), + FunctionName='arn:aws:lambda:::function::', + InvocationType='RequestResponse', + Payload='Non-JSON Data', + Qualifier='1' + ) + + logger.info(response['Payload'].read()) + except Exception as e: + logger.error(e) diff --git a/examples/HelloWorld/greengrassHelloWorld.py b/examples/HelloWorld/greengrassHelloWorld.py new file mode 100644 index 0000000..5bbe8ff --- /dev/null +++ b/examples/HelloWorld/greengrassHelloWorld.py @@ -0,0 +1,56 @@ +# +# Copyright 2010-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# + +# greengrassHelloWorld.py +# Demonstrates a simple publish to a topic using Greengrass core sdk +# This lambda function will retrieve underlying platform information and send +# a hello world message along with the platform information to the topic +# 'hello/world'. The function will sleep for five seconds, then repeat. +# Since the function is long-lived it will run forever when deployed to a +# Greengrass core. The handler will NOT be invoked in our example since +# the we are executing an infinite loop. + +import greengrasssdk +import platform +from threading import Timer + + +# Creating a greengrass core sdk client +client = greengrasssdk.client('iot-data') + +# Retrieving platform information to send from Greengrass Core +my_platform = platform.platform() + + +# When deployed to a Greengrass core, this code will be executed immediately +# as a long-lived lambda function. The code will enter the infinite while +# loop below. +# If you execute a 'test' on the Lambda Console, this test will fail by +# hitting the execution timeout of three seconds. This is expected as +# this function never returns a result. + +def greengrass_hello_world_run(): + if not my_platform: + client.publish( + topic='hello/world', + payload='Hello world! Sent from Greengrass Core.') + else: + client.publish( + topic='hello/world', + payload='Hello world! Sent from ' + 'Greengrass Core running on platform: {}' + .format(my_platform)) + + # Asynchronously schedule this function to be run again in 5 seconds + Timer(5, greengrass_hello_world_run).start() + + +# Start executing the function above +greengrass_hello_world_run() + + +# This is a dummy handler and will not be invoked +# Instead the code above will be executed in an infinite loop for our example +def function_handler(event, context): + return diff --git a/examples/Storyline_MessageLambda/messageLambda.py b/examples/Storyline_MessageLambda/messageLambda.py new file mode 100644 index 0000000..466a7f5 --- /dev/null +++ b/examples/Storyline_MessageLambda/messageLambda.py @@ -0,0 +1,30 @@ +# +# Copyright 2010-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# + +import sys +import logging +import greengrasssdk + +# Setup logging to stdout +logger = logging.getLogger(__name__) +logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) + +client = greengrasssdk.client('iot-data') + + +def message_handler(event, context): + logger.info("Received message!") + if 'state' in event: + if event['state'] == "on": + client.update_thing_shadow( + thingName="RobotArm_Thing", + payload='{"state":{"desired":{"myState":"on"}}}') + logger.info("Triggering publish to shadow " + "topic to set state to ON") + elif event['state'] == "off": + client.update_thing_shadow( + thingName="RobotArm_Thing", + payload='{"state":{"desired":{"myState":"off"}}}') + logger.info("Triggering publish to shadow " + "topic to set state to OFF") diff --git a/examples/Storyline_UptimeLambda/uptimeLambda.py b/examples/Storyline_UptimeLambda/uptimeLambda.py new file mode 100644 index 0000000..69fabeb --- /dev/null +++ b/examples/Storyline_UptimeLambda/uptimeLambda.py @@ -0,0 +1,30 @@ +# +# Copyright 2010-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# + +import sys +import logging +import greengrasssdk + +# Setup logging to stdout +logger = logging.getLogger(__name__) +logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) + +client = greengrasssdk.client('iot-data') + + +def uptime_handler(event, context): + logger.info("Received message!") + if 'state' in event: + if event['state'] == "on": + client.publish( + topic='/topic/metering', + payload="Robot arm turned ON") + logger.info("Triggering publish to topic " + "/topic/metering with ON state") + elif event['state'] == "off": + client.publish( + topic='/topic/metering', + payload="Robot arm turned OFF") + logger.info("Triggering publish to topic " + "/topic/metering with OFF state") diff --git a/examples/TES/README b/examples/TES/README new file mode 100644 index 0000000..60e4f0c --- /dev/null +++ b/examples/TES/README @@ -0,0 +1,74 @@ +The TES example package contains a Lambda function that, when run, +will attempt to retrieve AWS credentials. The goal of TES is to allow +users to retrieve credentials without having them hard-coded on the +system itself. + +Assuming no credentials exist on the system, when the Lambda function +is run, temporary credentials will be sourced from the cloud. They +will be formatted as . + +The code sample performs logger initialization and credential retrieval +is outside of the function handler. This means that a pinned lambda +should be used to test functionality. + +In case you would like to use an On-Demand Lambda function, you will then +need to create a subscription to invoke the Lambda function +(so that the handler is executed). + +### SETUP ### +1. In order to use this example, you will need to include greengrasssdk, boto3, +botocore and any of the dependencies those libraries require in the zip file before +uploading it. + +2. After creating a zip file, create a Lambda function +in the Lambda Console. + + Handler: lambda_function.lambda_handler + Runtime: python2.7 + Timeout: 3s + Role: any basic execution role can be used here + +3. Create a group that contains your Greengrass Core and the Lambda +function you've just created. + +4. Deploy the latest Greengrass release to your device. + +5. Ensure that your logging configuration includes either a logging +configuration for CW, the FileSystem, or both. For example: + + "Logging": { + "Content": [ + { + "Type": "FileSystem", + "Component": "GreengrassSystem", + "Level": "DEBUG", + "Space": 25600 + }, + { + "Type": "FileSystem", + "Component": "Lambda", + "Level": "DEBUG", + "Space": 25600 + }, + { + "Type": "AWSCloudWatch", + "Component": "Lambda", + "Level": "DEBUG" + }, + { + "Type": "AWSCloudWatch", + "Component": "GreengrassSystem", + "Level": "DEBUG" + } + ] + } + +6. Make a deployment to your Greengrass Core, then start your Greengrass +Core. The core will check for any new deployments and will proceed to +download the newest configuration file (group.json) as well as the Lambda +function you've created. + +7. Check either the local logs or CloudWatch logs for output from the function. + + Local Path: /greengrass/var/log/user///.log + CloudWatch: /aws/greengrass/Lambda/// diff --git a/examples/TES/lambda_function.py b/examples/TES/lambda_function.py new file mode 100644 index 0000000..32425e0 --- /dev/null +++ b/examples/TES/lambda_function.py @@ -0,0 +1,35 @@ +# +# Copyright 2010-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# + +from botocore.session import Session +import greengrasssdk + +import sys +import logging + +# Setup logging to stdout +logger = logging.getLogger(__name__) +logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) + +client = greengrasssdk.client('iot-data') + +logger.info('Hello from pinned lambda. Outside of handler.') + +# Get creds from TES +# Note: must make sure that creds are not available within local folder +# Can get cred info from /greengrass/var/log/system/tes.log +session = Session() +creds = session.get_credentials() +formatted_creds = """ +Access Key: {}\n +Secret Key: {}\n +Session Key: {}\n""".format(creds.access_key, creds.secret_key, creds.token) + +# Logging credential information is not recommended. This is for demonstration purposes only. +# logger.info(formatted_creds) + + +def lambda_handler(event, context): + logger.debug("Hello from pinned lambda. Inside handler.") + return diff --git a/examples/TrafficLight/carAggregator.py b/examples/TrafficLight/carAggregator.py new file mode 100644 index 0000000..f3e31cc --- /dev/null +++ b/examples/TrafficLight/carAggregator.py @@ -0,0 +1,136 @@ +# +# Copyright 2010-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# + +# carAggregator.py +# *********************** IMPORTANT *********************** +# This is part of the Traffic Light example and requires both LightController +# and TrafficLight GGADS to work properly. +# This also requires setup steps including permissions before it will work. +# Please refer to module 6 in Greengrass getting started guide for +# directions. +# ************************************************************************** + +# This example demonstrates how state can be tracked in a pinned lambda and how +# to interface with AWS, +# This lambda function will listens to shadow MQTT message on light status. +# When the light is green, it generates a random number to represent the number +# of cars that passed. +# This function stores statistics on these numbers and uploads them to DynamoDB +# on every fourth green light. +# Since this function is long-lived it will run forever when deployed to a +# Greengrass core. + +import logging +import boto3 +from datetime import datetime +from random import randint +from botocore.exceptions import ClientError + +# initialized dynamo db client +# Note this creates a dynamodb table in region us-east-1 (N. Virginia) +# Change the region name to something different if you like +# Note endpoint and aws credentials are not specified. By default this +# uses the credentials configured for the session. See Boto 3 docs +# for more details. +dynamodb = boto3.resource('dynamodb', region_name='us-east-1') +tableName = "CarStats" + +# Create the dynamo db table if needed +try: + table = dynamodb.create_table( + TableName=tableName, + KeySchema=[ + { + 'AttributeName': 'Time', + 'KeyType': 'HASH' # Partition key + } + ], + AttributeDefinitions=[ + { + 'AttributeName': 'Time', + 'AttributeType': 'S' + } + ], + ProvisionedThroughput={ + 'ReadCapacityUnits': 5, + 'WriteCapacityUnits': 5 + } + ) + + # Wait until the table exists. + table.meta.client.get_waiter('table_exists').wait(TableName=tableName) +except ClientError as e: + if e.response['Error']['Code'] == 'ResourceInUseException': + print("Table already created") + else: + raise e + +# initialize the logger +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +# This is a long lived lambda so we can keep state as below +totalTraffic = 0 +totalGreenlights = 0 +minCars = -1 +maxCars = -1 + +# This handler is called when an event is sent via MQTT +# Event targets are set in subscriptions settings +# This should be set up to listen to shadow document updates +# This function gets traffic light updates from the shadow MQTT event +# On every Green light it does the following: +# passing cars are simulated by a random number 1 <= n <= 20 +# the minimum and maximum cars passing during a green light are tracked +# the total number of cars passing during all green lights are tracked +# On every 3rd Green light these stats are sent to CarStats dynamodb table +# using a timestamp as the hash key + + +def function_handler(event, context): + global totalTraffic + global totalGreenlights + global minCars + global maxCars + + # grab the light status from the event + # Shadow JSON schema: + # { "state": { "desired": { "property": } } } + logger.info(event) + lightValue = event["current"]["state"]["reported"]["property"] + logger.info("reported light state: " + lightValue) + if lightValue == 'G': + logger.info("Green light") + + # generate a random number of cars passing during this green light + cars = randint(1, 20) + + # update stats + totalTraffic += cars + totalGreenlights += 1 + if cars < minCars or minCars == -1: + minCars = cars + if cars > maxCars: + maxCars = cars + + logger.info("Cars passed during green light: " + str(cars)) + logger.info("Total Traffic: " + str(totalTraffic)) + logger.info("Total Greenlights: " + str(totalGreenlights)) + logger.info("Minimum Cars passing: " + str(minCars)) + logger.info("Maximum Cars passing: " + str(maxCars)) + + # update car stats to dynamodb every 3 green lights + if totalGreenlights % 3 == 0: + global tableName + table = dynamodb.Table(tableName) + table.put_item( + Item={ + 'Time': str(datetime.utcnow()), + 'TotalTraffic': totalTraffic, + 'TotalGreenlights': totalGreenlights, + 'MinCarsPassing': minCars, + 'MaxCarsPassing': maxCars, + } + ) + return diff --git a/greengrasssdk/IoTDataPlane.py b/greengrasssdk/IoTDataPlane.py new file mode 100644 index 0000000..a04bd6d --- /dev/null +++ b/greengrasssdk/IoTDataPlane.py @@ -0,0 +1,154 @@ +# +# Copyright 2010-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# + +import base64 +import json +import logging + +from greengrasssdk import Lambda +from greengrass_common.env_vars import SHADOW_FUNCTION_ARN, ROUTER_FUNCTION_ARN, MY_FUNCTION_ARN + +# Log messages in the SDK are part of customer's log because they're helpful for debugging +# customer's lambdas. Since we configured the root logger to log to customer's log and set the +# propagate flag of this logger to True. The log messages submitted from this logger will be +# sent to the customer's local Cloudwatch handler. +customer_logger = logging.getLogger(__name__) +customer_logger.propagate = True + + +class ShadowError(Exception): + pass + + +class Client: + def __init__(self): + self.lambda_client = Lambda.Client() + + def get_thing_shadow(self, **kwargs): + r""" + Call shadow lambda to obtain current shadow state. + + :Keyword Arguments: + * *thingName* (``string``) -- + [REQUIRED] + The name of the thing. + + :returns: (``dict``) -- + The output from the GetThingShadow operation + * *payload* (``bytes``) -- + The state information, in JSON format. + """ + thing_name = self._get_required_parameter('thingName', **kwargs) + payload = b'' + + return self._shadow_op('get', thing_name, payload) + + def update_thing_shadow(self, **kwargs): + r""" + Updates the thing shadow for the specified thing. + + :Keyword Arguments: + * *thingName* (``string``) -- + [REQUIRED] + The name of the thing. + * *payload* (``bytes or seekable file-like object``) -- + [REQUIRED] + The state information, in JSON format. + + :returns: (``dict``) -- + The output from the UpdateThingShadow operation + * *payload* (``bytes``) -- + The state information, in JSON format. + """ + thing_name = self._get_required_parameter('thingName', **kwargs) + payload = self._get_required_parameter('payload', **kwargs) + + return self._shadow_op('update', thing_name, payload) + + def delete_thing_shadow(self, **kwargs): + r""" + Deletes the thing shadow for the specified thing. + + :Keyword Arguments: + * *thingName* (``string``) -- + [REQUIRED] + The name of the thing. + + :returns: (``dict``) -- + The output from the DeleteThingShadow operation + * *payload* (``bytes``) -- + The state information, in JSON format. + """ + thing_name = self._get_required_parameter('thingName', **kwargs) + payload = b'' + + return self._shadow_op('delete', thing_name, payload) + + def publish(self, **kwargs): + r""" + Publishes state information. + + :Keyword Arguments: + * *topic* (``string``) -- + [REQUIRED] + The name of the MQTT topic. + * *payload* (``bytes or seekable file-like object``) -- + The state information, in JSON format. + + :returns: None + """ + + topic = self._get_required_parameter('topic', **kwargs) + + # payload is an optional parameter + payload = kwargs.get('payload', b'') + + function_arn = ROUTER_FUNCTION_ARN + client_context = { + 'custom': { + 'source': MY_FUNCTION_ARN, + 'subject': topic + } + } + + customer_logger.debug('Publishing message on topic "{}" with Payload "{}"'.format(topic, payload)) + self.lambda_client._invoke_internal( + function_arn, + payload, + base64.b64encode(json.dumps(client_context).encode()), + 'Event' + ) + + def _get_required_parameter(self, parameter_name, **kwargs): + if parameter_name not in kwargs: + raise ValueError('Parameter "{parameter_name}" is a required parameter but was not provided.'.format( + parameter_name=parameter_name + )) + return kwargs[parameter_name] + + def _shadow_op(self, op, thing_name, payload): + topic = '$aws/things/{thing_name}/shadow/{op}'.format(thing_name=thing_name, op=op) + function_arn = SHADOW_FUNCTION_ARN + client_context = { + 'custom': { + 'subject': topic + } + } + + customer_logger.debug('Calling shadow service on topic "{}" with payload "{}"'.format(topic, payload)) + response = self.lambda_client._invoke_internal( + function_arn, + payload, + base64.b64encode(json.dumps(client_context).encode()) + ) + + payload = response['Payload'].read() + if response: + response_payload_map = json.loads(payload.decode('utf-8')) + if 'code' in response_payload_map and 'message' in response_payload_map: + raise ShadowError('Request for shadow state returned error code {} with message "{}"'.format( + response_payload_map['code'], response_payload_map['message'] + )) + + return {'payload': payload} diff --git a/greengrasssdk/Lambda.py b/greengrasssdk/Lambda.py new file mode 100644 index 0000000..d967273 --- /dev/null +++ b/greengrasssdk/Lambda.py @@ -0,0 +1,135 @@ +# +# Copyright 2010-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# + +import logging +import re + +from io import BytesIO + +from greengrass_common.function_arn_fields import FunctionArnFields +from greengrass_ipc_python_sdk.ipc_client import IPCClient, IPCException +from greengrasssdk.utils.testing import mock + +# Log messages in the SDK are part of customer's log because they're helpful for debugging +# customer's lambdas. Since we configured the root logger to log to customer's log and set the +# propagate flag of this logger to True. The log messages submitted from this logger will be +# sent to the customer's local Cloudwatch handler. +customer_logger = logging.getLogger(__name__) +customer_logger.propagate = True + +valid_base64_regex = '^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)$' + + +class InvocationException(Exception): + pass + + +class Client: + def __init__(self, endpoint='localhost', port=8000): + """ + :param endpoint: Endpoint used to connect to IPC. + :type endpoint: str + + :param port: Port number used to connect to the :code:`endpoint`. + :type port: int + """ + self.ipc = IPCClient(endpoint=endpoint, port=port) + + def invoke(self, **kwargs): + + # FunctionName is a required parameter + if 'FunctionName' not in kwargs: + raise ValueError( + '"FunctionName" argument of Lambda.Client.invoke is a required argument but was not provided.' + ) + + arn_fields = FunctionArnFields(kwargs['FunctionName']) + arn_qualifier = arn_fields.qualifier + + # A Function qualifier can be provided as part of the ARN in FunctionName, or it can be provided here. The + # behavior of the cloud is to throw an exception if both are specified but not equal + extraneous_qualifier = kwargs.get('Qualifier', '') + + if extraneous_qualifier and arn_qualifier and arn_qualifier != extraneous_qualifier: + raise ValueError('The derived qualifier from the function name does not match the specified qualifier.') + + final_qualifier = arn_qualifier if arn_qualifier else extraneous_qualifier + + function_arn = FunctionArnFields.build_arn_string( + arn_fields.region, arn_fields.account_id, arn_fields.name, final_qualifier + ) + + # ClientContext must be base64 if given, but is an option parameter + try: + client_context = kwargs.get('ClientContext', b'').decode() + except AttributeError as e: + customer_logger.exception(e) + raise ValueError( + '"ClientContext" argument must be a byte string or support a decode method which returns a string' + ) + + if client_context: + if not re.match(valid_base64_regex, client_context): + raise ValueError('"ClientContext" argument of Lambda.Client.invoke must be base64 encoded.') + + # Payload is an optional parameter + payload = kwargs.get('Payload', b'') + invocation_type = kwargs.get('InvocationType', 'RequestResponse') + customer_logger.debug('Invoking local lambda "{}" with payload "{}" and client context "{}"'.format( + function_arn, payload, client_context)) + + # Post the work to IPC and return the result of that work + return self._invoke_internal(function_arn, payload, client_context, invocation_type) + + @mock + def _invoke_internal(self, function_arn, payload, client_context, invocation_type="RequestResponse"): + """ + This private method is seperate from the main, public invoke method so that other code within this SDK can + give this Lambda client a raw payload/client context to invoke with, rather than having it built for them. + This lets you include custom ExtensionMap_ values like subject which are needed for our internal pinned Lambdas. + """ + customer_logger.debug('Invoking Lambda function "{}" with Greengrass Message "{}"'.format(function_arn, payload)) + + try: + invocation_id = self.ipc.post_work(function_arn, payload, client_context, invocation_type) + + if invocation_type == "Event": + # TODO: Properly return errors based on BOTO response + # https://boto3.readthedocs.io/en/latest/reference/services/lambda.html#Lambda.Client.invoke + return {'Payload': b'', 'FunctionError': ''} + + work_result_output = self.ipc.get_work_result(function_arn, invocation_id) + if not work_result_output.func_err: + output_payload = StreamingBody(work_result_output.payload) + else: + output_payload = work_result_output.payload + invoke_output = { + 'Payload': output_payload, + 'FunctionError': work_result_output.func_err, + } + return invoke_output + except IPCException as e: + customer_logger.exception(e) + raise InvocationException('Failed to invoke function due to ' + str(e)) + + +class StreamingBody(object): + """Wrapper class for http response payload + + This provides a consistent interface to AWS Lambda Python SDK + """ + def __init__(self, payload): + self._raw_stream = BytesIO(payload) + self._amount_read = 0 + + def read(self, amt=None): + """Read at most amt bytes from the stream. + If the amt argument is omitted, read all data. + """ + chunk = self._raw_stream.read(amt) + self._amount_read += len(chunk) + return chunk + + def close(self): + self._raw_stream.close() diff --git a/greengrasssdk/SecretsManager.py b/greengrasssdk/SecretsManager.py new file mode 100644 index 0000000..2a8385e --- /dev/null +++ b/greengrasssdk/SecretsManager.py @@ -0,0 +1,160 @@ +# +# Copyright 2010-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# + +import json +import logging +from datetime import datetime +from decimal import Decimal + +from greengrasssdk import Lambda +from greengrass_common.env_vars import MY_FUNCTION_ARN, SECRETS_MANAGER_FUNCTION_ARN + +# Log messages in the SDK are part of customer's log because they're helpful for debugging +# customer's lambdas. Since we configured the root logger to log to customer's log and set the +# propagate flag of this logger to True. The log messages submitted from this logger will be +# sent to the customer's local Cloudwatch handler. +customer_logger = logging.getLogger(__name__) +customer_logger.propagate = True + +KEY_NAME_PAYLOAD = 'Payload' +KEY_NAME_STATUS = 'Status' +KEY_NAME_MESSAGE = 'Message' +KEY_NAME_SECRET_ID = 'SecretId' +KEY_NAME_VERSION_ID = 'VersionId' +KEY_NAME_VERSION_STAGE = 'VersionStage' +KEY_NAME_CREATED_DATE = "CreatedDate" + + +class SecretsManagerError(Exception): + pass + + +class Client: + def __init__(self): + self.lambda_client = Lambda.Client() + + def get_secret_value(self, **kwargs): + r""" + Call secrets manager lambda to obtain the requested secret value. + + :Keyword Arguments: + * *SecretId* (``string``) -- + [REQUIRED] + Specifies the secret containing the version that you want to retrieve. You can specify either the + Amazon Resource Name (ARN) or the friendly name of the secret. + * *VersionId* (``string``) -- + Specifies the unique identifier of the version of the secret that you want to retrieve. If you + specify this parameter then don't specify ``VersionStage`` . If you don't specify either a + ``VersionStage`` or ``SecretVersionId`` then the default is to perform the operation on the version + with the ``VersionStage`` value of ``AWSCURRENT`` . + + This value is typically a UUID-type value with 32 hexadecimal digits. + * *VersionStage* (``string``) -- + Specifies the secret version that you want to retrieve by the staging label attached to the + version. + + Staging labels are used to keep track of different versions during the rotation process. If you + use this parameter then don't specify ``SecretVersionId`` . If you don't specify either a + ``VersionStage`` or ``SecretVersionId`` , then the default is to perform the operation on the + version with the ``VersionStage`` value of ``AWSCURRENT`` . + + :returns: (``dict``) -- + * *ARN* (``string``) -- + The ARN of the secret. + * *Name* (``string``) -- + The friendly name of the secret. + * *VersionId* (``string``) -- + The unique identifier of this version of the secret. + * *SecretBinary* (``bytes``) -- + The decrypted part of the protected secret information that was originally provided as + binary data in the form of a byte array. The response parameter represents the binary data + as a base64-encoded string. + + This parameter is not used if the secret is created by the Secrets Manager console. + + If you store custom information in this field of the secret, then you must code your Lambda + rotation function to parse and interpret whatever you store in the ``SecretString`` or + ``SecretBinary`` fields. + * *SecretString* (``string``) -- + The decrypted part of the protected secret information that was originally provided as a + string. + + If you create this secret by using the Secrets Manager console then only the ``SecretString`` + parameter contains data. Secrets Manager stores the information as a JSON structure of + key/value pairs that the Lambda rotation function knows how to parse. + + If you store custom information in the secret by using the CreateSecret , UpdateSecret , or + PutSecretValue API operations instead of the Secrets Manager console, or by using the + *Other secret type* in the console, then you must code your Lambda rotation function to + parse and interpret those values. + * *VersionStages* (``list``) -- + A list of all of the staging labels currently attached to this version of the secret. + * (``string``) -- + * *CreatedDate* (``datetime``) -- + The date and time that this version of the secret was created. + """ + + secret_id = self._get_required_parameter(KEY_NAME_SECRET_ID, **kwargs) + version_id = kwargs.get(KEY_NAME_VERSION_ID, '') + version_stage = kwargs.get(KEY_NAME_VERSION_STAGE, '') + + if version_id: # TODO: Remove this once we support query by VersionId + raise SecretsManagerError('Query by VersionId is not yet supported') + if version_id and version_stage: + raise ValueError('VersionId and VersionStage cannot both be specified at the same time') + + request_payload_bytes = self._generate_request_payload_bytes(secret_id=secret_id, + version_id=version_id, + version_stage=version_stage) + + customer_logger.debug('Retrieving secret value with id "{}", version id "{}" version stage "{}"' + .format(secret_id, version_id, version_stage)) + response = self.lambda_client._invoke_internal( + SECRETS_MANAGER_FUNCTION_ARN, + request_payload_bytes, + b'', # We do not need client context for Secrets Manager back-end lambda + ) # Use Request/Response here as we are mimicking boto3 Http APIs for SecretsManagerService + + payload = response[KEY_NAME_PAYLOAD].read() + payload_dict = json.loads(payload.decode('utf-8')) + + # All customer facing errors are presented within the response payload. For example: + # { + # "code": 404, + # "message": "Resource not found" + # } + if KEY_NAME_STATUS in payload_dict and KEY_NAME_MESSAGE in payload_dict: + raise SecretsManagerError('Request for secret value returned error code {} with message {}'.format( + payload_dict[KEY_NAME_STATUS], payload_dict[KEY_NAME_MESSAGE] + )) + + # Time is serialized as epoch timestamp (int) upon IPC routing. We need to deserialize it back to datetime object in Python + payload_dict[KEY_NAME_CREATED_DATE] = datetime.fromtimestamp( + # Cloud response contains timestamp in milliseconds while datetime.fromtimestamp is expecting seconds + Decimal(payload_dict[KEY_NAME_CREATED_DATE]) / Decimal(1000) + ) + + return payload_dict + + def _generate_request_payload_bytes(self, secret_id, version_id, version_stage): + request_payload = { + KEY_NAME_SECRET_ID: secret_id, + } + if version_stage: + request_payload[KEY_NAME_VERSION_STAGE] = version_stage + + # TODO: Add VersionId once we support query by VersionId + + # The allowed chars for secret id and version stage are strictly enforced when customers are configuring them + # through Secrets Manager Service in the cloud: + # https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_CreateSecret.html#API_CreateSecret_RequestSyntax + return json.dumps(request_payload).encode() + + @staticmethod + def _get_required_parameter(parameter_name, **kwargs): + if parameter_name not in kwargs: + raise ValueError('Parameter "{parameter_name}" is a required parameter but was not provided.'.format( + parameter_name=parameter_name + )) + return kwargs[parameter_name] diff --git a/greengrasssdk/__init__.py b/greengrasssdk/__init__.py new file mode 100644 index 0000000..41b3787 --- /dev/null +++ b/greengrasssdk/__init__.py @@ -0,0 +1,9 @@ +# +# Copyright 2010-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# + +from .client import client +from .Lambda import StreamingBody + +__version__ = '1.3.0' +INTERFACE_VERSION = '1.1' diff --git a/greengrasssdk/client.py b/greengrasssdk/client.py new file mode 100644 index 0000000..53c92d0 --- /dev/null +++ b/greengrasssdk/client.py @@ -0,0 +1,16 @@ +# +# Copyright 2010-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# + + +def client(client_type, *args): + if client_type == 'lambda': + from .Lambda import Client + elif client_type == 'iot-data': + from .IoTDataPlane import Client + elif client_type == 'secretsmanager': + from .SecretsManager import Client + else: + raise Exception('Client type {} is not recognized.'.format(repr(client_type))) + + return Client(*args) diff --git a/greengrasssdk/utils/__init__.py b/greengrasssdk/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/greengrasssdk/utils/testing.py b/greengrasssdk/utils/testing.py new file mode 100644 index 0000000..0a0bbf8 --- /dev/null +++ b/greengrasssdk/utils/testing.py @@ -0,0 +1,35 @@ +# +# Copyright 2010-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# + +import json +from functools import wraps +from greengrass_common.env_vars import MY_FUNCTION_ARN + + +def mock(func): + """ + mock decorates _invoke_internal by checking if MY_FUNCTION_ARN is present + if MY_FUNCTION_ARN is present, the actual _invoke_internal is invoked + otherwise, the mock _invoke_internal is invoked + """ + @wraps(func) + def mock_invoke_internal(self, function_arn, payload, client_context, invocation_type="RequestResponse"): + if MY_FUNCTION_ARN is None: + if invocation_type == 'RequestResponse': + return { + 'Payload': json.dumps({ + 'TestKey': 'TestValue' + }), + 'FunctionError': '' + } + elif invocation_type == 'Event': + return { + 'Payload': b'', + 'FunctionError': '' + } + else: + raise Exception('Unsupported invocation type {}'.format(invocation_type)) + else: + return func(self, function_arn, payload, client_context, invocation_type) + return mock_invoke_internal diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..2a9acf1 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..b73485f --- /dev/null +++ b/setup.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python + +""" +distutils/setuptools install script. +""" +import os +import re + +from setuptools import setup, find_packages + + +ROOT = os.path.dirname(__file__) +VERSION_RE = re.compile(r'''__version__ = ['"]([0-9.]+)['"]''') + + +requires = [ +] + + +def get_version(): + init = open(os.path.join(ROOT,'greengrasssdk','__init__.py')).read() + return VERSION_RE.search(init).group(1) + + +setup( + name='greengrasssdk', + version=get_version(), + description='The AWS Greengrass SDK for Python', + long_description=open('README.rst').read(), + author='Amazon Web Services', + url='', + scripts=[], + packages=find_packages(), + package_data={ + 'greengrasssdk': [ + ] + }, + include_package_data=True, + install_requires=requires, + license="Apache License 2.0", + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'Natural Language :: English', + 'License :: OSI Approved :: Apache Software License', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.7', + ], +)