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))
185 dti_event = cherrypy.request.json or {}
186 str_dti_event = json.dumps(dti_event)
188 req_info = _DTIWeb._get_request_info(cherrypy.request)
189 audit = Audit(req_message="{}: {}".format(req_info, str_dti_event), \
190 headers=cherrypy.request.headers)
191 DTIWeb.logger.info("%s: dti_event=%s headers=%s", \
192 req_info, str_dti_event, json.dumps(cherrypy.request.headers))
194 dcae_service_action = dti_event.get('dcae_service_action')
195 if not dcae_service_action:
196 msg = 'dcae_service_action is missing'
197 elif dcae_service_action.lower() not in self.VALID_EVENT_TYPES:
198 msg = 'dcae_service_action is invalid'
200 dcae_target_name = dti_event.get('dcae_target_name')
201 if not msg and not dcae_target_name:
202 msg = 'dcae_target_name is missing'
204 dcae_target_type = dti_event.get('dcae_target_type', '')
205 if not msg and not dcae_target_type:
206 msg = 'dcae_target_type is missing'
209 result = {"ERROR": msg}
211 DTIWeb.logger.error("%s: dti_event=%s result=%s", \
212 req_info, str_dti_event, json.dumps(result))
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 = "400 Bad Request"
229 cherrypy.response.status = http_status_code
233 def get_docker_events(self, request, service, location):
235 common routine for dti_docker_events and oti_docker_events
237 :param request: HTTP GET request
238 :param service: HTTP request query parameter for service name
239 :param location: HTTP request query parameter for location CLLI
243 if request.method != "GET":
244 raise cherrypy.HTTPError(404, "unexpected method {}".format(request.method))
246 req_info = _DTIWeb._get_request_info(request)
247 audit = Audit(req_message=req_info, headers=request.headers)
249 return DTIProcessor.get_docker_raw_events(service, location)
251 def get_k8s_events(self, request, **params):
253 common routine for dti_k8s_events and oti_k8s_events
255 :param request: HTTP GET request
256 :param params: HTTP request query parameters
259 if request.method != "GET":
260 raise cherrypy.HTTPError(404, "unexpected method {}".format(request.method))
262 req_info = _DTIWeb._get_request_info(request)
263 audit = Audit(req_message=req_info, headers=request.headers)
265 pod = request.params['pod']
266 namespace = request.params['namespace']
267 cluster = request.params['cluster']
269 return DTIProcessor.get_k8_raw_events(pod, cluster, namespace)
272 @cherrypy.tools.json_out()
273 def oti_k8s_events(self, **params):
275 Retrieve raw JSON events from application events database
277 GET /oti_k8_events?pod=<sts-1>&namespace=<ns1>&cluster=<cluster1>
282 POD ID of the stateful set POD
286 kubernetes cluster FQDN
291 JSON object containing the fully-bound configuration.
295 return self.get_k8s_events(cherrypy.request, params)
298 @cherrypy.tools.json_out()
299 def dti_k8s_events(self, **params):
301 Retrieve raw JSON events from application events database
303 GET /dti_k8_events?pod=<sts-1>&namespace=<ns1>&cluster=<cluster1>
308 POD ID of the stateful set POD
312 kubernetes cluster FQDN
317 JSON object containing the fully-bound configuration.
321 return self.get_k8s_events(cherrypy.request, params)
324 @cherrypy.tools.json_out()
325 def oti_docker_events(self, service, location=None):
327 Retrieve raw JSON events from application events database related to docker deployments
329 GET /oti_docker_events?service=<svc>&location=<location>
334 The service component name assigned by dockerplugin to the component
335 that is unique to the cloudify node instance and used in its Consul key(s).
337 optional. allows multiple values separated by commas. Filters DTI events with dcae_service_location in service_location.
338 If service_location is not provided, then defaults to dockerhost or k8s cluster master node service Consul TAGs if service_name is provided,
339 otherwise results are not location filtered.
344 JSON object containing the fully-bound configuration.
348 return self.get_docker_events(cherrypy.request, service, location)
351 @cherrypy.tools.json_out()
352 def dti_docker_events(self, service, location=None):
354 Retrieve raw JSON events from application events database related to docker deployments
356 GET /dti_docker_events?service=<svc>&location=<location>
361 The service component name assigned by dockerplugin to the component
362 that is unique to the cloudify node instance and used in its Consul key(s).
364 optional. allows multiple values separated by commas. Filters DTI events with dcae_service_location in service_location.
365 If service_location is not provided, then defaults to dockerhost or k8s cluster master node service Consul TAGs if service_name is provided,
366 otherwise results are not location filtered.
371 JSON object containing the fully-bound configuration.
375 return self.get_docker_events(cherrypy.request, service, location)
377 #----- Config Binding Service (CBS) endpoint methods
380 @cherrypy.popargs('service_name')
381 @cherrypy.tools.json_out()
382 def service_component(self, service_name):
384 Retrieve fully-bound configuration for service_name from Consul KVs.
386 GET /service_component/<service_name>
390 service_name : string
391 The service component name assigned by dockerplugin to the component
392 that is unique to the cloudify node instance and used in its Consul key(s).
397 JSON object containing the fully-bound configuration.
401 if cherrypy.request.method != "GET":
402 raise cherrypy.HTTPError(404, "unexpected method {}".format(cherrypy.request.method))
404 req_info = _DTIWeb._get_request_info(cherrypy.request)
405 audit = Audit(req_message=req_info, headers=cherrypy.request.headers)
406 DTIWeb.logger.info("%s: service_name=%s headers=%s", \
407 req_info, service_name, json.dumps(cherrypy.request.headers))
410 result = CBSRest.get_service_component(service_name)
411 except Exception as e:
412 result = {"ERROR": "exception {}: {!s}".format(type(e).__name__, e)}
413 audit.set_http_status_code(404)
415 DTIWeb.logger.info("%s: service_name=%s result=%s", \
416 req_info, service_name, json.dumps(result))
418 success, http_status_code, _ = audit.audit_done(result=json.dumps(result))
420 cherrypy.response.status = http_status_code
425 @cherrypy.popargs('service_name')
426 @cherrypy.tools.json_out()
427 def service_component_all(self, service_name, service_location=None, policy_ids="y"):
429 Retrieve all information for service_name (config, dti, dti_events, and policies) from Consul KVs.
431 GET /service_component_all/<service_name>
433 GET /service_component_all/<service_name>?service_location=<service_location>
435 GET /service_component_all/<service_name>?service_location=<service_location>;policy_ids=n
439 service_name : string
440 The service component name assigned by dockerplugin to the component
441 that is unique to the cloudify node instance and used in its Consul key(s).
442 service_location : string
443 optional, allows multiple values separated by commas.
444 Filters DTI events with dcae_service_location in service_location.
446 optional, default "y", any of these will unset: [ "f", "false", "False", "FALSE", "n", "no" ]
447 When unset, formats policies items as a list (without policy_ids) rather than as an object indexed by policy_id.
452 JSON object containing all information for component service_name.
453 The top-level keys may include the following:
455 The cloudify node's application_config property from when the start workflow was executed.
457 Keys are VNF Types that the component currently is assigned to monitor. Policy can change them.
459 The latest deploy DTI events, keyed by VNF Type and sub-keyed by VNF Instance ID.
462 Contains information about when the policies folder was last written.
464 Contains all policy bodies for the service_name component, keyed by policy_id.
468 if cherrypy.request.method != "GET":
469 raise cherrypy.HTTPError(404, "unexpected method {}".format(cherrypy.request.method))
471 req_info = _DTIWeb._get_request_info(cherrypy.request)
472 audit = Audit(req_message=req_info, headers=cherrypy.request.headers)
473 DTIWeb.logger.info("%s: service_name=%s headers=%s", \
474 req_info, service_name, json.dumps(cherrypy.request.headers))
476 policies_as_list = False
477 if (isinstance(policy_ids, bool) and not policy_ids) or \
478 (isinstance(policy_ids, str) and policy_ids.lower() in [ "f", "false", "n", "no" ]):
479 policies_as_list = True
481 result = CBSRest.get_service_component_all(service_name, service_location=service_location, policies_as_list=policies_as_list)
482 except Exception as e:
483 result = {"ERROR": "exception {}: {!s}".format(type(e).__name__, e)}
484 audit.set_http_status_code(404)
486 DTIWeb.logger.info("%s: service_name=%s result=%s", \
487 req_info, service_name, json.dumps(result))
489 success, http_status_code, _ = audit.audit_done(result=json.dumps(result))
491 cherrypy.response.status = http_status_code
496 @cherrypy.popargs('service_name')
497 @cherrypy.tools.json_out()
498 def dti(self, service_name=None, vnf_type=None, vnf_id=None, service_location=None):
500 Retrieve current (latest, not undeployed) DTI events from Consul KVs.
502 GET /dti/<service_name>
504 GET /dti/<service_name>?vnf_type=<vnf_type>;vnf_id=<vnf_id>;service_location=<service_location>
508 GET /dti?vnf_type=<vnf_type>;vnf_id=<vnf_id>;service_location=<service_location>
512 service_name : string
513 optional. The service component name assigned by dockerplugin to the component
514 that is unique to the cloudify node instance and used in its Consul key(s).
516 optional, allows multiple values separated by commas. Gets DTI events for these vnf_type(s).
518 optional. Requires vnf_type also. Gets DTI event for this vnf_id.
519 service_location : string
520 optional, allows multiple values separated by commas.
521 Filters DTI events with dcae_service_location in service_location.
526 Dictionary of DTI event(s).
527 If one vnf_type and vnf_id are both specified, then object returned will be just the one DTI event.
528 If one vnf_type is specified but not vnf_id, then DTI events will be keyed by vnf_id.
529 Otherwise the DTI events will be keyed by vnf_type, sub-keyed by vnf_id.
533 if cherrypy.request.method != "GET":
534 raise cherrypy.HTTPError(404, "unexpected method {}".format(cherrypy.request.method))
536 req_info = _DTIWeb._get_request_info(cherrypy.request)
537 audit = Audit(req_message=req_info, headers=cherrypy.request.headers)
538 DTIWeb.logger.info("%s: service_name=%s headers=%s", \
539 req_info, service_name, json.dumps(cherrypy.request.headers))
542 result = CBSRest.get_oti(service_name=service_name, vnf_type=vnf_type, vnf_id=vnf_id, service_location=service_location)
543 except Exception as e:
544 result = {"ERROR": "exception {}: {!s}".format(type(e).__name__, e)}
545 audit.set_http_status_code(404)
547 DTIWeb.logger.info("%s: service_name=%s result=%s", \
548 req_info, service_name, json.dumps(result))
550 success, http_status_code, _ = audit.audit_done(result=json.dumps(result))
552 cherrypy.response.status = http_status_code
557 @cherrypy.popargs('service_name')
558 @cherrypy.tools.json_out()
559 def policies(self, service_name, policy_id=None):
561 Retrieve policies for service_name from Consul KVs.
563 GET /policies/<service_name>
565 GET /policies/<service_name>?policy_id=<policy_id>
569 service_name : string
570 The service component name assigned by dockerplugin to the component
571 that is unique to the cloudify node instance and used in its Consul key(s).
573 optional. Limits returned policy to this policy_id.
578 JSON object containing policy bodies for the service_name component.
579 If policy_id is specified, then object returned will be just the one policy body.
580 If policy_id is not specified, then object will contain all policy bodies, keyed by policy_id.
584 if cherrypy.request.method != "GET":
585 raise cherrypy.HTTPError(404, "unexpected method {}".format(cherrypy.request.method))
587 req_info = _DTIWeb._get_request_info(cherrypy.request)
588 audit = Audit(req_message=req_info, headers=cherrypy.request.headers)
589 DTIWeb.logger.info("%s: service_name=%s headers=%s", \
590 req_info, service_name, json.dumps(cherrypy.request.headers))
593 result = CBSRest.get_policies(service_name, policy_id=policy_id)
594 except Exception as e:
595 result = {"ERROR": "exception {}: {!s}".format(type(e).__name__, e)}
596 audit.set_http_status_code(404)
598 DTIWeb.logger.info("%s: service_name=%s result=%s", \
599 req_info, service_name, json.dumps(result))
601 success, http_status_code, _ = audit.audit_done(result=json.dumps(result))
603 cherrypy.response.status = http_status_code