a9ecf41edb889b3f483093aa2a35a136eafafd83
[ric-plt/appmgr.git] / xapp_orchestrater / dev / xapp_onboarder / xapp_onboarder / helm_controller / xApp_builder.py
1 ################################################################################
2 #   Copyright (c) 2020 AT&T Intellectual Property.                             #
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 ################################################################################
16
17 import logging
18 import yaml
19 import json
20 import os
21 import io
22 import subprocess
23 import shutil
24 import re
25 import copy
26 import platform
27 import tarfile
28 import stat
29 import sys
30 from xapp_onboarder.server import settings
31 from xapp_onboarder.repo_manager.repo_manager import repo_manager, RepoManagerError
32 from pkg_resources import resource_filename
33 from subprocess import PIPE, check_output, STDOUT
34 from xapp_onboarder.repo_manager.repo_manager import requests_retry_session
35 log = logging.getLogger(__name__)
36
37
38 def indent(text, amount, ch=' '):
39     padding = amount * ch
40     return ''.join(padding + line for line in text.splitlines(True))
41
42
43 class xAppError(Exception):
44     def __init__(self, message, status_code):
45         # Call the base class constructor with the parameters it needs
46         super().__init__(message)
47         self.status_code = status_code
48
49
50 class xApp():
51     def __init__(self, config_file, schema_file):
52         self.config_file = config_file
53         self.schema_file = schema_file
54
55         if 'name' not in self.config_file:
56             raise xAppError(
57                 "xApp chart name not found. (Caused by: config-file.json does not contain xapp_name attribute.)", 500)
58
59         if 'version' not in self.config_file:
60             raise xAppError(
61                 "xApp chart version not found. (Caused by: config-file.json does not contain version attribute.)", 500)
62
63         self.chart_name = self.config_file['name']
64         self.chart_version = self.config_file['version']
65         self.configmap_config_json_file = copy.deepcopy(self.config_file)
66         self.chart_workspace_path = settings.CHART_WORKSPACE_PATH + '/' + self.chart_name + '-' + self.chart_version
67         if os.path.exists(self.chart_workspace_path):
68             shutil.rmtree(self.chart_workspace_path)
69         os.makedirs(self.chart_workspace_path)
70         shutil.copytree(resource_filename( 'xapp_onboarder', 'resources/xapp-std'), self.chart_workspace_path + '/' + self.chart_name)
71         self.setup_helm()
72
73     def setup_helm(self):
74         self.helm_client_path = 'helm'
75         try:
76             process = subprocess.run([self.helm_client_path], stdout=PIPE, stderr=PIPE, check=True)
77
78         except Exception as err:
79             print(err)
80             self.download_helm()
81             self.helm_client_path = settings.CHART_WORKSPACE_PATH + '/helm'
82
83     def download_helm(self):
84         if not os.path.isfile(settings.CHART_WORKSPACE_PATH + '/helm'):
85             log.info("Helm client missing. Trying to download it.")
86             helm_file_name = "helm-v{}-{}-amd64.tar.gz".format(settings.HELM_VERSION, platform.system().lower())
87             helm_download_link = "https://get.helm.sh/" + helm_file_name
88
89
90             try:
91                 response = requests_retry_session().get(helm_download_link, timeout=settings.HTTP_TIME_OUT)
92             except Exception as err:
93                 error_message = "Download helm client failed. (Caused by: " + str(err)+")"
94                 log.error(error_message)
95                 raise xAppError(error_message, 500)
96             else:
97                 if response.status_code != 200:
98                     error_message = "Download helm chart failed. Helm repo return status code: "+ str(response.status_code)  +" "+ response.content.decode("utf-8")
99                     log.error(error_message)
100                     raise xAppError(error_message, 500)
101
102                 file_stream = io.BytesIO(response.content)
103
104                 with tarfile.open(fileobj=file_stream) as tar:
105                     helm_client = tar.extractfile(platform.system().lower() + "-amd64/helm")
106                     with open(settings.CHART_WORKSPACE_PATH+'/helm', 'wb') as file:
107                         file.write(helm_client.read())
108                 st = os.stat(settings.CHART_WORKSPACE_PATH+'/helm')
109                 os.chmod(settings.CHART_WORKSPACE_PATH+'/helm', st.st_mode | stat.S_IEXEC)
110
111
112
113
114     def recursive_convert_config_file(self, node_list=list()):
115         current_node = self.configmap_config_json_file
116         helm_value_path = '.Values'
117         for node in node_list:
118             current_node = current_node.get(node)
119             helm_value_path = helm_value_path + ' ' + "\"" + node + "\""
120
121         if type(current_node) is not dict:
122             raise TypeError("Recursive write was called on a leaf node.")
123
124         for item in current_node.keys():
125             if type(current_node.get(item)) is not dict:
126                 current_node[item] = '{{ index '+ helm_value_path +' "'+ item + '" | toJson }}'
127             else:
128                 new_node_list = node_list.copy()
129                 new_node_list.append(item)
130                 self.recursive_convert_config_file(new_node_list)
131
132
133     def append_config_to_config_map(self):
134         with open(self.chart_workspace_path + '/' + self.chart_name + '/templates/appconfig.yaml', 'a') as outputfile:
135             self.recursive_convert_config_file()
136             config_file_json_text = json.dumps(self.configmap_config_json_file, indent=4)
137             indented_config_text = indent(config_file_json_text, 4)
138             indented_config_text = re.sub(r"\"{{", '{{', indented_config_text)
139             indented_config_text = re.sub(r"}}\"", '}}', indented_config_text)
140             indented_config_text = re.sub(r"\\", '', indented_config_text)
141             outputfile.write("  config-file.json: |\n")
142             outputfile.write(indented_config_text)
143             outputfile.write("\n  schema.json: |\n")
144             schema_json = json.dumps(self.schema_file, indent=4)
145             indented_schema_text = indent(schema_json, 4)
146             outputfile.write(indented_schema_text)
147
148
149 # This is a work around for the bronze release to be backward compatible to the previous xapp standard helm template
150     def write_config_and_schema(self):
151         os.makedirs(self.chart_workspace_path + '/' + self.chart_name + '/descriptors')
152         os.makedirs(self.chart_workspace_path + '/' + self.chart_name + '/config')
153         with open(self.chart_workspace_path + '/' + self.chart_name + '/descriptors/schema.json', 'w') as outfile:
154             json.dump(self.schema_file, outfile)
155         with open(self.chart_workspace_path + '/' + self.chart_name + '/config/config-file.json', 'w') as outfile:
156             json.dump(self.config_file, outfile)
157
158
159
160     def add_probes_to_deployment(self):
161         with open(self.chart_workspace_path + '/' + self.chart_name + '/templates/deployment.yaml', 'a') as outputfile:
162
163             for probes in ['readinessProbe', 'livenessProbe']:
164                 if self.configmap_config_json_file.get(probes):
165                     probe_definition = self.configmap_config_json_file.get(probes)
166                     probe_definition_yaml = yaml.dump(probe_definition, width=1000)
167
168                     print(probe_definition_yaml)
169
170                     indented_probe_definition_yaml = indent(probe_definition_yaml, 12)
171                     indented_probe_definition_yaml = re.sub(r" \| toJson", '', indented_probe_definition_yaml)
172                     indented_probe_definition_yaml = re.sub(r"'", '', indented_probe_definition_yaml)
173                     outputfile.write("          "+probes+":\n")
174                     outputfile.write(indented_probe_definition_yaml)
175
176
177     def append_config_to_values_yaml(self):
178         with open(self.chart_workspace_path + '/' + self.chart_name + '/values.yaml', 'a') as outputfile:
179             yaml.dump(self.config_file, outputfile, default_flow_style=False)
180
181
182     def change_chart_name_version(self):
183         with open(self.chart_workspace_path + '/' + self.chart_name + '/Chart.yaml', 'r') as inputfile:
184             self.chart_yaml = yaml.load(inputfile, Loader=yaml.FullLoader)
185             self.chart_yaml['version'] = self.chart_version
186             self.chart_yaml['name'] = self.chart_name
187
188         with open(self.chart_workspace_path + '/' + self.chart_name + '/Chart.yaml', 'w') as outputfile:
189             yaml.dump(self.chart_yaml, outputfile, default_flow_style=False)
190
191
192     def helm_lint(self):
193         try:
194             process = subprocess.run([self.helm_client_path, "lint", self.chart_workspace_path + "/" + self.chart_name], stdout=PIPE, stderr=PIPE, check=True)
195
196         except OSError as err:
197             raise xAppError(
198                 "xApp " + self.chart_name + '-' + self.chart_version + " helm lint failed. (Caused by: " + str(
199                     err) + ")", 500)
200         except subprocess.CalledProcessError as err:
201             raise xAppError(
202                 "xApp " + self.chart_name + '-' + self.chart_version + " helm lint failed. (Caused by: " +
203                 err.stderr.decode("utf-8") +  "\n" + err.stdout.decode("utf-8") + ")", 400)
204
205     def package_chart(self):
206         self.write_config_and_schema()
207         self.append_config_to_config_map()
208         self.append_config_to_values_yaml()
209         self.add_probes_to_deployment()
210         self.change_chart_name_version()
211         self.helm_lint()
212         try:
213             process = subprocess.run([self.helm_client_path, "package", self.chart_workspace_path + "/" + self.chart_name, "-d"
214                                ,self.chart_workspace_path], stdout=PIPE, stderr=PIPE, check=True)
215
216         except OSError as err:
217                 raise xAppError("xApp "+ self.chart_name+'-'+self.chart_version +" packaging failed. (Caused by: "+str(err) +")", 500)
218         except subprocess.CalledProcessError as err:
219             raise xAppError(
220                 "xApp " + self.chart_name + '-' + self.chart_version + " packaging failed. (Caused by: " +
221                     err.stderr.decode("utf-8") + ")", 500)
222
223
224
225     def distribute_chart(self):
226         try:
227             repo_manager.upload_chart(self)
228         except RepoManagerError as err:
229             raise xAppError( "xApp " + self.chart_name + '-' + self.chart_version + " distribution failed. (Caused by: " + str(err) + ")" , err.status_code)
230
231     def install_chart_package(xapp_chart_name, version, namespace, overridefile):
232         try: 
233           tar = tarfile.open(xapp_chart_name + "-" + version + ".tgz")
234           tar.extractall()
235           tar.close()
236           if overridefile != "":
237             process = subprocess.run(["helm", "install", xapp_chart_name, "./" + xapp_chart_name, "-f", overridefile, "--namespace=" + namespace], stdout=PIPE, stderr=PIPE, check=True)
238           else:
239             process = subprocess.run(["helm", "install", xapp_chart_name, "./" + xapp_chart_name, "--namespace=" + namespace], stdout=PIPE, stderr=PIPE, check=True)
240           status = 1
241         except subprocess.CalledProcessError as err:
242             print(err.stderr.decode())
243             status=0
244         except Exception as err:
245             print(err)
246             status = 0
247         subprocess.run(["rm", "-rf", xapp_chart_name ])
248         subprocess.run(["rm", "-rf", xapp_chart_name + "-" + version + ".tgz" ])
249         return status
250
251     def uninstall_chart_package(xapp_chart_name, namespace):
252
253         try:
254           process = subprocess.run(["helm", "delete", xapp_chart_name, "--namespace=" + namespace], stdout=PIPE, stderr=PIPE, check=True)
255           status = 1
256
257         except Exception as err:
258                 print(err.stderr.decode())
259                 status = 0
260
261         return status
262     def health_check_xapp(xapp_chart_name, namespace):
263        
264         try:
265           getpodname=subprocess.check_output("kubectl get po -n " + namespace + " |  grep -w " +  xapp_chart_name + " | awk '{print $1}'", shell=True).decode().strip("\n")
266           if getpodname=="":
267               print("No " + xapp_chart_name + " xapp found under " + namespace + " namespace.")
268               sys.exit()
269           process = subprocess.check_output("kubectl describe po " + getpodname +  " --namespace=" + namespace + "| grep -B 0 -A 5 'Conditions:'", shell=True).decode()
270
271           final= re.search("Initialized.*", process)
272           temp=final.group().split(' ',1)[1]
273           initialized=" ".join(temp.split())
274           
275           final= re.search("Ready.*", process)
276           temp=final.group().split(' ',1)[1]
277           ready=" ".join(temp.split())
278           
279           final= re.search("ContainersReady.*", process)
280           temp=final.group().split(' ',1)[1]
281           containersready=" ".join(temp.split())
282           
283           final= re.search("PodScheduled.*", process)
284           temp=final.group().split(' ',1)[1]
285           podscheduled=" ".join(temp.split())
286           
287           if "True"==initialized and "True"==podscheduled and "True"==containersready and "True"==ready:
288              print("Xapp health status : Healthy")
289           else:
290              print("Xapp health status : Unhealthy")
291              if "True"!=containersready:
292                print("ContainersReady=False, All the containers in the pod are not ready\n")
293              elif "True"!=initialized:
294                print("Initialized=False, Init containers have not yet started\n")
295              elif "True"!=podscheduled:
296                print("PodScheduled=False, Pod has not yet scheduled to node\n")
297              elif "True"!=ready:
298                print("Ready=False, Pod is not ready to serve any request\n")
299         except Exception as err:
300             print(err.output.decode())