blob: e3f326730ef6b3a9d47f5b47e42afa939f8598ab [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
Adam Israeld4ec83b2019-11-07 09:46:59 -050016import concurrent
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
beierlmf52cb7c2020-04-21 16:36:35 -040020
beierlmf52cb7c2020-04-21 16:36:35 -040021from juju.controller import Controller
beierlm55ca1c72020-05-05 14:55:19 -040022from juju.model import Model
beierlmf52cb7c2020-04-21 16:36:35 -040023from n2vc.exceptions import K8sException
24from n2vc.k8s_conn import K8sConnector
David Garcia5d799392020-07-02 13:56:58 +020025from n2vc.kubectl import Kubectl
David Garcia4f74f592020-07-23 15:04:19 +020026from .exceptions import MethodNotImplemented, N2VCNotFound
beierlmf52cb7c2020-04-21 16:36:35 -040027
28
29# from juju.bundle import BundleHandler
Adam Israeld4ec83b2019-11-07 09:46:59 -050030# import re
31# import ssl
Adam Israeld4ec83b2019-11-07 09:46:59 -050032# from .vnf import N2VC
Adam Israeld4ec83b2019-11-07 09:46:59 -050033class K8sJujuConnector(K8sConnector):
34 def __init__(
beierlmf52cb7c2020-04-21 16:36:35 -040035 self,
36 fs: object,
37 db: object,
38 kubectl_command: str = "/usr/bin/kubectl",
39 juju_command: str = "/usr/bin/juju",
40 log: object = None,
41 on_update_db=None,
Adam Israeld4ec83b2019-11-07 09:46:59 -050042 ):
43 """
44
45 :param kubectl_command: path to kubectl executable
46 :param helm_command: path to helm executable
47 :param fs: file system for kubernetes and helm configuration
48 :param log: logger
49 """
50
51 # parent class
52 K8sConnector.__init__(
beierlmf52cb7c2020-04-21 16:36:35 -040053 self, db, log=log, on_update_db=on_update_db,
Adam Israeld4ec83b2019-11-07 09:46:59 -050054 )
55
Adam Israeleef68932019-11-28 16:27:46 -050056 self.fs = fs
beierlmf52cb7c2020-04-21 16:36:35 -040057 self.log.debug("Initializing K8S Juju connector")
Adam Israeld4ec83b2019-11-07 09:46:59 -050058
Adam Israel40899212019-12-02 16:33:05 -050059 self.juju_command = juju_command
David Garcia4f74f592020-07-23 15:04:19 +020060 self.juju_public_key = None
Adam Israeleef68932019-11-28 16:27:46 -050061
beierlmf52cb7c2020-04-21 16:36:35 -040062 self.log.debug("K8S Juju connector initialized")
David Garcia4f74f592020-07-23 15:04:19 +020063 # TODO: Remove these commented lines:
64 # self.authenticated = False
65 # self.models = {}
66 # self.juju_secret = ""
Adam Israeld4ec83b2019-11-07 09:46:59 -050067
68 """Initialization"""
beierlmf52cb7c2020-04-21 16:36:35 -040069
Adam Israeld4ec83b2019-11-07 09:46:59 -050070 async def init_env(
71 self,
Adam Israeleef68932019-11-28 16:27:46 -050072 k8s_creds: str,
beierlmf52cb7c2020-04-21 16:36:35 -040073 namespace: str = "kube-system",
Adam Israeld4ec83b2019-11-07 09:46:59 -050074 reuse_cluster_uuid: str = None,
Adam Israeleef68932019-11-28 16:27:46 -050075 ) -> (str, bool):
garciadeblas54771fa2019-12-13 13:39:03 +010076 """
77 It prepares a given K8s cluster environment to run Juju bundles.
Adam Israeld4ec83b2019-11-07 09:46:59 -050078
beierlmf52cb7c2020-04-21 16:36:35 -040079 :param k8s_creds: credentials to access a given K8s cluster, i.e. a valid
80 '.kube/config'
81 :param namespace: optional namespace to be used for juju. By default,
82 'kube-system' will be used
garciadeblas54771fa2019-12-13 13:39:03 +010083 :param reuse_cluster_uuid: existing cluster uuid for reuse
beierlmf52cb7c2020-04-21 16:36:35 -040084 :return: uuid of the K8s cluster and True if connector has installed some
85 software in the cluster
86 (on error, an exception will be raised)
Adam Israeld4ec83b2019-11-07 09:46:59 -050087 """
88
89 """Bootstrapping
90
91 Bootstrapping cannot be done, by design, through the API. We need to
92 use the CLI tools.
93 """
Adam Israeld4ec83b2019-11-07 09:46:59 -050094
95 """
96 WIP: Workflow
97
98 1. Has the environment already been bootstrapped?
99 - Check the database to see if we have a record for this env
100
101 2. If this is a new env, create it
102 - Add the k8s cloud to Juju
103 - Bootstrap
104 - Record it in the database
105
106 3. Connect to the Juju controller for this cloud
107
108 """
109 # cluster_uuid = reuse_cluster_uuid
110 # if not cluster_uuid:
111 # cluster_uuid = str(uuid4())
112
113 ##################################################
114 # TODO: Pull info from db based on the namespace #
115 ##################################################
116
garciadeblas54771fa2019-12-13 13:39:03 +0100117 ###################################################
118 # TODO: Make it idempotent, calling add-k8s and #
119 # bootstrap whenever reuse_cluster_uuid is passed #
120 # as parameter #
121 # `init_env` is called to initialize the K8s #
122 # cluster for juju. If this initialization fails, #
123 # it can be called again by LCM with the param #
124 # reuse_cluster_uuid, e.g. to try to fix it. #
125 ###################################################
126
David Garcia37004982020-07-16 17:53:20 +0200127 # This is a new cluster, so bootstrap it
Adam Israeld4ec83b2019-11-07 09:46:59 -0500128
David Garcia37004982020-07-16 17:53:20 +0200129 cluster_uuid = reuse_cluster_uuid or str(uuid.uuid4())
Adam Israeld4ec83b2019-11-07 09:46:59 -0500130
David Garcia37004982020-07-16 17:53:20 +0200131 # Is a local k8s cluster?
132 localk8s = self.is_local_k8s(k8s_creds)
Adam Israeld4ec83b2019-11-07 09:46:59 -0500133
David Garcia37004982020-07-16 17:53:20 +0200134 # If the k8s is external, the juju controller needs a loadbalancer
135 loadbalancer = False if localk8s else True
Adam Israeld4ec83b2019-11-07 09:46:59 -0500136
David Garcia37004982020-07-16 17:53:20 +0200137 # Name the new k8s cloud
138 k8s_cloud = "k8s-{}".format(cluster_uuid)
Adam Israeld4ec83b2019-11-07 09:46:59 -0500139
David Garcia37004982020-07-16 17:53:20 +0200140 self.log.debug("Adding k8s cloud {}".format(k8s_cloud))
141 await self.add_k8s(k8s_cloud, k8s_creds)
Adam Israeld4ec83b2019-11-07 09:46:59 -0500142
David Garcia37004982020-07-16 17:53:20 +0200143 # Bootstrap Juju controller
144 self.log.debug("Bootstrapping...")
145 await self.bootstrap(k8s_cloud, cluster_uuid, loadbalancer)
146 self.log.debug("Bootstrap done.")
Adam Israeld4ec83b2019-11-07 09:46:59 -0500147
David Garcia37004982020-07-16 17:53:20 +0200148 # Get the controller information
Adam Israeld4ec83b2019-11-07 09:46:59 -0500149
David Garcia37004982020-07-16 17:53:20 +0200150 # Parse ~/.local/share/juju/controllers.yaml
151 # controllers.testing.api-endpoints|ca-cert|uuid
152 self.log.debug("Getting controller endpoints")
153 with open(os.path.expanduser("~/.local/share/juju/controllers.yaml")) as f:
154 controllers = yaml.load(f, Loader=yaml.Loader)
155 controller = controllers["controllers"][cluster_uuid]
156 endpoints = controller["api-endpoints"]
David Garcia4f74f592020-07-23 15:04:19 +0200157 juju_endpoint = endpoints[0]
158 juju_ca_cert = controller["ca-cert"]
Adam Israeld4ec83b2019-11-07 09:46:59 -0500159
David Garcia37004982020-07-16 17:53:20 +0200160 # Parse ~/.local/share/juju/accounts
161 # controllers.testing.user|password
162 self.log.debug("Getting accounts")
163 with open(os.path.expanduser("~/.local/share/juju/accounts.yaml")) as f:
164 controllers = yaml.load(f, Loader=yaml.Loader)
165 controller = controllers["controllers"][cluster_uuid]
Adam Israeld4ec83b2019-11-07 09:46:59 -0500166
David Garcia4f74f592020-07-23 15:04:19 +0200167 juju_user = controller["user"]
168 juju_secret = controller["password"]
Adam Israeld4ec83b2019-11-07 09:46:59 -0500169
David Garcia37004982020-07-16 17:53:20 +0200170 config = {
David Garcia4f74f592020-07-23 15:04:19 +0200171 "endpoint": juju_endpoint,
172 "username": juju_user,
173 "secret": juju_secret,
174 "cacert": juju_ca_cert,
David Garcia37004982020-07-16 17:53:20 +0200175 "loadbalancer": loadbalancer,
176 }
Adam Israeld4ec83b2019-11-07 09:46:59 -0500177
David Garcia37004982020-07-16 17:53:20 +0200178 # Store the cluster configuration so it
179 # can be used for subsequent calls
180 self.log.debug("Setting config")
181 await self.set_config(cluster_uuid, config)
Adam Israeld4ec83b2019-11-07 09:46:59 -0500182
David Garcia4f74f592020-07-23 15:04:19 +0200183 # Test connection
184 controller = await self.get_controller(cluster_uuid)
185 await controller.disconnect()
186
187 # TODO: Remove these commented lines
188 # raise Exception("EOL")
189 # self.juju_public_key = None
Adam Israeld4ec83b2019-11-07 09:46:59 -0500190 # Login to the k8s cluster
David Garcia4f74f592020-07-23 15:04:19 +0200191 # if not self.authenticated:
192 # await self.login(cluster_uuid)
Adam Israeld4ec83b2019-11-07 09:46:59 -0500193
194 # We're creating a new cluster
beierlmf52cb7c2020-04-21 16:36:35 -0400195 # print("Getting model {}".format(self.get_namespace(cluster_uuid),
196 # cluster_uuid=cluster_uuid))
197 # model = await self.get_model(
Dominik Fleischmann1ac78b32020-02-26 19:58:25 +0100198 # self.get_namespace(cluster_uuid),
199 # cluster_uuid=cluster_uuid
beierlmf52cb7c2020-04-21 16:36:35 -0400200 # )
Adam Israeld4ec83b2019-11-07 09:46:59 -0500201
beierlmf52cb7c2020-04-21 16:36:35 -0400202 # Disconnect from the model
203 # if model and model.is_connected():
Dominik Fleischmann1ac78b32020-02-26 19:58:25 +0100204 # await model.disconnect()
Adam Israeld4ec83b2019-11-07 09:46:59 -0500205
Adam Israeleef68932019-11-28 16:27:46 -0500206 return cluster_uuid, True
Adam Israeld4ec83b2019-11-07 09:46:59 -0500207
208 """Repo Management"""
beierlmf52cb7c2020-04-21 16:36:35 -0400209
Adam Israeld4ec83b2019-11-07 09:46:59 -0500210 async def repo_add(
beierlmf52cb7c2020-04-21 16:36:35 -0400211 self, name: str, url: str, _type: str = "charm",
Adam Israeld4ec83b2019-11-07 09:46:59 -0500212 ):
beierlmf52cb7c2020-04-21 16:36:35 -0400213 raise MethodNotImplemented()
Adam Israeld4ec83b2019-11-07 09:46:59 -0500214
215 async def repo_list(self):
beierlmf52cb7c2020-04-21 16:36:35 -0400216 raise MethodNotImplemented()
Adam Israeld4ec83b2019-11-07 09:46:59 -0500217
218 async def repo_remove(
beierlmf52cb7c2020-04-21 16:36:35 -0400219 self, name: str,
Adam Israeld4ec83b2019-11-07 09:46:59 -0500220 ):
beierlmf52cb7c2020-04-21 16:36:35 -0400221 raise MethodNotImplemented()
Adam Israeld4ec83b2019-11-07 09:46:59 -0500222
beierlmf52cb7c2020-04-21 16:36:35 -0400223 async def synchronize_repos(self, cluster_uuid: str, name: str):
lloretgalleg65ddf852020-02-20 12:01:17 +0100224 """
225 Returns None as currently add_repo is not implemented
226 """
227 return None
228
Adam Israeld4ec83b2019-11-07 09:46:59 -0500229 """Reset"""
beierlmf52cb7c2020-04-21 16:36:35 -0400230
Adam Israeld4ec83b2019-11-07 09:46:59 -0500231 async def reset(
beierlmf52cb7c2020-04-21 16:36:35 -0400232 self, cluster_uuid: str, force: bool = False, uninstall_sw: bool = False
Adam Israeld4ec83b2019-11-07 09:46:59 -0500233 ) -> bool:
234 """Reset a cluster
235
236 Resets the Kubernetes cluster by removing the model that represents it.
237
238 :param cluster_uuid str: The UUID of the cluster to reset
239 :return: Returns True if successful or raises an exception.
240 """
241
242 try:
Adam Israeld4ec83b2019-11-07 09:46:59 -0500243
David Garcia4f74f592020-07-23 15:04:19 +0200244 # Remove k8scluster from database
245 self.log.debug("[reset] Removing k8scluster from juju database")
246 juju_db = self.db.get_one("admin", {"_id": "juju"})
Adam Israeld4ec83b2019-11-07 09:46:59 -0500247
David Garcia4f74f592020-07-23 15:04:19 +0200248 for k in juju_db["k8sclusters"]:
249 if k["_id"] == cluster_uuid:
250 juju_db["k8sclusters"].remove(k)
251 self.db.set_one(
252 table="admin",
253 q_filter={"_id": "juju"},
254 update_dict={"k8sclusters": juju_db["k8sclusters"]},
255 )
256 break
Adam Israeld4ec83b2019-11-07 09:46:59 -0500257
David Garcia4f74f592020-07-23 15:04:19 +0200258 # Destroy the controller (via CLI)
259 self.log.debug("[reset] Destroying controller")
260 await self.destroy_controller(cluster_uuid)
Adam Israeld4ec83b2019-11-07 09:46:59 -0500261
David Garcia4f74f592020-07-23 15:04:19 +0200262 self.log.debug("[reset] Removing k8s cloud")
263 k8s_cloud = "k8s-{}".format(cluster_uuid)
264 await self.remove_cloud(k8s_cloud)
Adam Israeld4ec83b2019-11-07 09:46:59 -0500265
266 except Exception as ex:
Dominik Fleischmann2f2832c2020-02-26 14:37:16 +0100267 self.log.debug("Caught exception during reset: {}".format(ex))
Dominik Fleischmann1ac78b32020-02-26 19:58:25 +0100268 return True
David Garcia4f74f592020-07-23 15:04:19 +0200269 # TODO: Remove these commented lines
270 # if not self.authenticated:
271 # await self.login(cluster_uuid)
272
273 # if self.controller.is_connected():
274 # # Destroy the model
275 # namespace = self.get_namespace(cluster_uuid)
276 # if await self.has_model(namespace):
277 # self.log.debug("[reset] Destroying model")
278 # await self.controller.destroy_model(namespace, destroy_storage=True)
279
280 # # Disconnect from the controller
281 # self.log.debug("[reset] Disconnecting controller")
282 # await self.logout()
Dominik Fleischmann1ac78b32020-02-26 19:58:25 +0100283
Adam Israeld4ec83b2019-11-07 09:46:59 -0500284 """Deployment"""
Adam Israeleef68932019-11-28 16:27:46 -0500285
Adam Israeld4ec83b2019-11-07 09:46:59 -0500286 async def install(
287 self,
288 cluster_uuid: str,
289 kdu_model: str,
290 atomic: bool = True,
Adam Israeleef68932019-11-28 16:27:46 -0500291 timeout: float = 300,
Adam Israeld4ec83b2019-11-07 09:46:59 -0500292 params: dict = None,
Dominik Fleischmann847f3c02020-02-04 15:32:42 +0100293 db_dict: dict = None,
tierno53555f62020-04-07 11:08:16 +0000294 kdu_name: str = None,
beierlmf52cb7c2020-04-21 16:36:35 -0400295 namespace: str = None,
Adam Israeleef68932019-11-28 16:27:46 -0500296 ) -> bool:
Adam Israeld4ec83b2019-11-07 09:46:59 -0500297 """Install a bundle
298
299 :param cluster_uuid str: The UUID of the cluster to install to
300 :param kdu_model str: The name or path of a bundle to install
301 :param atomic bool: If set, waits until the model is active and resets
302 the cluster on failure.
303 :param timeout int: The time, in seconds, to wait for the install
304 to finish
305 :param params dict: Key-value pairs of instantiation parameters
Dominik Fleischmann847f3c02020-02-04 15:32:42 +0100306 :param kdu_name: Name of the KDU instance to be installed
tierno53555f62020-04-07 11:08:16 +0000307 :param namespace: K8s namespace to use for the KDU instance
Adam Israeld4ec83b2019-11-07 09:46:59 -0500308
309 :return: If successful, returns ?
310 """
311
David Garcia4f74f592020-07-23 15:04:19 +0200312 controller = await self.get_controller(cluster_uuid)
Adam Israeld4ec83b2019-11-07 09:46:59 -0500313
314 ##
Adam Israel3419aba2020-01-29 09:35:35 -0500315 # Get or create the model, based on the NS
Dominik Fleischmannad3a0542019-12-12 17:35:38 +0100316 # uuid.
Dominik Fleischmann847f3c02020-02-04 15:32:42 +0100317 if kdu_name:
318 kdu_instance = "{}-{}".format(kdu_name, db_dict["filter"]["_id"])
319 else:
320 kdu_instance = db_dict["filter"]["_id"]
Adam Israel40899212019-12-02 16:33:05 -0500321
Dominik Fleischmann847f3c02020-02-04 15:32:42 +0100322 self.log.debug("Checking for model named {}".format(kdu_instance))
Dominik Fleischmann1ac78b32020-02-26 19:58:25 +0100323
324 # Create the new model
325 self.log.debug("Adding model: {}".format(kdu_instance))
David Garcia4f74f592020-07-23 15:04:19 +0200326 model = await self.add_model(
327 kdu_instance, cluster_uuid=cluster_uuid, controller=controller
328 )
Adam Israeld4ec83b2019-11-07 09:46:59 -0500329
330 if model:
331 # TODO: Instantiation parameters
332
Adam Israeleef68932019-11-28 16:27:46 -0500333 """
334 "Juju bundle that models the KDU, in any of the following ways:
335 - <juju-repo>/<juju-bundle>
336 - <juju-bundle folder under k8s_models folder in the package>
beierlmf52cb7c2020-04-21 16:36:35 -0400337 - <juju-bundle tgz file (w/ or w/o extension) under k8s_models folder
338 in the package>
Adam Israeleef68932019-11-28 16:27:46 -0500339 - <URL_where_to_fetch_juju_bundle>
340 """
Dominik Fleischmanne85ba442020-05-28 14:33:22 +0200341 try:
342 previous_workdir = os.getcwd()
343 except FileNotFoundError:
344 previous_workdir = "/app/storage"
Dominik Fleischmann45d95772020-03-26 12:21:42 +0100345
Adam Israeleef68932019-11-28 16:27:46 -0500346 bundle = kdu_model
347 if kdu_model.startswith("cs:"):
348 bundle = kdu_model
349 elif kdu_model.startswith("http"):
350 # Download the file
351 pass
352 else:
Dominik Fleischmann45d95772020-03-26 12:21:42 +0100353 new_workdir = kdu_model.strip(kdu_model.split("/")[-1])
Adam Israeleef68932019-11-28 16:27:46 -0500354
Dominik Fleischmann45d95772020-03-26 12:21:42 +0100355 os.chdir(new_workdir)
356
357 bundle = "local:{}".format(kdu_model)
Adam Israeleef68932019-11-28 16:27:46 -0500358
359 if not bundle:
360 # Raise named exception that the bundle could not be found
361 raise Exception()
362
Dominik Fleischmann2f2832c2020-02-26 14:37:16 +0100363 self.log.debug("[install] deploying {}".format(bundle))
Adam Israeleef68932019-11-28 16:27:46 -0500364 await model.deploy(bundle)
Adam Israeld4ec83b2019-11-07 09:46:59 -0500365
366 # Get the application
367 if atomic:
368 # applications = model.applications
Dominik Fleischmann2f2832c2020-02-26 14:37:16 +0100369 self.log.debug("[install] Applications: {}".format(model.applications))
Adam Israeld4ec83b2019-11-07 09:46:59 -0500370 for name in model.applications:
Dominik Fleischmann2f2832c2020-02-26 14:37:16 +0100371 self.log.debug("[install] Waiting for {} to settle".format(name))
Adam Israeld4ec83b2019-11-07 09:46:59 -0500372 application = model.applications[name]
373 try:
374 # It's not enough to wait for all units to be active;
375 # the application status needs to be active as well.
Dominik Fleischmann2f2832c2020-02-26 14:37:16 +0100376 self.log.debug("Waiting for all units to be active...")
Adam Israeld4ec83b2019-11-07 09:46:59 -0500377 await model.block_until(
378 lambda: all(
beierlmf52cb7c2020-04-21 16:36:35 -0400379 unit.agent_status == "idle"
380 and application.status in ["active", "unknown"]
381 and unit.workload_status in ["active", "unknown"]
382 for unit in application.units
Adam Israeld4ec83b2019-11-07 09:46:59 -0500383 ),
beierlmf52cb7c2020-04-21 16:36:35 -0400384 timeout=timeout,
Adam Israeld4ec83b2019-11-07 09:46:59 -0500385 )
Dominik Fleischmann2f2832c2020-02-26 14:37:16 +0100386 self.log.debug("All units active.")
Adam Israeld4ec83b2019-11-07 09:46:59 -0500387
beierlmf52cb7c2020-04-21 16:36:35 -0400388 # TODO use asyncio.TimeoutError
389 except concurrent.futures._base.TimeoutError:
Dominik Fleischmann45d95772020-03-26 12:21:42 +0100390 os.chdir(previous_workdir)
Dominik Fleischmann2f2832c2020-02-26 14:37:16 +0100391 self.log.debug("[install] Timeout exceeded; resetting cluster")
Adam Israeld4ec83b2019-11-07 09:46:59 -0500392 await self.reset(cluster_uuid)
393 return False
394
395 # Wait for the application to be active
396 if model.is_connected():
Dominik Fleischmann2f2832c2020-02-26 14:37:16 +0100397 self.log.debug("[install] Disconnecting model")
Adam Israeld4ec83b2019-11-07 09:46:59 -0500398 await model.disconnect()
David Garcia4f74f592020-07-23 15:04:19 +0200399 await controller.disconnect()
Dominik Fleischmann45d95772020-03-26 12:21:42 +0100400 os.chdir(previous_workdir)
401
Dominik Fleischmann847f3c02020-02-04 15:32:42 +0100402 return kdu_instance
Adam Israeld4ec83b2019-11-07 09:46:59 -0500403 raise Exception("Unable to install")
404
beierlmf52cb7c2020-04-21 16:36:35 -0400405 async def instances_list(self, cluster_uuid: str) -> list:
Adam Israeld4ec83b2019-11-07 09:46:59 -0500406 """
407 returns a list of deployed releases in a cluster
408
409 :param cluster_uuid: the cluster
410 :return:
411 """
412 return []
413
414 async def upgrade(
415 self,
416 cluster_uuid: str,
417 kdu_instance: str,
418 kdu_model: str = None,
419 params: dict = None,
420 ) -> str:
421 """Upgrade a model
422
423 :param cluster_uuid str: The UUID of the cluster to upgrade
424 :param kdu_instance str: The unique name of the KDU instance
425 :param kdu_model str: The name or path of the bundle to upgrade to
426 :param params dict: Key-value pairs of instantiation parameters
427
428 :return: If successful, reference to the new revision number of the
429 KDU instance.
430 """
431
432 # TODO: Loop through the bundle and upgrade each charm individually
433
434 """
435 The API doesn't have a concept of bundle upgrades, because there are
436 many possible changes: charm revision, disk, number of units, etc.
437
438 As such, we are only supporting a limited subset of upgrades. We'll
439 upgrade the charm revision but leave storage and scale untouched.
440
441 Scale changes should happen through OSM constructs, and changes to
442 storage would require a redeployment of the service, at least in this
443 initial release.
444 """
beierlmf52cb7c2020-04-21 16:36:35 -0400445 raise MethodNotImplemented()
David Garcia4f74f592020-07-23 15:04:19 +0200446 # TODO: Remove these commented lines
447
448 # model = await self.get_model(namespace, cluster_uuid=cluster_uuid)
449
450 # model = None
451 # namespace = self.get_namespace(cluster_uuid)
452 # controller = await self.get_controller(cluster_uuid)
453
454 # try:
455 # if namespace not in await controller.list_models():
456 # raise N2VCNotFound(message="Model {} does not exist".format(namespace))
457
458 # model = await controller.get_model(namespace)
459 # with open(kdu_model, "r") as f:
460 # bundle = yaml.safe_load(f)
461
462 # """
463 # {
464 # 'description': 'Test bundle',
465 # 'bundle': 'kubernetes',
466 # 'applications': {
467 # 'mariadb-k8s': {
468 # 'charm': 'cs:~charmed-osm/mariadb-k8s-20',
469 # 'scale': 1,
470 # 'options': {
471 # 'password': 'manopw',
472 # 'root_password': 'osm4u',
473 # 'user': 'mano'
474 # },
475 # 'series': 'kubernetes'
476 # }
477 # }
478 # }
479 # """
480 # # TODO: This should be returned in an agreed-upon format
481 # for name in bundle["applications"]:
482 # self.log.debug(model.applications)
483 # application = model.applications[name]
484 # self.log.debug(application)
485
486 # path = bundle["applications"][name]["charm"]
487
488 # try:
489 # await application.upgrade_charm(switch=path)
490 # except juju.errors.JujuError as ex:
491 # if "already running charm" in str(ex):
492 # # We're already running this version
493 # pass
494 # finally:
495 # if model:
496 # await model.disconnect()
497 # await controller.disconnect()
498 # return True
Adam Israeld4ec83b2019-11-07 09:46:59 -0500499
500 """Rollback"""
beierlmf52cb7c2020-04-21 16:36:35 -0400501
Adam Israeld4ec83b2019-11-07 09:46:59 -0500502 async def rollback(
beierlmf52cb7c2020-04-21 16:36:35 -0400503 self, cluster_uuid: str, kdu_instance: str, revision: int = 0,
Adam Israeld4ec83b2019-11-07 09:46:59 -0500504 ) -> str:
505 """Rollback a model
506
507 :param cluster_uuid str: The UUID of the cluster to rollback
508 :param kdu_instance str: The unique name of the KDU instance
509 :param revision int: The revision to revert to. If omitted, rolls back
510 the previous upgrade.
511
512 :return: If successful, returns the revision of active KDU instance,
513 or raises an exception
514 """
beierlmf52cb7c2020-04-21 16:36:35 -0400515 raise MethodNotImplemented()
Adam Israeld4ec83b2019-11-07 09:46:59 -0500516
517 """Deletion"""
beierlmf52cb7c2020-04-21 16:36:35 -0400518
519 async def uninstall(self, cluster_uuid: str, kdu_instance: str) -> bool:
Adam Israeld4ec83b2019-11-07 09:46:59 -0500520 """Uninstall a KDU instance
521
Dominik Fleischmann847f3c02020-02-04 15:32:42 +0100522 :param cluster_uuid str: The UUID of the cluster
Adam Israeld4ec83b2019-11-07 09:46:59 -0500523 :param kdu_instance str: The unique name of the KDU instance
524
525 :return: Returns True if successful, or raises an exception
526 """
David Garcia4f74f592020-07-23 15:04:19 +0200527
528 controller = await self.get_controller(cluster_uuid)
Dominik Fleischmann1ac78b32020-02-26 19:58:25 +0100529
530 self.log.debug("[uninstall] Destroying model")
531
David Garcia4f74f592020-07-23 15:04:19 +0200532 await controller.destroy_models(kdu_instance)
Adam Israeld4ec83b2019-11-07 09:46:59 -0500533
Dominik Fleischmann1ac78b32020-02-26 19:58:25 +0100534 self.log.debug("[uninstall] Model destroyed and disconnecting")
David Garcia4f74f592020-07-23 15:04:19 +0200535 await controller.disconnect()
Dominik Fleischmann1ac78b32020-02-26 19:58:25 +0100536
Dominik Fleischmann847f3c02020-02-04 15:32:42 +0100537 return True
David Garcia4f74f592020-07-23 15:04:19 +0200538 # TODO: Remove these commented lines
539 # if not self.authenticated:
540 # self.log.debug("[uninstall] Connecting to controller")
541 # await self.login(cluster_uuid)
Adam Israeld4ec83b2019-11-07 09:46:59 -0500542
Dominik Fleischmannfc796cc2020-04-06 14:51:00 +0200543 async def exec_primitive(
544 self,
545 cluster_uuid: str = None,
546 kdu_instance: str = None,
547 primitive_name: str = None,
548 timeout: float = 300,
549 params: dict = None,
550 db_dict: dict = None,
551 ) -> str:
552 """Exec primitive (Juju action)
553
554 :param cluster_uuid str: The UUID of the cluster
555 :param kdu_instance str: The unique name of the KDU instance
556 :param primitive_name: Name of action that will be executed
557 :param timeout: Timeout for action execution
558 :param params: Dictionary of all the parameters needed for the action
559 :db_dict: Dictionary for any additional data
560
561 :return: Returns the output of the action
562 """
David Garcia4f74f592020-07-23 15:04:19 +0200563
564 controller = await self.get_controller(cluster_uuid)
Dominik Fleischmannfc796cc2020-04-06 14:51:00 +0200565
566 if not params or "application-name" not in params:
beierlmf52cb7c2020-04-21 16:36:35 -0400567 raise K8sException(
568 "Missing application-name argument, \
569 argument needed for K8s actions"
570 )
Dominik Fleischmannfc796cc2020-04-06 14:51:00 +0200571 try:
beierlmf52cb7c2020-04-21 16:36:35 -0400572 self.log.debug(
573 "[exec_primitive] Getting model "
574 "kdu_instance: {}".format(kdu_instance)
575 )
Dominik Fleischmannfc796cc2020-04-06 14:51:00 +0200576
David Garcia4f74f592020-07-23 15:04:19 +0200577 model = await self.get_model(kdu_instance, controller=controller)
Dominik Fleischmannfc796cc2020-04-06 14:51:00 +0200578
579 application_name = params["application-name"]
580 application = model.applications[application_name]
581
582 actions = await application.get_actions()
583 if primitive_name not in actions:
584 raise K8sException("Primitive {} not found".format(primitive_name))
585
586 unit = None
587 for u in application.units:
588 if await u.is_leader_from_status():
589 unit = u
590 break
591
592 if unit is None:
593 raise K8sException("No leader unit found to execute action")
594
595 self.log.debug("[exec_primitive] Running action: {}".format(primitive_name))
596 action = await unit.run_action(primitive_name, **params)
597
598 output = await model.get_action_output(action_uuid=action.entity_id)
599 status = await model.get_action_status(uuid_or_prefix=action.entity_id)
600
601 status = (
602 status[action.entity_id] if action.entity_id in status else "failed"
603 )
604
605 if status != "completed":
beierlmf52cb7c2020-04-21 16:36:35 -0400606 raise K8sException(
607 "status is not completed: {} output: {}".format(status, output)
608 )
Dominik Fleischmannfc796cc2020-04-06 14:51:00 +0200609
610 return output
611
612 except Exception as e:
613 error_msg = "Error executing primitive {}: {}".format(primitive_name, e)
614 self.log.error(error_msg)
615 raise K8sException(message=error_msg)
David Garcia4f74f592020-07-23 15:04:19 +0200616 finally:
617 await controller.disconnect()
618 # TODO: Remove these commented lines:
619 # if not self.authenticated:
620 # self.log.debug("[exec_primitive] Connecting to controller")
621 # await self.login(cluster_uuid)
Dominik Fleischmannfc796cc2020-04-06 14:51:00 +0200622
Adam Israeld4ec83b2019-11-07 09:46:59 -0500623 """Introspection"""
beierlmf52cb7c2020-04-21 16:36:35 -0400624
625 async def inspect_kdu(self, kdu_model: str,) -> dict:
Adam Israeld4ec83b2019-11-07 09:46:59 -0500626 """Inspect a KDU
627
628 Inspects a bundle and returns a dictionary of config parameters and
629 their default values.
630
631 :param kdu_model str: The name or path of the bundle to inspect.
632
633 :return: If successful, returns a dictionary of available parameters
634 and their default values.
635 """
636
637 kdu = {}
beierlmf52cb7c2020-04-21 16:36:35 -0400638 with open(kdu_model, "r") as f:
Adam Israeleef68932019-11-28 16:27:46 -0500639 bundle = yaml.safe_load(f)
Adam Israeld4ec83b2019-11-07 09:46:59 -0500640
641 """
642 {
643 'description': 'Test bundle',
644 'bundle': 'kubernetes',
645 'applications': {
646 'mariadb-k8s': {
647 'charm': 'cs:~charmed-osm/mariadb-k8s-20',
648 'scale': 1,
649 'options': {
650 'password': 'manopw',
651 'root_password': 'osm4u',
652 'user': 'mano'
653 },
654 'series': 'kubernetes'
655 }
656 }
657 }
658 """
659 # TODO: This should be returned in an agreed-upon format
beierlmf52cb7c2020-04-21 16:36:35 -0400660 kdu = bundle["applications"]
Adam Israeld4ec83b2019-11-07 09:46:59 -0500661
662 return kdu
663
beierlmf52cb7c2020-04-21 16:36:35 -0400664 async def help_kdu(self, kdu_model: str,) -> str:
Adam Israeld4ec83b2019-11-07 09:46:59 -0500665 """View the README
666
667 If available, returns the README of the bundle.
668
669 :param kdu_model str: The name or path of a bundle
670
671 :return: If found, returns the contents of the README.
672 """
673 readme = None
674
beierlmf52cb7c2020-04-21 16:36:35 -0400675 files = ["README", "README.txt", "README.md"]
Adam Israeld4ec83b2019-11-07 09:46:59 -0500676 path = os.path.dirname(kdu_model)
677 for file in os.listdir(path):
678 if file in files:
beierlmf52cb7c2020-04-21 16:36:35 -0400679 with open(file, "r") as f:
Adam Israeld4ec83b2019-11-07 09:46:59 -0500680 readme = f.read()
681 break
682
683 return readme
684
beierlmf52cb7c2020-04-21 16:36:35 -0400685 async def status_kdu(self, cluster_uuid: str, kdu_instance: str,) -> dict:
Adam Israeld4ec83b2019-11-07 09:46:59 -0500686 """Get the status of the KDU
687
688 Get the current status of the KDU instance.
689
690 :param cluster_uuid str: The UUID of the cluster
691 :param kdu_instance str: The unique id of the KDU instance
692
693 :return: Returns a dictionary containing namespace, state, resources,
694 and deployment_time.
695 """
696 status = {}
David Garcia4f74f592020-07-23 15:04:19 +0200697 controller = await self.get_controller(cluster_uuid)
698 model = await self.get_model(kdu_instance, controller=controller)
Adam Israeld4ec83b2019-11-07 09:46:59 -0500699
David Garcia4f74f592020-07-23 15:04:19 +0200700 model_status = await model.get_status()
701 status = model_status.applications
Adam Israeld4ec83b2019-11-07 09:46:59 -0500702
David Garcia4f74f592020-07-23 15:04:19 +0200703 for name in model_status.applications:
704 application = model_status.applications[name]
705 status[name] = {"status": application["status"]["status"]}
Adam Israeld4ec83b2019-11-07 09:46:59 -0500706
David Garcia4f74f592020-07-23 15:04:19 +0200707 await model.disconnect()
708 await controller.disconnect()
Adam Israeld4ec83b2019-11-07 09:46:59 -0500709
710 return status
711
David Garcia5d799392020-07-02 13:56:58 +0200712 async def get_services(
713 self, cluster_uuid: str, kdu_instance: str, namespace: str
714 ) -> list:
715 """Return a list of services of a kdu_instance"""
lloretgallegd99f3f22020-06-29 14:18:30 +0000716
David Garcia2c791b32020-07-22 17:56:12 +0200717 credentials = self.get_credentials(cluster_uuid=cluster_uuid)
718
719 config_path = "/tmp/{}".format(cluster_uuid)
720 config_file = "{}/config".format(config_path)
721
722 if not os.path.exists(config_path):
723 os.makedirs(config_path)
724 with open(config_file, "w") as f:
725 f.write(credentials)
726
David Garcia5d799392020-07-02 13:56:58 +0200727 kubectl = Kubectl(config_file=config_file)
728 return kubectl.get_services(
729 field_selector="metadata.namespace={}".format(kdu_instance)
730 )
731
732 async def get_service(
733 self, cluster_uuid: str, service_name: str, namespace: str
734 ) -> object:
735 """Return data for a specific service inside a namespace"""
736
David Garcia2c791b32020-07-22 17:56:12 +0200737 credentials = self.get_credentials(cluster_uuid=cluster_uuid)
738
739 config_path = "/tmp/{}".format(cluster_uuid)
740 config_file = "{}/config".format(config_path)
741
742 if not os.path.exists(config_path):
743 os.makedirs(config_path)
744 with open(config_file, "w") as f:
745 f.write(credentials)
746
David Garcia5d799392020-07-02 13:56:58 +0200747 kubectl = Kubectl(config_file=config_file)
748
749 return kubectl.get_services(
750 field_selector="metadata.name={},metadata.namespace={}".format(
751 service_name, namespace
752 )
753 )[0]
lloretgallegd99f3f22020-06-29 14:18:30 +0000754
Adam Israeld4ec83b2019-11-07 09:46:59 -0500755 # Private methods
beierlmf52cb7c2020-04-21 16:36:35 -0400756 async def add_k8s(self, cloud_name: str, credentials: str,) -> bool:
Adam Israeld4ec83b2019-11-07 09:46:59 -0500757 """Add a k8s cloud to Juju
758
759 Adds a Kubernetes cloud to Juju, so it can be bootstrapped with a
760 Juju Controller.
761
762 :param cloud_name str: The name of the cloud to add.
763 :param credentials dict: A dictionary representing the output of
764 `kubectl config view --raw`.
765
766 :returns: True if successful, otherwise raises an exception.
767 """
Adam Israeld4ec83b2019-11-07 09:46:59 -0500768
Adam Israel40899212019-12-02 16:33:05 -0500769 cmd = [self.juju_command, "add-k8s", "--local", cloud_name]
Dominik Fleischmann2f2832c2020-02-26 14:37:16 +0100770 self.log.debug(cmd)
Adam Israeld4ec83b2019-11-07 09:46:59 -0500771
Adam Israel3419aba2020-01-29 09:35:35 -0500772 process = await asyncio.create_subprocess_exec(
773 *cmd,
774 stdout=asyncio.subprocess.PIPE,
775 stderr=asyncio.subprocess.PIPE,
776 stdin=asyncio.subprocess.PIPE,
777 )
778
779 # Feed the process the credentials
780 process.stdin.write(credentials.encode("utf-8"))
781 await process.stdin.drain()
782 process.stdin.close()
783
beierlmf52cb7c2020-04-21 16:36:35 -0400784 _stdout, stderr = await process.communicate()
Adam Israel3419aba2020-01-29 09:35:35 -0500785
786 return_code = process.returncode
787
Dominik Fleischmann2f2832c2020-02-26 14:37:16 +0100788 self.log.debug("add-k8s return code: {}".format(return_code))
Adam Israel3419aba2020-01-29 09:35:35 -0500789
790 if return_code > 0:
791 raise Exception(stderr)
792
Adam Israeld4ec83b2019-11-07 09:46:59 -0500793 return True
794
David Garcia4f74f592020-07-23 15:04:19 +0200795 async def add_model(
796 self, model_name: str, cluster_uuid: str, controller: Controller
797 ) -> Model:
Adam Israeld4ec83b2019-11-07 09:46:59 -0500798 """Adds a model to the controller
799
800 Adds a new model to the Juju controller
801
802 :param model_name str: The name of the model to add.
David Garcia4f74f592020-07-23 15:04:19 +0200803 :param cluster_uuid str: ID of the cluster.
804 :param controller: Controller object in which the model will be added
Adam Israeld4ec83b2019-11-07 09:46:59 -0500805 :returns: The juju.model.Model object of the new model upon success or
806 raises an exception.
807 """
Adam Israeld4ec83b2019-11-07 09:46:59 -0500808
beierlmf52cb7c2020-04-21 16:36:35 -0400809 self.log.debug(
810 "Adding model '{}' to cluster_uuid '{}'".format(model_name, cluster_uuid)
811 )
tiernoe7b9a5b2020-07-17 11:47:32 +0000812 model = None
Dominik Fleischmann1ac78b32020-02-26 19:58:25 +0100813 try:
Dominik Fleischmann06bc9df2020-05-21 13:55:19 +0200814 if self.juju_public_key is not None:
David Garcia4f74f592020-07-23 15:04:19 +0200815 model = await controller.add_model(
Dominik Fleischmann06bc9df2020-05-21 13:55:19 +0200816 model_name, config={"authorized-keys": self.juju_public_key}
817 )
818 else:
David Garcia4f74f592020-07-23 15:04:19 +0200819 model = await controller.add_model(model_name)
Dominik Fleischmann1ac78b32020-02-26 19:58:25 +0100820 except Exception as ex:
821 self.log.debug(ex)
822 self.log.debug("Caught exception: {}".format(ex))
823 pass
824
Adam Israeld4ec83b2019-11-07 09:46:59 -0500825 return model
826
827 async def bootstrap(
beierlmf52cb7c2020-04-21 16:36:35 -0400828 self, cloud_name: str, cluster_uuid: str, loadbalancer: bool
Adam Israeld4ec83b2019-11-07 09:46:59 -0500829 ) -> bool:
830 """Bootstrap a Kubernetes controller
831
832 Bootstrap a Juju controller inside the Kubernetes cluster
833
834 :param cloud_name str: The name of the cloud.
835 :param cluster_uuid str: The UUID of the cluster to bootstrap.
David Garciaace992d2019-12-11 15:25:15 +0100836 :param loadbalancer bool: If the controller should use loadbalancer or not.
Adam Israeld4ec83b2019-11-07 09:46:59 -0500837 :returns: True upon success or raises an exception.
838 """
Adam Israel40899212019-12-02 16:33:05 -0500839
David Garciaace992d2019-12-11 15:25:15 +0100840 if not loadbalancer:
Adam Israel40899212019-12-02 16:33:05 -0500841 cmd = [self.juju_command, "bootstrap", cloud_name, cluster_uuid]
842 else:
843 """
beierlmf52cb7c2020-04-21 16:36:35 -0400844 For public clusters, specify that the controller service is using a
845 LoadBalancer.
Adam Israel40899212019-12-02 16:33:05 -0500846 """
beierlmf52cb7c2020-04-21 16:36:35 -0400847 cmd = [
848 self.juju_command,
849 "bootstrap",
850 cloud_name,
851 cluster_uuid,
852 "--config",
853 "controller-service-type=loadbalancer",
854 ]
Adam Israel40899212019-12-02 16:33:05 -0500855
beierlmf52cb7c2020-04-21 16:36:35 -0400856 self.log.debug(
857 "Bootstrapping controller {} in cloud {}".format(cluster_uuid, cloud_name)
Adam Israeld4ec83b2019-11-07 09:46:59 -0500858 )
Adam Israeld4ec83b2019-11-07 09:46:59 -0500859
beierlmf52cb7c2020-04-21 16:36:35 -0400860 process = await asyncio.create_subprocess_exec(
861 *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
862 )
863
864 _stdout, stderr = await process.communicate()
Adam Israel3419aba2020-01-29 09:35:35 -0500865
866 return_code = process.returncode
867
868 if return_code > 0:
Adam Israeld4ec83b2019-11-07 09:46:59 -0500869 #
beierlmf52cb7c2020-04-21 16:36:35 -0400870 if b"already exists" not in stderr:
Adam Israel3419aba2020-01-29 09:35:35 -0500871 raise Exception(stderr)
Adam Israeld4ec83b2019-11-07 09:46:59 -0500872
873 return True
874
beierlmf52cb7c2020-04-21 16:36:35 -0400875 async def destroy_controller(self, cluster_uuid: str) -> bool:
Adam Israeld4ec83b2019-11-07 09:46:59 -0500876 """Destroy a Kubernetes controller
877
878 Destroy an existing Kubernetes controller.
879
880 :param cluster_uuid str: The UUID of the cluster to bootstrap.
881 :returns: True upon success or raises an exception.
882 """
883 cmd = [
Adam Israel40899212019-12-02 16:33:05 -0500884 self.juju_command,
Adam Israeld4ec83b2019-11-07 09:46:59 -0500885 "destroy-controller",
886 "--destroy-all-models",
887 "--destroy-storage",
888 "-y",
beierlmf52cb7c2020-04-21 16:36:35 -0400889 cluster_uuid,
Adam Israeld4ec83b2019-11-07 09:46:59 -0500890 ]
891
Adam Israel3419aba2020-01-29 09:35:35 -0500892 process = await asyncio.create_subprocess_exec(
beierlmf52cb7c2020-04-21 16:36:35 -0400893 *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
Adam Israeld4ec83b2019-11-07 09:46:59 -0500894 )
Adam Israeld4ec83b2019-11-07 09:46:59 -0500895
beierlmf52cb7c2020-04-21 16:36:35 -0400896 _stdout, stderr = await process.communicate()
Adam Israel3419aba2020-01-29 09:35:35 -0500897
898 return_code = process.returncode
899
900 if return_code > 0:
Adam Israeld4ec83b2019-11-07 09:46:59 -0500901 #
beierlmf52cb7c2020-04-21 16:36:35 -0400902 if "already exists" not in stderr:
Adam Israel3419aba2020-01-29 09:35:35 -0500903 raise Exception(stderr)
Adam Israeld4ec83b2019-11-07 09:46:59 -0500904
David Garcia2c791b32020-07-22 17:56:12 +0200905 def get_credentials(self, cluster_uuid: str) -> str:
David Garcia5d799392020-07-02 13:56:58 +0200906 """
David Garcia2c791b32020-07-22 17:56:12 +0200907 Get Cluster Kubeconfig
David Garcia5d799392020-07-02 13:56:58 +0200908 """
David Garcia2c791b32020-07-22 17:56:12 +0200909 k8scluster = self.db.get_one(
910 "k8sclusters", q_filter={"_id": cluster_uuid}, fail_on_empty=False
911 )
912
913 self.db.encrypt_decrypt_fields(
914 k8scluster.get("credentials"),
915 "decrypt",
916 ["password", "secret"],
917 schema_version=k8scluster["schema_version"],
918 salt=k8scluster["_id"],
919 )
920
921 return yaml.safe_dump(k8scluster.get("credentials"))
David Garcia5d799392020-07-02 13:56:58 +0200922
beierlmf52cb7c2020-04-21 16:36:35 -0400923 def get_config(self, cluster_uuid: str,) -> dict:
Adam Israeld4ec83b2019-11-07 09:46:59 -0500924 """Get the cluster configuration
925
926 Gets the configuration of the cluster
927
928 :param cluster_uuid str: The UUID of the cluster.
929 :return: A dict upon success, or raises an exception.
930 """
David Garcia4f74f592020-07-23 15:04:19 +0200931
932 juju_db = self.db.get_one("admin", {"_id": "juju"})
933 config = None
934 for k in juju_db["k8sclusters"]:
935 if k["_id"] == cluster_uuid:
936 config = k["config"]
937 self.db.encrypt_decrypt_fields(
938 config,
939 "decrypt",
940 ["secret", "cacert"],
941 schema_version="1.1",
942 salt=k["_id"],
943 )
944 break
945 if not config:
Adam Israeld4ec83b2019-11-07 09:46:59 -0500946 raise Exception(
beierlmf52cb7c2020-04-21 16:36:35 -0400947 "Unable to locate configuration for cluster {}".format(cluster_uuid)
Adam Israeld4ec83b2019-11-07 09:46:59 -0500948 )
David Garcia4f74f592020-07-23 15:04:19 +0200949 return config
Adam Israeld4ec83b2019-11-07 09:46:59 -0500950
David Garcia4f74f592020-07-23 15:04:19 +0200951 async def get_model(self, model_name: str, controller: Controller) -> Model:
Adam Israeld4ec83b2019-11-07 09:46:59 -0500952 """Get a model from the Juju Controller.
953
954 Note: Model objects returned must call disconnected() before it goes
955 out of scope.
956
957 :param model_name str: The name of the model to get
David Garcia4f74f592020-07-23 15:04:19 +0200958 :param controller Controller: Controller object
Adam Israeld4ec83b2019-11-07 09:46:59 -0500959 :return The juju.model.Model object if found, or None.
960 """
Adam Israeld4ec83b2019-11-07 09:46:59 -0500961
David Garcia4f74f592020-07-23 15:04:19 +0200962 models = await controller.list_models()
963 if model_name not in models:
964 raise N2VCNotFound("Model {} not found".format(model_name))
965 self.log.debug("Found model: {}".format(model_name))
966 return await controller.get_model(model_name)
Adam Israeld4ec83b2019-11-07 09:46:59 -0500967
beierlmf52cb7c2020-04-21 16:36:35 -0400968 def get_namespace(self, cluster_uuid: str,) -> str:
Adam Israeld4ec83b2019-11-07 09:46:59 -0500969 """Get the namespace UUID
970 Gets the namespace's unique name
971
972 :param cluster_uuid str: The UUID of the cluster
973 :returns: The namespace UUID, or raises an exception
974 """
975 config = self.get_config(cluster_uuid)
976
977 # Make sure the name is in the config
beierlmf52cb7c2020-04-21 16:36:35 -0400978 if "namespace" not in config:
Adam Israeld4ec83b2019-11-07 09:46:59 -0500979 raise Exception("Namespace not found.")
980
981 # TODO: We want to make sure this is unique to the cluster, in case
982 # the cluster is being reused.
983 # Consider pre/appending the cluster id to the namespace string
beierlmf52cb7c2020-04-21 16:36:35 -0400984 return config["namespace"]
Adam Israeld4ec83b2019-11-07 09:46:59 -0500985
David Garcia4f74f592020-07-23 15:04:19 +0200986 # TODO: Remove these lines of code
987 # async def has_model(self, model_name: str) -> bool:
988 # """Check if a model exists in the controller
Adam Israeld4ec83b2019-11-07 09:46:59 -0500989
David Garcia4f74f592020-07-23 15:04:19 +0200990 # Checks to see if a model exists in the connected Juju controller.
Adam Israeld4ec83b2019-11-07 09:46:59 -0500991
David Garcia4f74f592020-07-23 15:04:19 +0200992 # :param model_name str: The name of the model
993 # :return: A boolean indicating if the model exists
994 # """
995 # models = await self.controller.list_models()
Adam Israeld4ec83b2019-11-07 09:46:59 -0500996
David Garcia4f74f592020-07-23 15:04:19 +0200997 # if model_name in models:
998 # return True
999 # return False
Adam Israeld4ec83b2019-11-07 09:46:59 -05001000
beierlmf52cb7c2020-04-21 16:36:35 -04001001 def is_local_k8s(self, credentials: str,) -> bool:
David Garciaace992d2019-12-11 15:25:15 +01001002 """Check if a cluster is local
Adam Israeld4ec83b2019-11-07 09:46:59 -05001003
David Garciaace992d2019-12-11 15:25:15 +01001004 Checks if a cluster is running in the local host
Adam Israeld4ec83b2019-11-07 09:46:59 -05001005
1006 :param credentials dict: A dictionary containing the k8s credentials
David Garciaace992d2019-12-11 15:25:15 +01001007 :returns: A boolean if the cluster is running locally
Adam Israeld4ec83b2019-11-07 09:46:59 -05001008 """
David Garciaace992d2019-12-11 15:25:15 +01001009
David Garcia81045962020-07-16 12:37:13 +02001010 creds = yaml.safe_load(credentials)
1011
1012 if creds and os.getenv("OSMLCM_VCA_APIPROXY"):
beierlmf52cb7c2020-04-21 16:36:35 -04001013 for cluster in creds["clusters"]:
1014 if "server" in cluster["cluster"]:
David Garcia81045962020-07-16 12:37:13 +02001015 if os.getenv("OSMLCM_VCA_APIPROXY") in cluster["cluster"]["server"]:
David Garciaace992d2019-12-11 15:25:15 +01001016 return True
Adam Israeld4ec83b2019-11-07 09:46:59 -05001017
1018 return False
1019
David Garcia4f74f592020-07-23 15:04:19 +02001020 async def get_controller(self, cluster_uuid):
Adam Israeld4ec83b2019-11-07 09:46:59 -05001021 """Login to the Juju controller."""
1022
Adam Israeleef68932019-11-28 16:27:46 -05001023 config = self.get_config(cluster_uuid)
1024
David Garcia4f74f592020-07-23 15:04:19 +02001025 juju_endpoint = config["endpoint"]
1026 juju_user = config["username"]
1027 juju_secret = config["secret"]
1028 juju_ca_cert = config["cacert"]
Adam Israeleef68932019-11-28 16:27:46 -05001029
David Garcia4f74f592020-07-23 15:04:19 +02001030 controller = Controller()
Adam Israeld4ec83b2019-11-07 09:46:59 -05001031
David Garcia4f74f592020-07-23 15:04:19 +02001032 if juju_secret:
Adam Israeld4ec83b2019-11-07 09:46:59 -05001033 self.log.debug(
David Garcia4f74f592020-07-23 15:04:19 +02001034 "Connecting to controller... ws://{} as {}".format(
1035 juju_endpoint, juju_user,
Adam Israeld4ec83b2019-11-07 09:46:59 -05001036 )
1037 )
1038 try:
David Garcia4f74f592020-07-23 15:04:19 +02001039 await controller.connect(
1040 endpoint=juju_endpoint,
1041 username=juju_user,
1042 password=juju_secret,
1043 cacert=juju_ca_cert,
Adam Israeld4ec83b2019-11-07 09:46:59 -05001044 )
Adam Israeld4ec83b2019-11-07 09:46:59 -05001045 self.log.debug("JujuApi: Logged into controller")
David Garcia4f74f592020-07-23 15:04:19 +02001046 return controller
Adam Israeld4ec83b2019-11-07 09:46:59 -05001047 except Exception as ex:
Dominik Fleischmann2f2832c2020-02-26 14:37:16 +01001048 self.log.debug(ex)
Adam Israeld4ec83b2019-11-07 09:46:59 -05001049 self.log.debug("Caught exception: {}".format(ex))
Adam Israeld4ec83b2019-11-07 09:46:59 -05001050 else:
1051 self.log.fatal("VCA credentials not configured.")
Adam Israeld4ec83b2019-11-07 09:46:59 -05001052
David Garcia4f74f592020-07-23 15:04:19 +02001053 # TODO: Remove these commented lines
1054 # self.authenticated = False
1055 # if self.authenticated:
1056 # return
Adam Israeld4ec83b2019-11-07 09:46:59 -05001057
David Garcia4f74f592020-07-23 15:04:19 +02001058 # self.connecting = True
1059 # juju_public_key = None
1060 # self.authenticated = True
1061 # Test: Make sure we have the credentials loaded
1062 # async def logout(self):
1063 # """Logout of the Juju controller."""
1064 # self.log.debug("[logout]")
1065 # if not self.authenticated:
1066 # return False
Adam Israeld4ec83b2019-11-07 09:46:59 -05001067
David Garcia4f74f592020-07-23 15:04:19 +02001068 # for model in self.models:
1069 # self.log.debug("Logging out of model {}".format(model))
1070 # await self.models[model].disconnect()
Adam Israeld4ec83b2019-11-07 09:46:59 -05001071
David Garcia4f74f592020-07-23 15:04:19 +02001072 # if self.controller:
1073 # self.log.debug("Disconnecting controller {}".format(self.controller))
1074 # await self.controller.disconnect()
1075 # self.controller = None
1076
1077 # self.authenticated = False
Adam Israeld4ec83b2019-11-07 09:46:59 -05001078
beierlmf52cb7c2020-04-21 16:36:35 -04001079 async def remove_cloud(self, cloud_name: str,) -> bool:
Adam Israeld4ec83b2019-11-07 09:46:59 -05001080 """Remove a k8s cloud from Juju
1081
1082 Removes a Kubernetes cloud from Juju.
1083
1084 :param cloud_name str: The name of the cloud to add.
1085
1086 :returns: True if successful, otherwise raises an exception.
1087 """
1088
1089 # Remove the bootstrapped controller
Adam Israel40899212019-12-02 16:33:05 -05001090 cmd = [self.juju_command, "remove-k8s", "--client", cloud_name]
Adam Israel3419aba2020-01-29 09:35:35 -05001091 process = await asyncio.create_subprocess_exec(
beierlmf52cb7c2020-04-21 16:36:35 -04001092 *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
Adam Israeld4ec83b2019-11-07 09:46:59 -05001093 )
Adam Israeld4ec83b2019-11-07 09:46:59 -05001094
beierlmf52cb7c2020-04-21 16:36:35 -04001095 _stdout, stderr = await process.communicate()
Adam Israel3419aba2020-01-29 09:35:35 -05001096
1097 return_code = process.returncode
1098
1099 if return_code > 0:
1100 raise Exception(stderr)
Adam Israeld4ec83b2019-11-07 09:46:59 -05001101
1102 # Remove the cloud from the local config
Adam Israel40899212019-12-02 16:33:05 -05001103 cmd = [self.juju_command, "remove-cloud", "--client", cloud_name]
Adam Israel3419aba2020-01-29 09:35:35 -05001104 process = await asyncio.create_subprocess_exec(
beierlmf52cb7c2020-04-21 16:36:35 -04001105 *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
Adam Israeld4ec83b2019-11-07 09:46:59 -05001106 )
Adam Israeld4ec83b2019-11-07 09:46:59 -05001107
beierlmf52cb7c2020-04-21 16:36:35 -04001108 _stdout, stderr = await process.communicate()
Adam Israel3419aba2020-01-29 09:35:35 -05001109
1110 return_code = process.returncode
1111
1112 if return_code > 0:
1113 raise Exception(stderr)
Adam Israeld4ec83b2019-11-07 09:46:59 -05001114
Adam Israeld4ec83b2019-11-07 09:46:59 -05001115 return True
1116
beierlmf52cb7c2020-04-21 16:36:35 -04001117 async def set_config(self, cluster_uuid: str, config: dict,) -> bool:
Adam Israeld4ec83b2019-11-07 09:46:59 -05001118 """Save the cluster configuration
1119
David Garcia4f74f592020-07-23 15:04:19 +02001120 Saves the cluster information to the Mongo database
Adam Israeld4ec83b2019-11-07 09:46:59 -05001121
1122 :param cluster_uuid str: The UUID of the cluster
1123 :param config dict: A dictionary containing the cluster configuration
Adam Israeld4ec83b2019-11-07 09:46:59 -05001124 """
1125
David Garcia4f74f592020-07-23 15:04:19 +02001126 juju_db = self.db.get_one("admin", {"_id": "juju"})
Adam Israeld4ec83b2019-11-07 09:46:59 -05001127
David Garcia4f74f592020-07-23 15:04:19 +02001128 k8sclusters = juju_db["k8sclusters"] if "k8sclusters" in juju_db else []
1129 self.db.encrypt_decrypt_fields(
1130 config,
1131 "encrypt",
1132 ["secret", "cacert"],
1133 schema_version="1.1",
1134 salt=cluster_uuid,
1135 )
1136 k8sclusters.append({"_id": cluster_uuid, "config": config})
1137 self.db.set_one(
1138 table="admin",
1139 q_filter={"_id": "juju"},
1140 update_dict={"k8sclusters": k8sclusters},
1141 )