From b28789e11dc4dc247cd998dc82e65f51c07b4876 Mon Sep 17 00:00:00 2001 From: Arshia Moghimi Date: Wed, 31 Jul 2024 20:37:20 -0700 Subject: [PATCH] Operator functions (#33) * added api for retrieving operator config * added api for generating upload url --- .../apiOperatorConfigRetreiver.py | 81 ++++++++++ .../apiOperatorUploadUrl.py | 19 +++ backend/lib/api-stack.ts | 151 +++++++++++------- 3 files changed, 194 insertions(+), 57 deletions(-) create mode 100644 backend/lambda/apiOperatorConfigRetreiver/apiOperatorConfigRetreiver.py create mode 100644 backend/lambda/apiOperatorUploadUrl/apiOperatorUploadUrl.py diff --git a/backend/lambda/apiOperatorConfigRetreiver/apiOperatorConfigRetreiver.py b/backend/lambda/apiOperatorConfigRetreiver/apiOperatorConfigRetreiver.py new file mode 100644 index 0000000..2e45f60 --- /dev/null +++ b/backend/lambda/apiOperatorConfigRetreiver/apiOperatorConfigRetreiver.py @@ -0,0 +1,81 @@ +import os +import boto3 +import json +import psycopg2 +import psycopg2.extras +import uuid +import csv +import base64 + +def get_db_secret(): + db_secret_name = os.environ["SM_DB_CREDENTIALS"] + # secretsmanager client to get db credentials + sm_client = boto3.client("secretsmanager") + response = sm_client.get_secret_value(SecretId=db_secret_name)["SecretString"] + secret = json.loads(response) + return secret + + +def get_config(operator_id): + valid_uuid = str(uuid.UUID(operator_id)) + + db_secret = get_db_secret() + connection = psycopg2.connect( + user=db_secret["username"], + password=db_secret["password"], + host=db_secret["host"], + dbname=db_secret["dbname"], + ) + cursor = connection.cursor(cursor_factory = psycopg2.extras.RealDictCursor) + + get_hydrophone_ids = f""" + SELECT h.hydrophone_id, h.sampling_frequency, h.directory, h.file_name, h.calibration_available + FROM hydrophones h + WHERE h.hydrophone_operator_id = %s; + """ + + cursor.execute(get_hydrophone_ids, (valid_uuid,)) + + results = cursor.fetchall() + if results == {}: + raise Exception("No hydrophones found for the given operator ID.") + return results + + +def get_calibration_data(hydrophone_id): + calibration_data = {"frequency": [], "sensitivity": []} + bucket = os.environ['BUCKET_NAME'] + object_name = f"{hydrophone_id}/calibration.csv" + s3_client = boto3.client('s3') + s3_client.download_file(bucket, object_name, '/tmp/calibration.csv') + with open('/tmp/calibration.csv') as f: + reader = csv.reader(f) + next(reader) + for row in reader: + calibration_data["frequency"].append(row[0]) + calibration_data["sensitivity"].append(row[1]) + return calibration_data + + +def handler(event, context): + operator_id = event['queryStringParameters']['operator_id'] + try: + configs = get_config(operator_id) + except ValueError: + return {'statusCode': 400, 'body': 'Entered ID is not valid.'} + except Exception as e: + return {'statusCode': 400, 'body': str(e)} + client_config = {"operator_id": operator_id, "hydrophones": [], "scan_interval": 5, "upload_interval": 5} + + for config in configs: + hydrophone_config = {'id': config['hydrophone_id'], 'metrics': ['spl'], + 'directory_to_watch': config['directory'], 'file_structure_pattern': config['file_name'], + 'sample_rate': config['sampling_frequency']} + if config['calibration_available']: + try: + hydrophone_config['calibration_curve'] = get_calibration_data(hydrophone_config['id']) + except Exception: + print(f"Could not find the calibration file for hydrophone: {hydrophone_config['id']}") + client_config['hydrophones'].append(hydrophone_config) + + return {'statusCode': 200, 'body': base64.b64encode(json.dumps(client_config, indent=2).encode('utf-8'))} diff --git a/backend/lambda/apiOperatorUploadUrl/apiOperatorUploadUrl.py b/backend/lambda/apiOperatorUploadUrl/apiOperatorUploadUrl.py new file mode 100644 index 0000000..6b7e190 --- /dev/null +++ b/backend/lambda/apiOperatorUploadUrl/apiOperatorUploadUrl.py @@ -0,0 +1,19 @@ +import boto3 +import os +import base64 +import json + +s3_client = boto3.client('s3') + + +def handler(event, context): + object_key = event['queryStringParameters']['object_key'] + bucket_name = os.environ['BUCKET_NAME'] + + response = s3_client.generate_presigned_post(bucket_name, object_key) + + response = { + 'statusCode': 200, + 'body': base64.b64encode(json.dumps(response, indent=2).encode('utf-8')) + } + return response diff --git a/backend/lib/api-stack.ts b/backend/lib/api-stack.ts index ace6e58..718c22d 100644 --- a/backend/lib/api-stack.ts +++ b/backend/lib/api-stack.ts @@ -11,6 +11,7 @@ import * as iam from 'aws-cdk-lib/aws-iam'; import { VpcStack } from './vpc-stack'; import { DBStack } from './database-stack'; import { FunctionalityStack } from './functionality-stack'; +import * as cdk from "aws-cdk-lib"; export class APIStack extends Stack { public readonly stageARN_APIGW: string; @@ -211,6 +212,36 @@ export class APIStack extends Stack { role: lambdaRole, }); + const apiOperatorConfigRetreiver = new lambda.Function(this, "noiseTracker-apiOperatorConfigRetreiver", { + functionName: "noiseTracker-apiOperatorConfigRetreiver", + runtime: lambda.Runtime.PYTHON_3_9, + handler: "apiOperatorConfigRetreiver.handler", + timeout: Duration.seconds(60), + memorySize: 512, + environment:{ + SM_DB_CREDENTIALS: db.secretPathUser.secretName, + BUCKET_NAME: functionalityStack.bucketName, + }, + vpc: vpcStack.vpc, + code: lambda.Code.fromAsset("lambda/apiOperatorConfigRetreiver"), + layers: [psyscopg2], + role: lambdaRole, + }) + + const apiOperatorUploadUrl = new lambda.Function(this, "noiseTracker-apiOperatorUploadUrl", { + functionName: "noiseTracker-apiOperatorUploadUrl", + runtime: lambda.Runtime.PYTHON_3_9, + handler: "apiOperatorUploadUrl.handler", + timeout: Duration.seconds(60), + memorySize: 512, + environment:{ + BUCKET_NAME: functionalityStack.bucketName, + }, + vpc: vpcStack.vpc, + code: lambda.Code.fromAsset("lambda/apiOperatorUploadUrl"), + role: lambdaRole, + }) + const sesStatement = new iam.PolicyStatement(); sesStatement.addActions("ses:SendEmail"); sesStatement.addResources("*"); @@ -295,85 +326,91 @@ export class APIStack extends Stack { const adminAuthorizer = new apigateway.TokenAuthorizer(this, 'adminAuthorizer', {handler: adminAuthorizerFunction}); const operatorAuthorizer = new apigateway.TokenAuthorizer(this, 'operatorAuthorizer', {handler: operatorAuthorizerFunction}); - // define api resources - const adminResource = api.root.addResource('admin'); - const adminHydrophones = adminResource.addResource('hydrophones') - const adminOperators = adminResource.addResource('operators') - - const operatorResource = api.root.addResource('operator'); - const operatorDownload = operatorResource.addResource('download') - const operatorHydrophones = operatorResource.addResource('hydrophones') - const operatorOperators = operatorResource.addResource('operators') - - const publicResource = api.root.addResource('public'); - const publicHydrophones = publicResource.addResource('hydrophones') - const publicSpectrograms = publicResource.addResource('spectrograms') - const publicSPL = publicResource.addResource('spl') - const publicGauge = publicResource.addResource('gauge') - - adminHydrophones.addMethod('GET', new apigateway.LambdaIntegration(apiAdminHandler, {proxy: true}), { + // define api resources + const adminResource = api.root.addResource('admin'); + const adminHydrophones = adminResource.addResource('hydrophones') + const adminOperators = adminResource.addResource('operators') + + const operatorResource = api.root.addResource('operator'); + const operatorDownload = operatorResource.addResource('download') + const operatorHydrophones = operatorResource.addResource('hydrophones') + const operatorOperators = operatorResource.addResource('operators') + const operatorConfig = operatorResource.addResource('config') + const operatorUploadUrl = operatorResource.addResource('upload-url') + + const publicResource = api.root.addResource('public'); + const publicHydrophones = publicResource.addResource('hydrophones') + const publicSpectrograms = publicResource.addResource('spectrograms') + const publicSPL = publicResource.addResource('spl') + const publicGauge = publicResource.addResource('gauge') + + adminHydrophones.addMethod('GET', new apigateway.LambdaIntegration(apiAdminHandler, {proxy: true}), { authorizer: adminAuthorizer, authorizationType: apigateway.AuthorizationType.CUSTOM, - }); - adminHydrophones.addMethod('POST', new apigateway.LambdaIntegration(apiAdminHandler, {proxy: true}), - { + }); + adminHydrophones.addMethod('POST', new apigateway.LambdaIntegration(apiAdminHandler, {proxy: true}), + { authorizer: adminAuthorizer, authorizationType: apigateway.AuthorizationType.CUSTOM, - }); - adminHydrophones.addMethod('PUT', new apigateway.LambdaIntegration(apiAdminHandler, {proxy: true}), - { + }); + adminHydrophones.addMethod('PUT', new apigateway.LambdaIntegration(apiAdminHandler, {proxy: true}), + { authorizer: adminAuthorizer, authorizationType: apigateway.AuthorizationType.CUSTOM, - }); - adminHydrophones.addMethod('DELETE', new apigateway.LambdaIntegration(apiAdminHandler, {proxy: true}), - { + }); + adminHydrophones.addMethod('DELETE', new apigateway.LambdaIntegration(apiAdminHandler, {proxy: true}), + { authorizer: adminAuthorizer, authorizationType: apigateway.AuthorizationType.CUSTOM, - }); + }); - adminOperators.addMethod('GET', new apigateway.LambdaIntegration(apiAdminHandler, {proxy: true}), - { + adminOperators.addMethod('GET', new apigateway.LambdaIntegration(apiAdminHandler, {proxy: true}), + { authorizer: adminAuthorizer, authorizationType: apigateway.AuthorizationType.CUSTOM, - }); - adminOperators.addMethod('POST', new apigateway.LambdaIntegration(apiAdminHandler, {proxy: true}), - { + }); + adminOperators.addMethod('POST', new apigateway.LambdaIntegration(apiAdminHandler, {proxy: true}), + { authorizer: adminAuthorizer, authorizationType: apigateway.AuthorizationType.CUSTOM, - }); - adminOperators.addMethod('PUT', new apigateway.LambdaIntegration(apiAdminHandler, {proxy: true}), - { + }); + adminOperators.addMethod('PUT', new apigateway.LambdaIntegration(apiAdminHandler, {proxy: true}), + { authorizer: adminAuthorizer, authorizationType: apigateway.AuthorizationType.CUSTOM, - }); - adminOperators.addMethod('DELETE', new apigateway.LambdaIntegration(apiAdminHandler, {proxy: true}), - { + }); + adminOperators.addMethod('DELETE', new apigateway.LambdaIntegration(apiAdminHandler, {proxy: true}), + { authorizer: adminAuthorizer, authorizationType: apigateway.AuthorizationType.CUSTOM, - }); - - operatorDownload.addMethod('GET', new apigateway.LambdaIntegration(operatorDownloadHandler, {proxy: true}), - { + }); + operatorDownload.addMethod('GET', new apigateway.LambdaIntegration(operatorDownloadHandler, {proxy: true}), + { authorizer: operatorAuthorizer, authorizationType: apigateway.AuthorizationType.CUSTOM, - }); - - operatorHydrophones.addMethod('GET', new apigateway.LambdaIntegration(apiOperatorHandler, {proxy: true}), - { + }); + operatorHydrophones.addMethod('GET', new apigateway.LambdaIntegration(apiOperatorHandler, {proxy: true}), + { authorizer: operatorAuthorizer, authorizationType: apigateway.AuthorizationType.CUSTOM, - }); - - operatorOperators.addMethod('GET', new apigateway.LambdaIntegration(apiOperatorHandler, {proxy: true}), - { + }); + operatorOperators.addMethod('GET', new apigateway.LambdaIntegration(apiOperatorHandler, {proxy: true}), + { authorizer: operatorAuthorizer, authorizationType: apigateway.AuthorizationType.CUSTOM, - }); - - publicHydrophones.addMethod('GET', new apigateway.LambdaIntegration(apiPublicHandler, {proxy: true})); - publicSpectrograms.addMethod('GET', new apigateway.LambdaIntegration(apiPublicHandler, {proxy: true})); - publicSPL.addMethod('GET', new apigateway.LambdaIntegration(apiPublicHandler, {proxy: true})); - publicGauge.addMethod('GET', new apigateway.LambdaIntegration(apiPublicHandler, {proxy: true})); - + }); + operatorConfig.addMethod('GET', new apigateway.LambdaIntegration(apiOperatorConfigRetreiver, {proxy: true})); + operatorUploadUrl.addMethod('GET', new apigateway.LambdaIntegration(apiOperatorUploadUrl, {proxy: true})); + publicHydrophones.addMethod('GET', new apigateway.LambdaIntegration(apiPublicHandler, {proxy: true})); + publicSpectrograms.addMethod('GET', new apigateway.LambdaIntegration(apiPublicHandler, {proxy: true})); + publicSPL.addMethod('GET', new apigateway.LambdaIntegration(apiPublicHandler, {proxy: true})); + publicGauge.addMethod('GET', new apigateway.LambdaIntegration(apiPublicHandler, {proxy: true})); + + new cdk.CfnOutput(this, 'Output-Message', { + value: ` + Operator Get Config URL: ${api.urlForPath()}operator/config + Operator Upload URL: ${api.urlForPath()}operator/upload-url + `, + }) } } \ No newline at end of file