1 # Copyright 2020 Canonical Ltd.
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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.
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
.unit
import Unit
26 from juju
.client
._definitions
import (
28 QueryApplicationOffersResults
,
32 from juju
.controller
import Controller
33 from juju
.client
import client
36 from n2vc
.juju_watcher
import JujuModelWatcher
37 from n2vc
.provisioner
import AsyncSSHProvisioner
38 from n2vc
.n2vc_conn
import N2VCConnector
39 from n2vc
.exceptions
import (
41 JujuApplicationNotFound
,
42 JujuLeaderUnitNotFound
,
44 JujuControllerFailedConnecting
,
45 JujuApplicationExists
,
46 JujuInvalidK8sConfiguration
,
49 from n2vc
.vca
.cloud
import Cloud
as VcaCloud
50 from n2vc
.vca
.connection
import Connection
51 from kubernetes
.client
.configuration
import Configuration
52 from retrying_async
import retry
55 RBAC_LABEL_KEY_NAME
= "rbac-id"
61 vca_connection
: Connection
,
62 loop
: asyncio
.AbstractEventLoop
= None,
63 log
: logging
.Logger
= None,
64 n2vc
: N2VCConnector
= None,
69 :param: vca_connection: n2vc.vca.connection object
70 :param: loop: Asyncio loop
72 :param: n2vc: N2VC object
75 self
.log
= log
or logging
.getLogger("Libjuju")
77 self
.vca_connection
= vca_connection
79 self
.loop
= loop
or asyncio
.get_event_loop()
80 self
.loop
.set_exception_handler(self
.handle_exception
)
81 self
.creating_model
= asyncio
.Lock(loop
=self
.loop
)
83 if self
.vca_connection
.is_default
:
84 self
.health_check_task
= self
._create
_health
_check
_task
()
86 def _create_health_check_task(self
):
87 return self
.loop
.create_task(self
.health_check())
89 async def get_controller(self
, timeout
: float = 60.0) -> Controller
:
93 :param: timeout: Time in seconds to wait for controller to connect
97 controller
= Controller(loop
=self
.loop
)
98 await asyncio
.wait_for(
100 endpoint
=self
.vca_connection
.data
.endpoints
,
101 username
=self
.vca_connection
.data
.user
,
102 password
=self
.vca_connection
.data
.secret
,
103 cacert
=self
.vca_connection
.data
.cacert
,
107 if self
.vca_connection
.is_default
:
108 endpoints
= await controller
.api_endpoints
110 endpoint
in self
.vca_connection
.endpoints
for endpoint
in endpoints
112 await self
.vca_connection
.update_endpoints(endpoints
)
114 except asyncio
.CancelledError
as e
:
116 except Exception as e
:
118 "Failed connecting to controller: {}... {}".format(
119 self
.vca_connection
.data
.endpoints
, e
123 await self
.disconnect_controller(controller
)
124 raise JujuControllerFailedConnecting(e
)
126 async def disconnect(self
):
128 # Cancel health check task
129 self
.health_check_task
.cancel()
130 self
.log
.debug("Libjuju disconnected!")
132 async def disconnect_model(self
, model
: Model
):
136 :param: model: Model that will be disconnected
138 await model
.disconnect()
140 async def disconnect_controller(self
, controller
: Controller
):
142 Disconnect controller
144 :param: controller: Controller that will be disconnected
147 await controller
.disconnect()
149 @retry(attempts
=3, delay
=5, timeout
=None)
150 async def add_model(self
, model_name
: str, cloud
: VcaCloud
):
154 :param: model_name: Model name
155 :param: cloud: Cloud object
159 controller
= await self
.get_controller()
162 # Block until other workers have finished model creation
163 while self
.creating_model
.locked():
164 await asyncio
.sleep(0.1)
167 async with self
.creating_model
:
168 if await self
.model_exists(model_name
, controller
=controller
):
170 self
.log
.debug("Creating model {}".format(model_name
))
171 model
= await controller
.add_model(
173 config
=self
.vca_connection
.data
.model_config
,
174 cloud_name
=cloud
.name
,
175 credential_name
=cloud
.credential_name
,
177 except JujuAPIError
as e
:
178 if "already exists" in e
.message
:
184 await self
.disconnect_model(model
)
185 await self
.disconnect_controller(controller
)
187 async def get_executed_actions(self
, model_name
: str) -> list:
189 Get executed/history of actions for a model.
191 :param: model_name: Model name, str.
192 :return: List of executed actions for a model.
195 executed_actions
= []
196 controller
= await self
.get_controller()
198 model
= await self
.get_model(controller
, model_name
)
199 # Get all unique action names
201 for application
in model
.applications
:
202 application_actions
= await self
.get_actions(application
, model_name
)
203 actions
.update(application_actions
)
204 # Get status of all actions
205 for application_action
in actions
:
206 app_action_status_list
= await model
.get_action_status(
207 name
=application_action
209 for action_id
, action_status
in app_action_status_list
.items():
212 "action": application_action
,
213 "status": action_status
,
215 # Get action output by id
216 action_status
= await model
.get_action_output(executed_action
["id"])
217 for k
, v
in action_status
.items():
218 executed_action
[k
] = v
219 executed_actions
.append(executed_action
)
220 except Exception as e
:
222 "Error in getting executed actions for model: {}. Error: {}".format(
228 await self
.disconnect_model(model
)
229 await self
.disconnect_controller(controller
)
230 return executed_actions
232 async def get_application_configs(
233 self
, model_name
: str, application_name
: str
236 Get available configs for an application.
238 :param: model_name: Model name, str.
239 :param: application_name: Application name, str.
241 :return: A dict which has key - action name, value - action description
244 application_configs
= {}
245 controller
= await self
.get_controller()
247 model
= await self
.get_model(controller
, model_name
)
248 application
= self
._get
_application
(
249 model
, application_name
=application_name
251 application_configs
= await application
.get_config()
252 except Exception as e
:
254 "Error in getting configs for application: {} in model: {}. Error: {}".format(
255 application_name
, model_name
, str(e
)
260 await self
.disconnect_model(model
)
261 await self
.disconnect_controller(controller
)
262 return application_configs
264 @retry(attempts
=3, delay
=5)
265 async def get_model(self
, controller
: Controller
, model_name
: str) -> Model
:
267 Get model from controller
269 :param: controller: Controller
270 :param: model_name: Model name
272 :return: Model: The created Juju model object
274 return await controller
.get_model(model_name
)
276 async def model_exists(self
, model_name
: str, controller
: Controller
= None) -> bool:
278 Check if model exists
280 :param: controller: Controller
281 :param: model_name: Model name
285 need_to_disconnect
= False
287 # Get controller if not passed
289 controller
= await self
.get_controller()
290 need_to_disconnect
= True
292 # Check if model exists
294 return model_name
in await controller
.list_models()
296 if need_to_disconnect
:
297 await self
.disconnect_controller(controller
)
299 async def models_exist(self
, model_names
: [str]) -> (bool, list):
301 Check if models exists
303 :param: model_names: List of strings with model names
305 :return (bool, list[str]): (True if all models exists, List of model names that don't exist)
309 "model_names must be a non-empty array. Given value: {}".format(
313 non_existing_models
= []
314 models
= await self
.list_models()
315 existing_models
= list(set(models
).intersection(model_names
))
316 non_existing_models
= list(set(model_names
) - set(existing_models
))
319 len(non_existing_models
) == 0,
323 async def get_model_status(self
, model_name
: str) -> FullStatus
:
327 :param: model_name: Model name
329 :return: Full status object
331 controller
= await self
.get_controller()
332 model
= await self
.get_model(controller
, model_name
)
334 return await model
.get_status()
336 await self
.disconnect_model(model
)
337 await self
.disconnect_controller(controller
)
339 async def create_machine(
342 machine_id
: str = None,
343 db_dict
: dict = None,
344 progress_timeout
: float = None,
345 total_timeout
: float = None,
346 series
: str = "xenial",
348 ) -> (Machine
, bool):
352 :param: model_name: Model name
353 :param: machine_id: Machine id
354 :param: db_dict: Dictionary with data of the DB to write the updates
355 :param: progress_timeout: Maximum time between two updates in the model
356 :param: total_timeout: Timeout for the entity to be active
357 :param: series: Series of the machine (xenial, bionic, focal, ...)
358 :param: wait: Wait until machine is ready
360 :return: (juju.machine.Machine, bool): Machine object and a boolean saying
361 if the machine is new or it already existed
367 "Creating machine (id={}) in model: {}".format(machine_id
, model_name
)
371 controller
= await self
.get_controller()
374 model
= await self
.get_model(controller
, model_name
)
376 if machine_id
is not None:
378 "Searching machine (id={}) in model {}".format(
379 machine_id
, model_name
383 # Get machines from model and get the machine with machine_id if exists
384 machines
= await model
.get_machines()
385 if machine_id
in machines
:
387 "Machine (id={}) found in model {}".format(
388 machine_id
, model_name
391 machine
= machines
[machine_id
]
393 raise JujuMachineNotFound("Machine {} not found".format(machine_id
))
396 self
.log
.debug("Creating a new machine in model {}".format(model_name
))
399 machine
= await model
.add_machine(
400 spec
=None, constraints
=None, disks
=None, series
=series
404 # Wait until the machine is ready
406 "Wait until machine {} is ready in model {}".format(
407 machine
.entity_id
, model_name
411 await JujuModelWatcher
.wait_for(
414 progress_timeout
=progress_timeout
,
415 total_timeout
=total_timeout
,
418 vca_id
=self
.vca_connection
._vca
_id
,
421 await self
.disconnect_model(model
)
422 await self
.disconnect_controller(controller
)
425 "Machine {} ready at {} in model {}".format(
426 machine
.entity_id
, machine
.dns_name
, model_name
431 async def provision_machine(
436 private_key_path
: str,
437 db_dict
: dict = None,
438 progress_timeout
: float = None,
439 total_timeout
: float = None,
442 Manually provisioning of a machine
444 :param: model_name: Model name
445 :param: hostname: IP to access the machine
446 :param: username: Username to login to the machine
447 :param: private_key_path: Local path for the private key
448 :param: db_dict: Dictionary with data of the DB to write the updates
449 :param: progress_timeout: Maximum time between two updates in the model
450 :param: total_timeout: Timeout for the entity to be active
452 :return: (Entity): Machine id
455 "Provisioning machine. model: {}, hostname: {}, username: {}".format(
456 model_name
, hostname
, username
461 controller
= await self
.get_controller()
464 model
= await self
.get_model(controller
, model_name
)
468 provisioner
= AsyncSSHProvisioner(
471 private_key_path
=private_key_path
,
476 params
= await provisioner
.provision_machine()
478 params
.jobs
= ["JobHostUnits"]
480 self
.log
.debug("Adding machine to model")
481 connection
= model
.connection()
482 client_facade
= client
.ClientFacade
.from_connection(connection
)
484 results
= await client_facade
.AddMachines(params
=[params
])
485 error
= results
.machines
[0].error
488 msg
= "Error adding machine: {}".format(error
.message
)
489 self
.log
.error(msg
=msg
)
490 raise ValueError(msg
)
492 machine_id
= results
.machines
[0].machine
494 self
.log
.debug("Installing Juju agent into machine {}".format(machine_id
))
495 asyncio
.ensure_future(
496 provisioner
.install_agent(
497 connection
=connection
,
499 machine_id
=machine_id
,
500 proxy
=self
.vca_connection
.data
.api_proxy
,
501 series
=params
.series
,
507 machine_list
= await model
.get_machines()
508 if machine_id
in machine_list
:
509 self
.log
.debug("Machine {} found in model!".format(machine_id
))
510 machine
= model
.machines
.get(machine_id
)
512 await asyncio
.sleep(2)
515 msg
= "Machine {} not found in model".format(machine_id
)
516 self
.log
.error(msg
=msg
)
517 raise JujuMachineNotFound(msg
)
520 "Wait until machine {} is ready in model {}".format(
521 machine
.entity_id
, model_name
524 await JujuModelWatcher
.wait_for(
527 progress_timeout
=progress_timeout
,
528 total_timeout
=total_timeout
,
531 vca_id
=self
.vca_connection
._vca
_id
,
533 except Exception as e
:
536 await self
.disconnect_model(model
)
537 await self
.disconnect_controller(controller
)
540 "Machine provisioned {} in model {}".format(machine_id
, model_name
)
546 self
, uri
: str, model_name
: str, wait
: bool = True, timeout
: float = 3600
549 Deploy bundle or charm: Similar to the juju CLI command `juju deploy`
551 :param: uri: Path or Charm Store uri in which the charm or bundle can be found
552 :param: model_name: Model name
553 :param: wait: Indicates whether to wait or not until all applications are active
554 :param: timeout: Time in seconds to wait until all applications are active
556 controller
= await self
.get_controller()
557 model
= await self
.get_model(controller
, model_name
)
559 await model
.deploy(uri
)
561 await JujuModelWatcher
.wait_for_model(model
, timeout
=timeout
)
562 self
.log
.debug("All units active in model {}".format(model_name
))
564 await self
.disconnect_model(model
)
565 await self
.disconnect_controller(controller
)
567 async def deploy_charm(
569 application_name
: str,
573 db_dict
: dict = None,
574 progress_timeout
: float = None,
575 total_timeout
: float = None,
582 :param: application_name: Application name
583 :param: path: Local path to the charm
584 :param: model_name: Model name
585 :param: machine_id ID of the machine
586 :param: db_dict: Dictionary with data of the DB to write the updates
587 :param: progress_timeout: Maximum time between two updates in the model
588 :param: total_timeout: Timeout for the entity to be active
589 :param: config: Config for the charm
590 :param: series: Series of the charm
591 :param: num_units: Number of units
593 :return: (juju.application.Application): Juju application
596 "Deploying charm {} to machine {} in model ~{}".format(
597 application_name
, machine_id
, model_name
600 self
.log
.debug("charm: {}".format(path
))
603 controller
= await self
.get_controller()
606 model
= await self
.get_model(controller
, model_name
)
610 if application_name
not in model
.applications
:
612 if machine_id
is not None:
613 if machine_id
not in model
.machines
:
614 msg
= "Machine {} not found in model".format(machine_id
)
615 self
.log
.error(msg
=msg
)
616 raise JujuMachineNotFound(msg
)
617 machine
= model
.machines
[machine_id
]
618 series
= machine
.series
620 application
= await model
.deploy(
622 application_name
=application_name
,
631 "Wait until application {} is ready in model {}".format(
632 application_name
, model_name
636 for _
in range(num_units
- 1):
637 m
, _
= await self
.create_machine(model_name
, wait
=False)
638 await application
.add_unit(to
=m
.entity_id
)
640 await JujuModelWatcher
.wait_for(
643 progress_timeout
=progress_timeout
,
644 total_timeout
=total_timeout
,
647 vca_id
=self
.vca_connection
._vca
_id
,
650 "Application {} is ready in model {}".format(
651 application_name
, model_name
655 raise JujuApplicationExists(
656 "Application {} exists".format(application_name
)
659 await self
.disconnect_model(model
)
660 await self
.disconnect_controller(controller
)
664 def _get_application(self
, model
: Model
, application_name
: str) -> Application
:
667 :param: model: Model object
668 :param: application_name: Application name
670 :return: juju.application.Application (or None if it doesn't exist)
672 if model
.applications
and application_name
in model
.applications
:
673 return model
.applications
[application_name
]
675 async def execute_action(
677 application_name
: str,
680 db_dict
: dict = None,
681 progress_timeout
: float = None,
682 total_timeout
: float = None,
687 :param: application_name: Application name
688 :param: model_name: Model name
689 :param: action_name: Name of the action
690 :param: db_dict: Dictionary with data of the DB to write the updates
691 :param: progress_timeout: Maximum time between two updates in the model
692 :param: total_timeout: Timeout for the entity to be active
694 :return: (str, str): (output and status)
697 "Executing action {} using params {}".format(action_name
, kwargs
)
700 controller
= await self
.get_controller()
703 model
= await self
.get_model(controller
, model_name
)
707 application
= self
._get
_application
(
709 application_name
=application_name
,
711 if application
is None:
712 raise JujuApplicationNotFound("Cannot execute action")
716 # Ocassionally, self._get_leader_unit() will return None
717 # because the leader elected hook has not been triggered yet.
718 # Therefore, we are doing some retries. If it happens again,
720 unit
= await self
._get
_leader
_unit
(application
)
722 actions
= await application
.get_actions()
724 if action_name
not in actions
:
725 raise JujuActionNotFound(
726 "Action {} not in available actions".format(action_name
)
729 action
= await unit
.run_action(action_name
, **kwargs
)
732 "Wait until action {} is completed in application {} (model={})".format(
733 action_name
, application_name
, model_name
736 await JujuModelWatcher
.wait_for(
739 progress_timeout
=progress_timeout
,
740 total_timeout
=total_timeout
,
743 vca_id
=self
.vca_connection
._vca
_id
,
746 output
= await model
.get_action_output(action_uuid
=action
.entity_id
)
747 status
= await model
.get_action_status(uuid_or_prefix
=action
.entity_id
)
749 status
[action
.entity_id
] if action
.entity_id
in status
else "failed"
753 "Action {} completed with status {} in application {} (model={})".format(
754 action_name
, action
.status
, application_name
, model_name
758 await self
.disconnect_model(model
)
759 await self
.disconnect_controller(controller
)
761 return output
, status
763 async def get_actions(self
, application_name
: str, model_name
: str) -> dict:
764 """Get list of actions
766 :param: application_name: Application name
767 :param: model_name: Model name
769 :return: Dict with this format
771 "action_name": "Description of the action",
776 "Getting list of actions for application {}".format(application_name
)
780 controller
= await self
.get_controller()
783 model
= await self
.get_model(controller
, model_name
)
787 application
= self
._get
_application
(
789 application_name
=application_name
,
792 # Return list of actions
793 return await application
.get_actions()
796 # Disconnect from model and controller
797 await self
.disconnect_model(model
)
798 await self
.disconnect_controller(controller
)
800 async def get_metrics(self
, model_name
: str, application_name
: str) -> dict:
801 """Get the metrics collected by the VCA.
803 :param model_name The name or unique id of the network service
804 :param application_name The name of the application
806 if not model_name
or not application_name
:
807 raise Exception("model_name and application_name must be non-empty strings")
809 controller
= await self
.get_controller()
810 model
= await self
.get_model(controller
, model_name
)
812 application
= self
._get
_application
(model
, application_name
)
813 if application
is not None:
814 metrics
= await application
.get_metrics()
816 self
.disconnect_model(model
)
817 self
.disconnect_controller(controller
)
820 async def add_relation(
828 :param: model_name: Model name
829 :param: endpoint_1 First endpoint name
830 ("app:endpoint" format or directly the saas name)
831 :param: endpoint_2: Second endpoint name (^ same format)
834 self
.log
.debug("Adding relation: {} -> {}".format(endpoint_1
, endpoint_2
))
837 controller
= await self
.get_controller()
840 model
= await self
.get_model(controller
, model_name
)
844 await model
.add_relation(endpoint_1
, endpoint_2
)
845 except JujuAPIError
as e
:
846 if "not found" in e
.message
:
847 self
.log
.warning("Relation not found: {}".format(e
.message
))
849 if "already exists" in e
.message
:
850 self
.log
.warning("Relation already exists: {}".format(e
.message
))
852 # another exception, raise it
855 await self
.disconnect_model(model
)
856 await self
.disconnect_controller(controller
)
864 Adds a remote offer to the model. Relations can be created later using "juju relate".
866 :param: offer_url: Offer Url
867 :param: model_name: Model name
869 :raises ParseError if there's a problem parsing the offer_url
870 :raises JujuError if remote offer includes and endpoint
871 :raises JujuAPIError if the operation is not successful
873 controller
= await self
.get_controller()
874 model
= await controller
.get_model(model_name
)
877 await model
.consume(offer_url
)
879 await self
.disconnect_model(model
)
880 await self
.disconnect_controller(controller
)
882 async def destroy_model(self
, model_name
: str, total_timeout
: float):
886 :param: model_name: Model name
887 :param: total_timeout: Timeout
890 controller
= await self
.get_controller()
893 if not await self
.model_exists(model_name
, controller
=controller
):
896 model
= await self
.get_model(controller
, model_name
)
897 self
.log
.debug("Destroying model {}".format(model_name
))
898 uuid
= model
.info
.uuid
900 # Destroy machines that are manually provisioned
901 # and still are in pending state
902 await self
._destroy
_pending
_machines
(model
, only_manual
=True)
905 await self
.disconnect_model(model
)
907 await controller
.destroy_model(uuid
, force
=True, max_wait
=0)
909 # Wait until model is destroyed
910 self
.log
.debug("Waiting for model {} to be destroyed...".format(model_name
))
912 if total_timeout
is None:
914 end
= time
.time() + total_timeout
915 while time
.time() < end
:
916 models
= await controller
.list_models()
917 if model_name
not in models
:
919 "The model {} ({}) was destroyed".format(model_name
, uuid
)
922 await asyncio
.sleep(5)
924 "Timeout waiting for model {} to be destroyed".format(model_name
)
926 except Exception as e
:
928 await self
.disconnect_model(model
)
931 await self
.disconnect_controller(controller
)
933 async def destroy_application(
934 self
, model_name
: str, application_name
: str, total_timeout
: float
939 :param: model_name: Model name
940 :param: application_name: Application name
941 :param: total_timeout: Timeout
944 controller
= await self
.get_controller()
948 model
= await self
.get_model(controller
, model_name
)
950 "Destroying application {} in model {}".format(
951 application_name
, model_name
954 application
= self
._get
_application
(model
, application_name
)
956 await application
.destroy()
958 self
.log
.warning("Application not found: {}".format(application_name
))
961 "Waiting for application {} to be destroyed in model {}...".format(
962 application_name
, model_name
965 if total_timeout
is None:
967 end
= time
.time() + total_timeout
968 while time
.time() < end
:
969 if not self
._get
_application
(model
, application_name
):
971 "The application {} was destroyed in model {} ".format(
972 application_name
, model_name
976 await asyncio
.sleep(5)
978 "Timeout waiting for application {} to be destroyed in model {}".format(
979 application_name
, model_name
983 if model
is not None:
984 await self
.disconnect_model(model
)
985 await self
.disconnect_controller(controller
)
987 async def _destroy_pending_machines(self
, model
: Model
, only_manual
: bool = False):
989 Destroy pending machines in a given model
991 :param: only_manual: Bool that indicates only manually provisioned
992 machines should be destroyed (if True), or that
993 all pending machines should be destroyed
995 status
= await model
.get_status()
996 for machine_id
in status
.machines
:
997 machine_status
= status
.machines
[machine_id
]
998 if machine_status
.agent_status
.status
== "pending":
999 if only_manual
and not machine_status
.instance_id
.startswith("manual:"):
1001 machine
= model
.machines
[machine_id
]
1002 await machine
.destroy(force
=True)
1004 async def configure_application(
1005 self
, model_name
: str, application_name
: str, config
: dict = None
1007 """Configure application
1009 :param: model_name: Model name
1010 :param: application_name: Application name
1011 :param: config: Config to apply to the charm
1013 self
.log
.debug("Configuring application {}".format(application_name
))
1016 controller
= await self
.get_controller()
1019 model
= await self
.get_model(controller
, model_name
)
1020 application
= self
._get
_application
(
1022 application_name
=application_name
,
1024 await application
.set_config(config
)
1027 await self
.disconnect_model(model
)
1028 await self
.disconnect_controller(controller
)
1030 def handle_exception(self
, loop
, context
):
1031 # All unhandled exceptions by libjuju are handled here.
1034 async def health_check(self
, interval
: float = 300.0):
1036 Health check to make sure controller and controller_model connections are OK
1038 :param: interval: Time in seconds between checks
1043 controller
= await self
.get_controller()
1044 # self.log.debug("VCA is alive")
1045 except Exception as e
:
1046 self
.log
.error("Health check to VCA failed: {}".format(e
))
1048 await self
.disconnect_controller(controller
)
1049 await asyncio
.sleep(interval
)
1051 async def list_models(self
, contains
: str = None) -> [str]:
1052 """List models with certain names
1054 :param: contains: String that is contained in model name
1056 :retur: [models] Returns list of model names
1059 controller
= await self
.get_controller()
1061 models
= await controller
.list_models()
1063 models
= [model
for model
in models
if contains
in model
]
1066 await self
.disconnect_controller(controller
)
1068 async def list_offers(self
, model_name
: str) -> QueryApplicationOffersResults
:
1069 """List models with certain names
1071 :param: model_name: Model name
1073 :return: Returns list of offers
1076 controller
= await self
.get_controller()
1078 return await controller
.list_offers(model_name
)
1080 await self
.disconnect_controller(controller
)
1087 client_cert_data
: str,
1088 configuration
: Configuration
,
1090 credential_name
: str = None,
1093 Add a Kubernetes cloud to the controller
1095 Similar to the `juju add-k8s` command in the CLI
1097 :param: name: Name for the K8s cloud
1098 :param: configuration: Kubernetes configuration object
1099 :param: storage_class: Storage Class to use in the cloud
1100 :param: credential_name: Storage Class to use in the cloud
1103 if not storage_class
:
1104 raise Exception("storage_class must be a non-empty string")
1106 raise Exception("name must be a non-empty string")
1107 if not configuration
:
1108 raise Exception("configuration must be provided")
1110 endpoint
= configuration
.host
1111 credential
= self
.get_k8s_cloud_credential(
1116 credential
.attrs
[RBAC_LABEL_KEY_NAME
] = rbac_id
1117 cloud
= client
.Cloud(
1119 auth_types
=[credential
.auth_type
],
1121 ca_certificates
=[client_cert_data
],
1123 "operator-storage": storage_class
,
1124 "workload-storage": storage_class
,
1128 return await self
.add_cloud(
1129 name
, cloud
, credential
, credential_name
=credential_name
1132 def get_k8s_cloud_credential(
1134 configuration
: Configuration
,
1135 client_cert_data
: str,
1137 ) -> client
.CloudCredential
:
1139 # TODO: Test with AKS
1140 key
= None # open(configuration.key_file, "r").read()
1141 username
= configuration
.username
1142 password
= configuration
.password
1144 if client_cert_data
:
1145 attrs
["ClientCertificateData"] = client_cert_data
1147 attrs
["ClientKeyData"] = key
1149 if username
or password
:
1150 raise JujuInvalidK8sConfiguration("Cannot set both token and user/pass")
1151 attrs
["Token"] = token
1155 auth_type
= "oauth2"
1156 if client_cert_data
:
1157 auth_type
= "oauth2withcert"
1159 raise JujuInvalidK8sConfiguration(
1160 "missing token for auth type {}".format(auth_type
)
1165 "credential for user {} has empty password".format(username
)
1167 attrs
["username"] = username
1168 attrs
["password"] = password
1169 if client_cert_data
:
1170 auth_type
= "userpasswithcert"
1172 auth_type
= "userpass"
1173 elif client_cert_data
and token
:
1174 auth_type
= "certificate"
1176 raise JujuInvalidK8sConfiguration("authentication method not supported")
1177 return client
.CloudCredential(auth_type
=auth_type
, attrs
=attrs
)
1179 async def add_cloud(
1183 credential
: CloudCredential
= None,
1184 credential_name
: str = None,
1187 Add cloud to the controller
1189 :param: name: Name of the cloud to be added
1190 :param: cloud: Cloud object
1191 :param: credential: CloudCredentials object for the cloud
1192 :param: credential_name: Credential name.
1193 If not defined, cloud of the name will be used.
1195 controller
= await self
.get_controller()
1197 _
= await controller
.add_cloud(name
, cloud
)
1199 await controller
.add_credential(
1200 credential_name
or name
, credential
=credential
, cloud
=name
1202 # Need to return the object returned by the controller.add_cloud() function
1203 # I'm returning the original value now until this bug is fixed:
1204 # https://github.com/juju/python-libjuju/issues/443
1207 await self
.disconnect_controller(controller
)
1209 async def remove_cloud(self
, name
: str):
1213 :param: name: Name of the cloud to be removed
1215 controller
= await self
.get_controller()
1217 await controller
.remove_cloud(name
)
1219 await self
.disconnect_controller(controller
)
1221 @retry(attempts
=20, delay
=5, fallback
=JujuLeaderUnitNotFound())
1222 async def _get_leader_unit(self
, application
: Application
) -> Unit
:
1224 for u
in application
.units
:
1225 if await u
.is_leader_from_status():
1232 async def get_cloud_credentials(self
, cloud
: Cloud
) -> typing
.List
:
1234 Get cloud credentials
1236 :param: cloud: Cloud object. The returned credentials will be from this cloud.
1238 :return: List of credentials object associated to the specified cloud
1241 controller
= await self
.get_controller()
1243 facade
= client
.CloudFacade
.from_connection(controller
.connection())
1244 cloud_cred_tag
= tag
.credential(
1245 cloud
.name
, self
.vca_connection
.data
.user
, cloud
.credential_name
1247 params
= [client
.Entity(cloud_cred_tag
)]
1248 return (await facade
.Credential(params
)).results
1250 await self
.disconnect_controller(controller
)