RIC-769: Committing individual files rather than tar archive
[ric-plt/appmgr.git] / xapp_orchestrater / dev / xapp_onboarder / xapp_onboarder / helm_controller / xApp_builder.py
diff --git a/xapp_orchestrater/dev/xapp_onboarder/xapp_onboarder/helm_controller/xApp_builder.py b/xapp_orchestrater/dev/xapp_onboarder/xapp_onboarder/helm_controller/xApp_builder.py
new file mode 100644 (file)
index 0000000..a9ecf41
--- /dev/null
@@ -0,0 +1,300 @@
+################################################################################
+#   Copyright (c) 2020 AT&T Intellectual Property.                             #
+#                                                                              #
+#   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.                                             #
+################################################################################
+
+import logging
+import yaml
+import json
+import os
+import io
+import subprocess
+import shutil
+import re
+import copy
+import platform
+import tarfile
+import stat
+import sys
+from xapp_onboarder.server import settings
+from xapp_onboarder.repo_manager.repo_manager import repo_manager, RepoManagerError
+from pkg_resources import resource_filename
+from subprocess import PIPE, check_output, STDOUT
+from xapp_onboarder.repo_manager.repo_manager import requests_retry_session
+log = logging.getLogger(__name__)
+
+
+def indent(text, amount, ch=' '):
+    padding = amount * ch
+    return ''.join(padding + line for line in text.splitlines(True))
+
+
+class xAppError(Exception):
+    def __init__(self, message, status_code):
+        # Call the base class constructor with the parameters it needs
+        super().__init__(message)
+        self.status_code = status_code
+
+
+class xApp():
+    def __init__(self, config_file, schema_file):
+        self.config_file = config_file
+        self.schema_file = schema_file
+
+        if 'name' not in self.config_file:
+            raise xAppError(
+                "xApp chart name not found. (Caused by: config-file.json does not contain xapp_name attribute.)", 500)
+
+        if 'version' not in self.config_file:
+            raise xAppError(
+                "xApp chart version not found. (Caused by: config-file.json does not contain version attribute.)", 500)
+
+        self.chart_name = self.config_file['name']
+        self.chart_version = self.config_file['version']
+        self.configmap_config_json_file = copy.deepcopy(self.config_file)
+        self.chart_workspace_path = settings.CHART_WORKSPACE_PATH + '/' + self.chart_name + '-' + self.chart_version
+        if os.path.exists(self.chart_workspace_path):
+            shutil.rmtree(self.chart_workspace_path)
+        os.makedirs(self.chart_workspace_path)
+        shutil.copytree(resource_filename( 'xapp_onboarder', 'resources/xapp-std'), self.chart_workspace_path + '/' + self.chart_name)
+        self.setup_helm()
+
+    def setup_helm(self):
+        self.helm_client_path = 'helm'
+        try:
+            process = subprocess.run([self.helm_client_path], stdout=PIPE, stderr=PIPE, check=True)
+
+        except Exception as err:
+            print(err)
+            self.download_helm()
+            self.helm_client_path = settings.CHART_WORKSPACE_PATH + '/helm'
+
+    def download_helm(self):
+        if not os.path.isfile(settings.CHART_WORKSPACE_PATH + '/helm'):
+            log.info("Helm client missing. Trying to download it.")
+            helm_file_name = "helm-v{}-{}-amd64.tar.gz".format(settings.HELM_VERSION, platform.system().lower())
+            helm_download_link = "https://get.helm.sh/" + helm_file_name
+
+
+            try:
+                response = requests_retry_session().get(helm_download_link, timeout=settings.HTTP_TIME_OUT)
+            except Exception as err:
+                error_message = "Download helm client failed. (Caused by: " + str(err)+")"
+                log.error(error_message)
+                raise xAppError(error_message, 500)
+            else:
+                if response.status_code != 200:
+                    error_message = "Download helm chart failed. Helm repo return status code: "+ str(response.status_code)  +" "+ response.content.decode("utf-8")
+                    log.error(error_message)
+                    raise xAppError(error_message, 500)
+
+                file_stream = io.BytesIO(response.content)
+
+                with tarfile.open(fileobj=file_stream) as tar:
+                    helm_client = tar.extractfile(platform.system().lower() + "-amd64/helm")
+                    with open(settings.CHART_WORKSPACE_PATH+'/helm', 'wb') as file:
+                        file.write(helm_client.read())
+                st = os.stat(settings.CHART_WORKSPACE_PATH+'/helm')
+                os.chmod(settings.CHART_WORKSPACE_PATH+'/helm', st.st_mode | stat.S_IEXEC)
+
+
+
+
+    def recursive_convert_config_file(self, node_list=list()):
+        current_node = self.configmap_config_json_file
+        helm_value_path = '.Values'
+        for node in node_list:
+            current_node = current_node.get(node)
+            helm_value_path = helm_value_path + ' ' + "\"" + node + "\""
+
+        if type(current_node) is not dict:
+            raise TypeError("Recursive write was called on a leaf node.")
+
+        for item in current_node.keys():
+            if type(current_node.get(item)) is not dict:
+                current_node[item] = '{{ index '+ helm_value_path +' "'+ item + '" | toJson }}'
+            else:
+                new_node_list = node_list.copy()
+                new_node_list.append(item)
+                self.recursive_convert_config_file(new_node_list)
+
+
+    def append_config_to_config_map(self):
+        with open(self.chart_workspace_path + '/' + self.chart_name + '/templates/appconfig.yaml', 'a') as outputfile:
+            self.recursive_convert_config_file()
+            config_file_json_text = json.dumps(self.configmap_config_json_file, indent=4)
+            indented_config_text = indent(config_file_json_text, 4)
+            indented_config_text = re.sub(r"\"{{", '{{', indented_config_text)
+            indented_config_text = re.sub(r"}}\"", '}}', indented_config_text)
+            indented_config_text = re.sub(r"\\", '', indented_config_text)
+            outputfile.write("  config-file.json: |\n")
+            outputfile.write(indented_config_text)
+            outputfile.write("\n  schema.json: |\n")
+            schema_json = json.dumps(self.schema_file, indent=4)
+            indented_schema_text = indent(schema_json, 4)
+            outputfile.write(indented_schema_text)
+
+
+# This is a work around for the bronze release to be backward compatible to the previous xapp standard helm template
+    def write_config_and_schema(self):
+        os.makedirs(self.chart_workspace_path + '/' + self.chart_name + '/descriptors')
+        os.makedirs(self.chart_workspace_path + '/' + self.chart_name + '/config')
+        with open(self.chart_workspace_path + '/' + self.chart_name + '/descriptors/schema.json', 'w') as outfile:
+            json.dump(self.schema_file, outfile)
+        with open(self.chart_workspace_path + '/' + self.chart_name + '/config/config-file.json', 'w') as outfile:
+            json.dump(self.config_file, outfile)
+
+
+
+    def add_probes_to_deployment(self):
+        with open(self.chart_workspace_path + '/' + self.chart_name + '/templates/deployment.yaml', 'a') as outputfile:
+
+            for probes in ['readinessProbe', 'livenessProbe']:
+                if self.configmap_config_json_file.get(probes):
+                    probe_definition = self.configmap_config_json_file.get(probes)
+                    probe_definition_yaml = yaml.dump(probe_definition, width=1000)
+
+                    print(probe_definition_yaml)
+
+                    indented_probe_definition_yaml = indent(probe_definition_yaml, 12)
+                    indented_probe_definition_yaml = re.sub(r" \| toJson", '', indented_probe_definition_yaml)
+                    indented_probe_definition_yaml = re.sub(r"'", '', indented_probe_definition_yaml)
+                    outputfile.write("          "+probes+":\n")
+                    outputfile.write(indented_probe_definition_yaml)
+
+
+    def append_config_to_values_yaml(self):
+        with open(self.chart_workspace_path + '/' + self.chart_name + '/values.yaml', 'a') as outputfile:
+            yaml.dump(self.config_file, outputfile, default_flow_style=False)
+
+
+    def change_chart_name_version(self):
+        with open(self.chart_workspace_path + '/' + self.chart_name + '/Chart.yaml', 'r') as inputfile:
+            self.chart_yaml = yaml.load(inputfile, Loader=yaml.FullLoader)
+            self.chart_yaml['version'] = self.chart_version
+            self.chart_yaml['name'] = self.chart_name
+
+        with open(self.chart_workspace_path + '/' + self.chart_name + '/Chart.yaml', 'w') as outputfile:
+            yaml.dump(self.chart_yaml, outputfile, default_flow_style=False)
+
+
+    def helm_lint(self):
+        try:
+            process = subprocess.run([self.helm_client_path, "lint", self.chart_workspace_path + "/" + self.chart_name], stdout=PIPE, stderr=PIPE, check=True)
+
+        except OSError as err:
+            raise xAppError(
+                "xApp " + self.chart_name + '-' + self.chart_version + " helm lint failed. (Caused by: " + str(
+                    err) + ")", 500)
+        except subprocess.CalledProcessError as err:
+            raise xAppError(
+                "xApp " + self.chart_name + '-' + self.chart_version + " helm lint failed. (Caused by: " +
+                err.stderr.decode("utf-8") +  "\n" + err.stdout.decode("utf-8") + ")", 400)
+
+    def package_chart(self):
+        self.write_config_and_schema()
+        self.append_config_to_config_map()
+        self.append_config_to_values_yaml()
+        self.add_probes_to_deployment()
+        self.change_chart_name_version()
+        self.helm_lint()
+        try:
+            process = subprocess.run([self.helm_client_path, "package", self.chart_workspace_path + "/" + self.chart_name, "-d"
+                               ,self.chart_workspace_path], stdout=PIPE, stderr=PIPE, check=True)
+
+        except OSError as err:
+                raise xAppError("xApp "+ self.chart_name+'-'+self.chart_version +" packaging failed. (Caused by: "+str(err) +")", 500)
+        except subprocess.CalledProcessError as err:
+            raise xAppError(
+                "xApp " + self.chart_name + '-' + self.chart_version + " packaging failed. (Caused by: " +
+                    err.stderr.decode("utf-8") + ")", 500)
+
+
+
+    def distribute_chart(self):
+        try:
+            repo_manager.upload_chart(self)
+        except RepoManagerError as err:
+            raise xAppError( "xApp " + self.chart_name + '-' + self.chart_version + " distribution failed. (Caused by: " + str(err) + ")" , err.status_code)
+
+    def install_chart_package(xapp_chart_name, version, namespace, overridefile):
+        try: 
+          tar = tarfile.open(xapp_chart_name + "-" + version + ".tgz")
+          tar.extractall()
+          tar.close()
+          if overridefile != "":
+            process = subprocess.run(["helm", "install", xapp_chart_name, "./" + xapp_chart_name, "-f", overridefile, "--namespace=" + namespace], stdout=PIPE, stderr=PIPE, check=True)
+          else:
+            process = subprocess.run(["helm", "install", xapp_chart_name, "./" + xapp_chart_name, "--namespace=" + namespace], stdout=PIPE, stderr=PIPE, check=True)
+          status = 1
+        except subprocess.CalledProcessError as err:
+            print(err.stderr.decode())
+            status=0
+        except Exception as err:
+            print(err)
+            status = 0
+        subprocess.run(["rm", "-rf", xapp_chart_name ])
+        subprocess.run(["rm", "-rf", xapp_chart_name + "-" + version + ".tgz" ])
+        return status
+
+    def uninstall_chart_package(xapp_chart_name, namespace):
+
+        try:
+          process = subprocess.run(["helm", "delete", xapp_chart_name, "--namespace=" + namespace], stdout=PIPE, stderr=PIPE, check=True)
+          status = 1
+
+        except Exception as err:
+                print(err.stderr.decode())
+                status = 0
+
+        return status
+    def health_check_xapp(xapp_chart_name, namespace):
+       
+        try:
+          getpodname=subprocess.check_output("kubectl get po -n " + namespace + " |  grep -w " +  xapp_chart_name + " | awk '{print $1}'", shell=True).decode().strip("\n")
+          if getpodname=="":
+              print("No " + xapp_chart_name + " xapp found under " + namespace + " namespace.")
+              sys.exit()
+          process = subprocess.check_output("kubectl describe po " + getpodname +  " --namespace=" + namespace + "| grep -B 0 -A 5 'Conditions:'", shell=True).decode()
+
+          final= re.search("Initialized.*", process)
+          temp=final.group().split(' ',1)[1]
+          initialized=" ".join(temp.split())
+          
+          final= re.search("Ready.*", process)
+          temp=final.group().split(' ',1)[1]
+          ready=" ".join(temp.split())
+          
+          final= re.search("ContainersReady.*", process)
+          temp=final.group().split(' ',1)[1]
+          containersready=" ".join(temp.split())
+          
+          final= re.search("PodScheduled.*", process)
+          temp=final.group().split(' ',1)[1]
+          podscheduled=" ".join(temp.split())
+         
+          if "True"==initialized and "True"==podscheduled and "True"==containersready and "True"==ready:
+             print("Xapp health status : Healthy")
+          else:
+             print("Xapp health status : Unhealthy")
+             if "True"!=containersready:
+               print("ContainersReady=False, All the containers in the pod are not ready\n")
+             elif "True"!=initialized:
+               print("Initialized=False, Init containers have not yet started\n")
+             elif "True"!=podscheduled:
+               print("PodScheduled=False, Pod has not yet scheduled to node\n")
+             elif "True"!=ready:
+               print("Ready=False, Pod is not ready to serve any request\n")
+        except Exception as err:
+            print(err.output.decode())