Merge "Add onap log spec 1.2 for osdf"
[optf/osdf.git] / osdfapp.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 from threading import Thread  # for scaling up, may need celery with RabbitMQ or redis
30
31 import pydevd
32 import yaml
33 from flask import Flask, request, Response, g
34 from requests import RequestException
35 from schematics.exceptions import DataError
36
37 yaml.warnings({'YAMLLoadWarning': False})
38
39 import osdf.adapters.aaf.sms as sms
40 import osdf.operation.responses
41 from osdf.adapters.policy.interface import get_policies
42 from osdf.adapters.policy.interface import upload_policy_models
43 from osdf.config.base import osdf_config
44 from osdf.logging.osdf_logging import MH, audit_log, error_log, debug_log
45 from osdf.models.api.pciOptimizationRequest import PCIOptimizationAPI
46 from osdf.models.api.placementRequest import PlacementAPI
47 from osdf.operation.error_handling import request_exception_to_json_body, internal_error_message
48 from osdf.operation.exceptions import BusinessException
49 from osdf.operation.responses import osdf_response_for_request_accept as req_accept
50 from osdf.optimizers.pciopt.pci_opt_processor import process_pci_optimation
51 from osdf.optimizers.placementopt.conductor.remote_opt_processor import process_placement_opt
52 from osdf.optimizers.routeopt.simple_route_opt import RouteOpt
53 from osdf.utils import api_data_utils
54 from osdf.utils.mdc_utils import clear_mdc, mdc_from_json
55 from osdf.webapp.appcontroller import auth_basic
56
57 ERROR_TEMPLATE = osdf.ERROR_TEMPLATE
58
59 app = Flask(__name__)
60
61 BAD_CLIENT_REQUEST_MESSAGE = 'Client sent an invalid request'
62
63
64 @app.errorhandler(BusinessException)
65 def handle_business_exception(e):
66     """An exception explicitly raised due to some business rule"""
67     error_log.error("Synchronous error for request id {} {}".format(g.request_id, traceback.format_exc()))
68     err_msg = ERROR_TEMPLATE.render(description=str(e))
69     response = Response(err_msg, content_type='application/json; charset=utf-8')
70     response.status_code = 400
71     return response
72
73
74 @app.errorhandler(RequestException)
75 def handle_request_exception(e):
76     """Returns a detailed synchronous message to the calling client
77     when osdf fails due to a remote call to another system"""
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     error_log.error("Synchronous error for request id {} {}".format(g.request_id, traceback.format_exc()))
89
90     body_dictionary = {
91         "serviceException": {
92             "text": BAD_CLIENT_REQUEST_MESSAGE,
93             "exceptionMessage": str(e.errors),
94             "errorType": "InvalidClientRequest"
95         }
96     }
97
98     body_as_json = json.dumps(body_dictionary)
99     response = Response(body_as_json, content_type='application/json; charset=utf-8')
100     response.status_code = 400
101     return response
102
103
104 @app.before_request
105 def log_request():
106     g.request_start = time.clock()
107     request_json = request.get_json()
108     g.request_id = request_json['requestInfo']['requestId']
109     mdc_from_json(request_json)
110
111
112 @app.after_request
113 def log_response(response):
114     clear_mdc()
115     return response
116
117
118 @app.route("/api/oof/v1/healthcheck", methods=["GET"])
119 def do_osdf_health_check():
120     """Simple health check"""
121     audit_log.info("A health check request is processed!")
122     return "OK"
123
124
125 @app.route("/api/oof/loadmodels/v1", methods=["GET"])
126 def do_osdf_load_policies():
127     audit_log.info("Uploading policy models")
128     """Upload policy models"""
129     response = upload_policy_models()
130     audit_log.info(response)
131     return "OK"
132
133
134 @app.route("/api/oof/v1/placement", methods=["POST"])
135 @auth_basic.login_required
136 def do_placement_opt():
137     return placement_rest_api()
138
139
140 @app.route("/api/oof/placement/v1", methods=["POST"])
141 @auth_basic.login_required
142 def do_placement_opt_common_versioning():
143     return placement_rest_api()
144
145
146 def placement_rest_api():
147     """Perform placement optimization after validating the request and fetching policies
148     Make a call to the call-back URL with the output of the placement request.
149     Note: Call to Conductor for placement optimization may have redirects, so account for them
150     """
151     request_json = request.get_json()
152     req_id = request_json['requestInfo']['requestId']
153     g.request_id = req_id
154     audit_log.info(MH.received_request(request.url, request.remote_addr, json.dumps(request_json)))
155     api_version_info = api_data_utils.retrieve_version_info(request, req_id)
156     PlacementAPI(request_json).validate()
157     policies = get_policies(request_json, "placement")
158     audit_log.info(MH.new_worker_thread(req_id, "[for placement]"))
159     t = Thread(target=process_placement_opt, args=(request_json, policies, osdf_config))
160     t.start()
161     audit_log.info(MH.accepted_valid_request(req_id, request))
162     return req_accept(request_id=req_id,
163                       transaction_id=request_json['requestInfo']['transactionId'],
164                       version_info=api_version_info, request_status="accepted", status_message="")
165
166
167 @app.route("/api/oof/v1/route", methods=["POST"])
168 def do_route_calc():
169     """
170     Perform the basic route calculations and returnn the vpn-bindings
171     """
172     request_json = request.get_json()
173     audit_log.info("Calculate Route request received!")
174     return RouteOpt().getRoute(request_json)
175
176
177 @app.route("/api/oof/v1/pci", methods=["POST"])
178 @app.route("/api/oof/pci/v1", methods=["POST"])
179 @auth_basic.login_required
180 def do_pci_optimization():
181     request_json = request.get_json()
182     req_id = request_json['requestInfo']['requestId']
183     g.request_id = req_id
184     audit_log.info(MH.received_request(request.url, request.remote_addr, json.dumps(request_json)))
185     PCIOptimizationAPI(request_json).validate()
186     # disable policy retrieval
187     # policies = get_policies(request_json, "pciopt")
188     audit_log.info(MH.new_worker_thread(req_id, "[for pciopt]"))
189     t = Thread(target=process_pci_optimation, args=(request_json, osdf_config, None))
190     t.start()
191     audit_log.info(MH.accepted_valid_request(req_id, request))
192     return req_accept(request_id=req_id,
193                       transaction_id=request_json['requestInfo']['transactionId'],
194                       request_status="accepted", status_message="")
195
196
197 @app.errorhandler(500)
198 def internal_failure(error):
199     """Returned when unexpected coding errors occur during initial synchronous processing"""
200     error_log.error("Synchronous error for request id {} {}".format(g.request_id, traceback.format_exc()))
201     response = Response(internal_error_message, content_type='application/json; charset=utf-8')
202     response.status_code = 500
203     return response
204
205
206 def get_options(argv):
207     program_version_string = '%%prog %s' % "v1.0"
208     program_longdesc = ""
209     program_license = ""
210
211     parser = OptionParser(version=program_version_string, epilog=program_longdesc, description=program_license)
212     parser.add_option("-l", "--local", dest="local", help="run locally", action="store_true", default=False)
213     parser.add_option("-t", "--devtest", dest="devtest", help="run in dev/test environment", action="store_true",
214                       default=False)
215     parser.add_option("-d", "--debughost", dest="debughost", help="IP Address of host running debug server", default='')
216     parser.add_option("-p", "--debugport", dest="debugport", help="Port number of debug server", type=int, default=5678)
217     opts, args = parser.parse_args(argv)
218
219     if opts.debughost:
220         debug_log.debug('pydevd.settrace({}, port={})'.format(opts.debughost, opts.debugport))
221         pydevd.settrace(opts.debughost, port=opts.debugport)
222     return opts
223
224
225 def build_ssl_context():
226     ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
227     ssl_context.set_ciphers('ECDHE-RSA-AES128-SHA256:EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH')
228     ssl_context.load_cert_chain(sys_conf['ssl_context'][0], sys_conf['ssl_context'][1])
229     return ssl_context
230
231
232 if __name__ == "__main__":
233
234     sys_conf = osdf_config['core']['osdf_system']
235     ports = sys_conf['osdf_ports']
236     internal_port, external_port = ports['internal'], ports['external']
237
238     local_host = sys_conf['osdf_ip_default']
239     common_app_opts = dict(host=local_host, threaded=True, use_reloader=False)
240
241     ssl_opts = sys_conf.get('ssl_context')
242     if ssl_opts:
243         common_app_opts.update({'ssl_context': build_ssl_context()})
244
245     opts = get_options(sys.argv)
246     # Load secrets from SMS
247     sms.load_secrets()
248     if not opts.local and not opts.devtest:  # normal deployment
249         app.run(port=internal_port, debug=False, **common_app_opts)
250     else:
251         port = internal_port if opts.local else external_port
252         app.run(port=port, debug=True, **common_app_opts)