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.
20 from juju
.errors
import JujuAPIError
21 from juju
.model
import Model
22 from juju
.machine
import Machine
23 from juju
.application
import Application
24 from juju
.unit
import Unit
25 from juju
.client
._definitions
import (
27 QueryApplicationOffersResults
,
31 from juju
.controller
import Controller
32 from juju
.client
import client
35 from n2vc
.juju_watcher
import JujuModelWatcher
36 from n2vc
.provisioner
import AsyncSSHProvisioner
37 from n2vc
.n2vc_conn
import N2VCConnector
38 from n2vc
.exceptions
import (
40 JujuApplicationNotFound
,
41 JujuLeaderUnitNotFound
,
43 JujuControllerFailedConnecting
,
44 JujuApplicationExists
,
45 JujuInvalidK8sConfiguration
,
47 from n2vc
.utils
import DB_DATA
48 from osm_common
.dbbase
import DbException
49 from kubernetes
.client
.configuration
import Configuration
51 RBAC_LABEL_KEY_NAME
= "rbac-id"
62 loop
: asyncio
.AbstractEventLoop
= None,
63 log
: logging
.Logger
= None,
65 n2vc
: N2VCConnector
= None,
66 apt_mirror
: str = None,
67 enable_os_upgrade
: bool = True,
72 :param: endpoint: Endpoint of the juju controller (host:port)
73 :param: api_proxy: Endpoint of the juju controller - Reachable from the VNFs
74 :param: username: Juju username
75 :param: password: Juju password
76 :param: cacert: Juju CA Certificate
77 :param: loop: Asyncio loop
80 :param: n2vc: N2VC object
81 :param: apt_mirror: APT Mirror
82 :param: enable_os_upgrade: Enable OS Upgrade
85 self
.log
= log
or logging
.getLogger("Libjuju")
87 db_endpoints
= self
._get
_api
_endpoints
_db
()
89 if (db_endpoints
and endpoint
not in db_endpoints
) or not db_endpoints
:
90 self
.endpoints
= [endpoint
]
91 self
._update
_api
_endpoints
_db
(self
.endpoints
)
93 self
.endpoints
= db_endpoints
94 self
.api_proxy
= api_proxy
95 self
.username
= username
96 self
.password
= password
98 self
.loop
= loop
or asyncio
.get_event_loop()
101 # Generate config for models
102 self
.model_config
= {}
104 self
.model_config
["apt-mirror"] = apt_mirror
105 self
.model_config
["enable-os-refresh-update"] = enable_os_upgrade
106 self
.model_config
["enable-os-upgrade"] = enable_os_upgrade
108 self
.loop
.set_exception_handler(self
.handle_exception
)
109 self
.creating_model
= asyncio
.Lock(loop
=self
.loop
)
111 self
.log
.debug("Libjuju initialized!")
113 self
.health_check_task
= self
._create
_health
_check
_task
()
115 def _create_health_check_task(self
):
116 return self
.loop
.create_task(self
.health_check())
118 async def get_controller(self
, timeout
: float = 15.0) -> Controller
:
122 :param: timeout: Time in seconds to wait for controller to connect
126 controller
= Controller(loop
=self
.loop
)
127 await asyncio
.wait_for(
129 endpoint
=self
.endpoints
,
130 username
=self
.username
,
131 password
=self
.password
,
136 endpoints
= await controller
.api_endpoints
137 if self
.endpoints
!= endpoints
:
138 self
.endpoints
= endpoints
139 self
._update
_api
_endpoints
_db
(self
.endpoints
)
141 except asyncio
.CancelledError
as e
:
143 except Exception as e
:
145 "Failed connecting to controller: {}...".format(self
.endpoints
)
148 await self
.disconnect_controller(controller
)
149 raise JujuControllerFailedConnecting(e
)
151 async def disconnect(self
):
153 # Cancel health check task
154 self
.health_check_task
.cancel()
155 self
.log
.debug("Libjuju disconnected!")
157 async def disconnect_model(self
, model
: Model
):
161 :param: model: Model that will be disconnected
163 await model
.disconnect()
165 async def disconnect_controller(self
, controller
: Controller
):
167 Disconnect controller
169 :param: controller: Controller that will be disconnected
172 await controller
.disconnect()
174 async def add_model(self
, model_name
: str, cloud_name
: str, credential_name
=None):
178 :param: model_name: Model name
179 :param: cloud_name: Cloud name
180 :param: credential_name: Credential name to use for adding the model
181 If not specified, same name as the cloud will be used.
185 controller
= await self
.get_controller()
188 # Block until other workers have finished model creation
189 while self
.creating_model
.locked():
190 await asyncio
.sleep(0.1)
193 async with self
.creating_model
:
194 if await self
.model_exists(model_name
, controller
=controller
):
196 self
.log
.debug("Creating model {}".format(model_name
))
197 model
= await controller
.add_model(
199 config
=self
.model_config
,
200 cloud_name
=cloud_name
,
201 credential_name
=credential_name
or cloud_name
,
205 await self
.disconnect_model(model
)
206 await self
.disconnect_controller(controller
)
209 self
, controller
: Controller
, model_name
: str, id=None
212 Get model from controller
214 :param: controller: Controller
215 :param: model_name: Model name
217 :return: Model: The created Juju model object
219 return await controller
.get_model(model_name
)
221 async def model_exists(
222 self
, model_name
: str, controller
: Controller
= None
225 Check if model exists
227 :param: controller: Controller
228 :param: model_name: Model name
232 need_to_disconnect
= False
234 # Get controller if not passed
236 controller
= await self
.get_controller()
237 need_to_disconnect
= True
239 # Check if model exists
241 return model_name
in await controller
.list_models()
243 if need_to_disconnect
:
244 await self
.disconnect_controller(controller
)
246 async def models_exist(self
, model_names
: [str]) -> (bool, list):
248 Check if models exists
250 :param: model_names: List of strings with model names
252 :return (bool, list[str]): (True if all models exists, List of model names that don't exist)
256 "model_names must be a non-empty array. Given value: {}".format(
260 non_existing_models
= []
261 models
= await self
.list_models()
262 existing_models
= list(set(models
).intersection(model_names
))
263 non_existing_models
= list(set(model_names
) - set(existing_models
))
266 len(non_existing_models
) == 0,
270 async def get_model_status(self
, model_name
: str) -> FullStatus
:
274 :param: model_name: Model name
276 :return: Full status object
278 controller
= await self
.get_controller()
279 model
= await self
.get_model(controller
, model_name
)
281 return await model
.get_status()
283 await self
.disconnect_model(model
)
284 await self
.disconnect_controller(controller
)
286 async def create_machine(
289 machine_id
: str = None,
290 db_dict
: dict = None,
291 progress_timeout
: float = None,
292 total_timeout
: float = None,
293 series
: str = "xenial",
295 ) -> (Machine
, bool):
299 :param: model_name: Model name
300 :param: machine_id: Machine id
301 :param: db_dict: Dictionary with data of the DB to write the updates
302 :param: progress_timeout: Maximum time between two updates in the model
303 :param: total_timeout: Timeout for the entity to be active
304 :param: series: Series of the machine (xenial, bionic, focal, ...)
305 :param: wait: Wait until machine is ready
307 :return: (juju.machine.Machine, bool): Machine object and a boolean saying
308 if the machine is new or it already existed
314 "Creating machine (id={}) in model: {}".format(machine_id
, model_name
)
318 controller
= await self
.get_controller()
321 model
= await self
.get_model(controller
, model_name
)
323 if machine_id
is not None:
325 "Searching machine (id={}) in model {}".format(
326 machine_id
, model_name
330 # Get machines from model and get the machine with machine_id if exists
331 machines
= await model
.get_machines()
332 if machine_id
in machines
:
334 "Machine (id={}) found in model {}".format(
335 machine_id
, model_name
338 machine
= machines
[machine_id
]
340 raise JujuMachineNotFound("Machine {} not found".format(machine_id
))
343 self
.log
.debug("Creating a new machine in model {}".format(model_name
))
346 machine
= await model
.add_machine(
347 spec
=None, constraints
=None, disks
=None, series
=series
351 # Wait until the machine is ready
353 "Wait until machine {} is ready in model {}".format(
354 machine
.entity_id
, model_name
358 await JujuModelWatcher
.wait_for(
361 progress_timeout
=progress_timeout
,
362 total_timeout
=total_timeout
,
367 await self
.disconnect_model(model
)
368 await self
.disconnect_controller(controller
)
371 "Machine {} ready at {} in model {}".format(
372 machine
.entity_id
, machine
.dns_name
, model_name
377 async def provision_machine(
382 private_key_path
: str,
383 db_dict
: dict = None,
384 progress_timeout
: float = None,
385 total_timeout
: float = None,
388 Manually provisioning of a machine
390 :param: model_name: Model name
391 :param: hostname: IP to access the machine
392 :param: username: Username to login to the machine
393 :param: private_key_path: Local path for the private key
394 :param: db_dict: Dictionary with data of the DB to write the updates
395 :param: progress_timeout: Maximum time between two updates in the model
396 :param: total_timeout: Timeout for the entity to be active
398 :return: (Entity): Machine id
401 "Provisioning machine. model: {}, hostname: {}, username: {}".format(
402 model_name
, hostname
, username
407 controller
= await self
.get_controller()
410 model
= await self
.get_model(controller
, model_name
)
414 provisioner
= AsyncSSHProvisioner(
417 private_key_path
=private_key_path
,
422 params
= await provisioner
.provision_machine()
424 params
.jobs
= ["JobHostUnits"]
426 self
.log
.debug("Adding machine to model")
427 connection
= model
.connection()
428 client_facade
= client
.ClientFacade
.from_connection(connection
)
430 results
= await client_facade
.AddMachines(params
=[params
])
431 error
= results
.machines
[0].error
434 msg
= "Error adding machine: {}".format(error
.message
)
435 self
.log
.error(msg
=msg
)
436 raise ValueError(msg
)
438 machine_id
= results
.machines
[0].machine
440 self
.log
.debug("Installing Juju agent into machine {}".format(machine_id
))
441 asyncio
.ensure_future(
442 provisioner
.install_agent(
443 connection
=connection
,
445 machine_id
=machine_id
,
446 proxy
=self
.api_proxy
,
452 machine_list
= await model
.get_machines()
453 if machine_id
in machine_list
:
454 self
.log
.debug("Machine {} found in model!".format(machine_id
))
455 machine
= model
.machines
.get(machine_id
)
457 await asyncio
.sleep(2)
460 msg
= "Machine {} not found in model".format(machine_id
)
461 self
.log
.error(msg
=msg
)
462 raise JujuMachineNotFound(msg
)
465 "Wait until machine {} is ready in model {}".format(
466 machine
.entity_id
, model_name
469 await JujuModelWatcher
.wait_for(
472 progress_timeout
=progress_timeout
,
473 total_timeout
=total_timeout
,
477 except Exception as e
:
480 await self
.disconnect_model(model
)
481 await self
.disconnect_controller(controller
)
484 "Machine provisioned {} in model {}".format(machine_id
, model_name
)
490 self
, uri
: str, model_name
: str, wait
: bool = True, timeout
: float = 3600
493 Deploy bundle or charm: Similar to the juju CLI command `juju deploy`
495 :param: uri: Path or Charm Store uri in which the charm or bundle can be found
496 :param: model_name: Model name
497 :param: wait: Indicates whether to wait or not until all applications are active
498 :param: timeout: Time in seconds to wait until all applications are active
500 controller
= await self
.get_controller()
501 model
= await self
.get_model(controller
, model_name
)
503 await model
.deploy(uri
)
505 await JujuModelWatcher
.wait_for_model(model
, timeout
=timeout
)
506 self
.log
.debug("All units active in model {}".format(model_name
))
508 await self
.disconnect_model(model
)
509 await self
.disconnect_controller(controller
)
511 async def deploy_charm(
513 application_name
: str,
517 db_dict
: dict = None,
518 progress_timeout
: float = None,
519 total_timeout
: float = None,
526 :param: application_name: Application name
527 :param: path: Local path to the charm
528 :param: model_name: Model name
529 :param: machine_id ID of the machine
530 :param: db_dict: Dictionary with data of the DB to write the updates
531 :param: progress_timeout: Maximum time between two updates in the model
532 :param: total_timeout: Timeout for the entity to be active
533 :param: config: Config for the charm
534 :param: series: Series of the charm
535 :param: num_units: Number of units
537 :return: (juju.application.Application): Juju application
540 "Deploying charm {} to machine {} in model ~{}".format(
541 application_name
, machine_id
, model_name
544 self
.log
.debug("charm: {}".format(path
))
547 controller
= await self
.get_controller()
550 model
= await self
.get_model(controller
, model_name
)
554 if application_name
not in model
.applications
:
556 if machine_id
is not None:
557 if machine_id
not in model
.machines
:
558 msg
= "Machine {} not found in model".format(machine_id
)
559 self
.log
.error(msg
=msg
)
560 raise JujuMachineNotFound(msg
)
561 machine
= model
.machines
[machine_id
]
562 series
= machine
.series
564 application
= await model
.deploy(
566 application_name
=application_name
,
575 "Wait until application {} is ready in model {}".format(
576 application_name
, model_name
580 for _
in range(num_units
- 1):
581 m
, _
= await self
.create_machine(model_name
, wait
=False)
582 await application
.add_unit(to
=m
.entity_id
)
584 await JujuModelWatcher
.wait_for(
587 progress_timeout
=progress_timeout
,
588 total_timeout
=total_timeout
,
593 "Application {} is ready in model {}".format(
594 application_name
, model_name
598 raise JujuApplicationExists(
599 "Application {} exists".format(application_name
)
602 await self
.disconnect_model(model
)
603 await self
.disconnect_controller(controller
)
607 def _get_application(self
, model
: Model
, application_name
: str) -> Application
:
610 :param: model: Model object
611 :param: application_name: Application name
613 :return: juju.application.Application (or None if it doesn't exist)
615 if model
.applications
and application_name
in model
.applications
:
616 return model
.applications
[application_name
]
618 async def execute_action(
620 application_name
: str,
623 db_dict
: dict = None,
624 progress_timeout
: float = None,
625 total_timeout
: float = None,
630 :param: application_name: Application name
631 :param: model_name: Model name
632 :param: action_name: Name of the action
633 :param: db_dict: Dictionary with data of the DB to write the updates
634 :param: progress_timeout: Maximum time between two updates in the model
635 :param: total_timeout: Timeout for the entity to be active
637 :return: (str, str): (output and status)
640 "Executing action {} using params {}".format(action_name
, kwargs
)
643 controller
= await self
.get_controller()
646 model
= await self
.get_model(controller
, model_name
)
650 application
= self
._get
_application
(
652 application_name
=application_name
,
654 if application
is None:
655 raise JujuApplicationNotFound("Cannot execute action")
659 # Ocassionally, self._get_leader_unit() will return None
660 # because the leader elected hook has not been triggered yet.
661 # Therefore, we are doing some retries. If it happens again,
664 time_between_retries
= 10
666 for _
in range(attempts
):
667 unit
= await self
._get
_leader
_unit
(application
)
669 await asyncio
.sleep(time_between_retries
)
673 raise JujuLeaderUnitNotFound(
674 "Cannot execute action: leader unit not found"
677 actions
= await application
.get_actions()
679 if action_name
not in actions
:
680 raise JujuActionNotFound(
681 "Action {} not in available actions".format(action_name
)
684 action
= await unit
.run_action(action_name
, **kwargs
)
687 "Wait until action {} is completed in application {} (model={})".format(
688 action_name
, application_name
, model_name
691 await JujuModelWatcher
.wait_for(
694 progress_timeout
=progress_timeout
,
695 total_timeout
=total_timeout
,
700 output
= await model
.get_action_output(action_uuid
=action
.entity_id
)
701 status
= await model
.get_action_status(uuid_or_prefix
=action
.entity_id
)
703 status
[action
.entity_id
] if action
.entity_id
in status
else "failed"
707 "Action {} completed with status {} in application {} (model={})".format(
708 action_name
, action
.status
, application_name
, model_name
712 await self
.disconnect_model(model
)
713 await self
.disconnect_controller(controller
)
715 return output
, status
717 async def get_actions(self
, application_name
: str, model_name
: str) -> dict:
718 """Get list of actions
720 :param: application_name: Application name
721 :param: model_name: Model name
723 :return: Dict with this format
725 "action_name": "Description of the action",
730 "Getting list of actions for application {}".format(application_name
)
734 controller
= await self
.get_controller()
737 model
= await self
.get_model(controller
, model_name
)
741 application
= self
._get
_application
(
743 application_name
=application_name
,
746 # Return list of actions
747 return await application
.get_actions()
750 # Disconnect from model and controller
751 await self
.disconnect_model(model
)
752 await self
.disconnect_controller(controller
)
754 async def get_metrics(self
, model_name
: str, application_name
: str) -> dict:
755 """Get the metrics collected by the VCA.
757 :param model_name The name or unique id of the network service
758 :param application_name The name of the application
760 if not model_name
or not application_name
:
761 raise Exception("model_name and application_name must be non-empty strings")
763 controller
= await self
.get_controller()
764 model
= await self
.get_model(controller
, model_name
)
766 application
= self
._get
_application
(model
, application_name
)
767 if application
is not None:
768 metrics
= await application
.get_metrics()
770 self
.disconnect_model(model
)
771 self
.disconnect_controller(controller
)
774 async def add_relation(
782 :param: model_name: Model name
783 :param: endpoint_1 First endpoint name
784 ("app:endpoint" format or directly the saas name)
785 :param: endpoint_2: Second endpoint name (^ same format)
788 self
.log
.debug("Adding relation: {} -> {}".format(endpoint_1
, endpoint_2
))
791 controller
= await self
.get_controller()
794 model
= await self
.get_model(controller
, model_name
)
798 await model
.add_relation(endpoint_1
, endpoint_2
)
799 except JujuAPIError
as e
:
800 if "not found" in e
.message
:
801 self
.log
.warning("Relation not found: {}".format(e
.message
))
803 if "already exists" in e
.message
:
804 self
.log
.warning("Relation already exists: {}".format(e
.message
))
806 # another exception, raise it
809 await self
.disconnect_model(model
)
810 await self
.disconnect_controller(controller
)
818 Adds a remote offer to the model. Relations can be created later using "juju relate".
820 :param: offer_url: Offer Url
821 :param: model_name: Model name
823 :raises ParseError if there's a problem parsing the offer_url
824 :raises JujuError if remote offer includes and endpoint
825 :raises JujuAPIError if the operation is not successful
827 controller
= await self
.get_controller()
828 model
= await controller
.get_model(model_name
)
831 await model
.consume(offer_url
)
833 await self
.disconnect_model(model
)
834 await self
.disconnect_controller(controller
)
836 async def destroy_model(self
, model_name
: str, total_timeout
: float):
840 :param: model_name: Model name
841 :param: total_timeout: Timeout
844 controller
= await self
.get_controller()
847 if not await self
.model_exists(model_name
, controller
=controller
):
850 model
= await self
.get_model(controller
, model_name
)
851 self
.log
.debug("Destroying model {}".format(model_name
))
852 uuid
= model
.info
.uuid
854 # Destroy machines that are manually provisioned
855 # and still are in pending state
856 await self
._destroy
_pending
_machines
(model
, only_manual
=True)
859 await self
.disconnect_model(model
)
861 await controller
.destroy_model(uuid
, force
=True, max_wait
=0)
863 # Wait until model is destroyed
864 self
.log
.debug("Waiting for model {} to be destroyed...".format(model_name
))
866 if total_timeout
is None:
868 end
= time
.time() + total_timeout
869 while time
.time() < end
:
870 models
= await controller
.list_models()
871 if model_name
not in models
:
873 "The model {} ({}) was destroyed".format(model_name
, uuid
)
876 await asyncio
.sleep(5)
878 "Timeout waiting for model {} to be destroyed".format(model_name
)
880 except Exception as e
:
882 await self
.disconnect_model(model
)
885 await self
.disconnect_controller(controller
)
887 async def destroy_application(self
, model
: Model
, application_name
: str):
891 :param: model: Model object
892 :param: application_name: Application name
895 "Destroying application {} in model {}".format(
896 application_name
, model
.info
.name
899 application
= model
.applications
.get(application_name
)
901 await application
.destroy()
903 self
.log
.warning("Application not found: {}".format(application_name
))
905 async def _destroy_pending_machines(self
, model
: Model
, only_manual
: bool = False):
907 Destroy pending machines in a given model
909 :param: only_manual: Bool that indicates only manually provisioned
910 machines should be destroyed (if True), or that
911 all pending machines should be destroyed
913 status
= await model
.get_status()
914 for machine_id
in status
.machines
:
915 machine_status
= status
.machines
[machine_id
]
916 if machine_status
.agent_status
.status
== "pending":
917 if only_manual
and not machine_status
.instance_id
.startswith("manual:"):
919 machine
= model
.machines
[machine_id
]
920 await machine
.destroy(force
=True)
922 # async def destroy_machine(
923 # self, model: Model, machine_id: str, total_timeout: float = 3600
928 # :param: model: Model object
929 # :param: machine_id: Machine id
930 # :param: total_timeout: Timeout in seconds
932 # machines = await model.get_machines()
933 # if machine_id in machines:
934 # machine = machines[machine_id]
935 # await machine.destroy(force=True)
937 # end = time.time() + total_timeout
939 # # wait for machine removal
940 # machines = await model.get_machines()
941 # while machine_id in machines and time.time() < end:
942 # self.log.debug("Waiting for machine {} is destroyed".format(machine_id))
943 # await asyncio.sleep(0.5)
944 # machines = await model.get_machines()
945 # self.log.debug("Machine destroyed: {}".format(machine_id))
947 # self.log.debug("Machine not found: {}".format(machine_id))
949 async def configure_application(
950 self
, model_name
: str, application_name
: str, config
: dict = None
952 """Configure application
954 :param: model_name: Model name
955 :param: application_name: Application name
956 :param: config: Config to apply to the charm
958 self
.log
.debug("Configuring application {}".format(application_name
))
961 controller
= await self
.get_controller()
964 model
= await self
.get_model(controller
, model_name
)
965 application
= self
._get
_application
(
967 application_name
=application_name
,
969 await application
.set_config(config
)
972 await self
.disconnect_model(model
)
973 await self
.disconnect_controller(controller
)
975 def _get_api_endpoints_db(self
) -> [str]:
977 Get API Endpoints from DB
979 :return: List of API endpoints
981 self
.log
.debug("Getting endpoints from database")
983 juju_info
= self
.db
.get_one(
984 DB_DATA
.api_endpoints
.table
,
985 q_filter
=DB_DATA
.api_endpoints
.filter,
988 if juju_info
and DB_DATA
.api_endpoints
.key
in juju_info
:
989 return juju_info
[DB_DATA
.api_endpoints
.key
]
991 def _update_api_endpoints_db(self
, endpoints
: [str]):
993 Update API endpoints in Database
995 :param: List of endpoints
997 self
.log
.debug("Saving endpoints {} in database".format(endpoints
))
999 juju_info
= self
.db
.get_one(
1000 DB_DATA
.api_endpoints
.table
,
1001 q_filter
=DB_DATA
.api_endpoints
.filter,
1002 fail_on_empty
=False,
1004 # If it doesn't, then create it
1008 DB_DATA
.api_endpoints
.table
,
1009 DB_DATA
.api_endpoints
.filter,
1011 except DbException
as e
:
1012 # Racing condition: check if another N2VC worker has created it
1013 juju_info
= self
.db
.get_one(
1014 DB_DATA
.api_endpoints
.table
,
1015 q_filter
=DB_DATA
.api_endpoints
.filter,
1016 fail_on_empty
=False,
1021 DB_DATA
.api_endpoints
.table
,
1022 DB_DATA
.api_endpoints
.filter,
1023 {DB_DATA
.api_endpoints
.key
: endpoints
},
1026 def handle_exception(self
, loop
, context
):
1027 # All unhandled exceptions by libjuju are handled here.
1030 async def health_check(self
, interval
: float = 300.0):
1032 Health check to make sure controller and controller_model connections are OK
1034 :param: interval: Time in seconds between checks
1039 controller
= await self
.get_controller()
1040 # self.log.debug("VCA is alive")
1041 except Exception as e
:
1042 self
.log
.error("Health check to VCA failed: {}".format(e
))
1044 await self
.disconnect_controller(controller
)
1045 await asyncio
.sleep(interval
)
1047 async def list_models(self
, contains
: str = None) -> [str]:
1048 """List models with certain names
1050 :param: contains: String that is contained in model name
1052 :retur: [models] Returns list of model names
1055 controller
= await self
.get_controller()
1057 models
= await controller
.list_models()
1059 models
= [model
for model
in models
if contains
in model
]
1062 await self
.disconnect_controller(controller
)
1064 async def list_offers(self
, model_name
: str) -> QueryApplicationOffersResults
:
1065 """List models with certain names
1067 :param: model_name: Model name
1069 :return: Returns list of offers
1072 controller
= await self
.get_controller()
1074 return await controller
.list_offers(model_name
)
1076 await self
.disconnect_controller(controller
)
1083 client_cert_data
: str,
1084 configuration
: Configuration
,
1086 credential_name
: str = None,
1089 Add a Kubernetes cloud to the controller
1091 Similar to the `juju add-k8s` command in the CLI
1093 :param: name: Name for the K8s cloud
1094 :param: configuration: Kubernetes configuration object
1095 :param: storage_class: Storage Class to use in the cloud
1096 :param: credential_name: Storage Class to use in the cloud
1099 if not storage_class
:
1100 raise Exception("storage_class must be a non-empty string")
1102 raise Exception("name must be a non-empty string")
1103 if not configuration
:
1104 raise Exception("configuration must be provided")
1106 endpoint
= configuration
.host
1107 credential
= self
.get_k8s_cloud_credential(
1112 credential
.attrs
[RBAC_LABEL_KEY_NAME
] = rbac_id
1113 cloud
= client
.Cloud(
1115 auth_types
=[credential
.auth_type
],
1117 ca_certificates
=[client_cert_data
],
1119 "operator-storage": storage_class
,
1120 "workload-storage": storage_class
,
1124 return await self
.add_cloud(
1125 name
, cloud
, credential
, credential_name
=credential_name
1128 def get_k8s_cloud_credential(
1130 configuration
: Configuration
,
1131 client_cert_data
: str,
1133 ) -> client
.CloudCredential
:
1135 # TODO: Test with AKS
1136 key
= None # open(configuration.key_file, "r").read()
1137 username
= configuration
.username
1138 password
= configuration
.password
1140 if client_cert_data
:
1141 attrs
["ClientCertificateData"] = client_cert_data
1143 attrs
["ClientKeyData"] = key
1145 if username
or password
:
1146 raise JujuInvalidK8sConfiguration("Cannot set both token and user/pass")
1147 attrs
["Token"] = token
1151 auth_type
= "oauth2"
1152 if client_cert_data
:
1153 auth_type
= "oauth2withcert"
1155 raise JujuInvalidK8sConfiguration(
1156 "missing token for auth type {}".format(auth_type
)
1161 "credential for user {} has empty password".format(username
)
1163 attrs
["username"] = username
1164 attrs
["password"] = password
1165 if client_cert_data
:
1166 auth_type
= "userpasswithcert"
1168 auth_type
= "userpass"
1169 elif client_cert_data
and token
:
1170 auth_type
= "certificate"
1172 raise JujuInvalidK8sConfiguration("authentication method not supported")
1173 return client
.CloudCredential(auth_type
=auth_type
, attrs
=attrs
)
1175 async def add_cloud(
1179 credential
: CloudCredential
= None,
1180 credential_name
: str = None,
1183 Add cloud to the controller
1185 :param: name: Name of the cloud to be added
1186 :param: cloud: Cloud object
1187 :param: credential: CloudCredentials object for the cloud
1188 :param: credential_name: Credential name.
1189 If not defined, cloud of the name will be used.
1191 controller
= await self
.get_controller()
1193 _
= await controller
.add_cloud(name
, cloud
)
1195 await controller
.add_credential(
1196 credential_name
or name
, credential
=credential
, cloud
=name
1198 # Need to return the object returned by the controller.add_cloud() function
1199 # I'm returning the original value now until this bug is fixed:
1200 # https://github.com/juju/python-libjuju/issues/443
1203 await self
.disconnect_controller(controller
)
1205 async def remove_cloud(self
, name
: str):
1209 :param: name: Name of the cloud to be removed
1211 controller
= await self
.get_controller()
1213 await controller
.remove_cloud(name
)
1215 await self
.disconnect_controller(controller
)
1217 async def _get_leader_unit(self
, application
: Application
) -> Unit
:
1219 for u
in application
.units
:
1220 if await u
.is_leader_from_status():
1225 async def get_cloud_credentials(self
, cloud_name
: str, credential_name
: str):
1226 controller
= await self
.get_controller()
1228 facade
= client
.CloudFacade
.from_connection(controller
.connection())
1229 cloud_cred_tag
= tag
.credential(cloud_name
, self
.username
, credential_name
)
1230 params
= [client
.Entity(cloud_cred_tag
)]
1231 return (await facade
.Credential(params
)).results
1233 await self
.disconnect_controller(controller
)