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
__(
64 kubectl_command
=kubectl_command
,
65 helm_command
=helm_command
,
66 on_update_db
=on_update_db
,
67 vca_config
=vca_config
,
70 self
.log
.info("K8S Helm3 connector initialized")
82 namespace
: str = None,
85 """Install a helm chart
87 :param cluster_uuid str: The UUID of the cluster to install to
88 :param kdu_model str: The name or path of a bundle to install
89 :param kdu_instance: Kdu instance name
90 :param atomic bool: If set, waits until the model is active and resets
91 the cluster on failure.
92 :param timeout int: The time, in seconds, to wait for the install
94 :param params dict: Key-value pairs of instantiation parameters
95 :param kdu_name: Name of the KDU instance to be installed
96 :param namespace: K8s namespace to use for the KDU instance
98 :param kwargs: Additional parameters (None yet)
100 :return: True if successful
102 _
, cluster_id
= self
._get
_namespace
_cluster
_id
(cluster_uuid
)
103 self
.log
.debug("installing {} in cluster {}".format(kdu_model
, cluster_id
))
106 self
.fs
.sync(from_path
=cluster_id
)
109 paths
, env
= self
._init
_paths
_env
(
110 cluster_name
=cluster_id
, create_if_not_exist
=True
113 # for helm3 if namespace does not exist must create it
114 if namespace
and namespace
!= "kube-system":
115 namespaces
= await self
._get
_namespaces
(cluster_id
)
116 if namespace
not in namespaces
:
117 await self
._create
_namespace
(cluster_id
, namespace
)
119 await self
._install
_impl
(
134 self
.fs
.reverse_sync(from_path
=cluster_id
)
136 self
.log
.debug("Returning kdu_instance {}".format(kdu_instance
))
139 async def inspect_kdu(self
, kdu_model
: str, repo_url
: str = None) -> str:
142 "inspect kdu_model {} from (optional) repo: {}".format(kdu_model
, repo_url
)
145 return await self
._exec
_inspect
_comand
(
146 inspect_command
="all", kdu_model
=kdu_model
, repo_url
=repo_url
150 ####################################################################################
151 ################################### P R I V A T E ##################################
152 ####################################################################################
155 def _init_paths_env(self
, cluster_name
: str, create_if_not_exist
: bool = True):
157 Creates and returns base cluster and kube dirs and returns them.
158 Also created helm3 dirs according to new directory specification, paths are
159 returned and also environment variables that must be provided to execute commands
161 Helm 3 directory specification uses XDG categories for variable support:
162 - Cache: $XDG_CACHE_HOME, for example, ${HOME}/.cache/helm/
163 - Configuration: $XDG_CONFIG_HOME, for example, ${HOME}/.config/helm/
164 - Data: $XDG_DATA_HOME, for example ${HOME}/.local/share/helm
166 The variables assigned for this paths are:
167 (In the documentation the variables names are $HELM_PATH_CACHE, $HELM_PATH_CONFIG,
168 $HELM_PATH_DATA but looking and helm env the variable names are different)
169 - Cache: $HELM_CACHE_HOME
170 - Config: $HELM_CONFIG_HOME
171 - Data: $HELM_DATA_HOME
172 - helm kubeconfig: $KUBECONFIG
174 :param cluster_name: cluster_name
175 :return: Dictionary with config_paths and dictionary with helm environment variables
179 if base
.endswith("/") or base
.endswith("\\"):
182 # base dir for cluster
183 cluster_dir
= base
+ "/" + cluster_name
186 kube_dir
= cluster_dir
+ "/" + ".kube"
187 if create_if_not_exist
and not os
.path
.exists(kube_dir
):
188 self
.log
.debug("Creating dir {}".format(kube_dir
))
189 os
.makedirs(kube_dir
)
191 helm_path_cache
= cluster_dir
+ "/.cache/helm"
192 if create_if_not_exist
and not os
.path
.exists(helm_path_cache
):
193 self
.log
.debug("Creating dir {}".format(helm_path_cache
))
194 os
.makedirs(helm_path_cache
)
196 helm_path_config
= cluster_dir
+ "/.config/helm"
197 if create_if_not_exist
and not os
.path
.exists(helm_path_config
):
198 self
.log
.debug("Creating dir {}".format(helm_path_config
))
199 os
.makedirs(helm_path_config
)
201 helm_path_data
= cluster_dir
+ "/.local/share/helm"
202 if create_if_not_exist
and not os
.path
.exists(helm_path_data
):
203 self
.log
.debug("Creating dir {}".format(helm_path_data
))
204 os
.makedirs(helm_path_data
)
206 config_filename
= kube_dir
+ "/config"
208 # 2 - Prepare dictionary with paths
210 "kube_dir": kube_dir
,
211 "kube_config": config_filename
,
212 "cluster_dir": cluster_dir
,
215 # 3 - Prepare environment variables
217 "HELM_CACHE_HOME": helm_path_cache
,
218 "HELM_CONFIG_HOME": helm_path_config
,
219 "HELM_DATA_HOME": helm_path_data
,
220 "KUBECONFIG": config_filename
,
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
)
231 async def _get_namespaces(self
, cluster_id
: str):
233 self
.log
.debug("get namespaces cluster_id {}".format(cluster_id
))
236 paths
, env
= self
._init
_paths
_env
(
237 cluster_name
=cluster_id
, create_if_not_exist
=True
240 command
= "{} --kubeconfig={} get namespaces -o=yaml".format(
241 self
.kubectl_command
, paths
["kube_config"]
243 output
, _rc
= await self
._local
_async
_exec
(
244 command
=command
, raise_exception_on_error
=True, env
=env
247 data
= yaml
.load(output
, Loader
=yaml
.SafeLoader
)
248 namespaces
= [item
["metadata"]["name"] for item
in data
["items"]]
249 self
.log
.debug(f
"namespaces {namespaces}")
253 async def _create_namespace(self
, cluster_id
: str, namespace
: str):
255 self
.log
.debug(f
"create namespace: {cluster_id} for cluster_id: {namespace}")
258 paths
, env
= self
._init
_paths
_env
(
259 cluster_name
=cluster_id
, create_if_not_exist
=True
262 command
= "{} --kubeconfig={} create namespace {}".format(
263 self
.kubectl_command
, paths
["kube_config"], namespace
265 _
, _rc
= await self
._local
_async
_exec
(
266 command
=command
, raise_exception_on_error
=True, env
=env
268 self
.log
.debug(f
"namespace {namespace} created")
272 async def _get_services(self
, cluster_id
: str, kdu_instance
: str, namespace
: str):
275 paths
, env
= self
._init
_paths
_env
(
276 cluster_name
=cluster_id
, create_if_not_exist
=True
279 command1
= "{} get manifest {} --namespace={}".format(
280 self
._helm
_command
, kdu_instance
, namespace
282 command2
= "{} get --namespace={} -f -".format(self
.kubectl_command
, namespace
)
283 output
, _rc
= await self
._local
_async
_exec
_pipe
(
284 command1
, command2
, env
=env
, raise_exception_on_error
=True
286 services
= self
._parse
_services
(output
)
290 async def _cluster_init(self
, cluster_id
, namespace
, paths
, env
):
292 Implements the helm version dependent cluster initialization:
293 For helm3 it creates the namespace if it is not created
295 if namespace
!= "kube-system":
296 namespaces
= await self
._get
_namespaces
(cluster_id
)
297 if namespace
not in namespaces
:
298 await self
._create
_namespace
(cluster_id
, namespace
)
300 # If default repo is not included add
301 cluster_uuid
= "{}:{}".format(namespace
, cluster_id
)
302 repo_list
= await self
.repo_list(cluster_uuid
)
303 for repo
in repo_list
:
304 self
.log
.debug("repo")
305 if repo
["name"] == "stable":
306 self
.log
.debug("Default repo already present")
309 await self
.repo_add(cluster_uuid
, "stable", self
._stable
_repo
_url
)
311 # Returns False as no software needs to be uninstalled
314 async def _uninstall_sw(self
, cluster_id
: str, namespace
: str):
315 # nothing to do to uninstall sw
318 async def _instances_list(self
, cluster_id
: str):
321 paths
, env
= self
._init
_paths
_env
(
322 cluster_name
=cluster_id
, create_if_not_exist
=True
325 command
= "{} list --all-namespaces --output yaml".format(self
._helm
_command
)
326 output
, _rc
= await self
._local
_async
_exec
(
327 command
=command
, raise_exception_on_error
=True, env
=env
330 if output
and len(output
) > 0:
331 self
.log
.debug("instances list output: {}".format(output
))
332 return yaml
.load(output
, Loader
=yaml
.SafeLoader
)
336 def _get_inspect_command(
337 self
, inspect_command
: str, kdu_model
: str, repo_str
: str, version
: str
339 inspect_command
= "{} show {} {}{} {}".format(
340 self
._helm
_command
, inspect_command
, kdu_model
, repo_str
, version
342 return inspect_command
344 async def _status_kdu(
348 namespace
: str = None,
349 show_error_log
: bool = False,
350 return_text
: bool = False,
354 "status of kdu_instance: {}, namespace: {} ".format(kdu_instance
, namespace
)
358 namespace
= "kube-system"
361 paths
, env
= self
._init
_paths
_env
(
362 cluster_name
=cluster_id
, create_if_not_exist
=True
364 command
= "{} status {} --namespace={} --output yaml".format(
365 self
._helm
_command
, kdu_instance
, namespace
368 output
, rc
= await self
._local
_async
_exec
(
370 raise_exception_on_error
=True,
371 show_error_log
=show_error_log
,
381 data
= yaml
.load(output
, Loader
=yaml
.SafeLoader
)
383 # remove field 'notes' and manifest
385 del data
.get("info")["notes"]
390 # unable to parse 'resources' as currently it is not included in helm3
393 def _get_install_command(
406 timeout_str
= "--timeout {}s".format(timeout
)
411 atomic_str
= "--atomic"
415 namespace_str
= "--namespace {}".format(namespace
)
420 version_str
= "--version {}".format(version
)
423 "{helm} install {name} {atomic} --output yaml "
424 "{params} {timeout} {ns} {model} {ver}".format(
425 helm
=self
._helm
_command
,
437 def _get_upgrade_command(
450 timeout_str
= "--timeout {}s".format(timeout
)
455 atomic_str
= "--atomic"
460 version_str
= "--version {}".format(version
)
465 namespace_str
= "--namespace {}".format(namespace
)
468 "{helm} upgrade {name} {model} {namespace} {atomic} --output yaml {params} "
469 "{timeout} {ver}".format(
470 helm
=self
._helm
_command
,
472 namespace
=namespace_str
,
482 def _get_rollback_command(
483 self
, kdu_instance
: str, namespace
: str, revision
: float
485 return "{} rollback {} {} --namespace={} --wait".format(
486 self
._helm
_command
, kdu_instance
, revision
, namespace
489 def _get_uninstall_command(self
, kdu_instance
: str, namespace
: str) -> str:
491 return "{} uninstall {} --namespace={}".format(
492 self
._helm
_command
, kdu_instance
, namespace
495 def _get_helm_chart_repos_ids(self
, cluster_uuid
) -> list:
497 cluster_filter
= {"_admin.helm-chart-v3.id": cluster_uuid
}
498 cluster
= self
.db
.get_one("k8sclusters", cluster_filter
)
500 repo_ids
= cluster
.get("_admin").get("helm_chart_repos") or []
504 "k8cluster with helm-id : {} not found".format(cluster_uuid
)