blob: 5e5766a6edda7720539daac1fda24f9b5556b7e4 [file] [log] [blame]
lloretgalleg1c83f2e2020-10-22 09:12:35 +00001##
2# Copyright 2019 Telefonica Investigacion y Desarrollo, S.A.U.
3# This file is part of OSM
4# All Rights Reserved.
5#
6# Licensed under the Apache License, Version 2.0 (the "License");
7# you may not use this file except in compliance with the License.
8# You may obtain a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS,
14# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
15# implied.
16# See the License for the specific language governing permissions and
17# limitations under the License.
18#
19# For those usages not covered by the Apache License, Version 2.0 please
20# contact with: nfvlabs@tid.es
21##
22import os
23import yaml
24
25from n2vc.k8s_helm_base_conn import K8sHelmBaseConnector
26from n2vc.exceptions import K8sException
27
28
29class K8sHelm3Connector(K8sHelmBaseConnector):
30
31 """
32 ####################################################################################
33 ################################### P U B L I C ####################################
34 ####################################################################################
35 """
36
37 def __init__(
garciadeblas82b591c2021-03-24 09:22:13 +010038 self,
39 fs: object,
40 db: object,
41 kubectl_command: str = "/usr/bin/kubectl",
42 helm_command: str = "/usr/bin/helm3",
43 log: object = None,
44 on_update_db=None,
lloretgalleg1c83f2e2020-10-22 09:12:35 +000045 ):
46 """
47 Initializes helm connector for helm v3
48
49 :param fs: file system for kubernetes and helm configuration
50 :param db: database object to write current operation status
51 :param kubectl_command: path to kubectl executable
52 :param helm_command: path to helm executable
53 :param log: logger
54 :param on_update_db: callback called when k8s connector updates database
55 """
56
57 # parent class
garciadeblas82b591c2021-03-24 09:22:13 +010058 K8sHelmBaseConnector.__init__(
59 self,
60 db=db,
61 log=log,
62 fs=fs,
63 kubectl_command=kubectl_command,
64 helm_command=helm_command,
65 on_update_db=on_update_db,
garciadeblas82b591c2021-03-24 09:22:13 +010066 )
lloretgalleg1c83f2e2020-10-22 09:12:35 +000067
68 self.log.info("K8S Helm3 connector initialized")
69
lloretgalleg095392b2020-11-20 11:28:08 +000070 async def install(
garciadeblas82b591c2021-03-24 09:22:13 +010071 self,
72 cluster_uuid: str,
73 kdu_model: str,
74 kdu_instance: str,
75 atomic: bool = True,
76 timeout: float = 300,
77 params: dict = None,
78 db_dict: dict = None,
79 kdu_name: str = None,
80 namespace: str = None,
81 **kwargs,
lloretgalleg095392b2020-11-20 11:28:08 +000082 ):
David Garciaeb8943a2021-04-12 12:07:37 +020083 """Install a helm chart
84
85 :param cluster_uuid str: The UUID of the cluster to install to
86 :param kdu_model str: The name or path of a bundle to install
87 :param kdu_instance: Kdu instance name
88 :param atomic bool: If set, waits until the model is active and resets
89 the cluster on failure.
90 :param timeout int: The time, in seconds, to wait for the install
91 to finish
92 :param params dict: Key-value pairs of instantiation parameters
93 :param kdu_name: Name of the KDU instance to be installed
94 :param namespace: K8s namespace to use for the KDU instance
95
96 :param kwargs: Additional parameters (None yet)
97
98 :return: True if successful
99 """
Pedro Escaleiraeba0ac32022-04-02 00:44:08 +0100100
101 self.log.debug("installing {} in cluster {}".format(kdu_model, cluster_uuid))
lloretgalleg095392b2020-11-20 11:28:08 +0000102
103 # sync local dir
Pedro Escaleiraeba0ac32022-04-02 00:44:08 +0100104 self.fs.sync(from_path=cluster_uuid)
lloretgalleg095392b2020-11-20 11:28:08 +0000105
106 # init env, paths
107 paths, env = self._init_paths_env(
Pedro Escaleiraeba0ac32022-04-02 00:44:08 +0100108 cluster_name=cluster_uuid, create_if_not_exist=True
lloretgalleg095392b2020-11-20 11:28:08 +0000109 )
110
111 # for helm3 if namespace does not exist must create it
112 if namespace and namespace != "kube-system":
Pedro Escaleiraeba0ac32022-04-02 00:44:08 +0100113 if not await self._namespace_exists(cluster_uuid, namespace):
aktas2a3ffde2021-06-24 11:37:11 +0300114 try:
Pedro Escaleiraeba0ac32022-04-02 00:44:08 +0100115 await self._create_namespace(cluster_uuid, namespace)
aktas2a3ffde2021-06-24 11:37:11 +0300116 except Exception as e:
Pedro Escaleiraeba0ac32022-04-02 00:44:08 +0100117 if not await self._namespace_exists(cluster_uuid, namespace):
aktas2a3ffde2021-06-24 11:37:11 +0300118 err_msg = (
119 "namespace {} does not exist in cluster_id {} "
David Garcia4ae527e2021-07-26 16:04:59 +0200120 "error message: ".format(namespace, e)
aktas2a3ffde2021-06-24 11:37:11 +0300121 )
122 self.log.error(err_msg)
123 raise K8sException(err_msg)
lloretgalleg095392b2020-11-20 11:28:08 +0000124
David Garciac4da25c2021-02-23 11:47:29 +0100125 await self._install_impl(
Pedro Escaleiraeba0ac32022-04-02 00:44:08 +0100126 cluster_uuid,
David Garciac4da25c2021-02-23 11:47:29 +0100127 kdu_model,
128 paths,
129 env,
130 kdu_instance,
131 atomic=atomic,
132 timeout=timeout,
133 params=params,
134 db_dict=db_dict,
135 kdu_name=kdu_name,
136 namespace=namespace,
137 )
lloretgalleg095392b2020-11-20 11:28:08 +0000138
139 # sync fs
Pedro Escaleiraeba0ac32022-04-02 00:44:08 +0100140 self.fs.reverse_sync(from_path=cluster_uuid)
lloretgalleg095392b2020-11-20 11:28:08 +0000141
142 self.log.debug("Returning kdu_instance {}".format(kdu_instance))
David Garciac4da25c2021-02-23 11:47:29 +0100143 return True
lloretgalleg095392b2020-11-20 11:28:08 +0000144
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000145 async def inspect_kdu(self, kdu_model: str, repo_url: str = None) -> str:
146
147 self.log.debug(
148 "inspect kdu_model {} from (optional) repo: {}".format(kdu_model, repo_url)
149 )
150
151 return await self._exec_inspect_comand(
152 inspect_command="all", kdu_model=kdu_model, repo_url=repo_url
153 )
154
155 """
156 ####################################################################################
157 ################################### P R I V A T E ##################################
158 ####################################################################################
159 """
160
161 def _init_paths_env(self, cluster_name: str, create_if_not_exist: bool = True):
162 """
163 Creates and returns base cluster and kube dirs and returns them.
164 Also created helm3 dirs according to new directory specification, paths are
165 returned and also environment variables that must be provided to execute commands
166
167 Helm 3 directory specification uses XDG categories for variable support:
168 - Cache: $XDG_CACHE_HOME, for example, ${HOME}/.cache/helm/
169 - Configuration: $XDG_CONFIG_HOME, for example, ${HOME}/.config/helm/
170 - Data: $XDG_DATA_HOME, for example ${HOME}/.local/share/helm
171
172 The variables assigned for this paths are:
173 (In the documentation the variables names are $HELM_PATH_CACHE, $HELM_PATH_CONFIG,
174 $HELM_PATH_DATA but looking and helm env the variable names are different)
175 - Cache: $HELM_CACHE_HOME
176 - Config: $HELM_CONFIG_HOME
177 - Data: $HELM_DATA_HOME
178 - helm kubeconfig: $KUBECONFIG
179
180 :param cluster_name: cluster_name
181 :return: Dictionary with config_paths and dictionary with helm environment variables
182 """
183
184 base = self.fs.path
185 if base.endswith("/") or base.endswith("\\"):
186 base = base[:-1]
187
188 # base dir for cluster
189 cluster_dir = base + "/" + cluster_name
190
191 # kube dir
192 kube_dir = cluster_dir + "/" + ".kube"
193 if create_if_not_exist and not os.path.exists(kube_dir):
194 self.log.debug("Creating dir {}".format(kube_dir))
195 os.makedirs(kube_dir)
196
197 helm_path_cache = cluster_dir + "/.cache/helm"
198 if create_if_not_exist and not os.path.exists(helm_path_cache):
199 self.log.debug("Creating dir {}".format(helm_path_cache))
200 os.makedirs(helm_path_cache)
201
202 helm_path_config = cluster_dir + "/.config/helm"
203 if create_if_not_exist and not os.path.exists(helm_path_config):
204 self.log.debug("Creating dir {}".format(helm_path_config))
205 os.makedirs(helm_path_config)
206
207 helm_path_data = cluster_dir + "/.local/share/helm"
208 if create_if_not_exist and not os.path.exists(helm_path_data):
209 self.log.debug("Creating dir {}".format(helm_path_data))
210 os.makedirs(helm_path_data)
211
212 config_filename = kube_dir + "/config"
213
214 # 2 - Prepare dictionary with paths
215 paths = {
216 "kube_dir": kube_dir,
217 "kube_config": config_filename,
garciadeblas82b591c2021-03-24 09:22:13 +0100218 "cluster_dir": cluster_dir,
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000219 }
220
221 # 3 - Prepare environment variables
222 env = {
223 "HELM_CACHE_HOME": helm_path_cache,
224 "HELM_CONFIG_HOME": helm_path_config,
225 "HELM_DATA_HOME": helm_path_data,
garciadeblas82b591c2021-03-24 09:22:13 +0100226 "KUBECONFIG": config_filename,
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000227 }
228
229 for file_name, file in paths.items():
230 if "dir" in file_name and not os.path.exists(file):
231 err_msg = "{} dir does not exist".format(file)
232 self.log.error(err_msg)
233 raise K8sException(err_msg)
234
235 return paths, env
236
aktas2a3ffde2021-06-24 11:37:11 +0300237 async def _namespace_exists(self, cluster_id, namespace) -> bool:
238 self.log.debug(
239 "checking if namespace {} exists cluster_id {}".format(
240 namespace, cluster_id
241 )
242 )
243 namespaces = await self._get_namespaces(cluster_id)
244 return namespace in namespaces if namespaces else False
245
garciadeblas82b591c2021-03-24 09:22:13 +0100246 async def _get_namespaces(self, cluster_id: str):
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000247
248 self.log.debug("get namespaces cluster_id {}".format(cluster_id))
249
250 # init config, env
251 paths, env = self._init_paths_env(
252 cluster_name=cluster_id, create_if_not_exist=True
253 )
254
255 command = "{} --kubeconfig={} get namespaces -o=yaml".format(
256 self.kubectl_command, paths["kube_config"]
257 )
258 output, _rc = await self._local_async_exec(
259 command=command, raise_exception_on_error=True, env=env
260 )
261
262 data = yaml.load(output, Loader=yaml.SafeLoader)
263 namespaces = [item["metadata"]["name"] for item in data["items"]]
264 self.log.debug(f"namespaces {namespaces}")
265
266 return namespaces
267
garciadeblas82b591c2021-03-24 09:22:13 +0100268 async def _create_namespace(self, cluster_id: str, namespace: str):
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000269
270 self.log.debug(f"create namespace: {cluster_id} for cluster_id: {namespace}")
271
272 # init config, env
273 paths, env = self._init_paths_env(
274 cluster_name=cluster_id, create_if_not_exist=True
275 )
276
277 command = "{} --kubeconfig={} create namespace {}".format(
278 self.kubectl_command, paths["kube_config"], namespace
279 )
280 _, _rc = await self._local_async_exec(
281 command=command, raise_exception_on_error=True, env=env
282 )
283 self.log.debug(f"namespace {namespace} created")
284
285 return _rc
286
bravof53dd7462021-11-17 11:14:57 -0300287 async def _get_services(
288 self, cluster_id: str, kdu_instance: str, namespace: str, kubeconfig: str
289 ):
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000290
291 # init config, env
292 paths, env = self._init_paths_env(
293 cluster_name=cluster_id, create_if_not_exist=True
294 )
295
bravof53dd7462021-11-17 11:14:57 -0300296 command1 = "env KUBECONFIG={} {} get manifest {} --namespace={}".format(
297 kubeconfig, self._helm_command, kdu_instance, namespace
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000298 )
garciadeblas82b591c2021-03-24 09:22:13 +0100299 command2 = "{} get --namespace={} -f -".format(self.kubectl_command, namespace)
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000300 output, _rc = await self._local_async_exec_pipe(
301 command1, command2, env=env, raise_exception_on_error=True
302 )
303 services = self._parse_services(output)
304
305 return services
306
307 async def _cluster_init(self, cluster_id, namespace, paths, env):
308 """
309 Implements the helm version dependent cluster initialization:
310 For helm3 it creates the namespace if it is not created
311 """
312 if namespace != "kube-system":
313 namespaces = await self._get_namespaces(cluster_id)
314 if namespace not in namespaces:
315 await self._create_namespace(cluster_id, namespace)
316
Pedro Escaleiraeba0ac32022-04-02 00:44:08 +0100317 repo_list = await self.repo_list(cluster_id)
David Garcia4395cfa2021-05-28 16:21:51 +0200318 stable_repo = [repo for repo in repo_list if repo["name"] == "stable"]
319 if not stable_repo and self._stable_repo_url:
Pedro Escaleiraeba0ac32022-04-02 00:44:08 +0100320 await self.repo_add(cluster_id, "stable", self._stable_repo_url)
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000321
322 # Returns False as no software needs to be uninstalled
323 return False
324
325 async def _uninstall_sw(self, cluster_id: str, namespace: str):
326 # nothing to do to uninstall sw
327 pass
328
329 async def _instances_list(self, cluster_id: str):
330
331 # init paths, env
332 paths, env = self._init_paths_env(
333 cluster_name=cluster_id, create_if_not_exist=True
334 )
335
garciadeblas82b591c2021-03-24 09:22:13 +0100336 command = "{} list --all-namespaces --output yaml".format(self._helm_command)
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000337 output, _rc = await self._local_async_exec(
338 command=command, raise_exception_on_error=True, env=env
339 )
340
341 if output and len(output) > 0:
342 self.log.debug("instances list output: {}".format(output))
343 return yaml.load(output, Loader=yaml.SafeLoader)
344 else:
345 return []
346
garciadeblas82b591c2021-03-24 09:22:13 +0100347 def _get_inspect_command(
348 self, inspect_command: str, kdu_model: str, repo_str: str, version: str
349 ):
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000350 inspect_command = "{} show {} {}{} {}".format(
351 self._helm_command, inspect_command, kdu_model, repo_str, version
352 )
353 return inspect_command
354
355 async def _status_kdu(
356 self,
357 cluster_id: str,
358 kdu_instance: str,
359 namespace: str = None,
360 show_error_log: bool = False,
361 return_text: bool = False,
362 ):
363
garciadeblas82b591c2021-03-24 09:22:13 +0100364 self.log.debug(
365 "status of kdu_instance: {}, namespace: {} ".format(kdu_instance, namespace)
366 )
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000367
368 if not namespace:
369 namespace = "kube-system"
370
371 # init config, env
372 paths, env = self._init_paths_env(
373 cluster_name=cluster_id, create_if_not_exist=True
374 )
bravof53dd7462021-11-17 11:14:57 -0300375 command = "env KUBECONFIG={} {} status {} --namespace={} --output yaml".format(
376 paths["kube_config"], self._helm_command, kdu_instance, namespace
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000377 )
378
379 output, rc = await self._local_async_exec(
380 command=command,
381 raise_exception_on_error=True,
382 show_error_log=show_error_log,
garciadeblas82b591c2021-03-24 09:22:13 +0100383 env=env,
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000384 )
385
386 if return_text:
387 return str(output)
388
389 if rc != 0:
390 return None
391
392 data = yaml.load(output, Loader=yaml.SafeLoader)
393
394 # remove field 'notes' and manifest
395 try:
396 del data.get("info")["notes"]
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000397 except KeyError:
398 pass
399
Pedro Escaleira2b4f9b22022-04-03 13:51:46 +0100400 # parse the manifest to a list of dictionaries
401 if "manifest" in data:
402 manifest_str = data.get("manifest")
403 manifest_docs = yaml.load_all(manifest_str, Loader=yaml.SafeLoader)
404
405 data["manifest"] = []
406 for doc in manifest_docs:
407 data["manifest"].append(doc)
408
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000409 return data
410
garciadeblas82b591c2021-03-24 09:22:13 +0100411 def _get_install_command(
412 self,
413 kdu_model: str,
414 kdu_instance: str,
415 namespace: str,
416 params_str: str,
417 version: str,
418 atomic: bool,
419 timeout: float,
bravof53dd7462021-11-17 11:14:57 -0300420 kubeconfig: str,
garciadeblas82b591c2021-03-24 09:22:13 +0100421 ) -> str:
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000422
423 timeout_str = ""
424 if timeout:
425 timeout_str = "--timeout {}s".format(timeout)
426
427 # atomic
428 atomic_str = ""
429 if atomic:
430 atomic_str = "--atomic"
431 # namespace
432 namespace_str = ""
433 if namespace:
434 namespace_str = "--namespace {}".format(namespace)
435
436 # version
437 version_str = ""
438 if version:
439 version_str = "--version {}".format(version)
440
441 command = (
bravof53dd7462021-11-17 11:14:57 -0300442 "env KUBECONFIG={kubeconfig} {helm} install {name} {atomic} --output yaml "
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000443 "{params} {timeout} {ns} {model} {ver}".format(
bravof53dd7462021-11-17 11:14:57 -0300444 kubeconfig=kubeconfig,
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000445 helm=self._helm_command,
446 name=kdu_instance,
447 atomic=atomic_str,
448 params=params_str,
449 timeout=timeout_str,
450 ns=namespace_str,
451 model=kdu_model,
452 ver=version_str,
453 )
454 )
455 return command
456
garciadeblas82b591c2021-03-24 09:22:13 +0100457 def _get_upgrade_command(
458 self,
459 kdu_model: str,
460 kdu_instance: str,
461 namespace: str,
462 params_str: str,
463 version: str,
464 atomic: bool,
465 timeout: float,
bravof53dd7462021-11-17 11:14:57 -0300466 kubeconfig: str,
garciadeblas82b591c2021-03-24 09:22:13 +0100467 ) -> str:
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000468
469 timeout_str = ""
470 if timeout:
471 timeout_str = "--timeout {}s".format(timeout)
472
473 # atomic
474 atomic_str = ""
475 if atomic:
476 atomic_str = "--atomic"
477
478 # version
479 version_str = ""
480 if version:
481 version_str = "--version {}".format(version)
482
483 # namespace
484 namespace_str = ""
485 if namespace:
486 namespace_str = "--namespace {}".format(namespace)
487
488 command = (
bravof53dd7462021-11-17 11:14:57 -0300489 "env KUBECONFIG={kubeconfig} {helm} upgrade {name} {model} {namespace} {atomic} "
490 "--output yaml {params} {timeout} {ver}"
491 ).format(
492 kubeconfig=kubeconfig,
493 helm=self._helm_command,
494 name=kdu_instance,
495 namespace=namespace_str,
496 atomic=atomic_str,
497 params=params_str,
498 timeout=timeout_str,
499 model=kdu_model,
500 ver=version_str,
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000501 )
502 return command
503
garciadeblas82b591c2021-03-24 09:22:13 +0100504 def _get_rollback_command(
bravof53dd7462021-11-17 11:14:57 -0300505 self, kdu_instance: str, namespace: str, revision: float, kubeconfig: str
garciadeblas82b591c2021-03-24 09:22:13 +0100506 ) -> str:
bravof53dd7462021-11-17 11:14:57 -0300507 return "env KUBECONFIG={} {} rollback {} {} --namespace={} --wait".format(
508 kubeconfig, self._helm_command, kdu_instance, revision, namespace
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000509 )
510
bravof53dd7462021-11-17 11:14:57 -0300511 def _get_uninstall_command(
512 self, kdu_instance: str, namespace: str, kubeconfig: str
513 ) -> str:
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000514
bravof53dd7462021-11-17 11:14:57 -0300515 return "env KUBECONFIG={} {} uninstall {} --namespace={}".format(
516 kubeconfig, self._helm_command, kdu_instance, namespace
garciadeblas82b591c2021-03-24 09:22:13 +0100517 )
lloretgalleg095392b2020-11-20 11:28:08 +0000518
519 def _get_helm_chart_repos_ids(self, cluster_uuid) -> list:
520 repo_ids = []
521 cluster_filter = {"_admin.helm-chart-v3.id": cluster_uuid}
522 cluster = self.db.get_one("k8sclusters", cluster_filter)
523 if cluster:
524 repo_ids = cluster.get("_admin").get("helm_chart_repos") or []
525 return repo_ids
526 else:
527 raise K8sException(
528 "k8cluster with helm-id : {} not found".format(cluster_uuid)
529 )