blob: 053aaa82c06238c74bf719215a8eb1b1b87296e6 [file] [log] [blame]
David Garcia4fee80e2020-05-13 12:18:38 +02001# Copyright 2020 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
15import asyncio
16import logging
David Garciaeb8943a2021-04-12 12:07:37 +020017import typing
David Garciaf6e9b002020-11-27 15:32:02 +010018
David Garcia4fee80e2020-05-13 12:18:38 +020019import time
20
David Garciaf980ac02021-07-27 15:07:42 +020021import juju.errors
David Garcia4fee80e2020-05-13 12:18:38 +020022from juju.model import Model
23from juju.machine import Machine
24from juju.application import Application
David Garcia59f520d2020-10-15 13:16:45 +020025from juju.unit import Unit
David Garcia12b29242020-09-17 16:01:48 +020026from juju.client._definitions import (
27 FullStatus,
28 QueryApplicationOffersResults,
29 Cloud,
30 CloudCredential,
31)
David Garciaf6e9b002020-11-27 15:32:02 +010032from juju.controller import Controller
33from juju.client import client
34from juju import tag
35
David Garcia582b9232021-10-26 12:30:44 +020036from n2vc.definitions import Offer, RelationEndpoint
David Garcia4fee80e2020-05-13 12:18:38 +020037from n2vc.juju_watcher import JujuModelWatcher
38from n2vc.provisioner import AsyncSSHProvisioner
39from n2vc.n2vc_conn import N2VCConnector
40from n2vc.exceptions import (
41 JujuMachineNotFound,
42 JujuApplicationNotFound,
Dominik Fleischmann7ff392f2020-07-07 13:11:19 +020043 JujuLeaderUnitNotFound,
44 JujuActionNotFound,
David Garcia4fee80e2020-05-13 12:18:38 +020045 JujuControllerFailedConnecting,
46 JujuApplicationExists,
David Garcia475a7222020-09-21 16:19:15 +020047 JujuInvalidK8sConfiguration,
David Garciaeb8943a2021-04-12 12:07:37 +020048 JujuError,
David Garcia4fee80e2020-05-13 12:18:38 +020049)
David Garciaeb8943a2021-04-12 12:07:37 +020050from n2vc.vca.cloud import Cloud as VcaCloud
51from n2vc.vca.connection import Connection
David Garcia475a7222020-09-21 16:19:15 +020052from kubernetes.client.configuration import Configuration
David Garciaeb8943a2021-04-12 12:07:37 +020053from retrying_async import retry
54
David Garcia4fee80e2020-05-13 12:18:38 +020055
David Garciaf6e9b002020-11-27 15:32:02 +010056RBAC_LABEL_KEY_NAME = "rbac-id"
57
David Garcia4fee80e2020-05-13 12:18:38 +020058
59class Libjuju:
60 def __init__(
61 self,
David Garciaeb8943a2021-04-12 12:07:37 +020062 vca_connection: Connection,
David Garcia4fee80e2020-05-13 12:18:38 +020063 loop: asyncio.AbstractEventLoop = None,
64 log: logging.Logger = None,
David Garcia4fee80e2020-05-13 12:18:38 +020065 n2vc: N2VCConnector = None,
David Garcia4fee80e2020-05-13 12:18:38 +020066 ):
67 """
68 Constructor
69
David Garciaeb8943a2021-04-12 12:07:37 +020070 :param: vca_connection: n2vc.vca.connection object
David Garcia4fee80e2020-05-13 12:18:38 +020071 :param: loop: Asyncio loop
72 :param: log: Logger
David Garcia4fee80e2020-05-13 12:18:38 +020073 :param: n2vc: N2VC object
David Garcia4fee80e2020-05-13 12:18:38 +020074 """
75
David Garcia2f66c4d2020-06-19 11:40:18 +020076 self.log = log or logging.getLogger("Libjuju")
David Garcia4fee80e2020-05-13 12:18:38 +020077 self.n2vc = n2vc
David Garciaeb8943a2021-04-12 12:07:37 +020078 self.vca_connection = vca_connection
David Garcia4fee80e2020-05-13 12:18:38 +020079
David Garciaeb8943a2021-04-12 12:07:37 +020080 self.loop = loop or asyncio.get_event_loop()
David Garcia2f66c4d2020-06-19 11:40:18 +020081 self.loop.set_exception_handler(self.handle_exception)
David Garcia4fee80e2020-05-13 12:18:38 +020082 self.creating_model = asyncio.Lock(loop=self.loop)
83
David Garciaeb8943a2021-04-12 12:07:37 +020084 if self.vca_connection.is_default:
85 self.health_check_task = self._create_health_check_task()
David Garciaa4f57d62020-10-22 10:50:56 +020086
87 def _create_health_check_task(self):
88 return self.loop.create_task(self.health_check())
David Garcia4fee80e2020-05-13 12:18:38 +020089
David Garciaeb8943a2021-04-12 12:07:37 +020090 async def get_controller(self, timeout: float = 60.0) -> Controller:
David Garcia2f66c4d2020-06-19 11:40:18 +020091 """
92 Get controller
David Garcia4fee80e2020-05-13 12:18:38 +020093
David Garcia2f66c4d2020-06-19 11:40:18 +020094 :param: timeout: Time in seconds to wait for controller to connect
95 """
96 controller = None
97 try:
Pedro Escaleira86a63142022-04-05 21:01:37 +010098 controller = Controller()
David Garcia2f66c4d2020-06-19 11:40:18 +020099 await asyncio.wait_for(
100 controller.connect(
David Garciaeb8943a2021-04-12 12:07:37 +0200101 endpoint=self.vca_connection.data.endpoints,
102 username=self.vca_connection.data.user,
103 password=self.vca_connection.data.secret,
104 cacert=self.vca_connection.data.cacert,
David Garcia2f66c4d2020-06-19 11:40:18 +0200105 ),
106 timeout=timeout,
107 )
David Garciaeb8943a2021-04-12 12:07:37 +0200108 if self.vca_connection.is_default:
109 endpoints = await controller.api_endpoints
110 if not all(
111 endpoint in self.vca_connection.endpoints for endpoint in endpoints
112 ):
113 await self.vca_connection.update_endpoints(endpoints)
David Garcia2f66c4d2020-06-19 11:40:18 +0200114 return controller
115 except asyncio.CancelledError as e:
116 raise e
117 except Exception as e:
118 self.log.error(
David Garciaeb8943a2021-04-12 12:07:37 +0200119 "Failed connecting to controller: {}... {}".format(
120 self.vca_connection.data.endpoints, e
121 )
David Garcia2f66c4d2020-06-19 11:40:18 +0200122 )
123 if controller:
124 await self.disconnect_controller(controller)
Pedro Escaleira58326382022-05-30 19:08:41 +0100125
126 raise JujuControllerFailedConnecting(
127 f"Error connecting to Juju controller: {e}"
128 )
David Garcia4fee80e2020-05-13 12:18:38 +0200129
130 async def disconnect(self):
David Garcia2f66c4d2020-06-19 11:40:18 +0200131 """Disconnect"""
132 # Cancel health check task
133 self.health_check_task.cancel()
134 self.log.debug("Libjuju disconnected!")
David Garcia4fee80e2020-05-13 12:18:38 +0200135
136 async def disconnect_model(self, model: Model):
137 """
138 Disconnect model
139
140 :param: model: Model that will be disconnected
141 """
David Garcia2f66c4d2020-06-19 11:40:18 +0200142 await model.disconnect()
David Garcia4fee80e2020-05-13 12:18:38 +0200143
David Garcia2f66c4d2020-06-19 11:40:18 +0200144 async def disconnect_controller(self, controller: Controller):
David Garcia4fee80e2020-05-13 12:18:38 +0200145 """
David Garcia2f66c4d2020-06-19 11:40:18 +0200146 Disconnect controller
David Garcia4fee80e2020-05-13 12:18:38 +0200147
David Garcia2f66c4d2020-06-19 11:40:18 +0200148 :param: controller: Controller that will be disconnected
David Garcia4fee80e2020-05-13 12:18:38 +0200149 """
David Garcia667696e2020-09-22 14:52:32 +0200150 if controller:
151 await controller.disconnect()
David Garcia4fee80e2020-05-13 12:18:38 +0200152
David Garciaeb8943a2021-04-12 12:07:37 +0200153 @retry(attempts=3, delay=5, timeout=None)
154 async def add_model(self, model_name: str, cloud: VcaCloud):
David Garcia4fee80e2020-05-13 12:18:38 +0200155 """
156 Create model
157
158 :param: model_name: Model name
David Garciaeb8943a2021-04-12 12:07:37 +0200159 :param: cloud: Cloud object
David Garcia4fee80e2020-05-13 12:18:38 +0200160 """
161
David Garcia2f66c4d2020-06-19 11:40:18 +0200162 # Get controller
163 controller = await self.get_controller()
164 model = None
165 try:
David Garcia2f66c4d2020-06-19 11:40:18 +0200166 # Block until other workers have finished model creation
167 while self.creating_model.locked():
168 await asyncio.sleep(0.1)
David Garcia4fee80e2020-05-13 12:18:38 +0200169
David Garcia2f66c4d2020-06-19 11:40:18 +0200170 # Create the model
171 async with self.creating_model:
David Garciab0a8f402021-03-15 18:41:34 +0100172 if await self.model_exists(model_name, controller=controller):
173 return
David Garcia2f66c4d2020-06-19 11:40:18 +0200174 self.log.debug("Creating model {}".format(model_name))
175 model = await controller.add_model(
176 model_name,
David Garciaeb8943a2021-04-12 12:07:37 +0200177 config=self.vca_connection.data.model_config,
178 cloud_name=cloud.name,
179 credential_name=cloud.credential_name,
David Garcia2f66c4d2020-06-19 11:40:18 +0200180 )
David Garciaf980ac02021-07-27 15:07:42 +0200181 except juju.errors.JujuAPIError as e:
David Garciaeb8943a2021-04-12 12:07:37 +0200182 if "already exists" in e.message:
183 pass
184 else:
185 raise e
David Garcia2f66c4d2020-06-19 11:40:18 +0200186 finally:
187 if model:
188 await self.disconnect_model(model)
189 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +0200190
ksaikiranrcdf0b8e2021-03-17 12:50:00 +0530191 async def get_executed_actions(self, model_name: str) -> list:
192 """
193 Get executed/history of actions for a model.
194
195 :param: model_name: Model name, str.
196 :return: List of executed actions for a model.
197 """
198 model = None
199 executed_actions = []
200 controller = await self.get_controller()
201 try:
202 model = await self.get_model(controller, model_name)
203 # Get all unique action names
204 actions = {}
205 for application in model.applications:
206 application_actions = await self.get_actions(application, model_name)
207 actions.update(application_actions)
208 # Get status of all actions
209 for application_action in actions:
David Garciaeb8943a2021-04-12 12:07:37 +0200210 app_action_status_list = await model.get_action_status(
211 name=application_action
212 )
ksaikiranrcdf0b8e2021-03-17 12:50:00 +0530213 for action_id, action_status in app_action_status_list.items():
David Garciaeb8943a2021-04-12 12:07:37 +0200214 executed_action = {
215 "id": action_id,
216 "action": application_action,
217 "status": action_status,
218 }
ksaikiranrcdf0b8e2021-03-17 12:50:00 +0530219 # Get action output by id
220 action_status = await model.get_action_output(executed_action["id"])
221 for k, v in action_status.items():
222 executed_action[k] = v
223 executed_actions.append(executed_action)
224 except Exception as e:
David Garciaeb8943a2021-04-12 12:07:37 +0200225 raise JujuError(
226 "Error in getting executed actions for model: {}. Error: {}".format(
227 model_name, str(e)
228 )
229 )
ksaikiranrcdf0b8e2021-03-17 12:50:00 +0530230 finally:
231 if model:
232 await self.disconnect_model(model)
233 await self.disconnect_controller(controller)
234 return executed_actions
235
David Garciaeb8943a2021-04-12 12:07:37 +0200236 async def get_application_configs(
237 self, model_name: str, application_name: str
238 ) -> dict:
ksaikiranrcdf0b8e2021-03-17 12:50:00 +0530239 """
240 Get available configs for an application.
241
242 :param: model_name: Model name, str.
243 :param: application_name: Application name, str.
244
245 :return: A dict which has key - action name, value - action description
246 """
247 model = None
248 application_configs = {}
249 controller = await self.get_controller()
250 try:
251 model = await self.get_model(controller, model_name)
David Garciaeb8943a2021-04-12 12:07:37 +0200252 application = self._get_application(
253 model, application_name=application_name
254 )
ksaikiranrcdf0b8e2021-03-17 12:50:00 +0530255 application_configs = await application.get_config()
256 except Exception as e:
David Garciaeb8943a2021-04-12 12:07:37 +0200257 raise JujuError(
258 "Error in getting configs for application: {} in model: {}. Error: {}".format(
259 application_name, model_name, str(e)
260 )
261 )
ksaikiranrcdf0b8e2021-03-17 12:50:00 +0530262 finally:
263 if model:
264 await self.disconnect_model(model)
265 await self.disconnect_controller(controller)
266 return application_configs
267
David Garciaeb8943a2021-04-12 12:07:37 +0200268 @retry(attempts=3, delay=5)
269 async def get_model(self, controller: Controller, model_name: str) -> Model:
David Garcia4fee80e2020-05-13 12:18:38 +0200270 """
271 Get model from controller
272
David Garcia2f66c4d2020-06-19 11:40:18 +0200273 :param: controller: Controller
David Garcia4fee80e2020-05-13 12:18:38 +0200274 :param: model_name: Model name
275
276 :return: Model: The created Juju model object
277 """
David Garcia2f66c4d2020-06-19 11:40:18 +0200278 return await controller.get_model(model_name)
David Garcia4fee80e2020-05-13 12:18:38 +0200279
garciadeblas82b591c2021-03-24 09:22:13 +0100280 async def model_exists(
281 self, model_name: str, controller: Controller = None
282 ) -> bool:
David Garcia4fee80e2020-05-13 12:18:38 +0200283 """
284 Check if model exists
285
David Garcia2f66c4d2020-06-19 11:40:18 +0200286 :param: controller: Controller
David Garcia4fee80e2020-05-13 12:18:38 +0200287 :param: model_name: Model name
288
289 :return bool
290 """
David Garcia2f66c4d2020-06-19 11:40:18 +0200291 need_to_disconnect = False
David Garcia4fee80e2020-05-13 12:18:38 +0200292
David Garcia2f66c4d2020-06-19 11:40:18 +0200293 # Get controller if not passed
294 if not controller:
295 controller = await self.get_controller()
296 need_to_disconnect = True
David Garcia4fee80e2020-05-13 12:18:38 +0200297
David Garcia2f66c4d2020-06-19 11:40:18 +0200298 # Check if model exists
299 try:
300 return model_name in await controller.list_models()
301 finally:
302 if need_to_disconnect:
303 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +0200304
David Garcia42f328a2020-08-25 15:03:01 +0200305 async def models_exist(self, model_names: [str]) -> (bool, list):
306 """
307 Check if models exists
308
309 :param: model_names: List of strings with model names
310
311 :return (bool, list[str]): (True if all models exists, List of model names that don't exist)
312 """
313 if not model_names:
314 raise Exception(
David Garciac38a6962020-09-16 13:31:33 +0200315 "model_names must be a non-empty array. Given value: {}".format(
316 model_names
317 )
David Garcia42f328a2020-08-25 15:03:01 +0200318 )
319 non_existing_models = []
320 models = await self.list_models()
321 existing_models = list(set(models).intersection(model_names))
322 non_existing_models = list(set(model_names) - set(existing_models))
323
324 return (
325 len(non_existing_models) == 0,
326 non_existing_models,
327 )
328
David Garcia4fee80e2020-05-13 12:18:38 +0200329 async def get_model_status(self, model_name: str) -> FullStatus:
330 """
331 Get model status
332
333 :param: model_name: Model name
334
335 :return: Full status object
336 """
David Garcia2f66c4d2020-06-19 11:40:18 +0200337 controller = await self.get_controller()
338 model = await self.get_model(controller, model_name)
339 try:
340 return await model.get_status()
341 finally:
342 await self.disconnect_model(model)
343 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +0200344
345 async def create_machine(
346 self,
347 model_name: str,
348 machine_id: str = None,
349 db_dict: dict = None,
350 progress_timeout: float = None,
351 total_timeout: float = None,
David Garciaf643c132021-05-28 12:23:44 +0200352 series: str = "bionic",
David Garciaf8a9d462020-03-25 18:19:02 +0100353 wait: bool = True,
David Garcia4fee80e2020-05-13 12:18:38 +0200354 ) -> (Machine, bool):
355 """
356 Create machine
357
358 :param: model_name: Model name
359 :param: machine_id: Machine id
360 :param: db_dict: Dictionary with data of the DB to write the updates
361 :param: progress_timeout: Maximum time between two updates in the model
362 :param: total_timeout: Timeout for the entity to be active
David Garciaf8a9d462020-03-25 18:19:02 +0100363 :param: series: Series of the machine (xenial, bionic, focal, ...)
364 :param: wait: Wait until machine is ready
David Garcia4fee80e2020-05-13 12:18:38 +0200365
366 :return: (juju.machine.Machine, bool): Machine object and a boolean saying
367 if the machine is new or it already existed
368 """
369 new = False
370 machine = None
371
372 self.log.debug(
373 "Creating machine (id={}) in model: {}".format(machine_id, model_name)
374 )
375
David Garcia2f66c4d2020-06-19 11:40:18 +0200376 # Get controller
377 controller = await self.get_controller()
378
David Garcia4fee80e2020-05-13 12:18:38 +0200379 # Get model
David Garcia2f66c4d2020-06-19 11:40:18 +0200380 model = await self.get_model(controller, model_name)
David Garcia4fee80e2020-05-13 12:18:38 +0200381 try:
382 if machine_id is not None:
383 self.log.debug(
384 "Searching machine (id={}) in model {}".format(
385 machine_id, model_name
386 )
387 )
388
389 # Get machines from model and get the machine with machine_id if exists
390 machines = await model.get_machines()
391 if machine_id in machines:
392 self.log.debug(
393 "Machine (id={}) found in model {}".format(
394 machine_id, model_name
395 )
396 )
Dominik Fleischmann7ff392f2020-07-07 13:11:19 +0200397 machine = machines[machine_id]
David Garcia4fee80e2020-05-13 12:18:38 +0200398 else:
399 raise JujuMachineNotFound("Machine {} not found".format(machine_id))
400
401 if machine is None:
402 self.log.debug("Creating a new machine in model {}".format(model_name))
403
404 # Create machine
405 machine = await model.add_machine(
406 spec=None, constraints=None, disks=None, series=series
407 )
408 new = True
409
410 # Wait until the machine is ready
David Garcia2f66c4d2020-06-19 11:40:18 +0200411 self.log.debug(
412 "Wait until machine {} is ready in model {}".format(
413 machine.entity_id, model_name
414 )
415 )
David Garciaf8a9d462020-03-25 18:19:02 +0100416 if wait:
417 await JujuModelWatcher.wait_for(
418 model=model,
419 entity=machine,
420 progress_timeout=progress_timeout,
421 total_timeout=total_timeout,
422 db_dict=db_dict,
423 n2vc=self.n2vc,
David Garciaeb8943a2021-04-12 12:07:37 +0200424 vca_id=self.vca_connection._vca_id,
David Garciaf8a9d462020-03-25 18:19:02 +0100425 )
David Garcia4fee80e2020-05-13 12:18:38 +0200426 finally:
427 await self.disconnect_model(model)
David Garcia2f66c4d2020-06-19 11:40:18 +0200428 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +0200429
David Garcia2f66c4d2020-06-19 11:40:18 +0200430 self.log.debug(
431 "Machine {} ready at {} in model {}".format(
432 machine.entity_id, machine.dns_name, model_name
433 )
434 )
David Garcia4fee80e2020-05-13 12:18:38 +0200435 return machine, new
436
437 async def provision_machine(
438 self,
439 model_name: str,
440 hostname: str,
441 username: str,
442 private_key_path: str,
443 db_dict: dict = None,
444 progress_timeout: float = None,
445 total_timeout: float = None,
446 ) -> str:
447 """
448 Manually provisioning of a machine
449
450 :param: model_name: Model name
451 :param: hostname: IP to access the machine
452 :param: username: Username to login to the machine
453 :param: private_key_path: Local path for the private key
454 :param: db_dict: Dictionary with data of the DB to write the updates
455 :param: progress_timeout: Maximum time between two updates in the model
456 :param: total_timeout: Timeout for the entity to be active
457
458 :return: (Entity): Machine id
459 """
460 self.log.debug(
461 "Provisioning machine. model: {}, hostname: {}, username: {}".format(
462 model_name, hostname, username
463 )
464 )
465
David Garcia2f66c4d2020-06-19 11:40:18 +0200466 # Get controller
467 controller = await self.get_controller()
468
David Garcia4fee80e2020-05-13 12:18:38 +0200469 # Get model
David Garcia2f66c4d2020-06-19 11:40:18 +0200470 model = await self.get_model(controller, model_name)
David Garcia4fee80e2020-05-13 12:18:38 +0200471
472 try:
473 # Get provisioner
474 provisioner = AsyncSSHProvisioner(
475 host=hostname,
476 user=username,
477 private_key_path=private_key_path,
478 log=self.log,
479 )
480
481 # Provision machine
482 params = await provisioner.provision_machine()
483
484 params.jobs = ["JobHostUnits"]
485
486 self.log.debug("Adding machine to model")
487 connection = model.connection()
488 client_facade = client.ClientFacade.from_connection(connection)
489
490 results = await client_facade.AddMachines(params=[params])
491 error = results.machines[0].error
492
493 if error:
494 msg = "Error adding machine: {}".format(error.message)
495 self.log.error(msg=msg)
496 raise ValueError(msg)
497
498 machine_id = results.machines[0].machine
499
500 self.log.debug("Installing Juju agent into machine {}".format(machine_id))
501 asyncio.ensure_future(
502 provisioner.install_agent(
503 connection=connection,
504 nonce=params.nonce,
505 machine_id=machine_id,
David Garciaeb8943a2021-04-12 12:07:37 +0200506 proxy=self.vca_connection.data.api_proxy,
endikaf97b2312020-09-16 15:41:18 +0200507 series=params.series,
David Garcia4fee80e2020-05-13 12:18:38 +0200508 )
509 )
510
511 machine = None
512 for _ in range(10):
513 machine_list = await model.get_machines()
514 if machine_id in machine_list:
515 self.log.debug("Machine {} found in model!".format(machine_id))
516 machine = model.machines.get(machine_id)
517 break
518 await asyncio.sleep(2)
519
520 if machine is None:
521 msg = "Machine {} not found in model".format(machine_id)
522 self.log.error(msg=msg)
523 raise JujuMachineNotFound(msg)
524
David Garcia2f66c4d2020-06-19 11:40:18 +0200525 self.log.debug(
526 "Wait until machine {} is ready in model {}".format(
527 machine.entity_id, model_name
528 )
529 )
David Garcia4fee80e2020-05-13 12:18:38 +0200530 await JujuModelWatcher.wait_for(
531 model=model,
532 entity=machine,
533 progress_timeout=progress_timeout,
534 total_timeout=total_timeout,
535 db_dict=db_dict,
536 n2vc=self.n2vc,
David Garciaeb8943a2021-04-12 12:07:37 +0200537 vca_id=self.vca_connection._vca_id,
David Garcia4fee80e2020-05-13 12:18:38 +0200538 )
539 except Exception as e:
540 raise e
541 finally:
542 await self.disconnect_model(model)
David Garcia2f66c4d2020-06-19 11:40:18 +0200543 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +0200544
David Garcia2f66c4d2020-06-19 11:40:18 +0200545 self.log.debug(
546 "Machine provisioned {} in model {}".format(machine_id, model_name)
547 )
David Garcia4fee80e2020-05-13 12:18:38 +0200548
549 return machine_id
550
David Garcia667696e2020-09-22 14:52:32 +0200551 async def deploy(
552 self, uri: str, model_name: str, wait: bool = True, timeout: float = 3600
553 ):
554 """
555 Deploy bundle or charm: Similar to the juju CLI command `juju deploy`
556
557 :param: uri: Path or Charm Store uri in which the charm or bundle can be found
558 :param: model_name: Model name
559 :param: wait: Indicates whether to wait or not until all applications are active
560 :param: timeout: Time in seconds to wait until all applications are active
561 """
562 controller = await self.get_controller()
563 model = await self.get_model(controller, model_name)
564 try:
David Garcia76ed7572021-09-07 11:15:48 +0200565 await model.deploy(uri, trust=True)
David Garcia667696e2020-09-22 14:52:32 +0200566 if wait:
567 await JujuModelWatcher.wait_for_model(model, timeout=timeout)
568 self.log.debug("All units active in model {}".format(model_name))
569 finally:
570 await self.disconnect_model(model)
571 await self.disconnect_controller(controller)
572
aktasfa02f8a2021-07-29 17:41:40 +0300573 async def add_unit(
574 self,
575 application_name: str,
576 model_name: str,
577 machine_id: str,
578 db_dict: dict = None,
579 progress_timeout: float = None,
580 total_timeout: float = None,
581 ):
582 """Add unit
583
584 :param: application_name: Application name
585 :param: model_name: Model name
586 :param: machine_id Machine id
587 :param: db_dict: Dictionary with data of the DB to write the updates
588 :param: progress_timeout: Maximum time between two updates in the model
589 :param: total_timeout: Timeout for the entity to be active
590
591 :return: None
592 """
593
594 model = None
595 controller = await self.get_controller()
596 try:
597 model = await self.get_model(controller, model_name)
598 application = self._get_application(model, application_name)
599
600 if application is not None:
601
602 # Checks if the given machine id in the model,
603 # otherwise function raises an error
604 _machine, _series = self._get_machine_info(model, machine_id)
605
606 self.log.debug(
607 "Adding unit (machine {}) to application {} in model ~{}".format(
608 machine_id, application_name, model_name
609 )
610 )
611
612 await application.add_unit(to=machine_id)
613
614 await JujuModelWatcher.wait_for(
615 model=model,
616 entity=application,
617 progress_timeout=progress_timeout,
618 total_timeout=total_timeout,
619 db_dict=db_dict,
620 n2vc=self.n2vc,
621 vca_id=self.vca_connection._vca_id,
622 )
623 self.log.debug(
624 "Unit is added to application {} in model {}".format(
625 application_name, model_name
626 )
627 )
628 else:
629 raise JujuApplicationNotFound(
630 "Application {} not exists".format(application_name)
631 )
632 finally:
633 if model:
634 await self.disconnect_model(model)
635 await self.disconnect_controller(controller)
636
637 async def destroy_unit(
638 self,
639 application_name: str,
640 model_name: str,
641 machine_id: str,
642 total_timeout: float = None,
643 ):
644 """Destroy unit
645
646 :param: application_name: Application name
647 :param: model_name: Model name
648 :param: machine_id Machine id
aktasfa02f8a2021-07-29 17:41:40 +0300649 :param: total_timeout: Timeout for the entity to be active
650
651 :return: None
652 """
653
654 model = None
655 controller = await self.get_controller()
656 try:
657 model = await self.get_model(controller, model_name)
658 application = self._get_application(model, application_name)
659
660 if application is None:
661 raise JujuApplicationNotFound(
662 "Application not found: {} (model={})".format(
663 application_name, model_name
664 )
665 )
666
667 unit = self._get_unit(application, machine_id)
668 if not unit:
669 raise JujuError(
670 "A unit with machine id {} not in available units".format(
671 machine_id
672 )
673 )
674
675 unit_name = unit.name
676
677 self.log.debug(
678 "Destroying unit {} from application {} in model {}".format(
679 unit_name, application_name, model_name
680 )
681 )
682 await application.destroy_unit(unit_name)
683
684 self.log.debug(
685 "Waiting for unit {} to be destroyed in application {} (model={})...".format(
686 unit_name, application_name, model_name
687 )
688 )
689
690 # TODO: Add functionality in the Juju watcher to replace this kind of blocks
691 if total_timeout is None:
692 total_timeout = 3600
693 end = time.time() + total_timeout
694 while time.time() < end:
695 if not self._get_unit(application, machine_id):
696 self.log.debug(
697 "The unit {} was destroyed in application {} (model={}) ".format(
698 unit_name, application_name, model_name
699 )
700 )
701 return
702 await asyncio.sleep(5)
703 self.log.debug(
704 "Unit {} is destroyed from application {} in model {}".format(
705 unit_name, application_name, model_name
706 )
707 )
708 finally:
709 if model:
710 await self.disconnect_model(model)
711 await self.disconnect_controller(controller)
712
David Garcia4fee80e2020-05-13 12:18:38 +0200713 async def deploy_charm(
714 self,
715 application_name: str,
716 path: str,
717 model_name: str,
718 machine_id: str,
719 db_dict: dict = None,
720 progress_timeout: float = None,
721 total_timeout: float = None,
722 config: dict = None,
723 series: str = None,
David Garciaf8a9d462020-03-25 18:19:02 +0100724 num_units: int = 1,
David Garcia4fee80e2020-05-13 12:18:38 +0200725 ):
726 """Deploy charm
727
728 :param: application_name: Application name
729 :param: path: Local path to the charm
730 :param: model_name: Model name
731 :param: machine_id ID of the machine
732 :param: db_dict: Dictionary with data of the DB to write the updates
733 :param: progress_timeout: Maximum time between two updates in the model
734 :param: total_timeout: Timeout for the entity to be active
735 :param: config: Config for the charm
736 :param: series: Series of the charm
David Garciaf8a9d462020-03-25 18:19:02 +0100737 :param: num_units: Number of units
David Garcia4fee80e2020-05-13 12:18:38 +0200738
739 :return: (juju.application.Application): Juju application
740 """
David Garcia2f66c4d2020-06-19 11:40:18 +0200741 self.log.debug(
742 "Deploying charm {} to machine {} in model ~{}".format(
743 application_name, machine_id, model_name
744 )
745 )
746 self.log.debug("charm: {}".format(path))
747
748 # Get controller
749 controller = await self.get_controller()
David Garcia4fee80e2020-05-13 12:18:38 +0200750
751 # Get model
David Garcia2f66c4d2020-06-19 11:40:18 +0200752 model = await self.get_model(controller, model_name)
David Garcia4fee80e2020-05-13 12:18:38 +0200753
754 try:
David Garcia4fee80e2020-05-13 12:18:38 +0200755 if application_name not in model.applications:
David Garcia2f66c4d2020-06-19 11:40:18 +0200756
David Garcia4fee80e2020-05-13 12:18:38 +0200757 if machine_id is not None:
aktasfa02f8a2021-07-29 17:41:40 +0300758 machine, series = self._get_machine_info(model, machine_id)
David Garcia4fee80e2020-05-13 12:18:38 +0200759
760 application = await model.deploy(
761 entity_url=path,
762 application_name=application_name,
763 channel="stable",
764 num_units=1,
765 series=series,
766 to=machine_id,
767 config=config,
768 )
769
David Garcia2f66c4d2020-06-19 11:40:18 +0200770 self.log.debug(
771 "Wait until application {} is ready in model {}".format(
772 application_name, model_name
773 )
774 )
David Garciaf8a9d462020-03-25 18:19:02 +0100775 if num_units > 1:
776 for _ in range(num_units - 1):
777 m, _ = await self.create_machine(model_name, wait=False)
778 await application.add_unit(to=m.entity_id)
779
David Garcia4fee80e2020-05-13 12:18:38 +0200780 await JujuModelWatcher.wait_for(
781 model=model,
782 entity=application,
783 progress_timeout=progress_timeout,
784 total_timeout=total_timeout,
785 db_dict=db_dict,
786 n2vc=self.n2vc,
David Garciaeb8943a2021-04-12 12:07:37 +0200787 vca_id=self.vca_connection._vca_id,
David Garcia4fee80e2020-05-13 12:18:38 +0200788 )
David Garcia2f66c4d2020-06-19 11:40:18 +0200789 self.log.debug(
790 "Application {} is ready in model {}".format(
791 application_name, model_name
792 )
793 )
David Garcia4fee80e2020-05-13 12:18:38 +0200794 else:
David Garcia2f66c4d2020-06-19 11:40:18 +0200795 raise JujuApplicationExists(
796 "Application {} exists".format(application_name)
797 )
aktas42e51cf2021-10-19 20:03:23 +0300798 except juju.errors.JujuError as e:
799 if "already exists" in e.message:
800 raise JujuApplicationExists(
801 "Application {} exists".format(application_name)
802 )
803 else:
804 raise e
David Garcia4fee80e2020-05-13 12:18:38 +0200805 finally:
806 await self.disconnect_model(model)
David Garcia2f66c4d2020-06-19 11:40:18 +0200807 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +0200808
809 return application
810
beierlmdb1d37b2022-04-14 16:33:51 -0400811 async def upgrade_charm(
812 self,
813 application_name: str,
814 path: str,
815 model_name: str,
816 total_timeout: float = None,
817 **kwargs,
818 ):
819 """Upgrade Charm
820
821 :param: application_name: Application name
822 :param: model_name: Model name
823 :param: path: Local path to the charm
824 :param: total_timeout: Timeout for the entity to be active
825
826 :return: (str, str): (output and status)
827 """
828
829 self.log.debug(
830 "Upgrading charm {} in model {} from path {}".format(
831 application_name, model_name, path
832 )
833 )
834
835 await self.resolve_application(
836 model_name=model_name, application_name=application_name
837 )
838
839 # Get controller
840 controller = await self.get_controller()
841
842 # Get model
843 model = await self.get_model(controller, model_name)
844
845 try:
846 # Get application
847 application = self._get_application(
848 model,
849 application_name=application_name,
850 )
851 if application is None:
852 raise JujuApplicationNotFound(
853 "Cannot find application {} to upgrade".format(application_name)
854 )
855
856 await application.refresh(path=path)
857
858 self.log.debug(
859 "Wait until charm upgrade is completed for application {} (model={})".format(
860 application_name, model_name
861 )
862 )
863
864 await JujuModelWatcher.ensure_units_idle(
865 model=model, application=application
866 )
867
868 if application.status == "error":
869 error_message = "Unknown"
870 for unit in application.units:
871 if (
872 unit.workload_status == "error"
873 and unit.workload_status_message != ""
874 ):
875 error_message = unit.workload_status_message
876
877 message = "Application {} failed update in {}: {}".format(
878 application_name, model_name, error_message
879 )
880 self.log.error(message)
881 raise JujuError(message=message)
882
883 self.log.debug(
884 "Application {} is ready in model {}".format(
885 application_name, model_name
886 )
887 )
888
889 finally:
890 await self.disconnect_model(model)
891 await self.disconnect_controller(controller)
892
893 return application
894
895 async def resolve_application(self, model_name: str, application_name: str):
896
897 controller = await self.get_controller()
898 model = await self.get_model(controller, model_name)
899
900 try:
901 application = self._get_application(
902 model,
903 application_name=application_name,
904 )
905 if application is None:
906 raise JujuApplicationNotFound(
907 "Cannot find application {} to resolve".format(application_name)
908 )
909
910 while application.status == "error":
911 for unit in application.units:
912 if unit.workload_status == "error":
913 self.log.debug(
914 "Model {}, Application {}, Unit {} in error state, resolving".format(
915 model_name, application_name, unit.entity_id
916 )
917 )
918 try:
919 await unit.resolved(retry=False)
920 except Exception:
921 pass
922
923 await asyncio.sleep(1)
924
925 finally:
926 await self.disconnect_model(model)
927 await self.disconnect_controller(controller)
928
David Garciaf2e33832022-06-02 16:11:28 +0200929 async def resolve(self, model_name: str):
930
931 controller = await self.get_controller()
932 model = await self.get_model(controller, model_name)
933 all_units_active = False
934 try:
935 while not all_units_active:
936 all_units_active = True
937 for application_name, application in model.applications.items():
938 if application.status == "error":
939 for unit in application.units:
940 if unit.workload_status == "error":
941 self.log.debug(
942 "Model {}, Application {}, Unit {} in error state, resolving".format(
943 model_name, application_name, unit.entity_id
944 )
945 )
946 try:
947 await unit.resolved(retry=False)
948 all_units_active = False
949 except Exception:
950 pass
951
952 if not all_units_active:
953 await asyncio.sleep(5)
954 finally:
955 await self.disconnect_model(model)
956 await self.disconnect_controller(controller)
957
aktas2962f3e2021-03-15 11:05:35 +0300958 async def scale_application(
garciadeblas82b591c2021-03-24 09:22:13 +0100959 self,
960 model_name: str,
961 application_name: str,
962 scale: int = 1,
963 total_timeout: float = None,
aktas2962f3e2021-03-15 11:05:35 +0300964 ):
965 """
966 Scale application (K8s)
967
968 :param: model_name: Model name
969 :param: application_name: Application name
970 :param: scale: Scale to which to set this application
971 :param: total_timeout: Timeout for the entity to be active
972 """
973
974 model = None
975 controller = await self.get_controller()
976 try:
977 model = await self.get_model(controller, model_name)
978
979 self.log.debug(
980 "Scaling application {} in model {}".format(
981 application_name, model_name
982 )
983 )
984 application = self._get_application(model, application_name)
985 if application is None:
986 raise JujuApplicationNotFound("Cannot scale application")
987 await application.scale(scale=scale)
988 # Wait until application is scaled in model
989 self.log.debug(
garciadeblas82b591c2021-03-24 09:22:13 +0100990 "Waiting for application {} to be scaled in model {}...".format(
aktas2962f3e2021-03-15 11:05:35 +0300991 application_name, model_name
992 )
993 )
994 if total_timeout is None:
995 total_timeout = 1800
996 end = time.time() + total_timeout
997 while time.time() < end:
998 application_scale = self._get_application_count(model, application_name)
999 # Before calling wait_for_model function,
1000 # wait until application unit count and scale count are equal.
1001 # Because there is a delay before scaling triggers in Juju model.
1002 if application_scale == scale:
garciadeblas82b591c2021-03-24 09:22:13 +01001003 await JujuModelWatcher.wait_for_model(
1004 model=model, timeout=total_timeout
1005 )
aktas2962f3e2021-03-15 11:05:35 +03001006 self.log.debug(
1007 "Application {} is scaled in model {}".format(
1008 application_name, model_name
1009 )
1010 )
1011 return
1012 await asyncio.sleep(5)
1013 raise Exception(
1014 "Timeout waiting for application {} in model {} to be scaled".format(
1015 application_name, model_name
1016 )
1017 )
1018 finally:
1019 if model:
1020 await self.disconnect_model(model)
1021 await self.disconnect_controller(controller)
1022
1023 def _get_application_count(self, model: Model, application_name: str) -> int:
1024 """Get number of units of the application
1025
1026 :param: model: Model object
1027 :param: application_name: Application name
1028
1029 :return: int (or None if application doesn't exist)
1030 """
1031 application = self._get_application(model, application_name)
1032 if application is not None:
1033 return len(application.units)
1034
David Garcia2f66c4d2020-06-19 11:40:18 +02001035 def _get_application(self, model: Model, application_name: str) -> Application:
David Garcia4fee80e2020-05-13 12:18:38 +02001036 """Get application
1037
1038 :param: model: Model object
1039 :param: application_name: Application name
1040
1041 :return: juju.application.Application (or None if it doesn't exist)
1042 """
1043 if model.applications and application_name in model.applications:
1044 return model.applications[application_name]
1045
aktasfa02f8a2021-07-29 17:41:40 +03001046 def _get_unit(self, application: Application, machine_id: str) -> Unit:
1047 """Get unit
1048
1049 :param: application: Application object
1050 :param: machine_id: Machine id
1051
1052 :return: Unit
1053 """
1054 unit = None
1055 for u in application.units:
1056 if u.machine_id == machine_id:
1057 unit = u
1058 break
1059 return unit
1060
1061 def _get_machine_info(
1062 self,
1063 model,
1064 machine_id: str,
1065 ) -> (str, str):
1066 """Get machine info
1067
1068 :param: model: Model object
1069 :param: machine_id: Machine id
1070
1071 :return: (str, str): (machine, series)
1072 """
1073 if machine_id not in model.machines:
1074 msg = "Machine {} not found in model".format(machine_id)
1075 self.log.error(msg=msg)
1076 raise JujuMachineNotFound(msg)
1077 machine = model.machines[machine_id]
1078 return machine, machine.series
1079
David Garcia4fee80e2020-05-13 12:18:38 +02001080 async def execute_action(
1081 self,
1082 application_name: str,
1083 model_name: str,
1084 action_name: str,
1085 db_dict: dict = None,
aktasfa02f8a2021-07-29 17:41:40 +03001086 machine_id: str = None,
David Garcia4fee80e2020-05-13 12:18:38 +02001087 progress_timeout: float = None,
1088 total_timeout: float = None,
David Garciaf980ac02021-07-27 15:07:42 +02001089 **kwargs,
David Garcia4fee80e2020-05-13 12:18:38 +02001090 ):
1091 """Execute action
1092
1093 :param: application_name: Application name
1094 :param: model_name: Model name
David Garcia4fee80e2020-05-13 12:18:38 +02001095 :param: action_name: Name of the action
1096 :param: db_dict: Dictionary with data of the DB to write the updates
aktasfa02f8a2021-07-29 17:41:40 +03001097 :param: machine_id Machine id
David Garcia4fee80e2020-05-13 12:18:38 +02001098 :param: progress_timeout: Maximum time between two updates in the model
1099 :param: total_timeout: Timeout for the entity to be active
1100
1101 :return: (str, str): (output and status)
1102 """
David Garcia2f66c4d2020-06-19 11:40:18 +02001103 self.log.debug(
1104 "Executing action {} using params {}".format(action_name, kwargs)
1105 )
1106 # Get controller
1107 controller = await self.get_controller()
1108
1109 # Get model
1110 model = await self.get_model(controller, model_name)
David Garcia4fee80e2020-05-13 12:18:38 +02001111
1112 try:
1113 # Get application
David Garcia2f66c4d2020-06-19 11:40:18 +02001114 application = self._get_application(
David Garciaf6e9b002020-11-27 15:32:02 +01001115 model,
1116 application_name=application_name,
David Garcia4fee80e2020-05-13 12:18:38 +02001117 )
1118 if application is None:
1119 raise JujuApplicationNotFound("Cannot execute action")
David Garcia59f520d2020-10-15 13:16:45 +02001120 # Racing condition:
1121 # Ocassionally, self._get_leader_unit() will return None
1122 # because the leader elected hook has not been triggered yet.
1123 # Therefore, we are doing some retries. If it happens again,
1124 # re-open bug 1236
aktasfa02f8a2021-07-29 17:41:40 +03001125 if machine_id is None:
1126 unit = await self._get_leader_unit(application)
1127 self.log.debug(
1128 "Action {} is being executed on the leader unit {}".format(
1129 action_name, unit.name
1130 )
1131 )
1132 else:
1133 unit = self._get_unit(application, machine_id)
1134 if not unit:
1135 raise JujuError(
1136 "A unit with machine id {} not in available units".format(
1137 machine_id
1138 )
1139 )
1140 self.log.debug(
1141 "Action {} is being executed on {} unit".format(
1142 action_name, unit.name
1143 )
1144 )
David Garcia4fee80e2020-05-13 12:18:38 +02001145
1146 actions = await application.get_actions()
1147
1148 if action_name not in actions:
Dominik Fleischmann7ff392f2020-07-07 13:11:19 +02001149 raise JujuActionNotFound(
David Garcia4fee80e2020-05-13 12:18:38 +02001150 "Action {} not in available actions".format(action_name)
1151 )
1152
David Garcia4fee80e2020-05-13 12:18:38 +02001153 action = await unit.run_action(action_name, **kwargs)
1154
David Garcia2f66c4d2020-06-19 11:40:18 +02001155 self.log.debug(
1156 "Wait until action {} is completed in application {} (model={})".format(
1157 action_name, application_name, model_name
1158 )
1159 )
David Garcia4fee80e2020-05-13 12:18:38 +02001160 await JujuModelWatcher.wait_for(
1161 model=model,
1162 entity=action,
1163 progress_timeout=progress_timeout,
1164 total_timeout=total_timeout,
1165 db_dict=db_dict,
1166 n2vc=self.n2vc,
David Garciaeb8943a2021-04-12 12:07:37 +02001167 vca_id=self.vca_connection._vca_id,
David Garcia4fee80e2020-05-13 12:18:38 +02001168 )
David Garcia2f66c4d2020-06-19 11:40:18 +02001169
David Garcia4fee80e2020-05-13 12:18:38 +02001170 output = await model.get_action_output(action_uuid=action.entity_id)
1171 status = await model.get_action_status(uuid_or_prefix=action.entity_id)
1172 status = (
1173 status[action.entity_id] if action.entity_id in status else "failed"
1174 )
1175
David Garcia2f66c4d2020-06-19 11:40:18 +02001176 self.log.debug(
1177 "Action {} completed with status {} in application {} (model={})".format(
1178 action_name, action.status, application_name, model_name
1179 )
1180 )
David Garcia4fee80e2020-05-13 12:18:38 +02001181 finally:
1182 await self.disconnect_model(model)
David Garcia2f66c4d2020-06-19 11:40:18 +02001183 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +02001184
1185 return output, status
1186
1187 async def get_actions(self, application_name: str, model_name: str) -> dict:
1188 """Get list of actions
1189
1190 :param: application_name: Application name
1191 :param: model_name: Model name
1192
1193 :return: Dict with this format
1194 {
1195 "action_name": "Description of the action",
1196 ...
1197 }
1198 """
David Garcia2f66c4d2020-06-19 11:40:18 +02001199 self.log.debug(
1200 "Getting list of actions for application {}".format(application_name)
David Garcia4fee80e2020-05-13 12:18:38 +02001201 )
1202
David Garcia2f66c4d2020-06-19 11:40:18 +02001203 # Get controller
1204 controller = await self.get_controller()
David Garcia4fee80e2020-05-13 12:18:38 +02001205
David Garcia2f66c4d2020-06-19 11:40:18 +02001206 # Get model
1207 model = await self.get_model(controller, model_name)
David Garcia4fee80e2020-05-13 12:18:38 +02001208
David Garcia2f66c4d2020-06-19 11:40:18 +02001209 try:
1210 # Get application
1211 application = self._get_application(
David Garciaf6e9b002020-11-27 15:32:02 +01001212 model,
1213 application_name=application_name,
David Garcia2f66c4d2020-06-19 11:40:18 +02001214 )
1215
1216 # Return list of actions
1217 return await application.get_actions()
1218
1219 finally:
1220 # Disconnect from model and controller
1221 await self.disconnect_model(model)
1222 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +02001223
David Garcia85755d12020-09-21 19:51:23 +02001224 async def get_metrics(self, model_name: str, application_name: str) -> dict:
1225 """Get the metrics collected by the VCA.
1226
1227 :param model_name The name or unique id of the network service
1228 :param application_name The name of the application
1229 """
1230 if not model_name or not application_name:
1231 raise Exception("model_name and application_name must be non-empty strings")
1232 metrics = {}
1233 controller = await self.get_controller()
1234 model = await self.get_model(controller, model_name)
1235 try:
1236 application = self._get_application(model, application_name)
1237 if application is not None:
1238 metrics = await application.get_metrics()
1239 finally:
1240 self.disconnect_model(model)
1241 self.disconnect_controller(controller)
1242 return metrics
1243
David Garcia4fee80e2020-05-13 12:18:38 +02001244 async def add_relation(
David Garciaf6e9b002020-11-27 15:32:02 +01001245 self,
1246 model_name: str,
1247 endpoint_1: str,
1248 endpoint_2: str,
David Garcia4fee80e2020-05-13 12:18:38 +02001249 ):
1250 """Add relation
1251
David Garcia8331f7c2020-08-25 16:10:07 +02001252 :param: model_name: Model name
1253 :param: endpoint_1 First endpoint name
1254 ("app:endpoint" format or directly the saas name)
1255 :param: endpoint_2: Second endpoint name (^ same format)
David Garcia4fee80e2020-05-13 12:18:38 +02001256 """
1257
David Garcia8331f7c2020-08-25 16:10:07 +02001258 self.log.debug("Adding relation: {} -> {}".format(endpoint_1, endpoint_2))
David Garcia2f66c4d2020-06-19 11:40:18 +02001259
1260 # Get controller
1261 controller = await self.get_controller()
1262
David Garcia4fee80e2020-05-13 12:18:38 +02001263 # Get model
David Garcia2f66c4d2020-06-19 11:40:18 +02001264 model = await self.get_model(controller, model_name)
David Garcia4fee80e2020-05-13 12:18:38 +02001265
David Garcia4fee80e2020-05-13 12:18:38 +02001266 # Add relation
David Garcia4fee80e2020-05-13 12:18:38 +02001267 try:
David Garcia8331f7c2020-08-25 16:10:07 +02001268 await model.add_relation(endpoint_1, endpoint_2)
David Garciaf980ac02021-07-27 15:07:42 +02001269 except juju.errors.JujuAPIError as e:
Patricia Reinosoa07f6952023-01-04 10:40:10 +00001270 if self._relation_is_not_found(e):
David Garcia4fee80e2020-05-13 12:18:38 +02001271 self.log.warning("Relation not found: {}".format(e.message))
1272 return
Patricia Reinosoa07f6952023-01-04 10:40:10 +00001273 if self._relation_already_exist(e):
David Garcia4fee80e2020-05-13 12:18:38 +02001274 self.log.warning("Relation already exists: {}".format(e.message))
1275 return
1276 # another exception, raise it
1277 raise e
1278 finally:
1279 await self.disconnect_model(model)
David Garcia2f66c4d2020-06-19 11:40:18 +02001280 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +02001281
Patricia Reinosoa07f6952023-01-04 10:40:10 +00001282 def _relation_is_not_found(self, juju_error):
1283 text = "not found"
1284 return (text in juju_error.message) or (
1285 juju_error.error_code and text in juju_error.error_code
1286 )
1287
1288 def _relation_already_exist(self, juju_error):
1289 text = "already exists"
1290 return (text in juju_error.message) or (
1291 juju_error.error_code and text in juju_error.error_code
1292 )
1293
David Garcia582b9232021-10-26 12:30:44 +02001294 async def offer(self, endpoint: RelationEndpoint) -> Offer:
1295 """
1296 Create an offer from a RelationEndpoint
1297
1298 :param: endpoint: Relation endpoint
1299
1300 :return: Offer object
1301 """
1302 model_name = endpoint.model_name
1303 offer_name = f"{endpoint.application_name}-{endpoint.endpoint_name}"
1304 controller = await self.get_controller()
1305 model = None
1306 try:
1307 model = await self.get_model(controller, model_name)
1308 await model.create_offer(endpoint.endpoint, offer_name=offer_name)
1309 offer_list = await self._list_offers(model_name, offer_name=offer_name)
1310 if offer_list:
1311 return Offer(offer_list[0].offer_url)
1312 else:
1313 raise Exception("offer was not created")
1314 except juju.errors.JujuError as e:
1315 if "application offer already exists" not in e.message:
1316 raise e
1317 finally:
1318 if model:
1319 self.disconnect_model(model)
1320 self.disconnect_controller(controller)
1321
David Garcia68b00722020-09-11 15:05:00 +02001322 async def consume(
David Garciaf6e9b002020-11-27 15:32:02 +01001323 self,
David Garciaf6e9b002020-11-27 15:32:02 +01001324 model_name: str,
David Garcia582b9232021-10-26 12:30:44 +02001325 offer: Offer,
1326 provider_libjuju: "Libjuju",
1327 ) -> str:
David Garcia68b00722020-09-11 15:05:00 +02001328 """
David Garcia582b9232021-10-26 12:30:44 +02001329 Consumes a remote offer in the model. Relations can be created later using "juju relate".
David Garcia68b00722020-09-11 15:05:00 +02001330
David Garcia582b9232021-10-26 12:30:44 +02001331 :param: model_name: Model name
1332 :param: offer: Offer object to consume
1333 :param: provider_libjuju: Libjuju object of the provider endpoint
David Garcia68b00722020-09-11 15:05:00 +02001334
1335 :raises ParseError if there's a problem parsing the offer_url
1336 :raises JujuError if remote offer includes and endpoint
1337 :raises JujuAPIError if the operation is not successful
David Garcia68b00722020-09-11 15:05:00 +02001338
David Garcia582b9232021-10-26 12:30:44 +02001339 :returns: Saas name. It is the application name in the model that reference the remote application.
1340 """
1341 saas_name = f'{offer.name}-{offer.model_name.replace("-", "")}'
1342 if offer.vca_id:
1343 saas_name = f"{saas_name}-{offer.vca_id}"
1344 controller = await self.get_controller()
1345 model = None
1346 provider_controller = None
David Garcia68b00722020-09-11 15:05:00 +02001347 try:
David Garcia582b9232021-10-26 12:30:44 +02001348 model = await controller.get_model(model_name)
1349 provider_controller = await provider_libjuju.get_controller()
1350 await model.consume(
1351 offer.url, application_alias=saas_name, controller=provider_controller
1352 )
1353 return saas_name
David Garcia68b00722020-09-11 15:05:00 +02001354 finally:
David Garcia582b9232021-10-26 12:30:44 +02001355 if model:
1356 await self.disconnect_model(model)
1357 if provider_controller:
1358 await provider_libjuju.disconnect_controller(provider_controller)
David Garcia68b00722020-09-11 15:05:00 +02001359 await self.disconnect_controller(controller)
1360
David Garciae610aed2021-07-26 15:04:37 +02001361 async def destroy_model(self, model_name: str, total_timeout: float = 1800):
David Garcia4fee80e2020-05-13 12:18:38 +02001362 """
1363 Destroy model
1364
1365 :param: model_name: Model name
1366 :param: total_timeout: Timeout
1367 """
David Garcia4fee80e2020-05-13 12:18:38 +02001368
David Garcia2f66c4d2020-06-19 11:40:18 +02001369 controller = await self.get_controller()
David Garcia435b8642021-03-10 17:09:44 +01001370 model = None
David Garcia2f66c4d2020-06-19 11:40:18 +02001371 try:
David Garciab0a8f402021-03-15 18:41:34 +01001372 if not await self.model_exists(model_name, controller=controller):
David Garcia1608b562022-05-06 12:26:20 +02001373 self.log.warn(f"Model {model_name} doesn't exist")
David Garciab0a8f402021-03-15 18:41:34 +01001374 return
1375
David Garcia1608b562022-05-06 12:26:20 +02001376 self.log.debug(f"Getting model {model_name} to be destroyed")
David Garciae610aed2021-07-26 15:04:37 +02001377 model = await self.get_model(controller, model_name)
David Garcia1608b562022-05-06 12:26:20 +02001378 self.log.debug(f"Destroying manual machines in model {model_name}")
David Garcia168bb192020-10-21 14:19:45 +02001379 # Destroy machines that are manually provisioned
1380 # and still are in pending state
1381 await self._destroy_pending_machines(model, only_manual=True)
David Garcia2f66c4d2020-06-19 11:40:18 +02001382 await self.disconnect_model(model)
1383
David Garciaf2e33832022-06-02 16:11:28 +02001384 await asyncio.wait_for(
1385 self._destroy_model(model_name, controller),
David Garciae610aed2021-07-26 15:04:37 +02001386 timeout=total_timeout,
1387 )
David Garcia5c966622022-05-03 12:23:59 +02001388 except Exception as e:
1389 if not await self.model_exists(model_name, controller=controller):
David Garcia1608b562022-05-06 12:26:20 +02001390 self.log.warn(
1391 f"Failed deleting model {model_name}: model doesn't exist"
1392 )
David Garcia5c966622022-05-03 12:23:59 +02001393 return
David Garcia1608b562022-05-06 12:26:20 +02001394 self.log.warn(f"Failed deleting model {model_name}: {e}")
David Garcia5c966622022-05-03 12:23:59 +02001395 raise e
David Garciae610aed2021-07-26 15:04:37 +02001396 finally:
1397 if model:
1398 await self.disconnect_model(model)
1399 await self.disconnect_controller(controller)
David Garcia2f66c4d2020-06-19 11:40:18 +02001400
David Garciae610aed2021-07-26 15:04:37 +02001401 async def _destroy_model(
David Garciaf2e33832022-06-02 16:11:28 +02001402 self,
1403 model_name: str,
1404 controller: Controller,
David Garciae610aed2021-07-26 15:04:37 +02001405 ):
1406 """
1407 Destroy model from controller
David Garcia2f66c4d2020-06-19 11:40:18 +02001408
David Garciae610aed2021-07-26 15:04:37 +02001409 :param: model: Model name to be removed
1410 :param: controller: Controller object
1411 :param: timeout: Timeout in seconds
1412 """
David Garcia1608b562022-05-06 12:26:20 +02001413 self.log.debug(f"Destroying model {model_name}")
David Garciae610aed2021-07-26 15:04:37 +02001414
David Garciaf2e33832022-06-02 16:11:28 +02001415 async def _destroy_model_gracefully(model_name: str, controller: Controller):
1416 self.log.info(f"Gracefully deleting model {model_name}")
1417 resolved = False
1418 while model_name in await controller.list_models():
1419 if not resolved:
1420 await self.resolve(model_name)
1421 resolved = True
1422 await controller.destroy_model(model_name, destroy_storage=True)
1423
1424 await asyncio.sleep(5)
1425 self.log.info(f"Model {model_name} deleted gracefully")
1426
1427 async def _destroy_model_forcefully(model_name: str, controller: Controller):
1428 self.log.info(f"Forcefully deleting model {model_name}")
1429 while model_name in await controller.list_models():
David Garciae610aed2021-07-26 15:04:37 +02001430 await controller.destroy_model(
David Garciaf2e33832022-06-02 16:11:28 +02001431 model_name, destroy_storage=True, force=True, max_wait=60
David Garciae610aed2021-07-26 15:04:37 +02001432 )
David Garcia2f66c4d2020-06-19 11:40:18 +02001433 await asyncio.sleep(5)
David Garciaf2e33832022-06-02 16:11:28 +02001434 self.log.info(f"Model {model_name} deleted forcefully")
David Garciae610aed2021-07-26 15:04:37 +02001435
1436 try:
David Garcia1cfed492022-06-08 11:16:54 +02001437 try:
1438 await asyncio.wait_for(
1439 _destroy_model_gracefully(model_name, controller), timeout=120
1440 )
1441 except asyncio.TimeoutError:
1442 await _destroy_model_forcefully(model_name, controller)
David Garcia7e887b22022-04-28 13:43:36 +02001443 except juju.errors.JujuError as e:
1444 if any("has been removed" in error for error in e.errors):
1445 return
David Garcia1cfed492022-06-08 11:16:54 +02001446 if any("model not found" in error for error in e.errors):
1447 return
David Garcia7e887b22022-04-28 13:43:36 +02001448 raise e
David Garcia4fee80e2020-05-13 12:18:38 +02001449
aktas56120292021-02-26 15:32:39 +03001450 async def destroy_application(
1451 self, model_name: str, application_name: str, total_timeout: float
1452 ):
David Garcia4fee80e2020-05-13 12:18:38 +02001453 """
1454 Destroy application
1455
aktas56120292021-02-26 15:32:39 +03001456 :param: model_name: Model name
David Garcia4fee80e2020-05-13 12:18:38 +02001457 :param: application_name: Application name
aktas56120292021-02-26 15:32:39 +03001458 :param: total_timeout: Timeout
David Garcia4fee80e2020-05-13 12:18:38 +02001459 """
aktas56120292021-02-26 15:32:39 +03001460
1461 controller = await self.get_controller()
1462 model = None
1463
1464 try:
1465 model = await self.get_model(controller, model_name)
1466 self.log.debug(
1467 "Destroying application {} in model {}".format(
1468 application_name, model_name
1469 )
David Garcia4fee80e2020-05-13 12:18:38 +02001470 )
aktas56120292021-02-26 15:32:39 +03001471 application = self._get_application(model, application_name)
1472 if application:
1473 await application.destroy()
1474 else:
1475 self.log.warning("Application not found: {}".format(application_name))
1476
1477 self.log.debug(
1478 "Waiting for application {} to be destroyed in model {}...".format(
1479 application_name, model_name
1480 )
1481 )
1482 if total_timeout is None:
1483 total_timeout = 3600
1484 end = time.time() + total_timeout
1485 while time.time() < end:
1486 if not self._get_application(model, application_name):
1487 self.log.debug(
1488 "The application {} was destroyed in model {} ".format(
1489 application_name, model_name
1490 )
1491 )
1492 return
1493 await asyncio.sleep(5)
1494 raise Exception(
1495 "Timeout waiting for application {} to be destroyed in model {}".format(
1496 application_name, model_name
1497 )
1498 )
1499 finally:
1500 if model is not None:
1501 await self.disconnect_model(model)
1502 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +02001503
David Garcia168bb192020-10-21 14:19:45 +02001504 async def _destroy_pending_machines(self, model: Model, only_manual: bool = False):
1505 """
1506 Destroy pending machines in a given model
1507
1508 :param: only_manual: Bool that indicates only manually provisioned
1509 machines should be destroyed (if True), or that
1510 all pending machines should be destroyed
1511 """
1512 status = await model.get_status()
1513 for machine_id in status.machines:
1514 machine_status = status.machines[machine_id]
1515 if machine_status.agent_status.status == "pending":
1516 if only_manual and not machine_status.instance_id.startswith("manual:"):
1517 break
1518 machine = model.machines[machine_id]
1519 await machine.destroy(force=True)
1520
David Garcia4fee80e2020-05-13 12:18:38 +02001521 async def configure_application(
1522 self, model_name: str, application_name: str, config: dict = None
1523 ):
1524 """Configure application
1525
1526 :param: model_name: Model name
1527 :param: application_name: Application name
1528 :param: config: Config to apply to the charm
1529 """
David Garcia2f66c4d2020-06-19 11:40:18 +02001530 self.log.debug("Configuring application {}".format(application_name))
1531
David Garcia4fee80e2020-05-13 12:18:38 +02001532 if config:
David Garcia5b802c92020-11-11 16:56:06 +01001533 controller = await self.get_controller()
1534 model = None
David Garcia2f66c4d2020-06-19 11:40:18 +02001535 try:
David Garcia2f66c4d2020-06-19 11:40:18 +02001536 model = await self.get_model(controller, model_name)
1537 application = self._get_application(
David Garciaf6e9b002020-11-27 15:32:02 +01001538 model,
1539 application_name=application_name,
David Garcia2f66c4d2020-06-19 11:40:18 +02001540 )
1541 await application.set_config(config)
1542 finally:
David Garcia5b802c92020-11-11 16:56:06 +01001543 if model:
1544 await self.disconnect_model(model)
David Garcia2f66c4d2020-06-19 11:40:18 +02001545 await self.disconnect_controller(controller)
1546
David Garcia2f66c4d2020-06-19 11:40:18 +02001547 def handle_exception(self, loop, context):
1548 # All unhandled exceptions by libjuju are handled here.
1549 pass
1550
1551 async def health_check(self, interval: float = 300.0):
1552 """
1553 Health check to make sure controller and controller_model connections are OK
1554
1555 :param: interval: Time in seconds between checks
1556 """
David Garcia667696e2020-09-22 14:52:32 +02001557 controller = None
David Garcia2f66c4d2020-06-19 11:40:18 +02001558 while True:
1559 try:
1560 controller = await self.get_controller()
1561 # self.log.debug("VCA is alive")
1562 except Exception as e:
1563 self.log.error("Health check to VCA failed: {}".format(e))
1564 finally:
1565 await self.disconnect_controller(controller)
1566 await asyncio.sleep(interval)
Dominik Fleischmannb9513342020-06-09 11:57:14 +02001567
1568 async def list_models(self, contains: str = None) -> [str]:
1569 """List models with certain names
1570
1571 :param: contains: String that is contained in model name
1572
1573 :retur: [models] Returns list of model names
1574 """
1575
1576 controller = await self.get_controller()
1577 try:
1578 models = await controller.list_models()
1579 if contains:
1580 models = [model for model in models if contains in model]
1581 return models
1582 finally:
1583 await self.disconnect_controller(controller)
David Garciabc538e42020-08-25 15:22:30 +02001584
David Garcia582b9232021-10-26 12:30:44 +02001585 async def _list_offers(
1586 self, model_name: str, offer_name: str = None
1587 ) -> QueryApplicationOffersResults:
1588 """
1589 List offers within a model
David Garciabc538e42020-08-25 15:22:30 +02001590
1591 :param: model_name: Model name
David Garcia582b9232021-10-26 12:30:44 +02001592 :param: offer_name: Offer name to filter.
David Garciabc538e42020-08-25 15:22:30 +02001593
David Garcia582b9232021-10-26 12:30:44 +02001594 :return: Returns application offers results in the model
David Garciabc538e42020-08-25 15:22:30 +02001595 """
1596
1597 controller = await self.get_controller()
1598 try:
David Garcia582b9232021-10-26 12:30:44 +02001599 offers = (await controller.list_offers(model_name)).results
1600 if offer_name:
1601 matching_offer = []
1602 for offer in offers:
1603 if offer.offer_name == offer_name:
1604 matching_offer.append(offer)
1605 break
1606 offers = matching_offer
1607 return offers
David Garciabc538e42020-08-25 15:22:30 +02001608 finally:
1609 await self.disconnect_controller(controller)
David Garcia12b29242020-09-17 16:01:48 +02001610
David Garcia475a7222020-09-21 16:19:15 +02001611 async def add_k8s(
David Garcia7077e262020-10-16 15:38:13 +02001612 self,
1613 name: str,
David Garciaf6e9b002020-11-27 15:32:02 +01001614 rbac_id: str,
1615 token: str,
1616 client_cert_data: str,
David Garcia7077e262020-10-16 15:38:13 +02001617 configuration: Configuration,
1618 storage_class: str,
1619 credential_name: str = None,
David Garcia475a7222020-09-21 16:19:15 +02001620 ):
David Garcia12b29242020-09-17 16:01:48 +02001621 """
1622 Add a Kubernetes cloud to the controller
1623
1624 Similar to the `juju add-k8s` command in the CLI
1625
David Garcia7077e262020-10-16 15:38:13 +02001626 :param: name: Name for the K8s cloud
1627 :param: configuration: Kubernetes configuration object
1628 :param: storage_class: Storage Class to use in the cloud
1629 :param: credential_name: Storage Class to use in the cloud
David Garcia12b29242020-09-17 16:01:48 +02001630 """
1631
David Garcia12b29242020-09-17 16:01:48 +02001632 if not storage_class:
1633 raise Exception("storage_class must be a non-empty string")
1634 if not name:
1635 raise Exception("name must be a non-empty string")
David Garcia475a7222020-09-21 16:19:15 +02001636 if not configuration:
1637 raise Exception("configuration must be provided")
David Garcia12b29242020-09-17 16:01:48 +02001638
David Garcia475a7222020-09-21 16:19:15 +02001639 endpoint = configuration.host
David Garciaf6e9b002020-11-27 15:32:02 +01001640 credential = self.get_k8s_cloud_credential(
1641 configuration,
1642 client_cert_data,
1643 token,
David Garcia475a7222020-09-21 16:19:15 +02001644 )
David Garciaf6e9b002020-11-27 15:32:02 +01001645 credential.attrs[RBAC_LABEL_KEY_NAME] = rbac_id
David Garcia12b29242020-09-17 16:01:48 +02001646 cloud = client.Cloud(
David Garcia475a7222020-09-21 16:19:15 +02001647 type_="kubernetes",
1648 auth_types=[credential.auth_type],
David Garcia12b29242020-09-17 16:01:48 +02001649 endpoint=endpoint,
David Garciaf6e9b002020-11-27 15:32:02 +01001650 ca_certificates=[client_cert_data],
David Garcia12b29242020-09-17 16:01:48 +02001651 config={
1652 "operator-storage": storage_class,
1653 "workload-storage": storage_class,
1654 },
David Garcia12b29242020-09-17 16:01:48 +02001655 )
1656
David Garcia7077e262020-10-16 15:38:13 +02001657 return await self.add_cloud(
1658 name, cloud, credential, credential_name=credential_name
1659 )
David Garcia475a7222020-09-21 16:19:15 +02001660
1661 def get_k8s_cloud_credential(
David Garciaf6e9b002020-11-27 15:32:02 +01001662 self,
1663 configuration: Configuration,
1664 client_cert_data: str,
1665 token: str = None,
David Garcia475a7222020-09-21 16:19:15 +02001666 ) -> client.CloudCredential:
1667 attrs = {}
David Garciaf6e9b002020-11-27 15:32:02 +01001668 # TODO: Test with AKS
1669 key = None # open(configuration.key_file, "r").read()
David Garcia475a7222020-09-21 16:19:15 +02001670 username = configuration.username
1671 password = configuration.password
1672
David Garciaf6e9b002020-11-27 15:32:02 +01001673 if client_cert_data:
1674 attrs["ClientCertificateData"] = client_cert_data
David Garcia475a7222020-09-21 16:19:15 +02001675 if key:
David Garciaf6e9b002020-11-27 15:32:02 +01001676 attrs["ClientKeyData"] = key
David Garcia475a7222020-09-21 16:19:15 +02001677 if token:
1678 if username or password:
1679 raise JujuInvalidK8sConfiguration("Cannot set both token and user/pass")
1680 attrs["Token"] = token
1681
1682 auth_type = None
1683 if key:
1684 auth_type = "oauth2"
David Garciaf6e9b002020-11-27 15:32:02 +01001685 if client_cert_data:
1686 auth_type = "oauth2withcert"
David Garcia475a7222020-09-21 16:19:15 +02001687 if not token:
1688 raise JujuInvalidK8sConfiguration(
1689 "missing token for auth type {}".format(auth_type)
1690 )
1691 elif username:
1692 if not password:
1693 self.log.debug(
1694 "credential for user {} has empty password".format(username)
1695 )
1696 attrs["username"] = username
1697 attrs["password"] = password
David Garciaf6e9b002020-11-27 15:32:02 +01001698 if client_cert_data:
David Garcia475a7222020-09-21 16:19:15 +02001699 auth_type = "userpasswithcert"
1700 else:
1701 auth_type = "userpass"
David Garciaf6e9b002020-11-27 15:32:02 +01001702 elif client_cert_data and token:
David Garcia475a7222020-09-21 16:19:15 +02001703 auth_type = "certificate"
1704 else:
1705 raise JujuInvalidK8sConfiguration("authentication method not supported")
David Garcia667696e2020-09-22 14:52:32 +02001706 return client.CloudCredential(auth_type=auth_type, attrs=attrs)
David Garcia12b29242020-09-17 16:01:48 +02001707
1708 async def add_cloud(
David Garcia7077e262020-10-16 15:38:13 +02001709 self,
1710 name: str,
1711 cloud: Cloud,
1712 credential: CloudCredential = None,
1713 credential_name: str = None,
David Garcia12b29242020-09-17 16:01:48 +02001714 ) -> Cloud:
1715 """
1716 Add cloud to the controller
1717
David Garcia7077e262020-10-16 15:38:13 +02001718 :param: name: Name of the cloud to be added
1719 :param: cloud: Cloud object
1720 :param: credential: CloudCredentials object for the cloud
1721 :param: credential_name: Credential name.
1722 If not defined, cloud of the name will be used.
David Garcia12b29242020-09-17 16:01:48 +02001723 """
1724 controller = await self.get_controller()
1725 try:
1726 _ = await controller.add_cloud(name, cloud)
1727 if credential:
David Garcia7077e262020-10-16 15:38:13 +02001728 await controller.add_credential(
1729 credential_name or name, credential=credential, cloud=name
1730 )
David Garcia12b29242020-09-17 16:01:48 +02001731 # Need to return the object returned by the controller.add_cloud() function
1732 # I'm returning the original value now until this bug is fixed:
1733 # https://github.com/juju/python-libjuju/issues/443
1734 return cloud
1735 finally:
1736 await self.disconnect_controller(controller)
1737
1738 async def remove_cloud(self, name: str):
1739 """
1740 Remove cloud
1741
1742 :param: name: Name of the cloud to be removed
1743 """
1744 controller = await self.get_controller()
1745 try:
1746 await controller.remove_cloud(name)
David Garciaf980ac02021-07-27 15:07:42 +02001747 except juju.errors.JujuError as e:
1748 if len(e.errors) == 1 and f'cloud "{name}" not found' == e.errors[0]:
1749 self.log.warning(f"Cloud {name} not found, so it could not be deleted.")
1750 else:
1751 raise e
David Garcia12b29242020-09-17 16:01:48 +02001752 finally:
1753 await self.disconnect_controller(controller)
David Garcia59f520d2020-10-15 13:16:45 +02001754
David Garciaeb8943a2021-04-12 12:07:37 +02001755 @retry(attempts=20, delay=5, fallback=JujuLeaderUnitNotFound())
David Garcia59f520d2020-10-15 13:16:45 +02001756 async def _get_leader_unit(self, application: Application) -> Unit:
1757 unit = None
1758 for u in application.units:
1759 if await u.is_leader_from_status():
1760 unit = u
1761 break
David Garciaeb8943a2021-04-12 12:07:37 +02001762 if not unit:
1763 raise Exception()
David Garcia59f520d2020-10-15 13:16:45 +02001764 return unit
David Garciaf6e9b002020-11-27 15:32:02 +01001765
David Garciaeb8943a2021-04-12 12:07:37 +02001766 async def get_cloud_credentials(self, cloud: Cloud) -> typing.List:
1767 """
1768 Get cloud credentials
1769
1770 :param: cloud: Cloud object. The returned credentials will be from this cloud.
1771
1772 :return: List of credentials object associated to the specified cloud
1773
1774 """
David Garciaf6e9b002020-11-27 15:32:02 +01001775 controller = await self.get_controller()
1776 try:
1777 facade = client.CloudFacade.from_connection(controller.connection())
David Garciaeb8943a2021-04-12 12:07:37 +02001778 cloud_cred_tag = tag.credential(
1779 cloud.name, self.vca_connection.data.user, cloud.credential_name
1780 )
David Garciaf6e9b002020-11-27 15:32:02 +01001781 params = [client.Entity(cloud_cred_tag)]
1782 return (await facade.Credential(params)).results
1783 finally:
1784 await self.disconnect_controller(controller)
aktasfa02f8a2021-07-29 17:41:40 +03001785
1786 async def check_application_exists(self, model_name, application_name) -> bool:
1787 """Check application exists
1788
1789 :param: model_name: Model Name
1790 :param: application_name: Application Name
1791
1792 :return: Boolean
1793 """
1794
1795 model = None
1796 controller = await self.get_controller()
1797 try:
1798 model = await self.get_model(controller, model_name)
1799 self.log.debug(
1800 "Checking if application {} exists in model {}".format(
1801 application_name, model_name
1802 )
1803 )
1804 return self._get_application(model, application_name) is not None
1805 finally:
1806 if model:
1807 await self.disconnect_model(model)
1808 await self.disconnect_controller(controller)