2 # Copyright 2019 Telefonica Investigacion y Desarrollo, S.A.U.
3 # This file is part of OSM
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
10 # http://www.apache.org/licenses/LICENSE-2.0
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
16 # See the License for the specific language governing permissions and
17 # limitations under the License.
19 # For those usages not covered by the Apache License, Version 2.0 please
20 # contact with: nfvlabs@tid.es
25 from n2vc
.k8s_helm_base_conn
import K8sHelmBaseConnector
26 from n2vc
.exceptions
import K8sException
29 class K8sHelm3Connector(K8sHelmBaseConnector
):
32 ####################################################################################
33 ################################### P U B L I C ####################################
34 ####################################################################################
41 kubectl_command
: str = "/usr/bin/kubectl",
42 helm_command
: str = "/usr/bin/helm3",
45 vca_config
: dict = None,
48 Initializes helm connector for helm v3
50 :param fs: file system for kubernetes and helm configuration
51 :param db: database object to write current operation status
52 :param kubectl_command: path to kubectl executable
53 :param helm_command: path to helm executable
55 :param on_update_db: callback called when k8s connector updates database
59 K8sHelmBaseConnector
.__init
__(self
,
63 kubectl_command
=kubectl_command
,
64 helm_command
=helm_command
,
65 on_update_db
=on_update_db
,
66 vca_config
=vca_config
)
68 self
.log
.info("K8S Helm3 connector initialized")
80 namespace
: str = None,
82 _
, cluster_id
= self
._get
_namespace
_cluster
_id
(cluster_uuid
)
83 self
.log
.debug("installing {} in cluster {}".format(kdu_model
, cluster_id
))
86 self
.fs
.sync(from_path
=cluster_id
)
89 paths
, env
= self
._init
_paths
_env
(
90 cluster_name
=cluster_id
, create_if_not_exist
=True
93 # for helm3 if namespace does not exist must create it
94 if namespace
and namespace
!= "kube-system":
95 if not await self
._namespace
_exists
(cluster_id
, namespace
):
97 await self
._create
_namespace
(cluster_id
, namespace
)
98 except Exception as e
:
99 if not await self
._namespace
_exists
(cluster_id
, namespace
):
101 "namespace {} does not exist in cluster_id {} "
102 "error message: ".format(
106 self
.log
.error(err_msg
)
107 raise K8sException(err_msg
)
109 await self
._install
_impl
(
124 self
.fs
.reverse_sync(from_path
=cluster_id
)
126 self
.log
.debug("Returning kdu_instance {}".format(kdu_instance
))
129 async def inspect_kdu(self
, kdu_model
: str, repo_url
: str = None) -> str:
132 "inspect kdu_model {} from (optional) repo: {}".format(kdu_model
, repo_url
)
135 return await self
._exec
_inspect
_comand
(
136 inspect_command
="all", kdu_model
=kdu_model
, repo_url
=repo_url
140 ####################################################################################
141 ################################### P R I V A T E ##################################
142 ####################################################################################
145 def _init_paths_env(self
, cluster_name
: str, create_if_not_exist
: bool = True):
147 Creates and returns base cluster and kube dirs and returns them.
148 Also created helm3 dirs according to new directory specification, paths are
149 returned and also environment variables that must be provided to execute commands
151 Helm 3 directory specification uses XDG categories for variable support:
152 - Cache: $XDG_CACHE_HOME, for example, ${HOME}/.cache/helm/
153 - Configuration: $XDG_CONFIG_HOME, for example, ${HOME}/.config/helm/
154 - Data: $XDG_DATA_HOME, for example ${HOME}/.local/share/helm
156 The variables assigned for this paths are:
157 (In the documentation the variables names are $HELM_PATH_CACHE, $HELM_PATH_CONFIG,
158 $HELM_PATH_DATA but looking and helm env the variable names are different)
159 - Cache: $HELM_CACHE_HOME
160 - Config: $HELM_CONFIG_HOME
161 - Data: $HELM_DATA_HOME
162 - helm kubeconfig: $KUBECONFIG
164 :param cluster_name: cluster_name
165 :return: Dictionary with config_paths and dictionary with helm environment variables
169 if base
.endswith("/") or base
.endswith("\\"):
172 # base dir for cluster
173 cluster_dir
= base
+ "/" + cluster_name
176 kube_dir
= cluster_dir
+ "/" + ".kube"
177 if create_if_not_exist
and not os
.path
.exists(kube_dir
):
178 self
.log
.debug("Creating dir {}".format(kube_dir
))
179 os
.makedirs(kube_dir
)
181 helm_path_cache
= cluster_dir
+ "/.cache/helm"
182 if create_if_not_exist
and not os
.path
.exists(helm_path_cache
):
183 self
.log
.debug("Creating dir {}".format(helm_path_cache
))
184 os
.makedirs(helm_path_cache
)
186 helm_path_config
= cluster_dir
+ "/.config/helm"
187 if create_if_not_exist
and not os
.path
.exists(helm_path_config
):
188 self
.log
.debug("Creating dir {}".format(helm_path_config
))
189 os
.makedirs(helm_path_config
)
191 helm_path_data
= cluster_dir
+ "/.local/share/helm"
192 if create_if_not_exist
and not os
.path
.exists(helm_path_data
):
193 self
.log
.debug("Creating dir {}".format(helm_path_data
))
194 os
.makedirs(helm_path_data
)
196 config_filename
= kube_dir
+ "/config"
198 # 2 - Prepare dictionary with paths
200 "kube_dir": kube_dir
,
201 "kube_config": config_filename
,
202 "cluster_dir": cluster_dir
205 # 3 - Prepare environment variables
207 "HELM_CACHE_HOME": helm_path_cache
,
208 "HELM_CONFIG_HOME": helm_path_config
,
209 "HELM_DATA_HOME": helm_path_data
,
210 "KUBECONFIG": config_filename
213 for file_name
, file in paths
.items():
214 if "dir" in file_name
and not os
.path
.exists(file):
215 err_msg
= "{} dir does not exist".format(file)
216 self
.log
.error(err_msg
)
217 raise K8sException(err_msg
)
221 async def _namespace_exists(self
, cluster_id
, namespace
) -> bool:
223 "checking if namespace {} exists cluster_id {}".format(
224 namespace
, cluster_id
227 namespaces
= await self
._get
_namespaces
(cluster_id
)
228 return namespace
in namespaces
if namespaces
else False
230 async def _get_namespaces(self
, cluster_id
: str):
232 self
.log
.debug("get namespaces cluster_id {}".format(cluster_id
))
235 paths
, env
= self
._init
_paths
_env
(
236 cluster_name
=cluster_id
, create_if_not_exist
=True
239 command
= "{} --kubeconfig={} get namespaces -o=yaml".format(
240 self
.kubectl_command
, paths
["kube_config"]
242 output
, _rc
= await self
._local
_async
_exec
(
243 command
=command
, raise_exception_on_error
=True, env
=env
246 data
= yaml
.load(output
, Loader
=yaml
.SafeLoader
)
247 namespaces
= [item
["metadata"]["name"] for item
in data
["items"]]
248 self
.log
.debug(f
"namespaces {namespaces}")
252 async def _create_namespace(self
,
256 self
.log
.debug(f
"create namespace: {cluster_id} for cluster_id: {namespace}")
259 paths
, env
= self
._init
_paths
_env
(
260 cluster_name
=cluster_id
, create_if_not_exist
=True
263 command
= "{} --kubeconfig={} create namespace {}".format(
264 self
.kubectl_command
, paths
["kube_config"], namespace
266 _
, _rc
= await self
._local
_async
_exec
(
267 command
=command
, raise_exception_on_error
=True, env
=env
269 self
.log
.debug(f
"namespace {namespace} created")
273 async def _get_services(self
, cluster_id
: str, kdu_instance
: str, namespace
: str):
276 paths
, env
= self
._init
_paths
_env
(
277 cluster_name
=cluster_id
, create_if_not_exist
=True
280 command1
= "{} get manifest {} --namespace={}".format(
281 self
._helm
_command
, kdu_instance
, namespace
283 command2
= "{} get --namespace={} -f -".format(
284 self
.kubectl_command
, namespace
286 output
, _rc
= await self
._local
_async
_exec
_pipe
(
287 command1
, command2
, env
=env
, raise_exception_on_error
=True
289 services
= self
._parse
_services
(output
)
293 async def _cluster_init(self
, cluster_id
, namespace
, paths
, env
):
295 Implements the helm version dependent cluster initialization:
296 For helm3 it creates the namespace if it is not created
298 if namespace
!= "kube-system":
299 namespaces
= await self
._get
_namespaces
(cluster_id
)
300 if namespace
not in namespaces
:
301 await self
._create
_namespace
(cluster_id
, namespace
)
303 # If default repo is not included add
304 cluster_uuid
= "{}:{}".format(namespace
, cluster_id
)
305 repo_list
= await self
.repo_list(cluster_uuid
)
306 for repo
in repo_list
:
307 self
.log
.debug("repo")
308 if repo
["name"] == "stable":
309 self
.log
.debug("Default repo already present")
312 await self
.repo_add(cluster_uuid
,
314 self
._stable
_repo
_url
)
316 # Returns False as no software needs to be uninstalled
319 async def _uninstall_sw(self
, cluster_id
: str, namespace
: str):
320 # nothing to do to uninstall sw
323 async def _instances_list(self
, cluster_id
: str):
326 paths
, env
= self
._init
_paths
_env
(
327 cluster_name
=cluster_id
, create_if_not_exist
=True
330 command
= "{} list --all-namespaces --output yaml".format(
333 output
, _rc
= await self
._local
_async
_exec
(
334 command
=command
, raise_exception_on_error
=True, env
=env
337 if output
and len(output
) > 0:
338 self
.log
.debug("instances list output: {}".format(output
))
339 return yaml
.load(output
, Loader
=yaml
.SafeLoader
)
343 def _get_inspect_command(self
, inspect_command
: str, kdu_model
: str, repo_str
: str,
345 inspect_command
= "{} show {} {}{} {}".format(
346 self
._helm
_command
, inspect_command
, kdu_model
, repo_str
, version
348 return inspect_command
350 async def _status_kdu(
354 namespace
: str = None,
355 show_error_log
: bool = False,
356 return_text
: bool = False,
359 self
.log
.debug("status of kdu_instance: {}, namespace: {} ".format(kdu_instance
, namespace
))
362 namespace
= "kube-system"
365 paths
, env
= self
._init
_paths
_env
(
366 cluster_name
=cluster_id
, create_if_not_exist
=True
368 command
= "{} status {} --namespace={} --output yaml".format(
369 self
._helm
_command
, kdu_instance
, namespace
372 output
, rc
= await self
._local
_async
_exec
(
374 raise_exception_on_error
=True,
375 show_error_log
=show_error_log
,
385 data
= yaml
.load(output
, Loader
=yaml
.SafeLoader
)
387 # remove field 'notes' and manifest
389 del data
.get("info")["notes"]
394 # unable to parse 'resources' as currently it is not included in helm3
397 def _get_install_command(self
, kdu_model
: str, kdu_instance
: str, namespace
: str,
398 params_str
: str, version
: str, atomic
: bool, timeout
: float) -> str:
402 timeout_str
= "--timeout {}s".format(timeout
)
407 atomic_str
= "--atomic"
411 namespace_str
= "--namespace {}".format(namespace
)
416 version_str
= "--version {}".format(version
)
419 "{helm} install {name} {atomic} --output yaml "
420 "{params} {timeout} {ns} {model} {ver}".format(
421 helm
=self
._helm
_command
,
433 def _get_upgrade_command(self
, kdu_model
: str, kdu_instance
: str, namespace
: str,
434 params_str
: str, version
: str, atomic
: bool, timeout
: float) -> str:
438 timeout_str
= "--timeout {}s".format(timeout
)
443 atomic_str
= "--atomic"
448 version_str
= "--version {}".format(version
)
453 namespace_str
= "--namespace {}".format(namespace
)
456 "{helm} upgrade {name} {model} {namespace} {atomic} --output yaml {params} "
457 "{timeout} {ver}".format(
458 helm
=self
._helm
_command
,
460 namespace
=namespace_str
,
470 def _get_rollback_command(self
, kdu_instance
: str, namespace
: str, revision
: float) -> str:
471 return "{} rollback {} {} --namespace={} --wait".format(
472 self
._helm
_command
, kdu_instance
, revision
, namespace
475 def _get_uninstall_command(self
, kdu_instance
: str, namespace
: str) -> str:
477 return "{} uninstall {} --namespace={}".format(
478 self
._helm
_command
, kdu_instance
, namespace
)
480 def _get_helm_chart_repos_ids(self
, cluster_uuid
) -> list:
482 cluster_filter
= {"_admin.helm-chart-v3.id": cluster_uuid
}
483 cluster
= self
.db
.get_one("k8sclusters", cluster_filter
)
485 repo_ids
= cluster
.get("_admin").get("helm_chart_repos") or []
489 "k8cluster with helm-id : {} not found".format(cluster_uuid
)