From c4120cff1b9bfc352683ba60e14bc477b1a041d5 Mon Sep 17 00:00:00 2001 From: Ben Kehoe Date: Mon, 2 Nov 2015 11:12:37 -0500 Subject: [PATCH 1/6] add debug logging of traceback on error --- cfnlambda.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cfnlambda.py b/cfnlambda.py index 9841b7e..410d132 100644 --- a/cfnlambda.py +++ b/cfnlambda.py @@ -19,6 +19,7 @@ from functools import wraps import boto3 from botocore.vendored import requests +import traceback logger = logging.getLogger(__name__) @@ -221,6 +222,7 @@ def handler_wrapper(event, context): (handler.__name__, e.message)) result = {'result': message} logger.error(message) + logger.debug(traceback.format_exc()) if event['RequestType'] == RequestType.DELETE: if status == Status.FAILED and hide_stack_delete_failure: From 2f24453a2525f47d9d84d565cf0ab2734be1ea22 Mon Sep 17 00:00:00 2001 From: Ben Kehoe Date: Tue, 3 Nov 2015 07:54:40 -0500 Subject: [PATCH 2/6] more informative logging, better return values --- cfnlambda.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/cfnlambda.py b/cfnlambda.py index 410d132..51c239f 100644 --- a/cfnlambda.py +++ b/cfnlambda.py @@ -20,6 +20,7 @@ import boto3 from botocore.vendored import requests import traceback +import httplib logger = logging.getLogger(__name__) @@ -93,7 +94,7 @@ def cfn_response(event, CloudWatch Logs log stream is used. Returns: - None + requests.Response object Raises: No exceptions raised @@ -121,12 +122,18 @@ def cfn_response(event, try: response = requests.put(event['ResponseURL'], data=response_body) - logger.debug("Status code: %s" % response.status_code) + body_text = "" + if response.status_code // 100 != 2: + body_text = "\n" + response.text + logger.debug("Status code: %s %s%s" % (response.status_code, httplib.responses[response.status_code], body_text)) + # logger.debug("Status message: %s" % response.status_message) # how do we get the status message? + return response except Exception as e: logger.error("send(..) failed executing https.request(..): %s" % e.message) + logger.debug(traceback.format_exc()) def handler_decorator(delete_logs=True, @@ -209,6 +216,7 @@ def handler_wrapper(event, context): logger.info('REQUEST RECEIVED: %s' % json.dumps(event)) logger.info('LambdaContext: %s' % json.dumps(vars(context), cls=PythonObjectEncoder)) + result = None try: result = handler(event, context) status = Status.SUCCESS if result else Status.FAILED @@ -224,6 +232,9 @@ def handler_wrapper(event, context): logger.error(message) logger.debug(traceback.format_exc()) + if not result: + result = {} + if event['RequestType'] == RequestType.DELETE: if status == Status.FAILED and hide_stack_delete_failure: message = ( @@ -232,7 +243,7 @@ def handler_wrapper(event, context): 'despite the fact that the stack status may be ' 'DELETE_COMPLETE.') logger.error(message) - result['result'] += ' %s' % message + result['result'] = result.get('result', '') + ' %s' % message status = Status.SUCCESS if status == Status.SUCCESS and delete_logs: @@ -245,6 +256,6 @@ def handler_wrapper(event, context): status, (result if type(result) is dict else {'result': result})) - return handler(event, context) + return result return handler_wrapper return inner_decorator From 6f7d2e4f35217c6a98a72ebb9e0a8120acd84425 Mon Sep 17 00:00:00 2001 From: Ben Kehoe Date: Tue, 3 Nov 2015 08:13:04 -0500 Subject: [PATCH 3/6] allow wrapped func to call cfn_response itself using more complex Status object --- cfnlambda.py | 86 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 67 insertions(+), 19 deletions(-) diff --git a/cfnlambda.py b/cfnlambda.py index 51c239f..c52504e 100644 --- a/cfnlambda.py +++ b/cfnlambda.py @@ -30,8 +30,36 @@ class Status: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-responses.html """ - SUCCESS = 'SUCCESS' - FAILED = 'FAILED' + + def __init__(self, value, reason=None): + self.value = value + self.reason = reason + + def __repr__(self): + r = '{}'.format(self.value) + if self.reason: + r += '({})'.format(self.reason) + return r + + def isSuccess(self): + return self.value == 'SUCCESS' or (self.isFinished() and self.value.value == 'SUCCESS') + + def isFailed(self): + return self.value == 'FAILED' or (self.isFinished() and self.value.value == 'FAILED') + + def isFinished(self): + return isinstance(self.value, Status) + + @classmethod + def getFailed(cls, reason): + return cls('FAILED', reason) + + @classmethod + def getFinished(cls, status): + return cls(status) + +Status.SUCCESS = Status('SUCCESS') +Status.FAILED = Status('FAILED') class RequestType: @@ -85,7 +113,9 @@ def cfn_response(event, information.[4] response_status: A status of SUCCESS or FAILED to send back to CloudFormation.[2] Use the Status.SUCCESS and Status.FAILED - constants. + constants, or Status.getFailed() to provide a reason for the + failure. If the status was wrapped using Status.getFinished(), + the call is a noop and returns None. response_data: A dictionary of key value pairs to pass back to CloudFormation which can be accessed with the Fn::GetAtt function on the CloudFormation custom resource.[5] @@ -105,12 +135,16 @@ def cfn_response(event, [4]: http://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html [5]: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-responses.html#crpg-ref-responses-data """ + if isinstance(response_status, Status) and response_status.isFinished(): + return + if physical_resource_id is None: physical_resource_id = context.log_stream_name + default_reason = ("See the details in CloudWatch Log Stream: %s" % + context.log_stream_name) body = { - "Status": response_status, - "Reason": ("See the details in CloudWatch Log Stream: %s" % - context.log_stream_name), + "Status": response_status.value if isinstance(response_status, Status) else response_status, + "Reason": response_status.reason or default_reason if isinstance(response_status, Status) else default_reason, "PhysicalResourceId": physical_resource_id, "StackId": event['StackId'], "RequestId": event['RequestId'], @@ -202,7 +236,13 @@ def handler_wrapper(event, context): information.[2] Returns: - None + If the handler returns a Status object, the wrapper returns an + empty dict. + + If the handler returns two values, the first being a Status object, + the wrapper returns the second value. + + Otherwise, the wrapper returns the value returned by the handler. Returns to CloudFormation: TODO @@ -219,17 +259,23 @@ def handler_wrapper(event, context): result = None try: result = handler(event, context) - status = Status.SUCCESS if result else Status.FAILED - if not status: + if isinstance(result, Status): + status = result + result = None + elif isinstance(result, tuple) and len(result) == 2 and isinstance(result[1], Status): + status, result = result + else: + status = Status.SUCCESS if result else Status.FAILED + if result is False: message = "Function %s returned False." % handler.__name__ logger.error(message) + status = Status.FAILED result = {'result': message} except Exception as e: - status = Status.FAILED - message = ('Function %s failed due to exception "%s".' % + status = Status.getFailed('Function %s failed due to exception "%s".' % (handler.__name__, e.message)) - result = {'result': message} - logger.error(message) + result = {} + logger.error(status.reason) logger.debug(traceback.format_exc()) if not result: @@ -246,16 +292,18 @@ def handler_wrapper(event, context): result['result'] = result.get('result', '') + ' %s' % message status = Status.SUCCESS - if status == Status.SUCCESS and delete_logs: + if status.isSuccess() and delete_logs: logging.disable(logging.CRITICAL) logs_client = boto3.client('logs') logs_client.delete_log_group( logGroupName=context.log_group_name) - cfn_response(event, - context, - status, - (result if type(result) is dict else - {'result': result})) + result = (dict(result) if isinstance(result, dict) else {'result': result}) + if not status.isFinished(): + cfn_response(event, + context, + status, + result, + ) return result return handler_wrapper return inner_decorator From f9996883d38e4d1c8b86616d08c4b7c1239e8bf5 Mon Sep 17 00:00:00 2001 From: Ben Kehoe Date: Tue, 3 Nov 2015 08:21:06 -0500 Subject: [PATCH 4/6] allow handler_decorator to be used without parens --- README.rst | 27 ++++----------------------- cfnlambda.py | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/README.rst b/README.rst index 1674e76..5664101 100644 --- a/README.rst +++ b/README.rst @@ -16,7 +16,7 @@ your AWS Lambda function. from cfnlambda import handler_decorator - @handler_decorator() + @handler_decorator def lambda_handler(event, context): sum = (float(event['ResourceProperties']['key1']) + float(event['ResourceProperties']['key2'])) @@ -104,7 +104,7 @@ First, this Lambda code must be zipped and uploaded to an s3 bucket. import logging logging.getLogger().setLevel(logging.INFO) - @handler_decorator() + @handler_decorator def lambda_handler(event, context): sum = (float(event['ResourceProperties']['key1']) + float(event['ResourceProperties']['key2'])) @@ -123,7 +123,7 @@ Here are a set of commands to create and upload the AWS Lambda function import logging logging.getLogger().setLevel(logging.INFO) - @handler_decorator() + @handler_decorator def lambda_handler(event, context): sum = (float(event['ResourceProperties']['key1']) + float(event['ResourceProperties']['key2'])) @@ -299,23 +299,4 @@ key of the author of `cfnlambda`. Go to `keybase`_ and type the `key ID` into the search bar. You should get back a single user's profile which lists out a collection of accounts that the user has proved control of. A strong indicator that the person is the author is if you can find `cfnlambda` in their github -account. - -FAQ ---- - -Q: What causes the error `inner_decorator() takes exactly 1 argument (2 given): TypeError Traceback -(most recent call last): File "/var/runtime/awslambda/bootstrap.py", line -177, in handle_event_request result = request_handler(json_input, context) -TypeError: inner_decorator() takes exactly 1 argument (2 given)` - -A: You likely used `@handler_decorator` to decorate your function instead of -`@handler_decorator()`. Because `handler_decorator` accepts arguments, you need -to use it with parenthesis. - -.. _AWS CLI: http://docs.aws.amazon.com/cli/latest/reference/s3/index.html -.. _install and configure AWS CLI: http://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-set-up.html -.. _returning: https://docs.python.org/2/reference/simple_stmts.html#return -.. _cfn-response: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html#cfn-lambda-function-code-cfnresponsemodule -.. _downloads section on PyPI: https://pypi.python.org/pypi/cfnlambda#downloads -.. _keybase: https://keybase.io/ \ No newline at end of file +account. \ No newline at end of file diff --git a/cfnlambda.py b/cfnlambda.py index 51c239f..42f81ac 100644 --- a/cfnlambda.py +++ b/cfnlambda.py @@ -136,13 +136,18 @@ def cfn_response(event, logger.debug(traceback.format_exc()) -def handler_decorator(delete_logs=True, - hide_stack_delete_failure=True): +def handler_decorator(*args, **kwargs): """Decorate an AWS Lambda function to add exception handling, emit CloudFormation responses and log. Usage: - >>> @handler_decorator() + >>> @handler_decorator + ... def lambda_handler(event, context): + ... sum = (float(event['ResourceProperties']['key1']) + + ... float(event['ResourceProperties']['key2'])) + ... return {'sum': sum} + + >>> @handler_decorator(delete_logs=False) ... def lambda_handler(event, context): ... sum = (float(event['ResourceProperties']['key1']) + ... float(event['ResourceProperties']['key2'])) @@ -169,6 +174,11 @@ def handler_decorator(delete_logs=True, Raises: No exceptions """ + if args: + return handler_decorator()(args[0]) + + delete_logs = kwargs.get('delete_logs', True) + hide_stack_delete_failure = kwargs.get('hide_stack_delete_failure', True) def inner_decorator(handler): """Bind handler_decorator to handler_wrapper in order to enable passing From a9475aca3f2efaa41d6a62531cf9ee720b084983 Mon Sep 17 00:00:00 2001 From: Ben Kehoe Date: Tue, 3 Nov 2015 16:37:37 -0500 Subject: [PATCH 5/6] only delete the log stream, not group --- cfnlambda.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cfnlambda.py b/cfnlambda.py index 39c3e38..7163f2d 100644 --- a/cfnlambda.py +++ b/cfnlambda.py @@ -305,8 +305,9 @@ def handler_wrapper(event, context): if status.isSuccess() and delete_logs: logging.disable(logging.CRITICAL) logs_client = boto3.client('logs') - logs_client.delete_log_group( - logGroupName=context.log_group_name) + logs_client.delete_log_stream( + logGroupName=context.log_group_name, + logStreamName=context.log_stream_name) result = (dict(result) if isinstance(result, dict) else {'result': result}) if not status.isFinished(): cfn_response(event, From 8b3fc3f6e8ebd652cb1e4a11e4d0a797aeec5062 Mon Sep 17 00:00:00 2001 From: Ben Kehoe Date: Fri, 6 Nov 2015 14:14:44 -0500 Subject: [PATCH 6/6] added Result class --- cfnlambda.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/cfnlambda.py b/cfnlambda.py index 7163f2d..da0124e 100644 --- a/cfnlambda.py +++ b/cfnlambda.py @@ -31,9 +31,10 @@ class Status: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-responses.html """ - def __init__(self, value, reason=None): + def __init__(self, value, reason=None, put_response=None): self.value = value self.reason = reason + self.put_response = put_response def __repr__(self): r = '{}'.format(self.value) @@ -55,12 +56,29 @@ def getFailed(cls, reason): return cls('FAILED', reason) @classmethod - def getFinished(cls, status): - return cls(status) + def getFinished(cls, status, put_response=None): + return cls(status, put_response=put_response) Status.SUCCESS = Status('SUCCESS') Status.FAILED = Status('FAILED') +class Result(object): + def __init__(self, physical_resource_id, data=None): + self.physical_resource_id = physical_resource_id + self.data = data + self.dict = {} + self.status = None + + def success(self): + self.status = Status.SUCCESS + return self + + def failed(self, reason): + self.status = Status.getFailed(reason) + return self + + def finish(self, event, context): + return Status.getFinished(self.status, put_response=cfn_response(event, context, self.status, physical_resource_id=self.physical_resource_id, response_data=self.data)) class RequestType: """CloudFormation custom resource request type constants @@ -317,4 +335,4 @@ def handler_wrapper(event, context): ) return result return handler_wrapper - return inner_decorator + return inner_decorator \ No newline at end of file