blob: 5293c65381199492a4c9bda0c200325e53ff4a39 [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
bravof7bd5c6a2021-11-17 11:14:57 -0300240 command1 = "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 )
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300243 command2 = "{} get --namespace={} -f -".format(
244 self.kubectl_command, quote(namespace)
245 )
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000246 output, _rc = await self._local_async_exec_pipe(
247 command1, command2, env=env, raise_exception_on_error=True
248 )
249 services = self._parse_services(output)
250
251 return services
252
garciadeblas82b591c2021-03-24 09:22:13 +0100253 async def _cluster_init(
254 self, cluster_id: str, namespace: str, paths: dict, env: dict
255 ):
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000256 """
257 Implements the helm version dependent cluster initialization:
258 For helm2 it initialized tiller environment if needed
259 """
quilesj26c78a42019-10-28 18:10:42 +0100260
261 # check if tiller pod is up in cluster
beierlmf52cb7c2020-04-21 16:36:35 -0400262 command = "{} --kubeconfig={} --namespace={} get deployments".format(
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300263 self.kubectl_command, paths["kube_config"], quote(namespace)
beierlmf52cb7c2020-04-21 16:36:35 -0400264 )
265 output, _rc = await self._local_async_exec(
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000266 command=command, raise_exception_on_error=True, env=env
beierlmf52cb7c2020-04-21 16:36:35 -0400267 )
quilesj26c78a42019-10-28 18:10:42 +0100268
tierno119f7232020-04-21 13:22:26 +0000269 output_table = self._output_to_table(output=output)
quilesj26c78a42019-10-28 18:10:42 +0100270
271 # find 'tiller' pod in all pods
272 already_initialized = False
273 try:
274 for row in output_table:
beierlmf52cb7c2020-04-21 16:36:35 -0400275 if row[0].startswith("tiller-deploy"):
quilesj26c78a42019-10-28 18:10:42 +0100276 already_initialized = True
277 break
beierlmf52cb7c2020-04-21 16:36:35 -0400278 except Exception:
quilesj26c78a42019-10-28 18:10:42 +0100279 pass
280
281 # helm init
282 n2vc_installed_sw = False
283 if not already_initialized:
beierlmf52cb7c2020-04-21 16:36:35 -0400284 self.log.info(
tiernoa5728bf2020-06-25 15:48:52 +0000285 "Initializing helm in client and server: {}".format(cluster_id)
beierlmf52cb7c2020-04-21 16:36:35 -0400286 )
tiernoa5728bf2020-06-25 15:48:52 +0000287 command = "{} --kubeconfig={} --namespace kube-system create serviceaccount {}".format(
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300288 self.kubectl_command, paths["kube_config"], quote(self.service_account)
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000289 )
290 _, _rc = await self._local_async_exec(
291 command=command, raise_exception_on_error=False, env=env
292 )
tiernoa5728bf2020-06-25 15:48:52 +0000293
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000294 command = (
295 "{} --kubeconfig={} create clusterrolebinding osm-tiller-cluster-rule "
296 "--clusterrole=cluster-admin --serviceaccount=kube-system:{}"
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300297 ).format(
298 self.kubectl_command, paths["kube_config"], quote(self.service_account)
299 )
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000300 _, _rc = await self._local_async_exec(
301 command=command, raise_exception_on_error=False, env=env
302 )
tiernoa5728bf2020-06-25 15:48:52 +0000303
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000304 command = (
Gabriel Cuba05ad8322022-07-05 15:07:33 -0500305 "{} init --kubeconfig={} --tiller-namespace={} --home={} --service-account {} "
306 " {}"
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000307 ).format(
308 self._helm_command,
309 paths["kube_config"],
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300310 quote(namespace),
311 quote(paths["helm_dir"]),
312 quote(self.service_account),
313 "--stable-repo-url {}".format(quote(self._stable_repo_url))
David Garcia4395cfa2021-05-28 16:21:51 +0200314 if self._stable_repo_url
315 else "--skip-repos",
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000316 )
317 _, _rc = await self._local_async_exec(
318 command=command, raise_exception_on_error=True, env=env
319 )
quilesj26c78a42019-10-28 18:10:42 +0100320 n2vc_installed_sw = True
321 else:
322 # check client helm installation
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000323 check_file = paths["helm_dir"] + "/repository/repositories.yaml"
324 if not self._check_file_exists(
325 filename=check_file, exception_if_not_exists=False
326 ):
tiernoa5728bf2020-06-25 15:48:52 +0000327 self.log.info("Initializing helm in client: {}".format(cluster_id))
beierlmf52cb7c2020-04-21 16:36:35 -0400328 command = (
Gabriel Cuba05ad8322022-07-05 15:07:33 -0500329 "{} init --kubeconfig={} --tiller-namespace={} "
330 "--home={} --client-only {} "
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000331 ).format(
332 self._helm_command,
333 paths["kube_config"],
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300334 quote(namespace),
335 quote(paths["helm_dir"]),
336 "--stable-repo-url {}".format(quote(self._stable_repo_url))
David Garcia4395cfa2021-05-28 16:21:51 +0200337 if self._stable_repo_url
338 else "--skip-repos",
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000339 )
beierlmf52cb7c2020-04-21 16:36:35 -0400340 output, _rc = await self._local_async_exec(
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000341 command=command, raise_exception_on_error=True, env=env
beierlmf52cb7c2020-04-21 16:36:35 -0400342 )
quilesj26c78a42019-10-28 18:10:42 +0100343 else:
beierlmf52cb7c2020-04-21 16:36:35 -0400344 self.log.info("Helm client already initialized")
quilesj26c78a42019-10-28 18:10:42 +0100345
Pedro Escaleirab41de172022-04-02 00:44:08 +0100346 repo_list = await self.repo_list(cluster_id)
lloretgalleg83e55892020-12-17 12:42:11 +0000347 for repo in repo_list:
348 if repo["name"] == "stable" and repo["url"] != self._stable_repo_url:
349 self.log.debug("Add new stable repo url: {}")
Pedro Escaleirab41de172022-04-02 00:44:08 +0100350 await self.repo_remove(cluster_id, "stable")
David Garcia4395cfa2021-05-28 16:21:51 +0200351 if self._stable_repo_url:
Pedro Escaleirab41de172022-04-02 00:44:08 +0100352 await self.repo_add(cluster_id, "stable", self._stable_repo_url)
lloretgalleg83e55892020-12-17 12:42:11 +0000353 break
354
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000355 return n2vc_installed_sw
lloretgallege308c712020-09-02 09:40:38 +0000356
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000357 async def _uninstall_sw(self, cluster_id: str, namespace: str):
358 # uninstall Tiller if necessary
quilesj26c78a42019-10-28 18:10:42 +0100359
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000360 self.log.debug("Uninstalling tiller from cluster {}".format(cluster_id))
quilesj26c78a42019-10-28 18:10:42 +0100361
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000362 # init paths, env
363 paths, env = self._init_paths_env(
tiernoa5728bf2020-06-25 15:48:52 +0000364 cluster_name=cluster_id, create_if_not_exist=True
beierlmf52cb7c2020-04-21 16:36:35 -0400365 )
quilesj26c78a42019-10-28 18:10:42 +0100366
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000367 if not namespace:
368 # find namespace for tiller pod
369 command = "{} --kubeconfig={} get deployments --all-namespaces".format(
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300370 self.kubectl_command, quote(paths["kube_config"])
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000371 )
372 output, _rc = await self._local_async_exec(
373 command=command, raise_exception_on_error=False, env=env
374 )
375 output_table = self._output_to_table(output=output)
376 namespace = None
377 for r in output_table:
378 try:
379 if "tiller-deploy" in r[1]:
380 namespace = r[0]
381 break
382 except Exception:
383 pass
quilesj26c78a42019-10-28 18:10:42 +0100384 else:
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000385 msg = "Tiller deployment not found in cluster {}".format(cluster_id)
386 self.log.error(msg)
quilesj26c78a42019-10-28 18:10:42 +0100387
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000388 self.log.debug("namespace for tiller: {}".format(namespace))
quilesj26c78a42019-10-28 18:10:42 +0100389
tierno53555f62020-04-07 11:08:16 +0000390 if namespace:
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000391 # uninstall tiller from cluster
392 self.log.debug("Uninstalling tiller from cluster {}".format(cluster_id))
393 command = "{} --kubeconfig={} --home={} reset".format(
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300394 self._helm_command,
395 quote(paths["kube_config"]),
396 quote(paths["helm_dir"]),
beierlmf52cb7c2020-04-21 16:36:35 -0400397 )
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000398 self.log.debug("resetting: {}".format(command))
399 output, _rc = await self._local_async_exec(
400 command=command, raise_exception_on_error=True, env=env
quilesj26c78a42019-10-28 18:10:42 +0100401 )
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000402 # Delete clusterrolebinding and serviceaccount.
403 # Ignore if errors for backward compatibility
404 command = (
405 "{} --kubeconfig={} delete clusterrolebinding.rbac.authorization.k8s."
406 "io/osm-tiller-cluster-rule"
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300407 ).format(self.kubectl_command, quote(paths["kube_config"]))
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000408 output, _rc = await self._local_async_exec(
409 command=command, raise_exception_on_error=False, env=env
quilesj26c78a42019-10-28 18:10:42 +0100410 )
Pedro Escaleirab41de172022-04-02 00:44:08 +0100411 command = (
412 "{} --kubeconfig={} --namespace {} delete serviceaccount/{}".format(
413 self.kubectl_command,
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300414 quote(paths["kube_config"]),
415 quote(namespace),
416 quote(self.service_account),
Pedro Escaleirab41de172022-04-02 00:44:08 +0100417 )
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000418 )
419 output, _rc = await self._local_async_exec(
420 command=command, raise_exception_on_error=False, env=env
421 )
quilesj26c78a42019-10-28 18:10:42 +0100422
423 else:
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000424 self.log.debug("namespace not found")
quilesj26c78a42019-10-28 18:10:42 +0100425
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000426 async def _instances_list(self, cluster_id):
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000427 # init paths, env
428 paths, env = self._init_paths_env(
tiernoa5728bf2020-06-25 15:48:52 +0000429 cluster_name=cluster_id, create_if_not_exist=True
beierlmf52cb7c2020-04-21 16:36:35 -0400430 )
quilesj26c78a42019-10-28 18:10:42 +0100431
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000432 command = "{} list --output yaml".format(self._helm_command)
quilesj26c78a42019-10-28 18:10:42 +0100433
beierlmf52cb7c2020-04-21 16:36:35 -0400434 output, _rc = await self._local_async_exec(
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000435 command=command, raise_exception_on_error=True, env=env
beierlmf52cb7c2020-04-21 16:36:35 -0400436 )
quilesj26c78a42019-10-28 18:10:42 +0100437
438 if output and len(output) > 0:
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000439 # parse yaml and update keys to lower case to unify with helm3
440 instances = yaml.load(output, Loader=yaml.SafeLoader).get("Releases")
441 new_instances = []
442 for instance in instances:
443 new_instance = dict((k.lower(), v) for k, v in instance.items())
444 new_instances.append(new_instance)
445 return new_instances
quilesj26c78a42019-10-28 18:10:42 +0100446 else:
447 return []
448
garciadeblas82b591c2021-03-24 09:22:13 +0100449 def _get_inspect_command(
450 self, show_command: str, kdu_model: str, repo_str: str, version: str
451 ):
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000452 inspect_command = "{} inspect {} {}{} {}".format(
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300453 self._helm_command, show_command, quote(kdu_model), repo_str, version
beierlmf52cb7c2020-04-21 16:36:35 -0400454 )
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000455 return inspect_command
quilesj1be06302019-11-29 11:17:11 +0000456
aktas867418c2021-10-19 18:26:13 +0300457 def _get_get_command(
458 self, get_command: str, kdu_instance: str, namespace: str, kubeconfig: str
459 ):
460 get_command = "env KUBECONFIG={} {} get {} {} --output yaml".format(
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300461 kubeconfig, self._helm_command, get_command, quote(kdu_instance)
aktas867418c2021-10-19 18:26:13 +0300462 )
463 return get_command
464
quilesj26c78a42019-10-28 18:10:42 +0100465 async def _status_kdu(
beierlmf52cb7c2020-04-21 16:36:35 -0400466 self,
tiernoa5728bf2020-06-25 15:48:52 +0000467 cluster_id: str,
beierlmf52cb7c2020-04-21 16:36:35 -0400468 kdu_instance: str,
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000469 namespace: str = None,
Pedro Escaleiraa8980cc2022-04-05 17:32:13 +0100470 yaml_format: bool = False,
beierlmf52cb7c2020-04-21 16:36:35 -0400471 show_error_log: bool = False,
Pedro Escaleiraa8980cc2022-04-05 17:32:13 +0100472 ) -> Union[str, dict]:
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000473 self.log.debug(
474 "status of kdu_instance: {}, namespace: {} ".format(kdu_instance, namespace)
475 )
quilesj26c78a42019-10-28 18:10:42 +0100476
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000477 # init config, env
478 paths, env = self._init_paths_env(
tiernoa5728bf2020-06-25 15:48:52 +0000479 cluster_name=cluster_id, create_if_not_exist=True
beierlmf52cb7c2020-04-21 16:36:35 -0400480 )
bravof7bd5c6a2021-11-17 11:14:57 -0300481 command = ("env KUBECONFIG={} {} status {} --output yaml").format(
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300482 paths["kube_config"], self._helm_command, quote(kdu_instance)
bravof7bd5c6a2021-11-17 11:14:57 -0300483 )
quilesj26c78a42019-10-28 18:10:42 +0100484 output, rc = await self._local_async_exec(
485 command=command,
486 raise_exception_on_error=True,
beierlmf52cb7c2020-04-21 16:36:35 -0400487 show_error_log=show_error_log,
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000488 env=env,
quilesj26c78a42019-10-28 18:10:42 +0100489 )
490
Pedro Escaleiraa8980cc2022-04-05 17:32:13 +0100491 if yaml_format:
quilesj1be06302019-11-29 11:17:11 +0000492 return str(output)
493
quilesj26c78a42019-10-28 18:10:42 +0100494 if rc != 0:
495 return None
496
497 data = yaml.load(output, Loader=yaml.SafeLoader)
498
499 # remove field 'notes'
500 try:
beierlmf52cb7c2020-04-21 16:36:35 -0400501 del data.get("info").get("status")["notes"]
quilesj26c78a42019-10-28 18:10:42 +0100502 except KeyError:
503 pass
504
Pedro Escaleiraed0ff052022-04-03 13:51:46 +0100505 # parse the manifest to a list of dictionaries
506 if "manifest" in data:
507 manifest_str = data.get("manifest")
508 manifest_docs = yaml.load_all(manifest_str, Loader=yaml.SafeLoader)
509
510 data["manifest"] = []
511 for doc in manifest_docs:
512 data["manifest"].append(doc)
513
quilesj26c78a42019-10-28 18:10:42 +0100514 # parse field 'resources'
515 try:
beierlmf52cb7c2020-04-21 16:36:35 -0400516 resources = str(data.get("info").get("status").get("resources"))
quilesj26c78a42019-10-28 18:10:42 +0100517 resource_table = self._output_to_table(resources)
beierlmf52cb7c2020-04-21 16:36:35 -0400518 data.get("info").get("status")["resources"] = resource_table
519 except Exception:
quilesj26c78a42019-10-28 18:10:42 +0100520 pass
521
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000522 # set description to lowercase (unify with helm3)
523 try:
524 data.get("info")["description"] = data.get("info").pop("Description")
525 except KeyError:
526 pass
527
quilesj26c78a42019-10-28 18:10:42 +0100528 return data
529
lloretgalleg095392b2020-11-20 11:28:08 +0000530 def _get_helm_chart_repos_ids(self, cluster_uuid) -> list:
531 repo_ids = []
532 cluster_filter = {"_admin.helm-chart.id": cluster_uuid}
533 cluster = self.db.get_one("k8sclusters", cluster_filter)
534 if cluster:
535 repo_ids = cluster.get("_admin").get("helm_chart_repos") or []
536 return repo_ids
537 else:
538 raise K8sException(
539 "k8cluster with helm-id : {} not found".format(cluster_uuid)
540 )
541
tiernoa5728bf2020-06-25 15:48:52 +0000542 async def _is_install_completed(self, cluster_id: str, kdu_instance: str) -> bool:
bravof7bd5c6a2021-11-17 11:14:57 -0300543 # init config, env
544 paths, env = self._init_paths_env(
545 cluster_name=cluster_id, create_if_not_exist=True
546 )
quilesj26c78a42019-10-28 18:10:42 +0100547
beierlmf52cb7c2020-04-21 16:36:35 -0400548 status = await self._status_kdu(
Pedro Escaleiraa8980cc2022-04-05 17:32:13 +0100549 cluster_id=cluster_id, kdu_instance=kdu_instance, yaml_format=False
beierlmf52cb7c2020-04-21 16:36:35 -0400550 )
quilesj26c78a42019-10-28 18:10:42 +0100551
552 # extract info.status.resources-> str
553 # format:
554 # ==> v1/Deployment
555 # NAME READY UP-TO-DATE AVAILABLE AGE
556 # halting-horse-mongodb 0/1 1 0 0s
557 # halting-petit-mongodb 1/1 1 0 0s
558 # blank line
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000559 resources = K8sHelmBaseConnector._get_deep(
560 status, ("info", "status", "resources")
561 )
quilesj26c78a42019-10-28 18:10:42 +0100562
563 # convert to table
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000564 resources = K8sHelmBaseConnector._output_to_table(resources)
quilesj26c78a42019-10-28 18:10:42 +0100565
566 num_lines = len(resources)
567 index = 0
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000568 ready = True
quilesj26c78a42019-10-28 18:10:42 +0100569 while index < num_lines:
570 try:
571 line1 = resources[index]
572 index += 1
573 # find '==>' in column 0
beierlmf52cb7c2020-04-21 16:36:35 -0400574 if line1[0] == "==>":
quilesj26c78a42019-10-28 18:10:42 +0100575 line2 = resources[index]
576 index += 1
577 # find READY in column 1
beierlmf52cb7c2020-04-21 16:36:35 -0400578 if line2[1] == "READY":
quilesj26c78a42019-10-28 18:10:42 +0100579 # read next lines
580 line3 = resources[index]
581 index += 1
582 while len(line3) > 1 and index < num_lines:
583 ready_value = line3[1]
beierlmf52cb7c2020-04-21 16:36:35 -0400584 parts = ready_value.split(sep="/")
quilesj26c78a42019-10-28 18:10:42 +0100585 current = int(parts[0])
586 total = int(parts[1])
587 if current < total:
beierlmf52cb7c2020-04-21 16:36:35 -0400588 self.log.debug("NOT READY:\n {}".format(line3))
quilesj26c78a42019-10-28 18:10:42 +0100589 ready = False
590 line3 = resources[index]
591 index += 1
592
beierlmf52cb7c2020-04-21 16:36:35 -0400593 except Exception:
quilesj26c78a42019-10-28 18:10:42 +0100594 pass
595
596 return ready
597
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000598 def _get_install_command(
bravof7bd5c6a2021-11-17 11:14:57 -0300599 self,
600 kdu_model,
601 kdu_instance,
602 namespace,
603 params_str,
604 version,
605 atomic,
606 timeout,
607 kubeconfig,
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000608 ) -> str:
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000609 timeout_str = ""
610 if timeout:
611 timeout_str = "--timeout {}".format(timeout)
lloretgallegd99f3f22020-06-29 14:18:30 +0000612
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000613 # atomic
614 atomic_str = ""
615 if atomic:
616 atomic_str = "--atomic"
617 # namespace
618 namespace_str = ""
619 if namespace:
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300620 namespace_str = "--namespace {}".format(quote(namespace))
lloretgallegd99f3f22020-06-29 14:18:30 +0000621
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000622 # version
623 version_str = ""
624 if version:
aktas867418c2021-10-19 18:26:13 +0300625 version_str = "--version {}".format(version)
lloretgallegd99f3f22020-06-29 14:18:30 +0000626
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000627 command = (
bravof7bd5c6a2021-11-17 11:14:57 -0300628 "env KUBECONFIG={kubeconfig} {helm} install {atomic} --output yaml "
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000629 "{params} {timeout} --name={name} {ns} {model} {ver}".format(
bravof7bd5c6a2021-11-17 11:14:57 -0300630 kubeconfig=kubeconfig,
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000631 helm=self._helm_command,
632 atomic=atomic_str,
633 params=params_str,
634 timeout=timeout_str,
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300635 name=quote(kdu_instance),
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000636 ns=namespace_str,
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300637 model=quote(kdu_model),
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000638 ver=version_str,
beierlmf52cb7c2020-04-21 16:36:35 -0400639 )
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000640 )
641 return command
quilesj26c78a42019-10-28 18:10:42 +0100642
aktas867418c2021-10-19 18:26:13 +0300643 def _get_upgrade_scale_command(
644 self,
645 kdu_model: str,
646 kdu_instance: str,
647 namespace: str,
648 scale: int,
649 version: str,
650 atomic: bool,
651 replica_str: str,
652 timeout: float,
653 resource_name: str,
654 kubeconfig: str,
655 ) -> str:
Pedro Escaleira0a2060c2022-07-07 22:18:35 +0100656 """Generates the command to scale a Helm Chart release
aktas867418c2021-10-19 18:26:13 +0300657
Pedro Escaleira0a2060c2022-07-07 22:18:35 +0100658 Args:
659 kdu_model (str): Kdu model name, corresponding to the Helm local location or repository
660 kdu_instance (str): KDU instance, corresponding to the Helm Chart release in question
661 namespace (str): Namespace where this KDU instance is deployed
662 scale (int): Scale count
663 version (str): Constraint with specific version of the Chart to use
664 atomic (bool): If set, upgrade process rolls back changes made in case of failed upgrade.
665 The --wait flag will be set automatically if --atomic is used
666 replica_str (str): The key under resource_name key where the scale count is stored
667 timeout (float): The time, in seconds, to wait
668 resource_name (str): The KDU's resource to scale
669 kubeconfig (str): Kubeconfig file path
aktas867418c2021-10-19 18:26:13 +0300670
Pedro Escaleira0a2060c2022-07-07 22:18:35 +0100671 Returns:
672 str: command to scale a Helm Chart release
673 """
aktas867418c2021-10-19 18:26:13 +0300674
675 # scale
676 if resource_name:
677 scale_dict = {"{}.{}".format(resource_name, replica_str): scale}
678 else:
679 scale_dict = {replica_str: scale}
680
681 scale_str = self._params_to_set_option(scale_dict)
682
Pedro Escaleira0a2060c2022-07-07 22:18:35 +0100683 return self._get_upgrade_command(
684 kdu_model=kdu_model,
685 kdu_instance=kdu_instance,
686 namespace=namespace,
687 params_str=scale_str,
688 version=version,
689 atomic=atomic,
690 timeout=timeout,
aktas867418c2021-10-19 18:26:13 +0300691 kubeconfig=kubeconfig,
692 )
aktas867418c2021-10-19 18:26:13 +0300693
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000694 def _get_upgrade_command(
bravof7bd5c6a2021-11-17 11:14:57 -0300695 self,
696 kdu_model,
697 kdu_instance,
698 namespace,
699 params_str,
700 version,
701 atomic,
702 timeout,
703 kubeconfig,
garciadeblasb2ac7622024-02-08 13:05:27 +0100704 reset_values: bool = False,
705 reuse_values: bool = True,
706 reset_then_reuse_values: bool = False,
Gabriel Cuba085fa8d2022-10-10 12:13:55 -0500707 force: bool = False,
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000708 ) -> str:
Pedro Escaleira0a2060c2022-07-07 22:18:35 +0100709 """Generates the command to upgrade a Helm Chart release
710
711 Args:
712 kdu_model (str): Kdu model name, corresponding to the Helm local location or repository
713 kdu_instance (str): KDU instance, corresponding to the Helm Chart release in question
714 namespace (str): Namespace where this KDU instance is deployed
715 params_str (str): Params used to upgrade the Helm Chart release
716 version (str): Constraint with specific version of the Chart to use
717 atomic (bool): If set, upgrade process rolls back changes made in case of failed upgrade.
718 The --wait flag will be set automatically if --atomic is used
719 timeout (float): The time, in seconds, to wait
720 kubeconfig (str): Kubeconfig file path
garciadeblasb2ac7622024-02-08 13:05:27 +0100721 reset_values(bool): If set, helm resets values instead of reusing previous values.
722 reuse_values(bool): If set, helm reuses previous values.
723 reset_then_reuse_values(bool): If set, helm resets values, then apply the last release's values
Gabriel Cuba085fa8d2022-10-10 12:13:55 -0500724 force (bool): If set, helm forces resource updates through a replacement strategy. This may recreate pods.
Pedro Escaleira0a2060c2022-07-07 22:18:35 +0100725 Returns:
726 str: command to upgrade a Helm Chart release
727 """
quilesj26c78a42019-10-28 18:10:42 +0100728
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000729 timeout_str = ""
730 if timeout:
731 timeout_str = "--timeout {}".format(timeout)
quilesj26c78a42019-10-28 18:10:42 +0100732
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000733 # atomic
734 atomic_str = ""
735 if atomic:
736 atomic_str = "--atomic"
quilesj26c78a42019-10-28 18:10:42 +0100737
Gabriel Cuba085fa8d2022-10-10 12:13:55 -0500738 # force
739 force_str = ""
740 if force:
741 force_str = "--force "
742
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000743 # version
744 version_str = ""
745 if version:
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300746 version_str = "--version {}".format(quote(version))
quilesj26c78a42019-10-28 18:10:42 +0100747
Gabriel Cuba085fa8d2022-10-10 12:13:55 -0500748 # namespace
749 namespace_str = ""
750 if namespace:
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300751 namespace_str = "--namespace {}".format(quote(namespace))
Gabriel Cuba085fa8d2022-10-10 12:13:55 -0500752
garciadeblasb2ac7622024-02-08 13:05:27 +0100753 # reset, reuse or reset_then_reuse values
754 on_values_str = "--reuse-values"
755 if reset_values:
756 on_values_str = "--reset-values"
757 elif reuse_values:
758 on_values_str = "--reuse-values"
759 elif reset_then_reuse_values:
760 on_values_str = "--reset-then-reuse-values"
761
bravof7bd5c6a2021-11-17 11:14:57 -0300762 command = (
Gabriel Cuba085fa8d2022-10-10 12:13:55 -0500763 "env KUBECONFIG={kubeconfig} {helm} upgrade {namespace} {atomic} --output yaml {params} {timeout} {force}"
garciadeblasb2ac7622024-02-08 13:05:27 +0100764 "{on_values} {name} {model} {ver}"
bravof7bd5c6a2021-11-17 11:14:57 -0300765 ).format(
766 kubeconfig=kubeconfig,
garciadeblas82b591c2021-03-24 09:22:13 +0100767 helm=self._helm_command,
Gabriel Cuba085fa8d2022-10-10 12:13:55 -0500768 namespace=namespace_str,
garciadeblas82b591c2021-03-24 09:22:13 +0100769 atomic=atomic_str,
Gabriel Cuba085fa8d2022-10-10 12:13:55 -0500770 force=force_str,
garciadeblas82b591c2021-03-24 09:22:13 +0100771 params=params_str,
772 timeout=timeout_str,
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300773 name=quote(kdu_instance),
garciadeblasb2ac7622024-02-08 13:05:27 +0100774 on_values=on_values_str,
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300775 model=quote(kdu_model),
garciadeblas82b591c2021-03-24 09:22:13 +0100776 ver=version_str,
777 )
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000778 return command
quilesj26c78a42019-10-28 18:10:42 +0100779
bravof7bd5c6a2021-11-17 11:14:57 -0300780 def _get_rollback_command(
781 self, kdu_instance, namespace, revision, kubeconfig
782 ) -> str:
783 return "env KUBECONFIG={} {} rollback {} {} --wait".format(
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300784 kubeconfig, self._helm_command, quote(kdu_instance), revision
lloretgalleg1c83f2e2020-10-22 09:12:35 +0000785 )
quilesj26c78a42019-10-28 18:10:42 +0100786
bravof7bd5c6a2021-11-17 11:14:57 -0300787 def _get_uninstall_command(
788 self, kdu_instance: str, namespace: str, kubeconfig: str
789 ) -> str:
790 return "env KUBECONFIG={} {} delete --purge {}".format(
Pedro Escaleirafebbaa02023-06-27 16:42:41 -0300791 kubeconfig, self._helm_command, quote(kdu_instance)
bravof7bd5c6a2021-11-17 11:14:57 -0300792 )