osdf rearchitecture into apps and libs
[optf/osdf.git] / osdf / apps / baseapp.py
1 # -------------------------------------------------------------------------
2 #   Copyright (c) 2015-2017 AT&T Intellectual Property
3 #
4 #   Licensed under the Apache License, Version 2.0 (the "License");
5 #   you may not use this file except in compliance with the License.
6 #   You may obtain a copy of the License at
7 #
8 #       http://www.apache.org/licenses/LICENSE-2.0
9 #
10 #   Unless required by applicable law or agreed to in writing, software
11 #   distributed under the License is distributed on an "AS IS" BASIS,
12 #   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 #   See the License for the specific language governing permissions and
14 #   limitations under the License.
15 #
16 # -------------------------------------------------------------------------
17 #
18
19 """
20 OSDF Manager Main Flask Application
21 """
22
23 import json
24 import ssl
25 import sys
26 import time
27 import traceback
28 from optparse import OptionParser
29
30 import pydevd
31 from flask import Flask, request, Response, g
32 from requests import RequestException
33 from schematics.exceptions import DataError
34
35 import osdf.adapters.aaf.sms as sms
36 import osdf.operation.responses
37 from osdf.config.base import osdf_config
38 from osdf.logging.osdf_logging import error_log, debug_log
39 from osdf.operation.error_handling import request_exception_to_json_body, internal_error_message
40 from osdf.operation.exceptions import BusinessException
41 from osdf.utils.mdc_utils import clear_mdc, mdc_from_json, default_mdc
42
43 ERROR_TEMPLATE = osdf.ERROR_TEMPLATE
44
45 app = Flask(__name__)
46
47 BAD_CLIENT_REQUEST_MESSAGE = 'Client sent an invalid request'
48
49
50 @app.errorhandler(BusinessException)
51 def handle_business_exception(e):
52     """An exception explicitly raised due to some business rule"""
53     error_log.error("Synchronous error for request id {} {}".format(g.request_id, traceback.format_exc()))
54     err_msg = ERROR_TEMPLATE.render(description=str(e))
55     response = Response(err_msg, content_type='application/json; charset=utf-8')
56     response.status_code = 400
57     return response
58
59
60 @app.errorhandler(RequestException)
61 def handle_request_exception(e):
62     """Returns a detailed synchronous message to the calling client
63     when osdf fails due to a remote call to another system"""
64     error_log.error("Synchronous error for request id {} {}".format(g.request_id, traceback.format_exc()))
65     err_msg = request_exception_to_json_body(e)
66     response = Response(err_msg, content_type='application/json; charset=utf-8')
67     response.status_code = 400
68     return response
69
70
71 @app.errorhandler(DataError)
72 def handle_data_error(e):
73     """Returns a detailed message to the calling client when the initial synchronous message is invalid"""
74     error_log.error("Synchronous error for request id {} {}".format(g.request_id, traceback.format_exc()))
75
76     body_dictionary = {
77         "serviceException": {
78             "text": BAD_CLIENT_REQUEST_MESSAGE,
79             "exceptionMessage": str(e.errors),
80             "errorType": "InvalidClientRequest"
81         }
82     }
83
84     body_as_json = json.dumps(body_dictionary)
85     response = Response(body_as_json, content_type='application/json; charset=utf-8')
86     response.status_code = 400
87     return response
88
89
90 @app.before_request
91 def log_request():
92     g.request_start = time.clock()
93     if request.get_json():
94
95         request_json = request.get_json()
96         g.request_id = request_json['requestInfo']['requestId']
97         mdc_from_json(request_json)
98     else:
99         g.request_id = "N/A"
100         default_mdc()
101
102
103
104 @app.after_request
105 def log_response(response):
106     clear_mdc()
107     return response
108
109
110 @app.errorhandler(500)
111 def internal_failure(error):
112     """Returned when unexpected coding errors occur during initial synchronous processing"""
113     error_log.error("Synchronous error for request id {} {}".format(g.request_id, traceback.format_exc()))
114     response = Response(internal_error_message, content_type='application/json; charset=utf-8')
115     response.status_code = 500
116     return response
117
118
119 def get_options(argv):
120     program_version_string = '%%prog %s' % "v1.0"
121     program_longdesc = ""
122     program_license = ""
123
124     parser = OptionParser(version=program_version_string, epilog=program_longdesc, description=program_license)
125     parser.add_option("-l", "--local", dest="local", help="run locally", action="store_true", default=False)
126     parser.add_option("-t", "--devtest", dest="devtest", help="run in dev/test environment", action="store_true",
127                       default=False)
128     parser.add_option("-d", "--debughost", dest="debughost", help="IP Address of host running debug server", default='')
129     parser.add_option("-p", "--debugport", dest="debugport", help="Port number of debug server", type=int, default=5678)
130     opts, args = parser.parse_args(argv)
131
132     if opts.debughost:
133         debug_log.debug('pydevd.settrace({}, port={})'.format(opts.debughost, opts.debugport))
134         pydevd.settrace(opts.debughost, port=opts.debugport)
135     return opts
136
137
138 def build_ssl_context():
139     ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
140     ssl_context.set_ciphers('ECDHE-RSA-AES128-SHA256:EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH')
141     ssl_context.load_cert_chain(sys_conf['ssl_context'][0], sys_conf['ssl_context'][1])
142     return ssl_context
143
144
145 def run_app():
146     global sys_conf
147     sys_conf = osdf_config['core']['osdf_system']
148     ports = sys_conf['osdf_ports']
149     internal_port, external_port = ports['internal'], ports['external']
150     local_host = sys_conf['osdf_ip_default']
151     common_app_opts = dict(host=local_host, threaded=True, use_reloader=False)
152     ssl_opts = sys_conf.get('ssl_context')
153     if ssl_opts:
154         common_app_opts.update({'ssl_context': build_ssl_context()})
155     opts = get_options(sys.argv)
156     # Load secrets from SMS
157     sms.load_secrets()
158     if not opts.local and not opts.devtest:  # normal deployment
159         app.run(port=internal_port, debug=False, **common_app_opts)
160     else:
161         port = internal_port if opts.local else external_port
162         app.run(port=port, debug=True, **common_app_opts)
163
164
165 if __name__ == "__main__":
166     run_app()