45c407f18c6562c1d56687129d9ea7d1924df842
[dcaegen2/platform.git] / oti / event-handler / otihandler / web_server.py
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
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 # ============LICENSE_END=========================================================
16
17 """web-service for oti_handler"""
18
19 import json
20 import logging
21 import os
22 import time
23 from datetime import datetime
24
25 import cherrypy
26
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
31
32
33 class DTIWeb(object):
34     """run REST API of OTI Handler"""
35
36     logger = logging.getLogger("oti_handler.web_server")
37     HOST_INADDR_ANY = ".".join("0"*4)
38
39     @staticmethod
40     def run_forever(audit):
41         """run the web-server of OTI Handler forever"""
42
43         cherrypy.config.update({"server.socket_host": DTIWeb.HOST_INADDR_ANY,
44                                 "server.socket_port": Config.wservice_port})
45
46         protocol = "http"
47         tls_info = ""
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
57             protocol = "https"
58             tls_info = "cert: {} {} {}".format(Config.tls_server_cert_file,
59                                                Config.tls_private_key_file,
60                                                Config.tls_server_ca_chain_file)
61
62         cherrypy.tree.mount(_DTIWeb(), '/')
63
64         DTIWeb.logger.info(
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()
69
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:
72             while True:
73                 time.sleep(600)
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()
80                     break
81
82
83 class _DTIWeb(object):
84     """REST API of DTI Handler"""
85
86     VALID_EVENT_TYPES = ['deploy', 'undeploy', 'add', 'delete', 'update', 'notify']
87
88     @staticmethod
89     def _get_request_info(request):
90         """Returns info about the http request."""
91
92         return "{} {}{}".format(request.method, request.script_name, request.path_info)
93
94
95     #----- Common endpoint methods
96
97     @cherrypy.expose
98     @cherrypy.tools.json_out()
99     def healthcheck(self):
100         """Returns healthcheck results."""
101
102         req_info = _DTIWeb._get_request_info(cherrypy.request)
103         audit = Audit(req_message=req_info, headers=cherrypy.request.headers)
104
105         DTIWeb.logger.info("%s", req_info)
106
107         result = Audit.health()
108
109         DTIWeb.logger.info("healthcheck %s: result=%s", req_info, json.dumps(result))
110
111         audit.audit_done(result=json.dumps(result))
112         return result
113
114     @cherrypy.expose
115     def shutdown(self):
116         """Shutdown the web server."""
117
118         req_info = _DTIWeb._get_request_info(cherrypy.request)
119         audit = Audit(req_message=req_info, headers=cherrypy.request.headers)
120
121         DTIWeb.logger.info("%s: --- stopping REST API of DTI Handler ---", req_info)
122
123         cherrypy.engine.exit()
124
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)
132
133     # ----- DTI Handler mock endpoint methods
134     @cherrypy.expose
135     @cherrypy.tools.json_out()
136     @cherrypy.tools.json_in()
137     def mockevents(self):
138
139         result = {"KubeNamespace":"com-my-dcae-test", "KubePod":"pod-0", "KubeServiceName":"pod-0.service.local", "KubeServicePort":"8880", "KubeClusterFqdn":"fqdn-1"}
140
141         return result
142
143     #----- DTI Handler endpoint methods
144
145     @cherrypy.expose
146     @cherrypy.tools.json_out()
147     @cherrypy.tools.json_in()
148     def events(self, notify="y"):
149         """
150         Run dti reconfig script in service component instances configured to accept the DTI Event.
151
152         POST /events < <dcae_event>
153
154         POST /events?ndtify="n" < <dcae_event>
155
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.
165
166         Parameters
167         ----------
168         notify : string
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.
171
172         Returns
173         -------
174         dict
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.
177
178         """
179
180         if cherrypy.request.method != "POST":
181             raise cherrypy.HTTPError(404, "unexpected method {}".format(cherrypy.request.method))
182
183         msg = ""
184
185         dti_event = cherrypy.request.json or {}
186         str_dti_event = json.dumps(dti_event)
187
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))
193
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'
199
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'
203
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'
207
208         if msg:
209             result = {"ERROR": msg}
210
211             DTIWeb.logger.error("%s: dti_event=%s result=%s", \
212                 req_info, str_dti_event, json.dumps(result))
213         else:
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
218
219             prc = DTIProcessor(dti_event, send_notification=send_notification)
220             result = prc.get_result()
221
222             DTIWeb.logger.info("%s: dti_event=%s result=%s", \
223                 req_info, str_dti_event, json.dumps(result))
224
225         success, http_status_code, _ = audit.audit_done(result=json.dumps(result))
226         if msg:
227             cherrypy.response.status = "400 Bad Request"
228         elif not success:
229             cherrypy.response.status = http_status_code
230
231         return result
232
233     def get_docker_events(self, request, service, location):
234         """
235         common routine for dti_docker_events and oti_docker_events
236
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
240         :return:
241         """
242
243         if request.method != "GET":
244             raise cherrypy.HTTPError(404, "unexpected method {}".format(request.method))
245
246         req_info = _DTIWeb._get_request_info(request)
247         audit = Audit(req_message=req_info, headers=request.headers)
248
249         return DTIProcessor.get_docker_raw_events(service, location)
250
251     def get_k8s_events(self, request, **params):
252         """
253         common routine for dti_k8s_events and oti_k8s_events
254
255         :param request: HTTP GET request
256         :param params: HTTP request query parameters
257         :return:
258         """
259         if request.method != "GET":
260             raise cherrypy.HTTPError(404, "unexpected method {}".format(request.method))
261
262         req_info = _DTIWeb._get_request_info(request)
263         audit = Audit(req_message=req_info, headers=request.headers)
264
265         pod = request.params['pod']
266         namespace = request.params['namespace']
267         cluster = request.params['cluster']
268
269         return DTIProcessor.get_k8_raw_events(pod, cluster, namespace)
270
271     @cherrypy.expose
272     @cherrypy.tools.json_out()
273     def oti_k8s_events(self, **params):
274         """
275         Retrieve raw JSON events from application events database
276
277         GET /oti_k8_events?pod=<sts-1>&namespace=<ns1>&cluster=<cluster1>
278
279         Parameters
280         ----------
281         pod ID : string
282             POD ID of the stateful set POD
283         namespace: string
284             kubernetes namespace
285         cluster: string
286             kubernetes cluster FQDN
287
288         Returns
289         -------
290         dict
291             JSON object containing the fully-bound configuration.
292
293         """
294
295         return self.get_k8s_events(cherrypy.request, params)
296
297     @cherrypy.expose
298     @cherrypy.tools.json_out()
299     def dti_k8s_events(self, **params):
300         """
301         Retrieve raw JSON events from application events database
302
303         GET /dti_k8_events?pod=<sts-1>&namespace=<ns1>&cluster=<cluster1>
304
305         Parameters
306         ----------
307         pod ID : string
308             POD ID of the stateful set POD
309         namespace: string
310             kubernetes namespace
311         cluster: string
312             kubernetes cluster FQDN
313
314         Returns
315         -------
316         dict
317             JSON object containing the fully-bound configuration.
318
319         """
320
321         return self.get_k8s_events(cherrypy.request, params)
322
323     @cherrypy.expose
324     @cherrypy.tools.json_out()
325     def oti_docker_events(self, service, location=None):
326         """
327         Retrieve raw JSON events from application events database related to docker deployments
328
329         GET /oti_docker_events?service=<svc>&location=<location>
330
331         Parameters
332         ----------
333         service : string
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).
336         location : string
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.
340
341         Returns
342         -------
343         dict
344             JSON object containing the fully-bound configuration.
345
346         """
347
348         return self.get_docker_events(cherrypy.request, service, location)
349
350     @cherrypy.expose
351     @cherrypy.tools.json_out()
352     def dti_docker_events(self, service, location=None):
353         """
354         Retrieve raw JSON events from application events database related to docker deployments
355
356         GET /dti_docker_events?service=<svc>&location=<location>
357
358         Parameters
359         ----------
360         service : string
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).
363         location : string
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.
367
368         Returns
369         -------
370         dict
371             JSON object containing the fully-bound configuration.
372
373         """
374
375         return self.get_docker_events(cherrypy.request, service, location)
376
377     #----- Config Binding Service (CBS) endpoint methods
378
379     @cherrypy.expose
380     @cherrypy.popargs('service_name')
381     @cherrypy.tools.json_out()
382     def service_component(self, service_name):
383         """
384         Retrieve fully-bound configuration for service_name from Consul KVs.
385
386         GET /service_component/<service_name>
387
388         Parameters
389         ----------
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).
393
394         Returns
395         -------
396         dict
397             JSON object containing the fully-bound configuration.
398
399         """
400
401         if cherrypy.request.method != "GET":
402             raise cherrypy.HTTPError(404, "unexpected method {}".format(cherrypy.request.method))
403
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))
408
409         try:
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)
414
415         DTIWeb.logger.info("%s: service_name=%s result=%s", \
416             req_info, service_name, json.dumps(result))
417
418         success, http_status_code, _ = audit.audit_done(result=json.dumps(result))
419         if not success:
420             cherrypy.response.status = http_status_code
421
422         return result
423
424     @cherrypy.expose
425     @cherrypy.popargs('service_name')
426     @cherrypy.tools.json_out()
427     def service_component_all(self, service_name, service_location=None, policy_ids="y"):
428         """
429         Retrieve all information for service_name (config, dti, dti_events, and policies) from Consul KVs.
430
431         GET /service_component_all/<service_name>
432
433         GET /service_component_all/<service_name>?service_location=<service_location>
434
435         GET /service_component_all/<service_name>?service_location=<service_location>;policy_ids=n
436
437         Parameters
438         ----------
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.
445         policy_ids : string
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.
448
449         Returns
450         -------
451         dict
452             JSON object containing all information for component service_name.
453             The top-level keys may include the following:
454             config : dict
455                 The cloudify node's application_config property from when the start workflow was executed.
456             dti : dict
457                 Keys are VNF Types that the component currently is assigned to monitor.  Policy can change them.
458             dti_events : dict
459                 The latest deploy DTI events, keyed by VNF Type and sub-keyed by VNF Instance ID.
460             policies : dict
461                 event : dict
462                     Contains information about when the policies folder was last written.
463                 items : dict
464                     Contains all policy bodies for the service_name component, keyed by policy_id.
465
466         """
467
468         if cherrypy.request.method != "GET":
469             raise cherrypy.HTTPError(404, "unexpected method {}".format(cherrypy.request.method))
470
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))
475
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
480         try:
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)
485
486         DTIWeb.logger.info("%s: service_name=%s result=%s", \
487             req_info, service_name, json.dumps(result))
488
489         success, http_status_code, _ = audit.audit_done(result=json.dumps(result))
490         if not success:
491             cherrypy.response.status = http_status_code
492
493         return result
494
495     @cherrypy.expose
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):
499         """
500         Retrieve current (latest, not undeployed) DTI events from Consul KVs.
501
502         GET /dti/<service_name>
503
504         GET /dti/<service_name>?vnf_type=<vnf_type>;vnf_id=<vnf_id>;service_location=<service_location>
505
506         GET /dti
507
508         GET /dti?vnf_type=<vnf_type>;vnf_id=<vnf_id>;service_location=<service_location>
509
510         Parameters
511         ----------
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).
515         vnf_type : string
516             optional, allows multiple values separated by commas.  Gets DTI events for these vnf_type(s).
517         vnf_id : string
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.
522
523         Returns
524         -------
525         dict
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.
530
531         """
532
533         if cherrypy.request.method != "GET":
534             raise cherrypy.HTTPError(404, "unexpected method {}".format(cherrypy.request.method))
535
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))
540
541         try:
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)
546
547         DTIWeb.logger.info("%s: service_name=%s result=%s", \
548             req_info, service_name, json.dumps(result))
549
550         success, http_status_code, _ = audit.audit_done(result=json.dumps(result))
551         if not success:
552             cherrypy.response.status = http_status_code
553
554         return result
555
556     @cherrypy.expose
557     @cherrypy.popargs('service_name')
558     @cherrypy.tools.json_out()
559     def policies(self, service_name, policy_id=None):
560         """
561         Retrieve policies for service_name from Consul KVs.
562
563         GET /policies/<service_name>
564
565         GET /policies/<service_name>?policy_id=<policy_id>
566
567         Parameters
568         ---------- 
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).
572         policy_id : string
573             optional.  Limits returned policy to this policy_id.
574
575         Returns
576         -------
577         dict
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.
581
582         """
583
584         if cherrypy.request.method != "GET":
585             raise cherrypy.HTTPError(404, "unexpected method {}".format(cherrypy.request.method))
586
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))
591
592         try:
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)
597
598         DTIWeb.logger.info("%s: service_name=%s result=%s", \
599             req_info, service_name, json.dumps(result))
600
601         success, http_status_code, _ = audit.audit_done(result=json.dumps(result))
602         if not success:
603             cherrypy.response.status = http_status_code
604
605         return result