diff --git a/app/config/development.default.py b/app/config/development.default.py index 963406edb64d8ab9958044b20558d41297d47897..e65d77367d0ccb734afa4b089f07a5c4d08a38a5 100644 --- a/app/config/development.default.py +++ b/app/config/development.default.py @@ -37,3 +37,9 @@ AUTH0_CLIENT_SECRET = os.environ['AUTH0_CLIENT_SECRET'] # Blocpower auth information. HEADER_AUTH_KEY = 'x-blocpower-auth-key' HEADER_AUTH_TOKEN = 'x-blocpower-auth-token' + +# AWS credentials +AWS_ACCESS_KEY = os.environ['AWS_ACCESS_KEY'] +AWS_SECRET_ACCESS_KEY = os.environ['AWS_SECRET_ACCESS_KEY'] +AWS_REGION = os.environ['AWS_REGION'] +AWS_ACCOUNT_ID = os.environ['AWS_ACCOUNT_ID'] diff --git a/app/config/local.default.py b/app/config/local.default.py index 0273fd7dbceb6e02f4e31c1300e63e4898534e2e..9cddc3832d48c756a89bb3afc3a0887a72a77742 100644 --- a/app/config/local.default.py +++ b/app/config/local.default.py @@ -34,3 +34,9 @@ AUTH0_CLIENT_SECRET = '$AUTH0_CLIENT_SECRET' # Blocpower auth information. HEADER_AUTH_KEY = 'x-blocpower-auth-key' HEADER_AUTH_TOKEN = 'x-blocpower-auth-token' + +# AWS credentials +AWS_ACCESS_KEY = '$AWS_ACCESS_KEY' +AWS_SECRET_ACCESS_KEY = '$AWS_SECRET_ACCESS_KEY' +AWS_REGION = '$AWS_REGION' +AWS_ACCOUNT_ID = '$AWS_ACCOUNT_ID' diff --git a/app/config/production.default.py b/app/config/production.default.py index 4e30ea106014e25061e6b5becbbfe1de08de3c0b..9520608c9f19268ca968fb1d037dc7fefdba7321 100644 --- a/app/config/production.default.py +++ b/app/config/production.default.py @@ -37,3 +37,9 @@ AUTH0_CLIENT_SECRET = os.environ['AUTH0_CLIENT_SECRET'] # Blocpower auth information. HEADER_AUTH_KEY = 'x-blocpower-auth-key' HEADER_AUTH_TOKEN = 'x-blocpower-auth-token' + +# AWS credentials +AWS_ACCESS_KEY = os.environ['AWS_ACCESS_KEY'] +AWS_SECRET_ACCESS_KEY = os.environ['AWS_SECRET_ACCESS_KEY'] +AWS_REGION = os.environ['AWS_REGION'] +AWS_ACCOUNT_ID = os.environ['AWS_ACCOUNT_ID'] diff --git a/app/config/staging.default.py b/app/config/staging.default.py index 3fa2a76db5e3af4d3dc81b185812b8cb37009215..8540a1ecd54983494cdd5e2ba93ffaad3d379876 100644 --- a/app/config/staging.default.py +++ b/app/config/staging.default.py @@ -37,3 +37,9 @@ AUTH0_CLIENT_SECRET = os.environ['AUTH0_CLIENT_SECRET'] # Blocpower auth information. HEADER_AUTH_KEY = 'x-blocpower-auth-key' HEADER_AUTH_TOKEN = 'x-blocpower-auth-token' + +# AWS credentials +AWS_ACCESS_KEY = os.environ['AWS_ACCESS_KEY'] +AWS_SECRET_ACCESS_KEY = os.environ['AWS_SECRET_ACCESS_KEY'] +AWS_REGION = os.environ['AWS_REGION'] +AWS_ACCOUNT_ID = os.environ['AWS_ACCOUNT_ID'] diff --git a/app/controllers/alert_subscription.py b/app/controllers/alert_subscription.py new file mode 100644 index 0000000000000000000000000000000000000000..0252aec3fb841376b5d49d611a76e540fb4f1a24 --- /dev/null +++ b/app/controllers/alert_subscription.py @@ -0,0 +1,125 @@ +from flask import current_app +from werkzeug.datastructures import MultiDict +from werkzeug.exceptions import BadRequest +import os + +from .base import RestController +from app.lib import get_sns_client + +class AlertSubscriptionController(RestController): + """An alert_subscription controller.""" + + def index(self, filter_data): + """ + Retrieve a list of topics and their subscribers by: + + building_id[] - filter by building id + """ + client = get_sns_client(current_app) + if 'building_id[]' in filter_data: + building_ids = filter_data.getlist('building_id[]') + topics = [] + for building_id in building_ids: + # Parse the building_ids and generate a list of subscriptions + topic_arn = self.generate_topic_arn(building_id) + try: + res = client.list_subscriptions_by_topic( + TopicArn=topic_arn, + ) + except client.exceptions.NotFoundException as e: + # If there is no topic there are no subscriptions + topics.append({ + 'building_id': building_id, + 'arn': topic_arn, + 'subscriptions': [], + }) + continue + topic = { + 'building_id': building_id, + 'arn': topic_arn, + 'subscriptions': res['Subscriptions'], + } + while 'NextToken' in res: + res = client.list_subscriptions_by_topic( + TopicArn=topic_arn, + NextToken=res['NextToken'], + ) + topic['subscriptions'].extend(res['Subscriptions']) + topics.append(topic) + else: + raise BadRequest( + 'An index request should have atleast one building_id[]' + ) + return topics + + + def post(self, data, filter_data): + ''' Add an email or telephone number to a topic''' + try: + building_id = data['building_id'] + sub_type = data['sub_type'] + sub_val = data['sub_val'] + except KeyError as e: + raise BadRequest('Missing a parameter in the request body') + if sub_type not in ['email', 'sms']: + raise BadRequest('sub_type must be one of email or sms') + client = get_sns_client(current_app) + topic_arn = self.generate_topic_arn(building_id) + try: + res = client.subscribe( + TopicArn=topic_arn, + Protocol=sub_type, + Endpoint=sub_val, + ) + return { + 'subscription_arn': res['SubscriptionArn'], + 'topic_arn': topic_arn, + 'building_id': building_id, + 'sub_type': sub_type, + 'sub_val': sub_val, + } + except client.exceptions.NotFoundException as e: + topic_name = self.generate_topic_name(building_id) + client.create_topic( + Name=topic_name, + ) + res = client.subscribe( + TopicArn=topic_arn, + Protocol=sub_type, + Endpoint=sub_val, + ) + return { + 'subscription_arn': res['SubscriptionArn'], + 'topic_arn': topic_arn, + 'building_id': building_id, + 'sub_type': sub_type, + 'sub_val': sub_val, + } + + def delete(self, id_, filter_data): + ''' Remove a subscription from a topic given a SubscriptionArn ''' + client = get_sns_client(current_app) + client.unsubscribe( + SubscriptionArn=id_, + ) + return { + 'status': 'success', + 'subscription_arn': id_, + } + + + def generate_topic_name(self, building_id): + ''' Generate the name of a topic given a building_id ''' + environment = os.environ['ENVIRONMENT'] + if environment == 'production': + env_string = '' + else: + env_string = f'_{environment}' + return f'building_{building_id}_iot_alert{env_string}' + + def generate_topic_arn(self, building_id): + ''' Generate the AWS ARN for an sns given a building id ''' + account_id = current_app.config.get('AWS_ACCOUNT_ID') + topic_name = self.generate_topic_name(building_id) + return f'arn:aws:sns:us-east-1:{account_id}:{topic_name}' + diff --git a/app/lib/__init__.py b/app/lib/__init__.py index 9e859d01100e454b76f89aa28f1a05b5489d8f42..94cba487703efdc4927e440106a7afb0832c4467 100644 --- a/app/lib/__init__.py +++ b/app/lib/__init__.py @@ -2,3 +2,4 @@ from .database import db from .red import redis from .auth0 import auth0_ from .redshift_database import get_redshift_db +from .sns import get_sns_client diff --git a/app/lib/redshift_database.py b/app/lib/redshift_database.py index 282d64127d4450a0b06035fca85fdb5078d58060..87190812e5e29394943b8cb250ff381ba6d609a1 100644 --- a/app/lib/redshift_database.py +++ b/app/lib/redshift_database.py @@ -7,7 +7,7 @@ app = Flask(__name__) def get_redshift_db(app): db = getattr(g, 'redshift_database', None) if db is None: - db = g._database = connect_to_database(app) + db = g.redshift_database = connect_to_database(app) return db @app.teardown_appcontext diff --git a/app/lib/sns.py b/app/lib/sns.py new file mode 100644 index 0000000000000000000000000000000000000000..789ec2b5ced6cfbb0332de5b6818c7afd0d31bdb --- /dev/null +++ b/app/lib/sns.py @@ -0,0 +1,24 @@ +from flask import Flask, g +import boto3 + +app = Flask(__name__) + +def get_sns_client(app): + client = getattr(g, 'sns_client', None) + if client is None: + client = g.sns_client = connect_to_client(app) + return client + +def connect_to_client(app): + AWS_ACCESS_KEY = app.config.get('AWS_ACCESS_KEY') + AWS_SECRET_ACCESS_KEY = app.config.get('AWS_SECRET_ACCESS_KEY') + REGION = app.config.get('AWS_REGION') + if not AWS_ACCESS_KEY or not AWS_SECRET_ACCESS_KEY: + raise ValueError("AWS keys are empty in the config file") + + return boto3.client( + 'sns', + aws_access_key_id=AWS_ACCESS_KEY, + aws_secret_access_key=AWS_SECRET_ACCESS_KEY, + region_name=REGION, + ) diff --git a/app/views/__init__.py b/app/views/__init__.py index caa8735359f24be060e040f8ba0d96b4329edc77..a8534aea068d910d6a0867bc0b055496bf22e2a5 100644 --- a/app/views/__init__.py +++ b/app/views/__init__.py @@ -5,6 +5,7 @@ from . import ( data, sensor_image, event, + alert_subscription, ) def register(app): @@ -19,3 +20,4 @@ def register(app): data.DataView.register(app) sensor_image.SensorImageView.register(app) event.EventView.register(app) + alert_subscription.AlertSubscriptionView.register(app) diff --git a/app/views/alert_subscription.py b/app/views/alert_subscription.py new file mode 100644 index 0000000000000000000000000000000000000000..7f6056c8f7e6b94217d966c52e7c3928c43882d5 --- /dev/null +++ b/app/views/alert_subscription.py @@ -0,0 +1,19 @@ +"""Views for working with alert_subscriptions.""" +from .base import RestView +from ..controllers.alert_subscription import AlertSubscriptionController + +from werkzeug.exceptions import MethodNotAllowed +from flask import request + +class AlertSubscriptionView(RestView): + """The alert_subscription view.""" + + def get(self, id_): + raise MethodNotAllowed('AlertSubscriptions can only retrieved via index') + + def put(self, id_): + raise MethodNotAllowed('AlertSubscriptions can only be created and removed') + + def get_controller(self): + """Return an instance of the alert_subscription controller.""" + return AlertSubscriptionController() diff --git a/requirements.txt b/requirements.txt index 35b57ab425102d11e207a2dff8674382097dcb87..a53a5ef247629e3136b869890bef6b44fba5140f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ arrow==0.7.0 blessed==1.9.5 -boto3==1.4.4 -botocore==1.5.48 +boto3==1.7.24 +botocore==1.10.24 git+ssh://git@github.com/Blocp/bpvalve.git@v1.3.1 cement==2.4.0 colorama==0.3.3