blob: 4e5f49e0920661ebbdaeae21c1c07b505aa285d7 [file] [log] [blame]
quilesj26c78a42019-10-28 18:10:42 +01001##
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##
quilesj26c78a42019-10-28 18:10:42 +010022import asyncio
Pedro Escaleiraa8980cc2022-04-05 17:32:13 +010023from typing import Union
Pedro Escaleirafebbaa02023-06-27 16:42:41 -030024from shlex import quote
beierlmf52cb7c2020-04-21 16:36:35 -040025import os
beierlmf52cb7c2020-04-21 16:36:35 -040026import yaml
quilesj26c78a42019-10-28 18:10:42 +010027
lloretgalleg1c83f2e2020-10-22 09:12:35 +000028from n2vc.k8s_helm_base_conn import K8sHelmBaseConnector
29from n2vc.exceptions import K8sException
quilesj26c78a42019-10-28 18:10:42 +010030
lloretgalleg1c83f2e2020-10-22 09:12:35 +000031
32class K8sHelmConnector(K8sHelmBaseConnector):
quilesj26c78a42019-10-28 18:10:42 +010033
34 """
beierlmf52cb7c2020-04-21 16:36:35 -040035 ####################################################################################
36 ################################### P U B L I C ####################################
37 ####################################################################################
quilesj26c78a42019-10-28 18:10:42 +010038 """
39
40 def __init__(
beierlmf52cb7c2020-04-21 16:36:35 -040041 self,
42 fs: object,
43 db: object,
44 kubectl_command: str = "/usr/bin/kubectl",
45 helm_command: str = "/usr/bin/helm",
46 log: object = None,
47 on_update_db=None,
quilesj26c78a42019-10-28 18:10:42 +010048 ):
49 """
lloretgalleg1c83f2e2020-10-22 09:12:35 +000050 Initializes helm connector for helm v2
quilesj26c78a42019-10-28 18:10:42 +010051
52 :param fs: file system for kubernetes and helm configuration
53 :param db: database object to write current operation status
54 :param kubectl_command: path to kubectl executable
55 :param helm_command: path to helm executable
56 :param log: logger
57 :param on_update_db: callback called when k8s connector updates database
58 """
59
60 # parent class
lloretgalleg1c83f2e2020-10-22 09:12:35 +000061 K8sHelmBaseConnector.__init__(
62 self,
63 db=db,
64 log=log,
65 fs=fs,
66 kubectl_command=kubectl_command,
67 helm_command=helm_command,
68 on_update_db=on_update_db,
69 )
quilesj26c78a42019-10-28 18:10:42 +010070
lloretgalleg1c83f2e2020-10-22 09:12:35 +000071 self.log.info("Initializing K8S Helm2 connector")
quilesj26c78a42019-10-28 18:10:42 +010072
quilesj1be06302019-11-29 11:17:11 +000073 # initialize helm client-only
beierlmf52cb7c2020-04-21 16:36:35 -040074 self.log.debug("Initializing helm client-only...")
David Garcia4395cfa2021-05-28 16:21:51 +020075 command = "{} init --client-only {} ".format(
76 self._helm_command,
Pedro Escaleirafebbaa02023-06-27 16:42:41 -030077 "--stable-repo-url {}".format(quote(self._stable_repo_url))
David Garcia4395cfa2021-05-28 16:21:51 +020078 if self._stable_repo_url
79 else "--skip-repos",
garciadeblas82b591c2021-03-24 09:22:13 +010080 )
quilesj1be06302019-11-29 11:17:11 +000081 try:
Mark Beierl714d8872023-05-18 15:08:06 -040082 asyncio.create_task(
beierlmf52cb7c2020-04-21 16:36:35 -040083 self._local_async_exec(command=command, raise_exception_on_error=False)
84 )
quilesj1be06302019-11-29 11:17:11 +000085 except Exception as e:
beierlmf52cb7c2020-04-21 16:36:35 -040086 self.warning(
87 msg="helm init failed (it was already initialized): {}".format(e)
88 )
quilesj1be06302019-11-29 11:17:11 +000089
lloretgalleg1c83f2e2020-10-22 09:12:35 +000090 self.log.info("K8S Helm2 connector initialized")
quilesj26c78a42019-10-28 18:10:42 +010091
lloretgalleg095392b2020-11-20 11:28:08 +000092 async def install(
garciadeblas82b591c2021-03-24 09:22:13 +010093 self,
94 cluster_uuid: str,
95 kdu_model: str,
96 kdu_instance: str,
97 atomic: bool = True,
98 timeout: float = 300,
99 params: dict = None,
100 db_dict: dict = None,
101 kdu_name: str = None,
102 namespace: str = None,
103 **kwargs,
lloretgalleg095392b2020-11-20 11:28:08 +0000104 ):
David Garciaeb8943a2021-04-12 12:07:37 +0200105 """
106 Deploys of a new KDU instance. It would implicitly rely on the `install` call
107 to deploy the Chart/Bundle properly parametrized (in practice, this call would
108 happen before any _initial-config-primitive_of the VNF is called).
109
110 :param cluster_uuid: UUID of a K8s cluster known by OSM
garciadeblas04393192022-06-08 15:39:24 +0200111 :param kdu_model: chart/reference (string), which can be either
David Garciaeb8943a2021-04-12 12:07:37 +0200112 of these options:
113 - a name of chart available via the repos known by OSM
garciadeblas04393192022-06-08 15:39:24 +0200114 (e.g. stable/openldap, stable/openldap:1.2.4)
115 - a path to a packaged chart (e.g. mychart.tgz)
116 - a path to an unpacked chart directory or a URL (e.g. mychart)
David Garciaeb8943a2021-04-12 12:07:37 +0200117 :param kdu_instance: Kdu instance name
118 :param atomic: If set, installation process purges chart/bundle on fail, also
119 will wait until all the K8s objects are active
120 :param timeout: Time in seconds to wait for the install of the chart/bundle
121 (defaults to Helm default timeout: 300s)
122 :param params: dictionary of key-value pairs for instantiation parameters
123 (overriding default values)
124 :param dict db_dict: where to write into database when the status changes.
125 It contains a dict with {collection: <str>, filter: {},
126 path: <str>},
127 e.g. {collection: "nsrs", filter:
128 {_id: <nsd-id>, path: "_admin.deployed.K8S.3"}
129 :param kdu_name: Name of the KDU instance to be installed
130 :param namespace: K8s namespace to use for the KDU instance
131 :param kwargs: Additional parameters (None yet)
132 :return: True if successful
133 """
Pedro Escaleirab41de172022-04-02 00:44:08 +0100134 self.log.debug("installing {} in cluster {}".format(kdu_model, cluster_uuid))
lloretgalleg095392b2020-11-20 11:28:08 +0000135
136 # sync local dir
Pedro Escaleirab41de172022-04-02 00:44:08 +0100137 self.fs.sync(from_path=cluster_uuid)
lloretgalleg095392b2020-11-20 11:28:08 +0000138
139 # init env, paths
140 paths, env = self._init_paths_env(
Pedro Escaleirab41de172022-04-02 00:44:08 +0100141 cluster_name=cluster_uuid, create_if_not_exist=True
lloretgalleg095392b2020-11-20 11:28:08 +0000142 )
143
David Garciac4da25c2021-02-23 11:47:29 +0100144 await self._install_impl(
Pedro Escaleirab41de172022-04-02 00:44:08 +0100145 cluster_uuid,
David Garciac4da25c2021-02-23 11:47:29 +0100146 kdu_model,
147 paths,
148 env,
149 kdu_instance,
150 atomic=atomic,
151 timeout=timeout,
152 params=params,
153 db_dict=db_dict,
154 kdu_name=kdu_name,
155 namespace=namespace,
156 )
lloretgalleg095392b2020-11-20 11:28:08 +0000157
158 # sync fs
Pedro Escaleirab41de172022-04-02 00:44:08 +0100159 self.fs.reverse_sync(from_path=cluster_uuid)
lloretgalleg095392b2020-11-20 11:28:08 +0000160
161 self.log.debug("Returning kdu_instance {}".format(kdu_instance))
David Garciac4da25c2021-02-23 11:47:29 +0100162 return True
lloretgalleg095392b2020-11-20 11:28:08 +0000163
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000164 async def inspect_kdu(self, kdu_model: str, repo_url: str = None) -> str:
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000165 self.log.debug(
166 "inspect kdu_model {} from (optional) repo: {}".format(kdu_model, repo_url)
167 )
168
aktas867418c2021-10-19 18:26:13 +0300169 return await self._exec_inspect_command(
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000170 inspect_command="", kdu_model=kdu_model, repo_url=repo_url
171 )
172
173 """
174 ####################################################################################
175 ################################### P R I V A T E ##################################
176 ####################################################################################
177 """
178
179 def _init_paths_env(self, cluster_name: str, create_if_not_exist: bool = True):
tiernoa5728bf2020-06-25 15:48:52 +0000180 """
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000181 Creates and returns base cluster and kube dirs and returns them.
182 Also created helm3 dirs according to new directory specification, paths are
183 returned and also environment variables that must be provided to execute commands
184
185 Helm 2 directory specification uses helm_home dir:
186
187 The variables assigned for this paths are:
188 - Helm hone: $HELM_HOME
189 - helm kubeconfig: $KUBECONFIG
190
191 :param cluster_name: cluster_name
192 :return: Dictionary with config_paths and dictionary with helm environment variables
tiernoa5728bf2020-06-25 15:48:52 +0000193 """
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000194 base = self.fs.path
195 if base.endswith("/") or base.endswith("\\"):
196 base = base[:-1]
tiernoa5728bf2020-06-25 15:48:52 +0000197
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000198 # base dir for cluster
199 cluster_dir = base + "/" + cluster_name
garciadeblas54771fa2019-12-13 13:39:03 +0100200
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000201 # kube dir
202 kube_dir = cluster_dir + "/" + ".kube"
203 if create_if_not_exist and not os.path.exists(kube_dir):
204 self.log.debug("Creating dir {}".format(kube_dir))
205 os.makedirs(kube_dir)
quilesj26c78a42019-10-28 18:10:42 +0100206
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000207 # helm home dir
208 helm_dir = cluster_dir + "/" + ".helm"
209 if create_if_not_exist and not os.path.exists(helm_dir):
210 self.log.debug("Creating dir {}".format(helm_dir))
211 os.makedirs(helm_dir)
quilesj26c78a42019-10-28 18:10:42 +0100212
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000213 config_filename = kube_dir + "/config"
quilesj26c78a42019-10-28 18:10:42 +0100214
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000215 # 2 - Prepare dictionary with paths
216 paths = {
217 "kube_dir": kube_dir,
218 "kube_config": config_filename,
219 "cluster_dir": cluster_dir,
220 "helm_dir": helm_dir,
221 }
222
223 for file_name, file in paths.items():
224 if "dir" in file_name and not os.path.exists(file):
225 err_msg = "{} dir does not exist".format(file)
226 self.log.error(err_msg)
227 raise K8sException(err_msg)
228
229 # 3 - Prepare environment variables
230 env = {"HELM_HOME": helm_dir, "KUBECONFIG": config_filename}
231
232 return paths, env
233
bravof7bd5c6a2021-11-17 11:14:57 -0300234 async def _get_services(self, cluster_id, kdu_instance, namespace, kubeconfig):
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000235 # init config, env
236 paths, env = self._init_paths_env(
tiernoa5728bf2020-06-25 15:48:52 +0000237 cluster_name=cluster_id, create_if_not_exist=True
beierlmf52cb7c2020-04-21 16:36:35 -0400238 )
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000239
garciadeblasae1d9492025-07-17 18:26:11 +0200240 command = "env KUBECONFIG={} {} get manifest {} ".format(
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300241 kubeconfig, self._helm_command, quote(kdu_instance)
bravof7bd5c6a2021-11-17 11:14:57 -0300242 )
garciadeblasae1d9492025-07-17 18:26:11 +0200243 output, _rc = await self._local_async_exec(
244 command, env=env, raise_exception_on_error=True
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000245 )
246 services = self._parse_services(output)
247
248 return services
249
garciadeblas82b591c2021-03-24 09:22:13 +0100250 async def _cluster_init(
251 self, cluster_id: str, namespace: str, paths: dict, env: dict
252 ):
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000253 """
254 Implements the helm version dependent cluster initialization:
255 For helm2 it initialized tiller environment if needed
256 """
quilesj26c78a42019-10-28 18:10:42 +0100257
258 # check if tiller pod is up in cluster
beierlmf52cb7c2020-04-21 16:36:35 -0400259 command = "{} --kubeconfig={} --namespace={} get deployments".format(
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300260 self.kubectl_command, paths["kube_config"], quote(namespace)
beierlmf52cb7c2020-04-21 16:36:35 -0400261 )
262 output, _rc = await self._local_async_exec(
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000263 command=command, raise_exception_on_error=True, env=env
beierlmf52cb7c2020-04-21 16:36:35 -0400264 )
quilesj26c78a42019-10-28 18:10:42 +0100265
tierno119f7232020-04-21 13:22:26 +0000266 output_table = self._output_to_table(output=output)
quilesj26c78a42019-10-28 18:10:42 +0100267
268 # find 'tiller' pod in all pods
269 already_initialized = False
270 try:
271 for row in output_table:
beierlmf52cb7c2020-04-21 16:36:35 -0400272 if row[0].startswith("tiller-deploy"):
quilesj26c78a42019-10-28 18:10:42 +0100273 already_initialized = True
274 break
beierlmf52cb7c2020-04-21 16:36:35 -0400275 except Exception:
quilesj26c78a42019-10-28 18:10:42 +0100276 pass
277
278 # helm init
279 n2vc_installed_sw = False
280 if not already_initialized:
beierlmf52cb7c2020-04-21 16:36:35 -0400281 self.log.info(
tiernoa5728bf2020-06-25 15:48:52 +0000282 "Initializing helm in client and server: {}".format(cluster_id)
beierlmf52cb7c2020-04-21 16:36:35 -0400283 )
tiernoa5728bf2020-06-25 15:48:52 +0000284 command = "{} --kubeconfig={} --namespace kube-system create serviceaccount {}".format(
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300285 self.kubectl_command, paths["kube_config"], quote(self.service_account)
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000286 )
287 _, _rc = await self._local_async_exec(
288 command=command, raise_exception_on_error=False, env=env
289 )
tiernoa5728bf2020-06-25 15:48:52 +0000290
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000291 command = (
292 "{} --kubeconfig={} create clusterrolebinding osm-tiller-cluster-rule "
293 "--clusterrole=cluster-admin --serviceaccount=kube-system:{}"
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300294 ).format(
295 self.kubectl_command, paths["kube_config"], quote(self.service_account)
296 )
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000297 _, _rc = await self._local_async_exec(
298 command=command, raise_exception_on_error=False, env=env
299 )
tiernoa5728bf2020-06-25 15:48:52 +0000300
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000301 command = (
Gabriel Cuba05ad8322022-07-05 15:07:33 -0500302 "{} init --kubeconfig={} --tiller-namespace={} --home={} --service-account {} "
303 " {}"
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000304 ).format(
305 self._helm_command,
306 paths["kube_config"],
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300307 quote(namespace),
308 quote(paths["helm_dir"]),
309 quote(self.service_account),
310 "--stable-repo-url {}".format(quote(self._stable_repo_url))
David Garcia4395cfa2021-05-28 16:21:51 +0200311 if self._stable_repo_url
312 else "--skip-repos",
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000313 )
314 _, _rc = await self._local_async_exec(
315 command=command, raise_exception_on_error=True, env=env
316 )
quilesj26c78a42019-10-28 18:10:42 +0100317 n2vc_installed_sw = True
318 else:
319 # check client helm installation
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000320 check_file = paths["helm_dir"] + "/repository/repositories.yaml"
321 if not self._check_file_exists(
322 filename=check_file, exception_if_not_exists=False
323 ):
tiernoa5728bf2020-06-25 15:48:52 +0000324 self.log.info("Initializing helm in client: {}".format(cluster_id))
beierlmf52cb7c2020-04-21 16:36:35 -0400325 command = (
Gabriel Cuba05ad8322022-07-05 15:07:33 -0500326 "{} init --kubeconfig={} --tiller-namespace={} "
327 "--home={} --client-only {} "
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000328 ).format(
329 self._helm_command,
330 paths["kube_config"],
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300331 quote(namespace),
332 quote(paths["helm_dir"]),
333 "--stable-repo-url {}".format(quote(self._stable_repo_url))
David Garcia4395cfa2021-05-28 16:21:51 +0200334 if self._stable_repo_url
335 else "--skip-repos",
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000336 )
beierlmf52cb7c2020-04-21 16:36:35 -0400337 output, _rc = await self._local_async_exec(
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000338 command=command, raise_exception_on_error=True, env=env
beierlmf52cb7c2020-04-21 16:36:35 -0400339 )
quilesj26c78a42019-10-28 18:10:42 +0100340 else:
beierlmf52cb7c2020-04-21 16:36:35 -0400341 self.log.info("Helm client already initialized")
quilesj26c78a42019-10-28 18:10:42 +0100342
Pedro Escaleirab41de172022-04-02 00:44:08 +0100343 repo_list = await self.repo_list(cluster_id)
lloretgalleg83e55892020-12-17 12:42:11 +0000344 for repo in repo_list:
345 if repo["name"] == "stable" and repo["url"] != self._stable_repo_url:
346 self.log.debug("Add new stable repo url: {}")
Pedro Escaleirab41de172022-04-02 00:44:08 +0100347 await self.repo_remove(cluster_id, "stable")
David Garcia4395cfa2021-05-28 16:21:51 +0200348 if self._stable_repo_url:
Pedro Escaleirab41de172022-04-02 00:44:08 +0100349 await self.repo_add(cluster_id, "stable", self._stable_repo_url)
lloretgalleg83e55892020-12-17 12:42:11 +0000350 break
351
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000352 return n2vc_installed_sw
lloretgallege308c712020-09-02 09:40:38 +0000353
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000354 async def _uninstall_sw(self, cluster_id: str, namespace: str):
355 # uninstall Tiller if necessary
quilesj26c78a42019-10-28 18:10:42 +0100356
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000357 self.log.debug("Uninstalling tiller from cluster {}".format(cluster_id))
quilesj26c78a42019-10-28 18:10:42 +0100358
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000359 # init paths, env
360 paths, env = self._init_paths_env(
tiernoa5728bf2020-06-25 15:48:52 +0000361 cluster_name=cluster_id, create_if_not_exist=True
beierlmf52cb7c2020-04-21 16:36:35 -0400362 )
quilesj26c78a42019-10-28 18:10:42 +0100363
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000364 if not namespace:
365 # find namespace for tiller pod
366 command = "{} --kubeconfig={} get deployments --all-namespaces".format(
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300367 self.kubectl_command, quote(paths["kube_config"])
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000368 )
369 output, _rc = await self._local_async_exec(
370 command=command, raise_exception_on_error=False, env=env
371 )
372 output_table = self._output_to_table(output=output)
373 namespace = None
374 for r in output_table:
375 try:
376 if "tiller-deploy" in r[1]:
377 namespace = r[0]
378 break
379 except Exception:
380 pass
quilesj26c78a42019-10-28 18:10:42 +0100381 else:
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000382 msg = "Tiller deployment not found in cluster {}".format(cluster_id)
383 self.log.error(msg)
quilesj26c78a42019-10-28 18:10:42 +0100384
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000385 self.log.debug("namespace for tiller: {}".format(namespace))
quilesj26c78a42019-10-28 18:10:42 +0100386
tierno53555f62020-04-07 11:08:16 +0000387 if namespace:
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000388 # uninstall tiller from cluster
389 self.log.debug("Uninstalling tiller from cluster {}".format(cluster_id))
390 command = "{} --kubeconfig={} --home={} reset".format(
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300391 self._helm_command,
392 quote(paths["kube_config"]),
393 quote(paths["helm_dir"]),
beierlmf52cb7c2020-04-21 16:36:35 -0400394 )
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000395 self.log.debug("resetting: {}".format(command))
396 output, _rc = await self._local_async_exec(
397 command=command, raise_exception_on_error=True, env=env
quilesj26c78a42019-10-28 18:10:42 +0100398 )
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000399 # Delete clusterrolebinding and serviceaccount.
400 # Ignore if errors for backward compatibility
401 command = (
402 "{} --kubeconfig={} delete clusterrolebinding.rbac.authorization.k8s."
403 "io/osm-tiller-cluster-rule"
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300404 ).format(self.kubectl_command, quote(paths["kube_config"]))
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000405 output, _rc = await self._local_async_exec(
406 command=command, raise_exception_on_error=False, env=env
quilesj26c78a42019-10-28 18:10:42 +0100407 )
Pedro Escaleirab41de172022-04-02 00:44:08 +0100408 command = (
409 "{} --kubeconfig={} --namespace {} delete serviceaccount/{}".format(
410 self.kubectl_command,
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300411 quote(paths["kube_config"]),
412 quote(namespace),
413 quote(self.service_account),
Pedro Escaleirab41de172022-04-02 00:44:08 +0100414 )
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000415 )
416 output, _rc = await self._local_async_exec(
417 command=command, raise_exception_on_error=False, env=env
418 )
quilesj26c78a42019-10-28 18:10:42 +0100419
420 else:
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000421 self.log.debug("namespace not found")
quilesj26c78a42019-10-28 18:10:42 +0100422
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000423 async def _instances_list(self, cluster_id):
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000424 # init paths, env
425 paths, env = self._init_paths_env(
tiernoa5728bf2020-06-25 15:48:52 +0000426 cluster_name=cluster_id, create_if_not_exist=True
beierlmf52cb7c2020-04-21 16:36:35 -0400427 )
quilesj26c78a42019-10-28 18:10:42 +0100428
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000429 command = "{} list --output yaml".format(self._helm_command)
quilesj26c78a42019-10-28 18:10:42 +0100430
beierlmf52cb7c2020-04-21 16:36:35 -0400431 output, _rc = await self._local_async_exec(
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000432 command=command, raise_exception_on_error=True, env=env
beierlmf52cb7c2020-04-21 16:36:35 -0400433 )
quilesj26c78a42019-10-28 18:10:42 +0100434
435 if output and len(output) > 0:
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000436 # parse yaml and update keys to lower case to unify with helm3
437 instances = yaml.load(output, Loader=yaml.SafeLoader).get("Releases")
438 new_instances = []
439 for instance in instances:
440 new_instance = dict((k.lower(), v) for k, v in instance.items())
441 new_instances.append(new_instance)
442 return new_instances
quilesj26c78a42019-10-28 18:10:42 +0100443 else:
444 return []
445
garciadeblas82b591c2021-03-24 09:22:13 +0100446 def _get_inspect_command(
447 self, show_command: str, kdu_model: str, repo_str: str, version: str
448 ):
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000449 inspect_command = "{} inspect {} {}{} {}".format(
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300450 self._helm_command, show_command, quote(kdu_model), repo_str, version
beierlmf52cb7c2020-04-21 16:36:35 -0400451 )
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000452 return inspect_command
quilesj1be06302019-11-29 11:17:11 +0000453
aktas867418c2021-10-19 18:26:13 +0300454 def _get_get_command(
455 self, get_command: str, kdu_instance: str, namespace: str, kubeconfig: str
456 ):
457 get_command = "env KUBECONFIG={} {} get {} {} --output yaml".format(
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300458 kubeconfig, self._helm_command, get_command, quote(kdu_instance)
aktas867418c2021-10-19 18:26:13 +0300459 )
460 return get_command
461
quilesj26c78a42019-10-28 18:10:42 +0100462 async def _status_kdu(
beierlmf52cb7c2020-04-21 16:36:35 -0400463 self,
tiernoa5728bf2020-06-25 15:48:52 +0000464 cluster_id: str,
beierlmf52cb7c2020-04-21 16:36:35 -0400465 kdu_instance: str,
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000466 namespace: str = None,
Pedro Escaleiraa8980cc2022-04-05 17:32:13 +0100467 yaml_format: bool = False,
beierlmf52cb7c2020-04-21 16:36:35 -0400468 show_error_log: bool = False,
Pedro Escaleiraa8980cc2022-04-05 17:32:13 +0100469 ) -> Union[str, dict]:
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000470 self.log.debug(
471 "status of kdu_instance: {}, namespace: {} ".format(kdu_instance, namespace)
472 )
quilesj26c78a42019-10-28 18:10:42 +0100473
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000474 # init config, env
475 paths, env = self._init_paths_env(
tiernoa5728bf2020-06-25 15:48:52 +0000476 cluster_name=cluster_id, create_if_not_exist=True
beierlmf52cb7c2020-04-21 16:36:35 -0400477 )
bravof7bd5c6a2021-11-17 11:14:57 -0300478 command = ("env KUBECONFIG={} {} status {} --output yaml").format(
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300479 paths["kube_config"], self._helm_command, quote(kdu_instance)
bravof7bd5c6a2021-11-17 11:14:57 -0300480 )
quilesj26c78a42019-10-28 18:10:42 +0100481 output, rc = await self._local_async_exec(
482 command=command,
483 raise_exception_on_error=True,
beierlmf52cb7c2020-04-21 16:36:35 -0400484 show_error_log=show_error_log,
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000485 env=env,
quilesj26c78a42019-10-28 18:10:42 +0100486 )
487
Pedro Escaleiraa8980cc2022-04-05 17:32:13 +0100488 if yaml_format:
quilesj1be06302019-11-29 11:17:11 +0000489 return str(output)
490
quilesj26c78a42019-10-28 18:10:42 +0100491 if rc != 0:
492 return None
493
494 data = yaml.load(output, Loader=yaml.SafeLoader)
495
496 # remove field 'notes'
497 try:
beierlmf52cb7c2020-04-21 16:36:35 -0400498 del data.get("info").get("status")["notes"]
quilesj26c78a42019-10-28 18:10:42 +0100499 except KeyError:
500 pass
501
Pedro Escaleiraed0ff052022-04-03 13:51:46 +0100502 # parse the manifest to a list of dictionaries
503 if "manifest" in data:
504 manifest_str = data.get("manifest")
505 manifest_docs = yaml.load_all(manifest_str, Loader=yaml.SafeLoader)
506
507 data["manifest"] = []
508 for doc in manifest_docs:
509 data["manifest"].append(doc)
510
quilesj26c78a42019-10-28 18:10:42 +0100511 # parse field 'resources'
512 try:
beierlmf52cb7c2020-04-21 16:36:35 -0400513 resources = str(data.get("info").get("status").get("resources"))
quilesj26c78a42019-10-28 18:10:42 +0100514 resource_table = self._output_to_table(resources)
beierlmf52cb7c2020-04-21 16:36:35 -0400515 data.get("info").get("status")["resources"] = resource_table
516 except Exception:
quilesj26c78a42019-10-28 18:10:42 +0100517 pass
518
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000519 # set description to lowercase (unify with helm3)
520 try:
521 data.get("info")["description"] = data.get("info").pop("Description")
522 except KeyError:
523 pass
524
quilesj26c78a42019-10-28 18:10:42 +0100525 return data
526
lloretgalleg095392b2020-11-20 11:28:08 +0000527 def _get_helm_chart_repos_ids(self, cluster_uuid) -> list:
528 repo_ids = []
529 cluster_filter = {"_admin.helm-chart.id": cluster_uuid}
530 cluster = self.db.get_one("k8sclusters", cluster_filter)
531 if cluster:
532 repo_ids = cluster.get("_admin").get("helm_chart_repos") or []
533 return repo_ids
534 else:
535 raise K8sException(
536 "k8cluster with helm-id : {} not found".format(cluster_uuid)
537 )
538
tiernoa5728bf2020-06-25 15:48:52 +0000539 async def _is_install_completed(self, cluster_id: str, kdu_instance: str) -> bool:
bravof7bd5c6a2021-11-17 11:14:57 -0300540 # init config, env
541 paths, env = self._init_paths_env(
542 cluster_name=cluster_id, create_if_not_exist=True
543 )
quilesj26c78a42019-10-28 18:10:42 +0100544
beierlmf52cb7c2020-04-21 16:36:35 -0400545 status = await self._status_kdu(
Pedro Escaleiraa8980cc2022-04-05 17:32:13 +0100546 cluster_id=cluster_id, kdu_instance=kdu_instance, yaml_format=False
beierlmf52cb7c2020-04-21 16:36:35 -0400547 )
quilesj26c78a42019-10-28 18:10:42 +0100548
549 # extract info.status.resources-> str
550 # format:
551 # ==> v1/Deployment
552 # NAME READY UP-TO-DATE AVAILABLE AGE
553 # halting-horse-mongodb 0/1 1 0 0s
554 # halting-petit-mongodb 1/1 1 0 0s
555 # blank line
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000556 resources = K8sHelmBaseConnector._get_deep(
557 status, ("info", "status", "resources")
558 )
quilesj26c78a42019-10-28 18:10:42 +0100559
560 # convert to table
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000561 resources = K8sHelmBaseConnector._output_to_table(resources)
quilesj26c78a42019-10-28 18:10:42 +0100562
563 num_lines = len(resources)
564 index = 0
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000565 ready = True
quilesj26c78a42019-10-28 18:10:42 +0100566 while index < num_lines:
567 try:
568 line1 = resources[index]
569 index += 1
570 # find '==>' in column 0
beierlmf52cb7c2020-04-21 16:36:35 -0400571 if line1[0] == "==>":
quilesj26c78a42019-10-28 18:10:42 +0100572 line2 = resources[index]
573 index += 1
574 # find READY in column 1
beierlmf52cb7c2020-04-21 16:36:35 -0400575 if line2[1] == "READY":
quilesj26c78a42019-10-28 18:10:42 +0100576 # read next lines
577 line3 = resources[index]
578 index += 1
579 while len(line3) > 1 and index < num_lines:
580 ready_value = line3[1]
beierlmf52cb7c2020-04-21 16:36:35 -0400581 parts = ready_value.split(sep="/")
quilesj26c78a42019-10-28 18:10:42 +0100582 current = int(parts[0])
583 total = int(parts[1])
584 if current < total:
beierlmf52cb7c2020-04-21 16:36:35 -0400585 self.log.debug("NOT READY:\n {}".format(line3))
quilesj26c78a42019-10-28 18:10:42 +0100586 ready = False
587 line3 = resources[index]
588 index += 1
589
beierlmf52cb7c2020-04-21 16:36:35 -0400590 except Exception:
quilesj26c78a42019-10-28 18:10:42 +0100591 pass
592
593 return ready
594
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000595 def _get_install_command(
bravof7bd5c6a2021-11-17 11:14:57 -0300596 self,
597 kdu_model,
598 kdu_instance,
599 namespace,
600 params_str,
601 version,
602 atomic,
603 timeout,
604 kubeconfig,
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000605 ) -> str:
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000606 timeout_str = ""
607 if timeout:
608 timeout_str = "--timeout {}".format(timeout)
lloretgallegd99f3f22020-06-29 14:18:30 +0000609
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000610 # atomic
611 atomic_str = ""
612 if atomic:
613 atomic_str = "--atomic"
614 # namespace
615 namespace_str = ""
616 if namespace:
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300617 namespace_str = "--namespace {}".format(quote(namespace))
lloretgallegd99f3f22020-06-29 14:18:30 +0000618
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000619 # version
620 version_str = ""
621 if version:
aktas867418c2021-10-19 18:26:13 +0300622 version_str = "--version {}".format(version)
lloretgallegd99f3f22020-06-29 14:18:30 +0000623
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000624 command = (
bravof7bd5c6a2021-11-17 11:14:57 -0300625 "env KUBECONFIG={kubeconfig} {helm} install {atomic} --output yaml "
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000626 "{params} {timeout} --name={name} {ns} {model} {ver}".format(
bravof7bd5c6a2021-11-17 11:14:57 -0300627 kubeconfig=kubeconfig,
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000628 helm=self._helm_command,
629 atomic=atomic_str,
630 params=params_str,
631 timeout=timeout_str,
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300632 name=quote(kdu_instance),
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000633 ns=namespace_str,
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300634 model=quote(kdu_model),
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000635 ver=version_str,
beierlmf52cb7c2020-04-21 16:36:35 -0400636 )
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000637 )
638 return command
quilesj26c78a42019-10-28 18:10:42 +0100639
aktas867418c2021-10-19 18:26:13 +0300640 def _get_upgrade_scale_command(
641 self,
642 kdu_model: str,
643 kdu_instance: str,
644 namespace: str,
645 scale: int,
646 version: str,
647 atomic: bool,
648 replica_str: str,
649 timeout: float,
650 resource_name: str,
651 kubeconfig: str,
652 ) -> str:
Pedro Escaleira0a2060c2022-07-07 22:18:35 +0100653 """Generates the command to scale a Helm Chart release
aktas867418c2021-10-19 18:26:13 +0300654
Pedro Escaleira0a2060c2022-07-07 22:18:35 +0100655 Args:
656 kdu_model (str): Kdu model name, corresponding to the Helm local location or repository
657 kdu_instance (str): KDU instance, corresponding to the Helm Chart release in question
658 namespace (str): Namespace where this KDU instance is deployed
659 scale (int): Scale count
660 version (str): Constraint with specific version of the Chart to use
661 atomic (bool): If set, upgrade process rolls back changes made in case of failed upgrade.
662 The --wait flag will be set automatically if --atomic is used
663 replica_str (str): The key under resource_name key where the scale count is stored
664 timeout (float): The time, in seconds, to wait
665 resource_name (str): The KDU's resource to scale
666 kubeconfig (str): Kubeconfig file path
aktas867418c2021-10-19 18:26:13 +0300667
Pedro Escaleira0a2060c2022-07-07 22:18:35 +0100668 Returns:
669 str: command to scale a Helm Chart release
670 """
aktas867418c2021-10-19 18:26:13 +0300671
672 # scale
673 if resource_name:
674 scale_dict = {"{}.{}".format(resource_name, replica_str): scale}
675 else:
676 scale_dict = {replica_str: scale}
677
678 scale_str = self._params_to_set_option(scale_dict)
679
Pedro Escaleira0a2060c2022-07-07 22:18:35 +0100680 return self._get_upgrade_command(
681 kdu_model=kdu_model,
682 kdu_instance=kdu_instance,
683 namespace=namespace,
684 params_str=scale_str,
685 version=version,
686 atomic=atomic,
687 timeout=timeout,
aktas867418c2021-10-19 18:26:13 +0300688 kubeconfig=kubeconfig,
689 )
aktas867418c2021-10-19 18:26:13 +0300690
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000691 def _get_upgrade_command(
bravof7bd5c6a2021-11-17 11:14:57 -0300692 self,
693 kdu_model,
694 kdu_instance,
695 namespace,
696 params_str,
697 version,
698 atomic,
699 timeout,
700 kubeconfig,
garciadeblasb2ac7622024-02-08 13:05:27 +0100701 reset_values: bool = False,
702 reuse_values: bool = True,
703 reset_then_reuse_values: bool = False,
Gabriel Cuba085fa8d2022-10-10 12:13:55 -0500704 force: bool = False,
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000705 ) -> str:
Pedro Escaleira0a2060c2022-07-07 22:18:35 +0100706 """Generates the command to upgrade a Helm Chart release
707
708 Args:
709 kdu_model (str): Kdu model name, corresponding to the Helm local location or repository
710 kdu_instance (str): KDU instance, corresponding to the Helm Chart release in question
711 namespace (str): Namespace where this KDU instance is deployed
712 params_str (str): Params used to upgrade the Helm Chart release
713 version (str): Constraint with specific version of the Chart to use
714 atomic (bool): If set, upgrade process rolls back changes made in case of failed upgrade.
715 The --wait flag will be set automatically if --atomic is used
716 timeout (float): The time, in seconds, to wait
717 kubeconfig (str): Kubeconfig file path
garciadeblasb2ac7622024-02-08 13:05:27 +0100718 reset_values(bool): If set, helm resets values instead of reusing previous values.
719 reuse_values(bool): If set, helm reuses previous values.
720 reset_then_reuse_values(bool): If set, helm resets values, then apply the last release's values
Gabriel Cuba085fa8d2022-10-10 12:13:55 -0500721 force (bool): If set, helm forces resource updates through a replacement strategy. This may recreate pods.
Pedro Escaleira0a2060c2022-07-07 22:18:35 +0100722 Returns:
723 str: command to upgrade a Helm Chart release
724 """
quilesj26c78a42019-10-28 18:10:42 +0100725
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000726 timeout_str = ""
727 if timeout:
728 timeout_str = "--timeout {}".format(timeout)
quilesj26c78a42019-10-28 18:10:42 +0100729
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000730 # atomic
731 atomic_str = ""
732 if atomic:
733 atomic_str = "--atomic"
quilesj26c78a42019-10-28 18:10:42 +0100734
Gabriel Cuba085fa8d2022-10-10 12:13:55 -0500735 # force
736 force_str = ""
737 if force:
738 force_str = "--force "
739
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000740 # version
741 version_str = ""
742 if version:
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300743 version_str = "--version {}".format(quote(version))
quilesj26c78a42019-10-28 18:10:42 +0100744
Gabriel Cuba085fa8d2022-10-10 12:13:55 -0500745 # namespace
746 namespace_str = ""
747 if namespace:
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300748 namespace_str = "--namespace {}".format(quote(namespace))
Gabriel Cuba085fa8d2022-10-10 12:13:55 -0500749
garciadeblasb2ac7622024-02-08 13:05:27 +0100750 # reset, reuse or reset_then_reuse values
751 on_values_str = "--reuse-values"
752 if reset_values:
753 on_values_str = "--reset-values"
754 elif reuse_values:
755 on_values_str = "--reuse-values"
756 elif reset_then_reuse_values:
757 on_values_str = "--reset-then-reuse-values"
758
bravof7bd5c6a2021-11-17 11:14:57 -0300759 command = (
Gabriel Cuba085fa8d2022-10-10 12:13:55 -0500760 "env KUBECONFIG={kubeconfig} {helm} upgrade {namespace} {atomic} --output yaml {params} {timeout} {force}"
garciadeblasb2ac7622024-02-08 13:05:27 +0100761 "{on_values} {name} {model} {ver}"
bravof7bd5c6a2021-11-17 11:14:57 -0300762 ).format(
763 kubeconfig=kubeconfig,
garciadeblas82b591c2021-03-24 09:22:13 +0100764 helm=self._helm_command,
Gabriel Cuba085fa8d2022-10-10 12:13:55 -0500765 namespace=namespace_str,
garciadeblas82b591c2021-03-24 09:22:13 +0100766 atomic=atomic_str,
Gabriel Cuba085fa8d2022-10-10 12:13:55 -0500767 force=force_str,
garciadeblas82b591c2021-03-24 09:22:13 +0100768 params=params_str,
769 timeout=timeout_str,
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300770 name=quote(kdu_instance),
garciadeblasb2ac7622024-02-08 13:05:27 +0100771 on_values=on_values_str,
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300772 model=quote(kdu_model),
garciadeblas82b591c2021-03-24 09:22:13 +0100773 ver=version_str,
774 )
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000775 return command
quilesj26c78a42019-10-28 18:10:42 +0100776
bravof7bd5c6a2021-11-17 11:14:57 -0300777 def _get_rollback_command(
778 self, kdu_instance, namespace, revision, kubeconfig
779 ) -> str:
780 return "env KUBECONFIG={} {} rollback {} {} --wait".format(
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300781 kubeconfig, self._helm_command, quote(kdu_instance), revision
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000782 )
quilesj26c78a42019-10-28 18:10:42 +0100783
bravof7bd5c6a2021-11-17 11:14:57 -0300784 def _get_uninstall_command(
785 self, kdu_instance: str, namespace: str, kubeconfig: str
786 ) -> str:
787 return "env KUBECONFIG={} {} delete --purge {}".format(
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300788 kubeconfig, self._helm_command, quote(kdu_instance)
bravof7bd5c6a2021-11-17 11:14:57 -0300789 )