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
,
42 JujuInvalidK8sConfiguration
,
44 from n2vc
.utils
import DB_DATA
45 from osm_common
.dbbase
import DbException
46 from kubernetes
.client
.configuration
import Configuration
57 loop
: asyncio
.AbstractEventLoop
= None,
58 log
: logging
.Logger
= None,
60 n2vc
: N2VCConnector
= None,
61 apt_mirror
: str = None,
62 enable_os_upgrade
: bool = True,
67 :param: endpoint: Endpoint of the juju controller (host:port)
68 :param: api_proxy: Endpoint of the juju controller - Reachable from the VNFs
69 :param: username: Juju username
70 :param: password: Juju password
71 :param: cacert: Juju CA Certificate
72 :param: loop: Asyncio loop
75 :param: n2vc: N2VC object
76 :param: apt_mirror: APT Mirror
77 :param: enable_os_upgrade: Enable OS Upgrade
80 self
.log
= log
or logging
.getLogger("Libjuju")
82 db_endpoints
= self
._get
_api
_endpoints
_db
()
83 self
.endpoints
= db_endpoints
or [endpoint
]
84 if db_endpoints
is None:
85 self
._update
_api
_endpoints
_db
(self
.endpoints
)
86 self
.api_proxy
= api_proxy
87 self
.username
= username
88 self
.password
= password
90 self
.loop
= loop
or asyncio
.get_event_loop()
93 # Generate config for models
94 self
.model_config
= {}
96 self
.model_config
["apt-mirror"] = apt_mirror
97 self
.model_config
["enable-os-refresh-update"] = enable_os_upgrade
98 self
.model_config
["enable-os-upgrade"] = enable_os_upgrade
100 self
.loop
.set_exception_handler(self
.handle_exception
)
101 self
.creating_model
= asyncio
.Lock(loop
=self
.loop
)
104 self
.log
.debug("Libjuju initialized!")
106 self
.health_check_task
= self
.loop
.create_task(self
.health_check())
108 async def get_controller(self
, timeout
: float = 5.0) -> Controller
:
112 :param: timeout: Time in seconds to wait for controller to connect
116 controller
= Controller(loop
=self
.loop
)
117 await asyncio
.wait_for(
119 endpoint
=self
.endpoints
,
120 username
=self
.username
,
121 password
=self
.password
,
126 endpoints
= await controller
.api_endpoints
127 if self
.endpoints
!= endpoints
:
128 self
.endpoints
= endpoints
129 self
._update
_api
_endpoints
_db
(self
.endpoints
)
131 except asyncio
.CancelledError
as e
:
133 except Exception as e
:
135 "Failed connecting to controller: {}...".format(self
.endpoints
)
138 await self
.disconnect_controller(controller
)
139 raise JujuControllerFailedConnecting(e
)
141 async def disconnect(self
):
143 # Cancel health check task
144 self
.health_check_task
.cancel()
145 self
.log
.debug("Libjuju disconnected!")
147 async def disconnect_model(self
, model
: Model
):
151 :param: model: Model that will be disconnected
153 await model
.disconnect()
155 async def disconnect_controller(self
, controller
: Controller
):
157 Disconnect controller
159 :param: controller: Controller that will be disconnected
161 await controller
.disconnect()
163 async def add_model(self
, model_name
: str, cloud_name
: str):
167 :param: model_name: Model name
168 :param: cloud_name: Cloud name
172 controller
= await self
.get_controller()
175 # Raise exception if model already exists
176 if await self
.model_exists(model_name
, controller
=controller
):
177 raise JujuModelAlreadyExists(
178 "Model {} already exists.".format(model_name
)
181 # Block until other workers have finished model creation
182 while self
.creating_model
.locked():
183 await asyncio
.sleep(0.1)
185 # If the model exists, return it from the controller
186 if model_name
in self
.models
:
190 async with self
.creating_model
:
191 self
.log
.debug("Creating model {}".format(model_name
))
192 model
= await controller
.add_model(
194 config
=self
.model_config
,
195 cloud_name
=cloud_name
,
196 credential_name
=cloud_name
,
198 self
.models
.add(model_name
)
201 await self
.disconnect_model(model
)
202 await self
.disconnect_controller(controller
)
205 self
, controller
: Controller
, model_name
: str, id=None
208 Get model from controller
210 :param: controller: Controller
211 :param: model_name: Model name
213 :return: Model: The created Juju model object
215 return await controller
.get_model(model_name
)
217 async def model_exists(
218 self
, model_name
: str, controller
: Controller
= None
221 Check if model exists
223 :param: controller: Controller
224 :param: model_name: Model name
228 need_to_disconnect
= False
230 # Get controller if not passed
232 controller
= await self
.get_controller()
233 need_to_disconnect
= True
235 # Check if model exists
237 return model_name
in await controller
.list_models()
239 if need_to_disconnect
:
240 await self
.disconnect_controller(controller
)
242 async def models_exist(self
, model_names
: [str]) -> (bool, list):
244 Check if models exists
246 :param: model_names: List of strings with model names
248 :return (bool, list[str]): (True if all models exists, List of model names that don't exist)
252 "model_names must be a non-empty array. Given value: {}".format(
256 non_existing_models
= []
257 models
= await self
.list_models()
258 existing_models
= list(set(models
).intersection(model_names
))
259 non_existing_models
= list(set(model_names
) - set(existing_models
))
262 len(non_existing_models
) == 0,
266 async def get_model_status(self
, model_name
: str) -> FullStatus
:
270 :param: model_name: Model name
272 :return: Full status object
274 controller
= await self
.get_controller()
275 model
= await self
.get_model(controller
, model_name
)
277 return await model
.get_status()
279 await self
.disconnect_model(model
)
280 await self
.disconnect_controller(controller
)
282 async def create_machine(
285 machine_id
: str = None,
286 db_dict
: dict = None,
287 progress_timeout
: float = None,
288 total_timeout
: float = None,
289 series
: str = "xenial",
291 ) -> (Machine
, bool):
295 :param: model_name: Model name
296 :param: machine_id: Machine id
297 :param: db_dict: Dictionary with data of the DB to write the updates
298 :param: progress_timeout: Maximum time between two updates in the model
299 :param: total_timeout: Timeout for the entity to be active
300 :param: series: Series of the machine (xenial, bionic, focal, ...)
301 :param: wait: Wait until machine is ready
303 :return: (juju.machine.Machine, bool): Machine object and a boolean saying
304 if the machine is new or it already existed
310 "Creating machine (id={}) in model: {}".format(machine_id
, model_name
)
314 controller
= await self
.get_controller()
317 model
= await self
.get_model(controller
, model_name
)
319 if machine_id
is not None:
321 "Searching machine (id={}) in model {}".format(
322 machine_id
, model_name
326 # Get machines from model and get the machine with machine_id if exists
327 machines
= await model
.get_machines()
328 if machine_id
in machines
:
330 "Machine (id={}) found in model {}".format(
331 machine_id
, model_name
334 machine
= machines
[machine_id
]
336 raise JujuMachineNotFound("Machine {} not found".format(machine_id
))
339 self
.log
.debug("Creating a new machine in model {}".format(model_name
))
342 machine
= await model
.add_machine(
343 spec
=None, constraints
=None, disks
=None, series
=series
347 # Wait until the machine is ready
349 "Wait until machine {} is ready in model {}".format(
350 machine
.entity_id
, model_name
354 await JujuModelWatcher
.wait_for(
357 progress_timeout
=progress_timeout
,
358 total_timeout
=total_timeout
,
363 await self
.disconnect_model(model
)
364 await self
.disconnect_controller(controller
)
367 "Machine {} ready at {} in model {}".format(
368 machine
.entity_id
, machine
.dns_name
, model_name
373 async def provision_machine(
378 private_key_path
: str,
379 db_dict
: dict = None,
380 progress_timeout
: float = None,
381 total_timeout
: float = None,
384 Manually provisioning of a machine
386 :param: model_name: Model name
387 :param: hostname: IP to access the machine
388 :param: username: Username to login to the machine
389 :param: private_key_path: Local path for the private key
390 :param: db_dict: Dictionary with data of the DB to write the updates
391 :param: progress_timeout: Maximum time between two updates in the model
392 :param: total_timeout: Timeout for the entity to be active
394 :return: (Entity): Machine id
397 "Provisioning machine. model: {}, hostname: {}, username: {}".format(
398 model_name
, hostname
, username
403 controller
= await self
.get_controller()
406 model
= await self
.get_model(controller
, model_name
)
410 provisioner
= AsyncSSHProvisioner(
413 private_key_path
=private_key_path
,
418 params
= await provisioner
.provision_machine()
420 params
.jobs
= ["JobHostUnits"]
422 self
.log
.debug("Adding machine to model")
423 connection
= model
.connection()
424 client_facade
= client
.ClientFacade
.from_connection(connection
)
426 results
= await client_facade
.AddMachines(params
=[params
])
427 error
= results
.machines
[0].error
430 msg
= "Error adding machine: {}".format(error
.message
)
431 self
.log
.error(msg
=msg
)
432 raise ValueError(msg
)
434 machine_id
= results
.machines
[0].machine
436 self
.log
.debug("Installing Juju agent into machine {}".format(machine_id
))
437 asyncio
.ensure_future(
438 provisioner
.install_agent(
439 connection
=connection
,
441 machine_id
=machine_id
,
442 proxy
=self
.api_proxy
,
448 machine_list
= await model
.get_machines()
449 if machine_id
in machine_list
:
450 self
.log
.debug("Machine {} found in model!".format(machine_id
))
451 machine
= model
.machines
.get(machine_id
)
453 await asyncio
.sleep(2)
456 msg
= "Machine {} not found in model".format(machine_id
)
457 self
.log
.error(msg
=msg
)
458 raise JujuMachineNotFound(msg
)
461 "Wait until machine {} is ready in model {}".format(
462 machine
.entity_id
, model_name
465 await JujuModelWatcher
.wait_for(
468 progress_timeout
=progress_timeout
,
469 total_timeout
=total_timeout
,
473 except Exception as e
:
476 await self
.disconnect_model(model
)
477 await self
.disconnect_controller(controller
)
480 "Machine provisioned {} in model {}".format(machine_id
, model_name
)
485 async def deploy_charm(
487 application_name
: str,
491 db_dict
: dict = None,
492 progress_timeout
: float = None,
493 total_timeout
: float = None,
500 :param: application_name: Application name
501 :param: path: Local path to the charm
502 :param: model_name: Model name
503 :param: machine_id ID of the machine
504 :param: db_dict: Dictionary with data of the DB to write the updates
505 :param: progress_timeout: Maximum time between two updates in the model
506 :param: total_timeout: Timeout for the entity to be active
507 :param: config: Config for the charm
508 :param: series: Series of the charm
509 :param: num_units: Number of units
511 :return: (juju.application.Application): Juju application
514 "Deploying charm {} to machine {} in model ~{}".format(
515 application_name
, machine_id
, model_name
518 self
.log
.debug("charm: {}".format(path
))
521 controller
= await self
.get_controller()
524 model
= await self
.get_model(controller
, model_name
)
528 if application_name
not in model
.applications
:
530 if machine_id
is not None:
531 if machine_id
not in model
.machines
:
532 msg
= "Machine {} not found in model".format(machine_id
)
533 self
.log
.error(msg
=msg
)
534 raise JujuMachineNotFound(msg
)
535 machine
= model
.machines
[machine_id
]
536 series
= machine
.series
538 application
= await model
.deploy(
540 application_name
=application_name
,
549 "Wait until application {} is ready in model {}".format(
550 application_name
, model_name
554 for _
in range(num_units
- 1):
555 m
, _
= await self
.create_machine(model_name
, wait
=False)
556 await application
.add_unit(to
=m
.entity_id
)
558 await JujuModelWatcher
.wait_for(
561 progress_timeout
=progress_timeout
,
562 total_timeout
=total_timeout
,
567 "Application {} is ready in model {}".format(
568 application_name
, model_name
572 raise JujuApplicationExists(
573 "Application {} exists".format(application_name
)
576 await self
.disconnect_model(model
)
577 await self
.disconnect_controller(controller
)
581 def _get_application(self
, model
: Model
, application_name
: str) -> Application
:
584 :param: model: Model object
585 :param: application_name: Application name
587 :return: juju.application.Application (or None if it doesn't exist)
589 if model
.applications
and application_name
in model
.applications
:
590 return model
.applications
[application_name
]
592 async def execute_action(
594 application_name
: str,
597 db_dict
: dict = None,
598 progress_timeout
: float = None,
599 total_timeout
: float = None,
604 :param: application_name: Application name
605 :param: model_name: Model name
606 :param: action_name: Name of the action
607 :param: db_dict: Dictionary with data of the DB to write the updates
608 :param: progress_timeout: Maximum time between two updates in the model
609 :param: total_timeout: Timeout for the entity to be active
611 :return: (str, str): (output and status)
614 "Executing action {} using params {}".format(action_name
, kwargs
)
617 controller
= await self
.get_controller()
620 model
= await self
.get_model(controller
, model_name
)
624 application
= self
._get
_application
(
625 model
, application_name
=application_name
,
627 if application
is None:
628 raise JujuApplicationNotFound("Cannot execute action")
632 for u
in application
.units
:
633 if await u
.is_leader_from_status():
636 raise JujuLeaderUnitNotFound(
637 "Cannot execute action: leader unit not found"
640 actions
= await application
.get_actions()
642 if action_name
not in actions
:
643 raise JujuActionNotFound(
644 "Action {} not in available actions".format(action_name
)
647 action
= await unit
.run_action(action_name
, **kwargs
)
650 "Wait until action {} is completed in application {} (model={})".format(
651 action_name
, application_name
, model_name
654 await JujuModelWatcher
.wait_for(
657 progress_timeout
=progress_timeout
,
658 total_timeout
=total_timeout
,
663 output
= await model
.get_action_output(action_uuid
=action
.entity_id
)
664 status
= await model
.get_action_status(uuid_or_prefix
=action
.entity_id
)
666 status
[action
.entity_id
] if action
.entity_id
in status
else "failed"
670 "Action {} completed with status {} in application {} (model={})".format(
671 action_name
, action
.status
, application_name
, model_name
675 await self
.disconnect_model(model
)
676 await self
.disconnect_controller(controller
)
678 return output
, status
680 async def get_actions(self
, application_name
: str, model_name
: str) -> dict:
681 """Get list of actions
683 :param: application_name: Application name
684 :param: model_name: Model name
686 :return: Dict with this format
688 "action_name": "Description of the action",
693 "Getting list of actions for application {}".format(application_name
)
697 controller
= await self
.get_controller()
700 model
= await self
.get_model(controller
, model_name
)
704 application
= self
._get
_application
(
705 model
, application_name
=application_name
,
708 # Return list of actions
709 return await application
.get_actions()
712 # Disconnect from model and controller
713 await self
.disconnect_model(model
)
714 await self
.disconnect_controller(controller
)
716 async def get_metrics(self
, model_name
: str, application_name
: str) -> dict:
717 """Get the metrics collected by the VCA.
719 :param model_name The name or unique id of the network service
720 :param application_name The name of the application
722 if not model_name
or not application_name
:
723 raise Exception("model_name and application_name must be non-empty strings")
725 controller
= await self
.get_controller()
726 model
= await self
.get_model(controller
, model_name
)
728 application
= self
._get
_application
(model
, application_name
)
729 if application
is not None:
730 metrics
= await application
.get_metrics()
732 self
.disconnect_model(model
)
733 self
.disconnect_controller(controller
)
736 async def add_relation(
737 self
, model_name
: str, endpoint_1
: str, endpoint_2
: str,
741 :param: model_name: Model name
742 :param: endpoint_1 First endpoint name
743 ("app:endpoint" format or directly the saas name)
744 :param: endpoint_2: Second endpoint name (^ same format)
747 self
.log
.debug("Adding relation: {} -> {}".format(endpoint_1
, endpoint_2
))
750 controller
= await self
.get_controller()
753 model
= await self
.get_model(controller
, model_name
)
757 await model
.add_relation(endpoint_1
, endpoint_2
)
758 except JujuAPIError
as e
:
759 if "not found" in e
.message
:
760 self
.log
.warning("Relation not found: {}".format(e
.message
))
762 if "already exists" in e
.message
:
763 self
.log
.warning("Relation already exists: {}".format(e
.message
))
765 # another exception, raise it
768 await self
.disconnect_model(model
)
769 await self
.disconnect_controller(controller
)
772 self
, offer_url
: str, model_name
: str,
775 Adds a remote offer to the model. Relations can be created later using "juju relate".
777 :param: offer_url: Offer Url
778 :param: model_name: Model name
780 :raises ParseError if there's a problem parsing the offer_url
781 :raises JujuError if remote offer includes and endpoint
782 :raises JujuAPIError if the operation is not successful
784 controller
= await self
.get_controller()
785 model
= await controller
.get_model(model_name
)
788 await model
.consume(offer_url
)
790 await self
.disconnect_model(model
)
791 await self
.disconnect_controller(controller
)
793 async def destroy_model(self
, model_name
: str, total_timeout
: float):
797 :param: model_name: Model name
798 :param: total_timeout: Timeout
801 controller
= await self
.get_controller()
802 model
= await self
.get_model(controller
, model_name
)
804 self
.log
.debug("Destroying model {}".format(model_name
))
805 uuid
= model
.info
.uuid
808 machines
= await model
.get_machines()
809 for machine_id
in machines
:
811 await self
.destroy_machine(
812 model
, machine_id
=machine_id
, total_timeout
=total_timeout
,
814 except asyncio
.CancelledError
:
820 await self
.disconnect_model(model
)
823 if model_name
in self
.models
:
824 self
.models
.remove(model_name
)
826 await controller
.destroy_model(uuid
)
828 # Wait until model is destroyed
829 self
.log
.debug("Waiting for model {} to be destroyed...".format(model_name
))
832 if total_timeout
is None:
834 end
= time
.time() + total_timeout
835 while time
.time() < end
:
837 models
= await controller
.list_models()
838 if model_name
not in models
:
840 "The model {} ({}) was destroyed".format(model_name
, uuid
)
843 except asyncio
.CancelledError
:
845 except Exception as e
:
847 await asyncio
.sleep(5)
849 "Timeout waiting for model {} to be destroyed {}".format(
850 model_name
, last_exception
854 await self
.disconnect_controller(controller
)
856 async def destroy_application(self
, model
: Model
, application_name
: str):
860 :param: model: Model object
861 :param: application_name: Application name
864 "Destroying application {} in model {}".format(
865 application_name
, model
.info
.name
868 application
= model
.applications
.get(application_name
)
870 await application
.destroy()
872 self
.log
.warning("Application not found: {}".format(application_name
))
874 async def destroy_machine(
875 self
, model
: Model
, machine_id
: str, total_timeout
: float = 3600
880 :param: model: Model object
881 :param: machine_id: Machine id
882 :param: total_timeout: Timeout in seconds
884 machines
= await model
.get_machines()
885 if machine_id
in machines
:
886 machine
= machines
[machine_id
]
887 await machine
.destroy(force
=True)
889 end
= time
.time() + total_timeout
891 # wait for machine removal
892 machines
= await model
.get_machines()
893 while machine_id
in machines
and time
.time() < end
:
894 self
.log
.debug("Waiting for machine {} is destroyed".format(machine_id
))
895 await asyncio
.sleep(0.5)
896 machines
= await model
.get_machines()
897 self
.log
.debug("Machine destroyed: {}".format(machine_id
))
899 self
.log
.debug("Machine not found: {}".format(machine_id
))
901 async def configure_application(
902 self
, model_name
: str, application_name
: str, config
: dict = None
904 """Configure application
906 :param: model_name: Model name
907 :param: application_name: Application name
908 :param: config: Config to apply to the charm
910 self
.log
.debug("Configuring application {}".format(application_name
))
914 controller
= await self
.get_controller()
915 model
= await self
.get_model(controller
, model_name
)
916 application
= self
._get
_application
(
917 model
, application_name
=application_name
,
919 await application
.set_config(config
)
921 await self
.disconnect_model(model
)
922 await self
.disconnect_controller(controller
)
924 def _get_api_endpoints_db(self
) -> [str]:
926 Get API Endpoints from DB
928 :return: List of API endpoints
930 self
.log
.debug("Getting endpoints from database")
932 juju_info
= self
.db
.get_one(
933 DB_DATA
.api_endpoints
.table
,
934 q_filter
=DB_DATA
.api_endpoints
.filter,
937 if juju_info
and DB_DATA
.api_endpoints
.key
in juju_info
:
938 return juju_info
[DB_DATA
.api_endpoints
.key
]
940 def _update_api_endpoints_db(self
, endpoints
: [str]):
942 Update API endpoints in Database
944 :param: List of endpoints
946 self
.log
.debug("Saving endpoints {} in database".format(endpoints
))
948 juju_info
= self
.db
.get_one(
949 DB_DATA
.api_endpoints
.table
,
950 q_filter
=DB_DATA
.api_endpoints
.filter,
953 # If it doesn't, then create it
957 DB_DATA
.api_endpoints
.table
, DB_DATA
.api_endpoints
.filter,
959 except DbException
as e
:
960 # Racing condition: check if another N2VC worker has created it
961 juju_info
= self
.db
.get_one(
962 DB_DATA
.api_endpoints
.table
,
963 q_filter
=DB_DATA
.api_endpoints
.filter,
969 DB_DATA
.api_endpoints
.table
,
970 DB_DATA
.api_endpoints
.filter,
971 {DB_DATA
.api_endpoints
.key
: endpoints
},
974 def handle_exception(self
, loop
, context
):
975 # All unhandled exceptions by libjuju are handled here.
978 async def health_check(self
, interval
: float = 300.0):
980 Health check to make sure controller and controller_model connections are OK
982 :param: interval: Time in seconds between checks
986 controller
= await self
.get_controller()
987 # self.log.debug("VCA is alive")
988 except Exception as e
:
989 self
.log
.error("Health check to VCA failed: {}".format(e
))
991 await self
.disconnect_controller(controller
)
992 await asyncio
.sleep(interval
)
994 async def list_models(self
, contains
: str = None) -> [str]:
995 """List models with certain names
997 :param: contains: String that is contained in model name
999 :retur: [models] Returns list of model names
1002 controller
= await self
.get_controller()
1004 models
= await controller
.list_models()
1006 models
= [model
for model
in models
if contains
in model
]
1009 await self
.disconnect_controller(controller
)
1011 async def list_offers(self
, model_name
: str) -> QueryApplicationOffersResults
:
1012 """List models with certain names
1014 :param: model_name: Model name
1016 :return: Returns list of offers
1019 controller
= await self
.get_controller()
1021 return await controller
.list_offers(model_name
)
1023 await self
.disconnect_controller(controller
)
1026 self
, name
: str, configuration
: Configuration
, storage_class
: str
1029 Add a Kubernetes cloud to the controller
1031 Similar to the `juju add-k8s` command in the CLI
1033 :param: name: Name for the K8s cloud
1034 :param: configuration: Kubernetes configuration object
1035 :param: storage_class: Storage Class to use in the cloud
1038 if not storage_class
:
1039 raise Exception("storage_class must be a non-empty string")
1041 raise Exception("name must be a non-empty string")
1042 if not configuration
:
1043 raise Exception("configuration must be provided")
1045 endpoint
= configuration
.host
1046 credential
= self
.get_k8s_cloud_credential(configuration
)
1048 [credential
.attrs
["ClientCertificateData"]]
1049 if "ClientCertificateData" in credential
.attrs
1052 cloud
= client
.Cloud(
1054 auth_types
=[credential
.auth_type
],
1056 ca_certificates
=ca_certificates
,
1058 "operator-storage": storage_class
,
1059 "workload-storage": storage_class
,
1063 return await self
.add_cloud(name
, cloud
, credential
)
1065 def get_k8s_cloud_credential(
1066 self
, configuration
: Configuration
,
1067 ) -> client
.CloudCredential
:
1069 ca_cert
= configuration
.ssl_ca_cert
or configuration
.cert_file
1070 key
= configuration
.key_file
1071 api_key
= configuration
.api_key
1073 username
= configuration
.username
1074 password
= configuration
.password
1076 if "authorization" in api_key
:
1077 authorization
= api_key
["authorization"]
1078 if "Bearer " in authorization
:
1079 bearer_list
= authorization
.split(" ")
1080 if len(bearer_list
) == 2:
1081 [_
, token
] = bearer_list
1083 raise JujuInvalidK8sConfiguration("unknown format of api_key")
1085 token
= authorization
1087 attrs
["ClientCertificateData"] = open(ca_cert
, "r").read()
1089 attrs
["ClientKeyData"] = open(key
, "r").read()
1091 if username
or password
:
1092 raise JujuInvalidK8sConfiguration("Cannot set both token and user/pass")
1093 attrs
["Token"] = token
1097 auth_type
= "oauth2"
1099 raise JujuInvalidK8sConfiguration(
1100 "missing token for auth type {}".format(auth_type
)
1105 "credential for user {} has empty password".format(username
)
1107 attrs
["username"] = username
1108 attrs
["password"] = password
1110 auth_type
= "userpasswithcert"
1112 auth_type
= "userpass"
1113 elif ca_cert
and token
:
1114 auth_type
= "certificate"
1116 raise JujuInvalidK8sConfiguration("authentication method not supported")
1117 return client
.CloudCredential(auth_type
=auth_type
, attrs
=attrs
,)
1119 async def add_cloud(
1120 self
, name
: str, cloud
: Cloud
, credential
: CloudCredential
= None
1123 Add cloud to the controller
1125 :param: name: Name of the cloud to be added
1126 :param: cloud: Cloud object
1127 :param: credential: CloudCredentials object for the cloud
1129 controller
= await self
.get_controller()
1131 _
= await controller
.add_cloud(name
, cloud
)
1133 await controller
.add_credential(name
, credential
=credential
, cloud
=name
)
1134 # Need to return the object returned by the controller.add_cloud() function
1135 # I'm returning the original value now until this bug is fixed:
1136 # https://github.com/juju/python-libjuju/issues/443
1139 await self
.disconnect_controller(controller
)
1141 async def remove_cloud(self
, name
: str):
1145 :param: name: Name of the cloud to be removed
1147 controller
= await self
.get_controller()
1149 await controller
.remove_cloud(name
)
1151 await self
.disconnect_controller(controller
)