blob: bca366577326e8132abb10dbbff88cabf01a1af4 [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)
125 raise JujuControllerFailedConnecting(e)
David Garcia4fee80e2020-05-13 12:18:38 +0200126
127 async def disconnect(self):
David Garcia2f66c4d2020-06-19 11:40:18 +0200128 """Disconnect"""
129 # Cancel health check task
130 self.health_check_task.cancel()
131 self.log.debug("Libjuju disconnected!")
David Garcia4fee80e2020-05-13 12:18:38 +0200132
133 async def disconnect_model(self, model: Model):
134 """
135 Disconnect model
136
137 :param: model: Model that will be disconnected
138 """
David Garcia2f66c4d2020-06-19 11:40:18 +0200139 await model.disconnect()
David Garcia4fee80e2020-05-13 12:18:38 +0200140
David Garcia2f66c4d2020-06-19 11:40:18 +0200141 async def disconnect_controller(self, controller: Controller):
David Garcia4fee80e2020-05-13 12:18:38 +0200142 """
David Garcia2f66c4d2020-06-19 11:40:18 +0200143 Disconnect controller
David Garcia4fee80e2020-05-13 12:18:38 +0200144
David Garcia2f66c4d2020-06-19 11:40:18 +0200145 :param: controller: Controller that will be disconnected
David Garcia4fee80e2020-05-13 12:18:38 +0200146 """
David Garcia667696e2020-09-22 14:52:32 +0200147 if controller:
148 await controller.disconnect()
David Garcia4fee80e2020-05-13 12:18:38 +0200149
David Garciaeb8943a2021-04-12 12:07:37 +0200150 @retry(attempts=3, delay=5, timeout=None)
151 async def add_model(self, model_name: str, cloud: VcaCloud):
David Garcia4fee80e2020-05-13 12:18:38 +0200152 """
153 Create model
154
155 :param: model_name: Model name
David Garciaeb8943a2021-04-12 12:07:37 +0200156 :param: cloud: Cloud object
David Garcia4fee80e2020-05-13 12:18:38 +0200157 """
158
David Garcia2f66c4d2020-06-19 11:40:18 +0200159 # Get controller
160 controller = await self.get_controller()
161 model = None
162 try:
David Garcia2f66c4d2020-06-19 11:40:18 +0200163 # Block until other workers have finished model creation
164 while self.creating_model.locked():
165 await asyncio.sleep(0.1)
David Garcia4fee80e2020-05-13 12:18:38 +0200166
David Garcia2f66c4d2020-06-19 11:40:18 +0200167 # Create the model
168 async with self.creating_model:
David Garciab0a8f402021-03-15 18:41:34 +0100169 if await self.model_exists(model_name, controller=controller):
170 return
David Garcia2f66c4d2020-06-19 11:40:18 +0200171 self.log.debug("Creating model {}".format(model_name))
172 model = await controller.add_model(
173 model_name,
David Garciaeb8943a2021-04-12 12:07:37 +0200174 config=self.vca_connection.data.model_config,
175 cloud_name=cloud.name,
176 credential_name=cloud.credential_name,
David Garcia2f66c4d2020-06-19 11:40:18 +0200177 )
David Garciaf980ac02021-07-27 15:07:42 +0200178 except juju.errors.JujuAPIError as e:
David Garciaeb8943a2021-04-12 12:07:37 +0200179 if "already exists" in e.message:
180 pass
181 else:
182 raise e
David Garcia2f66c4d2020-06-19 11:40:18 +0200183 finally:
184 if model:
185 await self.disconnect_model(model)
186 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +0200187
ksaikiranrcdf0b8e2021-03-17 12:50:00 +0530188 async def get_executed_actions(self, model_name: str) -> list:
189 """
190 Get executed/history of actions for a model.
191
192 :param: model_name: Model name, str.
193 :return: List of executed actions for a model.
194 """
195 model = None
196 executed_actions = []
197 controller = await self.get_controller()
198 try:
199 model = await self.get_model(controller, model_name)
200 # Get all unique action names
201 actions = {}
202 for application in model.applications:
203 application_actions = await self.get_actions(application, model_name)
204 actions.update(application_actions)
205 # Get status of all actions
206 for application_action in actions:
David Garciaeb8943a2021-04-12 12:07:37 +0200207 app_action_status_list = await model.get_action_status(
208 name=application_action
209 )
ksaikiranrcdf0b8e2021-03-17 12:50:00 +0530210 for action_id, action_status in app_action_status_list.items():
David Garciaeb8943a2021-04-12 12:07:37 +0200211 executed_action = {
212 "id": action_id,
213 "action": application_action,
214 "status": action_status,
215 }
ksaikiranrcdf0b8e2021-03-17 12:50:00 +0530216 # Get action output by id
217 action_status = await model.get_action_output(executed_action["id"])
218 for k, v in action_status.items():
219 executed_action[k] = v
220 executed_actions.append(executed_action)
221 except Exception as e:
David Garciaeb8943a2021-04-12 12:07:37 +0200222 raise JujuError(
223 "Error in getting executed actions for model: {}. Error: {}".format(
224 model_name, str(e)
225 )
226 )
ksaikiranrcdf0b8e2021-03-17 12:50:00 +0530227 finally:
228 if model:
229 await self.disconnect_model(model)
230 await self.disconnect_controller(controller)
231 return executed_actions
232
David Garciaeb8943a2021-04-12 12:07:37 +0200233 async def get_application_configs(
234 self, model_name: str, application_name: str
235 ) -> dict:
ksaikiranrcdf0b8e2021-03-17 12:50:00 +0530236 """
237 Get available configs for an application.
238
239 :param: model_name: Model name, str.
240 :param: application_name: Application name, str.
241
242 :return: A dict which has key - action name, value - action description
243 """
244 model = None
245 application_configs = {}
246 controller = await self.get_controller()
247 try:
248 model = await self.get_model(controller, model_name)
David Garciaeb8943a2021-04-12 12:07:37 +0200249 application = self._get_application(
250 model, application_name=application_name
251 )
ksaikiranrcdf0b8e2021-03-17 12:50:00 +0530252 application_configs = await application.get_config()
253 except Exception as e:
David Garciaeb8943a2021-04-12 12:07:37 +0200254 raise JujuError(
255 "Error in getting configs for application: {} in model: {}. Error: {}".format(
256 application_name, model_name, str(e)
257 )
258 )
ksaikiranrcdf0b8e2021-03-17 12:50:00 +0530259 finally:
260 if model:
261 await self.disconnect_model(model)
262 await self.disconnect_controller(controller)
263 return application_configs
264
David Garciaeb8943a2021-04-12 12:07:37 +0200265 @retry(attempts=3, delay=5)
266 async def get_model(self, controller: Controller, model_name: str) -> Model:
David Garcia4fee80e2020-05-13 12:18:38 +0200267 """
268 Get model from controller
269
David Garcia2f66c4d2020-06-19 11:40:18 +0200270 :param: controller: Controller
David Garcia4fee80e2020-05-13 12:18:38 +0200271 :param: model_name: Model name
272
273 :return: Model: The created Juju model object
274 """
David Garcia2f66c4d2020-06-19 11:40:18 +0200275 return await controller.get_model(model_name)
David Garcia4fee80e2020-05-13 12:18:38 +0200276
garciadeblas82b591c2021-03-24 09:22:13 +0100277 async def model_exists(
278 self, model_name: str, controller: Controller = None
279 ) -> bool:
David Garcia4fee80e2020-05-13 12:18:38 +0200280 """
281 Check if model exists
282
David Garcia2f66c4d2020-06-19 11:40:18 +0200283 :param: controller: Controller
David Garcia4fee80e2020-05-13 12:18:38 +0200284 :param: model_name: Model name
285
286 :return bool
287 """
David Garcia2f66c4d2020-06-19 11:40:18 +0200288 need_to_disconnect = False
David Garcia4fee80e2020-05-13 12:18:38 +0200289
David Garcia2f66c4d2020-06-19 11:40:18 +0200290 # Get controller if not passed
291 if not controller:
292 controller = await self.get_controller()
293 need_to_disconnect = True
David Garcia4fee80e2020-05-13 12:18:38 +0200294
David Garcia2f66c4d2020-06-19 11:40:18 +0200295 # Check if model exists
296 try:
297 return model_name in await controller.list_models()
298 finally:
299 if need_to_disconnect:
300 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +0200301
David Garcia42f328a2020-08-25 15:03:01 +0200302 async def models_exist(self, model_names: [str]) -> (bool, list):
303 """
304 Check if models exists
305
306 :param: model_names: List of strings with model names
307
308 :return (bool, list[str]): (True if all models exists, List of model names that don't exist)
309 """
310 if not model_names:
311 raise Exception(
David Garciac38a6962020-09-16 13:31:33 +0200312 "model_names must be a non-empty array. Given value: {}".format(
313 model_names
314 )
David Garcia42f328a2020-08-25 15:03:01 +0200315 )
316 non_existing_models = []
317 models = await self.list_models()
318 existing_models = list(set(models).intersection(model_names))
319 non_existing_models = list(set(model_names) - set(existing_models))
320
321 return (
322 len(non_existing_models) == 0,
323 non_existing_models,
324 )
325
David Garcia4fee80e2020-05-13 12:18:38 +0200326 async def get_model_status(self, model_name: str) -> FullStatus:
327 """
328 Get model status
329
330 :param: model_name: Model name
331
332 :return: Full status object
333 """
David Garcia2f66c4d2020-06-19 11:40:18 +0200334 controller = await self.get_controller()
335 model = await self.get_model(controller, model_name)
336 try:
337 return await model.get_status()
338 finally:
339 await self.disconnect_model(model)
340 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +0200341
342 async def create_machine(
343 self,
344 model_name: str,
345 machine_id: str = None,
346 db_dict: dict = None,
347 progress_timeout: float = None,
348 total_timeout: float = None,
David Garciaf643c132021-05-28 12:23:44 +0200349 series: str = "bionic",
David Garciaf8a9d462020-03-25 18:19:02 +0100350 wait: bool = True,
David Garcia4fee80e2020-05-13 12:18:38 +0200351 ) -> (Machine, bool):
352 """
353 Create machine
354
355 :param: model_name: Model name
356 :param: machine_id: Machine id
357 :param: db_dict: Dictionary with data of the DB to write the updates
358 :param: progress_timeout: Maximum time between two updates in the model
359 :param: total_timeout: Timeout for the entity to be active
David Garciaf8a9d462020-03-25 18:19:02 +0100360 :param: series: Series of the machine (xenial, bionic, focal, ...)
361 :param: wait: Wait until machine is ready
David Garcia4fee80e2020-05-13 12:18:38 +0200362
363 :return: (juju.machine.Machine, bool): Machine object and a boolean saying
364 if the machine is new or it already existed
365 """
366 new = False
367 machine = None
368
369 self.log.debug(
370 "Creating machine (id={}) in model: {}".format(machine_id, model_name)
371 )
372
David Garcia2f66c4d2020-06-19 11:40:18 +0200373 # Get controller
374 controller = await self.get_controller()
375
David Garcia4fee80e2020-05-13 12:18:38 +0200376 # Get model
David Garcia2f66c4d2020-06-19 11:40:18 +0200377 model = await self.get_model(controller, model_name)
David Garcia4fee80e2020-05-13 12:18:38 +0200378 try:
379 if machine_id is not None:
380 self.log.debug(
381 "Searching machine (id={}) in model {}".format(
382 machine_id, model_name
383 )
384 )
385
386 # Get machines from model and get the machine with machine_id if exists
387 machines = await model.get_machines()
388 if machine_id in machines:
389 self.log.debug(
390 "Machine (id={}) found in model {}".format(
391 machine_id, model_name
392 )
393 )
Dominik Fleischmann7ff392f2020-07-07 13:11:19 +0200394 machine = machines[machine_id]
David Garcia4fee80e2020-05-13 12:18:38 +0200395 else:
396 raise JujuMachineNotFound("Machine {} not found".format(machine_id))
397
398 if machine is None:
399 self.log.debug("Creating a new machine in model {}".format(model_name))
400
401 # Create machine
402 machine = await model.add_machine(
403 spec=None, constraints=None, disks=None, series=series
404 )
405 new = True
406
407 # Wait until the machine is ready
David Garcia2f66c4d2020-06-19 11:40:18 +0200408 self.log.debug(
409 "Wait until machine {} is ready in model {}".format(
410 machine.entity_id, model_name
411 )
412 )
David Garciaf8a9d462020-03-25 18:19:02 +0100413 if wait:
414 await JujuModelWatcher.wait_for(
415 model=model,
416 entity=machine,
417 progress_timeout=progress_timeout,
418 total_timeout=total_timeout,
419 db_dict=db_dict,
420 n2vc=self.n2vc,
David Garciaeb8943a2021-04-12 12:07:37 +0200421 vca_id=self.vca_connection._vca_id,
David Garciaf8a9d462020-03-25 18:19:02 +0100422 )
David Garcia4fee80e2020-05-13 12:18:38 +0200423 finally:
424 await self.disconnect_model(model)
David Garcia2f66c4d2020-06-19 11:40:18 +0200425 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +0200426
David Garcia2f66c4d2020-06-19 11:40:18 +0200427 self.log.debug(
428 "Machine {} ready at {} in model {}".format(
429 machine.entity_id, machine.dns_name, model_name
430 )
431 )
David Garcia4fee80e2020-05-13 12:18:38 +0200432 return machine, new
433
434 async def provision_machine(
435 self,
436 model_name: str,
437 hostname: str,
438 username: str,
439 private_key_path: str,
440 db_dict: dict = None,
441 progress_timeout: float = None,
442 total_timeout: float = None,
443 ) -> str:
444 """
445 Manually provisioning of a machine
446
447 :param: model_name: Model name
448 :param: hostname: IP to access the machine
449 :param: username: Username to login to the machine
450 :param: private_key_path: Local path for the private key
451 :param: db_dict: Dictionary with data of the DB to write the updates
452 :param: progress_timeout: Maximum time between two updates in the model
453 :param: total_timeout: Timeout for the entity to be active
454
455 :return: (Entity): Machine id
456 """
457 self.log.debug(
458 "Provisioning machine. model: {}, hostname: {}, username: {}".format(
459 model_name, hostname, username
460 )
461 )
462
David Garcia2f66c4d2020-06-19 11:40:18 +0200463 # Get controller
464 controller = await self.get_controller()
465
David Garcia4fee80e2020-05-13 12:18:38 +0200466 # Get model
David Garcia2f66c4d2020-06-19 11:40:18 +0200467 model = await self.get_model(controller, model_name)
David Garcia4fee80e2020-05-13 12:18:38 +0200468
469 try:
470 # Get provisioner
471 provisioner = AsyncSSHProvisioner(
472 host=hostname,
473 user=username,
474 private_key_path=private_key_path,
475 log=self.log,
476 )
477
478 # Provision machine
479 params = await provisioner.provision_machine()
480
481 params.jobs = ["JobHostUnits"]
482
483 self.log.debug("Adding machine to model")
484 connection = model.connection()
485 client_facade = client.ClientFacade.from_connection(connection)
486
487 results = await client_facade.AddMachines(params=[params])
488 error = results.machines[0].error
489
490 if error:
491 msg = "Error adding machine: {}".format(error.message)
492 self.log.error(msg=msg)
493 raise ValueError(msg)
494
495 machine_id = results.machines[0].machine
496
497 self.log.debug("Installing Juju agent into machine {}".format(machine_id))
498 asyncio.ensure_future(
499 provisioner.install_agent(
500 connection=connection,
501 nonce=params.nonce,
502 machine_id=machine_id,
David Garciaeb8943a2021-04-12 12:07:37 +0200503 proxy=self.vca_connection.data.api_proxy,
endikaf97b2312020-09-16 15:41:18 +0200504 series=params.series,
David Garcia4fee80e2020-05-13 12:18:38 +0200505 )
506 )
507
508 machine = None
509 for _ in range(10):
510 machine_list = await model.get_machines()
511 if machine_id in machine_list:
512 self.log.debug("Machine {} found in model!".format(machine_id))
513 machine = model.machines.get(machine_id)
514 break
515 await asyncio.sleep(2)
516
517 if machine is None:
518 msg = "Machine {} not found in model".format(machine_id)
519 self.log.error(msg=msg)
520 raise JujuMachineNotFound(msg)
521
David Garcia2f66c4d2020-06-19 11:40:18 +0200522 self.log.debug(
523 "Wait until machine {} is ready in model {}".format(
524 machine.entity_id, model_name
525 )
526 )
David Garcia4fee80e2020-05-13 12:18:38 +0200527 await JujuModelWatcher.wait_for(
528 model=model,
529 entity=machine,
530 progress_timeout=progress_timeout,
531 total_timeout=total_timeout,
532 db_dict=db_dict,
533 n2vc=self.n2vc,
David Garciaeb8943a2021-04-12 12:07:37 +0200534 vca_id=self.vca_connection._vca_id,
David Garcia4fee80e2020-05-13 12:18:38 +0200535 )
536 except Exception as e:
537 raise e
538 finally:
539 await self.disconnect_model(model)
David Garcia2f66c4d2020-06-19 11:40:18 +0200540 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +0200541
David Garcia2f66c4d2020-06-19 11:40:18 +0200542 self.log.debug(
543 "Machine provisioned {} in model {}".format(machine_id, model_name)
544 )
David Garcia4fee80e2020-05-13 12:18:38 +0200545
546 return machine_id
547
David Garcia667696e2020-09-22 14:52:32 +0200548 async def deploy(
549 self, uri: str, model_name: str, wait: bool = True, timeout: float = 3600
550 ):
551 """
552 Deploy bundle or charm: Similar to the juju CLI command `juju deploy`
553
554 :param: uri: Path or Charm Store uri in which the charm or bundle can be found
555 :param: model_name: Model name
556 :param: wait: Indicates whether to wait or not until all applications are active
557 :param: timeout: Time in seconds to wait until all applications are active
558 """
559 controller = await self.get_controller()
560 model = await self.get_model(controller, model_name)
561 try:
David Garcia76ed7572021-09-07 11:15:48 +0200562 await model.deploy(uri, trust=True)
David Garcia667696e2020-09-22 14:52:32 +0200563 if wait:
564 await JujuModelWatcher.wait_for_model(model, timeout=timeout)
565 self.log.debug("All units active in model {}".format(model_name))
566 finally:
567 await self.disconnect_model(model)
568 await self.disconnect_controller(controller)
569
aktasfa02f8a2021-07-29 17:41:40 +0300570 async def add_unit(
571 self,
572 application_name: str,
573 model_name: str,
574 machine_id: str,
575 db_dict: dict = None,
576 progress_timeout: float = None,
577 total_timeout: float = None,
578 ):
579 """Add unit
580
581 :param: application_name: Application name
582 :param: model_name: Model name
583 :param: machine_id Machine id
584 :param: db_dict: Dictionary with data of the DB to write the updates
585 :param: progress_timeout: Maximum time between two updates in the model
586 :param: total_timeout: Timeout for the entity to be active
587
588 :return: None
589 """
590
591 model = None
592 controller = await self.get_controller()
593 try:
594 model = await self.get_model(controller, model_name)
595 application = self._get_application(model, application_name)
596
597 if application is not None:
598
599 # Checks if the given machine id in the model,
600 # otherwise function raises an error
601 _machine, _series = self._get_machine_info(model, machine_id)
602
603 self.log.debug(
604 "Adding unit (machine {}) to application {} in model ~{}".format(
605 machine_id, application_name, model_name
606 )
607 )
608
609 await application.add_unit(to=machine_id)
610
611 await JujuModelWatcher.wait_for(
612 model=model,
613 entity=application,
614 progress_timeout=progress_timeout,
615 total_timeout=total_timeout,
616 db_dict=db_dict,
617 n2vc=self.n2vc,
618 vca_id=self.vca_connection._vca_id,
619 )
620 self.log.debug(
621 "Unit is added to application {} in model {}".format(
622 application_name, model_name
623 )
624 )
625 else:
626 raise JujuApplicationNotFound(
627 "Application {} not exists".format(application_name)
628 )
629 finally:
630 if model:
631 await self.disconnect_model(model)
632 await self.disconnect_controller(controller)
633
634 async def destroy_unit(
635 self,
636 application_name: str,
637 model_name: str,
638 machine_id: str,
639 total_timeout: float = None,
640 ):
641 """Destroy unit
642
643 :param: application_name: Application name
644 :param: model_name: Model name
645 :param: machine_id Machine id
aktasfa02f8a2021-07-29 17:41:40 +0300646 :param: total_timeout: Timeout for the entity to be active
647
648 :return: None
649 """
650
651 model = None
652 controller = await self.get_controller()
653 try:
654 model = await self.get_model(controller, model_name)
655 application = self._get_application(model, application_name)
656
657 if application is None:
658 raise JujuApplicationNotFound(
659 "Application not found: {} (model={})".format(
660 application_name, model_name
661 )
662 )
663
664 unit = self._get_unit(application, machine_id)
665 if not unit:
666 raise JujuError(
667 "A unit with machine id {} not in available units".format(
668 machine_id
669 )
670 )
671
672 unit_name = unit.name
673
674 self.log.debug(
675 "Destroying unit {} from application {} in model {}".format(
676 unit_name, application_name, model_name
677 )
678 )
679 await application.destroy_unit(unit_name)
680
681 self.log.debug(
682 "Waiting for unit {} to be destroyed in application {} (model={})...".format(
683 unit_name, application_name, model_name
684 )
685 )
686
687 # TODO: Add functionality in the Juju watcher to replace this kind of blocks
688 if total_timeout is None:
689 total_timeout = 3600
690 end = time.time() + total_timeout
691 while time.time() < end:
692 if not self._get_unit(application, machine_id):
693 self.log.debug(
694 "The unit {} was destroyed in application {} (model={}) ".format(
695 unit_name, application_name, model_name
696 )
697 )
698 return
699 await asyncio.sleep(5)
700 self.log.debug(
701 "Unit {} is destroyed from application {} in model {}".format(
702 unit_name, application_name, model_name
703 )
704 )
705 finally:
706 if model:
707 await self.disconnect_model(model)
708 await self.disconnect_controller(controller)
709
David Garcia4fee80e2020-05-13 12:18:38 +0200710 async def deploy_charm(
711 self,
712 application_name: str,
713 path: str,
714 model_name: str,
715 machine_id: str,
716 db_dict: dict = None,
717 progress_timeout: float = None,
718 total_timeout: float = None,
719 config: dict = None,
720 series: str = None,
David Garciaf8a9d462020-03-25 18:19:02 +0100721 num_units: int = 1,
David Garcia4fee80e2020-05-13 12:18:38 +0200722 ):
723 """Deploy charm
724
725 :param: application_name: Application name
726 :param: path: Local path to the charm
727 :param: model_name: Model name
728 :param: machine_id ID of the machine
729 :param: db_dict: Dictionary with data of the DB to write the updates
730 :param: progress_timeout: Maximum time between two updates in the model
731 :param: total_timeout: Timeout for the entity to be active
732 :param: config: Config for the charm
733 :param: series: Series of the charm
David Garciaf8a9d462020-03-25 18:19:02 +0100734 :param: num_units: Number of units
David Garcia4fee80e2020-05-13 12:18:38 +0200735
736 :return: (juju.application.Application): Juju application
737 """
David Garcia2f66c4d2020-06-19 11:40:18 +0200738 self.log.debug(
739 "Deploying charm {} to machine {} in model ~{}".format(
740 application_name, machine_id, model_name
741 )
742 )
743 self.log.debug("charm: {}".format(path))
744
745 # Get controller
746 controller = await self.get_controller()
David Garcia4fee80e2020-05-13 12:18:38 +0200747
748 # Get model
David Garcia2f66c4d2020-06-19 11:40:18 +0200749 model = await self.get_model(controller, model_name)
David Garcia4fee80e2020-05-13 12:18:38 +0200750
751 try:
David Garcia4fee80e2020-05-13 12:18:38 +0200752 if application_name not in model.applications:
David Garcia2f66c4d2020-06-19 11:40:18 +0200753
David Garcia4fee80e2020-05-13 12:18:38 +0200754 if machine_id is not None:
aktasfa02f8a2021-07-29 17:41:40 +0300755 machine, series = self._get_machine_info(model, machine_id)
David Garcia4fee80e2020-05-13 12:18:38 +0200756
757 application = await model.deploy(
758 entity_url=path,
759 application_name=application_name,
760 channel="stable",
761 num_units=1,
762 series=series,
763 to=machine_id,
764 config=config,
765 )
766
David Garcia2f66c4d2020-06-19 11:40:18 +0200767 self.log.debug(
768 "Wait until application {} is ready in model {}".format(
769 application_name, model_name
770 )
771 )
David Garciaf8a9d462020-03-25 18:19:02 +0100772 if num_units > 1:
773 for _ in range(num_units - 1):
774 m, _ = await self.create_machine(model_name, wait=False)
775 await application.add_unit(to=m.entity_id)
776
David Garcia4fee80e2020-05-13 12:18:38 +0200777 await JujuModelWatcher.wait_for(
778 model=model,
779 entity=application,
780 progress_timeout=progress_timeout,
781 total_timeout=total_timeout,
782 db_dict=db_dict,
783 n2vc=self.n2vc,
David Garciaeb8943a2021-04-12 12:07:37 +0200784 vca_id=self.vca_connection._vca_id,
David Garcia4fee80e2020-05-13 12:18:38 +0200785 )
David Garcia2f66c4d2020-06-19 11:40:18 +0200786 self.log.debug(
787 "Application {} is ready in model {}".format(
788 application_name, model_name
789 )
790 )
David Garcia4fee80e2020-05-13 12:18:38 +0200791 else:
David Garcia2f66c4d2020-06-19 11:40:18 +0200792 raise JujuApplicationExists(
793 "Application {} exists".format(application_name)
794 )
aktas42e51cf2021-10-19 20:03:23 +0300795 except juju.errors.JujuError as e:
796 if "already exists" in e.message:
797 raise JujuApplicationExists(
798 "Application {} exists".format(application_name)
799 )
800 else:
801 raise e
David Garcia4fee80e2020-05-13 12:18:38 +0200802 finally:
803 await self.disconnect_model(model)
David Garcia2f66c4d2020-06-19 11:40:18 +0200804 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +0200805
806 return application
807
beierlmdb1d37b2022-04-14 16:33:51 -0400808 async def upgrade_charm(
809 self,
810 application_name: str,
811 path: str,
812 model_name: str,
813 total_timeout: float = None,
814 **kwargs,
815 ):
816 """Upgrade Charm
817
818 :param: application_name: Application name
819 :param: model_name: Model name
820 :param: path: Local path to the charm
821 :param: total_timeout: Timeout for the entity to be active
822
823 :return: (str, str): (output and status)
824 """
825
826 self.log.debug(
827 "Upgrading charm {} in model {} from path {}".format(
828 application_name, model_name, path
829 )
830 )
831
832 await self.resolve_application(
833 model_name=model_name, application_name=application_name
834 )
835
836 # Get controller
837 controller = await self.get_controller()
838
839 # Get model
840 model = await self.get_model(controller, model_name)
841
842 try:
843 # Get application
844 application = self._get_application(
845 model,
846 application_name=application_name,
847 )
848 if application is None:
849 raise JujuApplicationNotFound(
850 "Cannot find application {} to upgrade".format(application_name)
851 )
852
853 await application.refresh(path=path)
854
855 self.log.debug(
856 "Wait until charm upgrade is completed for application {} (model={})".format(
857 application_name, model_name
858 )
859 )
860
861 await JujuModelWatcher.ensure_units_idle(
862 model=model, application=application
863 )
864
865 if application.status == "error":
866 error_message = "Unknown"
867 for unit in application.units:
868 if (
869 unit.workload_status == "error"
870 and unit.workload_status_message != ""
871 ):
872 error_message = unit.workload_status_message
873
874 message = "Application {} failed update in {}: {}".format(
875 application_name, model_name, error_message
876 )
877 self.log.error(message)
878 raise JujuError(message=message)
879
880 self.log.debug(
881 "Application {} is ready in model {}".format(
882 application_name, model_name
883 )
884 )
885
886 finally:
887 await self.disconnect_model(model)
888 await self.disconnect_controller(controller)
889
890 return application
891
892 async def resolve_application(self, model_name: str, application_name: str):
893
894 controller = await self.get_controller()
895 model = await self.get_model(controller, model_name)
896
897 try:
898 application = self._get_application(
899 model,
900 application_name=application_name,
901 )
902 if application is None:
903 raise JujuApplicationNotFound(
904 "Cannot find application {} to resolve".format(application_name)
905 )
906
907 while application.status == "error":
908 for unit in application.units:
909 if unit.workload_status == "error":
910 self.log.debug(
911 "Model {}, Application {}, Unit {} in error state, resolving".format(
912 model_name, application_name, unit.entity_id
913 )
914 )
915 try:
916 await unit.resolved(retry=False)
917 except Exception:
918 pass
919
920 await asyncio.sleep(1)
921
922 finally:
923 await self.disconnect_model(model)
924 await self.disconnect_controller(controller)
925
aktas2962f3e2021-03-15 11:05:35 +0300926 async def scale_application(
garciadeblas82b591c2021-03-24 09:22:13 +0100927 self,
928 model_name: str,
929 application_name: str,
930 scale: int = 1,
931 total_timeout: float = None,
aktas2962f3e2021-03-15 11:05:35 +0300932 ):
933 """
934 Scale application (K8s)
935
936 :param: model_name: Model name
937 :param: application_name: Application name
938 :param: scale: Scale to which to set this application
939 :param: total_timeout: Timeout for the entity to be active
940 """
941
942 model = None
943 controller = await self.get_controller()
944 try:
945 model = await self.get_model(controller, model_name)
946
947 self.log.debug(
948 "Scaling application {} in model {}".format(
949 application_name, model_name
950 )
951 )
952 application = self._get_application(model, application_name)
953 if application is None:
954 raise JujuApplicationNotFound("Cannot scale application")
955 await application.scale(scale=scale)
956 # Wait until application is scaled in model
957 self.log.debug(
garciadeblas82b591c2021-03-24 09:22:13 +0100958 "Waiting for application {} to be scaled in model {}...".format(
aktas2962f3e2021-03-15 11:05:35 +0300959 application_name, model_name
960 )
961 )
962 if total_timeout is None:
963 total_timeout = 1800
964 end = time.time() + total_timeout
965 while time.time() < end:
966 application_scale = self._get_application_count(model, application_name)
967 # Before calling wait_for_model function,
968 # wait until application unit count and scale count are equal.
969 # Because there is a delay before scaling triggers in Juju model.
970 if application_scale == scale:
garciadeblas82b591c2021-03-24 09:22:13 +0100971 await JujuModelWatcher.wait_for_model(
972 model=model, timeout=total_timeout
973 )
aktas2962f3e2021-03-15 11:05:35 +0300974 self.log.debug(
975 "Application {} is scaled in model {}".format(
976 application_name, model_name
977 )
978 )
979 return
980 await asyncio.sleep(5)
981 raise Exception(
982 "Timeout waiting for application {} in model {} to be scaled".format(
983 application_name, model_name
984 )
985 )
986 finally:
987 if model:
988 await self.disconnect_model(model)
989 await self.disconnect_controller(controller)
990
991 def _get_application_count(self, model: Model, application_name: str) -> int:
992 """Get number of units of the application
993
994 :param: model: Model object
995 :param: application_name: Application name
996
997 :return: int (or None if application doesn't exist)
998 """
999 application = self._get_application(model, application_name)
1000 if application is not None:
1001 return len(application.units)
1002
David Garcia2f66c4d2020-06-19 11:40:18 +02001003 def _get_application(self, model: Model, application_name: str) -> Application:
David Garcia4fee80e2020-05-13 12:18:38 +02001004 """Get application
1005
1006 :param: model: Model object
1007 :param: application_name: Application name
1008
1009 :return: juju.application.Application (or None if it doesn't exist)
1010 """
1011 if model.applications and application_name in model.applications:
1012 return model.applications[application_name]
1013
aktasfa02f8a2021-07-29 17:41:40 +03001014 def _get_unit(self, application: Application, machine_id: str) -> Unit:
1015 """Get unit
1016
1017 :param: application: Application object
1018 :param: machine_id: Machine id
1019
1020 :return: Unit
1021 """
1022 unit = None
1023 for u in application.units:
1024 if u.machine_id == machine_id:
1025 unit = u
1026 break
1027 return unit
1028
1029 def _get_machine_info(
1030 self,
1031 model,
1032 machine_id: str,
1033 ) -> (str, str):
1034 """Get machine info
1035
1036 :param: model: Model object
1037 :param: machine_id: Machine id
1038
1039 :return: (str, str): (machine, series)
1040 """
1041 if machine_id not in model.machines:
1042 msg = "Machine {} not found in model".format(machine_id)
1043 self.log.error(msg=msg)
1044 raise JujuMachineNotFound(msg)
1045 machine = model.machines[machine_id]
1046 return machine, machine.series
1047
David Garcia4fee80e2020-05-13 12:18:38 +02001048 async def execute_action(
1049 self,
1050 application_name: str,
1051 model_name: str,
1052 action_name: str,
1053 db_dict: dict = None,
aktasfa02f8a2021-07-29 17:41:40 +03001054 machine_id: str = None,
David Garcia4fee80e2020-05-13 12:18:38 +02001055 progress_timeout: float = None,
1056 total_timeout: float = None,
David Garciaf980ac02021-07-27 15:07:42 +02001057 **kwargs,
David Garcia4fee80e2020-05-13 12:18:38 +02001058 ):
1059 """Execute action
1060
1061 :param: application_name: Application name
1062 :param: model_name: Model name
David Garcia4fee80e2020-05-13 12:18:38 +02001063 :param: action_name: Name of the action
1064 :param: db_dict: Dictionary with data of the DB to write the updates
aktasfa02f8a2021-07-29 17:41:40 +03001065 :param: machine_id Machine id
David Garcia4fee80e2020-05-13 12:18:38 +02001066 :param: progress_timeout: Maximum time between two updates in the model
1067 :param: total_timeout: Timeout for the entity to be active
1068
1069 :return: (str, str): (output and status)
1070 """
David Garcia2f66c4d2020-06-19 11:40:18 +02001071 self.log.debug(
1072 "Executing action {} using params {}".format(action_name, kwargs)
1073 )
1074 # Get controller
1075 controller = await self.get_controller()
1076
1077 # Get model
1078 model = await self.get_model(controller, model_name)
David Garcia4fee80e2020-05-13 12:18:38 +02001079
1080 try:
1081 # Get application
David Garcia2f66c4d2020-06-19 11:40:18 +02001082 application = self._get_application(
David Garciaf6e9b002020-11-27 15:32:02 +01001083 model,
1084 application_name=application_name,
David Garcia4fee80e2020-05-13 12:18:38 +02001085 )
1086 if application is None:
1087 raise JujuApplicationNotFound("Cannot execute action")
David Garcia59f520d2020-10-15 13:16:45 +02001088 # Racing condition:
1089 # Ocassionally, self._get_leader_unit() will return None
1090 # because the leader elected hook has not been triggered yet.
1091 # Therefore, we are doing some retries. If it happens again,
1092 # re-open bug 1236
aktasfa02f8a2021-07-29 17:41:40 +03001093 if machine_id is None:
1094 unit = await self._get_leader_unit(application)
1095 self.log.debug(
1096 "Action {} is being executed on the leader unit {}".format(
1097 action_name, unit.name
1098 )
1099 )
1100 else:
1101 unit = self._get_unit(application, machine_id)
1102 if not unit:
1103 raise JujuError(
1104 "A unit with machine id {} not in available units".format(
1105 machine_id
1106 )
1107 )
1108 self.log.debug(
1109 "Action {} is being executed on {} unit".format(
1110 action_name, unit.name
1111 )
1112 )
David Garcia4fee80e2020-05-13 12:18:38 +02001113
1114 actions = await application.get_actions()
1115
1116 if action_name not in actions:
Dominik Fleischmann7ff392f2020-07-07 13:11:19 +02001117 raise JujuActionNotFound(
David Garcia4fee80e2020-05-13 12:18:38 +02001118 "Action {} not in available actions".format(action_name)
1119 )
1120
David Garcia4fee80e2020-05-13 12:18:38 +02001121 action = await unit.run_action(action_name, **kwargs)
1122
David Garcia2f66c4d2020-06-19 11:40:18 +02001123 self.log.debug(
1124 "Wait until action {} is completed in application {} (model={})".format(
1125 action_name, application_name, model_name
1126 )
1127 )
David Garcia4fee80e2020-05-13 12:18:38 +02001128 await JujuModelWatcher.wait_for(
1129 model=model,
1130 entity=action,
1131 progress_timeout=progress_timeout,
1132 total_timeout=total_timeout,
1133 db_dict=db_dict,
1134 n2vc=self.n2vc,
David Garciaeb8943a2021-04-12 12:07:37 +02001135 vca_id=self.vca_connection._vca_id,
David Garcia4fee80e2020-05-13 12:18:38 +02001136 )
David Garcia2f66c4d2020-06-19 11:40:18 +02001137
David Garcia4fee80e2020-05-13 12:18:38 +02001138 output = await model.get_action_output(action_uuid=action.entity_id)
1139 status = await model.get_action_status(uuid_or_prefix=action.entity_id)
1140 status = (
1141 status[action.entity_id] if action.entity_id in status else "failed"
1142 )
1143
David Garcia2f66c4d2020-06-19 11:40:18 +02001144 self.log.debug(
1145 "Action {} completed with status {} in application {} (model={})".format(
1146 action_name, action.status, application_name, model_name
1147 )
1148 )
David Garcia4fee80e2020-05-13 12:18:38 +02001149 finally:
1150 await self.disconnect_model(model)
David Garcia2f66c4d2020-06-19 11:40:18 +02001151 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +02001152
1153 return output, status
1154
1155 async def get_actions(self, application_name: str, model_name: str) -> dict:
1156 """Get list of actions
1157
1158 :param: application_name: Application name
1159 :param: model_name: Model name
1160
1161 :return: Dict with this format
1162 {
1163 "action_name": "Description of the action",
1164 ...
1165 }
1166 """
David Garcia2f66c4d2020-06-19 11:40:18 +02001167 self.log.debug(
1168 "Getting list of actions for application {}".format(application_name)
David Garcia4fee80e2020-05-13 12:18:38 +02001169 )
1170
David Garcia2f66c4d2020-06-19 11:40:18 +02001171 # Get controller
1172 controller = await self.get_controller()
David Garcia4fee80e2020-05-13 12:18:38 +02001173
David Garcia2f66c4d2020-06-19 11:40:18 +02001174 # Get model
1175 model = await self.get_model(controller, model_name)
David Garcia4fee80e2020-05-13 12:18:38 +02001176
David Garcia2f66c4d2020-06-19 11:40:18 +02001177 try:
1178 # Get application
1179 application = self._get_application(
David Garciaf6e9b002020-11-27 15:32:02 +01001180 model,
1181 application_name=application_name,
David Garcia2f66c4d2020-06-19 11:40:18 +02001182 )
1183
1184 # Return list of actions
1185 return await application.get_actions()
1186
1187 finally:
1188 # Disconnect from model and controller
1189 await self.disconnect_model(model)
1190 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +02001191
David Garcia85755d12020-09-21 19:51:23 +02001192 async def get_metrics(self, model_name: str, application_name: str) -> dict:
1193 """Get the metrics collected by the VCA.
1194
1195 :param model_name The name or unique id of the network service
1196 :param application_name The name of the application
1197 """
1198 if not model_name or not application_name:
1199 raise Exception("model_name and application_name must be non-empty strings")
1200 metrics = {}
1201 controller = await self.get_controller()
1202 model = await self.get_model(controller, model_name)
1203 try:
1204 application = self._get_application(model, application_name)
1205 if application is not None:
1206 metrics = await application.get_metrics()
1207 finally:
1208 self.disconnect_model(model)
1209 self.disconnect_controller(controller)
1210 return metrics
1211
David Garcia4fee80e2020-05-13 12:18:38 +02001212 async def add_relation(
David Garciaf6e9b002020-11-27 15:32:02 +01001213 self,
1214 model_name: str,
1215 endpoint_1: str,
1216 endpoint_2: str,
David Garcia4fee80e2020-05-13 12:18:38 +02001217 ):
1218 """Add relation
1219
David Garcia8331f7c2020-08-25 16:10:07 +02001220 :param: model_name: Model name
1221 :param: endpoint_1 First endpoint name
1222 ("app:endpoint" format or directly the saas name)
1223 :param: endpoint_2: Second endpoint name (^ same format)
David Garcia4fee80e2020-05-13 12:18:38 +02001224 """
1225
David Garcia8331f7c2020-08-25 16:10:07 +02001226 self.log.debug("Adding relation: {} -> {}".format(endpoint_1, endpoint_2))
David Garcia2f66c4d2020-06-19 11:40:18 +02001227
1228 # Get controller
1229 controller = await self.get_controller()
1230
David Garcia4fee80e2020-05-13 12:18:38 +02001231 # Get model
David Garcia2f66c4d2020-06-19 11:40:18 +02001232 model = await self.get_model(controller, model_name)
David Garcia4fee80e2020-05-13 12:18:38 +02001233
David Garcia4fee80e2020-05-13 12:18:38 +02001234 # Add relation
David Garcia4fee80e2020-05-13 12:18:38 +02001235 try:
David Garcia8331f7c2020-08-25 16:10:07 +02001236 await model.add_relation(endpoint_1, endpoint_2)
David Garciaf980ac02021-07-27 15:07:42 +02001237 except juju.errors.JujuAPIError as e:
David Garcia4fee80e2020-05-13 12:18:38 +02001238 if "not found" in e.message:
1239 self.log.warning("Relation not found: {}".format(e.message))
1240 return
1241 if "already exists" in e.message:
1242 self.log.warning("Relation already exists: {}".format(e.message))
1243 return
1244 # another exception, raise it
1245 raise e
1246 finally:
1247 await self.disconnect_model(model)
David Garcia2f66c4d2020-06-19 11:40:18 +02001248 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +02001249
David Garcia582b9232021-10-26 12:30:44 +02001250 async def offer(self, endpoint: RelationEndpoint) -> Offer:
1251 """
1252 Create an offer from a RelationEndpoint
1253
1254 :param: endpoint: Relation endpoint
1255
1256 :return: Offer object
1257 """
1258 model_name = endpoint.model_name
1259 offer_name = f"{endpoint.application_name}-{endpoint.endpoint_name}"
1260 controller = await self.get_controller()
1261 model = None
1262 try:
1263 model = await self.get_model(controller, model_name)
1264 await model.create_offer(endpoint.endpoint, offer_name=offer_name)
1265 offer_list = await self._list_offers(model_name, offer_name=offer_name)
1266 if offer_list:
1267 return Offer(offer_list[0].offer_url)
1268 else:
1269 raise Exception("offer was not created")
1270 except juju.errors.JujuError as e:
1271 if "application offer already exists" not in e.message:
1272 raise e
1273 finally:
1274 if model:
1275 self.disconnect_model(model)
1276 self.disconnect_controller(controller)
1277
David Garcia68b00722020-09-11 15:05:00 +02001278 async def consume(
David Garciaf6e9b002020-11-27 15:32:02 +01001279 self,
David Garciaf6e9b002020-11-27 15:32:02 +01001280 model_name: str,
David Garcia582b9232021-10-26 12:30:44 +02001281 offer: Offer,
1282 provider_libjuju: "Libjuju",
1283 ) -> str:
David Garcia68b00722020-09-11 15:05:00 +02001284 """
David Garcia582b9232021-10-26 12:30:44 +02001285 Consumes a remote offer in the model. Relations can be created later using "juju relate".
David Garcia68b00722020-09-11 15:05:00 +02001286
David Garcia582b9232021-10-26 12:30:44 +02001287 :param: model_name: Model name
1288 :param: offer: Offer object to consume
1289 :param: provider_libjuju: Libjuju object of the provider endpoint
David Garcia68b00722020-09-11 15:05:00 +02001290
1291 :raises ParseError if there's a problem parsing the offer_url
1292 :raises JujuError if remote offer includes and endpoint
1293 :raises JujuAPIError if the operation is not successful
David Garcia68b00722020-09-11 15:05:00 +02001294
David Garcia582b9232021-10-26 12:30:44 +02001295 :returns: Saas name. It is the application name in the model that reference the remote application.
1296 """
1297 saas_name = f'{offer.name}-{offer.model_name.replace("-", "")}'
1298 if offer.vca_id:
1299 saas_name = f"{saas_name}-{offer.vca_id}"
1300 controller = await self.get_controller()
1301 model = None
1302 provider_controller = None
David Garcia68b00722020-09-11 15:05:00 +02001303 try:
David Garcia582b9232021-10-26 12:30:44 +02001304 model = await controller.get_model(model_name)
1305 provider_controller = await provider_libjuju.get_controller()
1306 await model.consume(
1307 offer.url, application_alias=saas_name, controller=provider_controller
1308 )
1309 return saas_name
David Garcia68b00722020-09-11 15:05:00 +02001310 finally:
David Garcia582b9232021-10-26 12:30:44 +02001311 if model:
1312 await self.disconnect_model(model)
1313 if provider_controller:
1314 await provider_libjuju.disconnect_controller(provider_controller)
David Garcia68b00722020-09-11 15:05:00 +02001315 await self.disconnect_controller(controller)
1316
David Garciae610aed2021-07-26 15:04:37 +02001317 async def destroy_model(self, model_name: str, total_timeout: float = 1800):
David Garcia4fee80e2020-05-13 12:18:38 +02001318 """
1319 Destroy model
1320
1321 :param: model_name: Model name
1322 :param: total_timeout: Timeout
1323 """
David Garcia4fee80e2020-05-13 12:18:38 +02001324
David Garcia2f66c4d2020-06-19 11:40:18 +02001325 controller = await self.get_controller()
David Garcia435b8642021-03-10 17:09:44 +01001326 model = None
David Garcia2f66c4d2020-06-19 11:40:18 +02001327 try:
David Garciab0a8f402021-03-15 18:41:34 +01001328 if not await self.model_exists(model_name, controller=controller):
1329 return
1330
David Garcia2f66c4d2020-06-19 11:40:18 +02001331 self.log.debug("Destroying model {}".format(model_name))
David Garcia2f66c4d2020-06-19 11:40:18 +02001332
David Garciae610aed2021-07-26 15:04:37 +02001333 model = await self.get_model(controller, model_name)
David Garcia168bb192020-10-21 14:19:45 +02001334 # Destroy machines that are manually provisioned
1335 # and still are in pending state
1336 await self._destroy_pending_machines(model, only_manual=True)
David Garcia2f66c4d2020-06-19 11:40:18 +02001337 await self.disconnect_model(model)
1338
David Garciae610aed2021-07-26 15:04:37 +02001339 await self._destroy_model(
1340 model_name,
1341 controller,
1342 timeout=total_timeout,
1343 )
David Garcia5c966622022-05-03 12:23:59 +02001344 except Exception as e:
1345 if not await self.model_exists(model_name, controller=controller):
1346 return
1347 raise e
David Garciae610aed2021-07-26 15:04:37 +02001348 finally:
1349 if model:
1350 await self.disconnect_model(model)
1351 await self.disconnect_controller(controller)
David Garcia2f66c4d2020-06-19 11:40:18 +02001352
David Garciae610aed2021-07-26 15:04:37 +02001353 async def _destroy_model(
1354 self, model_name: str, controller: Controller, timeout: float = 1800
1355 ):
1356 """
1357 Destroy model from controller
David Garcia2f66c4d2020-06-19 11:40:18 +02001358
David Garciae610aed2021-07-26 15:04:37 +02001359 :param: model: Model name to be removed
1360 :param: controller: Controller object
1361 :param: timeout: Timeout in seconds
1362 """
1363
1364 async def _destroy_model_loop(model_name: str, controller: Controller):
1365 while await self.model_exists(model_name, controller=controller):
1366 await controller.destroy_model(
1367 model_name, destroy_storage=True, force=True, max_wait=0
1368 )
David Garcia2f66c4d2020-06-19 11:40:18 +02001369 await asyncio.sleep(5)
David Garciae610aed2021-07-26 15:04:37 +02001370
1371 try:
1372 await asyncio.wait_for(
1373 _destroy_model_loop(model_name, controller), timeout=timeout
1374 )
1375 except asyncio.TimeoutError:
David Garcia2f66c4d2020-06-19 11:40:18 +02001376 raise Exception(
David Garcia5ef42a12020-09-29 19:48:13 +02001377 "Timeout waiting for model {} to be destroyed".format(model_name)
David Garcia4fee80e2020-05-13 12:18:38 +02001378 )
David Garcia7e887b22022-04-28 13:43:36 +02001379 except juju.errors.JujuError as e:
1380 if any("has been removed" in error for error in e.errors):
1381 return
1382 raise e
David Garcia4fee80e2020-05-13 12:18:38 +02001383
aktas56120292021-02-26 15:32:39 +03001384 async def destroy_application(
1385 self, model_name: str, application_name: str, total_timeout: float
1386 ):
David Garcia4fee80e2020-05-13 12:18:38 +02001387 """
1388 Destroy application
1389
aktas56120292021-02-26 15:32:39 +03001390 :param: model_name: Model name
David Garcia4fee80e2020-05-13 12:18:38 +02001391 :param: application_name: Application name
aktas56120292021-02-26 15:32:39 +03001392 :param: total_timeout: Timeout
David Garcia4fee80e2020-05-13 12:18:38 +02001393 """
aktas56120292021-02-26 15:32:39 +03001394
1395 controller = await self.get_controller()
1396 model = None
1397
1398 try:
1399 model = await self.get_model(controller, model_name)
1400 self.log.debug(
1401 "Destroying application {} in model {}".format(
1402 application_name, model_name
1403 )
David Garcia4fee80e2020-05-13 12:18:38 +02001404 )
aktas56120292021-02-26 15:32:39 +03001405 application = self._get_application(model, application_name)
1406 if application:
1407 await application.destroy()
1408 else:
1409 self.log.warning("Application not found: {}".format(application_name))
1410
1411 self.log.debug(
1412 "Waiting for application {} to be destroyed in model {}...".format(
1413 application_name, model_name
1414 )
1415 )
1416 if total_timeout is None:
1417 total_timeout = 3600
1418 end = time.time() + total_timeout
1419 while time.time() < end:
1420 if not self._get_application(model, application_name):
1421 self.log.debug(
1422 "The application {} was destroyed in model {} ".format(
1423 application_name, model_name
1424 )
1425 )
1426 return
1427 await asyncio.sleep(5)
1428 raise Exception(
1429 "Timeout waiting for application {} to be destroyed in model {}".format(
1430 application_name, model_name
1431 )
1432 )
1433 finally:
1434 if model is not None:
1435 await self.disconnect_model(model)
1436 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +02001437
David Garcia168bb192020-10-21 14:19:45 +02001438 async def _destroy_pending_machines(self, model: Model, only_manual: bool = False):
1439 """
1440 Destroy pending machines in a given model
1441
1442 :param: only_manual: Bool that indicates only manually provisioned
1443 machines should be destroyed (if True), or that
1444 all pending machines should be destroyed
1445 """
1446 status = await model.get_status()
1447 for machine_id in status.machines:
1448 machine_status = status.machines[machine_id]
1449 if machine_status.agent_status.status == "pending":
1450 if only_manual and not machine_status.instance_id.startswith("manual:"):
1451 break
1452 machine = model.machines[machine_id]
1453 await machine.destroy(force=True)
1454
David Garcia4fee80e2020-05-13 12:18:38 +02001455 async def configure_application(
1456 self, model_name: str, application_name: str, config: dict = None
1457 ):
1458 """Configure application
1459
1460 :param: model_name: Model name
1461 :param: application_name: Application name
1462 :param: config: Config to apply to the charm
1463 """
David Garcia2f66c4d2020-06-19 11:40:18 +02001464 self.log.debug("Configuring application {}".format(application_name))
1465
David Garcia4fee80e2020-05-13 12:18:38 +02001466 if config:
David Garcia5b802c92020-11-11 16:56:06 +01001467 controller = await self.get_controller()
1468 model = None
David Garcia2f66c4d2020-06-19 11:40:18 +02001469 try:
David Garcia2f66c4d2020-06-19 11:40:18 +02001470 model = await self.get_model(controller, model_name)
1471 application = self._get_application(
David Garciaf6e9b002020-11-27 15:32:02 +01001472 model,
1473 application_name=application_name,
David Garcia2f66c4d2020-06-19 11:40:18 +02001474 )
1475 await application.set_config(config)
1476 finally:
David Garcia5b802c92020-11-11 16:56:06 +01001477 if model:
1478 await self.disconnect_model(model)
David Garcia2f66c4d2020-06-19 11:40:18 +02001479 await self.disconnect_controller(controller)
1480
David Garcia2f66c4d2020-06-19 11:40:18 +02001481 def handle_exception(self, loop, context):
1482 # All unhandled exceptions by libjuju are handled here.
1483 pass
1484
1485 async def health_check(self, interval: float = 300.0):
1486 """
1487 Health check to make sure controller and controller_model connections are OK
1488
1489 :param: interval: Time in seconds between checks
1490 """
David Garcia667696e2020-09-22 14:52:32 +02001491 controller = None
David Garcia2f66c4d2020-06-19 11:40:18 +02001492 while True:
1493 try:
1494 controller = await self.get_controller()
1495 # self.log.debug("VCA is alive")
1496 except Exception as e:
1497 self.log.error("Health check to VCA failed: {}".format(e))
1498 finally:
1499 await self.disconnect_controller(controller)
1500 await asyncio.sleep(interval)
Dominik Fleischmannb9513342020-06-09 11:57:14 +02001501
1502 async def list_models(self, contains: str = None) -> [str]:
1503 """List models with certain names
1504
1505 :param: contains: String that is contained in model name
1506
1507 :retur: [models] Returns list of model names
1508 """
1509
1510 controller = await self.get_controller()
1511 try:
1512 models = await controller.list_models()
1513 if contains:
1514 models = [model for model in models if contains in model]
1515 return models
1516 finally:
1517 await self.disconnect_controller(controller)
David Garciabc538e42020-08-25 15:22:30 +02001518
David Garcia582b9232021-10-26 12:30:44 +02001519 async def _list_offers(
1520 self, model_name: str, offer_name: str = None
1521 ) -> QueryApplicationOffersResults:
1522 """
1523 List offers within a model
David Garciabc538e42020-08-25 15:22:30 +02001524
1525 :param: model_name: Model name
David Garcia582b9232021-10-26 12:30:44 +02001526 :param: offer_name: Offer name to filter.
David Garciabc538e42020-08-25 15:22:30 +02001527
David Garcia582b9232021-10-26 12:30:44 +02001528 :return: Returns application offers results in the model
David Garciabc538e42020-08-25 15:22:30 +02001529 """
1530
1531 controller = await self.get_controller()
1532 try:
David Garcia582b9232021-10-26 12:30:44 +02001533 offers = (await controller.list_offers(model_name)).results
1534 if offer_name:
1535 matching_offer = []
1536 for offer in offers:
1537 if offer.offer_name == offer_name:
1538 matching_offer.append(offer)
1539 break
1540 offers = matching_offer
1541 return offers
David Garciabc538e42020-08-25 15:22:30 +02001542 finally:
1543 await self.disconnect_controller(controller)
David Garcia12b29242020-09-17 16:01:48 +02001544
David Garcia475a7222020-09-21 16:19:15 +02001545 async def add_k8s(
David Garcia7077e262020-10-16 15:38:13 +02001546 self,
1547 name: str,
David Garciaf6e9b002020-11-27 15:32:02 +01001548 rbac_id: str,
1549 token: str,
1550 client_cert_data: str,
David Garcia7077e262020-10-16 15:38:13 +02001551 configuration: Configuration,
1552 storage_class: str,
1553 credential_name: str = None,
David Garcia475a7222020-09-21 16:19:15 +02001554 ):
David Garcia12b29242020-09-17 16:01:48 +02001555 """
1556 Add a Kubernetes cloud to the controller
1557
1558 Similar to the `juju add-k8s` command in the CLI
1559
David Garcia7077e262020-10-16 15:38:13 +02001560 :param: name: Name for the K8s cloud
1561 :param: configuration: Kubernetes configuration object
1562 :param: storage_class: Storage Class to use in the cloud
1563 :param: credential_name: Storage Class to use in the cloud
David Garcia12b29242020-09-17 16:01:48 +02001564 """
1565
David Garcia12b29242020-09-17 16:01:48 +02001566 if not storage_class:
1567 raise Exception("storage_class must be a non-empty string")
1568 if not name:
1569 raise Exception("name must be a non-empty string")
David Garcia475a7222020-09-21 16:19:15 +02001570 if not configuration:
1571 raise Exception("configuration must be provided")
David Garcia12b29242020-09-17 16:01:48 +02001572
David Garcia475a7222020-09-21 16:19:15 +02001573 endpoint = configuration.host
David Garciaf6e9b002020-11-27 15:32:02 +01001574 credential = self.get_k8s_cloud_credential(
1575 configuration,
1576 client_cert_data,
1577 token,
David Garcia475a7222020-09-21 16:19:15 +02001578 )
David Garciaf6e9b002020-11-27 15:32:02 +01001579 credential.attrs[RBAC_LABEL_KEY_NAME] = rbac_id
David Garcia12b29242020-09-17 16:01:48 +02001580 cloud = client.Cloud(
David Garcia475a7222020-09-21 16:19:15 +02001581 type_="kubernetes",
1582 auth_types=[credential.auth_type],
David Garcia12b29242020-09-17 16:01:48 +02001583 endpoint=endpoint,
David Garciaf6e9b002020-11-27 15:32:02 +01001584 ca_certificates=[client_cert_data],
David Garcia12b29242020-09-17 16:01:48 +02001585 config={
1586 "operator-storage": storage_class,
1587 "workload-storage": storage_class,
1588 },
David Garcia12b29242020-09-17 16:01:48 +02001589 )
1590
David Garcia7077e262020-10-16 15:38:13 +02001591 return await self.add_cloud(
1592 name, cloud, credential, credential_name=credential_name
1593 )
David Garcia475a7222020-09-21 16:19:15 +02001594
1595 def get_k8s_cloud_credential(
David Garciaf6e9b002020-11-27 15:32:02 +01001596 self,
1597 configuration: Configuration,
1598 client_cert_data: str,
1599 token: str = None,
David Garcia475a7222020-09-21 16:19:15 +02001600 ) -> client.CloudCredential:
1601 attrs = {}
David Garciaf6e9b002020-11-27 15:32:02 +01001602 # TODO: Test with AKS
1603 key = None # open(configuration.key_file, "r").read()
David Garcia475a7222020-09-21 16:19:15 +02001604 username = configuration.username
1605 password = configuration.password
1606
David Garciaf6e9b002020-11-27 15:32:02 +01001607 if client_cert_data:
1608 attrs["ClientCertificateData"] = client_cert_data
David Garcia475a7222020-09-21 16:19:15 +02001609 if key:
David Garciaf6e9b002020-11-27 15:32:02 +01001610 attrs["ClientKeyData"] = key
David Garcia475a7222020-09-21 16:19:15 +02001611 if token:
1612 if username or password:
1613 raise JujuInvalidK8sConfiguration("Cannot set both token and user/pass")
1614 attrs["Token"] = token
1615
1616 auth_type = None
1617 if key:
1618 auth_type = "oauth2"
David Garciaf6e9b002020-11-27 15:32:02 +01001619 if client_cert_data:
1620 auth_type = "oauth2withcert"
David Garcia475a7222020-09-21 16:19:15 +02001621 if not token:
1622 raise JujuInvalidK8sConfiguration(
1623 "missing token for auth type {}".format(auth_type)
1624 )
1625 elif username:
1626 if not password:
1627 self.log.debug(
1628 "credential for user {} has empty password".format(username)
1629 )
1630 attrs["username"] = username
1631 attrs["password"] = password
David Garciaf6e9b002020-11-27 15:32:02 +01001632 if client_cert_data:
David Garcia475a7222020-09-21 16:19:15 +02001633 auth_type = "userpasswithcert"
1634 else:
1635 auth_type = "userpass"
David Garciaf6e9b002020-11-27 15:32:02 +01001636 elif client_cert_data and token:
David Garcia475a7222020-09-21 16:19:15 +02001637 auth_type = "certificate"
1638 else:
1639 raise JujuInvalidK8sConfiguration("authentication method not supported")
David Garcia667696e2020-09-22 14:52:32 +02001640 return client.CloudCredential(auth_type=auth_type, attrs=attrs)
David Garcia12b29242020-09-17 16:01:48 +02001641
1642 async def add_cloud(
David Garcia7077e262020-10-16 15:38:13 +02001643 self,
1644 name: str,
1645 cloud: Cloud,
1646 credential: CloudCredential = None,
1647 credential_name: str = None,
David Garcia12b29242020-09-17 16:01:48 +02001648 ) -> Cloud:
1649 """
1650 Add cloud to the controller
1651
David Garcia7077e262020-10-16 15:38:13 +02001652 :param: name: Name of the cloud to be added
1653 :param: cloud: Cloud object
1654 :param: credential: CloudCredentials object for the cloud
1655 :param: credential_name: Credential name.
1656 If not defined, cloud of the name will be used.
David Garcia12b29242020-09-17 16:01:48 +02001657 """
1658 controller = await self.get_controller()
1659 try:
1660 _ = await controller.add_cloud(name, cloud)
1661 if credential:
David Garcia7077e262020-10-16 15:38:13 +02001662 await controller.add_credential(
1663 credential_name or name, credential=credential, cloud=name
1664 )
David Garcia12b29242020-09-17 16:01:48 +02001665 # Need to return the object returned by the controller.add_cloud() function
1666 # I'm returning the original value now until this bug is fixed:
1667 # https://github.com/juju/python-libjuju/issues/443
1668 return cloud
1669 finally:
1670 await self.disconnect_controller(controller)
1671
1672 async def remove_cloud(self, name: str):
1673 """
1674 Remove cloud
1675
1676 :param: name: Name of the cloud to be removed
1677 """
1678 controller = await self.get_controller()
1679 try:
1680 await controller.remove_cloud(name)
David Garciaf980ac02021-07-27 15:07:42 +02001681 except juju.errors.JujuError as e:
1682 if len(e.errors) == 1 and f'cloud "{name}" not found' == e.errors[0]:
1683 self.log.warning(f"Cloud {name} not found, so it could not be deleted.")
1684 else:
1685 raise e
David Garcia12b29242020-09-17 16:01:48 +02001686 finally:
1687 await self.disconnect_controller(controller)
David Garcia59f520d2020-10-15 13:16:45 +02001688
David Garciaeb8943a2021-04-12 12:07:37 +02001689 @retry(attempts=20, delay=5, fallback=JujuLeaderUnitNotFound())
David Garcia59f520d2020-10-15 13:16:45 +02001690 async def _get_leader_unit(self, application: Application) -> Unit:
1691 unit = None
1692 for u in application.units:
1693 if await u.is_leader_from_status():
1694 unit = u
1695 break
David Garciaeb8943a2021-04-12 12:07:37 +02001696 if not unit:
1697 raise Exception()
David Garcia59f520d2020-10-15 13:16:45 +02001698 return unit
David Garciaf6e9b002020-11-27 15:32:02 +01001699
David Garciaeb8943a2021-04-12 12:07:37 +02001700 async def get_cloud_credentials(self, cloud: Cloud) -> typing.List:
1701 """
1702 Get cloud credentials
1703
1704 :param: cloud: Cloud object. The returned credentials will be from this cloud.
1705
1706 :return: List of credentials object associated to the specified cloud
1707
1708 """
David Garciaf6e9b002020-11-27 15:32:02 +01001709 controller = await self.get_controller()
1710 try:
1711 facade = client.CloudFacade.from_connection(controller.connection())
David Garciaeb8943a2021-04-12 12:07:37 +02001712 cloud_cred_tag = tag.credential(
1713 cloud.name, self.vca_connection.data.user, cloud.credential_name
1714 )
David Garciaf6e9b002020-11-27 15:32:02 +01001715 params = [client.Entity(cloud_cred_tag)]
1716 return (await facade.Credential(params)).results
1717 finally:
1718 await self.disconnect_controller(controller)
aktasfa02f8a2021-07-29 17:41:40 +03001719
1720 async def check_application_exists(self, model_name, application_name) -> bool:
1721 """Check application exists
1722
1723 :param: model_name: Model Name
1724 :param: application_name: Application Name
1725
1726 :return: Boolean
1727 """
1728
1729 model = None
1730 controller = await self.get_controller()
1731 try:
1732 model = await self.get_model(controller, model_name)
1733 self.log.debug(
1734 "Checking if application {} exists in model {}".format(
1735 application_name, model_name
1736 )
1737 )
1738 return self._get_application(model, application_name) is not None
1739 finally:
1740 if model:
1741 await self.disconnect_model(model)
1742 await self.disconnect_controller(controller)