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",
47 Initializes helm connector for helm v3
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
54 :param on_update_db: callback called when k8s connector updates database
58 K8sHelmBaseConnector
.__init
__(
63 kubectl_command
=kubectl_command
,
64 helm_command
=helm_command
,
65 on_update_db
=on_update_db
,
68 self
.log
.info("K8S Helm3 connector initialized")
80 namespace
: str = None,
83 """Install a helm chart
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
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
96 :param kwargs: Additional parameters (None yet)
98 :return: True if successful
100 _
, cluster_id
= self
._get
_namespace
_cluster
_id
(cluster_uuid
)
101 self
.log
.debug("installing {} in cluster {}".format(kdu_model
, cluster_id
))
104 self
.fs
.sync(from_path
=cluster_id
)
107 paths
, env
= self
._init
_paths
_env
(
108 cluster_name
=cluster_id
, create_if_not_exist
=True
111 # for helm3 if namespace does not exist must create it
112 if namespace
and namespace
!= "kube-system":
113 namespaces
= await self
._get
_namespaces
(cluster_id
)
114 if namespace
not in namespaces
:
115 await self
._create
_namespace
(cluster_id
, namespace
)
117 await self
._install
_impl
(
132 self
.fs
.reverse_sync(from_path
=cluster_id
)
134 self
.log
.debug("Returning kdu_instance {}".format(kdu_instance
))
137 async def inspect_kdu(self
, kdu_model
: str, repo_url
: str = None) -> str:
140 "inspect kdu_model {} from (optional) repo: {}".format(kdu_model
, repo_url
)
143 return await self
._exec
_inspect
_comand
(
144 inspect_command
="all", kdu_model
=kdu_model
, repo_url
=repo_url
148 ####################################################################################
149 ################################### P R I V A T E ##################################
150 ####################################################################################
153 def _init_paths_env(self
, cluster_name
: str, create_if_not_exist
: bool = True):
155 Creates and returns base cluster and kube dirs and returns them.
156 Also created helm3 dirs according to new directory specification, paths are
157 returned and also environment variables that must be provided to execute commands
159 Helm 3 directory specification uses XDG categories for variable support:
160 - Cache: $XDG_CACHE_HOME, for example, ${HOME}/.cache/helm/
161 - Configuration: $XDG_CONFIG_HOME, for example, ${HOME}/.config/helm/
162 - Data: $XDG_DATA_HOME, for example ${HOME}/.local/share/helm
164 The variables assigned for this paths are:
165 (In the documentation the variables names are $HELM_PATH_CACHE, $HELM_PATH_CONFIG,
166 $HELM_PATH_DATA but looking and helm env the variable names are different)
167 - Cache: $HELM_CACHE_HOME
168 - Config: $HELM_CONFIG_HOME
169 - Data: $HELM_DATA_HOME
170 - helm kubeconfig: $KUBECONFIG
172 :param cluster_name: cluster_name
173 :return: Dictionary with config_paths and dictionary with helm environment variables
177 if base
.endswith("/") or base
.endswith("\\"):
180 # base dir for cluster
181 cluster_dir
= base
+ "/" + cluster_name
184 kube_dir
= cluster_dir
+ "/" + ".kube"
185 if create_if_not_exist
and not os
.path
.exists(kube_dir
):
186 self
.log
.debug("Creating dir {}".format(kube_dir
))
187 os
.makedirs(kube_dir
)
189 helm_path_cache
= cluster_dir
+ "/.cache/helm"
190 if create_if_not_exist
and not os
.path
.exists(helm_path_cache
):
191 self
.log
.debug("Creating dir {}".format(helm_path_cache
))
192 os
.makedirs(helm_path_cache
)
194 helm_path_config
= cluster_dir
+ "/.config/helm"
195 if create_if_not_exist
and not os
.path
.exists(helm_path_config
):
196 self
.log
.debug("Creating dir {}".format(helm_path_config
))
197 os
.makedirs(helm_path_config
)
199 helm_path_data
= cluster_dir
+ "/.local/share/helm"
200 if create_if_not_exist
and not os
.path
.exists(helm_path_data
):
201 self
.log
.debug("Creating dir {}".format(helm_path_data
))
202 os
.makedirs(helm_path_data
)
204 config_filename
= kube_dir
+ "/config"
206 # 2 - Prepare dictionary with paths
208 "kube_dir": kube_dir
,
209 "kube_config": config_filename
,
210 "cluster_dir": cluster_dir
,
213 # 3 - Prepare environment variables
215 "HELM_CACHE_HOME": helm_path_cache
,
216 "HELM_CONFIG_HOME": helm_path_config
,
217 "HELM_DATA_HOME": helm_path_data
,
218 "KUBECONFIG": config_filename
,
221 for file_name
, file in paths
.items():
222 if "dir" in file_name
and not os
.path
.exists(file):
223 err_msg
= "{} dir does not exist".format(file)
224 self
.log
.error(err_msg
)
225 raise K8sException(err_msg
)
229 async def _get_namespaces(self
, cluster_id
: str):
231 self
.log
.debug("get namespaces cluster_id {}".format(cluster_id
))
234 paths
, env
= self
._init
_paths
_env
(
235 cluster_name
=cluster_id
, create_if_not_exist
=True
238 command
= "{} --kubeconfig={} get namespaces -o=yaml".format(
239 self
.kubectl_command
, paths
["kube_config"]
241 output
, _rc
= await self
._local
_async
_exec
(
242 command
=command
, raise_exception_on_error
=True, env
=env
245 data
= yaml
.load(output
, Loader
=yaml
.SafeLoader
)
246 namespaces
= [item
["metadata"]["name"] for item
in data
["items"]]
247 self
.log
.debug(f
"namespaces {namespaces}")
251 async def _create_namespace(self
, cluster_id
: str, namespace
: str):
253 self
.log
.debug(f
"create namespace: {cluster_id} for cluster_id: {namespace}")
256 paths
, env
= self
._init
_paths
_env
(
257 cluster_name
=cluster_id
, create_if_not_exist
=True
260 command
= "{} --kubeconfig={} create namespace {}".format(
261 self
.kubectl_command
, paths
["kube_config"], namespace
263 _
, _rc
= await self
._local
_async
_exec
(
264 command
=command
, raise_exception_on_error
=True, env
=env
266 self
.log
.debug(f
"namespace {namespace} created")
270 async def _get_services(self
, cluster_id
: str, kdu_instance
: str, namespace
: str):
273 paths
, env
= self
._init
_paths
_env
(
274 cluster_name
=cluster_id
, create_if_not_exist
=True
277 command1
= "{} get manifest {} --namespace={}".format(
278 self
._helm
_command
, kdu_instance
, namespace
280 command2
= "{} get --namespace={} -f -".format(self
.kubectl_command
, namespace
)
281 output
, _rc
= await self
._local
_async
_exec
_pipe
(
282 command1
, command2
, env
=env
, raise_exception_on_error
=True
284 services
= self
._parse
_services
(output
)
288 async def _cluster_init(self
, cluster_id
, namespace
, paths
, env
):
290 Implements the helm version dependent cluster initialization:
291 For helm3 it creates the namespace if it is not created
293 if namespace
!= "kube-system":
294 namespaces
= await self
._get
_namespaces
(cluster_id
)
295 if namespace
not in namespaces
:
296 await self
._create
_namespace
(cluster_id
, namespace
)
298 # If default repo is not included add
299 cluster_uuid
= "{}:{}".format(namespace
, cluster_id
)
300 repo_list
= await self
.repo_list(cluster_uuid
)
301 stable_repo
= [repo
for repo
in repo_list
if repo
["name"] == "stable"]
302 if not stable_repo
and self
._stable
_repo
_url
:
303 await self
.repo_add(cluster_uuid
, "stable", self
._stable
_repo
_url
)
305 # Returns False as no software needs to be uninstalled
308 async def _uninstall_sw(self
, cluster_id
: str, namespace
: str):
309 # nothing to do to uninstall sw
312 async def _instances_list(self
, cluster_id
: str):
315 paths
, env
= self
._init
_paths
_env
(
316 cluster_name
=cluster_id
, create_if_not_exist
=True
319 command
= "{} list --all-namespaces --output yaml".format(self
._helm
_command
)
320 output
, _rc
= await self
._local
_async
_exec
(
321 command
=command
, raise_exception_on_error
=True, env
=env
324 if output
and len(output
) > 0:
325 self
.log
.debug("instances list output: {}".format(output
))
326 return yaml
.load(output
, Loader
=yaml
.SafeLoader
)
330 def _get_inspect_command(
331 self
, inspect_command
: str, kdu_model
: str, repo_str
: str, version
: str
333 inspect_command
= "{} show {} {}{} {}".format(
334 self
._helm
_command
, inspect_command
, kdu_model
, repo_str
, version
336 return inspect_command
338 async def _status_kdu(
342 namespace
: str = None,
343 show_error_log
: bool = False,
344 return_text
: bool = False,
348 "status of kdu_instance: {}, namespace: {} ".format(kdu_instance
, namespace
)
352 namespace
= "kube-system"
355 paths
, env
= self
._init
_paths
_env
(
356 cluster_name
=cluster_id
, create_if_not_exist
=True
358 command
= "{} status {} --namespace={} --output yaml".format(
359 self
._helm
_command
, kdu_instance
, namespace
362 output
, rc
= await self
._local
_async
_exec
(
364 raise_exception_on_error
=True,
365 show_error_log
=show_error_log
,
375 data
= yaml
.load(output
, Loader
=yaml
.SafeLoader
)
377 # remove field 'notes' and manifest
379 del data
.get("info")["notes"]
384 # unable to parse 'resources' as currently it is not included in helm3
387 def _get_install_command(
400 timeout_str
= "--timeout {}s".format(timeout
)
405 atomic_str
= "--atomic"
409 namespace_str
= "--namespace {}".format(namespace
)
414 version_str
= "--version {}".format(version
)
417 "{helm} install {name} {atomic} --output yaml "
418 "{params} {timeout} {ns} {model} {ver}".format(
419 helm
=self
._helm
_command
,
431 def _get_upgrade_command(
444 timeout_str
= "--timeout {}s".format(timeout
)
449 atomic_str
= "--atomic"
454 version_str
= "--version {}".format(version
)
459 namespace_str
= "--namespace {}".format(namespace
)
462 "{helm} upgrade {name} {model} {namespace} {atomic} --output yaml {params} "
463 "{timeout} {ver}".format(
464 helm
=self
._helm
_command
,
466 namespace
=namespace_str
,
476 def _get_rollback_command(
477 self
, kdu_instance
: str, namespace
: str, revision
: float
479 return "{} rollback {} {} --namespace={} --wait".format(
480 self
._helm
_command
, kdu_instance
, revision
, namespace
483 def _get_uninstall_command(self
, kdu_instance
: str, namespace
: str) -> str:
485 return "{} uninstall {} --namespace={}".format(
486 self
._helm
_command
, kdu_instance
, namespace
489 def _get_helm_chart_repos_ids(self
, cluster_uuid
) -> list:
491 cluster_filter
= {"_admin.helm-chart-v3.id": cluster_uuid
}
492 cluster
= self
.db
.get_one("k8sclusters", cluster_filter
)
494 repo_ids
= cluster
.get("_admin").get("helm_chart_repos") or []
498 "k8cluster with helm-id : {} not found".format(cluster_uuid
)