1ad7556d436957e4321048984074fec31f53d430
[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 from optparse import OptionParser
25 import ssl
26 import sys
27 import time
28 import traceback
29
30 from flask import Flask
31 from flask import g
32 from flask import request
33 from flask import Response
34 from onaplogging.mdcContext import MDC
35 from requests import RequestException
36 from schematics.exceptions import DataError
37
38 import osdf.adapters.aaf.sms as sms
39 from osdf.config.base import osdf_config
40 from osdf.logging.osdf_logging import audit_log
41 from osdf.logging.osdf_logging import debug_log
42 from osdf.logging.osdf_logging import error_log
43 from osdf.operation.error_handling import internal_error_message
44 from osdf.operation.error_handling import request_exception_to_json_body
45 from osdf.operation.exceptions import BusinessException
46 import osdf.operation.responses
47 from osdf.utils.mdc_utils import clear_mdc
48 from osdf.utils.mdc_utils import get_request_id
49 from osdf.utils.mdc_utils import populate_default_mdc
50 from osdf.utils.mdc_utils import populate_mdc
51 from osdf.utils.mdc_utils import set_error_details
52
53 ERROR_TEMPLATE = osdf.ERROR_TEMPLATE
54
55 app = Flask(__name__)
56
57 BAD_CLIENT_REQUEST_MESSAGE = 'Client sent an invalid request'
58
59
60 @app.errorhandler(BusinessException)
61 def handle_business_exception(e):
62     """An exception explicitly raised due to some business rule
63
64     """
65     error_log.error("Synchronous error for request id {} {}"
66                     .format(g.request_id, traceback.format_exc()))
67     err_msg = ERROR_TEMPLATE.render(description=str(e))
68     response = Response(err_msg, content_type='application/json; charset=utf-8')
69     response.status_code = 400
70     return response
71
72
73 @app.errorhandler(RequestException)
74 def handle_request_exception(e):
75     """Returns a detailed synchronous message to the calling client when osdf fails due to a remote call to another system
76
77     """
78     error_log.error("Synchronous error for request id {} {}".format(g.request_id, traceback.format_exc()))
79     err_msg = request_exception_to_json_body(e)
80     response = Response(err_msg, content_type='application/json; charset=utf-8')
81     response.status_code = 400
82     return response
83
84
85 @app.errorhandler(DataError)
86 def handle_data_error(e):
87     """Returns a detailed message to the calling client when the initial synchronous message is invalid
88
89     """
90     error_log.error("Synchronous error for request id {} {}".format(g.request_id, traceback.format_exc()))
91
92     body_dictionary = {
93         "serviceException": {
94             "text": BAD_CLIENT_REQUEST_MESSAGE,
95             "exceptionMessage": str(e.errors),
96             "errorType": "InvalidClientRequest"
97         }
98     }
99
100     body_as_json = json.dumps(body_dictionary)
101     response = Response(body_as_json, content_type='application/json; charset=utf-8')
102     response.status_code = 400
103     return response
104
105
106 @app.before_request
107 def log_request():
108     clear_mdc()
109     if request.content_type and 'json' in request.content_type:
110         populate_mdc(request)
111         g.request_id = get_request_id(request.get_json())
112         log_message(json.dumps(request.get_json()), "INPROGRESS", 'ENTRY')
113     else:
114         populate_default_mdc(request)
115         log_message('', "INPROGRESS", 'ENTRY')
116
117
118 @app.after_request
119 def log_response(response):
120     log_response_data(response)
121     return response
122
123
124 def log_response_data(response):
125     status_value = ''
126     try:
127         status_value = map_status_value(response)
128         log_message(response.get_data(as_text=True), status_value, 'EXIT')
129     except Exception:
130         try:
131             set_default_audit_mdc(request, status_value, 'EXIT')
132             audit_log.info(response.get_data(as_text=True))
133         except Exception:
134             set_error_details(300, 'Internal Error')
135             error_log.error("Error logging the response data due to {}".format(traceback.format_exc()))
136
137
138 def set_default_audit_mdc(request, status_value, p_marker):
139     MDC.put('partnerName', 'internal')
140     MDC.put('serviceName', request.path)
141     MDC.put('statusCode', status_value)
142     MDC.put('requestID', 'internal')
143     MDC.put('timer', int((time.process_time() - g.request_start) * 1000))
144     MDC.put('customField1', p_marker)
145
146
147 def log_message(message, status_value, p_marker='INVOKE'):
148     MDC.put('statusCode', status_value)
149     MDC.put('customField1', p_marker)
150     MDC.put('timer', int((time.process_time() - g.request_start) * 1000))
151     audit_log.info(message)
152
153
154 def map_status_value(response):
155     if 200 <= response.status_code < 300:
156         status_value = "COMPLETE"
157     else:
158         status_value = "ERROR"
159     return status_value
160
161
162 @app.errorhandler(500)
163 def internal_failure(error):
164     """Returned when unexpected coding errors occur during initial synchronous processing
165
166     """
167     error_log.error("Synchronous error for request id {} {}".format(g.request_id, traceback.format_exc()))
168     response = Response(internal_error_message, content_type='application/json; charset=utf-8')
169     response.status_code = 500
170     return response
171
172
173 def get_options(argv):
174     program_version_string = '%%prog %s' % "v1.0"
175     program_longdesc = ""
176     program_license = ""
177
178     parser = OptionParser(version=program_version_string, epilog=program_longdesc, description=program_license)
179     parser.add_option("-l", "--local", dest="local", help="run locally", action="store_true", default=False)
180     parser.add_option("-t", "--devtest", dest="devtest", help="run in dev/test environment", action="store_true",
181                       default=False)
182     parser.add_option("-d", "--debughost", dest="debughost",
183                       help="IP Address of host running debug server", default='')
184     parser.add_option("-p", "--debugport", dest="debugport",
185                       help="Port number of debug server", type=int, default=5678)
186     opts, args = parser.parse_args(argv)
187
188     if opts.debughost:
189         debug_log.debug('pydevd.settrace({}, port={})'.format(opts.debughost, opts.debugport))
190         # pydevd.settrace(opts.debughost, port=opts.debugport)
191     return opts
192
193
194 def build_ssl_context():
195     ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
196     ssl_context.set_ciphers('ECDHE-RSA-AES128-SHA256:EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH')
197     ssl_context.load_cert_chain(sys_conf['ssl_context'][0], sys_conf['ssl_context'][1])
198     return ssl_context
199
200
201 def run_app():
202     global sys_conf
203     sys_conf = osdf_config['core']['osdf_system']
204     ports = sys_conf['osdf_ports']
205     internal_port, external_port = ports['internal'], ports['external']
206     local_host = sys_conf['osdf_ip_default']
207     common_app_opts = dict(host=local_host, threaded=True, use_reloader=False)
208     ssl_opts = sys_conf.get('ssl_context')
209     if ssl_opts:
210         common_app_opts.update({'ssl_context': build_ssl_context()})
211     opts = get_options(sys.argv)
212     # Load secrets from SMS
213     sms.load_secrets()
214     if not opts.local and not opts.devtest:  # normal deployment
215         app.run(port=internal_port, debug=False, **common_app_opts)
216     else:
217         port = internal_port if opts.local else external_port
218         app.run(port=port, debug=True, **common_app_opts)
219
220
221 if __name__ == "__main__":
222     run_app()