# ================================================================================ # Copyright (c) 2019-2020 AT&T Intellectual Property. All rights reserved. # ================================================================================ # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ============LICENSE_END========================================================= """web-service for oti_handler""" import json import logging import os import time from datetime import datetime import cherrypy from otihandler.cbs_rest import CBSRest from otihandler.config import Config from otihandler.dti_processor import DTIProcessor from otihandler.onap.audit import Audit class DTIWeb(object): """run REST API of OTI Handler""" logger = logging.getLogger("oti_handler.web_server") HOST_INADDR_ANY = ".".join("0"*4) @staticmethod def run_forever(audit): """run the web-server of OTI Handler forever""" cherrypy.config.update({"server.socket_host": DTIWeb.HOST_INADDR_ANY, "server.socket_port": Config.wservice_port}) protocol = "http" tls_info = "" if Config.tls_server_cert_file and Config.tls_private_key_file: tm_cert = os.path.getmtime(Config.tls_server_cert_file) tm_key = os.path.getmtime(Config.tls_private_key_file) #cherrypy.server.ssl_module = 'builtin' cherrypy.server.ssl_module = 'pyOpenSSL' cherrypy.server.ssl_certificate = Config.tls_server_cert_file cherrypy.server.ssl_private_key = Config.tls_private_key_file if Config.tls_server_ca_chain_file: cherrypy.server.ssl_certificate_chain = Config.tls_server_ca_chain_file protocol = "https" tls_info = "cert: {} {} {}".format(Config.tls_server_cert_file, Config.tls_private_key_file, Config.tls_server_ca_chain_file) cherrypy.tree.mount(_DTIWeb(), '/') DTIWeb.logger.info( "%s with config: %s", audit.info("running oti_handler as {}://{}:{} {}".format( protocol, cherrypy.server.socket_host, cherrypy.server.socket_port, tls_info)), json.dumps(cherrypy.config)) cherrypy.engine.start() # If HTTPS server certificate changes, exit to let kubernetes restart us if Config.tls_server_cert_file and Config.tls_private_key_file: while True: time.sleep(600) c_tm_cert = os.path.getmtime(Config.tls_server_cert_file) c_tm_key = os.path.getmtime(Config.tls_private_key_file) if c_tm_cert > tm_cert or c_tm_key > tm_key: DTIWeb.logger.info("cert or key file updated") cherrypy.engine.stop() cherrypy.engine.exit() break class _DTIWeb(object): """REST API of DTI Handler""" VALID_EVENT_TYPES = ['deploy', 'undeploy', 'add', 'delete', 'update', 'notify'] @staticmethod def _get_request_info(request): """Returns info about the http request.""" return "{} {}{}".format(request.method, request.script_name, request.path_info) #----- Common endpoint methods @cherrypy.expose @cherrypy.tools.json_out() def healthcheck(self): """Returns healthcheck results.""" req_info = _DTIWeb._get_request_info(cherrypy.request) audit = Audit(req_message=req_info, headers=cherrypy.request.headers) DTIWeb.logger.info("%s", req_info) result = Audit.health() DTIWeb.logger.info("healthcheck %s: result=%s", req_info, json.dumps(result)) audit.audit_done(result=json.dumps(result)) return result @cherrypy.expose def shutdown(self): """Shutdown the web server.""" req_info = _DTIWeb._get_request_info(cherrypy.request) audit = Audit(req_message=req_info, headers=cherrypy.request.headers) DTIWeb.logger.info("%s: --- stopping REST API of DTI Handler ---", req_info) cherrypy.engine.exit() health = json.dumps(Audit.health()) audit.info("oti_handler health: {}".format(health)) DTIWeb.logger.info("oti_handler health: %s", health) DTIWeb.logger.info("%s: --------- the end -----------", req_info) result = str(datetime.now()) audit.info_requested(result) return "goodbye! shutdown requested {}".format(result) # ----- DTI Handler mock endpoint methods @cherrypy.expose @cherrypy.tools.json_out() @cherrypy.tools.json_in() def mockevents(self): result = {"KubeNamespace":"com-my-dcae-test", "KubePod":"pod-0", "KubeServiceName":"pod-0.service.local", "KubeServicePort":"8880", "KubeClusterFqdn":"fqdn-1"} return result #----- DTI Handler endpoint methods @cherrypy.expose @cherrypy.tools.json_out() @cherrypy.tools.json_in() def events(self, notify="y"): """ Run dti reconfig script in service component instances configured to accept the DTI Event. POST /events < POST /events?ndtify="n" < where is the entire DTI Event passed as a JSON object and contains at least these top-level keys: dcae_service_action : string required, 'deploy' or 'undeploy' dcae_target_name : string required, VNF Instance ID dcae_target_type : string required, VNF Type of the VNF Instance dcae_service_location : string optional, CLLI location. Not provided or '' infers all locations. Parameters ---------- notify : string optional, default "y", any of these will not notify components: [ "f", "false", "False", "FALSE", "n", "no" ] When "n" will **not** notify components of this DTI Event update to Consul. Returns ------- dict JSON object containing success or error of executing the dti reconfig script on each component instance's docker container, keyed by service_component_name. """ if cherrypy.request.method != "POST": raise cherrypy.HTTPError(404, "unexpected method {}".format(cherrypy.request.method)) msg = "" dti_event = cherrypy.request.json or {} str_dti_event = json.dumps(dti_event) req_info = _DTIWeb._get_request_info(cherrypy.request) audit = Audit(req_message="{}: {}".format(req_info, str_dti_event), \ headers=cherrypy.request.headers) DTIWeb.logger.info("%s: dti_event=%s headers=%s", \ req_info, str_dti_event, json.dumps(cherrypy.request.headers)) dcae_service_action = dti_event.get('dcae_service_action') if not dcae_service_action: msg = 'dcae_service_action is missing' elif dcae_service_action.lower() not in self.VALID_EVENT_TYPES: msg = 'dcae_service_action is invalid' dcae_target_name = dti_event.get('dcae_target_name') if not msg and not dcae_target_name: msg = 'dcae_target_name is missing' dcae_target_type = dti_event.get('dcae_target_type', '') if not msg and not dcae_target_type: msg = 'dcae_target_type is missing' if msg: result = {"ERROR": msg} DTIWeb.logger.error("%s: dti_event=%s result=%s", \ req_info, str_dti_event, json.dumps(result)) else: send_notification = True if (isinstance(notify, bool) and not notify) or \ (isinstance(notify, str) and notify.lower() in [ "f", "false", "n", "no" ]): send_notification = False prc = DTIProcessor(dti_event, send_notification=send_notification) result = prc.get_result() DTIWeb.logger.info("%s: dti_event=%s result=%s", \ req_info, str_dti_event, json.dumps(result)) success, http_status_code, _ = audit.audit_done(result=json.dumps(result)) if msg: cherrypy.response.status = "400 Bad Request" elif not success: cherrypy.response.status = http_status_code return result def get_docker_events(self, request, service, location): """ common routine for dti_docker_events and oti_docker_events :param request: HTTP GET request :param service: HTTP request query parameter for service name :param location: HTTP request query parameter for location CLLI :return: """ if request.method != "GET": raise cherrypy.HTTPError(404, "unexpected method {}".format(request.method)) req_info = _DTIWeb._get_request_info(request) audit = Audit(req_message=req_info, headers=request.headers) return DTIProcessor.get_docker_raw_events(service, location) def get_k8s_events(self, request, **params): """ common routine for dti_k8s_events and oti_k8s_events :param request: HTTP GET request :param params: HTTP request query parameters :return: """ if request.method != "GET": raise cherrypy.HTTPError(404, "unexpected method {}".format(request.method)) req_info = _DTIWeb._get_request_info(request) audit = Audit(req_message=req_info, headers=request.headers) pod = request.params['pod'] namespace = request.params['namespace'] cluster = request.params['cluster'] return DTIProcessor.get_k8_raw_events(pod, cluster, namespace) @cherrypy.expose @cherrypy.tools.json_out() def oti_k8s_events(self, **params): """ Retrieve raw JSON events from application events database GET /oti_k8_events?pod=&namespace=&cluster= Parameters ---------- pod ID : string POD ID of the stateful set POD namespace: string kubernetes namespace cluster: string kubernetes cluster FQDN Returns ------- dict JSON object containing the fully-bound configuration. """ return self.get_k8s_events(cherrypy.request, params) @cherrypy.expose @cherrypy.tools.json_out() def dti_k8s_events(self, **params): """ Retrieve raw JSON events from application events database GET /dti_k8_events?pod=&namespace=&cluster= Parameters ---------- pod ID : string POD ID of the stateful set POD namespace: string kubernetes namespace cluster: string kubernetes cluster FQDN Returns ------- dict JSON object containing the fully-bound configuration. """ return self.get_k8s_events(cherrypy.request, params) @cherrypy.expose @cherrypy.tools.json_out() def oti_docker_events(self, service, location=None): """ Retrieve raw JSON events from application events database related to docker deployments GET /oti_docker_events?service=&location= Parameters ---------- service : string The service component name assigned by dockerplugin to the component that is unique to the cloudify node instance and used in its Consul key(s). location : string optional. allows multiple values separated by commas. Filters DTI events with dcae_service_location in service_location. If service_location is not provided, then defaults to dockerhost or k8s cluster master node service Consul TAGs if service_name is provided, otherwise results are not location filtered. Returns ------- dict JSON object containing the fully-bound configuration. """ return self.get_docker_events(cherrypy.request, service, location) @cherrypy.expose @cherrypy.tools.json_out() def dti_docker_events(self, service, location=None): """ Retrieve raw JSON events from application events database related to docker deployments GET /dti_docker_events?service=&location= Parameters ---------- service : string The service component name assigned by dockerplugin to the component that is unique to the cloudify node instance and used in its Consul key(s). location : string optional. allows multiple values separated by commas. Filters DTI events with dcae_service_location in service_location. If service_location is not provided, then defaults to dockerhost or k8s cluster master node service Consul TAGs if service_name is provided, otherwise results are not location filtered. Returns ------- dict JSON object containing the fully-bound configuration. """ return self.get_docker_events(cherrypy.request, service, location) #----- Config Binding Service (CBS) endpoint methods @cherrypy.expose @cherrypy.popargs('service_name') @cherrypy.tools.json_out() def service_component(self, service_name): """ Retrieve fully-bound configuration for service_name from Consul KVs. GET /service_component/ Parameters ---------- service_name : string The service component name assigned by dockerplugin to the component that is unique to the cloudify node instance and used in its Consul key(s). Returns ------- dict JSON object containing the fully-bound configuration. """ if cherrypy.request.method != "GET": raise cherrypy.HTTPError(404, "unexpected method {}".format(cherrypy.request.method)) req_info = _DTIWeb._get_request_info(cherrypy.request) audit = Audit(req_message=req_info, headers=cherrypy.request.headers) DTIWeb.logger.info("%s: service_name=%s headers=%s", \ req_info, service_name, json.dumps(cherrypy.request.headers)) try: result = CBSRest.get_service_component(service_name) except Exception as e: result = {"ERROR": "exception {}: {!s}".format(type(e).__name__, e)} audit.set_http_status_code(404) DTIWeb.logger.info("%s: service_name=%s result=%s", \ req_info, service_name, json.dumps(result)) success, http_status_code, _ = audit.audit_done(result=json.dumps(result)) if not success: cherrypy.response.status = http_status_code return result @cherrypy.expose @cherrypy.popargs('service_name') @cherrypy.tools.json_out() def service_component_all(self, service_name, service_location=None, policy_ids="y"): """ Retrieve all information for service_name (config, dti, dti_events, and policies) from Consul KVs. GET /service_component_all/ GET /service_component_all/?service_location= GET /service_component_all/?service_location=;policy_ids=n Parameters ---------- service_name : string The service component name assigned by dockerplugin to the component that is unique to the cloudify node instance and used in its Consul key(s). service_location : string optional, allows multiple values separated by commas. Filters DTI events with dcae_service_location in service_location. policy_ids : string optional, default "y", any of these will unset: [ "f", "false", "False", "FALSE", "n", "no" ] When unset, formats policies items as a list (without policy_ids) rather than as an object indexed by policy_id. Returns ------- dict JSON object containing all information for component service_name. The top-level keys may include the following: config : dict The cloudify node's application_config property from when the start workflow was executed. dti : dict Keys are VNF Types that the component currently is assigned to monitor. Policy can change them. dti_events : dict The latest deploy DTI events, keyed by VNF Type and sub-keyed by VNF Instance ID. policies : dict event : dict Contains information about when the policies folder was last written. items : dict Contains all policy bodies for the service_name component, keyed by policy_id. """ if cherrypy.request.method != "GET": raise cherrypy.HTTPError(404, "unexpected method {}".format(cherrypy.request.method)) req_info = _DTIWeb._get_request_info(cherrypy.request) audit = Audit(req_message=req_info, headers=cherrypy.request.headers) DTIWeb.logger.info("%s: service_name=%s headers=%s", \ req_info, service_name, json.dumps(cherrypy.request.headers)) policies_as_list = False if (isinstance(policy_ids, bool) and not policy_ids) or \ (isinstance(policy_ids, str) and policy_ids.lower() in [ "f", "false", "n", "no" ]): policies_as_list = True try: result = CBSRest.get_service_component_all(service_name, service_location=service_location, policies_as_list=policies_as_list) except Exception as e: result = {"ERROR": "exception {}: {!s}".format(type(e).__name__, e)} audit.set_http_status_code(404) DTIWeb.logger.info("%s: service_name=%s result=%s", \ req_info, service_name, json.dumps(result)) success, http_status_code, _ = audit.audit_done(result=json.dumps(result)) if not success: cherrypy.response.status = http_status_code return result @cherrypy.expose @cherrypy.popargs('service_name') @cherrypy.tools.json_out() def dti(self, service_name=None, vnf_type=None, vnf_id=None, service_location=None): """ Retrieve current (latest, not undeployed) DTI events from Consul KVs. GET /dti/ GET /dti/?vnf_type=;vnf_id=;service_location= GET /dti GET /dti?vnf_type=;vnf_id=;service_location= Parameters ---------- service_name : string optional. The service component name assigned by dockerplugin to the component that is unique to the cloudify node instance and used in its Consul key(s). vnf_type : string optional, allows multiple values separated by commas. Gets DTI events for these vnf_type(s). vnf_id : string optional. Requires vnf_type also. Gets DTI event for this vnf_id. service_location : string optional, allows multiple values separated by commas. Filters DTI events with dcae_service_location in service_location. Returns ------- dict Dictionary of DTI event(s). If one vnf_type and vnf_id are both specified, then object returned will be just the one DTI event. If one vnf_type is specified but not vnf_id, then DTI events will be keyed by vnf_id. Otherwise the DTI events will be keyed by vnf_type, sub-keyed by vnf_id. """ if cherrypy.request.method != "GET": raise cherrypy.HTTPError(404, "unexpected method {}".format(cherrypy.request.method)) req_info = _DTIWeb._get_request_info(cherrypy.request) audit = Audit(req_message=req_info, headers=cherrypy.request.headers) DTIWeb.logger.info("%s: service_name=%s headers=%s", \ req_info, service_name, json.dumps(cherrypy.request.headers)) try: result = CBSRest.get_oti(service_name=service_name, vnf_type=vnf_type, vnf_id=vnf_id, service_location=service_location) except Exception as e: result = {"ERROR": "exception {}: {!s}".format(type(e).__name__, e)} audit.set_http_status_code(404) DTIWeb.logger.info("%s: service_name=%s result=%s", \ req_info, service_name, json.dumps(result)) success, http_status_code, _ = audit.audit_done(result=json.dumps(result)) if not success: cherrypy.response.status = http_status_code return result @cherrypy.expose @cherrypy.popargs('service_name') @cherrypy.tools.json_out() def policies(self, service_name, policy_id=None): """ Retrieve policies for service_name from Consul KVs. GET /policies/ GET /policies/?policy_id= Parameters ---------- service_name : string The service component name assigned by dockerplugin to the component that is unique to the cloudify node instance and used in its Consul key(s). policy_id : string optional. Limits returned policy to this policy_id. Returns ------- dict JSON object containing policy bodies for the service_name component. If policy_id is specified, then object returned will be just the one policy body. If policy_id is not specified, then object will contain all policy bodies, keyed by policy_id. """ if cherrypy.request.method != "GET": raise cherrypy.HTTPError(404, "unexpected method {}".format(cherrypy.request.method)) req_info = _DTIWeb._get_request_info(cherrypy.request) audit = Audit(req_message=req_info, headers=cherrypy.request.headers) DTIWeb.logger.info("%s: service_name=%s headers=%s", \ req_info, service_name, json.dumps(cherrypy.request.headers)) try: result = CBSRest.get_policies(service_name, policy_id=policy_id) except Exception as e: result = {"ERROR": "exception {}: {!s}".format(type(e).__name__, e)} audit.set_http_status_code(404) DTIWeb.logger.info("%s: service_name=%s result=%s", \ req_info, service_name, json.dumps(result)) success, http_status_code, _ = audit.audit_done(result=json.dumps(result)) if not success: cherrypy.response.status = http_status_code return result