blob: 8139a4e7b1fbe8b82de4937565c674ed5d428128 [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
Patricia Reinosofedf9152023-01-17 08:39:44 +000017import os
David Garciaeb8943a2021-04-12 12:07:37 +020018import typing
Patricia Reinosofedf9152023-01-17 08:39:44 +000019import yaml
David Garciaf6e9b002020-11-27 15:32:02 +010020
David Garcia4fee80e2020-05-13 12:18:38 +020021import time
22
David Garciaf980ac02021-07-27 15:07:42 +020023import juju.errors
Patricia Reinosofedf9152023-01-17 08:39:44 +000024from juju.bundle import BundleHandler
David Garcia4fee80e2020-05-13 12:18:38 +020025from juju.model import Model
26from juju.machine import Machine
27from juju.application import Application
David Garcia59f520d2020-10-15 13:16:45 +020028from juju.unit import Unit
Patricia Reinosofedf9152023-01-17 08:39:44 +000029from juju.url import URL
30from juju.version import DEFAULT_ARCHITECTURE
David Garcia12b29242020-09-17 16:01:48 +020031from juju.client._definitions import (
32 FullStatus,
33 QueryApplicationOffersResults,
34 Cloud,
35 CloudCredential,
36)
David Garciaf6e9b002020-11-27 15:32:02 +010037from juju.controller import Controller
38from juju.client import client
39from juju import tag
40
David Garcia582b9232021-10-26 12:30:44 +020041from n2vc.definitions import Offer, RelationEndpoint
David Garcia4fee80e2020-05-13 12:18:38 +020042from n2vc.juju_watcher import JujuModelWatcher
43from n2vc.provisioner import AsyncSSHProvisioner
44from n2vc.n2vc_conn import N2VCConnector
45from n2vc.exceptions import (
46 JujuMachineNotFound,
47 JujuApplicationNotFound,
Dominik Fleischmann7ff392f2020-07-07 13:11:19 +020048 JujuLeaderUnitNotFound,
49 JujuActionNotFound,
David Garcia4fee80e2020-05-13 12:18:38 +020050 JujuControllerFailedConnecting,
51 JujuApplicationExists,
David Garcia475a7222020-09-21 16:19:15 +020052 JujuInvalidK8sConfiguration,
David Garciaeb8943a2021-04-12 12:07:37 +020053 JujuError,
David Garcia4fee80e2020-05-13 12:18:38 +020054)
David Garciaeb8943a2021-04-12 12:07:37 +020055from n2vc.vca.cloud import Cloud as VcaCloud
56from n2vc.vca.connection import Connection
David Garcia475a7222020-09-21 16:19:15 +020057from kubernetes.client.configuration import Configuration
David Garciaeb8943a2021-04-12 12:07:37 +020058from retrying_async import retry
59
David Garcia4fee80e2020-05-13 12:18:38 +020060
David Garciaf6e9b002020-11-27 15:32:02 +010061RBAC_LABEL_KEY_NAME = "rbac-id"
62
David Garcia4fee80e2020-05-13 12:18:38 +020063
Mark Beierl714d8872023-05-18 15:08:06 -040064@asyncio.coroutine
65def retry_callback(attempt, exc, args, kwargs, delay=0.5, *, loop):
66 # Specifically overridden from upstream implementation so it can
67 # continue to work with Python 3.10
68 yield from asyncio.sleep(attempt * delay)
69 return retry
70
71
David Garcia4fee80e2020-05-13 12:18:38 +020072class Libjuju:
73 def __init__(
74 self,
David Garciaeb8943a2021-04-12 12:07:37 +020075 vca_connection: Connection,
David Garcia4fee80e2020-05-13 12:18:38 +020076 log: logging.Logger = None,
David Garcia4fee80e2020-05-13 12:18:38 +020077 n2vc: N2VCConnector = None,
David Garcia4fee80e2020-05-13 12:18:38 +020078 ):
79 """
80 Constructor
81
David Garciaeb8943a2021-04-12 12:07:37 +020082 :param: vca_connection: n2vc.vca.connection object
David Garcia4fee80e2020-05-13 12:18:38 +020083 :param: log: Logger
David Garcia4fee80e2020-05-13 12:18:38 +020084 :param: n2vc: N2VC object
David Garcia4fee80e2020-05-13 12:18:38 +020085 """
86
David Garcia2f66c4d2020-06-19 11:40:18 +020087 self.log = log or logging.getLogger("Libjuju")
David Garcia4fee80e2020-05-13 12:18:38 +020088 self.n2vc = n2vc
David Garciaeb8943a2021-04-12 12:07:37 +020089 self.vca_connection = vca_connection
David Garcia4fee80e2020-05-13 12:18:38 +020090
Guillermo Calvino474fd952023-04-28 11:51:43 +020091 self.creating_model = asyncio.Lock()
David Garcia4fee80e2020-05-13 12:18:38 +020092
David Garciaeb8943a2021-04-12 12:07:37 +020093 if self.vca_connection.is_default:
94 self.health_check_task = self._create_health_check_task()
David Garciaa4f57d62020-10-22 10:50:56 +020095
96 def _create_health_check_task(self):
Mark Beierl2c3c1462023-05-15 16:17:02 -040097 return asyncio.get_event_loop().create_task(self.health_check())
David Garcia4fee80e2020-05-13 12:18:38 +020098
David Garciaeb8943a2021-04-12 12:07:37 +020099 async def get_controller(self, timeout: float = 60.0) -> Controller:
David Garcia2f66c4d2020-06-19 11:40:18 +0200100 """
101 Get controller
David Garcia4fee80e2020-05-13 12:18:38 +0200102
David Garcia2f66c4d2020-06-19 11:40:18 +0200103 :param: timeout: Time in seconds to wait for controller to connect
104 """
105 controller = None
106 try:
Pedro Escaleira86a63142022-04-05 21:01:37 +0100107 controller = Controller()
David Garcia2f66c4d2020-06-19 11:40:18 +0200108 await asyncio.wait_for(
109 controller.connect(
David Garciaeb8943a2021-04-12 12:07:37 +0200110 endpoint=self.vca_connection.data.endpoints,
111 username=self.vca_connection.data.user,
112 password=self.vca_connection.data.secret,
113 cacert=self.vca_connection.data.cacert,
David Garcia2f66c4d2020-06-19 11:40:18 +0200114 ),
115 timeout=timeout,
116 )
David Garciaeb8943a2021-04-12 12:07:37 +0200117 if self.vca_connection.is_default:
118 endpoints = await controller.api_endpoints
119 if not all(
120 endpoint in self.vca_connection.endpoints for endpoint in endpoints
121 ):
122 await self.vca_connection.update_endpoints(endpoints)
David Garcia2f66c4d2020-06-19 11:40:18 +0200123 return controller
124 except asyncio.CancelledError as e:
125 raise e
126 except Exception as e:
127 self.log.error(
David Garciaeb8943a2021-04-12 12:07:37 +0200128 "Failed connecting to controller: {}... {}".format(
129 self.vca_connection.data.endpoints, e
130 )
David Garcia2f66c4d2020-06-19 11:40:18 +0200131 )
132 if controller:
133 await self.disconnect_controller(controller)
Pedro Escaleira58326382022-05-30 19:08:41 +0100134
135 raise JujuControllerFailedConnecting(
136 f"Error connecting to Juju controller: {e}"
137 )
David Garcia4fee80e2020-05-13 12:18:38 +0200138
139 async def disconnect(self):
David Garcia2f66c4d2020-06-19 11:40:18 +0200140 """Disconnect"""
141 # Cancel health check task
142 self.health_check_task.cancel()
143 self.log.debug("Libjuju disconnected!")
David Garcia4fee80e2020-05-13 12:18:38 +0200144
145 async def disconnect_model(self, model: Model):
146 """
147 Disconnect model
148
149 :param: model: Model that will be disconnected
150 """
David Garcia2f66c4d2020-06-19 11:40:18 +0200151 await model.disconnect()
David Garcia4fee80e2020-05-13 12:18:38 +0200152
David Garcia2f66c4d2020-06-19 11:40:18 +0200153 async def disconnect_controller(self, controller: Controller):
David Garcia4fee80e2020-05-13 12:18:38 +0200154 """
David Garcia2f66c4d2020-06-19 11:40:18 +0200155 Disconnect controller
David Garcia4fee80e2020-05-13 12:18:38 +0200156
David Garcia2f66c4d2020-06-19 11:40:18 +0200157 :param: controller: Controller that will be disconnected
David Garcia4fee80e2020-05-13 12:18:38 +0200158 """
David Garcia667696e2020-09-22 14:52:32 +0200159 if controller:
160 await controller.disconnect()
David Garcia4fee80e2020-05-13 12:18:38 +0200161
Mark Beierl714d8872023-05-18 15:08:06 -0400162 @retry(attempts=3, delay=5, timeout=None, callback=retry_callback)
David Garciaeb8943a2021-04-12 12:07:37 +0200163 async def add_model(self, model_name: str, cloud: VcaCloud):
David Garcia4fee80e2020-05-13 12:18:38 +0200164 """
165 Create model
166
167 :param: model_name: Model name
David Garciaeb8943a2021-04-12 12:07:37 +0200168 :param: cloud: Cloud object
David Garcia4fee80e2020-05-13 12:18:38 +0200169 """
170
David Garcia2f66c4d2020-06-19 11:40:18 +0200171 # Get controller
172 controller = await self.get_controller()
173 model = None
174 try:
David Garcia2f66c4d2020-06-19 11:40:18 +0200175 # Block until other workers have finished model creation
176 while self.creating_model.locked():
177 await asyncio.sleep(0.1)
David Garcia4fee80e2020-05-13 12:18:38 +0200178
David Garcia2f66c4d2020-06-19 11:40:18 +0200179 # Create the model
180 async with self.creating_model:
David Garciab0a8f402021-03-15 18:41:34 +0100181 if await self.model_exists(model_name, controller=controller):
182 return
David Garcia2f66c4d2020-06-19 11:40:18 +0200183 self.log.debug("Creating model {}".format(model_name))
184 model = await controller.add_model(
185 model_name,
David Garciaeb8943a2021-04-12 12:07:37 +0200186 config=self.vca_connection.data.model_config,
187 cloud_name=cloud.name,
188 credential_name=cloud.credential_name,
David Garcia2f66c4d2020-06-19 11:40:18 +0200189 )
David Garciaf980ac02021-07-27 15:07:42 +0200190 except juju.errors.JujuAPIError as e:
David Garciaeb8943a2021-04-12 12:07:37 +0200191 if "already exists" in e.message:
192 pass
193 else:
194 raise e
David Garcia2f66c4d2020-06-19 11:40:18 +0200195 finally:
196 if model:
197 await self.disconnect_model(model)
198 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +0200199
ksaikiranrcdf0b8e2021-03-17 12:50:00 +0530200 async def get_executed_actions(self, model_name: str) -> list:
201 """
202 Get executed/history of actions for a model.
203
204 :param: model_name: Model name, str.
205 :return: List of executed actions for a model.
206 """
207 model = None
208 executed_actions = []
209 controller = await self.get_controller()
210 try:
211 model = await self.get_model(controller, model_name)
212 # Get all unique action names
213 actions = {}
214 for application in model.applications:
215 application_actions = await self.get_actions(application, model_name)
216 actions.update(application_actions)
217 # Get status of all actions
218 for application_action in actions:
David Garciaeb8943a2021-04-12 12:07:37 +0200219 app_action_status_list = await model.get_action_status(
220 name=application_action
221 )
ksaikiranrcdf0b8e2021-03-17 12:50:00 +0530222 for action_id, action_status in app_action_status_list.items():
David Garciaeb8943a2021-04-12 12:07:37 +0200223 executed_action = {
224 "id": action_id,
225 "action": application_action,
226 "status": action_status,
227 }
ksaikiranrcdf0b8e2021-03-17 12:50:00 +0530228 # Get action output by id
229 action_status = await model.get_action_output(executed_action["id"])
230 for k, v in action_status.items():
231 executed_action[k] = v
232 executed_actions.append(executed_action)
233 except Exception as e:
David Garciaeb8943a2021-04-12 12:07:37 +0200234 raise JujuError(
235 "Error in getting executed actions for model: {}. Error: {}".format(
236 model_name, str(e)
237 )
238 )
ksaikiranrcdf0b8e2021-03-17 12:50:00 +0530239 finally:
240 if model:
241 await self.disconnect_model(model)
242 await self.disconnect_controller(controller)
243 return executed_actions
244
David Garciaeb8943a2021-04-12 12:07:37 +0200245 async def get_application_configs(
246 self, model_name: str, application_name: str
247 ) -> dict:
ksaikiranrcdf0b8e2021-03-17 12:50:00 +0530248 """
249 Get available configs for an application.
250
251 :param: model_name: Model name, str.
252 :param: application_name: Application name, str.
253
254 :return: A dict which has key - action name, value - action description
255 """
256 model = None
257 application_configs = {}
258 controller = await self.get_controller()
259 try:
260 model = await self.get_model(controller, model_name)
David Garciaeb8943a2021-04-12 12:07:37 +0200261 application = self._get_application(
262 model, application_name=application_name
263 )
ksaikiranrcdf0b8e2021-03-17 12:50:00 +0530264 application_configs = await application.get_config()
265 except Exception as e:
David Garciaeb8943a2021-04-12 12:07:37 +0200266 raise JujuError(
267 "Error in getting configs for application: {} in model: {}. Error: {}".format(
268 application_name, model_name, str(e)
269 )
270 )
ksaikiranrcdf0b8e2021-03-17 12:50:00 +0530271 finally:
272 if model:
273 await self.disconnect_model(model)
274 await self.disconnect_controller(controller)
275 return application_configs
276
Mark Beierl714d8872023-05-18 15:08:06 -0400277 @retry(attempts=3, delay=5, callback=retry_callback)
David Garciaeb8943a2021-04-12 12:07:37 +0200278 async def get_model(self, controller: Controller, model_name: str) -> Model:
David Garcia4fee80e2020-05-13 12:18:38 +0200279 """
280 Get model from controller
281
David Garcia2f66c4d2020-06-19 11:40:18 +0200282 :param: controller: Controller
David Garcia4fee80e2020-05-13 12:18:38 +0200283 :param: model_name: Model name
284
285 :return: Model: The created Juju model object
286 """
David Garcia2f66c4d2020-06-19 11:40:18 +0200287 return await controller.get_model(model_name)
David Garcia4fee80e2020-05-13 12:18:38 +0200288
garciadeblas82b591c2021-03-24 09:22:13 +0100289 async def model_exists(
290 self, model_name: str, controller: Controller = None
291 ) -> bool:
David Garcia4fee80e2020-05-13 12:18:38 +0200292 """
293 Check if model exists
294
David Garcia2f66c4d2020-06-19 11:40:18 +0200295 :param: controller: Controller
David Garcia4fee80e2020-05-13 12:18:38 +0200296 :param: model_name: Model name
297
298 :return bool
299 """
David Garcia2f66c4d2020-06-19 11:40:18 +0200300 need_to_disconnect = False
David Garcia4fee80e2020-05-13 12:18:38 +0200301
David Garcia2f66c4d2020-06-19 11:40:18 +0200302 # Get controller if not passed
303 if not controller:
304 controller = await self.get_controller()
305 need_to_disconnect = True
David Garcia4fee80e2020-05-13 12:18:38 +0200306
David Garcia2f66c4d2020-06-19 11:40:18 +0200307 # Check if model exists
308 try:
309 return model_name in await controller.list_models()
310 finally:
311 if need_to_disconnect:
312 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +0200313
David Garcia42f328a2020-08-25 15:03:01 +0200314 async def models_exist(self, model_names: [str]) -> (bool, list):
315 """
316 Check if models exists
317
318 :param: model_names: List of strings with model names
319
320 :return (bool, list[str]): (True if all models exists, List of model names that don't exist)
321 """
322 if not model_names:
323 raise Exception(
David Garciac38a6962020-09-16 13:31:33 +0200324 "model_names must be a non-empty array. Given value: {}".format(
325 model_names
326 )
David Garcia42f328a2020-08-25 15:03:01 +0200327 )
328 non_existing_models = []
329 models = await self.list_models()
330 existing_models = list(set(models).intersection(model_names))
331 non_existing_models = list(set(model_names) - set(existing_models))
332
333 return (
334 len(non_existing_models) == 0,
335 non_existing_models,
336 )
337
David Garcia4fee80e2020-05-13 12:18:38 +0200338 async def get_model_status(self, model_name: str) -> FullStatus:
339 """
340 Get model status
341
342 :param: model_name: Model name
343
344 :return: Full status object
345 """
David Garcia2f66c4d2020-06-19 11:40:18 +0200346 controller = await self.get_controller()
347 model = await self.get_model(controller, model_name)
348 try:
349 return await model.get_status()
350 finally:
351 await self.disconnect_model(model)
352 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +0200353
354 async def create_machine(
355 self,
356 model_name: str,
357 machine_id: str = None,
358 db_dict: dict = None,
359 progress_timeout: float = None,
360 total_timeout: float = None,
David Garciaf643c132021-05-28 12:23:44 +0200361 series: str = "bionic",
David Garciaf8a9d462020-03-25 18:19:02 +0100362 wait: bool = True,
David Garcia4fee80e2020-05-13 12:18:38 +0200363 ) -> (Machine, bool):
364 """
365 Create machine
366
367 :param: model_name: Model name
368 :param: machine_id: Machine id
369 :param: db_dict: Dictionary with data of the DB to write the updates
370 :param: progress_timeout: Maximum time between two updates in the model
371 :param: total_timeout: Timeout for the entity to be active
David Garciaf8a9d462020-03-25 18:19:02 +0100372 :param: series: Series of the machine (xenial, bionic, focal, ...)
373 :param: wait: Wait until machine is ready
David Garcia4fee80e2020-05-13 12:18:38 +0200374
375 :return: (juju.machine.Machine, bool): Machine object and a boolean saying
376 if the machine is new or it already existed
377 """
378 new = False
379 machine = None
380
381 self.log.debug(
382 "Creating machine (id={}) in model: {}".format(machine_id, model_name)
383 )
384
David Garcia2f66c4d2020-06-19 11:40:18 +0200385 # Get controller
386 controller = await self.get_controller()
387
David Garcia4fee80e2020-05-13 12:18:38 +0200388 # Get model
David Garcia2f66c4d2020-06-19 11:40:18 +0200389 model = await self.get_model(controller, model_name)
David Garcia4fee80e2020-05-13 12:18:38 +0200390 try:
391 if machine_id is not None:
392 self.log.debug(
393 "Searching machine (id={}) in model {}".format(
394 machine_id, model_name
395 )
396 )
397
398 # Get machines from model and get the machine with machine_id if exists
399 machines = await model.get_machines()
400 if machine_id in machines:
401 self.log.debug(
402 "Machine (id={}) found in model {}".format(
403 machine_id, model_name
404 )
405 )
Dominik Fleischmann7ff392f2020-07-07 13:11:19 +0200406 machine = machines[machine_id]
David Garcia4fee80e2020-05-13 12:18:38 +0200407 else:
408 raise JujuMachineNotFound("Machine {} not found".format(machine_id))
409
410 if machine is None:
411 self.log.debug("Creating a new machine in model {}".format(model_name))
412
413 # Create machine
414 machine = await model.add_machine(
415 spec=None, constraints=None, disks=None, series=series
416 )
417 new = True
418
419 # Wait until the machine is ready
David Garcia2f66c4d2020-06-19 11:40:18 +0200420 self.log.debug(
421 "Wait until machine {} is ready in model {}".format(
422 machine.entity_id, model_name
423 )
424 )
David Garciaf8a9d462020-03-25 18:19:02 +0100425 if wait:
426 await JujuModelWatcher.wait_for(
427 model=model,
428 entity=machine,
429 progress_timeout=progress_timeout,
430 total_timeout=total_timeout,
431 db_dict=db_dict,
432 n2vc=self.n2vc,
David Garciaeb8943a2021-04-12 12:07:37 +0200433 vca_id=self.vca_connection._vca_id,
David Garciaf8a9d462020-03-25 18:19:02 +0100434 )
David Garcia4fee80e2020-05-13 12:18:38 +0200435 finally:
436 await self.disconnect_model(model)
David Garcia2f66c4d2020-06-19 11:40:18 +0200437 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +0200438
David Garcia2f66c4d2020-06-19 11:40:18 +0200439 self.log.debug(
440 "Machine {} ready at {} in model {}".format(
441 machine.entity_id, machine.dns_name, model_name
442 )
443 )
David Garcia4fee80e2020-05-13 12:18:38 +0200444 return machine, new
445
446 async def provision_machine(
447 self,
448 model_name: str,
449 hostname: str,
450 username: str,
451 private_key_path: str,
452 db_dict: dict = None,
453 progress_timeout: float = None,
454 total_timeout: float = None,
455 ) -> str:
456 """
457 Manually provisioning of a machine
458
459 :param: model_name: Model name
460 :param: hostname: IP to access the machine
461 :param: username: Username to login to the machine
462 :param: private_key_path: Local path for the private key
463 :param: db_dict: Dictionary with data of the DB to write the updates
464 :param: progress_timeout: Maximum time between two updates in the model
465 :param: total_timeout: Timeout for the entity to be active
466
467 :return: (Entity): Machine id
468 """
469 self.log.debug(
470 "Provisioning machine. model: {}, hostname: {}, username: {}".format(
471 model_name, hostname, username
472 )
473 )
474
David Garcia2f66c4d2020-06-19 11:40:18 +0200475 # Get controller
476 controller = await self.get_controller()
477
David Garcia4fee80e2020-05-13 12:18:38 +0200478 # Get model
David Garcia2f66c4d2020-06-19 11:40:18 +0200479 model = await self.get_model(controller, model_name)
David Garcia4fee80e2020-05-13 12:18:38 +0200480
481 try:
482 # Get provisioner
483 provisioner = AsyncSSHProvisioner(
484 host=hostname,
485 user=username,
486 private_key_path=private_key_path,
487 log=self.log,
488 )
489
490 # Provision machine
491 params = await provisioner.provision_machine()
492
493 params.jobs = ["JobHostUnits"]
494
495 self.log.debug("Adding machine to model")
496 connection = model.connection()
497 client_facade = client.ClientFacade.from_connection(connection)
498
499 results = await client_facade.AddMachines(params=[params])
500 error = results.machines[0].error
501
502 if error:
503 msg = "Error adding machine: {}".format(error.message)
504 self.log.error(msg=msg)
505 raise ValueError(msg)
506
507 machine_id = results.machines[0].machine
508
509 self.log.debug("Installing Juju agent into machine {}".format(machine_id))
510 asyncio.ensure_future(
511 provisioner.install_agent(
512 connection=connection,
513 nonce=params.nonce,
514 machine_id=machine_id,
David Garciaeb8943a2021-04-12 12:07:37 +0200515 proxy=self.vca_connection.data.api_proxy,
endikaf97b2312020-09-16 15:41:18 +0200516 series=params.series,
David Garcia4fee80e2020-05-13 12:18:38 +0200517 )
518 )
519
520 machine = None
521 for _ in range(10):
522 machine_list = await model.get_machines()
523 if machine_id in machine_list:
524 self.log.debug("Machine {} found in model!".format(machine_id))
525 machine = model.machines.get(machine_id)
526 break
527 await asyncio.sleep(2)
528
529 if machine is None:
530 msg = "Machine {} not found in model".format(machine_id)
531 self.log.error(msg=msg)
532 raise JujuMachineNotFound(msg)
533
David Garcia2f66c4d2020-06-19 11:40:18 +0200534 self.log.debug(
535 "Wait until machine {} is ready in model {}".format(
536 machine.entity_id, model_name
537 )
538 )
David Garcia4fee80e2020-05-13 12:18:38 +0200539 await JujuModelWatcher.wait_for(
540 model=model,
541 entity=machine,
542 progress_timeout=progress_timeout,
543 total_timeout=total_timeout,
544 db_dict=db_dict,
545 n2vc=self.n2vc,
David Garciaeb8943a2021-04-12 12:07:37 +0200546 vca_id=self.vca_connection._vca_id,
David Garcia4fee80e2020-05-13 12:18:38 +0200547 )
548 except Exception as e:
549 raise e
550 finally:
551 await self.disconnect_model(model)
David Garcia2f66c4d2020-06-19 11:40:18 +0200552 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +0200553
David Garcia2f66c4d2020-06-19 11:40:18 +0200554 self.log.debug(
555 "Machine provisioned {} in model {}".format(machine_id, model_name)
556 )
David Garcia4fee80e2020-05-13 12:18:38 +0200557
558 return machine_id
559
David Garcia667696e2020-09-22 14:52:32 +0200560 async def deploy(
Patricia Reinosofedf9152023-01-17 08:39:44 +0000561 self,
562 uri: str,
563 model_name: str,
564 wait: bool = True,
565 timeout: float = 3600,
566 instantiation_params: dict = None,
David Garcia667696e2020-09-22 14:52:32 +0200567 ):
568 """
569 Deploy bundle or charm: Similar to the juju CLI command `juju deploy`
570
Patricia Reinosofedf9152023-01-17 08:39:44 +0000571 :param uri: Path or Charm Store uri in which the charm or bundle can be found
572 :param model_name: Model name
573 :param wait: Indicates whether to wait or not until all applications are active
574 :param timeout: Time in seconds to wait until all applications are active
575 :param instantiation_params: To be applied as overlay bundle over primary bundle.
David Garcia667696e2020-09-22 14:52:32 +0200576 """
577 controller = await self.get_controller()
578 model = await self.get_model(controller, model_name)
Patricia Reinosofedf9152023-01-17 08:39:44 +0000579 overlays = []
David Garcia667696e2020-09-22 14:52:32 +0200580 try:
Patricia Reinosofedf9152023-01-17 08:39:44 +0000581 await self._validate_instantiation_params(uri, model, instantiation_params)
582 overlays = self._get_overlays(model_name, instantiation_params)
583 await model.deploy(uri, trust=True, overlays=overlays)
David Garcia667696e2020-09-22 14:52:32 +0200584 if wait:
585 await JujuModelWatcher.wait_for_model(model, timeout=timeout)
586 self.log.debug("All units active in model {}".format(model_name))
587 finally:
Patricia Reinosofedf9152023-01-17 08:39:44 +0000588 self._remove_overlay_file(overlays)
David Garcia667696e2020-09-22 14:52:32 +0200589 await self.disconnect_model(model)
590 await self.disconnect_controller(controller)
591
Patricia Reinosofedf9152023-01-17 08:39:44 +0000592 async def _validate_instantiation_params(
593 self, uri: str, model, instantiation_params: dict
594 ) -> None:
595 """Checks if all the applications in instantiation_params
596 exist ins the original bundle.
597
598 Raises:
599 JujuApplicationNotFound if there is an invalid app in
600 the instantiation params.
601 """
602 overlay_apps = self._get_apps_in_instantiation_params(instantiation_params)
603 if not overlay_apps:
604 return
605 original_apps = await self._get_apps_in_original_bundle(uri, model)
606 if not all(app in original_apps for app in overlay_apps):
607 raise JujuApplicationNotFound(
608 "Cannot find application {} in original bundle {}".format(
609 overlay_apps, original_apps
610 )
611 )
612
613 async def _get_apps_in_original_bundle(self, uri: str, model) -> set:
614 """Bundle is downloaded in BundleHandler.fetch_plan.
615 That method takes care of opening and exception handling.
616
617 Resolve method gets all the information regarding the channel,
618 track, revision, type, source.
619
620 Returns:
621 Set with the names of the applications in original bundle.
622 """
623 url = URL.parse(uri)
624 architecture = DEFAULT_ARCHITECTURE # only AMD64 is allowed
625 res = await model.deploy_types[str(url.schema)].resolve(
626 url, architecture, entity_url=uri
627 )
628 handler = BundleHandler(model, trusted=True, forced=False)
629 await handler.fetch_plan(url, res.origin)
630 return handler.applications
631
632 def _get_apps_in_instantiation_params(self, instantiation_params: dict) -> list:
633 """Extract applications key in instantiation params.
634
635 Returns:
636 List with the names of the applications in instantiation params.
637
638 Raises:
639 JujuError if applications key is not found.
640 """
641 if not instantiation_params:
642 return []
643 try:
644 return [key for key in instantiation_params.get("applications")]
645 except Exception as e:
646 raise JujuError("Invalid overlay format. {}".format(str(e)))
647
648 def _get_overlays(self, model_name: str, instantiation_params: dict) -> list:
649 """Creates a temporary overlay file which includes the instantiation params.
650 Only one overlay file is created.
651
652 Returns:
653 List with one overlay filename. Empty list if there are no instantiation params.
654 """
655 if not instantiation_params:
656 return []
657 file_name = model_name + "-overlay.yaml"
658 self._write_overlay_file(file_name, instantiation_params)
659 return [file_name]
660
661 def _write_overlay_file(self, file_name: str, instantiation_params: dict) -> None:
662 with open(file_name, "w") as file:
663 yaml.dump(instantiation_params, file)
664
665 def _remove_overlay_file(self, overlay: list) -> None:
666 """Overlay contains either one or zero file names."""
667 if not overlay:
668 return
669 try:
670 filename = overlay[0]
671 os.remove(filename)
672 except OSError as e:
673 self.log.warning(
674 "Overlay file {} could not be removed: {}".format(filename, e)
675 )
676
aktasfa02f8a2021-07-29 17:41:40 +0300677 async def add_unit(
678 self,
679 application_name: str,
680 model_name: str,
681 machine_id: str,
682 db_dict: dict = None,
683 progress_timeout: float = None,
684 total_timeout: float = None,
685 ):
686 """Add unit
687
688 :param: application_name: Application name
689 :param: model_name: Model name
690 :param: machine_id Machine id
691 :param: db_dict: Dictionary with data of the DB to write the updates
692 :param: progress_timeout: Maximum time between two updates in the model
693 :param: total_timeout: Timeout for the entity to be active
694
695 :return: None
696 """
697
698 model = None
699 controller = await self.get_controller()
700 try:
701 model = await self.get_model(controller, model_name)
702 application = self._get_application(model, application_name)
703
704 if application is not None:
aktasfa02f8a2021-07-29 17:41:40 +0300705 # Checks if the given machine id in the model,
706 # otherwise function raises an error
707 _machine, _series = self._get_machine_info(model, machine_id)
708
709 self.log.debug(
710 "Adding unit (machine {}) to application {} in model ~{}".format(
711 machine_id, application_name, model_name
712 )
713 )
714
715 await application.add_unit(to=machine_id)
716
717 await JujuModelWatcher.wait_for(
718 model=model,
719 entity=application,
720 progress_timeout=progress_timeout,
721 total_timeout=total_timeout,
722 db_dict=db_dict,
723 n2vc=self.n2vc,
724 vca_id=self.vca_connection._vca_id,
725 )
726 self.log.debug(
727 "Unit is added to application {} in model {}".format(
728 application_name, model_name
729 )
730 )
731 else:
732 raise JujuApplicationNotFound(
733 "Application {} not exists".format(application_name)
734 )
735 finally:
736 if model:
737 await self.disconnect_model(model)
738 await self.disconnect_controller(controller)
739
740 async def destroy_unit(
741 self,
742 application_name: str,
743 model_name: str,
744 machine_id: str,
745 total_timeout: float = None,
746 ):
747 """Destroy unit
748
749 :param: application_name: Application name
750 :param: model_name: Model name
751 :param: machine_id Machine id
aktasfa02f8a2021-07-29 17:41:40 +0300752 :param: total_timeout: Timeout for the entity to be active
753
754 :return: None
755 """
756
757 model = None
758 controller = await self.get_controller()
759 try:
760 model = await self.get_model(controller, model_name)
761 application = self._get_application(model, application_name)
762
763 if application is None:
764 raise JujuApplicationNotFound(
765 "Application not found: {} (model={})".format(
766 application_name, model_name
767 )
768 )
769
770 unit = self._get_unit(application, machine_id)
771 if not unit:
772 raise JujuError(
773 "A unit with machine id {} not in available units".format(
774 machine_id
775 )
776 )
777
778 unit_name = unit.name
779
780 self.log.debug(
781 "Destroying unit {} from application {} in model {}".format(
782 unit_name, application_name, model_name
783 )
784 )
785 await application.destroy_unit(unit_name)
786
787 self.log.debug(
788 "Waiting for unit {} to be destroyed in application {} (model={})...".format(
789 unit_name, application_name, model_name
790 )
791 )
792
793 # TODO: Add functionality in the Juju watcher to replace this kind of blocks
794 if total_timeout is None:
795 total_timeout = 3600
796 end = time.time() + total_timeout
797 while time.time() < end:
798 if not self._get_unit(application, machine_id):
799 self.log.debug(
800 "The unit {} was destroyed in application {} (model={}) ".format(
801 unit_name, application_name, model_name
802 )
803 )
804 return
805 await asyncio.sleep(5)
806 self.log.debug(
807 "Unit {} is destroyed from application {} in model {}".format(
808 unit_name, application_name, model_name
809 )
810 )
811 finally:
812 if model:
813 await self.disconnect_model(model)
814 await self.disconnect_controller(controller)
815
David Garcia4fee80e2020-05-13 12:18:38 +0200816 async def deploy_charm(
817 self,
818 application_name: str,
819 path: str,
820 model_name: str,
821 machine_id: str,
822 db_dict: dict = None,
823 progress_timeout: float = None,
824 total_timeout: float = None,
825 config: dict = None,
826 series: str = None,
David Garciaf8a9d462020-03-25 18:19:02 +0100827 num_units: int = 1,
David Garcia4fee80e2020-05-13 12:18:38 +0200828 ):
829 """Deploy charm
830
831 :param: application_name: Application name
832 :param: path: Local path to the charm
833 :param: model_name: Model name
834 :param: machine_id ID of the machine
835 :param: db_dict: Dictionary with data of the DB to write the updates
836 :param: progress_timeout: Maximum time between two updates in the model
837 :param: total_timeout: Timeout for the entity to be active
838 :param: config: Config for the charm
839 :param: series: Series of the charm
David Garciaf8a9d462020-03-25 18:19:02 +0100840 :param: num_units: Number of units
David Garcia4fee80e2020-05-13 12:18:38 +0200841
842 :return: (juju.application.Application): Juju application
843 """
David Garcia2f66c4d2020-06-19 11:40:18 +0200844 self.log.debug(
845 "Deploying charm {} to machine {} in model ~{}".format(
846 application_name, machine_id, model_name
847 )
848 )
849 self.log.debug("charm: {}".format(path))
850
851 # Get controller
852 controller = await self.get_controller()
David Garcia4fee80e2020-05-13 12:18:38 +0200853
854 # Get model
David Garcia2f66c4d2020-06-19 11:40:18 +0200855 model = await self.get_model(controller, model_name)
David Garcia4fee80e2020-05-13 12:18:38 +0200856
857 try:
David Garcia4fee80e2020-05-13 12:18:38 +0200858 if application_name not in model.applications:
David Garcia4fee80e2020-05-13 12:18:38 +0200859 if machine_id is not None:
aktasfa02f8a2021-07-29 17:41:40 +0300860 machine, series = self._get_machine_info(model, machine_id)
David Garcia4fee80e2020-05-13 12:18:38 +0200861
862 application = await model.deploy(
863 entity_url=path,
864 application_name=application_name,
865 channel="stable",
866 num_units=1,
867 series=series,
868 to=machine_id,
869 config=config,
870 )
871
David Garcia2f66c4d2020-06-19 11:40:18 +0200872 self.log.debug(
873 "Wait until application {} is ready in model {}".format(
874 application_name, model_name
875 )
876 )
David Garciaf8a9d462020-03-25 18:19:02 +0100877 if num_units > 1:
878 for _ in range(num_units - 1):
879 m, _ = await self.create_machine(model_name, wait=False)
880 await application.add_unit(to=m.entity_id)
881
David Garcia4fee80e2020-05-13 12:18:38 +0200882 await JujuModelWatcher.wait_for(
883 model=model,
884 entity=application,
885 progress_timeout=progress_timeout,
886 total_timeout=total_timeout,
887 db_dict=db_dict,
888 n2vc=self.n2vc,
David Garciaeb8943a2021-04-12 12:07:37 +0200889 vca_id=self.vca_connection._vca_id,
David Garcia4fee80e2020-05-13 12:18:38 +0200890 )
David Garcia2f66c4d2020-06-19 11:40:18 +0200891 self.log.debug(
892 "Application {} is ready in model {}".format(
893 application_name, model_name
894 )
895 )
David Garcia4fee80e2020-05-13 12:18:38 +0200896 else:
David Garcia2f66c4d2020-06-19 11:40:18 +0200897 raise JujuApplicationExists(
898 "Application {} exists".format(application_name)
899 )
aktas42e51cf2021-10-19 20:03:23 +0300900 except juju.errors.JujuError as e:
901 if "already exists" in e.message:
902 raise JujuApplicationExists(
903 "Application {} exists".format(application_name)
904 )
905 else:
906 raise e
David Garcia4fee80e2020-05-13 12:18:38 +0200907 finally:
908 await self.disconnect_model(model)
David Garcia2f66c4d2020-06-19 11:40:18 +0200909 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +0200910
911 return application
912
beierlmdb1d37b2022-04-14 16:33:51 -0400913 async def upgrade_charm(
914 self,
915 application_name: str,
916 path: str,
917 model_name: str,
918 total_timeout: float = None,
919 **kwargs,
920 ):
921 """Upgrade Charm
922
923 :param: application_name: Application name
924 :param: model_name: Model name
925 :param: path: Local path to the charm
926 :param: total_timeout: Timeout for the entity to be active
927
928 :return: (str, str): (output and status)
929 """
930
931 self.log.debug(
932 "Upgrading charm {} in model {} from path {}".format(
933 application_name, model_name, path
934 )
935 )
936
937 await self.resolve_application(
938 model_name=model_name, application_name=application_name
939 )
940
941 # Get controller
942 controller = await self.get_controller()
943
944 # Get model
945 model = await self.get_model(controller, model_name)
946
947 try:
948 # Get application
949 application = self._get_application(
950 model,
951 application_name=application_name,
952 )
953 if application is None:
954 raise JujuApplicationNotFound(
955 "Cannot find application {} to upgrade".format(application_name)
956 )
957
958 await application.refresh(path=path)
959
960 self.log.debug(
961 "Wait until charm upgrade is completed for application {} (model={})".format(
962 application_name, model_name
963 )
964 )
965
966 await JujuModelWatcher.ensure_units_idle(
967 model=model, application=application
968 )
969
970 if application.status == "error":
971 error_message = "Unknown"
972 for unit in application.units:
973 if (
974 unit.workload_status == "error"
garciadeblas3e8e4dc2024-11-26 14:52:44 +0100975 and unit.workload_status_message != "" # pylint: disable=E1101
beierlmdb1d37b2022-04-14 16:33:51 -0400976 ):
garciadeblas3e8e4dc2024-11-26 14:52:44 +0100977 error_message = (
978 unit.workload_status_message # pylint: disable=E1101
979 )
beierlmdb1d37b2022-04-14 16:33:51 -0400980
981 message = "Application {} failed update in {}: {}".format(
982 application_name, model_name, error_message
983 )
984 self.log.error(message)
985 raise JujuError(message=message)
986
987 self.log.debug(
988 "Application {} is ready in model {}".format(
989 application_name, model_name
990 )
991 )
992
993 finally:
994 await self.disconnect_model(model)
995 await self.disconnect_controller(controller)
996
997 return application
998
999 async def resolve_application(self, model_name: str, application_name: str):
beierlmdb1d37b2022-04-14 16:33:51 -04001000 controller = await self.get_controller()
1001 model = await self.get_model(controller, model_name)
1002
1003 try:
1004 application = self._get_application(
1005 model,
1006 application_name=application_name,
1007 )
1008 if application is None:
1009 raise JujuApplicationNotFound(
1010 "Cannot find application {} to resolve".format(application_name)
1011 )
1012
1013 while application.status == "error":
1014 for unit in application.units:
1015 if unit.workload_status == "error":
1016 self.log.debug(
1017 "Model {}, Application {}, Unit {} in error state, resolving".format(
1018 model_name, application_name, unit.entity_id
1019 )
1020 )
1021 try:
garciadeblas3e8e4dc2024-11-26 14:52:44 +01001022 await unit.resolved(retry=False) # pylint: disable=E1101
beierlmdb1d37b2022-04-14 16:33:51 -04001023 except Exception:
1024 pass
1025
1026 await asyncio.sleep(1)
1027
1028 finally:
1029 await self.disconnect_model(model)
1030 await self.disconnect_controller(controller)
1031
David Garciaf2e33832022-06-02 16:11:28 +02001032 async def resolve(self, model_name: str):
David Garciaf2e33832022-06-02 16:11:28 +02001033 controller = await self.get_controller()
1034 model = await self.get_model(controller, model_name)
1035 all_units_active = False
1036 try:
1037 while not all_units_active:
1038 all_units_active = True
1039 for application_name, application in model.applications.items():
1040 if application.status == "error":
1041 for unit in application.units:
1042 if unit.workload_status == "error":
1043 self.log.debug(
1044 "Model {}, Application {}, Unit {} in error state, resolving".format(
1045 model_name, application_name, unit.entity_id
1046 )
1047 )
1048 try:
1049 await unit.resolved(retry=False)
1050 all_units_active = False
1051 except Exception:
1052 pass
1053
1054 if not all_units_active:
1055 await asyncio.sleep(5)
1056 finally:
1057 await self.disconnect_model(model)
1058 await self.disconnect_controller(controller)
1059
aktas2962f3e2021-03-15 11:05:35 +03001060 async def scale_application(
garciadeblas82b591c2021-03-24 09:22:13 +01001061 self,
1062 model_name: str,
1063 application_name: str,
1064 scale: int = 1,
1065 total_timeout: float = None,
aktas2962f3e2021-03-15 11:05:35 +03001066 ):
1067 """
1068 Scale application (K8s)
1069
1070 :param: model_name: Model name
1071 :param: application_name: Application name
1072 :param: scale: Scale to which to set this application
1073 :param: total_timeout: Timeout for the entity to be active
1074 """
1075
1076 model = None
1077 controller = await self.get_controller()
1078 try:
1079 model = await self.get_model(controller, model_name)
1080
1081 self.log.debug(
1082 "Scaling application {} in model {}".format(
1083 application_name, model_name
1084 )
1085 )
1086 application = self._get_application(model, application_name)
1087 if application is None:
1088 raise JujuApplicationNotFound("Cannot scale application")
1089 await application.scale(scale=scale)
1090 # Wait until application is scaled in model
1091 self.log.debug(
garciadeblas82b591c2021-03-24 09:22:13 +01001092 "Waiting for application {} to be scaled in model {}...".format(
aktas2962f3e2021-03-15 11:05:35 +03001093 application_name, model_name
1094 )
1095 )
1096 if total_timeout is None:
1097 total_timeout = 1800
1098 end = time.time() + total_timeout
1099 while time.time() < end:
1100 application_scale = self._get_application_count(model, application_name)
1101 # Before calling wait_for_model function,
1102 # wait until application unit count and scale count are equal.
1103 # Because there is a delay before scaling triggers in Juju model.
1104 if application_scale == scale:
garciadeblas82b591c2021-03-24 09:22:13 +01001105 await JujuModelWatcher.wait_for_model(
1106 model=model, timeout=total_timeout
1107 )
aktas2962f3e2021-03-15 11:05:35 +03001108 self.log.debug(
1109 "Application {} is scaled in model {}".format(
1110 application_name, model_name
1111 )
1112 )
1113 return
1114 await asyncio.sleep(5)
1115 raise Exception(
1116 "Timeout waiting for application {} in model {} to be scaled".format(
1117 application_name, model_name
1118 )
1119 )
1120 finally:
1121 if model:
1122 await self.disconnect_model(model)
1123 await self.disconnect_controller(controller)
1124
1125 def _get_application_count(self, model: Model, application_name: str) -> int:
1126 """Get number of units of the application
1127
1128 :param: model: Model object
1129 :param: application_name: Application name
1130
1131 :return: int (or None if application doesn't exist)
1132 """
1133 application = self._get_application(model, application_name)
1134 if application is not None:
1135 return len(application.units)
1136
David Garcia2f66c4d2020-06-19 11:40:18 +02001137 def _get_application(self, model: Model, application_name: str) -> Application:
David Garcia4fee80e2020-05-13 12:18:38 +02001138 """Get application
1139
1140 :param: model: Model object
1141 :param: application_name: Application name
1142
1143 :return: juju.application.Application (or None if it doesn't exist)
1144 """
1145 if model.applications and application_name in model.applications:
1146 return model.applications[application_name]
1147
aktasfa02f8a2021-07-29 17:41:40 +03001148 def _get_unit(self, application: Application, machine_id: str) -> Unit:
1149 """Get unit
1150
1151 :param: application: Application object
1152 :param: machine_id: Machine id
1153
1154 :return: Unit
1155 """
1156 unit = None
1157 for u in application.units:
1158 if u.machine_id == machine_id:
1159 unit = u
1160 break
1161 return unit
1162
1163 def _get_machine_info(
1164 self,
1165 model,
1166 machine_id: str,
1167 ) -> (str, str):
1168 """Get machine info
1169
1170 :param: model: Model object
1171 :param: machine_id: Machine id
1172
1173 :return: (str, str): (machine, series)
1174 """
1175 if machine_id not in model.machines:
1176 msg = "Machine {} not found in model".format(machine_id)
1177 self.log.error(msg=msg)
1178 raise JujuMachineNotFound(msg)
1179 machine = model.machines[machine_id]
1180 return machine, machine.series
1181
David Garcia4fee80e2020-05-13 12:18:38 +02001182 async def execute_action(
1183 self,
1184 application_name: str,
1185 model_name: str,
1186 action_name: str,
1187 db_dict: dict = None,
aktasfa02f8a2021-07-29 17:41:40 +03001188 machine_id: str = None,
David Garcia4fee80e2020-05-13 12:18:38 +02001189 progress_timeout: float = None,
1190 total_timeout: float = None,
David Garciaf980ac02021-07-27 15:07:42 +02001191 **kwargs,
David Garcia4fee80e2020-05-13 12:18:38 +02001192 ):
1193 """Execute action
1194
1195 :param: application_name: Application name
1196 :param: model_name: Model name
David Garcia4fee80e2020-05-13 12:18:38 +02001197 :param: action_name: Name of the action
1198 :param: db_dict: Dictionary with data of the DB to write the updates
aktasfa02f8a2021-07-29 17:41:40 +03001199 :param: machine_id Machine id
David Garcia4fee80e2020-05-13 12:18:38 +02001200 :param: progress_timeout: Maximum time between two updates in the model
1201 :param: total_timeout: Timeout for the entity to be active
1202
1203 :return: (str, str): (output and status)
1204 """
David Garcia2f66c4d2020-06-19 11:40:18 +02001205 self.log.debug(
1206 "Executing action {} using params {}".format(action_name, kwargs)
1207 )
1208 # Get controller
1209 controller = await self.get_controller()
1210
1211 # Get model
1212 model = await self.get_model(controller, model_name)
David Garcia4fee80e2020-05-13 12:18:38 +02001213
1214 try:
1215 # Get application
David Garcia2f66c4d2020-06-19 11:40:18 +02001216 application = self._get_application(
David Garciaf6e9b002020-11-27 15:32:02 +01001217 model,
1218 application_name=application_name,
David Garcia4fee80e2020-05-13 12:18:38 +02001219 )
1220 if application is None:
1221 raise JujuApplicationNotFound("Cannot execute action")
David Garcia59f520d2020-10-15 13:16:45 +02001222 # Racing condition:
1223 # Ocassionally, self._get_leader_unit() will return None
1224 # because the leader elected hook has not been triggered yet.
1225 # Therefore, we are doing some retries. If it happens again,
1226 # re-open bug 1236
aktasfa02f8a2021-07-29 17:41:40 +03001227 if machine_id is None:
1228 unit = await self._get_leader_unit(application)
1229 self.log.debug(
1230 "Action {} is being executed on the leader unit {}".format(
1231 action_name, unit.name
1232 )
1233 )
1234 else:
1235 unit = self._get_unit(application, machine_id)
1236 if not unit:
1237 raise JujuError(
1238 "A unit with machine id {} not in available units".format(
1239 machine_id
1240 )
1241 )
1242 self.log.debug(
1243 "Action {} is being executed on {} unit".format(
1244 action_name, unit.name
1245 )
1246 )
David Garcia4fee80e2020-05-13 12:18:38 +02001247
1248 actions = await application.get_actions()
1249
1250 if action_name not in actions:
Dominik Fleischmann7ff392f2020-07-07 13:11:19 +02001251 raise JujuActionNotFound(
David Garcia4fee80e2020-05-13 12:18:38 +02001252 "Action {} not in available actions".format(action_name)
1253 )
1254
David Garcia4fee80e2020-05-13 12:18:38 +02001255 action = await unit.run_action(action_name, **kwargs)
1256
David Garcia2f66c4d2020-06-19 11:40:18 +02001257 self.log.debug(
1258 "Wait until action {} is completed in application {} (model={})".format(
1259 action_name, application_name, model_name
1260 )
1261 )
David Garcia4fee80e2020-05-13 12:18:38 +02001262 await JujuModelWatcher.wait_for(
1263 model=model,
1264 entity=action,
1265 progress_timeout=progress_timeout,
1266 total_timeout=total_timeout,
1267 db_dict=db_dict,
1268 n2vc=self.n2vc,
David Garciaeb8943a2021-04-12 12:07:37 +02001269 vca_id=self.vca_connection._vca_id,
David Garcia4fee80e2020-05-13 12:18:38 +02001270 )
David Garcia2f66c4d2020-06-19 11:40:18 +02001271
David Garcia4fee80e2020-05-13 12:18:38 +02001272 output = await model.get_action_output(action_uuid=action.entity_id)
1273 status = await model.get_action_status(uuid_or_prefix=action.entity_id)
1274 status = (
1275 status[action.entity_id] if action.entity_id in status else "failed"
1276 )
1277
David Garcia2f66c4d2020-06-19 11:40:18 +02001278 self.log.debug(
1279 "Action {} completed with status {} in application {} (model={})".format(
1280 action_name, action.status, application_name, model_name
1281 )
1282 )
David Garcia4fee80e2020-05-13 12:18:38 +02001283 finally:
1284 await self.disconnect_model(model)
David Garcia2f66c4d2020-06-19 11:40:18 +02001285 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +02001286
1287 return output, status
1288
1289 async def get_actions(self, application_name: str, model_name: str) -> dict:
1290 """Get list of actions
1291
1292 :param: application_name: Application name
1293 :param: model_name: Model name
1294
1295 :return: Dict with this format
1296 {
1297 "action_name": "Description of the action",
1298 ...
1299 }
1300 """
David Garcia2f66c4d2020-06-19 11:40:18 +02001301 self.log.debug(
1302 "Getting list of actions for application {}".format(application_name)
David Garcia4fee80e2020-05-13 12:18:38 +02001303 )
1304
David Garcia2f66c4d2020-06-19 11:40:18 +02001305 # Get controller
1306 controller = await self.get_controller()
David Garcia4fee80e2020-05-13 12:18:38 +02001307
David Garcia2f66c4d2020-06-19 11:40:18 +02001308 # Get model
1309 model = await self.get_model(controller, model_name)
David Garcia4fee80e2020-05-13 12:18:38 +02001310
David Garcia2f66c4d2020-06-19 11:40:18 +02001311 try:
1312 # Get application
1313 application = self._get_application(
David Garciaf6e9b002020-11-27 15:32:02 +01001314 model,
1315 application_name=application_name,
David Garcia2f66c4d2020-06-19 11:40:18 +02001316 )
1317
1318 # Return list of actions
1319 return await application.get_actions()
1320
1321 finally:
1322 # Disconnect from model and controller
1323 await self.disconnect_model(model)
1324 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +02001325
David Garcia85755d12020-09-21 19:51:23 +02001326 async def get_metrics(self, model_name: str, application_name: str) -> dict:
1327 """Get the metrics collected by the VCA.
1328
1329 :param model_name The name or unique id of the network service
1330 :param application_name The name of the application
1331 """
1332 if not model_name or not application_name:
1333 raise Exception("model_name and application_name must be non-empty strings")
1334 metrics = {}
1335 controller = await self.get_controller()
1336 model = await self.get_model(controller, model_name)
1337 try:
1338 application = self._get_application(model, application_name)
1339 if application is not None:
1340 metrics = await application.get_metrics()
1341 finally:
1342 self.disconnect_model(model)
1343 self.disconnect_controller(controller)
1344 return metrics
1345
David Garcia4fee80e2020-05-13 12:18:38 +02001346 async def add_relation(
David Garciaf6e9b002020-11-27 15:32:02 +01001347 self,
1348 model_name: str,
1349 endpoint_1: str,
1350 endpoint_2: str,
David Garcia4fee80e2020-05-13 12:18:38 +02001351 ):
1352 """Add relation
1353
David Garcia8331f7c2020-08-25 16:10:07 +02001354 :param: model_name: Model name
1355 :param: endpoint_1 First endpoint name
1356 ("app:endpoint" format or directly the saas name)
1357 :param: endpoint_2: Second endpoint name (^ same format)
David Garcia4fee80e2020-05-13 12:18:38 +02001358 """
1359
David Garcia8331f7c2020-08-25 16:10:07 +02001360 self.log.debug("Adding relation: {} -> {}".format(endpoint_1, endpoint_2))
David Garcia2f66c4d2020-06-19 11:40:18 +02001361
1362 # Get controller
1363 controller = await self.get_controller()
1364
David Garcia4fee80e2020-05-13 12:18:38 +02001365 # Get model
David Garcia2f66c4d2020-06-19 11:40:18 +02001366 model = await self.get_model(controller, model_name)
David Garcia4fee80e2020-05-13 12:18:38 +02001367
David Garcia4fee80e2020-05-13 12:18:38 +02001368 # Add relation
David Garcia4fee80e2020-05-13 12:18:38 +02001369 try:
David Garcia8331f7c2020-08-25 16:10:07 +02001370 await model.add_relation(endpoint_1, endpoint_2)
David Garciaf980ac02021-07-27 15:07:42 +02001371 except juju.errors.JujuAPIError as e:
Patricia Reinosoa07f6952023-01-04 10:40:10 +00001372 if self._relation_is_not_found(e):
David Garcia4fee80e2020-05-13 12:18:38 +02001373 self.log.warning("Relation not found: {}".format(e.message))
1374 return
Patricia Reinosoa07f6952023-01-04 10:40:10 +00001375 if self._relation_already_exist(e):
David Garcia4fee80e2020-05-13 12:18:38 +02001376 self.log.warning("Relation already exists: {}".format(e.message))
1377 return
1378 # another exception, raise it
1379 raise e
1380 finally:
1381 await self.disconnect_model(model)
David Garcia2f66c4d2020-06-19 11:40:18 +02001382 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +02001383
Patricia Reinosoa07f6952023-01-04 10:40:10 +00001384 def _relation_is_not_found(self, juju_error):
1385 text = "not found"
1386 return (text in juju_error.message) or (
1387 juju_error.error_code and text in juju_error.error_code
1388 )
1389
1390 def _relation_already_exist(self, juju_error):
1391 text = "already exists"
1392 return (text in juju_error.message) or (
1393 juju_error.error_code and text in juju_error.error_code
1394 )
1395
David Garcia582b9232021-10-26 12:30:44 +02001396 async def offer(self, endpoint: RelationEndpoint) -> Offer:
1397 """
1398 Create an offer from a RelationEndpoint
1399
1400 :param: endpoint: Relation endpoint
1401
1402 :return: Offer object
1403 """
1404 model_name = endpoint.model_name
1405 offer_name = f"{endpoint.application_name}-{endpoint.endpoint_name}"
1406 controller = await self.get_controller()
1407 model = None
1408 try:
1409 model = await self.get_model(controller, model_name)
1410 await model.create_offer(endpoint.endpoint, offer_name=offer_name)
1411 offer_list = await self._list_offers(model_name, offer_name=offer_name)
1412 if offer_list:
1413 return Offer(offer_list[0].offer_url)
1414 else:
1415 raise Exception("offer was not created")
1416 except juju.errors.JujuError as e:
1417 if "application offer already exists" not in e.message:
1418 raise e
1419 finally:
1420 if model:
1421 self.disconnect_model(model)
1422 self.disconnect_controller(controller)
1423
David Garcia68b00722020-09-11 15:05:00 +02001424 async def consume(
David Garciaf6e9b002020-11-27 15:32:02 +01001425 self,
David Garciaf6e9b002020-11-27 15:32:02 +01001426 model_name: str,
David Garcia582b9232021-10-26 12:30:44 +02001427 offer: Offer,
1428 provider_libjuju: "Libjuju",
1429 ) -> str:
David Garcia68b00722020-09-11 15:05:00 +02001430 """
David Garcia582b9232021-10-26 12:30:44 +02001431 Consumes a remote offer in the model. Relations can be created later using "juju relate".
David Garcia68b00722020-09-11 15:05:00 +02001432
David Garcia582b9232021-10-26 12:30:44 +02001433 :param: model_name: Model name
1434 :param: offer: Offer object to consume
1435 :param: provider_libjuju: Libjuju object of the provider endpoint
David Garcia68b00722020-09-11 15:05:00 +02001436
1437 :raises ParseError if there's a problem parsing the offer_url
1438 :raises JujuError if remote offer includes and endpoint
1439 :raises JujuAPIError if the operation is not successful
David Garcia68b00722020-09-11 15:05:00 +02001440
David Garcia582b9232021-10-26 12:30:44 +02001441 :returns: Saas name. It is the application name in the model that reference the remote application.
1442 """
1443 saas_name = f'{offer.name}-{offer.model_name.replace("-", "")}'
1444 if offer.vca_id:
1445 saas_name = f"{saas_name}-{offer.vca_id}"
1446 controller = await self.get_controller()
1447 model = None
1448 provider_controller = None
David Garcia68b00722020-09-11 15:05:00 +02001449 try:
David Garcia582b9232021-10-26 12:30:44 +02001450 model = await controller.get_model(model_name)
1451 provider_controller = await provider_libjuju.get_controller()
1452 await model.consume(
1453 offer.url, application_alias=saas_name, controller=provider_controller
1454 )
1455 return saas_name
David Garcia68b00722020-09-11 15:05:00 +02001456 finally:
David Garcia582b9232021-10-26 12:30:44 +02001457 if model:
1458 await self.disconnect_model(model)
1459 if provider_controller:
1460 await provider_libjuju.disconnect_controller(provider_controller)
David Garcia68b00722020-09-11 15:05:00 +02001461 await self.disconnect_controller(controller)
1462
David Garciae610aed2021-07-26 15:04:37 +02001463 async def destroy_model(self, model_name: str, total_timeout: float = 1800):
David Garcia4fee80e2020-05-13 12:18:38 +02001464 """
1465 Destroy model
1466
1467 :param: model_name: Model name
1468 :param: total_timeout: Timeout
1469 """
David Garcia4fee80e2020-05-13 12:18:38 +02001470
David Garcia2f66c4d2020-06-19 11:40:18 +02001471 controller = await self.get_controller()
David Garcia435b8642021-03-10 17:09:44 +01001472 model = None
David Garcia2f66c4d2020-06-19 11:40:18 +02001473 try:
David Garciab0a8f402021-03-15 18:41:34 +01001474 if not await self.model_exists(model_name, controller=controller):
David Garcia1608b562022-05-06 12:26:20 +02001475 self.log.warn(f"Model {model_name} doesn't exist")
David Garciab0a8f402021-03-15 18:41:34 +01001476 return
1477
David Garcia1608b562022-05-06 12:26:20 +02001478 self.log.debug(f"Getting model {model_name} to be destroyed")
David Garciae610aed2021-07-26 15:04:37 +02001479 model = await self.get_model(controller, model_name)
David Garcia1608b562022-05-06 12:26:20 +02001480 self.log.debug(f"Destroying manual machines in model {model_name}")
David Garcia168bb192020-10-21 14:19:45 +02001481 # Destroy machines that are manually provisioned
1482 # and still are in pending state
1483 await self._destroy_pending_machines(model, only_manual=True)
David Garcia2f66c4d2020-06-19 11:40:18 +02001484 await self.disconnect_model(model)
1485
David Garciaf2e33832022-06-02 16:11:28 +02001486 await asyncio.wait_for(
1487 self._destroy_model(model_name, controller),
David Garciae610aed2021-07-26 15:04:37 +02001488 timeout=total_timeout,
1489 )
David Garcia5c966622022-05-03 12:23:59 +02001490 except Exception as e:
1491 if not await self.model_exists(model_name, controller=controller):
David Garcia1608b562022-05-06 12:26:20 +02001492 self.log.warn(
1493 f"Failed deleting model {model_name}: model doesn't exist"
1494 )
David Garcia5c966622022-05-03 12:23:59 +02001495 return
David Garcia1608b562022-05-06 12:26:20 +02001496 self.log.warn(f"Failed deleting model {model_name}: {e}")
David Garcia5c966622022-05-03 12:23:59 +02001497 raise e
David Garciae610aed2021-07-26 15:04:37 +02001498 finally:
1499 if model:
1500 await self.disconnect_model(model)
1501 await self.disconnect_controller(controller)
David Garcia2f66c4d2020-06-19 11:40:18 +02001502
David Garciae610aed2021-07-26 15:04:37 +02001503 async def _destroy_model(
David Garciaf2e33832022-06-02 16:11:28 +02001504 self,
1505 model_name: str,
1506 controller: Controller,
David Garciae610aed2021-07-26 15:04:37 +02001507 ):
1508 """
1509 Destroy model from controller
David Garcia2f66c4d2020-06-19 11:40:18 +02001510
David Garciae610aed2021-07-26 15:04:37 +02001511 :param: model: Model name to be removed
1512 :param: controller: Controller object
1513 :param: timeout: Timeout in seconds
1514 """
David Garcia1608b562022-05-06 12:26:20 +02001515 self.log.debug(f"Destroying model {model_name}")
David Garciae610aed2021-07-26 15:04:37 +02001516
David Garciaf2e33832022-06-02 16:11:28 +02001517 async def _destroy_model_gracefully(model_name: str, controller: Controller):
1518 self.log.info(f"Gracefully deleting model {model_name}")
1519 resolved = False
1520 while model_name in await controller.list_models():
1521 if not resolved:
1522 await self.resolve(model_name)
1523 resolved = True
1524 await controller.destroy_model(model_name, destroy_storage=True)
1525
1526 await asyncio.sleep(5)
1527 self.log.info(f"Model {model_name} deleted gracefully")
1528
1529 async def _destroy_model_forcefully(model_name: str, controller: Controller):
1530 self.log.info(f"Forcefully deleting model {model_name}")
1531 while model_name in await controller.list_models():
David Garciae610aed2021-07-26 15:04:37 +02001532 await controller.destroy_model(
David Garciaf2e33832022-06-02 16:11:28 +02001533 model_name, destroy_storage=True, force=True, max_wait=60
David Garciae610aed2021-07-26 15:04:37 +02001534 )
David Garcia2f66c4d2020-06-19 11:40:18 +02001535 await asyncio.sleep(5)
David Garciaf2e33832022-06-02 16:11:28 +02001536 self.log.info(f"Model {model_name} deleted forcefully")
David Garciae610aed2021-07-26 15:04:37 +02001537
1538 try:
David Garcia1cfed492022-06-08 11:16:54 +02001539 try:
1540 await asyncio.wait_for(
1541 _destroy_model_gracefully(model_name, controller), timeout=120
1542 )
1543 except asyncio.TimeoutError:
1544 await _destroy_model_forcefully(model_name, controller)
David Garcia7e887b22022-04-28 13:43:36 +02001545 except juju.errors.JujuError as e:
1546 if any("has been removed" in error for error in e.errors):
1547 return
David Garcia1cfed492022-06-08 11:16:54 +02001548 if any("model not found" in error for error in e.errors):
1549 return
David Garcia7e887b22022-04-28 13:43:36 +02001550 raise e
David Garcia4fee80e2020-05-13 12:18:38 +02001551
aktas56120292021-02-26 15:32:39 +03001552 async def destroy_application(
1553 self, model_name: str, application_name: str, total_timeout: float
1554 ):
David Garcia4fee80e2020-05-13 12:18:38 +02001555 """
1556 Destroy application
1557
aktas56120292021-02-26 15:32:39 +03001558 :param: model_name: Model name
David Garcia4fee80e2020-05-13 12:18:38 +02001559 :param: application_name: Application name
aktas56120292021-02-26 15:32:39 +03001560 :param: total_timeout: Timeout
David Garcia4fee80e2020-05-13 12:18:38 +02001561 """
aktas56120292021-02-26 15:32:39 +03001562
1563 controller = await self.get_controller()
1564 model = None
1565
1566 try:
1567 model = await self.get_model(controller, model_name)
1568 self.log.debug(
1569 "Destroying application {} in model {}".format(
1570 application_name, model_name
1571 )
David Garcia4fee80e2020-05-13 12:18:38 +02001572 )
aktas56120292021-02-26 15:32:39 +03001573 application = self._get_application(model, application_name)
1574 if application:
1575 await application.destroy()
1576 else:
1577 self.log.warning("Application not found: {}".format(application_name))
1578
1579 self.log.debug(
1580 "Waiting for application {} to be destroyed in model {}...".format(
1581 application_name, model_name
1582 )
1583 )
1584 if total_timeout is None:
1585 total_timeout = 3600
1586 end = time.time() + total_timeout
1587 while time.time() < end:
1588 if not self._get_application(model, application_name):
1589 self.log.debug(
1590 "The application {} was destroyed in model {} ".format(
1591 application_name, model_name
1592 )
1593 )
1594 return
1595 await asyncio.sleep(5)
1596 raise Exception(
1597 "Timeout waiting for application {} to be destroyed in model {}".format(
1598 application_name, model_name
1599 )
1600 )
1601 finally:
1602 if model is not None:
1603 await self.disconnect_model(model)
1604 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +02001605
David Garcia168bb192020-10-21 14:19:45 +02001606 async def _destroy_pending_machines(self, model: Model, only_manual: bool = False):
1607 """
1608 Destroy pending machines in a given model
1609
1610 :param: only_manual: Bool that indicates only manually provisioned
1611 machines should be destroyed (if True), or that
1612 all pending machines should be destroyed
1613 """
1614 status = await model.get_status()
1615 for machine_id in status.machines:
1616 machine_status = status.machines[machine_id]
1617 if machine_status.agent_status.status == "pending":
1618 if only_manual and not machine_status.instance_id.startswith("manual:"):
1619 break
1620 machine = model.machines[machine_id]
1621 await machine.destroy(force=True)
1622
David Garcia4fee80e2020-05-13 12:18:38 +02001623 async def configure_application(
1624 self, model_name: str, application_name: str, config: dict = None
1625 ):
1626 """Configure application
1627
1628 :param: model_name: Model name
1629 :param: application_name: Application name
1630 :param: config: Config to apply to the charm
1631 """
David Garcia2f66c4d2020-06-19 11:40:18 +02001632 self.log.debug("Configuring application {}".format(application_name))
1633
David Garcia4fee80e2020-05-13 12:18:38 +02001634 if config:
David Garcia5b802c92020-11-11 16:56:06 +01001635 controller = await self.get_controller()
1636 model = None
David Garcia2f66c4d2020-06-19 11:40:18 +02001637 try:
David Garcia2f66c4d2020-06-19 11:40:18 +02001638 model = await self.get_model(controller, model_name)
1639 application = self._get_application(
David Garciaf6e9b002020-11-27 15:32:02 +01001640 model,
1641 application_name=application_name,
David Garcia2f66c4d2020-06-19 11:40:18 +02001642 )
1643 await application.set_config(config)
1644 finally:
David Garcia5b802c92020-11-11 16:56:06 +01001645 if model:
1646 await self.disconnect_model(model)
David Garcia2f66c4d2020-06-19 11:40:18 +02001647 await self.disconnect_controller(controller)
1648
David Garcia2f66c4d2020-06-19 11:40:18 +02001649 async def health_check(self, interval: float = 300.0):
1650 """
1651 Health check to make sure controller and controller_model connections are OK
1652
1653 :param: interval: Time in seconds between checks
1654 """
David Garcia667696e2020-09-22 14:52:32 +02001655 controller = None
David Garcia2f66c4d2020-06-19 11:40:18 +02001656 while True:
1657 try:
1658 controller = await self.get_controller()
1659 # self.log.debug("VCA is alive")
1660 except Exception as e:
1661 self.log.error("Health check to VCA failed: {}".format(e))
1662 finally:
1663 await self.disconnect_controller(controller)
1664 await asyncio.sleep(interval)
Dominik Fleischmannb9513342020-06-09 11:57:14 +02001665
1666 async def list_models(self, contains: str = None) -> [str]:
1667 """List models with certain names
1668
1669 :param: contains: String that is contained in model name
1670
1671 :retur: [models] Returns list of model names
1672 """
1673
1674 controller = await self.get_controller()
1675 try:
1676 models = await controller.list_models()
1677 if contains:
1678 models = [model for model in models if contains in model]
1679 return models
1680 finally:
1681 await self.disconnect_controller(controller)
David Garciabc538e42020-08-25 15:22:30 +02001682
David Garcia582b9232021-10-26 12:30:44 +02001683 async def _list_offers(
1684 self, model_name: str, offer_name: str = None
1685 ) -> QueryApplicationOffersResults:
1686 """
1687 List offers within a model
David Garciabc538e42020-08-25 15:22:30 +02001688
1689 :param: model_name: Model name
David Garcia582b9232021-10-26 12:30:44 +02001690 :param: offer_name: Offer name to filter.
David Garciabc538e42020-08-25 15:22:30 +02001691
David Garcia582b9232021-10-26 12:30:44 +02001692 :return: Returns application offers results in the model
David Garciabc538e42020-08-25 15:22:30 +02001693 """
1694
1695 controller = await self.get_controller()
1696 try:
David Garcia582b9232021-10-26 12:30:44 +02001697 offers = (await controller.list_offers(model_name)).results
1698 if offer_name:
1699 matching_offer = []
1700 for offer in offers:
1701 if offer.offer_name == offer_name:
1702 matching_offer.append(offer)
1703 break
1704 offers = matching_offer
1705 return offers
David Garciabc538e42020-08-25 15:22:30 +02001706 finally:
1707 await self.disconnect_controller(controller)
David Garcia12b29242020-09-17 16:01:48 +02001708
David Garcia475a7222020-09-21 16:19:15 +02001709 async def add_k8s(
David Garcia7077e262020-10-16 15:38:13 +02001710 self,
1711 name: str,
David Garciaf6e9b002020-11-27 15:32:02 +01001712 rbac_id: str,
1713 token: str,
1714 client_cert_data: str,
David Garcia7077e262020-10-16 15:38:13 +02001715 configuration: Configuration,
1716 storage_class: str,
1717 credential_name: str = None,
David Garcia475a7222020-09-21 16:19:15 +02001718 ):
David Garcia12b29242020-09-17 16:01:48 +02001719 """
1720 Add a Kubernetes cloud to the controller
1721
1722 Similar to the `juju add-k8s` command in the CLI
1723
David Garcia7077e262020-10-16 15:38:13 +02001724 :param: name: Name for the K8s cloud
1725 :param: configuration: Kubernetes configuration object
1726 :param: storage_class: Storage Class to use in the cloud
1727 :param: credential_name: Storage Class to use in the cloud
David Garcia12b29242020-09-17 16:01:48 +02001728 """
1729
David Garcia12b29242020-09-17 16:01:48 +02001730 if not storage_class:
1731 raise Exception("storage_class must be a non-empty string")
1732 if not name:
1733 raise Exception("name must be a non-empty string")
David Garcia475a7222020-09-21 16:19:15 +02001734 if not configuration:
1735 raise Exception("configuration must be provided")
David Garcia12b29242020-09-17 16:01:48 +02001736
David Garcia475a7222020-09-21 16:19:15 +02001737 endpoint = configuration.host
David Garciaf6e9b002020-11-27 15:32:02 +01001738 credential = self.get_k8s_cloud_credential(
1739 configuration,
1740 client_cert_data,
1741 token,
David Garcia475a7222020-09-21 16:19:15 +02001742 )
David Garciaf6e9b002020-11-27 15:32:02 +01001743 credential.attrs[RBAC_LABEL_KEY_NAME] = rbac_id
David Garcia12b29242020-09-17 16:01:48 +02001744 cloud = client.Cloud(
David Garcia475a7222020-09-21 16:19:15 +02001745 type_="kubernetes",
1746 auth_types=[credential.auth_type],
David Garcia12b29242020-09-17 16:01:48 +02001747 endpoint=endpoint,
David Garciaf6e9b002020-11-27 15:32:02 +01001748 ca_certificates=[client_cert_data],
David Garcia12b29242020-09-17 16:01:48 +02001749 config={
1750 "operator-storage": storage_class,
1751 "workload-storage": storage_class,
1752 },
David Garcia12b29242020-09-17 16:01:48 +02001753 )
1754
David Garcia7077e262020-10-16 15:38:13 +02001755 return await self.add_cloud(
1756 name, cloud, credential, credential_name=credential_name
1757 )
David Garcia475a7222020-09-21 16:19:15 +02001758
1759 def get_k8s_cloud_credential(
David Garciaf6e9b002020-11-27 15:32:02 +01001760 self,
1761 configuration: Configuration,
1762 client_cert_data: str,
1763 token: str = None,
David Garcia475a7222020-09-21 16:19:15 +02001764 ) -> client.CloudCredential:
1765 attrs = {}
David Garciaf6e9b002020-11-27 15:32:02 +01001766 # TODO: Test with AKS
1767 key = None # open(configuration.key_file, "r").read()
David Garcia475a7222020-09-21 16:19:15 +02001768 username = configuration.username
1769 password = configuration.password
1770
David Garciaf6e9b002020-11-27 15:32:02 +01001771 if client_cert_data:
1772 attrs["ClientCertificateData"] = client_cert_data
David Garcia475a7222020-09-21 16:19:15 +02001773 if key:
David Garciaf6e9b002020-11-27 15:32:02 +01001774 attrs["ClientKeyData"] = key
David Garcia475a7222020-09-21 16:19:15 +02001775 if token:
1776 if username or password:
1777 raise JujuInvalidK8sConfiguration("Cannot set both token and user/pass")
1778 attrs["Token"] = token
1779
1780 auth_type = None
1781 if key:
1782 auth_type = "oauth2"
David Garciaf6e9b002020-11-27 15:32:02 +01001783 if client_cert_data:
1784 auth_type = "oauth2withcert"
David Garcia475a7222020-09-21 16:19:15 +02001785 if not token:
1786 raise JujuInvalidK8sConfiguration(
1787 "missing token for auth type {}".format(auth_type)
1788 )
1789 elif username:
1790 if not password:
1791 self.log.debug(
1792 "credential for user {} has empty password".format(username)
1793 )
1794 attrs["username"] = username
1795 attrs["password"] = password
David Garciaf6e9b002020-11-27 15:32:02 +01001796 if client_cert_data:
David Garcia475a7222020-09-21 16:19:15 +02001797 auth_type = "userpasswithcert"
1798 else:
1799 auth_type = "userpass"
David Garciaf6e9b002020-11-27 15:32:02 +01001800 elif client_cert_data and token:
David Garcia475a7222020-09-21 16:19:15 +02001801 auth_type = "certificate"
1802 else:
1803 raise JujuInvalidK8sConfiguration("authentication method not supported")
David Garcia667696e2020-09-22 14:52:32 +02001804 return client.CloudCredential(auth_type=auth_type, attrs=attrs)
David Garcia12b29242020-09-17 16:01:48 +02001805
1806 async def add_cloud(
David Garcia7077e262020-10-16 15:38:13 +02001807 self,
1808 name: str,
1809 cloud: Cloud,
1810 credential: CloudCredential = None,
1811 credential_name: str = None,
David Garcia12b29242020-09-17 16:01:48 +02001812 ) -> Cloud:
1813 """
1814 Add cloud to the controller
1815
David Garcia7077e262020-10-16 15:38:13 +02001816 :param: name: Name of the cloud to be added
1817 :param: cloud: Cloud object
1818 :param: credential: CloudCredentials object for the cloud
1819 :param: credential_name: Credential name.
1820 If not defined, cloud of the name will be used.
David Garcia12b29242020-09-17 16:01:48 +02001821 """
1822 controller = await self.get_controller()
1823 try:
1824 _ = await controller.add_cloud(name, cloud)
1825 if credential:
David Garcia7077e262020-10-16 15:38:13 +02001826 await controller.add_credential(
1827 credential_name or name, credential=credential, cloud=name
1828 )
David Garcia12b29242020-09-17 16:01:48 +02001829 # Need to return the object returned by the controller.add_cloud() function
1830 # I'm returning the original value now until this bug is fixed:
1831 # https://github.com/juju/python-libjuju/issues/443
1832 return cloud
1833 finally:
1834 await self.disconnect_controller(controller)
1835
1836 async def remove_cloud(self, name: str):
1837 """
1838 Remove cloud
1839
1840 :param: name: Name of the cloud to be removed
1841 """
1842 controller = await self.get_controller()
1843 try:
1844 await controller.remove_cloud(name)
David Garciaf980ac02021-07-27 15:07:42 +02001845 except juju.errors.JujuError as e:
1846 if len(e.errors) == 1 and f'cloud "{name}" not found' == e.errors[0]:
1847 self.log.warning(f"Cloud {name} not found, so it could not be deleted.")
1848 else:
1849 raise e
David Garcia12b29242020-09-17 16:01:48 +02001850 finally:
1851 await self.disconnect_controller(controller)
David Garcia59f520d2020-10-15 13:16:45 +02001852
Mark Beierl714d8872023-05-18 15:08:06 -04001853 @retry(
1854 attempts=20, delay=5, fallback=JujuLeaderUnitNotFound(), callback=retry_callback
1855 )
David Garcia59f520d2020-10-15 13:16:45 +02001856 async def _get_leader_unit(self, application: Application) -> Unit:
1857 unit = None
1858 for u in application.units:
1859 if await u.is_leader_from_status():
1860 unit = u
1861 break
David Garciaeb8943a2021-04-12 12:07:37 +02001862 if not unit:
1863 raise Exception()
David Garcia59f520d2020-10-15 13:16:45 +02001864 return unit
David Garciaf6e9b002020-11-27 15:32:02 +01001865
David Garciaeb8943a2021-04-12 12:07:37 +02001866 async def get_cloud_credentials(self, cloud: Cloud) -> typing.List:
1867 """
1868 Get cloud credentials
1869
1870 :param: cloud: Cloud object. The returned credentials will be from this cloud.
1871
1872 :return: List of credentials object associated to the specified cloud
1873
1874 """
David Garciaf6e9b002020-11-27 15:32:02 +01001875 controller = await self.get_controller()
1876 try:
1877 facade = client.CloudFacade.from_connection(controller.connection())
David Garciaeb8943a2021-04-12 12:07:37 +02001878 cloud_cred_tag = tag.credential(
1879 cloud.name, self.vca_connection.data.user, cloud.credential_name
1880 )
David Garciaf6e9b002020-11-27 15:32:02 +01001881 params = [client.Entity(cloud_cred_tag)]
1882 return (await facade.Credential(params)).results
1883 finally:
1884 await self.disconnect_controller(controller)
aktasfa02f8a2021-07-29 17:41:40 +03001885
1886 async def check_application_exists(self, model_name, application_name) -> bool:
1887 """Check application exists
1888
1889 :param: model_name: Model Name
1890 :param: application_name: Application Name
1891
1892 :return: Boolean
1893 """
1894
1895 model = None
1896 controller = await self.get_controller()
1897 try:
1898 model = await self.get_model(controller, model_name)
1899 self.log.debug(
1900 "Checking if application {} exists in model {}".format(
1901 application_name, model_name
1902 )
1903 )
1904 return self._get_application(model, application_name) is not None
1905 finally:
1906 if model:
1907 await self.disconnect_model(model)
1908 await self.disconnect_controller(controller)