1 ################################################################################
2 # Copyright (c) 2020 AT&T Intellectual Property. #
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 #
8 # http://www.apache.org/licenses/LICENSE-2.0 #
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 ################################################################################
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__)
38 def indent(text, amount, ch=' '):
40 return ''.join(padding + line for line in text.splitlines(True))
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
51 def __init__(self, config_file, schema_file):
52 self.config_file = config_file
53 self.schema_file = schema_file
56 if 'name' not in self.config_file:
58 if 'xapp_name' not in self.config_file:
60 "xApp chart name not found. (Caused by: config-file.json does not contain xapp_name attribute.)", 500)
62 if 'version' not in self.config_file:
64 "xApp chart version not found. (Caused by: config-file.json does not contain version attribute.)", 500)
66 if isnamepresent == 1:
67 self.chart_name = self.config_file['xapp_name']
69 self.chart_name = self.config_file['name']
70 self.chart_version = self.config_file['version']
71 self.configmap_config_json_file = copy.deepcopy(self.config_file)
72 self.chart_workspace_path = settings.CHART_WORKSPACE_PATH + '/' + self.chart_name + '-' + self.chart_version
73 if os.path.exists(self.chart_workspace_path):
74 shutil.rmtree(self.chart_workspace_path)
75 os.makedirs(self.chart_workspace_path)
76 shutil.copytree(resource_filename( 'xapp_onboarder', 'resources/xapp-std'), self.chart_workspace_path + '/' + self.chart_name)
80 self.helm_client_path = 'helm'
82 process = subprocess.run([self.helm_client_path], stdout=PIPE, stderr=PIPE, check=True)
84 except Exception as err:
87 self.helm_client_path = settings.CHART_WORKSPACE_PATH + '/helm'
89 def download_helm(self):
90 if not os.path.isfile(settings.CHART_WORKSPACE_PATH + '/helm'):
91 log.info("Helm client missing. Trying to download it.")
92 helm_file_name = "helm-v{}-{}-amd64.tar.gz".format(settings.HELM_VERSION, platform.system().lower())
93 helm_download_link = "https://get.helm.sh/" + helm_file_name
97 response = requests_retry_session().get(helm_download_link, timeout=settings.HTTP_TIME_OUT)
98 except Exception as err:
99 error_message = "Download helm client failed. (Caused by: " + str(err)+")"
100 log.error(error_message)
101 raise xAppError(error_message, 500)
103 if response.status_code != 200:
104 error_message = "Download helm chart failed. Helm repo return status code: "+ str(response.status_code) +" "+ response.content.decode("utf-8")
105 log.error(error_message)
106 raise xAppError(error_message, 500)
108 file_stream = io.BytesIO(response.content)
110 with tarfile.open(fileobj=file_stream) as tar:
111 helm_client = tar.extractfile(platform.system().lower() + "-amd64/helm")
112 with open(settings.CHART_WORKSPACE_PATH+'/helm', 'wb') as file:
113 file.write(helm_client.read())
114 st = os.stat(settings.CHART_WORKSPACE_PATH+'/helm')
115 os.chmod(settings.CHART_WORKSPACE_PATH+'/helm', st.st_mode | stat.S_IEXEC)
120 def recursive_convert_config_file(self, node_list=list()):
121 current_node = self.configmap_config_json_file
122 helm_value_path = '.Values'
123 for node in node_list:
124 current_node = current_node.get(node)
125 helm_value_path = helm_value_path + ' ' + "\"" + node + "\""
127 if type(current_node) is not dict:
128 raise TypeError("Recursive write was called on a leaf node.")
130 for item in current_node.keys():
131 if type(current_node.get(item)) is not dict:
132 current_node[item] = '{{ index '+ helm_value_path +' "'+ item + '" | toJson }}'
134 new_node_list = node_list.copy()
135 new_node_list.append(item)
136 self.recursive_convert_config_file(new_node_list)
139 def append_config_to_config_map(self):
140 with open(self.chart_workspace_path + '/' + self.chart_name + '/templates/appconfig.yaml', 'a') as outputfile:
141 self.recursive_convert_config_file()
142 config_file_json_text = json.dumps(self.configmap_config_json_file, indent=4)
143 indented_config_text = indent(config_file_json_text, 4)
144 indented_config_text = re.sub(r"\"{{", '{{', indented_config_text)
145 indented_config_text = re.sub(r"}}\"", '}}', indented_config_text)
146 indented_config_text = re.sub(r"\\", '', indented_config_text)
147 outputfile.write(" config-file.json: |\n")
148 outputfile.write(indented_config_text)
149 outputfile.write("\n schema.json: |\n")
150 schema_json = json.dumps(self.schema_file, indent=4)
151 indented_schema_text = indent(schema_json, 4)
152 outputfile.write(indented_schema_text)
155 # This is a work around for the bronze release to be backward compatible to the previous xapp standard helm template
156 def write_config_and_schema(self):
157 os.makedirs(self.chart_workspace_path + '/' + self.chart_name + '/descriptors')
158 os.makedirs(self.chart_workspace_path + '/' + self.chart_name + '/config')
159 with open(self.chart_workspace_path + '/' + self.chart_name + '/descriptors/schema.json', 'w') as outfile:
160 json.dump(self.schema_file, outfile)
161 with open(self.chart_workspace_path + '/' + self.chart_name + '/config/config-file.json', 'w') as outfile:
162 json.dump(self.config_file, outfile)
166 def add_probes_to_deployment(self):
167 with open(self.chart_workspace_path + '/' + self.chart_name + '/templates/deployment.yaml', 'a') as outputfile:
169 for probes in ['readinessProbe', 'livenessProbe']:
170 if self.configmap_config_json_file.get(probes):
171 probe_definition = self.configmap_config_json_file.get(probes)
172 probe_definition_yaml = yaml.dump(probe_definition, width=1000)
174 print(probe_definition_yaml)
176 indented_probe_definition_yaml = indent(probe_definition_yaml, 12)
177 indented_probe_definition_yaml = re.sub(r" \| toJson", '', indented_probe_definition_yaml)
178 indented_probe_definition_yaml = re.sub(r"'", '', indented_probe_definition_yaml)
179 outputfile.write(" "+probes+":\n")
180 outputfile.write(indented_probe_definition_yaml)
183 def append_config_to_values_yaml(self):
184 with open(self.chart_workspace_path + '/' + self.chart_name + '/values.yaml', 'a') as outputfile:
185 yaml.dump(self.config_file, outputfile, default_flow_style=False)
188 def change_chart_name_version(self):
189 with open(self.chart_workspace_path + '/' + self.chart_name + '/Chart.yaml', 'r') as inputfile:
190 self.chart_yaml = yaml.load(inputfile, Loader=yaml.FullLoader)
191 self.chart_yaml['version'] = self.chart_version
192 self.chart_yaml['name'] = self.chart_name
194 with open(self.chart_workspace_path + '/' + self.chart_name + '/Chart.yaml', 'w') as outputfile:
195 yaml.dump(self.chart_yaml, outputfile, default_flow_style=False)
200 process = subprocess.run([self.helm_client_path, "lint", self.chart_workspace_path + "/" + self.chart_name], stdout=PIPE, stderr=PIPE, check=True)
202 except OSError as err:
204 "xApp " + self.chart_name + '-' + self.chart_version + " helm lint failed. (Caused by: " + str(
206 except subprocess.CalledProcessError as err:
208 "xApp " + self.chart_name + '-' + self.chart_version + " helm lint failed. (Caused by: " +
209 err.stderr.decode("utf-8") + "\n" + err.stdout.decode("utf-8") + ")", 400)
211 def package_chart(self):
212 self.write_config_and_schema()
213 self.append_config_to_config_map()
214 self.append_config_to_values_yaml()
215 self.add_probes_to_deployment()
216 self.change_chart_name_version()
219 process = subprocess.run([self.helm_client_path, "package", self.chart_workspace_path + "/" + self.chart_name, "-d"
220 ,self.chart_workspace_path], stdout=PIPE, stderr=PIPE, check=True)
222 except OSError as err:
223 raise xAppError("xApp "+ self.chart_name+'-'+self.chart_version +" packaging failed. (Caused by: "+str(err) +")", 500)
224 except subprocess.CalledProcessError as err:
226 "xApp " + self.chart_name + '-' + self.chart_version + " packaging failed. (Caused by: " +
227 err.stderr.decode("utf-8") + ")", 500)
231 def distribute_chart(self):
233 repo_manager.upload_chart(self)
234 except RepoManagerError as err:
235 raise xAppError( "xApp " + self.chart_name + '-' + self.chart_version + " distribution failed. (Caused by: " + str(err) + ")" , err.status_code)
237 def install_chart_package(xapp_chart_name, version, namespace, overridefile):
239 tar = tarfile.open(xapp_chart_name + "-" + version + ".tgz")
242 if overridefile != "":
243 process = subprocess.run(["helm", "install", xapp_chart_name, "./" + xapp_chart_name, "-f", overridefile, "--namespace=" + namespace], stdout=PIPE, stderr=PIPE, check=True)
245 process = subprocess.run(["helm", "install", xapp_chart_name, "./" + xapp_chart_name, "--namespace=" + namespace], stdout=PIPE, stderr=PIPE, check=True)
247 except subprocess.CalledProcessError as err:
248 print(err.stderr.decode())
250 except Exception as err:
253 subprocess.run(["rm", "-rf", xapp_chart_name ])
254 subprocess.run(["rm", "-rf", xapp_chart_name + "-" + version + ".tgz" ])
257 def uninstall_chart_package(xapp_chart_name, namespace):
260 process = subprocess.run(["helm", "delete", xapp_chart_name, "--namespace=" + namespace], stdout=PIPE, stderr=PIPE, check=True)
263 except Exception as err:
264 print(err.stderr.decode())
268 def health_check_xapp(xapp_chart_name, namespace):
271 getpodname=subprocess.check_output("kubectl get po -n " + namespace + " | grep -w " + xapp_chart_name + " | awk '{print $1}'", shell=True).decode().strip("\n")
273 print("No " + xapp_chart_name + " xapp found under " + namespace + " namespace.")
275 process = subprocess.check_output("kubectl describe po " + getpodname + " --namespace=" + namespace + "| grep -B 0 -A 5 'Conditions:'", shell=True).decode()
277 final= re.search("Initialized.*", process)
278 temp=final.group().split(' ',1)[1]
279 initialized=" ".join(temp.split())
281 final= re.search("Ready.*", process)
282 temp=final.group().split(' ',1)[1]
283 ready=" ".join(temp.split())
285 final= re.search("ContainersReady.*", process)
286 temp=final.group().split(' ',1)[1]
287 containersready=" ".join(temp.split())
289 final= re.search("PodScheduled.*", process)
290 temp=final.group().split(' ',1)[1]
291 podscheduled=" ".join(temp.split())
293 if "True"==initialized and "True"==podscheduled and "True"==containersready and "True"==ready:
294 print("Xapp health status : Healthy")
296 print("Xapp health status : Unhealthy")
297 if "True"!=containersready:
298 print("ContainersReady=False, All the containers in the pod are not ready\n")
299 elif "True"!=initialized:
300 print("Initialized=False, Init containers have not yet started\n")
301 elif "True"!=podscheduled:
302 print("PodScheduled=False, Pod has not yet scheduled to node\n")
304 print("Ready=False, Pod is not ready to serve any request\n")
305 except Exception as err:
306 print(err.output.decode())