1 # ================================================================================
2 # Copyright (c) 2019-2020 AT&T Intellectual Property. All rights reserved.
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
8 # http://www.apache.org/licenses/LICENSE-2.0
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 # ============LICENSE_END=========================================================
17 """web-service for oti_handler"""
23 from datetime import datetime
27 from otihandler.cbs_rest import CBSRest
28 from otihandler.config import Config
29 from otihandler.dti_processor import DTIProcessor
30 from otihandler.onap.audit import Audit
34 """run REST API of OTI Handler"""
36 logger = logging.getLogger("oti_handler.web_server")
37 HOST_INADDR_ANY = ".".join("0"*4)
40 def run_forever(audit):
41 """run the web-server of OTI Handler forever"""
43 cherrypy.config.update({"server.socket_host": DTIWeb.HOST_INADDR_ANY,
44 "server.socket_port": Config.wservice_port})
48 if Config.tls_server_cert_file and Config.tls_private_key_file:
49 tm_cert = os.path.getmtime(Config.tls_server_cert_file)
50 tm_key = os.path.getmtime(Config.tls_private_key_file)
51 #cherrypy.server.ssl_module = 'builtin'
52 cherrypy.server.ssl_module = 'pyOpenSSL'
53 cherrypy.server.ssl_certificate = Config.tls_server_cert_file
54 cherrypy.server.ssl_private_key = Config.tls_private_key_file
55 if Config.tls_server_ca_chain_file:
56 cherrypy.server.ssl_certificate_chain = Config.tls_server_ca_chain_file
58 tls_info = "cert: {} {} {}".format(Config.tls_server_cert_file,
59 Config.tls_private_key_file,
60 Config.tls_server_ca_chain_file)
62 cherrypy.tree.mount(_DTIWeb(), '/')
65 "%s with config: %s", audit.info("running oti_handler as {}://{}:{} {}".format(
66 protocol, cherrypy.server.socket_host, cherrypy.server.socket_port, tls_info)),
67 json.dumps(cherrypy.config))
68 cherrypy.engine.start()
70 # If HTTPS server certificate changes, exit to let kubernetes restart us
71 if Config.tls_server_cert_file and Config.tls_private_key_file:
74 c_tm_cert = os.path.getmtime(Config.tls_server_cert_file)
75 c_tm_key = os.path.getmtime(Config.tls_private_key_file)
76 if c_tm_cert > tm_cert or c_tm_key > tm_key:
77 DTIWeb.logger.info("cert or key file updated")
78 cherrypy.engine.stop()
79 cherrypy.engine.exit()
83 class _DTIWeb(object):
84 """REST API of DTI Handler"""
86 VALID_EVENT_TYPES = ['deploy', 'undeploy', 'add', 'delete', 'update', 'notify']
89 def _get_request_info(request):
90 """Returns info about the http request."""
92 return "{} {}{}".format(request.method, request.script_name, request.path_info)
95 #----- Common endpoint methods
98 @cherrypy.tools.json_out()
99 def healthcheck(self):
100 """Returns healthcheck results."""
102 req_info = _DTIWeb._get_request_info(cherrypy.request)
103 audit = Audit(req_message=req_info, headers=cherrypy.request.headers)
105 DTIWeb.logger.info("%s", req_info)
107 result = Audit.health()
109 DTIWeb.logger.info("healthcheck %s: result=%s", req_info, json.dumps(result))
111 audit.audit_done(result=json.dumps(result))
116 """Shutdown the web server."""
118 req_info = _DTIWeb._get_request_info(cherrypy.request)
119 audit = Audit(req_message=req_info, headers=cherrypy.request.headers)
121 DTIWeb.logger.info("%s: --- stopping REST API of DTI Handler ---", req_info)
123 cherrypy.engine.exit()
125 health = json.dumps(Audit.health())
126 audit.info("oti_handler health: {}".format(health))
127 DTIWeb.logger.info("oti_handler health: %s", health)
128 DTIWeb.logger.info("%s: --------- the end -----------", req_info)
129 result = str(datetime.now())
130 audit.info_requested(result)
131 return "goodbye! shutdown requested {}".format(result)
133 # ----- DTI Handler mock endpoint methods
135 @cherrypy.tools.json_out()
136 @cherrypy.tools.json_in()
137 def mockevents(self):
139 result = {"KubeNamespace":"com-my-dcae-test", "KubePod":"pod-0", "KubeServiceName":"pod-0.service.local", "KubeServicePort":"8880", "KubeClusterFqdn":"fqdn-1"}
143 #----- DTI Handler endpoint methods
146 @cherrypy.tools.json_out()
147 @cherrypy.tools.json_in()
148 def events(self, notify="y"):
150 Run dti reconfig script in service component instances configured to accept the DTI Event.
152 POST /events < <dcae_event>
154 POST /events?ndtify="n" < <dcae_event>
156 where <dcae_event> is the entire DTI Event passed as a JSON object and contains at least these top-level keys:
157 dcae_service_action : string
158 required, 'deploy' or 'undeploy'
159 dcae_target_name : string
160 required, VNF Instance ID
161 dcae_target_type : string
162 required, VNF Type of the VNF Instance
163 dcae_service_location : string
164 optional, CLLI location. Not provided or '' infers all locations.
169 optional, default "y", any of these will not notify components: [ "f", "false", "False", "FALSE", "n", "no" ]
170 When "n" will **not** notify components of this DTI Event update to Consul.
175 JSON object containing success or error of executing the dti reconfig script on
176 each component instance's docker container, keyed by service_component_name.
180 if cherrypy.request.method != "POST":
181 raise cherrypy.HTTPError(404, "unexpected method {}".format(cherrypy.request.method))
183 dti_event = cherrypy.request.json or {}
184 str_dti_event = json.dumps(dti_event)
186 req_info = _DTIWeb._get_request_info(cherrypy.request)
187 audit = Audit(req_message="{}: {}".format(req_info, str_dti_event), \
188 headers=cherrypy.request.headers)
189 DTIWeb.logger.info("%s: dti_event=%s headers=%s", \
190 req_info, str_dti_event, json.dumps(cherrypy.request.headers))
192 dcae_service_action = dti_event.get('dcae_service_action')
193 if not dcae_service_action:
194 msg = 'dcae_service_action is missing'
195 DTIWeb.logger.error(msg)
196 raise cherrypy.HTTPError(400, msg)
197 elif dcae_service_action.lower() not in self.VALID_EVENT_TYPES:
198 msg = 'dcae_service_action is invalid'
199 DTIWeb.logger.error(msg)
200 raise cherrypy.HTTPError(400,msg)
202 dcae_target_name = dti_event.get('dcae_target_name')
203 if not dcae_target_name:
204 msg = 'dcae_target_name is missing'
205 DTIWeb.logger.error(msg)
206 raise cherrypy.HTTPError(400, msg)
208 dcae_target_type = dti_event.get('dcae_target_type', '')
209 if not dcae_target_type:
210 msg = 'dcae_target_type is missing'
211 DTIWeb.logger.error(msg)
212 raise cherrypy.HTTPError(400, msg)
214 send_notification = True
215 if (isinstance(notify, bool) and not notify) or \
216 (isinstance(notify, str) and notify.lower() in [ "f", "false", "n", "no" ]):
217 send_notification = False
219 prc = DTIProcessor(dti_event, send_notification=send_notification)
220 result = prc.get_result()
222 DTIWeb.logger.info("%s: dti_event=%s result=%s", \
223 req_info, str_dti_event, json.dumps(result))
225 success, http_status_code, _ = audit.audit_done(result=json.dumps(result))
227 cherrypy.response.status = http_status_code
231 def get_docker_events(self, request, service, location):
233 common routine for dti_docker_events and oti_docker_events
235 :param request: HTTP GET request
236 :param service: HTTP request query parameter for service name
237 :param location: HTTP request query parameter for location CLLI
241 if request.method != "GET":
242 raise cherrypy.HTTPError(404, "unexpected method {}".format(request.method))
244 req_info = _DTIWeb._get_request_info(request)
245 audit = Audit(req_message=req_info, headers=request.headers)
247 return DTIProcessor.get_docker_raw_events(service, location)
249 def get_k8s_events(self, request, **params):
251 common routine for dti_k8s_events and oti_k8s_events
253 :param request: HTTP GET request
254 :param params: HTTP request query parameters
257 if request.method != "GET":
258 raise cherrypy.HTTPError(404, "unexpected method {}".format(request.method))
260 req_info = _DTIWeb._get_request_info(request)
261 audit = Audit(req_message=req_info, headers=request.headers)
263 pod = request.params['pod']
264 namespace = request.params['namespace']
265 cluster = request.params['cluster']
267 return DTIProcessor.get_k8_raw_events(pod, cluster, namespace)
270 @cherrypy.tools.json_out()
271 def oti_k8s_events(self, **params):
273 Retrieve raw JSON events from application events database
275 GET /oti_k8_events?pod=<sts-1>&namespace=<ns1>&cluster=<cluster1>
280 POD ID of the stateful set POD
284 kubernetes cluster FQDN
289 JSON object containing the fully-bound configuration.
293 return self.get_k8s_events(cherrypy.request, params)
296 @cherrypy.tools.json_out()
297 def dti_k8s_events(self, **params):
299 Retrieve raw JSON events from application events database
301 GET /dti_k8_events?pod=<sts-1>&namespace=<ns1>&cluster=<cluster1>
306 POD ID of the stateful set POD
310 kubernetes cluster FQDN
315 JSON object containing the fully-bound configuration.
319 return self.get_k8s_events(cherrypy.request, params)
322 @cherrypy.tools.json_out()
323 def oti_docker_events(self, service, location=None):
325 Retrieve raw JSON events from application events database related to docker deployments
327 GET /oti_docker_events?service=<svc>&location=<location>
332 The service component name assigned by dockerplugin to the component
333 that is unique to the cloudify node instance and used in its Consul key(s).
335 optional. allows multiple values separated by commas. Filters DTI events with dcae_service_location in service_location.
336 If service_location is not provided, then defaults to dockerhost or k8s cluster master node service Consul TAGs if service_name is provided,
337 otherwise results are not location filtered.
342 JSON object containing the fully-bound configuration.
346 return self.get_docker_events(cherrypy.request, service, location)
349 @cherrypy.tools.json_out()
350 def dti_docker_events(self, service, location=None):
352 Retrieve raw JSON events from application events database related to docker deployments
354 GET /dti_docker_events?service=<svc>&location=<location>
359 The service component name assigned by dockerplugin to the component
360 that is unique to the cloudify node instance and used in its Consul key(s).
362 optional. allows multiple values separated by commas. Filters DTI events with dcae_service_location in service_location.
363 If service_location is not provided, then defaults to dockerhost or k8s cluster master node service Consul TAGs if service_name is provided,
364 otherwise results are not location filtered.
369 JSON object containing the fully-bound configuration.
373 return self.get_docker_events(cherrypy.request, service, location)
375 #----- Config Binding Service (CBS) endpoint methods
378 @cherrypy.popargs('service_name')
379 @cherrypy.tools.json_out()
380 def service_component(self, service_name):
382 Retrieve fully-bound configuration for service_name from Consul KVs.
384 GET /service_component/<service_name>
388 service_name : string
389 The service component name assigned by dockerplugin to the component
390 that is unique to the cloudify node instance and used in its Consul key(s).
395 JSON object containing the fully-bound configuration.
399 if cherrypy.request.method != "GET":
400 raise cherrypy.HTTPError(404, "unexpected method {}".format(cherrypy.request.method))
402 req_info = _DTIWeb._get_request_info(cherrypy.request)
403 audit = Audit(req_message=req_info, headers=cherrypy.request.headers)
404 DTIWeb.logger.info("%s: service_name=%s headers=%s", \
405 req_info, service_name, json.dumps(cherrypy.request.headers))
408 result = CBSRest.get_service_component(service_name)
409 except Exception as e:
410 result = {"ERROR": "exception {}: {!s}".format(type(e).__name__, e)}
411 audit.set_http_status_code(404)
413 DTIWeb.logger.info("%s: service_name=%s result=%s", \
414 req_info, service_name, json.dumps(result))
416 success, http_status_code, _ = audit.audit_done(result=json.dumps(result))
418 cherrypy.response.status = http_status_code
423 @cherrypy.popargs('service_name')
424 @cherrypy.tools.json_out()
425 def service_component_all(self, service_name, service_location=None, policy_ids="y"):
427 Retrieve all information for service_name (config, dti, dti_events, and policies) from Consul KVs.
429 GET /service_component_all/<service_name>
431 GET /service_component_all/<service_name>?service_location=<service_location>
433 GET /service_component_all/<service_name>?service_location=<service_location>;policy_ids=n
437 service_name : string
438 The service component name assigned by dockerplugin to the component
439 that is unique to the cloudify node instance and used in its Consul key(s).
440 service_location : string
441 optional, allows multiple values separated by commas.
442 Filters DTI events with dcae_service_location in service_location.
444 optional, default "y", any of these will unset: [ "f", "false", "False", "FALSE", "n", "no" ]
445 When unset, formats policies items as a list (without policy_ids) rather than as an object indexed by policy_id.
450 JSON object containing all information for component service_name.
451 The top-level keys may include the following:
453 The cloudify node's application_config property from when the start workflow was executed.
455 Keys are VNF Types that the component currently is assigned to monitor. Policy can change them.
457 The latest deploy DTI events, keyed by VNF Type and sub-keyed by VNF Instance ID.
460 Contains information about when the policies folder was last written.
462 Contains all policy bodies for the service_name component, keyed by policy_id.
466 if cherrypy.request.method != "GET":
467 raise cherrypy.HTTPError(404, "unexpected method {}".format(cherrypy.request.method))
469 req_info = _DTIWeb._get_request_info(cherrypy.request)
470 audit = Audit(req_message=req_info, headers=cherrypy.request.headers)
471 DTIWeb.logger.info("%s: service_name=%s headers=%s", \
472 req_info, service_name, json.dumps(cherrypy.request.headers))
474 policies_as_list = False
475 if (isinstance(policy_ids, bool) and not policy_ids) or \
476 (isinstance(policy_ids, str) and policy_ids.lower() in [ "f", "false", "n", "no" ]):
477 policies_as_list = True
479 result = CBSRest.get_service_component_all(service_name, service_location=service_location, policies_as_list=policies_as_list)
480 except Exception as e:
481 result = {"ERROR": "exception {}: {!s}".format(type(e).__name__, e)}
482 audit.set_http_status_code(404)
484 DTIWeb.logger.info("%s: service_name=%s result=%s", \
485 req_info, service_name, json.dumps(result))
487 success, http_status_code, _ = audit.audit_done(result=json.dumps(result))
489 cherrypy.response.status = http_status_code
494 @cherrypy.popargs('service_name')
495 @cherrypy.tools.json_out()
496 def dti(self, service_name=None, vnf_type=None, vnf_id=None, service_location=None):
498 Retrieve current (latest, not undeployed) DTI events from Consul KVs.
500 GET /dti/<service_name>
502 GET /dti/<service_name>?vnf_type=<vnf_type>;vnf_id=<vnf_id>;service_location=<service_location>
506 GET /dti?vnf_type=<vnf_type>;vnf_id=<vnf_id>;service_location=<service_location>
510 service_name : string
511 optional. The service component name assigned by dockerplugin to the component
512 that is unique to the cloudify node instance and used in its Consul key(s).
514 optional, allows multiple values separated by commas. Gets DTI events for these vnf_type(s).
516 optional. Requires vnf_type also. Gets DTI event for this vnf_id.
517 service_location : string
518 optional, allows multiple values separated by commas.
519 Filters DTI events with dcae_service_location in service_location.
524 Dictionary of DTI event(s).
525 If one vnf_type and vnf_id are both specified, then object returned will be just the one DTI event.
526 If one vnf_type is specified but not vnf_id, then DTI events will be keyed by vnf_id.
527 Otherwise the DTI events will be keyed by vnf_type, sub-keyed by vnf_id.
531 if cherrypy.request.method != "GET":
532 raise cherrypy.HTTPError(404, "unexpected method {}".format(cherrypy.request.method))
534 req_info = _DTIWeb._get_request_info(cherrypy.request)
535 audit = Audit(req_message=req_info, headers=cherrypy.request.headers)
536 DTIWeb.logger.info("%s: service_name=%s headers=%s", \
537 req_info, service_name, json.dumps(cherrypy.request.headers))
540 result = CBSRest.get_dti(service_name=service_name, vnf_type=vnf_type, vnf_id=vnf_id, service_location=service_location)
541 except Exception as e:
542 result = {"ERROR": "exception {}: {!s}".format(type(e).__name__, e)}
543 audit.set_http_status_code(404)
545 DTIWeb.logger.info("%s: service_name=%s result=%s", \
546 req_info, service_name, json.dumps(result))
548 success, http_status_code, _ = audit.audit_done(result=json.dumps(result))
550 cherrypy.response.status = http_status_code
555 @cherrypy.popargs('service_name')
556 @cherrypy.tools.json_out()
557 def policies(self, service_name, policy_id=None):
559 Retrieve policies for service_name from Consul KVs.
561 GET /policies/<service_name>
563 GET /policies/<service_name>?policy_id=<policy_id>
567 service_name : string
568 The service component name assigned by dockerplugin to the component
569 that is unique to the cloudify node instance and used in its Consul key(s).
571 optional. Limits returned policy to this policy_id.
576 JSON object containing policy bodies for the service_name component.
577 If policy_id is specified, then object returned will be just the one policy body.
578 If policy_id is not specified, then object will contain all policy bodies, keyed by policy_id.
582 if cherrypy.request.method != "GET":
583 raise cherrypy.HTTPError(404, "unexpected method {}".format(cherrypy.request.method))
585 req_info = _DTIWeb._get_request_info(cherrypy.request)
586 audit = Audit(req_message=req_info, headers=cherrypy.request.headers)
587 DTIWeb.logger.info("%s: service_name=%s headers=%s", \
588 req_info, service_name, json.dumps(cherrypy.request.headers))
591 result = CBSRest.get_policies(service_name, policy_id=policy_id)
592 except Exception as e:
593 result = {"ERROR": "exception {}: {!s}".format(type(e).__name__, e)}
594 audit.set_http_status_code(404)
596 DTIWeb.logger.info("%s: service_name=%s result=%s", \
597 req_info, service_name, json.dumps(result))
599 success, http_status_code, _ = audit.audit_done(result=json.dumps(result))
601 cherrypy.response.status = http_status_code