Merge "Improve mod/distributorapi code coverage"
[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         dti_event = cherrypy.request.json or {}
184         str_dti_event = json.dumps(dti_event)
185
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))
191
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)
201
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)
207
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)
213
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 not success:
227             cherrypy.response.status = http_status_code
228
229         return result
230
231     def get_docker_events(self, request, service, location):
232         """
233         common routine for dti_docker_events and oti_docker_events
234
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
238         :return:
239         """
240
241         if request.method != "GET":
242             raise cherrypy.HTTPError(404, "unexpected method {}".format(request.method))
243
244         req_info = _DTIWeb._get_request_info(request)
245         audit = Audit(req_message=req_info, headers=request.headers)
246
247         return DTIProcessor.get_docker_raw_events(service, location)
248
249     def get_k8s_events(self, request, **params):
250         """
251         common routine for dti_k8s_events and oti_k8s_events
252
253         :param request: HTTP GET request
254         :param params: HTTP request query parameters
255         :return:
256         """
257         if request.method != "GET":
258             raise cherrypy.HTTPError(404, "unexpected method {}".format(request.method))
259
260         req_info = _DTIWeb._get_request_info(request)
261         audit = Audit(req_message=req_info, headers=request.headers)
262
263         pod = request.params['pod']
264         namespace = request.params['namespace']
265         cluster = request.params['cluster']
266
267         return DTIProcessor.get_k8_raw_events(pod, cluster, namespace)
268
269     @cherrypy.expose
270     @cherrypy.tools.json_out()
271     def oti_k8s_events(self, **params):
272         """
273         Retrieve raw JSON events from application events database
274
275         GET /oti_k8_events?pod=<sts-1>&namespace=<ns1>&cluster=<cluster1>
276
277         Parameters
278         ----------
279         pod ID : string
280             POD ID of the stateful set POD
281         namespace: string
282             kubernetes namespace
283         cluster: string
284             kubernetes cluster FQDN
285
286         Returns
287         -------
288         dict
289             JSON object containing the fully-bound configuration.
290
291         """
292
293         return self.get_k8s_events(cherrypy.request, params)
294
295     @cherrypy.expose
296     @cherrypy.tools.json_out()
297     def dti_k8s_events(self, **params):
298         """
299         Retrieve raw JSON events from application events database
300
301         GET /dti_k8_events?pod=<sts-1>&namespace=<ns1>&cluster=<cluster1>
302
303         Parameters
304         ----------
305         pod ID : string
306             POD ID of the stateful set POD
307         namespace: string
308             kubernetes namespace
309         cluster: string
310             kubernetes cluster FQDN
311
312         Returns
313         -------
314         dict
315             JSON object containing the fully-bound configuration.
316
317         """
318
319         return self.get_k8s_events(cherrypy.request, params)
320
321     @cherrypy.expose
322     @cherrypy.tools.json_out()
323     def oti_docker_events(self, service, location=None):
324         """
325         Retrieve raw JSON events from application events database related to docker deployments
326
327         GET /oti_docker_events?service=<svc>&location=<location>
328
329         Parameters
330         ----------
331         service : string
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).
334         location : string
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.
338
339         Returns
340         -------
341         dict
342             JSON object containing the fully-bound configuration.
343
344         """
345
346         return self.get_docker_events(cherrypy.request, service, location)
347
348     @cherrypy.expose
349     @cherrypy.tools.json_out()
350     def dti_docker_events(self, service, location=None):
351         """
352         Retrieve raw JSON events from application events database related to docker deployments
353
354         GET /dti_docker_events?service=<svc>&location=<location>
355
356         Parameters
357         ----------
358         service : string
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).
361         location : string
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.
365
366         Returns
367         -------
368         dict
369             JSON object containing the fully-bound configuration.
370
371         """
372
373         return self.get_docker_events(cherrypy.request, service, location)
374
375     #----- Config Binding Service (CBS) endpoint methods
376
377     @cherrypy.expose
378     @cherrypy.popargs('service_name')
379     @cherrypy.tools.json_out()
380     def service_component(self, service_name):
381         """
382         Retrieve fully-bound configuration for service_name from Consul KVs.
383
384         GET /service_component/<service_name>
385
386         Parameters
387         ----------
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).
391
392         Returns
393         -------
394         dict
395             JSON object containing the fully-bound configuration.
396
397         """
398
399         if cherrypy.request.method != "GET":
400             raise cherrypy.HTTPError(404, "unexpected method {}".format(cherrypy.request.method))
401
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))
406
407         try:
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)
412
413         DTIWeb.logger.info("%s: service_name=%s result=%s", \
414             req_info, service_name, json.dumps(result))
415
416         success, http_status_code, _ = audit.audit_done(result=json.dumps(result))
417         if not success:
418             cherrypy.response.status = http_status_code
419
420         return result
421
422     @cherrypy.expose
423     @cherrypy.popargs('service_name')
424     @cherrypy.tools.json_out()
425     def service_component_all(self, service_name, service_location=None, policy_ids="y"):
426         """
427         Retrieve all information for service_name (config, dti, dti_events, and policies) from Consul KVs.
428
429         GET /service_component_all/<service_name>
430
431         GET /service_component_all/<service_name>?service_location=<service_location>
432
433         GET /service_component_all/<service_name>?service_location=<service_location>;policy_ids=n
434
435         Parameters
436         ----------
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.
443         policy_ids : string
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.
446
447         Returns
448         -------
449         dict
450             JSON object containing all information for component service_name.
451             The top-level keys may include the following:
452             config : dict
453                 The cloudify node's application_config property from when the start workflow was executed.
454             dti : dict
455                 Keys are VNF Types that the component currently is assigned to monitor.  Policy can change them.
456             dti_events : dict
457                 The latest deploy DTI events, keyed by VNF Type and sub-keyed by VNF Instance ID.
458             policies : dict
459                 event : dict
460                     Contains information about when the policies folder was last written.
461                 items : dict
462                     Contains all policy bodies for the service_name component, keyed by policy_id.
463
464         """
465
466         if cherrypy.request.method != "GET":
467             raise cherrypy.HTTPError(404, "unexpected method {}".format(cherrypy.request.method))
468
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))
473
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
478         try:
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)
483
484         DTIWeb.logger.info("%s: service_name=%s result=%s", \
485             req_info, service_name, json.dumps(result))
486
487         success, http_status_code, _ = audit.audit_done(result=json.dumps(result))
488         if not success:
489             cherrypy.response.status = http_status_code
490
491         return result
492
493     @cherrypy.expose
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):
497         """
498         Retrieve current (latest, not undeployed) DTI events from Consul KVs.
499
500         GET /dti/<service_name>
501
502         GET /dti/<service_name>?vnf_type=<vnf_type>;vnf_id=<vnf_id>;service_location=<service_location>
503
504         GET /dti
505
506         GET /dti?vnf_type=<vnf_type>;vnf_id=<vnf_id>;service_location=<service_location>
507
508         Parameters
509         ----------
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).
513         vnf_type : string
514             optional, allows multiple values separated by commas.  Gets DTI events for these vnf_type(s).
515         vnf_id : string
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.
520
521         Returns
522         -------
523         dict
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.
528
529         """
530
531         if cherrypy.request.method != "GET":
532             raise cherrypy.HTTPError(404, "unexpected method {}".format(cherrypy.request.method))
533
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))
538
539         try:
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)
544
545         DTIWeb.logger.info("%s: service_name=%s result=%s", \
546             req_info, service_name, json.dumps(result))
547
548         success, http_status_code, _ = audit.audit_done(result=json.dumps(result))
549         if not success:
550             cherrypy.response.status = http_status_code
551
552         return result
553
554     @cherrypy.expose
555     @cherrypy.popargs('service_name')
556     @cherrypy.tools.json_out()
557     def policies(self, service_name, policy_id=None):
558         """
559         Retrieve policies for service_name from Consul KVs.
560
561         GET /policies/<service_name>
562
563         GET /policies/<service_name>?policy_id=<policy_id>
564
565         Parameters
566         ---------- 
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).
570         policy_id : string
571             optional.  Limits returned policy to this policy_id.
572
573         Returns
574         -------
575         dict
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.
579
580         """
581
582         if cherrypy.request.method != "GET":
583             raise cherrypy.HTTPError(404, "unexpected method {}".format(cherrypy.request.method))
584
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))
589
590         try:
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)
595
596         DTIWeb.logger.info("%s: service_name=%s result=%s", \
597             req_info, service_name, json.dumps(result))
598
599         success, http_status_code, _ = audit.audit_done(result=json.dumps(result))
600         if not success:
601             cherrypy.response.status = http_status_code
602
603         return result