blob: c27449bfcbbe551ba481a635c07c2da422778de3 [file] [log] [blame]
Adam Israeld4ec83b2019-11-07 09:46:59 -05001# Copyright 2019 Canonical Ltd.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
Adam Israel3419aba2020-01-29 09:35:35 -050015import asyncio
Pedro Escaleiraa8980cc2022-04-05 17:32:13 +010016from typing import Union
Adam Israeld4ec83b2019-11-07 09:46:59 -050017import os
beierlmf52cb7c2020-04-21 16:36:35 -040018import uuid
beierlm55ca1c72020-05-05 14:55:19 -040019import yaml
David Garcia667696e2020-09-22 14:52:32 +020020import tempfile
David Garciaf6e9b002020-11-27 15:32:02 +010021import binascii
beierlmf52cb7c2020-04-21 16:36:35 -040022
David Garciaeb8943a2021-04-12 12:07:37 +020023from n2vc.config import EnvironConfig
David Garcia582b9232021-10-26 12:30:44 +020024from n2vc.definitions import RelationEndpoint
David Garciaeb8943a2021-04-12 12:07:37 +020025from n2vc.exceptions import K8sException
beierlmf52cb7c2020-04-21 16:36:35 -040026from n2vc.k8s_conn import K8sConnector
David Garciad8d4b6e2021-06-24 18:47:22 +020027from n2vc.kubectl import Kubectl
David Garcia667696e2020-09-22 14:52:32 +020028from .exceptions import MethodNotImplemented
David Garcia667696e2020-09-22 14:52:32 +020029from n2vc.libjuju import Libjuju
ksaikiranrb816d822021-03-17 12:50:20 +053030from n2vc.utils import obj_to_dict, obj_to_yaml
David Garciaeb8943a2021-04-12 12:07:37 +020031from n2vc.store import MotorStore
32from n2vc.vca.cloud import Cloud
33from n2vc.vca.connection import get_connection
David Garciaf6e9b002020-11-27 15:32:02 +010034
David Garciaf6e9b002020-11-27 15:32:02 +010035
David Garciaf6e9b002020-11-27 15:32:02 +010036RBAC_LABEL_KEY_NAME = "rbac-id"
David Garciaf6e9b002020-11-27 15:32:02 +010037RBAC_STACK_PREFIX = "juju-credential"
beierlmf52cb7c2020-04-21 16:36:35 -040038
David Garciaf6e9b002020-11-27 15:32:02 +010039
40def generate_rbac_id():
41 return binascii.hexlify(os.urandom(4)).decode()
42
43
Adam Israeld4ec83b2019-11-07 09:46:59 -050044class K8sJujuConnector(K8sConnector):
David Garciaeb8943a2021-04-12 12:07:37 +020045 libjuju = None
46
Adam Israeld4ec83b2019-11-07 09:46:59 -050047 def __init__(
beierlmf52cb7c2020-04-21 16:36:35 -040048 self,
49 fs: object,
50 db: object,
51 kubectl_command: str = "/usr/bin/kubectl",
52 juju_command: str = "/usr/bin/juju",
53 log: object = None,
David Garciaa0620742020-10-16 13:00:18 +020054 loop: object = None,
beierlmf52cb7c2020-04-21 16:36:35 -040055 on_update_db=None,
Adam Israeld4ec83b2019-11-07 09:46:59 -050056 ):
57 """
David Garcia667696e2020-09-22 14:52:32 +020058 :param fs: file system for kubernetes and helm configuration
59 :param db: Database object
Adam Israeld4ec83b2019-11-07 09:46:59 -050060 :param kubectl_command: path to kubectl executable
61 :param helm_command: path to helm executable
Adam Israeld4ec83b2019-11-07 09:46:59 -050062 :param log: logger
David Garcia667696e2020-09-22 14:52:32 +020063 :param: loop: Asyncio loop
Adam Israeld4ec83b2019-11-07 09:46:59 -050064 """
65
66 # parent class
67 K8sConnector.__init__(
David Garcia667696e2020-09-22 14:52:32 +020068 self,
69 db,
70 log=log,
71 on_update_db=on_update_db,
Adam Israeld4ec83b2019-11-07 09:46:59 -050072 )
73
Adam Israeleef68932019-11-28 16:27:46 -050074 self.fs = fs
David Garcia667696e2020-09-22 14:52:32 +020075 self.loop = loop or asyncio.get_event_loop()
beierlmf52cb7c2020-04-21 16:36:35 -040076 self.log.debug("Initializing K8S Juju connector")
Adam Israeld4ec83b2019-11-07 09:46:59 -050077
David Garciaeb8943a2021-04-12 12:07:37 +020078 db_uri = EnvironConfig(prefixes=["OSMLCM_", "OSMMON_"]).get("database_uri")
79 self._store = MotorStore(db_uri)
80 self.loading_libjuju = asyncio.Lock(loop=self.loop)
Adam Israeleef68932019-11-28 16:27:46 -050081
beierlmf52cb7c2020-04-21 16:36:35 -040082 self.log.debug("K8S Juju connector initialized")
David Garcia4f74f592020-07-23 15:04:19 +020083 # TODO: Remove these commented lines:
84 # self.authenticated = False
85 # self.models = {}
86 # self.juju_secret = ""
Adam Israeld4ec83b2019-11-07 09:46:59 -050087
88 """Initialization"""
beierlmf52cb7c2020-04-21 16:36:35 -040089
Adam Israeld4ec83b2019-11-07 09:46:59 -050090 async def init_env(
91 self,
Adam Israeleef68932019-11-28 16:27:46 -050092 k8s_creds: str,
beierlmf52cb7c2020-04-21 16:36:35 -040093 namespace: str = "kube-system",
Adam Israeld4ec83b2019-11-07 09:46:59 -050094 reuse_cluster_uuid: str = None,
David Garciaeb8943a2021-04-12 12:07:37 +020095 **kwargs,
Adam Israeleef68932019-11-28 16:27:46 -050096 ) -> (str, bool):
garciadeblas54771fa2019-12-13 13:39:03 +010097 """
98 It prepares a given K8s cluster environment to run Juju bundles.
Adam Israeld4ec83b2019-11-07 09:46:59 -050099
beierlmf52cb7c2020-04-21 16:36:35 -0400100 :param k8s_creds: credentials to access a given K8s cluster, i.e. a valid
101 '.kube/config'
102 :param namespace: optional namespace to be used for juju. By default,
103 'kube-system' will be used
garciadeblas54771fa2019-12-13 13:39:03 +0100104 :param reuse_cluster_uuid: existing cluster uuid for reuse
David Garciaeb8943a2021-04-12 12:07:37 +0200105 :param: kwargs: Additional parameters
106 vca_id (str): VCA ID
107
beierlmf52cb7c2020-04-21 16:36:35 -0400108 :return: uuid of the K8s cluster and True if connector has installed some
109 software in the cluster
110 (on error, an exception will be raised)
Adam Israeld4ec83b2019-11-07 09:46:59 -0500111 """
David Garciaeb8943a2021-04-12 12:07:37 +0200112 libjuju = await self._get_libjuju(kwargs.get("vca_id"))
Adam Israeld4ec83b2019-11-07 09:46:59 -0500113
David Garcia37004982020-07-16 17:53:20 +0200114 cluster_uuid = reuse_cluster_uuid or str(uuid.uuid4())
David Garciad8d4b6e2021-06-24 18:47:22 +0200115 kubectl = self._get_kubectl(k8s_creds)
Adam Israeld4ec83b2019-11-07 09:46:59 -0500116
David Garciaf6e9b002020-11-27 15:32:02 +0100117 # CREATING RESOURCES IN K8S
118 rbac_id = generate_rbac_id()
119 metadata_name = "{}-{}".format(RBAC_STACK_PREFIX, rbac_id)
120 labels = {RBAC_STACK_PREFIX: rbac_id}
David Garcia4f74f592020-07-23 15:04:19 +0200121
David Garciaf6e9b002020-11-27 15:32:02 +0100122 # Create cleanup dictionary to clean up created resources
123 # if it fails in the middle of the process
124 cleanup_data = []
125 try:
garciadeblas47f65382021-05-31 15:49:15 +0200126 self.log.debug("Initializing K8s cluster for juju")
David Garciad8d4b6e2021-06-24 18:47:22 +0200127 kubectl.create_cluster_role(
David Garciaf6e9b002020-11-27 15:32:02 +0100128 name=metadata_name,
129 labels=labels,
130 )
garciadeblas47f65382021-05-31 15:49:15 +0200131 self.log.debug("Cluster role created")
David Garciaf6e9b002020-11-27 15:32:02 +0100132 cleanup_data.append(
133 {
David Garciad8d4b6e2021-06-24 18:47:22 +0200134 "delete": kubectl.delete_cluster_role,
Pedro Escaleira15667792022-03-17 23:59:29 +0000135 "args": (metadata_name,),
David Garciaf6e9b002020-11-27 15:32:02 +0100136 }
137 )
Adam Israeld4ec83b2019-11-07 09:46:59 -0500138
David Garciad8d4b6e2021-06-24 18:47:22 +0200139 kubectl.create_service_account(
David Garciaf6e9b002020-11-27 15:32:02 +0100140 name=metadata_name,
141 labels=labels,
142 )
garciadeblas47f65382021-05-31 15:49:15 +0200143 self.log.debug("Service account created")
David Garciaf6e9b002020-11-27 15:32:02 +0100144 cleanup_data.append(
145 {
David Garciad8d4b6e2021-06-24 18:47:22 +0200146 "delete": kubectl.delete_service_account,
Pedro Escaleira15667792022-03-17 23:59:29 +0000147 "args": (metadata_name,),
David Garciaf6e9b002020-11-27 15:32:02 +0100148 }
149 )
Adam Israeld4ec83b2019-11-07 09:46:59 -0500150
David Garciad8d4b6e2021-06-24 18:47:22 +0200151 kubectl.create_cluster_role_binding(
David Garciaf6e9b002020-11-27 15:32:02 +0100152 name=metadata_name,
153 labels=labels,
154 )
garciadeblas47f65382021-05-31 15:49:15 +0200155 self.log.debug("Role binding created")
David Garciaf6e9b002020-11-27 15:32:02 +0100156 cleanup_data.append(
157 {
David Garciad8d4b6e2021-06-24 18:47:22 +0200158 "delete": kubectl.delete_service_account,
Pedro Escaleira15667792022-03-17 23:59:29 +0000159 "args": (metadata_name,),
David Garciaf6e9b002020-11-27 15:32:02 +0100160 }
161 )
David Garciad8d4b6e2021-06-24 18:47:22 +0200162 token, client_cert_data = await kubectl.get_secret_data(
David Garciaf6e9b002020-11-27 15:32:02 +0100163 metadata_name,
164 )
Adam Israeld4ec83b2019-11-07 09:46:59 -0500165
David Garciaf6e9b002020-11-27 15:32:02 +0100166 default_storage_class = kubectl.get_default_storage_class()
garciadeblas47f65382021-05-31 15:49:15 +0200167 self.log.debug("Default storage class: {}".format(default_storage_class))
David Garciaeb8943a2021-04-12 12:07:37 +0200168 await libjuju.add_k8s(
David Garciaf6e9b002020-11-27 15:32:02 +0100169 name=cluster_uuid,
170 rbac_id=rbac_id,
171 token=token,
172 client_cert_data=client_cert_data,
173 configuration=kubectl.configuration,
174 storage_class=default_storage_class,
175 credential_name=self._get_credential_name(cluster_uuid),
176 )
garciadeblas47f65382021-05-31 15:49:15 +0200177 self.log.debug("K8s cluster added to juju controller")
David Garciaf6e9b002020-11-27 15:32:02 +0100178 return cluster_uuid, True
179 except Exception as e:
garciadeblas47f65382021-05-31 15:49:15 +0200180 self.log.error("Error initializing k8scluster: {}".format(e), exc_info=True)
David Garciaf6e9b002020-11-27 15:32:02 +0100181 if len(cleanup_data) > 0:
182 self.log.debug("Cleaning up created resources in k8s cluster...")
183 for item in cleanup_data:
184 delete_function = item["delete"]
185 delete_args = item["args"]
186 delete_function(*delete_args)
187 self.log.debug("Cleanup finished")
188 raise e
Adam Israeld4ec83b2019-11-07 09:46:59 -0500189
190 """Repo Management"""
beierlmf52cb7c2020-04-21 16:36:35 -0400191
Adam Israeld4ec83b2019-11-07 09:46:59 -0500192 async def repo_add(
David Garcia667696e2020-09-22 14:52:32 +0200193 self,
194 name: str,
195 url: str,
196 _type: str = "charm",
bravof0ab522f2021-11-23 19:33:18 -0300197 cert: str = None,
198 user: str = None,
199 password: str = None,
Adam Israeld4ec83b2019-11-07 09:46:59 -0500200 ):
beierlmf52cb7c2020-04-21 16:36:35 -0400201 raise MethodNotImplemented()
Adam Israeld4ec83b2019-11-07 09:46:59 -0500202
203 async def repo_list(self):
beierlmf52cb7c2020-04-21 16:36:35 -0400204 raise MethodNotImplemented()
Adam Israeld4ec83b2019-11-07 09:46:59 -0500205
206 async def repo_remove(
David Garcia667696e2020-09-22 14:52:32 +0200207 self,
208 name: str,
Adam Israeld4ec83b2019-11-07 09:46:59 -0500209 ):
beierlmf52cb7c2020-04-21 16:36:35 -0400210 raise MethodNotImplemented()
Adam Israeld4ec83b2019-11-07 09:46:59 -0500211
beierlmf52cb7c2020-04-21 16:36:35 -0400212 async def synchronize_repos(self, cluster_uuid: str, name: str):
lloretgalleg65ddf852020-02-20 12:01:17 +0100213 """
214 Returns None as currently add_repo is not implemented
215 """
216 return None
217
Adam Israeld4ec83b2019-11-07 09:46:59 -0500218 """Reset"""
beierlmf52cb7c2020-04-21 16:36:35 -0400219
Adam Israeld4ec83b2019-11-07 09:46:59 -0500220 async def reset(
David Garciaeb8943a2021-04-12 12:07:37 +0200221 self,
222 cluster_uuid: str,
223 force: bool = False,
224 uninstall_sw: bool = False,
225 **kwargs,
Adam Israeld4ec83b2019-11-07 09:46:59 -0500226 ) -> bool:
227 """Reset a cluster
228
229 Resets the Kubernetes cluster by removing the model that represents it.
230
231 :param cluster_uuid str: The UUID of the cluster to reset
David Garciaeb8943a2021-04-12 12:07:37 +0200232 :param force: Force reset
233 :param uninstall_sw: Boolean to uninstall sw
234 :param: kwargs: Additional parameters
235 vca_id (str): VCA ID
236
Adam Israeld4ec83b2019-11-07 09:46:59 -0500237 :return: Returns True if successful or raises an exception.
238 """
239
240 try:
David Garcia4f74f592020-07-23 15:04:19 +0200241 self.log.debug("[reset] Removing k8s cloud")
David Garciaeb8943a2021-04-12 12:07:37 +0200242 libjuju = await self._get_libjuju(kwargs.get("vca_id"))
David Garciaf6e9b002020-11-27 15:32:02 +0100243
David Garciaeb8943a2021-04-12 12:07:37 +0200244 cloud = Cloud(cluster_uuid, self._get_credential_name(cluster_uuid))
David Garciaf6e9b002020-11-27 15:32:02 +0100245
David Garciaeb8943a2021-04-12 12:07:37 +0200246 cloud_creds = await libjuju.get_cloud_credentials(cloud)
247
248 await libjuju.remove_cloud(cluster_uuid)
Adam Israeld4ec83b2019-11-07 09:46:59 -0500249
David Garciad8d4b6e2021-06-24 18:47:22 +0200250 credentials = self.get_credentials(cluster_uuid=cluster_uuid)
David Garciaf6e9b002020-11-27 15:32:02 +0100251
David Garciad8d4b6e2021-06-24 18:47:22 +0200252 kubectl = self._get_kubectl(credentials)
David Garciaf6e9b002020-11-27 15:32:02 +0100253
254 delete_functions = [
David Garciad8d4b6e2021-06-24 18:47:22 +0200255 kubectl.delete_cluster_role_binding,
256 kubectl.delete_service_account,
257 kubectl.delete_cluster_role,
David Garciaf6e9b002020-11-27 15:32:02 +0100258 ]
259
260 credential_attrs = cloud_creds[0].result["attrs"]
261 if RBAC_LABEL_KEY_NAME in credential_attrs:
262 rbac_id = credential_attrs[RBAC_LABEL_KEY_NAME]
263 metadata_name = "{}-{}".format(RBAC_STACK_PREFIX, rbac_id)
David Garciaf6e9b002020-11-27 15:32:02 +0100264 for delete_func in delete_functions:
265 try:
David Garciad8d4b6e2021-06-24 18:47:22 +0200266 delete_func(metadata_name)
David Garciaf6e9b002020-11-27 15:32:02 +0100267 except Exception as e:
268 self.log.warning("Cannot remove resource in K8s {}".format(e))
269
David Garcia667696e2020-09-22 14:52:32 +0200270 except Exception as e:
271 self.log.debug("Caught exception during reset: {}".format(e))
272 raise e
Dominik Fleischmann1ac78b32020-02-26 19:58:25 +0100273 return True
274
Adam Israeld4ec83b2019-11-07 09:46:59 -0500275 """Deployment"""
Adam Israeleef68932019-11-28 16:27:46 -0500276
Adam Israeld4ec83b2019-11-07 09:46:59 -0500277 async def install(
278 self,
279 cluster_uuid: str,
280 kdu_model: str,
David Garciac4da25c2021-02-23 11:47:29 +0100281 kdu_instance: str,
Adam Israeld4ec83b2019-11-07 09:46:59 -0500282 atomic: bool = True,
David Garcia667696e2020-09-22 14:52:32 +0200283 timeout: float = 1800,
Adam Israeld4ec83b2019-11-07 09:46:59 -0500284 params: dict = None,
Dominik Fleischmann847f3c02020-02-04 15:32:42 +0100285 db_dict: dict = None,
tierno53555f62020-04-07 11:08:16 +0000286 kdu_name: str = None,
beierlmf52cb7c2020-04-21 16:36:35 -0400287 namespace: str = None,
David Garciaeb8943a2021-04-12 12:07:37 +0200288 **kwargs,
Adam Israeleef68932019-11-28 16:27:46 -0500289 ) -> bool:
Adam Israeld4ec83b2019-11-07 09:46:59 -0500290 """Install a bundle
291
292 :param cluster_uuid str: The UUID of the cluster to install to
293 :param kdu_model str: The name or path of a bundle to install
David Garciac4da25c2021-02-23 11:47:29 +0100294 :param kdu_instance: Kdu instance name
Adam Israeld4ec83b2019-11-07 09:46:59 -0500295 :param atomic bool: If set, waits until the model is active and resets
296 the cluster on failure.
297 :param timeout int: The time, in seconds, to wait for the install
298 to finish
299 :param params dict: Key-value pairs of instantiation parameters
Dominik Fleischmann847f3c02020-02-04 15:32:42 +0100300 :param kdu_name: Name of the KDU instance to be installed
tierno53555f62020-04-07 11:08:16 +0000301 :param namespace: K8s namespace to use for the KDU instance
David Garciaeb8943a2021-04-12 12:07:37 +0200302 :param kwargs: Additional parameters
303 vca_id (str): VCA ID
Adam Israeld4ec83b2019-11-07 09:46:59 -0500304
305 :return: If successful, returns ?
306 """
David Garciaeb8943a2021-04-12 12:07:37 +0200307 libjuju = await self._get_libjuju(kwargs.get("vca_id"))
David Garcia667696e2020-09-22 14:52:32 +0200308 bundle = kdu_model
Adam Israeld4ec83b2019-11-07 09:46:59 -0500309
David Garcia667696e2020-09-22 14:52:32 +0200310 if not db_dict:
311 raise K8sException("db_dict must be set")
312 if not bundle:
313 raise K8sException("bundle must be set")
314
315 if bundle.startswith("cs:"):
Pedro Escaleira86a63142022-04-05 21:01:37 +0100316 # For Juju Bundles provided by the Charm Store
317 pass
318 elif bundle.startswith("ch:"):
319 # For Juju Bundles provided by the Charm Hub (this only works for juju version >= 2.9)
David Garcia667696e2020-09-22 14:52:32 +0200320 pass
321 elif bundle.startswith("http"):
322 # Download the file
323 pass
324 else:
325 new_workdir = kdu_model.strip(kdu_model.split("/")[-1])
326 os.chdir(new_workdir)
327 bundle = "local:{}".format(kdu_model)
328
Dominik Fleischmann847f3c02020-02-04 15:32:42 +0100329 self.log.debug("Checking for model named {}".format(kdu_instance))
Dominik Fleischmann1ac78b32020-02-26 19:58:25 +0100330
331 # Create the new model
332 self.log.debug("Adding model: {}".format(kdu_instance))
David Garciaeb8943a2021-04-12 12:07:37 +0200333 cloud = Cloud(cluster_uuid, self._get_credential_name(cluster_uuid))
334 await libjuju.add_model(kdu_instance, cloud)
Adam Israeld4ec83b2019-11-07 09:46:59 -0500335
David Garcia667696e2020-09-22 14:52:32 +0200336 # if model:
337 # TODO: Instantiation parameters
Adam Israeld4ec83b2019-11-07 09:46:59 -0500338
David Garcia667696e2020-09-22 14:52:32 +0200339 """
340 "Juju bundle that models the KDU, in any of the following ways:
341 - <juju-repo>/<juju-bundle>
342 - <juju-bundle folder under k8s_models folder in the package>
343 - <juju-bundle tgz file (w/ or w/o extension) under k8s_models folder
344 in the package>
345 - <URL_where_to_fetch_juju_bundle>
346 """
347 try:
348 previous_workdir = os.getcwd()
349 except FileNotFoundError:
350 previous_workdir = "/app/storage"
Dominik Fleischmann45d95772020-03-26 12:21:42 +0100351
David Garcia667696e2020-09-22 14:52:32 +0200352 self.log.debug("[install] deploying {}".format(bundle))
David Garciaeb8943a2021-04-12 12:07:37 +0200353 await libjuju.deploy(
David Garcia667696e2020-09-22 14:52:32 +0200354 bundle, model_name=kdu_instance, wait=atomic, timeout=timeout
355 )
David Garcia667696e2020-09-22 14:52:32 +0200356 os.chdir(previous_workdir)
ksaikiranrb816d822021-03-17 12:50:20 +0530357 if self.on_update_db:
David Garciaeb8943a2021-04-12 12:07:37 +0200358 await self.on_update_db(
359 cluster_uuid,
360 kdu_instance,
361 filter=db_dict["filter"],
garciadeblas82b591c2021-03-24 09:22:13 +0100362 vca_id=kwargs.get("vca_id"),
David Garciaeb8943a2021-04-12 12:07:37 +0200363 )
David Garciac4da25c2021-02-23 11:47:29 +0100364 return True
Adam Israeld4ec83b2019-11-07 09:46:59 -0500365
aktas2962f3e2021-03-15 11:05:35 +0300366 async def scale(
garciadeblas82b591c2021-03-24 09:22:13 +0100367 self,
368 kdu_instance: str,
369 scale: int,
370 resource_name: str,
371 total_timeout: float = 1800,
372 **kwargs,
aktas2962f3e2021-03-15 11:05:35 +0300373 ) -> bool:
374 """Scale an application in a model
375
376 :param: kdu_instance str: KDU instance name
aktas867418c2021-10-19 18:26:13 +0300377 :param: scale int: Scale to which to set the application
378 :param: resource_name str: The application name in the Juju Bundle
aktas2962f3e2021-03-15 11:05:35 +0300379 :param: timeout float: The time, in seconds, to wait for the install
380 to finish
381 :param kwargs: Additional parameters
382 vca_id (str): VCA ID
383
384 :return: If successful, returns True
385 """
386
387 try:
388 libjuju = await self._get_libjuju(kwargs.get("vca_id"))
389 await libjuju.scale_application(
390 model_name=kdu_instance,
391 application_name=resource_name,
392 scale=scale,
garciadeblas82b591c2021-03-24 09:22:13 +0100393 total_timeout=total_timeout,
aktas2962f3e2021-03-15 11:05:35 +0300394 )
395 except Exception as e:
396 error_msg = "Error scaling application {} in kdu instance {}: {}".format(
garciadeblas82b591c2021-03-24 09:22:13 +0100397 resource_name, kdu_instance, e
398 )
aktas2962f3e2021-03-15 11:05:35 +0300399 self.log.error(error_msg)
400 raise K8sException(message=error_msg)
401 return True
402
403 async def get_scale_count(
garciadeblas82b591c2021-03-24 09:22:13 +0100404 self,
405 resource_name: str,
406 kdu_instance: str,
407 **kwargs,
aktas2962f3e2021-03-15 11:05:35 +0300408 ) -> int:
409 """Get an application scale count
410
aktas867418c2021-10-19 18:26:13 +0300411 :param: resource_name str: The application name in the Juju Bundle
aktas2962f3e2021-03-15 11:05:35 +0300412 :param: kdu_instance str: KDU instance name
413 :param kwargs: Additional parameters
414 vca_id (str): VCA ID
415 :return: Return application instance count
416 """
aktas867418c2021-10-19 18:26:13 +0300417
aktas2962f3e2021-03-15 11:05:35 +0300418 try:
419 libjuju = await self._get_libjuju(kwargs.get("vca_id"))
420 status = await libjuju.get_model_status(kdu_instance)
421 return len(status.applications[resource_name].units)
422 except Exception as e:
423 error_msg = "Error getting scale count from application {} in kdu instance {}: {}".format(
garciadeblas82b591c2021-03-24 09:22:13 +0100424 resource_name, kdu_instance, e
425 )
aktas2962f3e2021-03-15 11:05:35 +0300426 self.log.error(error_msg)
427 raise K8sException(message=error_msg)
428
beierlmf52cb7c2020-04-21 16:36:35 -0400429 async def instances_list(self, cluster_uuid: str) -> list:
Adam Israeld4ec83b2019-11-07 09:46:59 -0500430 """
431 returns a list of deployed releases in a cluster
432
433 :param cluster_uuid: the cluster
434 :return:
435 """
436 return []
437
438 async def upgrade(
439 self,
440 cluster_uuid: str,
441 kdu_instance: str,
442 kdu_model: str = None,
443 params: dict = None,
444 ) -> str:
445 """Upgrade a model
446
447 :param cluster_uuid str: The UUID of the cluster to upgrade
448 :param kdu_instance str: The unique name of the KDU instance
449 :param kdu_model str: The name or path of the bundle to upgrade to
450 :param params dict: Key-value pairs of instantiation parameters
451
452 :return: If successful, reference to the new revision number of the
453 KDU instance.
454 """
455
456 # TODO: Loop through the bundle and upgrade each charm individually
457
458 """
459 The API doesn't have a concept of bundle upgrades, because there are
460 many possible changes: charm revision, disk, number of units, etc.
461
462 As such, we are only supporting a limited subset of upgrades. We'll
463 upgrade the charm revision but leave storage and scale untouched.
464
465 Scale changes should happen through OSM constructs, and changes to
466 storage would require a redeployment of the service, at least in this
467 initial release.
468 """
beierlmf52cb7c2020-04-21 16:36:35 -0400469 raise MethodNotImplemented()
Adam Israeld4ec83b2019-11-07 09:46:59 -0500470
471 """Rollback"""
beierlmf52cb7c2020-04-21 16:36:35 -0400472
Adam Israeld4ec83b2019-11-07 09:46:59 -0500473 async def rollback(
David Garcia667696e2020-09-22 14:52:32 +0200474 self,
475 cluster_uuid: str,
476 kdu_instance: str,
477 revision: int = 0,
Adam Israeld4ec83b2019-11-07 09:46:59 -0500478 ) -> str:
479 """Rollback a model
480
481 :param cluster_uuid str: The UUID of the cluster to rollback
482 :param kdu_instance str: The unique name of the KDU instance
483 :param revision int: The revision to revert to. If omitted, rolls back
484 the previous upgrade.
485
486 :return: If successful, returns the revision of active KDU instance,
487 or raises an exception
488 """
beierlmf52cb7c2020-04-21 16:36:35 -0400489 raise MethodNotImplemented()
Adam Israeld4ec83b2019-11-07 09:46:59 -0500490
491 """Deletion"""
beierlmf52cb7c2020-04-21 16:36:35 -0400492
David Garciaeb8943a2021-04-12 12:07:37 +0200493 async def uninstall(
494 self,
495 cluster_uuid: str,
496 kdu_instance: str,
497 **kwargs,
498 ) -> bool:
Adam Israeld4ec83b2019-11-07 09:46:59 -0500499 """Uninstall a KDU instance
500
Dominik Fleischmann847f3c02020-02-04 15:32:42 +0100501 :param cluster_uuid str: The UUID of the cluster
Adam Israeld4ec83b2019-11-07 09:46:59 -0500502 :param kdu_instance str: The unique name of the KDU instance
David Garciaeb8943a2021-04-12 12:07:37 +0200503 :param kwargs: Additional parameters
504 vca_id (str): VCA ID
Adam Israeld4ec83b2019-11-07 09:46:59 -0500505
506 :return: Returns True if successful, or raises an exception
507 """
David Garcia4f74f592020-07-23 15:04:19 +0200508
Dominik Fleischmann1ac78b32020-02-26 19:58:25 +0100509 self.log.debug("[uninstall] Destroying model")
David Garciaeb8943a2021-04-12 12:07:37 +0200510 libjuju = await self._get_libjuju(kwargs.get("vca_id"))
Dominik Fleischmann1ac78b32020-02-26 19:58:25 +0100511
David Garciaeb8943a2021-04-12 12:07:37 +0200512 await libjuju.destroy_model(kdu_instance, total_timeout=3600)
Adam Israeld4ec83b2019-11-07 09:46:59 -0500513
David Garcia667696e2020-09-22 14:52:32 +0200514 # self.log.debug("[uninstall] Model destroyed and disconnecting")
515 # await controller.disconnect()
Dominik Fleischmann1ac78b32020-02-26 19:58:25 +0100516
Dominik Fleischmann847f3c02020-02-04 15:32:42 +0100517 return True
David Garcia4f74f592020-07-23 15:04:19 +0200518 # TODO: Remove these commented lines
519 # if not self.authenticated:
520 # self.log.debug("[uninstall] Connecting to controller")
521 # await self.login(cluster_uuid)
Adam Israeld4ec83b2019-11-07 09:46:59 -0500522
Dominik Fleischmannfc796cc2020-04-06 14:51:00 +0200523 async def exec_primitive(
524 self,
525 cluster_uuid: str = None,
526 kdu_instance: str = None,
527 primitive_name: str = None,
528 timeout: float = 300,
529 params: dict = None,
530 db_dict: dict = None,
David Garciaeb8943a2021-04-12 12:07:37 +0200531 **kwargs,
Dominik Fleischmannfc796cc2020-04-06 14:51:00 +0200532 ) -> str:
533 """Exec primitive (Juju action)
534
535 :param cluster_uuid str: The UUID of the cluster
536 :param kdu_instance str: The unique name of the KDU instance
537 :param primitive_name: Name of action that will be executed
538 :param timeout: Timeout for action execution
539 :param params: Dictionary of all the parameters needed for the action
David Garciaeb8943a2021-04-12 12:07:37 +0200540 :param db_dict: Dictionary for any additional data
541 :param kwargs: Additional parameters
542 vca_id (str): VCA ID
Dominik Fleischmannfc796cc2020-04-06 14:51:00 +0200543
544 :return: Returns the output of the action
545 """
David Garciaeb8943a2021-04-12 12:07:37 +0200546 libjuju = await self._get_libjuju(kwargs.get("vca_id"))
David Garcia4f74f592020-07-23 15:04:19 +0200547
Dominik Fleischmannfc796cc2020-04-06 14:51:00 +0200548 if not params or "application-name" not in params:
beierlmf52cb7c2020-04-21 16:36:35 -0400549 raise K8sException(
550 "Missing application-name argument, \
551 argument needed for K8s actions"
552 )
Dominik Fleischmannfc796cc2020-04-06 14:51:00 +0200553 try:
beierlmf52cb7c2020-04-21 16:36:35 -0400554 self.log.debug(
555 "[exec_primitive] Getting model "
556 "kdu_instance: {}".format(kdu_instance)
557 )
Dominik Fleischmannfc796cc2020-04-06 14:51:00 +0200558 application_name = params["application-name"]
David Garciaeb8943a2021-04-12 12:07:37 +0200559 actions = await libjuju.get_actions(application_name, kdu_instance)
Dominik Fleischmannfc796cc2020-04-06 14:51:00 +0200560 if primitive_name not in actions:
561 raise K8sException("Primitive {} not found".format(primitive_name))
David Garciaeb8943a2021-04-12 12:07:37 +0200562 output, status = await libjuju.execute_action(
David Garcia667696e2020-09-22 14:52:32 +0200563 application_name, kdu_instance, primitive_name, **params
Dominik Fleischmannfc796cc2020-04-06 14:51:00 +0200564 )
565
566 if status != "completed":
beierlmf52cb7c2020-04-21 16:36:35 -0400567 raise K8sException(
568 "status is not completed: {} output: {}".format(status, output)
569 )
ksaikiranrb816d822021-03-17 12:50:20 +0530570 if self.on_update_db:
garciadeblas82b591c2021-03-24 09:22:13 +0100571 await self.on_update_db(
572 cluster_uuid, kdu_instance, filter=db_dict["filter"]
573 )
Dominik Fleischmannfc796cc2020-04-06 14:51:00 +0200574
575 return output
576
577 except Exception as e:
578 error_msg = "Error executing primitive {}: {}".format(primitive_name, e)
579 self.log.error(error_msg)
580 raise K8sException(message=error_msg)
581
Adam Israeld4ec83b2019-11-07 09:46:59 -0500582 """Introspection"""
beierlmf52cb7c2020-04-21 16:36:35 -0400583
David Garcia667696e2020-09-22 14:52:32 +0200584 async def inspect_kdu(
585 self,
586 kdu_model: str,
587 ) -> dict:
Adam Israeld4ec83b2019-11-07 09:46:59 -0500588 """Inspect a KDU
589
590 Inspects a bundle and returns a dictionary of config parameters and
591 their default values.
592
593 :param kdu_model str: The name or path of the bundle to inspect.
594
595 :return: If successful, returns a dictionary of available parameters
596 and their default values.
597 """
598
599 kdu = {}
David Garcia667696e2020-09-22 14:52:32 +0200600 if not os.path.exists(kdu_model):
601 raise K8sException("file {} not found".format(kdu_model))
602
beierlmf52cb7c2020-04-21 16:36:35 -0400603 with open(kdu_model, "r") as f:
David Garcia667696e2020-09-22 14:52:32 +0200604 bundle = yaml.safe_load(f.read())
Adam Israeld4ec83b2019-11-07 09:46:59 -0500605
606 """
607 {
608 'description': 'Test bundle',
609 'bundle': 'kubernetes',
610 'applications': {
611 'mariadb-k8s': {
612 'charm': 'cs:~charmed-osm/mariadb-k8s-20',
613 'scale': 1,
614 'options': {
615 'password': 'manopw',
616 'root_password': 'osm4u',
617 'user': 'mano'
618 },
619 'series': 'kubernetes'
620 }
621 }
622 }
623 """
624 # TODO: This should be returned in an agreed-upon format
beierlmf52cb7c2020-04-21 16:36:35 -0400625 kdu = bundle["applications"]
Adam Israeld4ec83b2019-11-07 09:46:59 -0500626
627 return kdu
628
David Garcia667696e2020-09-22 14:52:32 +0200629 async def help_kdu(
630 self,
631 kdu_model: str,
632 ) -> str:
Adam Israeld4ec83b2019-11-07 09:46:59 -0500633 """View the README
634
635 If available, returns the README of the bundle.
636
637 :param kdu_model str: The name or path of a bundle
638
639 :return: If found, returns the contents of the README.
640 """
641 readme = None
642
beierlmf52cb7c2020-04-21 16:36:35 -0400643 files = ["README", "README.txt", "README.md"]
Adam Israeld4ec83b2019-11-07 09:46:59 -0500644 path = os.path.dirname(kdu_model)
645 for file in os.listdir(path):
646 if file in files:
beierlmf52cb7c2020-04-21 16:36:35 -0400647 with open(file, "r") as f:
Adam Israeld4ec83b2019-11-07 09:46:59 -0500648 readme = f.read()
649 break
650
651 return readme
652
David Garcia667696e2020-09-22 14:52:32 +0200653 async def status_kdu(
654 self,
655 cluster_uuid: str,
656 kdu_instance: str,
ksaikiranrb816d822021-03-17 12:50:20 +0530657 complete_status: bool = False,
David Garciaeb8943a2021-04-12 12:07:37 +0200658 yaml_format: bool = False,
659 **kwargs,
Pedro Escaleiraa8980cc2022-04-05 17:32:13 +0100660 ) -> Union[str, dict]:
Adam Israeld4ec83b2019-11-07 09:46:59 -0500661 """Get the status of the KDU
662
663 Get the current status of the KDU instance.
664
665 :param cluster_uuid str: The UUID of the cluster
666 :param kdu_instance str: The unique id of the KDU instance
ksaikiranrb816d822021-03-17 12:50:20 +0530667 :param complete_status: To get the complete_status of the KDU
668 :param yaml_format: To get the status in proper format for NSR record
David Garciaeb8943a2021-04-12 12:07:37 +0200669 :param: kwargs: Additional parameters
670 vca_id (str): VCA ID
Adam Israeld4ec83b2019-11-07 09:46:59 -0500671
672 :return: Returns a dictionary containing namespace, state, resources,
ksaikiranrb816d822021-03-17 12:50:20 +0530673 and deployment_time and returns complete_status if complete_status is True
Adam Israeld4ec83b2019-11-07 09:46:59 -0500674 """
David Garciaeb8943a2021-04-12 12:07:37 +0200675 libjuju = await self._get_libjuju(kwargs.get("vca_id"))
Adam Israeld4ec83b2019-11-07 09:46:59 -0500676 status = {}
ksaikiranrb816d822021-03-17 12:50:20 +0530677
David Garciaeb8943a2021-04-12 12:07:37 +0200678 model_status = await libjuju.get_model_status(kdu_instance)
ksaikiranrb816d822021-03-17 12:50:20 +0530679
680 if not complete_status:
681 for name in model_status.applications:
682 application = model_status.applications[name]
683 status[name] = {"status": application["status"]["status"]}
684 else:
685 if yaml_format:
686 return obj_to_yaml(model_status)
687 else:
688 return obj_to_dict(model_status)
Adam Israeld4ec83b2019-11-07 09:46:59 -0500689
Adam Israeld4ec83b2019-11-07 09:46:59 -0500690 return status
691
David Garcia582b9232021-10-26 12:30:44 +0200692 async def add_relation(
693 self,
694 provider: RelationEndpoint,
695 requirer: RelationEndpoint,
696 ):
697 """
698 Add relation between two charmed endpoints
699
700 :param: provider: Provider relation endpoint
701 :param: requirer: Requirer relation endpoint
702 """
703 self.log.debug(f"adding new relation between {provider} and {requirer}")
704 cross_model_relation = (
705 provider.model_name != requirer.model_name
706 or requirer.vca_id != requirer.vca_id
707 )
708 try:
709 if cross_model_relation:
710 # Cross-model relation
711 provider_libjuju = await self._get_libjuju(provider.vca_id)
712 requirer_libjuju = await self._get_libjuju(requirer.vca_id)
713 offer = await provider_libjuju.offer(provider)
714 if offer:
715 saas_name = await requirer_libjuju.consume(
716 requirer.model_name, offer, provider_libjuju
717 )
718 await requirer_libjuju.add_relation(
719 requirer.model_name,
720 requirer.endpoint,
721 saas_name,
722 )
723 else:
724 # Standard relation
725 vca_id = provider.vca_id
726 model = provider.model_name
727 libjuju = await self._get_libjuju(vca_id)
728 # add juju relations between two applications
729 await libjuju.add_relation(
730 model_name=model,
731 endpoint_1=provider.endpoint,
732 endpoint_2=requirer.endpoint,
733 )
734 except Exception as e:
735 message = f"Error adding relation between {provider} and {requirer}: {e}"
736 self.log.error(message)
737 raise Exception(message=message)
738
David Garciaeb8943a2021-04-12 12:07:37 +0200739 async def update_vca_status(self, vcastatus: dict, kdu_instance: str, **kwargs):
ksaikiranrb816d822021-03-17 12:50:20 +0530740 """
741 Add all configs, actions, executed actions of all applications in a model to vcastatus dict
742
743 :param vcastatus dict: dict containing vcastatus
744 :param kdu_instance str: The unique id of the KDU instance
David Garciaeb8943a2021-04-12 12:07:37 +0200745 :param: kwargs: Additional parameters
746 vca_id (str): VCA ID
ksaikiranrb816d822021-03-17 12:50:20 +0530747
748 :return: None
749 """
David Garciaeb8943a2021-04-12 12:07:37 +0200750 libjuju = await self._get_libjuju(kwargs.get("vca_id"))
ksaikiranrb816d822021-03-17 12:50:20 +0530751 try:
752 for model_name in vcastatus:
753 # Adding executed actions
garciadeblas82b591c2021-03-24 09:22:13 +0100754 vcastatus[model_name][
755 "executedActions"
756 ] = await libjuju.get_executed_actions(kdu_instance)
ksaikiranrb816d822021-03-17 12:50:20 +0530757
758 for application in vcastatus[model_name]["applications"]:
759 # Adding application actions
garciadeblas82b591c2021-03-24 09:22:13 +0100760 vcastatus[model_name]["applications"][application][
761 "actions"
762 ] = await libjuju.get_actions(application, kdu_instance)
ksaikiranrb816d822021-03-17 12:50:20 +0530763 # Adding application configs
garciadeblas82b591c2021-03-24 09:22:13 +0100764 vcastatus[model_name]["applications"][application][
765 "configs"
766 ] = await libjuju.get_application_configs(kdu_instance, application)
ksaikiranrb816d822021-03-17 12:50:20 +0530767
768 except Exception as e:
769 self.log.debug("Error in updating vca status: {}".format(str(e)))
770
David Garcia5d799392020-07-02 13:56:58 +0200771 async def get_services(
772 self, cluster_uuid: str, kdu_instance: str, namespace: str
773 ) -> list:
774 """Return a list of services of a kdu_instance"""
lloretgallegd99f3f22020-06-29 14:18:30 +0000775
David Garcia2c791b32020-07-22 17:56:12 +0200776 credentials = self.get_credentials(cluster_uuid=cluster_uuid)
David Garciad8d4b6e2021-06-24 18:47:22 +0200777 kubectl = self._get_kubectl(credentials)
David Garcia5d799392020-07-02 13:56:58 +0200778 return kubectl.get_services(
779 field_selector="metadata.namespace={}".format(kdu_instance)
780 )
781
782 async def get_service(
783 self, cluster_uuid: str, service_name: str, namespace: str
784 ) -> object:
785 """Return data for a specific service inside a namespace"""
786
David Garcia2c791b32020-07-22 17:56:12 +0200787 credentials = self.get_credentials(cluster_uuid=cluster_uuid)
David Garciad8d4b6e2021-06-24 18:47:22 +0200788 kubectl = self._get_kubectl(credentials)
David Garcia5d799392020-07-02 13:56:58 +0200789 return kubectl.get_services(
790 field_selector="metadata.name={},metadata.namespace={}".format(
791 service_name, namespace
792 )
793 )[0]
lloretgallegd99f3f22020-06-29 14:18:30 +0000794
David Garcia2c791b32020-07-22 17:56:12 +0200795 def get_credentials(self, cluster_uuid: str) -> str:
David Garcia5d799392020-07-02 13:56:58 +0200796 """
David Garcia2c791b32020-07-22 17:56:12 +0200797 Get Cluster Kubeconfig
David Garcia5d799392020-07-02 13:56:58 +0200798 """
David Garcia2c791b32020-07-22 17:56:12 +0200799 k8scluster = self.db.get_one(
800 "k8sclusters", q_filter={"_id": cluster_uuid}, fail_on_empty=False
801 )
802
803 self.db.encrypt_decrypt_fields(
804 k8scluster.get("credentials"),
805 "decrypt",
806 ["password", "secret"],
807 schema_version=k8scluster["schema_version"],
808 salt=k8scluster["_id"],
809 )
810
811 return yaml.safe_dump(k8scluster.get("credentials"))
David Garcia5d799392020-07-02 13:56:58 +0200812
David Garcia667696e2020-09-22 14:52:32 +0200813 def _get_credential_name(self, cluster_uuid: str) -> str:
Adam Israeld4ec83b2019-11-07 09:46:59 -0500814 """
David Garcia667696e2020-09-22 14:52:32 +0200815 Get credential name for a k8s cloud
David Garcia4f74f592020-07-23 15:04:19 +0200816
David Garcia667696e2020-09-22 14:52:32 +0200817 We cannot use the cluster_uuid for the credential name directly,
818 because it cannot start with a number, it must start with a letter.
819 Therefore, the k8s cloud credential name will be "cred-" followed
820 by the cluster uuid.
Adam Israeld4ec83b2019-11-07 09:46:59 -0500821
David Garcia667696e2020-09-22 14:52:32 +0200822 :param: cluster_uuid: Cluster UUID of the kubernetes cloud (=cloud_name)
Adam Israeld4ec83b2019-11-07 09:46:59 -0500823
David Garcia667696e2020-09-22 14:52:32 +0200824 :return: Name to use for the credential name.
Adam Israeld4ec83b2019-11-07 09:46:59 -0500825 """
David Garcia667696e2020-09-22 14:52:32 +0200826 return "cred-{}".format(cluster_uuid)
Adam Israeld4ec83b2019-11-07 09:46:59 -0500827
David Garcia667696e2020-09-22 14:52:32 +0200828 def get_namespace(
829 self,
830 cluster_uuid: str,
831 ) -> str:
Adam Israeld4ec83b2019-11-07 09:46:59 -0500832 """Get the namespace UUID
833 Gets the namespace's unique name
834
835 :param cluster_uuid str: The UUID of the cluster
836 :returns: The namespace UUID, or raises an exception
837 """
David Garcia667696e2020-09-22 14:52:32 +0200838 pass
Adam Israeld4ec83b2019-11-07 09:46:59 -0500839
David Garciac4da25c2021-02-23 11:47:29 +0100840 @staticmethod
841 def generate_kdu_instance_name(**kwargs):
842 db_dict = kwargs.get("db_dict")
843 kdu_name = kwargs.get("kdu_name", None)
844 if kdu_name:
845 kdu_instance = "{}-{}".format(kdu_name, db_dict["filter"]["_id"])
846 else:
847 kdu_instance = db_dict["filter"]["_id"]
848 return kdu_instance
David Garciaeb8943a2021-04-12 12:07:37 +0200849
850 async def _get_libjuju(self, vca_id: str = None) -> Libjuju:
851 """
852 Get libjuju object
853
854 :param: vca_id: VCA ID
855 If None, get a libjuju object with a Connection to the default VCA
856 Else, geta libjuju object with a Connection to the specified VCA
857 """
858 if not vca_id:
859 while self.loading_libjuju.locked():
860 await asyncio.sleep(0.1)
861 if not self.libjuju:
862 async with self.loading_libjuju:
863 vca_connection = await get_connection(self._store)
864 self.libjuju = Libjuju(vca_connection, loop=self.loop, log=self.log)
865 return self.libjuju
866 else:
867 vca_connection = await get_connection(self._store, vca_id)
868 return Libjuju(
869 vca_connection,
870 loop=self.loop,
871 log=self.log,
872 n2vc=self,
873 )
David Garciad8d4b6e2021-06-24 18:47:22 +0200874
875 def _get_kubectl(self, credentials: str) -> Kubectl:
876 """
877 Get Kubectl object
878
879 :param: kubeconfig_credentials: Kubeconfig credentials
880 """
881 kubecfg = tempfile.NamedTemporaryFile()
882 with open(kubecfg.name, "w") as kubecfg_file:
883 kubecfg_file.write(credentials)
884 return Kubectl(config_file=kubecfg.name)