blob: ceb5e027c8ce82684a9242503efa3652dbf80b5b [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
17from juju.controller import Controller
David Garcia4fee80e2020-05-13 12:18:38 +020018from juju.client import client
19import time
20
21from juju.errors import JujuAPIError
22from juju.model import Model
23from juju.machine import Machine
24from juju.application import Application
David Garciabc538e42020-08-25 15:22:30 +020025from juju.client._definitions import FullStatus, QueryApplicationOffersResults
David Garcia4fee80e2020-05-13 12:18:38 +020026from n2vc.juju_watcher import JujuModelWatcher
27from n2vc.provisioner import AsyncSSHProvisioner
28from n2vc.n2vc_conn import N2VCConnector
29from n2vc.exceptions import (
30 JujuMachineNotFound,
31 JujuApplicationNotFound,
Dominik Fleischmann7ff392f2020-07-07 13:11:19 +020032 JujuLeaderUnitNotFound,
33 JujuActionNotFound,
David Garcia4fee80e2020-05-13 12:18:38 +020034 JujuModelAlreadyExists,
35 JujuControllerFailedConnecting,
36 JujuApplicationExists,
37)
David Garcia2f66c4d2020-06-19 11:40:18 +020038from n2vc.utils import DB_DATA
39from osm_common.dbbase import DbException
David Garcia4fee80e2020-05-13 12:18:38 +020040
41
42class Libjuju:
43 def __init__(
44 self,
45 endpoint: str,
46 api_proxy: str,
47 username: str,
48 password: str,
49 cacert: str,
50 loop: asyncio.AbstractEventLoop = None,
51 log: logging.Logger = None,
52 db: dict = None,
53 n2vc: N2VCConnector = None,
54 apt_mirror: str = None,
55 enable_os_upgrade: bool = True,
56 ):
57 """
58 Constructor
59
60 :param: endpoint: Endpoint of the juju controller (host:port)
61 :param: api_proxy: Endpoint of the juju controller - Reachable from the VNFs
62 :param: username: Juju username
63 :param: password: Juju password
64 :param: cacert: Juju CA Certificate
65 :param: loop: Asyncio loop
66 :param: log: Logger
67 :param: db: DB object
68 :param: n2vc: N2VC object
69 :param: apt_mirror: APT Mirror
70 :param: enable_os_upgrade: Enable OS Upgrade
71 """
72
David Garcia2f66c4d2020-06-19 11:40:18 +020073 self.log = log or logging.getLogger("Libjuju")
74 self.db = db
David Garcia2cf8b2e2020-07-01 20:25:30 +020075 db_endpoints = self._get_api_endpoints_db()
76 self.endpoints = db_endpoints or [endpoint]
77 if db_endpoints is None:
78 self._update_api_endpoints_db(self.endpoints)
David Garcia4fee80e2020-05-13 12:18:38 +020079 self.api_proxy = api_proxy
80 self.username = username
81 self.password = password
82 self.cacert = cacert
83 self.loop = loop or asyncio.get_event_loop()
David Garcia4fee80e2020-05-13 12:18:38 +020084 self.n2vc = n2vc
85
86 # Generate config for models
87 self.model_config = {}
88 if apt_mirror:
89 self.model_config["apt-mirror"] = apt_mirror
90 self.model_config["enable-os-refresh-update"] = enable_os_upgrade
91 self.model_config["enable-os-upgrade"] = enable_os_upgrade
92
David Garcia2f66c4d2020-06-19 11:40:18 +020093 self.loop.set_exception_handler(self.handle_exception)
David Garcia4fee80e2020-05-13 12:18:38 +020094 self.creating_model = asyncio.Lock(loop=self.loop)
95
96 self.models = set()
David Garcia2f66c4d2020-06-19 11:40:18 +020097 self.log.debug("Libjuju initialized!")
David Garcia4fee80e2020-05-13 12:18:38 +020098
David Garcia2f66c4d2020-06-19 11:40:18 +020099 self.health_check_task = self.loop.create_task(self.health_check())
David Garcia4fee80e2020-05-13 12:18:38 +0200100
David Garcia2f66c4d2020-06-19 11:40:18 +0200101 async def get_controller(self, timeout: float = 5.0) -> Controller:
102 """
103 Get controller
David Garcia4fee80e2020-05-13 12:18:38 +0200104
David Garcia2f66c4d2020-06-19 11:40:18 +0200105 :param: timeout: Time in seconds to wait for controller to connect
106 """
107 controller = None
108 try:
109 controller = Controller(loop=self.loop)
110 await asyncio.wait_for(
111 controller.connect(
112 endpoint=self.endpoints,
113 username=self.username,
114 password=self.password,
115 cacert=self.cacert,
116 ),
117 timeout=timeout,
118 )
119 endpoints = await controller.api_endpoints
120 if self.endpoints != endpoints:
121 self.endpoints = endpoints
122 self._update_api_endpoints_db(self.endpoints)
123 return controller
124 except asyncio.CancelledError as e:
125 raise e
126 except Exception as e:
127 self.log.error(
128 "Failed connecting to controller: {}...".format(self.endpoints)
129 )
130 if controller:
131 await self.disconnect_controller(controller)
132 raise JujuControllerFailedConnecting(e)
David Garcia4fee80e2020-05-13 12:18:38 +0200133
134 async def disconnect(self):
David Garcia2f66c4d2020-06-19 11:40:18 +0200135 """Disconnect"""
136 # Cancel health check task
137 self.health_check_task.cancel()
138 self.log.debug("Libjuju disconnected!")
David Garcia4fee80e2020-05-13 12:18:38 +0200139
140 async def disconnect_model(self, model: Model):
141 """
142 Disconnect model
143
144 :param: model: Model that will be disconnected
145 """
David Garcia2f66c4d2020-06-19 11:40:18 +0200146 await model.disconnect()
David Garcia4fee80e2020-05-13 12:18:38 +0200147
David Garcia2f66c4d2020-06-19 11:40:18 +0200148 async def disconnect_controller(self, controller: Controller):
David Garcia4fee80e2020-05-13 12:18:38 +0200149 """
David Garcia2f66c4d2020-06-19 11:40:18 +0200150 Disconnect controller
David Garcia4fee80e2020-05-13 12:18:38 +0200151
David Garcia2f66c4d2020-06-19 11:40:18 +0200152 :param: controller: Controller that will be disconnected
David Garcia4fee80e2020-05-13 12:18:38 +0200153 """
David Garcia2f66c4d2020-06-19 11:40:18 +0200154 await controller.disconnect()
David Garcia4fee80e2020-05-13 12:18:38 +0200155
156 async def add_model(self, model_name: str, cloud_name: str):
157 """
158 Create model
159
160 :param: model_name: Model name
161 :param: cloud_name: Cloud name
162 """
163
David Garcia2f66c4d2020-06-19 11:40:18 +0200164 # Get controller
165 controller = await self.get_controller()
166 model = None
167 try:
168 # Raise exception if model already exists
169 if await self.model_exists(model_name, controller=controller):
170 raise JujuModelAlreadyExists(
171 "Model {} already exists.".format(model_name)
172 )
David Garcia4fee80e2020-05-13 12:18:38 +0200173
David Garcia2f66c4d2020-06-19 11:40:18 +0200174 # Block until other workers have finished model creation
175 while self.creating_model.locked():
176 await asyncio.sleep(0.1)
David Garcia4fee80e2020-05-13 12:18:38 +0200177
David Garcia2f66c4d2020-06-19 11:40:18 +0200178 # If the model exists, return it from the controller
179 if model_name in self.models:
180 return
David Garcia4fee80e2020-05-13 12:18:38 +0200181
David Garcia2f66c4d2020-06-19 11:40:18 +0200182 # Create the model
183 async with self.creating_model:
184 self.log.debug("Creating model {}".format(model_name))
185 model = await controller.add_model(
186 model_name,
187 config=self.model_config,
188 cloud_name=cloud_name,
189 credential_name=cloud_name,
190 )
191 self.models.add(model_name)
192 finally:
193 if model:
194 await self.disconnect_model(model)
195 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +0200196
David Garcia2f66c4d2020-06-19 11:40:18 +0200197 async def get_model(
198 self, controller: Controller, model_name: str, id=None
199 ) -> Model:
David Garcia4fee80e2020-05-13 12:18:38 +0200200 """
201 Get model from controller
202
David Garcia2f66c4d2020-06-19 11:40:18 +0200203 :param: controller: Controller
David Garcia4fee80e2020-05-13 12:18:38 +0200204 :param: model_name: Model name
205
206 :return: Model: The created Juju model object
207 """
David Garcia2f66c4d2020-06-19 11:40:18 +0200208 return await controller.get_model(model_name)
David Garcia4fee80e2020-05-13 12:18:38 +0200209
David Garcia2f66c4d2020-06-19 11:40:18 +0200210 async def model_exists(
211 self, model_name: str, controller: Controller = None
212 ) -> bool:
David Garcia4fee80e2020-05-13 12:18:38 +0200213 """
214 Check if model exists
215
David Garcia2f66c4d2020-06-19 11:40:18 +0200216 :param: controller: Controller
David Garcia4fee80e2020-05-13 12:18:38 +0200217 :param: model_name: Model name
218
219 :return bool
220 """
David Garcia2f66c4d2020-06-19 11:40:18 +0200221 need_to_disconnect = False
David Garcia4fee80e2020-05-13 12:18:38 +0200222
David Garcia2f66c4d2020-06-19 11:40:18 +0200223 # Get controller if not passed
224 if not controller:
225 controller = await self.get_controller()
226 need_to_disconnect = True
David Garcia4fee80e2020-05-13 12:18:38 +0200227
David Garcia2f66c4d2020-06-19 11:40:18 +0200228 # Check if model exists
229 try:
230 return model_name in await controller.list_models()
231 finally:
232 if need_to_disconnect:
233 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +0200234
David Garcia42f328a2020-08-25 15:03:01 +0200235 async def models_exist(self, model_names: [str]) -> (bool, list):
236 """
237 Check if models exists
238
239 :param: model_names: List of strings with model names
240
241 :return (bool, list[str]): (True if all models exists, List of model names that don't exist)
242 """
243 if not model_names:
244 raise Exception(
245 "model_names must be a non-empty array. Given value: {}".format(model_names)
246 )
247 non_existing_models = []
248 models = await self.list_models()
249 existing_models = list(set(models).intersection(model_names))
250 non_existing_models = list(set(model_names) - set(existing_models))
251
252 return (
253 len(non_existing_models) == 0,
254 non_existing_models,
255 )
256
David Garcia4fee80e2020-05-13 12:18:38 +0200257 async def get_model_status(self, model_name: str) -> FullStatus:
258 """
259 Get model status
260
261 :param: model_name: Model name
262
263 :return: Full status object
264 """
David Garcia2f66c4d2020-06-19 11:40:18 +0200265 controller = await self.get_controller()
266 model = await self.get_model(controller, model_name)
267 try:
268 return await model.get_status()
269 finally:
270 await self.disconnect_model(model)
271 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +0200272
273 async def create_machine(
274 self,
275 model_name: str,
276 machine_id: str = None,
277 db_dict: dict = None,
278 progress_timeout: float = None,
279 total_timeout: float = None,
280 series: str = "xenial",
David Garciaf8a9d462020-03-25 18:19:02 +0100281 wait: bool = True,
David Garcia4fee80e2020-05-13 12:18:38 +0200282 ) -> (Machine, bool):
283 """
284 Create machine
285
286 :param: model_name: Model name
287 :param: machine_id: Machine id
288 :param: db_dict: Dictionary with data of the DB to write the updates
289 :param: progress_timeout: Maximum time between two updates in the model
290 :param: total_timeout: Timeout for the entity to be active
David Garciaf8a9d462020-03-25 18:19:02 +0100291 :param: series: Series of the machine (xenial, bionic, focal, ...)
292 :param: wait: Wait until machine is ready
David Garcia4fee80e2020-05-13 12:18:38 +0200293
294 :return: (juju.machine.Machine, bool): Machine object and a boolean saying
295 if the machine is new or it already existed
296 """
297 new = False
298 machine = None
299
300 self.log.debug(
301 "Creating machine (id={}) in model: {}".format(machine_id, model_name)
302 )
303
David Garcia2f66c4d2020-06-19 11:40:18 +0200304 # Get controller
305 controller = await self.get_controller()
306
David Garcia4fee80e2020-05-13 12:18:38 +0200307 # Get model
David Garcia2f66c4d2020-06-19 11:40:18 +0200308 model = await self.get_model(controller, model_name)
David Garcia4fee80e2020-05-13 12:18:38 +0200309 try:
310 if machine_id is not None:
311 self.log.debug(
312 "Searching machine (id={}) in model {}".format(
313 machine_id, model_name
314 )
315 )
316
317 # Get machines from model and get the machine with machine_id if exists
318 machines = await model.get_machines()
319 if machine_id in machines:
320 self.log.debug(
321 "Machine (id={}) found in model {}".format(
322 machine_id, model_name
323 )
324 )
Dominik Fleischmann7ff392f2020-07-07 13:11:19 +0200325 machine = machines[machine_id]
David Garcia4fee80e2020-05-13 12:18:38 +0200326 else:
327 raise JujuMachineNotFound("Machine {} not found".format(machine_id))
328
329 if machine is None:
330 self.log.debug("Creating a new machine in model {}".format(model_name))
331
332 # Create machine
333 machine = await model.add_machine(
334 spec=None, constraints=None, disks=None, series=series
335 )
336 new = True
337
338 # Wait until the machine is ready
David Garcia2f66c4d2020-06-19 11:40:18 +0200339 self.log.debug(
340 "Wait until machine {} is ready in model {}".format(
341 machine.entity_id, model_name
342 )
343 )
David Garciaf8a9d462020-03-25 18:19:02 +0100344 if wait:
345 await JujuModelWatcher.wait_for(
346 model=model,
347 entity=machine,
348 progress_timeout=progress_timeout,
349 total_timeout=total_timeout,
350 db_dict=db_dict,
351 n2vc=self.n2vc,
352 )
David Garcia4fee80e2020-05-13 12:18:38 +0200353 finally:
354 await self.disconnect_model(model)
David Garcia2f66c4d2020-06-19 11:40:18 +0200355 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +0200356
David Garcia2f66c4d2020-06-19 11:40:18 +0200357 self.log.debug(
358 "Machine {} ready at {} in model {}".format(
359 machine.entity_id, machine.dns_name, model_name
360 )
361 )
David Garcia4fee80e2020-05-13 12:18:38 +0200362 return machine, new
363
364 async def provision_machine(
365 self,
366 model_name: str,
367 hostname: str,
368 username: str,
369 private_key_path: str,
370 db_dict: dict = None,
371 progress_timeout: float = None,
372 total_timeout: float = None,
373 ) -> str:
374 """
375 Manually provisioning of a machine
376
377 :param: model_name: Model name
378 :param: hostname: IP to access the machine
379 :param: username: Username to login to the machine
380 :param: private_key_path: Local path for the private key
381 :param: db_dict: Dictionary with data of the DB to write the updates
382 :param: progress_timeout: Maximum time between two updates in the model
383 :param: total_timeout: Timeout for the entity to be active
384
385 :return: (Entity): Machine id
386 """
387 self.log.debug(
388 "Provisioning machine. model: {}, hostname: {}, username: {}".format(
389 model_name, hostname, username
390 )
391 )
392
David Garcia2f66c4d2020-06-19 11:40:18 +0200393 # Get controller
394 controller = await self.get_controller()
395
David Garcia4fee80e2020-05-13 12:18:38 +0200396 # Get model
David Garcia2f66c4d2020-06-19 11:40:18 +0200397 model = await self.get_model(controller, model_name)
David Garcia4fee80e2020-05-13 12:18:38 +0200398
399 try:
400 # Get provisioner
401 provisioner = AsyncSSHProvisioner(
402 host=hostname,
403 user=username,
404 private_key_path=private_key_path,
405 log=self.log,
406 )
407
408 # Provision machine
409 params = await provisioner.provision_machine()
410
411 params.jobs = ["JobHostUnits"]
412
413 self.log.debug("Adding machine to model")
414 connection = model.connection()
415 client_facade = client.ClientFacade.from_connection(connection)
416
417 results = await client_facade.AddMachines(params=[params])
418 error = results.machines[0].error
419
420 if error:
421 msg = "Error adding machine: {}".format(error.message)
422 self.log.error(msg=msg)
423 raise ValueError(msg)
424
425 machine_id = results.machines[0].machine
426
427 self.log.debug("Installing Juju agent into machine {}".format(machine_id))
428 asyncio.ensure_future(
429 provisioner.install_agent(
430 connection=connection,
431 nonce=params.nonce,
432 machine_id=machine_id,
David Garcia81045962020-07-16 12:37:13 +0200433 proxy=self.api_proxy,
David Garcia4fee80e2020-05-13 12:18:38 +0200434 )
435 )
436
437 machine = None
438 for _ in range(10):
439 machine_list = await model.get_machines()
440 if machine_id in machine_list:
441 self.log.debug("Machine {} found in model!".format(machine_id))
442 machine = model.machines.get(machine_id)
443 break
444 await asyncio.sleep(2)
445
446 if machine is None:
447 msg = "Machine {} not found in model".format(machine_id)
448 self.log.error(msg=msg)
449 raise JujuMachineNotFound(msg)
450
David Garcia2f66c4d2020-06-19 11:40:18 +0200451 self.log.debug(
452 "Wait until machine {} is ready in model {}".format(
453 machine.entity_id, model_name
454 )
455 )
David Garcia4fee80e2020-05-13 12:18:38 +0200456 await JujuModelWatcher.wait_for(
457 model=model,
458 entity=machine,
459 progress_timeout=progress_timeout,
460 total_timeout=total_timeout,
461 db_dict=db_dict,
462 n2vc=self.n2vc,
463 )
464 except Exception as e:
465 raise e
466 finally:
467 await self.disconnect_model(model)
David Garcia2f66c4d2020-06-19 11:40:18 +0200468 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +0200469
David Garcia2f66c4d2020-06-19 11:40:18 +0200470 self.log.debug(
471 "Machine provisioned {} in model {}".format(machine_id, model_name)
472 )
David Garcia4fee80e2020-05-13 12:18:38 +0200473
474 return machine_id
475
476 async def deploy_charm(
477 self,
478 application_name: str,
479 path: str,
480 model_name: str,
481 machine_id: str,
482 db_dict: dict = None,
483 progress_timeout: float = None,
484 total_timeout: float = None,
485 config: dict = None,
486 series: str = None,
David Garciaf8a9d462020-03-25 18:19:02 +0100487 num_units: int = 1,
David Garcia4fee80e2020-05-13 12:18:38 +0200488 ):
489 """Deploy charm
490
491 :param: application_name: Application name
492 :param: path: Local path to the charm
493 :param: model_name: Model name
494 :param: machine_id ID of the machine
495 :param: db_dict: Dictionary with data of the DB to write the updates
496 :param: progress_timeout: Maximum time between two updates in the model
497 :param: total_timeout: Timeout for the entity to be active
498 :param: config: Config for the charm
499 :param: series: Series of the charm
David Garciaf8a9d462020-03-25 18:19:02 +0100500 :param: num_units: Number of units
David Garcia4fee80e2020-05-13 12:18:38 +0200501
502 :return: (juju.application.Application): Juju application
503 """
David Garcia2f66c4d2020-06-19 11:40:18 +0200504 self.log.debug(
505 "Deploying charm {} to machine {} in model ~{}".format(
506 application_name, machine_id, model_name
507 )
508 )
509 self.log.debug("charm: {}".format(path))
510
511 # Get controller
512 controller = await self.get_controller()
David Garcia4fee80e2020-05-13 12:18:38 +0200513
514 # Get model
David Garcia2f66c4d2020-06-19 11:40:18 +0200515 model = await self.get_model(controller, model_name)
David Garcia4fee80e2020-05-13 12:18:38 +0200516
517 try:
518 application = None
519 if application_name not in model.applications:
David Garcia2f66c4d2020-06-19 11:40:18 +0200520
David Garcia4fee80e2020-05-13 12:18:38 +0200521 if machine_id is not None:
522 if machine_id not in model.machines:
523 msg = "Machine {} not found in model".format(machine_id)
524 self.log.error(msg=msg)
525 raise JujuMachineNotFound(msg)
526 machine = model.machines[machine_id]
527 series = machine.series
528
529 application = await model.deploy(
530 entity_url=path,
531 application_name=application_name,
532 channel="stable",
533 num_units=1,
534 series=series,
535 to=machine_id,
536 config=config,
537 )
538
David Garcia2f66c4d2020-06-19 11:40:18 +0200539 self.log.debug(
540 "Wait until application {} is ready in model {}".format(
541 application_name, model_name
542 )
543 )
David Garciaf8a9d462020-03-25 18:19:02 +0100544 if num_units > 1:
545 for _ in range(num_units - 1):
546 m, _ = await self.create_machine(model_name, wait=False)
547 await application.add_unit(to=m.entity_id)
548
David Garcia4fee80e2020-05-13 12:18:38 +0200549 await JujuModelWatcher.wait_for(
550 model=model,
551 entity=application,
552 progress_timeout=progress_timeout,
553 total_timeout=total_timeout,
554 db_dict=db_dict,
555 n2vc=self.n2vc,
556 )
David Garcia2f66c4d2020-06-19 11:40:18 +0200557 self.log.debug(
558 "Application {} is ready in model {}".format(
559 application_name, model_name
560 )
561 )
David Garcia4fee80e2020-05-13 12:18:38 +0200562 else:
David Garcia2f66c4d2020-06-19 11:40:18 +0200563 raise JujuApplicationExists(
564 "Application {} exists".format(application_name)
565 )
David Garcia4fee80e2020-05-13 12:18:38 +0200566 finally:
567 await self.disconnect_model(model)
David Garcia2f66c4d2020-06-19 11:40:18 +0200568 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +0200569
570 return application
571
David Garcia2f66c4d2020-06-19 11:40:18 +0200572 def _get_application(self, model: Model, application_name: str) -> Application:
David Garcia4fee80e2020-05-13 12:18:38 +0200573 """Get application
574
575 :param: model: Model object
576 :param: application_name: Application name
577
578 :return: juju.application.Application (or None if it doesn't exist)
579 """
580 if model.applications and application_name in model.applications:
581 return model.applications[application_name]
582
583 async def execute_action(
584 self,
585 application_name: str,
586 model_name: str,
587 action_name: str,
588 db_dict: dict = None,
589 progress_timeout: float = None,
590 total_timeout: float = None,
591 **kwargs
592 ):
593 """Execute action
594
595 :param: application_name: Application name
596 :param: model_name: Model name
David Garcia4fee80e2020-05-13 12:18:38 +0200597 :param: action_name: Name of the action
598 :param: db_dict: Dictionary with data of the DB to write the updates
599 :param: progress_timeout: Maximum time between two updates in the model
600 :param: total_timeout: Timeout for the entity to be active
601
602 :return: (str, str): (output and status)
603 """
David Garcia2f66c4d2020-06-19 11:40:18 +0200604 self.log.debug(
605 "Executing action {} using params {}".format(action_name, kwargs)
606 )
607 # Get controller
608 controller = await self.get_controller()
609
610 # Get model
611 model = await self.get_model(controller, model_name)
David Garcia4fee80e2020-05-13 12:18:38 +0200612
613 try:
614 # Get application
David Garcia2f66c4d2020-06-19 11:40:18 +0200615 application = self._get_application(
David Garcia4fee80e2020-05-13 12:18:38 +0200616 model, application_name=application_name,
617 )
618 if application is None:
619 raise JujuApplicationNotFound("Cannot execute action")
620
621 # Get unit
622 unit = None
623 for u in application.units:
624 if await u.is_leader_from_status():
625 unit = u
626 if unit is None:
Dominik Fleischmann7ff392f2020-07-07 13:11:19 +0200627 raise JujuLeaderUnitNotFound("Cannot execute action: leader unit not found")
David Garcia4fee80e2020-05-13 12:18:38 +0200628
629 actions = await application.get_actions()
630
631 if action_name not in actions:
Dominik Fleischmann7ff392f2020-07-07 13:11:19 +0200632 raise JujuActionNotFound(
David Garcia4fee80e2020-05-13 12:18:38 +0200633 "Action {} not in available actions".format(action_name)
634 )
635
David Garcia4fee80e2020-05-13 12:18:38 +0200636 action = await unit.run_action(action_name, **kwargs)
637
David Garcia2f66c4d2020-06-19 11:40:18 +0200638 self.log.debug(
639 "Wait until action {} is completed in application {} (model={})".format(
640 action_name, application_name, model_name
641 )
642 )
David Garcia4fee80e2020-05-13 12:18:38 +0200643 await JujuModelWatcher.wait_for(
644 model=model,
645 entity=action,
646 progress_timeout=progress_timeout,
647 total_timeout=total_timeout,
648 db_dict=db_dict,
649 n2vc=self.n2vc,
650 )
David Garcia2f66c4d2020-06-19 11:40:18 +0200651
David Garcia4fee80e2020-05-13 12:18:38 +0200652 output = await model.get_action_output(action_uuid=action.entity_id)
653 status = await model.get_action_status(uuid_or_prefix=action.entity_id)
654 status = (
655 status[action.entity_id] if action.entity_id in status else "failed"
656 )
657
David Garcia2f66c4d2020-06-19 11:40:18 +0200658 self.log.debug(
659 "Action {} completed with status {} in application {} (model={})".format(
660 action_name, action.status, application_name, model_name
661 )
662 )
David Garcia4fee80e2020-05-13 12:18:38 +0200663 finally:
664 await self.disconnect_model(model)
David Garcia2f66c4d2020-06-19 11:40:18 +0200665 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +0200666
667 return output, status
668
669 async def get_actions(self, application_name: str, model_name: str) -> dict:
670 """Get list of actions
671
672 :param: application_name: Application name
673 :param: model_name: Model name
674
675 :return: Dict with this format
676 {
677 "action_name": "Description of the action",
678 ...
679 }
680 """
David Garcia2f66c4d2020-06-19 11:40:18 +0200681 self.log.debug(
682 "Getting list of actions for application {}".format(application_name)
David Garcia4fee80e2020-05-13 12:18:38 +0200683 )
684
David Garcia2f66c4d2020-06-19 11:40:18 +0200685 # Get controller
686 controller = await self.get_controller()
David Garcia4fee80e2020-05-13 12:18:38 +0200687
David Garcia2f66c4d2020-06-19 11:40:18 +0200688 # Get model
689 model = await self.get_model(controller, model_name)
David Garcia4fee80e2020-05-13 12:18:38 +0200690
David Garcia2f66c4d2020-06-19 11:40:18 +0200691 try:
692 # Get application
693 application = self._get_application(
694 model, application_name=application_name,
695 )
696
697 # Return list of actions
698 return await application.get_actions()
699
700 finally:
701 # Disconnect from model and controller
702 await self.disconnect_model(model)
703 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +0200704
705 async def add_relation(
706 self,
707 model_name: str,
David Garcia8331f7c2020-08-25 16:10:07 +0200708 endpoint_1: str,
709 endpoint_2: str,
David Garcia4fee80e2020-05-13 12:18:38 +0200710 ):
711 """Add relation
712
David Garcia8331f7c2020-08-25 16:10:07 +0200713 :param: model_name: Model name
714 :param: endpoint_1 First endpoint name
715 ("app:endpoint" format or directly the saas name)
716 :param: endpoint_2: Second endpoint name (^ same format)
David Garcia4fee80e2020-05-13 12:18:38 +0200717 """
718
David Garcia8331f7c2020-08-25 16:10:07 +0200719 self.log.debug("Adding relation: {} -> {}".format(endpoint_1, endpoint_2))
David Garcia2f66c4d2020-06-19 11:40:18 +0200720
721 # Get controller
722 controller = await self.get_controller()
723
David Garcia4fee80e2020-05-13 12:18:38 +0200724 # Get model
David Garcia2f66c4d2020-06-19 11:40:18 +0200725 model = await self.get_model(controller, model_name)
David Garcia4fee80e2020-05-13 12:18:38 +0200726
David Garcia4fee80e2020-05-13 12:18:38 +0200727 # Add relation
David Garcia4fee80e2020-05-13 12:18:38 +0200728 try:
David Garcia8331f7c2020-08-25 16:10:07 +0200729 await model.add_relation(endpoint_1, endpoint_2)
David Garcia4fee80e2020-05-13 12:18:38 +0200730 except JujuAPIError as e:
731 if "not found" in e.message:
732 self.log.warning("Relation not found: {}".format(e.message))
733 return
734 if "already exists" in e.message:
735 self.log.warning("Relation already exists: {}".format(e.message))
736 return
737 # another exception, raise it
738 raise e
739 finally:
740 await self.disconnect_model(model)
David Garcia2f66c4d2020-06-19 11:40:18 +0200741 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +0200742
David Garciaf8a9d462020-03-25 18:19:02 +0100743 async def destroy_model(self, model_name: str, total_timeout: float):
David Garcia4fee80e2020-05-13 12:18:38 +0200744 """
745 Destroy model
746
747 :param: model_name: Model name
748 :param: total_timeout: Timeout
749 """
David Garcia4fee80e2020-05-13 12:18:38 +0200750
David Garcia2f66c4d2020-06-19 11:40:18 +0200751 controller = await self.get_controller()
752 model = await self.get_model(controller, model_name)
753 try:
754 self.log.debug("Destroying model {}".format(model_name))
755 uuid = model.info.uuid
756
David Garcia2f66c4d2020-06-19 11:40:18 +0200757 # Destroy machines
758 machines = await model.get_machines()
759 for machine_id in machines:
760 try:
761 await self.destroy_machine(
762 model, machine_id=machine_id, total_timeout=total_timeout,
763 )
764 except asyncio.CancelledError:
765 raise
766 except Exception:
767 pass
768
769 # Disconnect model
770 await self.disconnect_model(model)
771
772 # Destroy model
773 if model_name in self.models:
774 self.models.remove(model_name)
775
776 await controller.destroy_model(uuid)
777
778 # Wait until model is destroyed
779 self.log.debug("Waiting for model {} to be destroyed...".format(model_name))
780 last_exception = ""
781
782 if total_timeout is None:
783 total_timeout = 3600
784 end = time.time() + total_timeout
785 while time.time() < end:
786 try:
787 models = await controller.list_models()
788 if model_name not in models:
789 self.log.debug(
790 "The model {} ({}) was destroyed".format(model_name, uuid)
791 )
792 return
793 except asyncio.CancelledError:
794 raise
795 except Exception as e:
796 last_exception = e
797 await asyncio.sleep(5)
798 raise Exception(
799 "Timeout waiting for model {} to be destroyed {}".format(
800 model_name, last_exception
801 )
David Garcia4fee80e2020-05-13 12:18:38 +0200802 )
David Garcia2f66c4d2020-06-19 11:40:18 +0200803 finally:
804 await self.disconnect_controller(controller)
David Garcia4fee80e2020-05-13 12:18:38 +0200805
806 async def destroy_application(self, model: Model, application_name: str):
807 """
808 Destroy application
809
810 :param: model: Model object
811 :param: application_name: Application name
812 """
813 self.log.debug(
814 "Destroying application {} in model {}".format(
815 application_name, model.info.name
816 )
817 )
818 application = model.applications.get(application_name)
819 if application:
820 await application.destroy()
821 else:
822 self.log.warning("Application not found: {}".format(application_name))
823
824 async def destroy_machine(
825 self, model: Model, machine_id: str, total_timeout: float = 3600
826 ):
827 """
828 Destroy machine
829
830 :param: model: Model object
831 :param: machine_id: Machine id
832 :param: total_timeout: Timeout in seconds
833 """
834 machines = await model.get_machines()
835 if machine_id in machines:
Dominik Fleischmann7ff392f2020-07-07 13:11:19 +0200836 machine = machines[machine_id]
David Garciab8ff39b2020-06-25 17:18:31 +0200837 await machine.destroy(force=True)
838 # max timeout
839 end = time.time() + total_timeout
David Garcia4fee80e2020-05-13 12:18:38 +0200840
David Garciab8ff39b2020-06-25 17:18:31 +0200841 # wait for machine removal
842 machines = await model.get_machines()
843 while machine_id in machines and time.time() < end:
844 self.log.debug(
845 "Waiting for machine {} is destroyed".format(machine_id)
846 )
847 await asyncio.sleep(0.5)
David Garcia4fee80e2020-05-13 12:18:38 +0200848 machines = await model.get_machines()
David Garciab8ff39b2020-06-25 17:18:31 +0200849 self.log.debug("Machine destroyed: {}".format(machine_id))
David Garcia4fee80e2020-05-13 12:18:38 +0200850 else:
851 self.log.debug("Machine not found: {}".format(machine_id))
852
853 async def configure_application(
854 self, model_name: str, application_name: str, config: dict = None
855 ):
856 """Configure application
857
858 :param: model_name: Model name
859 :param: application_name: Application name
860 :param: config: Config to apply to the charm
861 """
David Garcia2f66c4d2020-06-19 11:40:18 +0200862 self.log.debug("Configuring application {}".format(application_name))
863
David Garcia4fee80e2020-05-13 12:18:38 +0200864 if config:
David Garcia2f66c4d2020-06-19 11:40:18 +0200865 try:
866 controller = await self.get_controller()
867 model = await self.get_model(controller, model_name)
868 application = self._get_application(
869 model, application_name=application_name,
870 )
871 await application.set_config(config)
872 finally:
873 await self.disconnect_model(model)
874 await self.disconnect_controller(controller)
875
876 def _get_api_endpoints_db(self) -> [str]:
877 """
878 Get API Endpoints from DB
879
880 :return: List of API endpoints
881 """
882 self.log.debug("Getting endpoints from database")
883
884 juju_info = self.db.get_one(
885 DB_DATA.api_endpoints.table,
886 q_filter=DB_DATA.api_endpoints.filter,
887 fail_on_empty=False,
888 )
889 if juju_info and DB_DATA.api_endpoints.key in juju_info:
890 return juju_info[DB_DATA.api_endpoints.key]
891
892 def _update_api_endpoints_db(self, endpoints: [str]):
893 """
894 Update API endpoints in Database
895
896 :param: List of endpoints
897 """
898 self.log.debug("Saving endpoints {} in database".format(endpoints))
899
900 juju_info = self.db.get_one(
901 DB_DATA.api_endpoints.table,
902 q_filter=DB_DATA.api_endpoints.filter,
903 fail_on_empty=False,
904 )
905 # If it doesn't, then create it
906 if not juju_info:
907 try:
908 self.db.create(
909 DB_DATA.api_endpoints.table, DB_DATA.api_endpoints.filter,
910 )
911 except DbException as e:
912 # Racing condition: check if another N2VC worker has created it
913 juju_info = self.db.get_one(
914 DB_DATA.api_endpoints.table,
915 q_filter=DB_DATA.api_endpoints.filter,
916 fail_on_empty=False,
917 )
918 if not juju_info:
919 raise e
920 self.db.set_one(
921 DB_DATA.api_endpoints.table,
922 DB_DATA.api_endpoints.filter,
923 {DB_DATA.api_endpoints.key: endpoints},
924 )
925
926 def handle_exception(self, loop, context):
927 # All unhandled exceptions by libjuju are handled here.
928 pass
929
930 async def health_check(self, interval: float = 300.0):
931 """
932 Health check to make sure controller and controller_model connections are OK
933
934 :param: interval: Time in seconds between checks
935 """
936 while True:
937 try:
938 controller = await self.get_controller()
939 # self.log.debug("VCA is alive")
940 except Exception as e:
941 self.log.error("Health check to VCA failed: {}".format(e))
942 finally:
943 await self.disconnect_controller(controller)
944 await asyncio.sleep(interval)
Dominik Fleischmannb9513342020-06-09 11:57:14 +0200945
946 async def list_models(self, contains: str = None) -> [str]:
947 """List models with certain names
948
949 :param: contains: String that is contained in model name
950
951 :retur: [models] Returns list of model names
952 """
953
954 controller = await self.get_controller()
955 try:
956 models = await controller.list_models()
957 if contains:
958 models = [model for model in models if contains in model]
959 return models
960 finally:
961 await self.disconnect_controller(controller)
David Garciabc538e42020-08-25 15:22:30 +0200962
963 async def list_offers(self, model_name: str) -> QueryApplicationOffersResults:
964 """List models with certain names
965
966 :param: model_name: Model name
967
968 :return: Returns list of offers
969 """
970
971 controller = await self.get_controller()
972 try:
973 return await controller.list_offers(model_name)
974 finally:
975 await self.disconnect_controller(controller)