From e13411fc0d8de5209d4ad6bf795475baa414a8e6 Mon Sep 17 00:00:00 2001 From: Kaushik Narayan R Date: Fri, 5 Apr 2024 19:09:03 -0700 Subject: [PATCH] proj-2-part-1 done --- Project-2/Part-1/Project 2 (Part-I)_ PaaS.pdf | Bin 155663 -> 156234 bytes Project-2/Part-1/src/Dockerfile | 54 ++++ Project-2/Part-1/src/README.md | 5 + .../Part-1/src/dummy_s3_trigger_event.json | 38 +++ Project-2/Part-1/src/entry.sh | 6 + Project-2/Part-1/src/grader_script_p1.py | 270 ++++++++++++++++++ Project-2/Part-1/src/handler.py | 66 +++++ Project-2/Part-1/src/lambda_s3_policy.json | 28 ++ Project-2/Part-1/src/requirements.txt | 1 + Project-2/Part-1/src/workload_generator.py | 73 +++++ 10 files changed, 541 insertions(+) create mode 100644 Project-2/Part-1/src/Dockerfile create mode 100644 Project-2/Part-1/src/README.md create mode 100644 Project-2/Part-1/src/dummy_s3_trigger_event.json create mode 100644 Project-2/Part-1/src/entry.sh create mode 100644 Project-2/Part-1/src/grader_script_p1.py create mode 100644 Project-2/Part-1/src/handler.py create mode 100644 Project-2/Part-1/src/lambda_s3_policy.json create mode 100644 Project-2/Part-1/src/requirements.txt create mode 100644 Project-2/Part-1/src/workload_generator.py diff --git a/Project-2/Part-1/Project 2 (Part-I)_ PaaS.pdf b/Project-2/Part-1/Project 2 (Part-I)_ PaaS.pdf index c92530a288ede16281855ccb11569b1a56a25082..ca949a6aaf3899b40770f7bc34d2980cb15681f0 100644 GIT binary patch delta 24903 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%0DMa~Od|mOq(qQmsyq@#{D4; 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 Co*AY9 diff --git a/Project-2/Part-1/src/Dockerfile b/Project-2/Part-1/src/Dockerfile new file mode 100644 index 0000000..e7bdf9c --- /dev/null +++ b/Project-2/Part-1/src/Dockerfile @@ -0,0 +1,54 @@ +#__copyright__ = "Copyright 2024, VISA Lab" +#__license__ = "MIT" + +# Define global args +ARG FUNCTION_DIR="/home/app/" +ARG RUNTIME_VERSION="3.8" +ARG DISTRO_VERSION="3.12" + +FROM alpine:latest +FROM python:${RUNTIME_VERSION} AS python-alpine + +RUN python${RUNTIME_VERSION} -m pip install --upgrade pip + +FROM python-alpine AS build-image + +# Include global args in this stage of the build +ARG FUNCTION_DIR +ARG RUNTIME_VERSION +# Create function directory +RUN mkdir -p ${FUNCTION_DIR} + +# Install Lambda Runtime Interface Client for Python +RUN python${RUNTIME_VERSION} -m pip install awslambdaric --target ${FUNCTION_DIR} + +# Stage 3 - final runtime image +# Grab a fresh copy of the Python image +FROM python-alpine +# Include global arg in this stage of the build +ARG FUNCTION_DIR +# Set working directory to function root directory +WORKDIR ${FUNCTION_DIR} +# Copy in the built dependencies +COPY --from=build-image ${FUNCTION_DIR} ${FUNCTION_DIR} +# (Optional) Add Lambda Runtime Interface Emulator and use a script in the ENTRYPOINT for simpler local runs +ADD https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie /usr/bin/aws-lambda-rie +RUN chmod 755 /usr/bin/aws-lambda-rie + +# Install ffmpeg +RUN apt-get update +RUN apt-get install -y ffmpeg + +# Copy handler function +COPY requirements.txt ${FUNCTION_DIR} + +RUN python${RUNTIME_VERSION} -m pip install -r requirements.txt --target ${FUNCTION_DIR} +COPY entry.sh / + +# Copy function code +COPY handler.py ${FUNCTION_DIR} +RUN chmod 777 /entry.sh + +# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile) +ENTRYPOINT [ "/entry.sh" ] +CMD [ "handler.handler" ] diff --git a/Project-2/Part-1/src/README.md b/Project-2/Part-1/src/README.md new file mode 100644 index 0000000..fe53fb8 --- /dev/null +++ b/Project-2/Part-1/src/README.md @@ -0,0 +1,5 @@ +# Part-1: Video-splitting stage - S3-triggered Lambda + +- `handler.py` gets the uploaded video file, splits 10 frames using `ffmpeg`, uploads the output folder of frames to another bucket +- `lambda_s3_policy.json` defines the permission policy needed for the lambda function's IAM role +- `dummy_s3_trigger_event.json` is a sample S3 PUT event diff --git a/Project-2/Part-1/src/dummy_s3_trigger_event.json b/Project-2/Part-1/src/dummy_s3_trigger_event.json new file mode 100644 index 0000000..26e19d7 --- /dev/null +++ b/Project-2/Part-1/src/dummy_s3_trigger_event.json @@ -0,0 +1,38 @@ +{ +"Records":[ +{ +"eventVersion":"2.0", +"eventSource":"aws:s3", +"awsRegion":"us-east-1", +"eventTime":"1970-01-01T00:00:00.000Z", +"eventName":"ObjectCreated:Put", +"userIdentity":{ +"principalId":"EXAMPLE" +}, +"requestParameters":{ +"sourceIPAddress":"127.0.0.1" +}, +"responseElements":{ +"x-amz-request-id":"EXAMPLE123456789", +"x-amz-id-2":"EXAMPLE123/5678abcdefghijklambdaisawesome/mnopqrstuvwxyzABCDEFGH" +}, +"s3":{ +"s3SchemaVersion":"1.0", +"configurationId":"testConfigRule", +"bucket":{ +"name":"1229569564-input", +"ownerIdentity":{ +"principalId":"EXAMPLE" +}, +"arn":"arn:aws:s3:::1229569564-input" +}, +"object":{ +"key":"jellyfish jam.mp4", +"size":1024, +"eTag":"0123456789abcdef0123456789abcdef", +"sequencer":"0A1B2C3D4E5F678901" +} +} +} +] +} \ No newline at end of file diff --git a/Project-2/Part-1/src/entry.sh b/Project-2/Part-1/src/entry.sh new file mode 100644 index 0000000..a608361 --- /dev/null +++ b/Project-2/Part-1/src/entry.sh @@ -0,0 +1,6 @@ +#!/bin/sh +if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then + exec /usr/bin/aws-lambda-rie /usr/local/bin/python -m awslambdaric $1 +else + exec /usr/local/bin/python -m awslambdaric $1 +fi diff --git a/Project-2/Part-1/src/grader_script_p1.py b/Project-2/Part-1/src/grader_script_p1.py new file mode 100644 index 0000000..2a1cd34 --- /dev/null +++ b/Project-2/Part-1/src/grader_script_p1.py @@ -0,0 +1,270 @@ +__copyright__ = "Copyright 2024, VISA Lab" +__license__ = "MIT" + +import pdb +import time +import botocore +import argparse +import textwrap +import boto3 +from boto3 import client as boto3_client +from botocore.exceptions import ClientError +from datetime import datetime,timezone,timedelta + +class aws_grader(): + def __init__(self, access_key, secret_key, input_bucket, output_bucket, lambda_name, region): + + self.access_key = access_key + self.secret_key = secret_key + self.region = region + self.s3 = boto3_client('s3', aws_access_key_id=self.access_key, + aws_secret_access_key=self.secret_key, region_name=region) + self.cloudwatch = boto3_client('cloudwatch', aws_access_key_id=self.access_key, + aws_secret_access_key=self.secret_key, region_name=region) + self.iam_session = boto3.Session(aws_access_key_id=self.access_key, + aws_secret_access_key=self.secret_key) + self.s3_resources = self.iam_session.resource('s3', region) + self.lambda_function = boto3_client('lambda', aws_access_key_id=self.access_key, + aws_secret_access_key=self.secret_key, region_name=region) + self.in_bucket_name = input_bucket + self.out_bucket_name = output_bucket + self.lambda_name = lambda_name + self.test_result = {} + + def validate_lambda_exists(self, TC_num): + try: + response = self.lambda_function.get_function( + FunctionName=self.lambda_name + ) + print(f"Lambda function {self.lambda_name} HTTPStatusCode {response['ResponseMetadata']['HTTPStatusCode']}") + self.test_result[TC_num] = "PASS" + except self.lambda_function.exceptions.ResourceNotFoundException as e: + print(f"Error {e}") + self.test_result[TC_num] = "FAIL" + print(f"Test status of {TC_num} : {self.test_result[TC_num]}") + + def validate_s3_subfolders(self, TC_num): + in_objects = self.s3.list_objects_v2(Bucket=self.in_bucket_name) + if in_objects['KeyCount']==0: + self.test_result[TC_num] = "FAIL" + print(f"Empty bucket {self.in_bucket_name}") + print(f"Test status of {TC_num} : {self.test_result[TC_num]}") + return + self.test_result[TC_num] = "PASS" + for obj in in_objects['Contents']: + folder_name = obj['Key'].rsplit('.',1)[0] + out_objects = self.s3.list_objects_v2(Bucket=self.out_bucket_name, Prefix=folder_name, Delimiter='/') + if out_objects['KeyCount'] == 1 or out_objects['KeyCount'] == 11: + folder_name = out_objects['CommonPrefixes'][0]['Prefix'].rsplit("/")[0] + prefix_name = out_objects['Prefix'] + if folder_name == prefix_name: + print(f"{prefix_name} matches with {folder_name}") + else: + prefix_name = out_objects['Prefix'] + self.test_result[TC_num] = "FAIL" + print(f"NO folder named {prefix_name}") + print(out_objects) + print(f"Test status of {TC_num} : {self.test_result[TC_num]}") + + def validate_s3_output_objects(self, TC_num): + bucket = self.s3_resources.Bucket(self.out_bucket_name) + in_bucket = self.s3_resources.Bucket(self.in_bucket_name) + + try: + objects = list(bucket.objects.all()) + print(f"Got {len(objects)} objects {[o.key for o in objects]} from bucket {bucket.name}") + in_objects = list(in_bucket.objects.all()) + self.test_result[TC_num] = "PASS" + + for i,folder_n in enumerate(in_objects): + if len(in_objects) * 10 == len(objects) or len(in_objects) * 11 == len(objects): + print(f"Number of objects matches for given input {folder_n}") + self.test_result[TC_num] = "PASS" + else: + self.test_result[TC_num] = "FAIL" + break + print(f"Test status of {TC_num} : {self.test_result[TC_num]}") + + + except ClientError: + print(f"Couldn't get objects for bucket {bucket.name}") + raise + else: + return + + # You have to make sure to run the workload generator and it executes within 15 mins + # of polling for cloudwatch metrics. + def check_lambda_duration(self, TC_num): + response = self.cloudwatch.get_metric_data( + MetricDataQueries=[ + { + 'Id': 'testDuration', + 'MetricStat': { + 'Metric': { + 'Namespace': 'AWS/Lambda', + 'MetricName': 'Duration' + }, + 'Period': 600, + 'Stat': 'Average' + }, + 'ReturnData': True, + }, + ], + StartTime=datetime.now().utcnow() - timedelta(minutes=15), + EndTime=datetime.now().utcnow(), + ScanBy='TimestampAscending' + ) + print(response['MetricDataResults'][0]['Values']) + values = response['MetricDataResults'][0]['Values'] + if not values: + self.test_result[TC_num] = "FAIL" + print(f"Test status of {TC_num} : {self.test_result[TC_num]}") + return + if max(values) > 10000: + self.test_result[TC_num] = "FAIL" + else: + self.test_result[TC_num] = "PASS" + print(f"Test status of {TC_num} : {self.test_result[TC_num]}") + + def check_lambda_concurrency(self,TC_num): + response = self.cloudwatch.get_metric_data( + MetricDataQueries=[ + { + 'Id': 'testConcurrency', + 'MetricStat': { + 'Metric': { + 'Namespace': 'AWS/Lambda', + 'MetricName': 'ConcurrentExecutions' + }, + 'Period': 600, + 'Stat': 'Maximum' + }, + 'ReturnData': True, + }, + ], + StartTime=datetime.now().utcnow() - timedelta(minutes=15), + EndTime=datetime.now().utcnow(), + ScanBy='TimestampAscending' + ) + print(response['MetricDataResults'][0]['Values']) + values = response['MetricDataResults'][0]['Values'] + if not values: + self.test_result[TC_num] = "FAIL" + print(f"Test status of {TC_num} : {self.test_result[TC_num]}") + return + if max(values) < 5: + self.test_result[TC_num] = "FAIL" + else: + self.test_result[TC_num] = "PASS" + print(f"Test status of {TC_num} : {self.test_result[TC_num]}") + + def check_bucket_exist(self, bucket): + if not bucket: + print(f"Bucket name is empty!") + return False + try: + self.s3.head_bucket(Bucket=bucket) + print(f"Bucket {bucket} Exists!") + return True + except botocore.exceptions.ClientError as e: + # If a client error is thrown, then check that it was a 404 error. + # If it was a 404 error, then the bucket does not exist. + error_code = int(e.response['Error']['Code']) + if error_code == 403: + print("Private Bucket. Forbidden Access!") + return True + elif error_code == 404: + print(f"Bucket {bucket} does Not Exist!") + return False + def empty_s3_bucket(self, bucket_name): + bucket = self.s3_resources.Bucket(bucket_name) + bucket.objects.all().delete() + print(f"{bucket_name} S3 Bucket is now EMPTY !!") + + def count_bucket_objects(self, bucket_name): + bucket = self.s3_resources.Bucket(bucket_name) + count = 0 + for index in bucket.objects.all(): + count += 1 + #print(f"{bucket_name} S3 Bucket has {count} objects !!") + return count + + def validate_s3_buckets_initial(self, TC_num): + print(" - Run this BEFORE the workload generator client starts. Press Ctrl^C to exit.") + print(" - WARN: If there are objects in the S3 buckets; they will be deleted") + print(" ---------------------------------------------------------") + + in_isExist = self.check_bucket_exist(self.in_bucket_name) + out_isExist = self.check_bucket_exist(self.out_bucket_name) + + if in_isExist: + ip_obj_count = self.count_bucket_objects(self.in_bucket_name) + print(f"S3 Input Bucket:{self.in_bucket_name} has {ip_obj_count} object(s)") + if out_isExist: + op_obj_count = self.count_bucket_objects(self.out_bucket_name) + print(f"S3 Output Bucket:{self.out_bucket_name} has {op_obj_count} object(s)") + + if in_isExist and out_isExist and ip_obj_count==0 and op_obj_count==0: + self.test_result[TC_num] = "PASS" + print(f"Test status of {TC_num} : {self.test_result[TC_num]}") + else: + self.test_result[TC_num] = "FAIL" + print(f"Test status of {TC_num} : {self.test_result[TC_num]}") + + def display_menu(self): + print("\n") + print("=============================================================================") + print("======== Welcome to CSE546 Cloud Computing AWS Console ======================") + print("=============================================================================") + print(f"IAM ACESS KEY ID: {self.access_key}") + print(f"IAM SECRET ACCESS KEY: {self.secret_key}") + print("=============================================================================") + print("1 - Validate 1 Lambda function") + print("2 - Validate S3 Buckets names and initial states") + print("3 - Validate S3 output bucket subfolders") + print("4 - Validate S3 output objects") + print("5 - Check lambda average duration") + print("6 - Check lambda concurrency") + print("0 - Exit") + print("Enter a choice:") + choice = input() + return choice + + def main(self): + while(1): + choice = self.display_menu() + if int(choice) == 1: + self.validate_lambda_exists('Test_1') + elif int(choice) == 2: + self.validate_s3_buckets_initial('Test_2') + elif int(choice) == 3: + self.validate_s3_subfolders('Test_3') + elif int(choice) == 4: + self.validate_s3_output_objects('Test_4') + elif int(choice) == 5: + self.check_lambda_duration('Test_5') + elif int(choice) == 6: + self.check_lambda_concurrency('Test_6') + elif int(choice) == 0: + break + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='Grading Script') + parser.add_argument('--access_key', type=str, help='ACCCESS KEY ID of the grading IAM user') + parser.add_argument('--secret_key', type=str, help='SECRET KEY of the grading IAM user') + parser.add_argument('--input_bucket', type=str, help='Name of the S3 Input Bucket') + parser.add_argument('--output_bucket', type=str, help='Name of the S3 Output Bucket') + parser.add_argument('--lambda_name', type=str, help="Name of the Lambda function") + + + args = parser.parse_args() + + access_key = args.access_key + secret_key = args.secret_key + input_bucket = args.input_bucket + output_bucket = args.output_bucket + lambda_name = args.lambda_name + region = 'us-east-1' + + aws_obj = aws_grader(access_key, secret_key, input_bucket, output_bucket, lambda_name,region) + aws_obj.main() diff --git a/Project-2/Part-1/src/handler.py b/Project-2/Part-1/src/handler.py new file mode 100644 index 0000000..d70385f --- /dev/null +++ b/Project-2/Part-1/src/handler.py @@ -0,0 +1,66 @@ +import os +import subprocess +import json +import urllib.parse +import boto3 + +print("Loading function") + + +# attach execution policies and IAM roles in deployment lambda +sesh = boto3.Session() + +s3 = sesh.client("s3", region_name="us-east-1") + + +def video_splitting_cmdline(video_filename): + filename = os.path.basename(video_filename) + outdir = os.path.splitext(filename)[0] + outdir = os.path.join("/tmp", outdir) + if not os.path.exists(outdir): + os.makedirs(outdir) + + split_cmd = ( + "ffmpeg -ss 0 -r 1 -i " + + video_filename + + " -vf fps=1/1 -start_number 0 -vframes 10 " + + outdir + + "/" + + "output-%02d.jpg -y" + ) + try: + subprocess.check_call(split_cmd, shell=True) + except subprocess.CalledProcessError as e: + print(e.returncode) + print(e.output) + + return outdir + + +def handler(event, context): + for record in event["Records"]: + # get uploaded object + in_bucket = record["s3"]["bucket"]["name"] + if in_bucket != "1229569564-input": + continue + key = urllib.parse.unquote_plus(record["s3"]["object"]["key"], encoding="utf-8") + tmpkey = key.replace("/", "") + download_path = "/tmp/{}".format(tmpkey) + s3.download_file(in_bucket, key, download_path) + + # process it + out_dir = video_splitting_cmdline(download_path) + + # upload output objects + for frame in os.listdir(out_dir): + s3.upload_file( + os.path.join(out_dir, frame), + "1229569564-stage-1", + os.path.splitext(tmpkey)[0] + "/" + frame, + ) + + +if __name__ == "__main__": + with open("dummy_s3_trigger_event.json", "r") as dummy_event: + event = json.loads(dummy_event.read()) + handler(event, None) diff --git a/Project-2/Part-1/src/lambda_s3_policy.json b/Project-2/Part-1/src/lambda_s3_policy.json new file mode 100644 index 0000000..98512e6 --- /dev/null +++ b/Project-2/Part-1/src/lambda_s3_policy.json @@ -0,0 +1,28 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "logs:PutLogEvents", + "logs:CreateLogGroup", + "logs:CreateLogStream" + ], + "Resource": "arn:aws:logs:*:*:*" + }, + { + "Effect": "Allow", + "Action": [ + "s3:GetObject" + ], + "Resource": "arn:aws:s3:::*/*" + }, + { + "Effect": "Allow", + "Action": [ + "s3:PutObject" + ], + "Resource": "arn:aws:s3:::*/*" + } + ] +} \ No newline at end of file diff --git a/Project-2/Part-1/src/requirements.txt b/Project-2/Part-1/src/requirements.txt new file mode 100644 index 0000000..30ddf82 --- /dev/null +++ b/Project-2/Part-1/src/requirements.txt @@ -0,0 +1 @@ +boto3 diff --git a/Project-2/Part-1/src/workload_generator.py b/Project-2/Part-1/src/workload_generator.py new file mode 100644 index 0000000..d55cd2a --- /dev/null +++ b/Project-2/Part-1/src/workload_generator.py @@ -0,0 +1,73 @@ +#__copyright__ = "Copyright 2024, VISA Lab" +#__license__ = "MIT" + +from boto3 import client as boto3_client +import os +import argparse +import time + +input_bucket = "546proj2" +output_bucket = "546proj2output" +test_cases = "test_cases/" + +start_time = time.time() +parser = argparse.ArgumentParser(description='Upload videos to input S3') +# parser.add_argument('--num_request', type=int, help='one video per request') +parser.add_argument('--access_key', type=str, help='ACCCESS KEY of the grading IAM user') +parser.add_argument('--secret_key', type=str, help='SECRET KEY of the grading IAM user') +parser.add_argument('--input_bucket', type=str, help='Name of the input bucket, e.g. 1234567890-input') +parser.add_argument('--output_bucket', type=str, help='Name of the output bucket, e.g. 1234567890-stage-1') +parser.add_argument('--testcase_folder', type=str, help='the path of the folder where videos are saved, e.g. test_cases/test_case_1/') + +args = parser.parse_args() + +access_key = args.access_key +secret_key = args.secret_key +input_bucket = args.input_bucket +output_bucket = args.output_bucket +test_cases = args.testcase_folder +region = 'us-east-1' + +s3 = boto3_client('s3', aws_access_key_id = access_key, + aws_secret_access_key = secret_key, region_name=region) +def clear_input_bucket(input_bucket): + global s3 + list_obj = s3.list_objects_v2(Bucket=input_bucket) + try: + for item in list_obj["Contents"]: + key = item["Key"] + s3.delete_object(Bucket=input_bucket, Key=key) + except: + print("Nothing to clear in input bucket") + +def clear_output_bucket(output_bucket): + global s3 + list_obj = s3.list_objects_v2(Bucket=output_bucket) + try: + for item in list_obj["Contents"]: + key = item["Key"] + s3.delete_object(Bucket=output_bucket, Key=key) + except: + print("Nothing to clear in output bucket") + +def upload_to_input_bucket_s3(input_bucket, path, name): + global s3 + s3.upload_file(path + name, input_bucket, name) + +def upload_files(input_bucket, test_dir): + for filename in os.listdir(test_dir): + if filename.endswith(".mp4") or filename.endswith(".MP4"): + print("Uploading to input bucket.. name: " + str(filename)) + upload_to_input_bucket_s3(input_bucket, test_dir, filename) + + + +clear_input_bucket(input_bucket) +clear_input_bucket(output_bucket) + +upload_files(input_bucket,test_cases) + +end_time = time.time() +print("Time to run = ", end_time-start_time, "(seconds)") +print(f"Timestamps: start {start_time}, end {end_time}") +