import logging import datetime from time import sleep import boto3 as boto from botocore.exceptions import ClientError LOGGER = logging.getLogger(__name__) class Environment(object): TWO_WEEKS = 1209600 def __init__(self, queue_name, bucket_name, function_name, cron_schedule='rate(1 day)'): self._queue_name = queue_name self._bucket_name = bucket_name self._function_name = function_name self._cron_schedule = cron_schedule, self._s3 = boto.resource('s3') self._sqs = boto.resource('sqs') self._lambda_client = boto.client('lambda') self._iam_client = boto.client('iam') self._queue = None self._bucket = None def _create_queue_with_pushback(self, name, att_dict): """ If a SQS queue is deleted recently (for example during testing), we have to wait 60 secs before recreating. """ try: q = self._sqs.create_queue(QueueName=name, Attributes=att_dict) except ClientError as e: if e.response['Error']['Code'] == 'AWS.SimpleQueueService.QueueDeletedRecently': sleep(60) q = self._sqs.create_queue(QueueName=name, Attributes=att_dict) else: raise e return q def get_create_queue(self): if not self._queue: try: q = self._sqs.get_queue_by_name(QueueName=self._queue_name) except ClientError as e: if e.response['Error']['Code'] == 'AWS.SimpleQueueService.NonExistentQueue': q = None else: raise e if not q: LOGGER.info('Creating queue {}'.format(self._queue_name)) q = self._create_queue_with_pushback( self._queue_name, {'MessageRetentionPeriod': str(self.TWO_WEEKS)} ) self._queue = q return self._queue def _bucket_exists(self, name): try: self._s3.meta.client.head_bucket(Bucket=name) return True except ClientError as e: if e.response['Error']['Code'] == '404': return False else: raise e def get_create_bucket(self): if not self._bucket: b = self._s3.Bucket(self._bucket_name) if not self._bucket_exists(self._bucket_name): LOGGER.info('Creating bucket {}'.format(self._bucket_name)) region_name = boto.session.Session().region_name b = self._s3.create_bucket( Bucket=self._bucket_name, CreateBucketConfiguration={ 'LocationConstraint': region_name } ) self._bucket = b return self._bucket def _delete_function_if_exists(self, function_name): try: self._lambda_client.get_function(FunctionName=self._function_name) logging.info('Deleting old version of the function {}'.format(self._function_name)) self._lambda_client.delete_function(FunctionName=self._function_name) except ClientError as e: if e.response['Error']['Code'] == 'ResourceNotFoundException': pass else: raise e def update_function(self, role_arn, filepath, memory_size=128, timeout=300, schedule=None): env_variables = { 'QUEUE_NAME': self._queue_name, 'BUCKET_NAME': self._bucket_name } self._delete_function_if_exists(function_name=self._function_name) uploaded_package_name = '_function/{}{}.zip'.format(self._function_name, datetime.datetime.now()) self.get_create_bucket().upload_file(filepath, uploaded_package_name) res = self._lambda_client.create_function( FunctionName=self._function_name, Runtime='python3.6', Role=role_arn, Handler='lambda_function.handler', Code={ 'S3Bucket': self._bucket_name, 'S3Key': uploaded_package_name, }, MemorySize=memory_size, Timeout=timeout, Environment={ 'Variables': env_variables } ) self.get_create_queue() # TODO This doesn't seem to be deleting the temp function. self.get_create_bucket().delete_objects(Delete={'Objects': [{'Key': uploaded_package_name}]}) if schedule: self._schedule_function(res['FunctionArn'], schedule) return res def _schedule_function(self, function_arn, schedule): LOGGER.info('Scheduling function {} to {}'.format(self._function_name, schedule)) events_client = boto.client('events') trigger_name = '{}-trigger'.format(self._function_name) rule_response = events_client.put_rule( Name=trigger_name, ScheduleExpression=schedule, State='ENABLED', ) self._lambda_client.add_permission( FunctionName=self._function_name, StatementId="{0}-Event".format(trigger_name), Action='lambda:InvokeFunction', Principal='events.amazonaws.com', SourceArn=rule_response['RuleArn'], ) events_client.put_targets( Rule=trigger_name, Targets=[{'Id': "1", 'Arn': function_arn}] ) def update_role_policy(self, role_name, policy_config): assume_role_policy = '''{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com" }, "Action": "sts:AssumeRole" } ] } ''' LOGGER.info('Updating role policy {}'.format(role_name)) try: self._iam_client.create_role(RoleName=role_name, AssumeRolePolicyDocument=assume_role_policy) except ClientError as e: if e.response['Error']['Code'] == 'EntityAlreadyExists': pass else: raise e self._iam_client.put_role_policy( PolicyDocument=policy_config, PolicyName=role_name+'Policy', RoleName=role_name ) return self._iam_client.get_role(RoleName=role_name)['Role']['Arn'] def destroy(self, delete_function=False, delete_s3_bucket=False): LOGGER.info('Deleting queue {}'.format(self._queue_name)) self.get_create_queue().delete() if delete_function: LOGGER.info('Deleting function {}'.format(self._function_name)) self._lambda_client.delete_function(FunctionName=self._function_name) if delete_s3_bucket: LOGGER.info('Deleting bucket {}'.format(self._bucket_name)) b = self.get_create_bucket() for k in b.objects.all(): k.delete() b.delete()