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.
17 from juju
.controller
import Controller
18 from juju
.client
import client
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 (
27 QueryApplicationOffersResults
,
31 from n2vc
.juju_watcher
import JujuModelWatcher
32 from n2vc
.provisioner
import AsyncSSHProvisioner
33 from n2vc
.n2vc_conn
import N2VCConnector
34 from n2vc
.exceptions
import (
36 JujuApplicationNotFound
,
37 JujuLeaderUnitNotFound
,
39 JujuModelAlreadyExists
,
40 JujuControllerFailedConnecting
,
41 JujuApplicationExists
,
43 from n2vc
.utils
import DB_DATA
44 from osm_common
.dbbase
import DbException
55 loop
: asyncio
.AbstractEventLoop
= None,
56 log
: logging
.Logger
= None,
58 n2vc
: N2VCConnector
= None,
59 apt_mirror
: str = None,
60 enable_os_upgrade
: bool = True,
65 :param: endpoint: Endpoint of the juju controller (host:port)
66 :param: api_proxy: Endpoint of the juju controller - Reachable from the VNFs
67 :param: username: Juju username
68 :param: password: Juju password
69 :param: cacert: Juju CA Certificate
70 :param: loop: Asyncio loop
73 :param: n2vc: N2VC object
74 :param: apt_mirror: APT Mirror
75 :param: enable_os_upgrade: Enable OS Upgrade
78 self
.log
= log
or logging
.getLogger("Libjuju")
80 db_endpoints
= self
._get
_api
_endpoints
_db
()
81 self
.endpoints
= db_endpoints
or [endpoint
]
82 if db_endpoints
is None:
83 self
._update
_api
_endpoints
_db
(self
.endpoints
)
84 self
.api_proxy
= api_proxy
85 self
.username
= username
86 self
.password
= password
88 self
.loop
= loop
or asyncio
.get_event_loop()
91 # Generate config for models
92 self
.model_config
= {}
94 self
.model_config
["apt-mirror"] = apt_mirror
95 self
.model_config
["enable-os-refresh-update"] = enable_os_upgrade
96 self
.model_config
["enable-os-upgrade"] = enable_os_upgrade
98 self
.loop
.set_exception_handler(self
.handle_exception
)
99 self
.creating_model
= asyncio
.Lock(loop
=self
.loop
)
102 self
.log
.debug("Libjuju initialized!")
104 self
.health_check_task
= self
.loop
.create_task(self
.health_check())
106 async def get_controller(self
, timeout
: float = 5.0) -> Controller
:
110 :param: timeout: Time in seconds to wait for controller to connect
114 controller
= Controller(loop
=self
.loop
)
115 await asyncio
.wait_for(
117 endpoint
=self
.endpoints
,
118 username
=self
.username
,
119 password
=self
.password
,
124 endpoints
= await controller
.api_endpoints
125 if self
.endpoints
!= endpoints
:
126 self
.endpoints
= endpoints
127 self
._update
_api
_endpoints
_db
(self
.endpoints
)
129 except asyncio
.CancelledError
as e
:
131 except Exception as e
:
133 "Failed connecting to controller: {}...".format(self
.endpoints
)
136 await self
.disconnect_controller(controller
)
137 raise JujuControllerFailedConnecting(e
)
139 async def disconnect(self
):
141 # Cancel health check task
142 self
.health_check_task
.cancel()
143 self
.log
.debug("Libjuju disconnected!")
145 async def disconnect_model(self
, model
: Model
):
149 :param: model: Model that will be disconnected
151 await model
.disconnect()
153 async def disconnect_controller(self
, controller
: Controller
):
155 Disconnect controller
157 :param: controller: Controller that will be disconnected
159 await controller
.disconnect()
161 async def add_model(self
, model_name
: str, cloud_name
: str):
165 :param: model_name: Model name
166 :param: cloud_name: Cloud name
170 controller
= await self
.get_controller()
173 # Raise exception if model already exists
174 if await self
.model_exists(model_name
, controller
=controller
):
175 raise JujuModelAlreadyExists(
176 "Model {} already exists.".format(model_name
)
179 # Block until other workers have finished model creation
180 while self
.creating_model
.locked():
181 await asyncio
.sleep(0.1)
183 # If the model exists, return it from the controller
184 if model_name
in self
.models
:
188 async with self
.creating_model
:
189 self
.log
.debug("Creating model {}".format(model_name
))
190 model
= await controller
.add_model(
192 config
=self
.model_config
,
193 cloud_name
=cloud_name
,
194 credential_name
=cloud_name
,
196 self
.models
.add(model_name
)
199 await self
.disconnect_model(model
)
200 await self
.disconnect_controller(controller
)
203 self
, controller
: Controller
, model_name
: str, id=None
206 Get model from controller
208 :param: controller: Controller
209 :param: model_name: Model name
211 :return: Model: The created Juju model object
213 return await controller
.get_model(model_name
)
215 async def model_exists(
216 self
, model_name
: str, controller
: Controller
= None
219 Check if model exists
221 :param: controller: Controller
222 :param: model_name: Model name
226 need_to_disconnect
= False
228 # Get controller if not passed
230 controller
= await self
.get_controller()
231 need_to_disconnect
= True
233 # Check if model exists
235 return model_name
in await controller
.list_models()
237 if need_to_disconnect
:
238 await self
.disconnect_controller(controller
)
240 async def models_exist(self
, model_names
: [str]) -> (bool, list):
242 Check if models exists
244 :param: model_names: List of strings with model names
246 :return (bool, list[str]): (True if all models exists, List of model names that don't exist)
250 "model_names must be a non-empty array. Given value: {}".format(
254 non_existing_models
= []
255 models
= await self
.list_models()
256 existing_models
= list(set(models
).intersection(model_names
))
257 non_existing_models
= list(set(model_names
) - set(existing_models
))
260 len(non_existing_models
) == 0,
264 async def get_model_status(self
, model_name
: str) -> FullStatus
:
268 :param: model_name: Model name
270 :return: Full status object
272 controller
= await self
.get_controller()
273 model
= await self
.get_model(controller
, model_name
)
275 return await model
.get_status()
277 await self
.disconnect_model(model
)
278 await self
.disconnect_controller(controller
)
280 async def create_machine(
283 machine_id
: str = None,
284 db_dict
: dict = None,
285 progress_timeout
: float = None,
286 total_timeout
: float = None,
287 series
: str = "xenial",
289 ) -> (Machine
, bool):
293 :param: model_name: Model name
294 :param: machine_id: Machine id
295 :param: db_dict: Dictionary with data of the DB to write the updates
296 :param: progress_timeout: Maximum time between two updates in the model
297 :param: total_timeout: Timeout for the entity to be active
298 :param: series: Series of the machine (xenial, bionic, focal, ...)
299 :param: wait: Wait until machine is ready
301 :return: (juju.machine.Machine, bool): Machine object and a boolean saying
302 if the machine is new or it already existed
308 "Creating machine (id={}) in model: {}".format(machine_id
, model_name
)
312 controller
= await self
.get_controller()
315 model
= await self
.get_model(controller
, model_name
)
317 if machine_id
is not None:
319 "Searching machine (id={}) in model {}".format(
320 machine_id
, model_name
324 # Get machines from model and get the machine with machine_id if exists
325 machines
= await model
.get_machines()
326 if machine_id
in machines
:
328 "Machine (id={}) found in model {}".format(
329 machine_id
, model_name
332 machine
= machines
[machine_id
]
334 raise JujuMachineNotFound("Machine {} not found".format(machine_id
))
337 self
.log
.debug("Creating a new machine in model {}".format(model_name
))
340 machine
= await model
.add_machine(
341 spec
=None, constraints
=None, disks
=None, series
=series
345 # Wait until the machine is ready
347 "Wait until machine {} is ready in model {}".format(
348 machine
.entity_id
, model_name
352 await JujuModelWatcher
.wait_for(
355 progress_timeout
=progress_timeout
,
356 total_timeout
=total_timeout
,
361 await self
.disconnect_model(model
)
362 await self
.disconnect_controller(controller
)
365 "Machine {} ready at {} in model {}".format(
366 machine
.entity_id
, machine
.dns_name
, model_name
371 async def provision_machine(
376 private_key_path
: str,
377 db_dict
: dict = None,
378 progress_timeout
: float = None,
379 total_timeout
: float = None,
382 Manually provisioning of a machine
384 :param: model_name: Model name
385 :param: hostname: IP to access the machine
386 :param: username: Username to login to the machine
387 :param: private_key_path: Local path for the private key
388 :param: db_dict: Dictionary with data of the DB to write the updates
389 :param: progress_timeout: Maximum time between two updates in the model
390 :param: total_timeout: Timeout for the entity to be active
392 :return: (Entity): Machine id
395 "Provisioning machine. model: {}, hostname: {}, username: {}".format(
396 model_name
, hostname
, username
401 controller
= await self
.get_controller()
404 model
= await self
.get_model(controller
, model_name
)
408 provisioner
= AsyncSSHProvisioner(
411 private_key_path
=private_key_path
,
416 params
= await provisioner
.provision_machine()
418 params
.jobs
= ["JobHostUnits"]
420 self
.log
.debug("Adding machine to model")
421 connection
= model
.connection()
422 client_facade
= client
.ClientFacade
.from_connection(connection
)
424 results
= await client_facade
.AddMachines(params
=[params
])
425 error
= results
.machines
[0].error
428 msg
= "Error adding machine: {}".format(error
.message
)
429 self
.log
.error(msg
=msg
)
430 raise ValueError(msg
)
432 machine_id
= results
.machines
[0].machine
434 self
.log
.debug("Installing Juju agent into machine {}".format(machine_id
))
435 asyncio
.ensure_future(
436 provisioner
.install_agent(
437 connection
=connection
,
439 machine_id
=machine_id
,
440 proxy
=self
.api_proxy
,
446 machine_list
= await model
.get_machines()
447 if machine_id
in machine_list
:
448 self
.log
.debug("Machine {} found in model!".format(machine_id
))
449 machine
= model
.machines
.get(machine_id
)
451 await asyncio
.sleep(2)
454 msg
= "Machine {} not found in model".format(machine_id
)
455 self
.log
.error(msg
=msg
)
456 raise JujuMachineNotFound(msg
)
459 "Wait until machine {} is ready in model {}".format(
460 machine
.entity_id
, model_name
463 await JujuModelWatcher
.wait_for(
466 progress_timeout
=progress_timeout
,
467 total_timeout
=total_timeout
,
471 except Exception as e
:
474 await self
.disconnect_model(model
)
475 await self
.disconnect_controller(controller
)
478 "Machine provisioned {} in model {}".format(machine_id
, model_name
)
483 async def deploy_charm(
485 application_name
: str,
489 db_dict
: dict = None,
490 progress_timeout
: float = None,
491 total_timeout
: float = None,
498 :param: application_name: Application name
499 :param: path: Local path to the charm
500 :param: model_name: Model name
501 :param: machine_id ID of the machine
502 :param: db_dict: Dictionary with data of the DB to write the updates
503 :param: progress_timeout: Maximum time between two updates in the model
504 :param: total_timeout: Timeout for the entity to be active
505 :param: config: Config for the charm
506 :param: series: Series of the charm
507 :param: num_units: Number of units
509 :return: (juju.application.Application): Juju application
512 "Deploying charm {} to machine {} in model ~{}".format(
513 application_name
, machine_id
, model_name
516 self
.log
.debug("charm: {}".format(path
))
519 controller
= await self
.get_controller()
522 model
= await self
.get_model(controller
, model_name
)
526 if application_name
not in model
.applications
:
528 if machine_id
is not None:
529 if machine_id
not in model
.machines
:
530 msg
= "Machine {} not found in model".format(machine_id
)
531 self
.log
.error(msg
=msg
)
532 raise JujuMachineNotFound(msg
)
533 machine
= model
.machines
[machine_id
]
534 series
= machine
.series
536 application
= await model
.deploy(
538 application_name
=application_name
,
547 "Wait until application {} is ready in model {}".format(
548 application_name
, model_name
552 for _
in range(num_units
- 1):
553 m
, _
= await self
.create_machine(model_name
, wait
=False)
554 await application
.add_unit(to
=m
.entity_id
)
556 await JujuModelWatcher
.wait_for(
559 progress_timeout
=progress_timeout
,
560 total_timeout
=total_timeout
,
565 "Application {} is ready in model {}".format(
566 application_name
, model_name
570 raise JujuApplicationExists(
571 "Application {} exists".format(application_name
)
574 await self
.disconnect_model(model
)
575 await self
.disconnect_controller(controller
)
579 def _get_application(self
, model
: Model
, application_name
: str) -> Application
:
582 :param: model: Model object
583 :param: application_name: Application name
585 :return: juju.application.Application (or None if it doesn't exist)
587 if model
.applications
and application_name
in model
.applications
:
588 return model
.applications
[application_name
]
590 async def execute_action(
592 application_name
: str,
595 db_dict
: dict = None,
596 progress_timeout
: float = None,
597 total_timeout
: float = None,
602 :param: application_name: Application name
603 :param: model_name: Model name
604 :param: action_name: Name of the action
605 :param: db_dict: Dictionary with data of the DB to write the updates
606 :param: progress_timeout: Maximum time between two updates in the model
607 :param: total_timeout: Timeout for the entity to be active
609 :return: (str, str): (output and status)
612 "Executing action {} using params {}".format(action_name
, kwargs
)
615 controller
= await self
.get_controller()
618 model
= await self
.get_model(controller
, model_name
)
622 application
= self
._get
_application
(
623 model
, application_name
=application_name
,
625 if application
is None:
626 raise JujuApplicationNotFound("Cannot execute action")
630 for u
in application
.units
:
631 if await u
.is_leader_from_status():
634 raise JujuLeaderUnitNotFound(
635 "Cannot execute action: leader unit not found"
638 actions
= await application
.get_actions()
640 if action_name
not in actions
:
641 raise JujuActionNotFound(
642 "Action {} not in available actions".format(action_name
)
645 action
= await unit
.run_action(action_name
, **kwargs
)
648 "Wait until action {} is completed in application {} (model={})".format(
649 action_name
, application_name
, model_name
652 await JujuModelWatcher
.wait_for(
655 progress_timeout
=progress_timeout
,
656 total_timeout
=total_timeout
,
661 output
= await model
.get_action_output(action_uuid
=action
.entity_id
)
662 status
= await model
.get_action_status(uuid_or_prefix
=action
.entity_id
)
664 status
[action
.entity_id
] if action
.entity_id
in status
else "failed"
668 "Action {} completed with status {} in application {} (model={})".format(
669 action_name
, action
.status
, application_name
, model_name
673 await self
.disconnect_model(model
)
674 await self
.disconnect_controller(controller
)
676 return output
, status
678 async def get_actions(self
, application_name
: str, model_name
: str) -> dict:
679 """Get list of actions
681 :param: application_name: Application name
682 :param: model_name: Model name
684 :return: Dict with this format
686 "action_name": "Description of the action",
691 "Getting list of actions for application {}".format(application_name
)
695 controller
= await self
.get_controller()
698 model
= await self
.get_model(controller
, model_name
)
702 application
= self
._get
_application
(
703 model
, application_name
=application_name
,
706 # Return list of actions
707 return await application
.get_actions()
710 # Disconnect from model and controller
711 await self
.disconnect_model(model
)
712 await self
.disconnect_controller(controller
)
714 async def get_metrics(self
, model_name
: str, application_name
: str) -> dict:
715 """Get the metrics collected by the VCA.
717 :param model_name The name or unique id of the network service
718 :param application_name The name of the application
720 if not model_name
or not application_name
:
721 raise Exception("model_name and application_name must be non-empty strings")
723 controller
= await self
.get_controller()
724 model
= await self
.get_model(controller
, model_name
)
726 application
= self
._get
_application
(model
, application_name
)
727 if application
is not None:
728 metrics
= await application
.get_metrics()
730 self
.disconnect_model(model
)
731 self
.disconnect_controller(controller
)
734 async def add_relation(
735 self
, model_name
: str, endpoint_1
: str, endpoint_2
: str,
739 :param: model_name: Model name
740 :param: endpoint_1 First endpoint name
741 ("app:endpoint" format or directly the saas name)
742 :param: endpoint_2: Second endpoint name (^ same format)
745 self
.log
.debug("Adding relation: {} -> {}".format(endpoint_1
, endpoint_2
))
748 controller
= await self
.get_controller()
751 model
= await self
.get_model(controller
, model_name
)
755 await model
.add_relation(endpoint_1
, endpoint_2
)
756 except JujuAPIError
as e
:
757 if "not found" in e
.message
:
758 self
.log
.warning("Relation not found: {}".format(e
.message
))
760 if "already exists" in e
.message
:
761 self
.log
.warning("Relation already exists: {}".format(e
.message
))
763 # another exception, raise it
766 await self
.disconnect_model(model
)
767 await self
.disconnect_controller(controller
)
770 self
, offer_url
: str, model_name
: str,
773 Adds a remote offer to the model. Relations can be created later using "juju relate".
775 :param: offer_url: Offer Url
776 :param: model_name: Model name
778 :raises ParseError if there's a problem parsing the offer_url
779 :raises JujuError if remote offer includes and endpoint
780 :raises JujuAPIError if the operation is not successful
782 controller
= await self
.get_controller()
783 model
= await controller
.get_model(model_name
)
786 await model
.consume(offer_url
)
788 await self
.disconnect_model(model
)
789 await self
.disconnect_controller(controller
)
791 async def destroy_model(self
, model_name
: str, total_timeout
: float):
795 :param: model_name: Model name
796 :param: total_timeout: Timeout
799 controller
= await self
.get_controller()
800 model
= await self
.get_model(controller
, model_name
)
802 self
.log
.debug("Destroying model {}".format(model_name
))
803 uuid
= model
.info
.uuid
806 machines
= await model
.get_machines()
807 for machine_id
in machines
:
809 await self
.destroy_machine(
810 model
, machine_id
=machine_id
, total_timeout
=total_timeout
,
812 except asyncio
.CancelledError
:
818 await self
.disconnect_model(model
)
821 if model_name
in self
.models
:
822 self
.models
.remove(model_name
)
824 await controller
.destroy_model(uuid
)
826 # Wait until model is destroyed
827 self
.log
.debug("Waiting for model {} to be destroyed...".format(model_name
))
830 if total_timeout
is None:
832 end
= time
.time() + total_timeout
833 while time
.time() < end
:
835 models
= await controller
.list_models()
836 if model_name
not in models
:
838 "The model {} ({}) was destroyed".format(model_name
, uuid
)
841 except asyncio
.CancelledError
:
843 except Exception as e
:
845 await asyncio
.sleep(5)
847 "Timeout waiting for model {} to be destroyed {}".format(
848 model_name
, last_exception
852 await self
.disconnect_controller(controller
)
854 async def destroy_application(self
, model
: Model
, application_name
: str):
858 :param: model: Model object
859 :param: application_name: Application name
862 "Destroying application {} in model {}".format(
863 application_name
, model
.info
.name
866 application
= model
.applications
.get(application_name
)
868 await application
.destroy()
870 self
.log
.warning("Application not found: {}".format(application_name
))
872 async def destroy_machine(
873 self
, model
: Model
, machine_id
: str, total_timeout
: float = 3600
878 :param: model: Model object
879 :param: machine_id: Machine id
880 :param: total_timeout: Timeout in seconds
882 machines
= await model
.get_machines()
883 if machine_id
in machines
:
884 machine
= machines
[machine_id
]
885 await machine
.destroy(force
=True)
887 end
= time
.time() + total_timeout
889 # wait for machine removal
890 machines
= await model
.get_machines()
891 while machine_id
in machines
and time
.time() < end
:
892 self
.log
.debug("Waiting for machine {} is destroyed".format(machine_id
))
893 await asyncio
.sleep(0.5)
894 machines
= await model
.get_machines()
895 self
.log
.debug("Machine destroyed: {}".format(machine_id
))
897 self
.log
.debug("Machine not found: {}".format(machine_id
))
899 async def configure_application(
900 self
, model_name
: str, application_name
: str, config
: dict = None
902 """Configure application
904 :param: model_name: Model name
905 :param: application_name: Application name
906 :param: config: Config to apply to the charm
908 self
.log
.debug("Configuring application {}".format(application_name
))
912 controller
= await self
.get_controller()
913 model
= await self
.get_model(controller
, model_name
)
914 application
= self
._get
_application
(
915 model
, application_name
=application_name
,
917 await application
.set_config(config
)
919 await self
.disconnect_model(model
)
920 await self
.disconnect_controller(controller
)
922 def _get_api_endpoints_db(self
) -> [str]:
924 Get API Endpoints from DB
926 :return: List of API endpoints
928 self
.log
.debug("Getting endpoints from database")
930 juju_info
= self
.db
.get_one(
931 DB_DATA
.api_endpoints
.table
,
932 q_filter
=DB_DATA
.api_endpoints
.filter,
935 if juju_info
and DB_DATA
.api_endpoints
.key
in juju_info
:
936 return juju_info
[DB_DATA
.api_endpoints
.key
]
938 def _update_api_endpoints_db(self
, endpoints
: [str]):
940 Update API endpoints in Database
942 :param: List of endpoints
944 self
.log
.debug("Saving endpoints {} in database".format(endpoints
))
946 juju_info
= self
.db
.get_one(
947 DB_DATA
.api_endpoints
.table
,
948 q_filter
=DB_DATA
.api_endpoints
.filter,
951 # If it doesn't, then create it
955 DB_DATA
.api_endpoints
.table
, DB_DATA
.api_endpoints
.filter,
957 except DbException
as e
:
958 # Racing condition: check if another N2VC worker has created it
959 juju_info
= self
.db
.get_one(
960 DB_DATA
.api_endpoints
.table
,
961 q_filter
=DB_DATA
.api_endpoints
.filter,
967 DB_DATA
.api_endpoints
.table
,
968 DB_DATA
.api_endpoints
.filter,
969 {DB_DATA
.api_endpoints
.key
: endpoints
},
972 def handle_exception(self
, loop
, context
):
973 # All unhandled exceptions by libjuju are handled here.
976 async def health_check(self
, interval
: float = 300.0):
978 Health check to make sure controller and controller_model connections are OK
980 :param: interval: Time in seconds between checks
984 controller
= await self
.get_controller()
985 # self.log.debug("VCA is alive")
986 except Exception as e
:
987 self
.log
.error("Health check to VCA failed: {}".format(e
))
989 await self
.disconnect_controller(controller
)
990 await asyncio
.sleep(interval
)
992 async def list_models(self
, contains
: str = None) -> [str]:
993 """List models with certain names
995 :param: contains: String that is contained in model name
997 :retur: [models] Returns list of model names
1000 controller
= await self
.get_controller()
1002 models
= await controller
.list_models()
1004 models
= [model
for model
in models
if contains
in model
]
1007 await self
.disconnect_controller(controller
)
1009 async def list_offers(self
, model_name
: str) -> QueryApplicationOffersResults
:
1010 """List models with certain names
1012 :param: model_name: Model name
1014 :return: Returns list of offers
1017 controller
= await self
.get_controller()
1019 return await controller
.list_offers(model_name
)
1021 await self
.disconnect_controller(controller
)
1023 async def add_k8s(self
, name
: str, auth_data
: dict, storage_class
: str):
1025 Add a Kubernetes cloud to the controller
1027 Similar to the `juju add-k8s` command in the CLI
1029 :param: name: Name for the K8s cloud
1030 :param: auth_data: Dictionary with needed credentials. Format:
1032 "server": "192.168.0.21:16443",
1033 "cacert": "-----BEGIN CERTIFI...",
1034 "token": "clhkRExRem5Xd1dCdnFEVXdvRGt...",
1037 :param: storage_class: Storage Class to use in the cloud
1040 required_auth_data_keys
= ["server", "cacert", "token"]
1042 for k
in required_auth_data_keys
:
1043 if k
not in auth_data
:
1044 missing_keys
.append(k
)
1047 "missing keys in auth_data: {}".format(",".join(missing_keys
))
1049 if not storage_class
:
1050 raise Exception("storage_class must be a non-empty string")
1052 raise Exception("name must be a non-empty string")
1054 endpoint
= auth_data
["server"]
1055 cacert
= auth_data
["cacert"]
1056 token
= auth_data
["token"]
1057 region_name
= "{}-region".format(name
)
1059 cloud
= client
.Cloud(
1060 auth_types
=["certificate"],
1061 ca_certificates
=[cacert
],
1064 "operator-storage": storage_class
,
1065 "workload-storage": storage_class
,
1067 regions
=[client
.CloudRegion(endpoint
=endpoint
, name
=region_name
)],
1071 cred
= client
.CloudCredential(
1072 auth_type
="certificate",
1073 attrs
={"ClientCertificateData": cacert
, "Token": token
},
1075 return await self
.add_cloud(name
, cloud
, cred
)
1077 async def add_cloud(
1078 self
, name
: str, cloud
: Cloud
, credential
: CloudCredential
= None
1081 Add cloud to the controller
1083 :param: name: Name of the cloud to be added
1084 :param: cloud: Cloud object
1085 :param: credential: CloudCredentials object for the cloud
1087 controller
= await self
.get_controller()
1089 _
= await controller
.add_cloud(name
, cloud
)
1091 await controller
.add_credential(name
, credential
=credential
, cloud
=name
)
1092 # Need to return the object returned by the controller.add_cloud() function
1093 # I'm returning the original value now until this bug is fixed:
1094 # https://github.com/juju/python-libjuju/issues/443
1097 await self
.disconnect_controller(controller
)
1099 async def remove_cloud(self
, name
: str):
1103 :param: name: Name of the cloud to be removed
1105 controller
= await self
.get_controller()
1107 await controller
.remove_cloud(name
)
1109 await self
.disconnect_controller(controller
)