Basil Eljuse | 4b14afb | 2020-09-30 13:07:23 +0100 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | |
| 3 | from __future__ import print_function |
| 4 | from data_validator import DataValidator |
| 5 | import credentials |
| 6 | |
| 7 | __copyright__ = """ |
| 8 | /* |
| 9 | * Copyright (c) 2020, Arm Limited. All rights reserved. |
| 10 | * |
| 11 | * SPDX-License-Identifier: BSD-3-Clause |
| 12 | * |
| 13 | */ |
| 14 | """ |
| 15 | |
| 16 | """ quality_metrics_server.py: |
| 17 | |
| 18 | This is the broker component which accepts the data from data |
| 19 | generator scripts, and performs basic sanity check and pushes |
| 20 | the data to Influx-DB for visualisation with Grafana component. |
| 21 | It is not mandatory to push data via data generator scripts. |
| 22 | The request to push data to database, in this case - InfluxDB, |
| 23 | is expected to be be a POST request with right credentials and |
| 24 | should be in agreed upon format. |
| 25 | |
| 26 | """ |
| 27 | |
| 28 | from pprint import pprint |
| 29 | from pprint import pformat |
| 30 | from db_manager import dbManager |
| 31 | from flask_jwt import JWT, jwt_required |
| 32 | from flask import Flask, jsonify, request |
| 33 | from werkzeug.security import safe_str_cmp |
| 34 | from logging.handlers import RotatingFileHandler |
| 35 | |
| 36 | import sys |
| 37 | import json |
| 38 | import random |
| 39 | import logging |
| 40 | import argparse |
| 41 | import datetime |
| 42 | |
| 43 | import constants |
| 44 | """ It is suggested to keep credentials.py is kept locally in the |
| 45 | system where server is running. This file has been provided |
| 46 | for reference. |
| 47 | """ |
| 48 | |
| 49 | username_table = {u.username: u for u in credentials.users} |
| 50 | userid_table = {u.id: u for u in credentials.users} |
| 51 | |
| 52 | |
| 53 | def authenticate(username, password): |
| 54 | user = username_table.get(username, None) |
| 55 | if user and safe_str_cmp( |
| 56 | user.password.encode('utf-8'), |
| 57 | password.encode('utf-8')): |
| 58 | return user |
| 59 | |
| 60 | |
| 61 | def identity(payload): |
| 62 | user_id = payload['identity'] |
| 63 | return userid_table.get(user_id, None) |
| 64 | |
| 65 | |
| 66 | def setup_logging(app): |
| 67 | # maxBytes and backupCount values to allow the file to rollover at a predetermined size. |
| 68 | # When the size is about to be exceeded, the file is closed and a new file is silently |
| 69 | # opened for output. Rollover occurs whenever the current log file is nearly maxBytes in length. |
| 70 | # When backupCount is non-zero, the system will save old log files by appending the extensions |
| 71 | # ‘.1’, ‘.2’ etc., to the filename. |
| 72 | file_handler = RotatingFileHandler( |
| 73 | "./flask.log", |
| 74 | maxBytes=1024 * 1024 * 1024 * 5, |
| 75 | backupCount=5) |
| 76 | file_handler.setFormatter( |
| 77 | logging.Formatter( |
| 78 | '[%(asctime)s][PID:%(process)d][%(levelname)s]' |
| 79 | '[%(lineno)s][%(name)s.%(funcName)s()] %(message)s')) |
| 80 | file_handler.setLevel(logging.INFO) |
| 81 | loggers = [app.logger] |
| 82 | for logger in loggers: |
| 83 | logger.addHandler(file_handler) |
| 84 | app.logger.setLevel(logging.INFO) |
| 85 | |
| 86 | |
| 87 | app = Flask(__name__) |
| 88 | |
| 89 | setup_logging(app) |
| 90 | |
| 91 | logger = logging.getLogger(__name__) |
| 92 | |
| 93 | app.debug = True |
| 94 | app.config['SECRET_KEY'] = credentials.SECRET_KEY |
| 95 | app.config['JWT_EXPIRATION_DELTA'] = datetime.timedelta( |
| 96 | days=constants.JWT_EXPIRATION_DAYS) |
| 97 | |
| 98 | dbm = dbManager(app=app).start_daemon() |
| 99 | |
| 100 | jwt = JWT(app, authenticate, identity) |
| 101 | |
| 102 | # ----------------------- Database Methods ----------------------------------# |
| 103 | |
| 104 | |
| 105 | def store_to_db(data_dict): |
| 106 | """ |
| 107 | Use the database manager to asynchronously update the database |
| 108 | |
| 109 | :param: data_dict: Dictionary containing data to be stored |
| 110 | """ |
| 111 | validation, err_code = dbm.store(data_dict) |
| 112 | return validation, err_code |
| 113 | |
| 114 | # ----------------------- FLASK API Methods ---------------------------------- # |
| 115 | |
| 116 | |
| 117 | @app.route('/', methods=['POST']) |
| 118 | @jwt_required() |
| 119 | def add_db_entry(): |
| 120 | """ |
| 121 | Store received data to database if validation is okay |
| 122 | |
| 123 | :return: validation information and error code |
| 124 | """ |
| 125 | |
| 126 | data = request.get_json() |
| 127 | app.logger.debug("Received Data (POST)") |
| 128 | app.logger.debug(pformat(data)) |
| 129 | # Make sure the data is valid |
| 130 | validation, err_code = DataValidator.validate_request_sanity(data) |
| 131 | if validation == "OK": |
| 132 | app.logger.info("<<<<VALIDATION OK>>>>") |
| 133 | validation, err_code = store_to_db(data) |
| 134 | else: |
| 135 | app.logger.error("<<<<VALIDATION NOT OK>>>>") |
| 136 | app.logger.error(pformat({"data": validation, "error_code": err_code})) |
| 137 | info_json = jsonify({"data": validation, "error_code": err_code}) |
| 138 | return info_json, err_code |
| 139 | |
| 140 | |
| 141 | @app.route("/") |
| 142 | def home(): |
| 143 | info_json = jsonify({"type": "INFO", "data": "Quality Metrics"}) |
| 144 | return info_json, 200 |
| 145 | |
| 146 | |
| 147 | if __name__ == '__main__': |
| 148 | try: |
| 149 | app.run(host=constants.LISTEN_ALL_IPS, port=5000) |
| 150 | except Exception as ex: |
| 151 | template = "An exception of type {0} occurred. Arguments:\n{1!r}" |
| 152 | message = template.format(type(ex).__name__, ex.args) |
| 153 | app.logger.error("message") |
| 154 | dbm.stop() |