Fix bug 1216: Remove machines only for native charms
[osm/N2VC.git] / n2vc / libjuju.py
1 # 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
15 import asyncio
16 import logging
17 from juju.controller import Controller
18 from juju.client import client
19 import time
20
21 from juju.errors import JujuAPIError
22 from juju.model import Model
23 from juju.machine import Machine
24 from juju.application import Application
25 from juju.client._definitions import FullStatus
26 from n2vc.juju_watcher import JujuModelWatcher
27 from n2vc.provisioner import AsyncSSHProvisioner
28 from n2vc.n2vc_conn import N2VCConnector
29 from n2vc.exceptions import (
30 JujuMachineNotFound,
31 JujuApplicationNotFound,
32 JujuLeaderUnitNotFound,
33 JujuActionNotFound,
34 JujuModelAlreadyExists,
35 JujuControllerFailedConnecting,
36 JujuApplicationExists,
37 )
38 from n2vc.utils import DB_DATA
39 from osm_common.dbbase import DbException
40
41
42 class 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
73 self.log = log or logging.getLogger("Libjuju")
74 self.db = db
75 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)
79 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()
84 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
93 self.loop.set_exception_handler(self.handle_exception)
94 self.creating_model = asyncio.Lock(loop=self.loop)
95
96 self.models = set()
97 self.log.debug("Libjuju initialized!")
98
99 self.health_check_task = self.loop.create_task(self.health_check())
100
101 async def get_controller(self, timeout: float = 5.0) -> Controller:
102 """
103 Get controller
104
105 :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)
133
134 async def disconnect(self):
135 """Disconnect"""
136 # Cancel health check task
137 self.health_check_task.cancel()
138 self.log.debug("Libjuju disconnected!")
139
140 async def disconnect_model(self, model: Model):
141 """
142 Disconnect model
143
144 :param: model: Model that will be disconnected
145 """
146 await model.disconnect()
147
148 async def disconnect_controller(self, controller: Controller):
149 """
150 Disconnect controller
151
152 :param: controller: Controller that will be disconnected
153 """
154 await controller.disconnect()
155
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
164 # 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 )
173
174 # Block until other workers have finished model creation
175 while self.creating_model.locked():
176 await asyncio.sleep(0.1)
177
178 # If the model exists, return it from the controller
179 if model_name in self.models:
180 return
181
182 # 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)
196
197 async def get_model(
198 self, controller: Controller, model_name: str, id=None
199 ) -> Model:
200 """
201 Get model from controller
202
203 :param: controller: Controller
204 :param: model_name: Model name
205
206 :return: Model: The created Juju model object
207 """
208 return await controller.get_model(model_name)
209
210 async def model_exists(
211 self, model_name: str, controller: Controller = None
212 ) -> bool:
213 """
214 Check if model exists
215
216 :param: controller: Controller
217 :param: model_name: Model name
218
219 :return bool
220 """
221 need_to_disconnect = False
222
223 # Get controller if not passed
224 if not controller:
225 controller = await self.get_controller()
226 need_to_disconnect = True
227
228 # 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)
234
235 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(
246 model_names
247 )
248 )
249 non_existing_models = []
250 models = await self.list_models()
251 existing_models = list(set(models).intersection(model_names))
252 non_existing_models = list(set(model_names) - set(existing_models))
253
254 return (
255 len(non_existing_models) == 0,
256 non_existing_models,
257 )
258
259 async def get_model_status(self, model_name: str) -> FullStatus:
260 """
261 Get model status
262
263 :param: model_name: Model name
264
265 :return: Full status object
266 """
267 controller = await self.get_controller()
268 model = await self.get_model(controller, model_name)
269 try:
270 return await model.get_status()
271 finally:
272 await self.disconnect_model(model)
273 await self.disconnect_controller(controller)
274
275 async def create_machine(
276 self,
277 model_name: str,
278 machine_id: str = None,
279 db_dict: dict = None,
280 progress_timeout: float = None,
281 total_timeout: float = None,
282 series: str = "xenial",
283 wait: bool = True,
284 ) -> (Machine, bool):
285 """
286 Create machine
287
288 :param: model_name: Model name
289 :param: machine_id: Machine id
290 :param: db_dict: Dictionary with data of the DB to write the updates
291 :param: progress_timeout: Maximum time between two updates in the model
292 :param: total_timeout: Timeout for the entity to be active
293 :param: series: Series of the machine (xenial, bionic, focal, ...)
294 :param: wait: Wait until machine is ready
295
296 :return: (juju.machine.Machine, bool): Machine object and a boolean saying
297 if the machine is new or it already existed
298 """
299 new = False
300 machine = None
301
302 self.log.debug(
303 "Creating machine (id={}) in model: {}".format(machine_id, model_name)
304 )
305
306 # Get controller
307 controller = await self.get_controller()
308
309 # Get model
310 model = await self.get_model(controller, model_name)
311 try:
312 if machine_id is not None:
313 self.log.debug(
314 "Searching machine (id={}) in model {}".format(
315 machine_id, model_name
316 )
317 )
318
319 # Get machines from model and get the machine with machine_id if exists
320 machines = await model.get_machines()
321 if machine_id in machines:
322 self.log.debug(
323 "Machine (id={}) found in model {}".format(
324 machine_id, model_name
325 )
326 )
327 machine = machines[machine_id]
328 else:
329 raise JujuMachineNotFound("Machine {} not found".format(machine_id))
330
331 if machine is None:
332 self.log.debug("Creating a new machine in model {}".format(model_name))
333
334 # Create machine
335 machine = await model.add_machine(
336 spec=None, constraints=None, disks=None, series=series
337 )
338 new = True
339
340 # Wait until the machine is ready
341 self.log.debug(
342 "Wait until machine {} is ready in model {}".format(
343 machine.entity_id, model_name
344 )
345 )
346 if wait:
347 await JujuModelWatcher.wait_for(
348 model=model,
349 entity=machine,
350 progress_timeout=progress_timeout,
351 total_timeout=total_timeout,
352 db_dict=db_dict,
353 n2vc=self.n2vc,
354 )
355 finally:
356 await self.disconnect_model(model)
357 await self.disconnect_controller(controller)
358
359 self.log.debug(
360 "Machine {} ready at {} in model {}".format(
361 machine.entity_id, machine.dns_name, model_name
362 )
363 )
364 return machine, new
365
366 async def provision_machine(
367 self,
368 model_name: str,
369 hostname: str,
370 username: str,
371 private_key_path: str,
372 db_dict: dict = None,
373 progress_timeout: float = None,
374 total_timeout: float = None,
375 ) -> str:
376 """
377 Manually provisioning of a machine
378
379 :param: model_name: Model name
380 :param: hostname: IP to access the machine
381 :param: username: Username to login to the machine
382 :param: private_key_path: Local path for the private key
383 :param: db_dict: Dictionary with data of the DB to write the updates
384 :param: progress_timeout: Maximum time between two updates in the model
385 :param: total_timeout: Timeout for the entity to be active
386
387 :return: (Entity): Machine id
388 """
389 self.log.debug(
390 "Provisioning machine. model: {}, hostname: {}, username: {}".format(
391 model_name, hostname, username
392 )
393 )
394
395 # Get controller
396 controller = await self.get_controller()
397
398 # Get model
399 model = await self.get_model(controller, model_name)
400
401 try:
402 # Get provisioner
403 provisioner = AsyncSSHProvisioner(
404 host=hostname,
405 user=username,
406 private_key_path=private_key_path,
407 log=self.log,
408 )
409
410 # Provision machine
411 params = await provisioner.provision_machine()
412
413 params.jobs = ["JobHostUnits"]
414
415 self.log.debug("Adding machine to model")
416 connection = model.connection()
417 client_facade = client.ClientFacade.from_connection(connection)
418
419 results = await client_facade.AddMachines(params=[params])
420 error = results.machines[0].error
421
422 if error:
423 msg = "Error adding machine: {}".format(error.message)
424 self.log.error(msg=msg)
425 raise ValueError(msg)
426
427 machine_id = results.machines[0].machine
428
429 self.log.debug("Installing Juju agent into machine {}".format(machine_id))
430 asyncio.ensure_future(
431 provisioner.install_agent(
432 connection=connection,
433 nonce=params.nonce,
434 machine_id=machine_id,
435 proxy=self.api_proxy,
436 )
437 )
438
439 machine = None
440 for _ in range(10):
441 machine_list = await model.get_machines()
442 if machine_id in machine_list:
443 self.log.debug("Machine {} found in model!".format(machine_id))
444 machine = model.machines.get(machine_id)
445 break
446 await asyncio.sleep(2)
447
448 if machine is None:
449 msg = "Machine {} not found in model".format(machine_id)
450 self.log.error(msg=msg)
451 raise JujuMachineNotFound(msg)
452
453 self.log.debug(
454 "Wait until machine {} is ready in model {}".format(
455 machine.entity_id, model_name
456 )
457 )
458 await JujuModelWatcher.wait_for(
459 model=model,
460 entity=machine,
461 progress_timeout=progress_timeout,
462 total_timeout=total_timeout,
463 db_dict=db_dict,
464 n2vc=self.n2vc,
465 )
466 except Exception as e:
467 raise e
468 finally:
469 await self.disconnect_model(model)
470 await self.disconnect_controller(controller)
471
472 self.log.debug(
473 "Machine provisioned {} in model {}".format(machine_id, model_name)
474 )
475
476 return machine_id
477
478 async def deploy_charm(
479 self,
480 application_name: str,
481 path: str,
482 model_name: str,
483 machine_id: str,
484 db_dict: dict = None,
485 progress_timeout: float = None,
486 total_timeout: float = None,
487 config: dict = None,
488 series: str = None,
489 num_units: int = 1,
490 ):
491 """Deploy charm
492
493 :param: application_name: Application name
494 :param: path: Local path to the charm
495 :param: model_name: Model name
496 :param: machine_id ID of the machine
497 :param: db_dict: Dictionary with data of the DB to write the updates
498 :param: progress_timeout: Maximum time between two updates in the model
499 :param: total_timeout: Timeout for the entity to be active
500 :param: config: Config for the charm
501 :param: series: Series of the charm
502 :param: num_units: Number of units
503
504 :return: (juju.application.Application): Juju application
505 """
506 self.log.debug(
507 "Deploying charm {} to machine {} in model ~{}".format(
508 application_name, machine_id, model_name
509 )
510 )
511 self.log.debug("charm: {}".format(path))
512
513 # Get controller
514 controller = await self.get_controller()
515
516 # Get model
517 model = await self.get_model(controller, model_name)
518
519 try:
520 application = None
521 if application_name not in model.applications:
522
523 if machine_id is not None:
524 if machine_id not in model.machines:
525 msg = "Machine {} not found in model".format(machine_id)
526 self.log.error(msg=msg)
527 raise JujuMachineNotFound(msg)
528 machine = model.machines[machine_id]
529 series = machine.series
530
531 application = await model.deploy(
532 entity_url=path,
533 application_name=application_name,
534 channel="stable",
535 num_units=1,
536 series=series,
537 to=machine_id,
538 config=config,
539 )
540
541 self.log.debug(
542 "Wait until application {} is ready in model {}".format(
543 application_name, model_name
544 )
545 )
546 if num_units > 1:
547 for _ in range(num_units - 1):
548 m, _ = await self.create_machine(model_name, wait=False)
549 await application.add_unit(to=m.entity_id)
550
551 await JujuModelWatcher.wait_for(
552 model=model,
553 entity=application,
554 progress_timeout=progress_timeout,
555 total_timeout=total_timeout,
556 db_dict=db_dict,
557 n2vc=self.n2vc,
558 )
559 self.log.debug(
560 "Application {} is ready in model {}".format(
561 application_name, model_name
562 )
563 )
564 else:
565 raise JujuApplicationExists(
566 "Application {} exists".format(application_name)
567 )
568 finally:
569 await self.disconnect_model(model)
570 await self.disconnect_controller(controller)
571
572 return application
573
574 def _get_application(self, model: Model, application_name: str) -> Application:
575 """Get application
576
577 :param: model: Model object
578 :param: application_name: Application name
579
580 :return: juju.application.Application (or None if it doesn't exist)
581 """
582 if model.applications and application_name in model.applications:
583 return model.applications[application_name]
584
585 async def execute_action(
586 self,
587 application_name: str,
588 model_name: str,
589 action_name: str,
590 db_dict: dict = None,
591 progress_timeout: float = None,
592 total_timeout: float = None,
593 **kwargs
594 ):
595 """Execute action
596
597 :param: application_name: Application name
598 :param: model_name: Model name
599 :param: action_name: Name of the action
600 :param: db_dict: Dictionary with data of the DB to write the updates
601 :param: progress_timeout: Maximum time between two updates in the model
602 :param: total_timeout: Timeout for the entity to be active
603
604 :return: (str, str): (output and status)
605 """
606 self.log.debug(
607 "Executing action {} using params {}".format(action_name, kwargs)
608 )
609 # Get controller
610 controller = await self.get_controller()
611
612 # Get model
613 model = await self.get_model(controller, model_name)
614
615 try:
616 # Get application
617 application = self._get_application(
618 model, application_name=application_name,
619 )
620 if application is None:
621 raise JujuApplicationNotFound("Cannot execute action")
622
623 # Get unit
624 unit = None
625 for u in application.units:
626 if await u.is_leader_from_status():
627 unit = u
628 if unit is None:
629 raise JujuLeaderUnitNotFound(
630 "Cannot execute action: leader unit not found"
631 )
632
633 actions = await application.get_actions()
634
635 if action_name not in actions:
636 raise JujuActionNotFound(
637 "Action {} not in available actions".format(action_name)
638 )
639
640 action = await unit.run_action(action_name, **kwargs)
641
642 self.log.debug(
643 "Wait until action {} is completed in application {} (model={})".format(
644 action_name, application_name, model_name
645 )
646 )
647 await JujuModelWatcher.wait_for(
648 model=model,
649 entity=action,
650 progress_timeout=progress_timeout,
651 total_timeout=total_timeout,
652 db_dict=db_dict,
653 n2vc=self.n2vc,
654 )
655
656 output = await model.get_action_output(action_uuid=action.entity_id)
657 status = await model.get_action_status(uuid_or_prefix=action.entity_id)
658 status = (
659 status[action.entity_id] if action.entity_id in status else "failed"
660 )
661
662 self.log.debug(
663 "Action {} completed with status {} in application {} (model={})".format(
664 action_name, action.status, application_name, model_name
665 )
666 )
667 finally:
668 await self.disconnect_model(model)
669 await self.disconnect_controller(controller)
670
671 return output, status
672
673 async def get_actions(self, application_name: str, model_name: str) -> dict:
674 """Get list of actions
675
676 :param: application_name: Application name
677 :param: model_name: Model name
678
679 :return: Dict with this format
680 {
681 "action_name": "Description of the action",
682 ...
683 }
684 """
685 self.log.debug(
686 "Getting list of actions for application {}".format(application_name)
687 )
688
689 # Get controller
690 controller = await self.get_controller()
691
692 # Get model
693 model = await self.get_model(controller, model_name)
694
695 try:
696 # Get application
697 application = self._get_application(
698 model, application_name=application_name,
699 )
700
701 # Return list of actions
702 return await application.get_actions()
703
704 finally:
705 # Disconnect from model and controller
706 await self.disconnect_model(model)
707 await self.disconnect_controller(controller)
708
709 async def add_relation(
710 self,
711 model_name: str,
712 application_name_1: str,
713 application_name_2: str,
714 relation_1: str,
715 relation_2: str,
716 ):
717 """Add relation
718
719 :param: model_name: Model name
720 :param: application_name_1 First application name
721 :param: application_name_2: Second application name
722 :param: relation_1: First relation name
723 :param: relation_2: Second relation name
724 """
725
726 self.log.debug("Adding relation: {} -> {}".format(relation_1, relation_2))
727
728 # Get controller
729 controller = await self.get_controller()
730
731 # Get model
732 model = await self.get_model(controller, model_name)
733
734 # Build relation strings
735 r1 = "{}:{}".format(application_name_1, relation_1)
736 r2 = "{}:{}".format(application_name_2, relation_2)
737
738 # Add relation
739 try:
740 await model.add_relation(relation1=r1, relation2=r2)
741 except JujuAPIError as e:
742 if "not found" in e.message:
743 self.log.warning("Relation not found: {}".format(e.message))
744 return
745 if "already exists" in e.message:
746 self.log.warning("Relation already exists: {}".format(e.message))
747 return
748 # another exception, raise it
749 raise e
750 finally:
751 await self.disconnect_model(model)
752 await self.disconnect_controller(controller)
753
754 async def destroy_model(self, model_name: str, total_timeout: float):
755 """
756 Destroy model
757
758 :param: model_name: Model name
759 :param: total_timeout: Timeout
760 """
761
762 controller = await self.get_controller()
763 model = await self.get_model(controller, model_name)
764 try:
765 self.log.debug("Destroying model {}".format(model_name))
766 uuid = model.info.uuid
767
768 # Disconnect model
769 await self.disconnect_model(model)
770
771 # Destroy model
772 if model_name in self.models:
773 self.models.remove(model_name)
774
775 await controller.destroy_model(uuid, force=True, max_wait=0)
776
777 # Wait until model is destroyed
778 self.log.debug("Waiting for model {} to be destroyed...".format(model_name))
779
780 if total_timeout is None:
781 total_timeout = 3600
782 end = time.time() + total_timeout
783 while time.time() < end:
784 models = await controller.list_models()
785 if model_name not in models:
786 self.log.debug(
787 "The model {} ({}) was destroyed".format(model_name, uuid)
788 )
789 return
790 await asyncio.sleep(5)
791 raise Exception(
792 "Timeout waiting for model {} to be destroyed".format(model_name)
793 )
794 finally:
795 await self.disconnect_controller(controller)
796
797 async def destroy_application(self, model: Model, application_name: str):
798 """
799 Destroy application
800
801 :param: model: Model object
802 :param: application_name: Application name
803 """
804 self.log.debug(
805 "Destroying application {} in model {}".format(
806 application_name, model.info.name
807 )
808 )
809 application = model.applications.get(application_name)
810 if application:
811 await application.destroy()
812 else:
813 self.log.warning("Application not found: {}".format(application_name))
814
815 # async def destroy_machine(
816 # self, model: Model, machine_id: str, total_timeout: float = 3600
817 # ):
818 # """
819 # Destroy machine
820
821 # :param: model: Model object
822 # :param: machine_id: Machine id
823 # :param: total_timeout: Timeout in seconds
824 # """
825 # machines = await model.get_machines()
826 # if machine_id in machines:
827 # machine = machines[machine_id]
828 # await machine.destroy(force=True)
829 # # max timeout
830 # end = time.time() + total_timeout
831
832 # # wait for machine removal
833 # machines = await model.get_machines()
834 # while machine_id in machines and time.time() < end:
835 # self.log.debug("Waiting for machine {} is destroyed".format(machine_id))
836 # await asyncio.sleep(0.5)
837 # machines = await model.get_machines()
838 # self.log.debug("Machine destroyed: {}".format(machine_id))
839 # else:
840 # self.log.debug("Machine not found: {}".format(machine_id))
841
842 async def configure_application(
843 self, model_name: str, application_name: str, config: dict = None
844 ):
845 """Configure application
846
847 :param: model_name: Model name
848 :param: application_name: Application name
849 :param: config: Config to apply to the charm
850 """
851 self.log.debug("Configuring application {}".format(application_name))
852
853 if config:
854 try:
855 controller = await self.get_controller()
856 model = await self.get_model(controller, model_name)
857 application = self._get_application(
858 model, application_name=application_name,
859 )
860 await application.set_config(config)
861 finally:
862 await self.disconnect_model(model)
863 await self.disconnect_controller(controller)
864
865 def _get_api_endpoints_db(self) -> [str]:
866 """
867 Get API Endpoints from DB
868
869 :return: List of API endpoints
870 """
871 self.log.debug("Getting endpoints from database")
872
873 juju_info = self.db.get_one(
874 DB_DATA.api_endpoints.table,
875 q_filter=DB_DATA.api_endpoints.filter,
876 fail_on_empty=False,
877 )
878 if juju_info and DB_DATA.api_endpoints.key in juju_info:
879 return juju_info[DB_DATA.api_endpoints.key]
880
881 def _update_api_endpoints_db(self, endpoints: [str]):
882 """
883 Update API endpoints in Database
884
885 :param: List of endpoints
886 """
887 self.log.debug("Saving endpoints {} in database".format(endpoints))
888
889 juju_info = self.db.get_one(
890 DB_DATA.api_endpoints.table,
891 q_filter=DB_DATA.api_endpoints.filter,
892 fail_on_empty=False,
893 )
894 # If it doesn't, then create it
895 if not juju_info:
896 try:
897 self.db.create(
898 DB_DATA.api_endpoints.table, DB_DATA.api_endpoints.filter,
899 )
900 except DbException as e:
901 # Racing condition: check if another N2VC worker has created it
902 juju_info = self.db.get_one(
903 DB_DATA.api_endpoints.table,
904 q_filter=DB_DATA.api_endpoints.filter,
905 fail_on_empty=False,
906 )
907 if not juju_info:
908 raise e
909 self.db.set_one(
910 DB_DATA.api_endpoints.table,
911 DB_DATA.api_endpoints.filter,
912 {DB_DATA.api_endpoints.key: endpoints},
913 )
914
915 def handle_exception(self, loop, context):
916 # All unhandled exceptions by libjuju are handled here.
917 pass
918
919 async def health_check(self, interval: float = 300.0):
920 """
921 Health check to make sure controller and controller_model connections are OK
922
923 :param: interval: Time in seconds between checks
924 """
925 while True:
926 try:
927 controller = await self.get_controller()
928 # self.log.debug("VCA is alive")
929 except Exception as e:
930 self.log.error("Health check to VCA failed: {}".format(e))
931 finally:
932 await self.disconnect_controller(controller)
933 await asyncio.sleep(interval)
934
935 async def list_models(self, contains: str = None) -> [str]:
936 """List models with certain names
937
938 :param: contains: String that is contained in model name
939
940 :retur: [models] Returns list of model names
941 """
942
943 controller = await self.get_controller()
944 try:
945 models = await controller.list_models()
946 if contains:
947 models = [model for model in models if contains in model]
948 return models
949 finally:
950 await self.disconnect_controller(controller)