Add OTI event-handler project
[dcaegen2/platform.git] / oti / event-handler / otihandler / web_server.py
diff --git a/oti/event-handler/otihandler/web_server.py b/oti/event-handler/otihandler/web_server.py
new file mode 100644 (file)
index 0000000..f3eb071
--- /dev/null
@@ -0,0 +1,603 @@
+# ================================================================================
+# 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 < <dcae_event>
+
+        POST /events?ndtify="n" < <dcae_event>
+
+        where <dcae_event> 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))
+
+        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'
+            DTIWeb.logger.error(msg)
+            raise cherrypy.HTTPError(400, msg)
+        elif dcae_service_action.lower() not in self.VALID_EVENT_TYPES:
+            msg = 'dcae_service_action is invalid'
+            DTIWeb.logger.error(msg)
+            raise cherrypy.HTTPError(400,msg)
+
+        dcae_target_name = dti_event.get('dcae_target_name')
+        if not dcae_target_name:
+            msg = 'dcae_target_name is missing'
+            DTIWeb.logger.error(msg)
+            raise cherrypy.HTTPError(400, msg)
+
+        dcae_target_type = dti_event.get('dcae_target_type', '')
+        if not dcae_target_type:
+            msg = 'dcae_target_type is missing'
+            DTIWeb.logger.error(msg)
+            raise cherrypy.HTTPError(400, msg)
+
+        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 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=<sts-1>&namespace=<ns1>&cluster=<cluster1>
+
+        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=<sts-1>&namespace=<ns1>&cluster=<cluster1>
+
+        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=<svc>&location=<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=<svc>&location=<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/<service_name>
+
+        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/<service_name>
+
+        GET /service_component_all/<service_name>?service_location=<service_location>
+
+        GET /service_component_all/<service_name>?service_location=<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/<service_name>
+
+        GET /dti/<service_name>?vnf_type=<vnf_type>;vnf_id=<vnf_id>;service_location=<service_location>
+
+        GET /dti
+
+        GET /dti?vnf_type=<vnf_type>;vnf_id=<vnf_id>;service_location=<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_dti(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/<service_name>
+
+        GET /policies/<service_name>?policy_id=<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