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, credential_name
=None):
167 :param: model_name: Model name
168 :param: cloud_name: Cloud name
169 :param: credential_name: Credential name to use for adding the model
170 If not specified, same name as the cloud will be used.
174 controller
= await self
.get_controller()
177 # Raise exception if model already exists
178 if await self
.model_exists(model_name
, controller
=controller
):
179 raise JujuModelAlreadyExists(
180 "Model {} already exists.".format(model_name
)
183 # Block until other workers have finished model creation
184 while self
.creating_model
.locked():
185 await asyncio
.sleep(0.1)
187 # If the model exists, return it from the controller
188 if model_name
in self
.models
:
192 async with self
.creating_model
:
193 self
.log
.debug("Creating model {}".format(model_name
))
194 model
= await controller
.add_model(
196 config
=self
.model_config
,
197 cloud_name
=cloud_name
,
198 credential_name
=credential_name
or cloud_name
,
200 self
.models
.add(model_name
)
203 await self
.disconnect_model(model
)
204 await self
.disconnect_controller(controller
)
207 self
, controller
: Controller
, model_name
: str, id=None
210 Get model from controller
212 :param: controller: Controller
213 :param: model_name: Model name
215 :return: Model: The created Juju model object
217 return await controller
.get_model(model_name
)
219 async def model_exists(
220 self
, model_name
: str, controller
: Controller
= None
223 Check if model exists
225 :param: controller: Controller
226 :param: model_name: Model name
230 need_to_disconnect
= False
232 # Get controller if not passed
234 controller
= await self
.get_controller()
235 need_to_disconnect
= True
237 # Check if model exists
239 return model_name
in await controller
.list_models()
241 if need_to_disconnect
:
242 await self
.disconnect_controller(controller
)
244 async def models_exist(self
, model_names
: [str]) -> (bool, list):
246 Check if models exists
248 :param: model_names: List of strings with model names
250 :return (bool, list[str]): (True if all models exists, List of model names that don't exist)
254 "model_names must be a non-empty array. Given value: {}".format(
258 non_existing_models
= []
259 models
= await self
.list_models()
260 existing_models
= list(set(models
).intersection(model_names
))
261 non_existing_models
= list(set(model_names
) - set(existing_models
))
264 len(non_existing_models
) == 0,
268 async def get_model_status(self
, model_name
: str) -> FullStatus
:
272 :param: model_name: Model name
274 :return: Full status object
276 controller
= await self
.get_controller()
277 model
= await self
.get_model(controller
, model_name
)
279 return await model
.get_status()
281 await self
.disconnect_model(model
)
282 await self
.disconnect_controller(controller
)
284 async def create_machine(
287 machine_id
: str = None,
288 db_dict
: dict = None,
289 progress_timeout
: float = None,
290 total_timeout
: float = None,
291 series
: str = "xenial",
293 ) -> (Machine
, bool):
297 :param: model_name: Model name
298 :param: machine_id: Machine id
299 :param: db_dict: Dictionary with data of the DB to write the updates
300 :param: progress_timeout: Maximum time between two updates in the model
301 :param: total_timeout: Timeout for the entity to be active
302 :param: series: Series of the machine (xenial, bionic, focal, ...)
303 :param: wait: Wait until machine is ready
305 :return: (juju.machine.Machine, bool): Machine object and a boolean saying
306 if the machine is new or it already existed
312 "Creating machine (id={}) in model: {}".format(machine_id
, model_name
)
316 controller
= await self
.get_controller()
319 model
= await self
.get_model(controller
, model_name
)
321 if machine_id
is not None:
323 "Searching machine (id={}) in model {}".format(
324 machine_id
, model_name
328 # Get machines from model and get the machine with machine_id if exists
329 machines
= await model
.get_machines()
330 if machine_id
in machines
:
332 "Machine (id={}) found in model {}".format(
333 machine_id
, model_name
336 machine
= machines
[machine_id
]
338 raise JujuMachineNotFound("Machine {} not found".format(machine_id
))
341 self
.log
.debug("Creating a new machine in model {}".format(model_name
))
344 machine
= await model
.add_machine(
345 spec
=None, constraints
=None, disks
=None, series
=series
349 # Wait until the machine is ready
351 "Wait until machine {} is ready in model {}".format(
352 machine
.entity_id
, model_name
356 await JujuModelWatcher
.wait_for(
359 progress_timeout
=progress_timeout
,
360 total_timeout
=total_timeout
,
365 await self
.disconnect_model(model
)
366 await self
.disconnect_controller(controller
)
369 "Machine {} ready at {} in model {}".format(
370 machine
.entity_id
, machine
.dns_name
, model_name
375 async def provision_machine(
380 private_key_path
: str,
381 db_dict
: dict = None,
382 progress_timeout
: float = None,
383 total_timeout
: float = None,
386 Manually provisioning of a machine
388 :param: model_name: Model name
389 :param: hostname: IP to access the machine
390 :param: username: Username to login to the machine
391 :param: private_key_path: Local path for the private key
392 :param: db_dict: Dictionary with data of the DB to write the updates
393 :param: progress_timeout: Maximum time between two updates in the model
394 :param: total_timeout: Timeout for the entity to be active
396 :return: (Entity): Machine id
399 "Provisioning machine. model: {}, hostname: {}, username: {}".format(
400 model_name
, hostname
, username
405 controller
= await self
.get_controller()
408 model
= await self
.get_model(controller
, model_name
)
412 provisioner
= AsyncSSHProvisioner(
415 private_key_path
=private_key_path
,
420 params
= await provisioner
.provision_machine()
422 params
.jobs
= ["JobHostUnits"]
424 self
.log
.debug("Adding machine to model")
425 connection
= model
.connection()
426 client_facade
= client
.ClientFacade
.from_connection(connection
)
428 results
= await client_facade
.AddMachines(params
=[params
])
429 error
= results
.machines
[0].error
432 msg
= "Error adding machine: {}".format(error
.message
)
433 self
.log
.error(msg
=msg
)
434 raise ValueError(msg
)
436 machine_id
= results
.machines
[0].machine
438 self
.log
.debug("Installing Juju agent into machine {}".format(machine_id
))
439 asyncio
.ensure_future(
440 provisioner
.install_agent(
441 connection
=connection
,
443 machine_id
=machine_id
,
444 proxy
=self
.api_proxy
,
450 machine_list
= await model
.get_machines()
451 if machine_id
in machine_list
:
452 self
.log
.debug("Machine {} found in model!".format(machine_id
))
453 machine
= model
.machines
.get(machine_id
)
455 await asyncio
.sleep(2)
458 msg
= "Machine {} not found in model".format(machine_id
)
459 self
.log
.error(msg
=msg
)
460 raise JujuMachineNotFound(msg
)
463 "Wait until machine {} is ready in model {}".format(
464 machine
.entity_id
, model_name
467 await JujuModelWatcher
.wait_for(
470 progress_timeout
=progress_timeout
,
471 total_timeout
=total_timeout
,
475 except Exception as e
:
478 await self
.disconnect_model(model
)
479 await self
.disconnect_controller(controller
)
482 "Machine provisioned {} in model {}".format(machine_id
, model_name
)
487 async def deploy_charm(
489 application_name
: str,
493 db_dict
: dict = None,
494 progress_timeout
: float = None,
495 total_timeout
: float = None,
502 :param: application_name: Application name
503 :param: path: Local path to the charm
504 :param: model_name: Model name
505 :param: machine_id ID of the machine
506 :param: db_dict: Dictionary with data of the DB to write the updates
507 :param: progress_timeout: Maximum time between two updates in the model
508 :param: total_timeout: Timeout for the entity to be active
509 :param: config: Config for the charm
510 :param: series: Series of the charm
511 :param: num_units: Number of units
513 :return: (juju.application.Application): Juju application
516 "Deploying charm {} to machine {} in model ~{}".format(
517 application_name
, machine_id
, model_name
520 self
.log
.debug("charm: {}".format(path
))
523 controller
= await self
.get_controller()
526 model
= await self
.get_model(controller
, model_name
)
530 if application_name
not in model
.applications
:
532 if machine_id
is not None:
533 if machine_id
not in model
.machines
:
534 msg
= "Machine {} not found in model".format(machine_id
)
535 self
.log
.error(msg
=msg
)
536 raise JujuMachineNotFound(msg
)
537 machine
= model
.machines
[machine_id
]
538 series
= machine
.series
540 application
= await model
.deploy(
542 application_name
=application_name
,
551 "Wait until application {} is ready in model {}".format(
552 application_name
, model_name
556 for _
in range(num_units
- 1):
557 m
, _
= await self
.create_machine(model_name
, wait
=False)
558 await application
.add_unit(to
=m
.entity_id
)
560 await JujuModelWatcher
.wait_for(
563 progress_timeout
=progress_timeout
,
564 total_timeout
=total_timeout
,
569 "Application {} is ready in model {}".format(
570 application_name
, model_name
574 raise JujuApplicationExists(
575 "Application {} exists".format(application_name
)
578 await self
.disconnect_model(model
)
579 await self
.disconnect_controller(controller
)
583 def _get_application(self
, model
: Model
, application_name
: str) -> Application
:
586 :param: model: Model object
587 :param: application_name: Application name
589 :return: juju.application.Application (or None if it doesn't exist)
591 if model
.applications
and application_name
in model
.applications
:
592 return model
.applications
[application_name
]
594 async def execute_action(
596 application_name
: str,
599 db_dict
: dict = None,
600 progress_timeout
: float = None,
601 total_timeout
: float = None,
606 :param: application_name: Application name
607 :param: model_name: Model name
608 :param: action_name: Name of the action
609 :param: db_dict: Dictionary with data of the DB to write the updates
610 :param: progress_timeout: Maximum time between two updates in the model
611 :param: total_timeout: Timeout for the entity to be active
613 :return: (str, str): (output and status)
616 "Executing action {} using params {}".format(action_name
, kwargs
)
619 controller
= await self
.get_controller()
622 model
= await self
.get_model(controller
, model_name
)
626 application
= self
._get
_application
(
627 model
, application_name
=application_name
,
629 if application
is None:
630 raise JujuApplicationNotFound("Cannot execute action")
634 for u
in application
.units
:
635 if await u
.is_leader_from_status():
638 raise JujuLeaderUnitNotFound(
639 "Cannot execute action: leader unit not found"
642 actions
= await application
.get_actions()
644 if action_name
not in actions
:
645 raise JujuActionNotFound(
646 "Action {} not in available actions".format(action_name
)
649 action
= await unit
.run_action(action_name
, **kwargs
)
652 "Wait until action {} is completed in application {} (model={})".format(
653 action_name
, application_name
, model_name
656 await JujuModelWatcher
.wait_for(
659 progress_timeout
=progress_timeout
,
660 total_timeout
=total_timeout
,
665 output
= await model
.get_action_output(action_uuid
=action
.entity_id
)
666 status
= await model
.get_action_status(uuid_or_prefix
=action
.entity_id
)
668 status
[action
.entity_id
] if action
.entity_id
in status
else "failed"
672 "Action {} completed with status {} in application {} (model={})".format(
673 action_name
, action
.status
, application_name
, model_name
677 await self
.disconnect_model(model
)
678 await self
.disconnect_controller(controller
)
680 return output
, status
682 async def get_actions(self
, application_name
: str, model_name
: str) -> dict:
683 """Get list of actions
685 :param: application_name: Application name
686 :param: model_name: Model name
688 :return: Dict with this format
690 "action_name": "Description of the action",
695 "Getting list of actions for application {}".format(application_name
)
699 controller
= await self
.get_controller()
702 model
= await self
.get_model(controller
, model_name
)
706 application
= self
._get
_application
(
707 model
, application_name
=application_name
,
710 # Return list of actions
711 return await application
.get_actions()
714 # Disconnect from model and controller
715 await self
.disconnect_model(model
)
716 await self
.disconnect_controller(controller
)
718 async def get_metrics(self
, model_name
: str, application_name
: str) -> dict:
719 """Get the metrics collected by the VCA.
721 :param model_name The name or unique id of the network service
722 :param application_name The name of the application
724 if not model_name
or not application_name
:
725 raise Exception("model_name and application_name must be non-empty strings")
727 controller
= await self
.get_controller()
728 model
= await self
.get_model(controller
, model_name
)
730 application
= self
._get
_application
(model
, application_name
)
731 if application
is not None:
732 metrics
= await application
.get_metrics()
734 self
.disconnect_model(model
)
735 self
.disconnect_controller(controller
)
738 async def add_relation(
739 self
, model_name
: str, endpoint_1
: str, endpoint_2
: str,
743 :param: model_name: Model name
744 :param: endpoint_1 First endpoint name
745 ("app:endpoint" format or directly the saas name)
746 :param: endpoint_2: Second endpoint name (^ same format)
749 self
.log
.debug("Adding relation: {} -> {}".format(endpoint_1
, endpoint_2
))
752 controller
= await self
.get_controller()
755 model
= await self
.get_model(controller
, model_name
)
759 await model
.add_relation(endpoint_1
, endpoint_2
)
760 except JujuAPIError
as e
:
761 if "not found" in e
.message
:
762 self
.log
.warning("Relation not found: {}".format(e
.message
))
764 if "already exists" in e
.message
:
765 self
.log
.warning("Relation already exists: {}".format(e
.message
))
767 # another exception, raise it
770 await self
.disconnect_model(model
)
771 await self
.disconnect_controller(controller
)
774 self
, offer_url
: str, model_name
: str,
777 Adds a remote offer to the model. Relations can be created later using "juju relate".
779 :param: offer_url: Offer Url
780 :param: model_name: Model name
782 :raises ParseError if there's a problem parsing the offer_url
783 :raises JujuError if remote offer includes and endpoint
784 :raises JujuAPIError if the operation is not successful
786 controller
= await self
.get_controller()
787 model
= await controller
.get_model(model_name
)
790 await model
.consume(offer_url
)
792 await self
.disconnect_model(model
)
793 await self
.disconnect_controller(controller
)
795 async def destroy_model(self
, model_name
: str, total_timeout
: float):
799 :param: model_name: Model name
800 :param: total_timeout: Timeout
803 controller
= await self
.get_controller()
804 model
= await self
.get_model(controller
, model_name
)
806 self
.log
.debug("Destroying model {}".format(model_name
))
807 uuid
= model
.info
.uuid
810 machines
= await model
.get_machines()
811 for machine_id
in machines
:
813 await self
.destroy_machine(
814 model
, machine_id
=machine_id
, total_timeout
=total_timeout
,
816 except asyncio
.CancelledError
:
822 await self
.disconnect_model(model
)
825 if model_name
in self
.models
:
826 self
.models
.remove(model_name
)
828 await controller
.destroy_model(uuid
)
830 # Wait until model is destroyed
831 self
.log
.debug("Waiting for model {} to be destroyed...".format(model_name
))
834 if total_timeout
is None:
836 end
= time
.time() + total_timeout
837 while time
.time() < end
:
839 models
= await controller
.list_models()
840 if model_name
not in models
:
842 "The model {} ({}) was destroyed".format(model_name
, uuid
)
845 except asyncio
.CancelledError
:
847 except Exception as e
:
849 await asyncio
.sleep(5)
851 "Timeout waiting for model {} to be destroyed {}".format(
852 model_name
, last_exception
856 await self
.disconnect_controller(controller
)
858 async def destroy_application(self
, model
: Model
, application_name
: str):
862 :param: model: Model object
863 :param: application_name: Application name
866 "Destroying application {} in model {}".format(
867 application_name
, model
.info
.name
870 application
= model
.applications
.get(application_name
)
872 await application
.destroy()
874 self
.log
.warning("Application not found: {}".format(application_name
))
876 async def destroy_machine(
877 self
, model
: Model
, machine_id
: str, total_timeout
: float = 3600
882 :param: model: Model object
883 :param: machine_id: Machine id
884 :param: total_timeout: Timeout in seconds
886 machines
= await model
.get_machines()
887 if machine_id
in machines
:
888 machine
= machines
[machine_id
]
889 await machine
.destroy(force
=True)
891 end
= time
.time() + total_timeout
893 # wait for machine removal
894 machines
= await model
.get_machines()
895 while machine_id
in machines
and time
.time() < end
:
896 self
.log
.debug("Waiting for machine {} is destroyed".format(machine_id
))
897 await asyncio
.sleep(0.5)
898 machines
= await model
.get_machines()
899 self
.log
.debug("Machine destroyed: {}".format(machine_id
))
901 self
.log
.debug("Machine not found: {}".format(machine_id
))
903 async def configure_application(
904 self
, model_name
: str, application_name
: str, config
: dict = None
906 """Configure application
908 :param: model_name: Model name
909 :param: application_name: Application name
910 :param: config: Config to apply to the charm
912 self
.log
.debug("Configuring application {}".format(application_name
))
916 controller
= await self
.get_controller()
917 model
= await self
.get_model(controller
, model_name
)
918 application
= self
._get
_application
(
919 model
, application_name
=application_name
,
921 await application
.set_config(config
)
923 await self
.disconnect_model(model
)
924 await self
.disconnect_controller(controller
)
926 def _get_api_endpoints_db(self
) -> [str]:
928 Get API Endpoints from DB
930 :return: List of API endpoints
932 self
.log
.debug("Getting endpoints from database")
934 juju_info
= self
.db
.get_one(
935 DB_DATA
.api_endpoints
.table
,
936 q_filter
=DB_DATA
.api_endpoints
.filter,
939 if juju_info
and DB_DATA
.api_endpoints
.key
in juju_info
:
940 return juju_info
[DB_DATA
.api_endpoints
.key
]
942 def _update_api_endpoints_db(self
, endpoints
: [str]):
944 Update API endpoints in Database
946 :param: List of endpoints
948 self
.log
.debug("Saving endpoints {} in database".format(endpoints
))
950 juju_info
= self
.db
.get_one(
951 DB_DATA
.api_endpoints
.table
,
952 q_filter
=DB_DATA
.api_endpoints
.filter,
955 # If it doesn't, then create it
959 DB_DATA
.api_endpoints
.table
, DB_DATA
.api_endpoints
.filter,
961 except DbException
as e
:
962 # Racing condition: check if another N2VC worker has created it
963 juju_info
= self
.db
.get_one(
964 DB_DATA
.api_endpoints
.table
,
965 q_filter
=DB_DATA
.api_endpoints
.filter,
971 DB_DATA
.api_endpoints
.table
,
972 DB_DATA
.api_endpoints
.filter,
973 {DB_DATA
.api_endpoints
.key
: endpoints
},
976 def handle_exception(self
, loop
, context
):
977 # All unhandled exceptions by libjuju are handled here.
980 async def health_check(self
, interval
: float = 300.0):
982 Health check to make sure controller and controller_model connections are OK
984 :param: interval: Time in seconds between checks
988 controller
= await self
.get_controller()
989 # self.log.debug("VCA is alive")
990 except Exception as e
:
991 self
.log
.error("Health check to VCA failed: {}".format(e
))
993 await self
.disconnect_controller(controller
)
994 await asyncio
.sleep(interval
)
996 async def list_models(self
, contains
: str = None) -> [str]:
997 """List models with certain names
999 :param: contains: String that is contained in model name
1001 :retur: [models] Returns list of model names
1004 controller
= await self
.get_controller()
1006 models
= await controller
.list_models()
1008 models
= [model
for model
in models
if contains
in model
]
1011 await self
.disconnect_controller(controller
)
1013 async def list_offers(self
, model_name
: str) -> QueryApplicationOffersResults
:
1014 """List models with certain names
1016 :param: model_name: Model name
1018 :return: Returns list of offers
1021 controller
= await self
.get_controller()
1023 return await controller
.list_offers(model_name
)
1025 await self
.disconnect_controller(controller
)
1030 configuration
: Configuration
,
1032 credential_name
: str = None,
1035 Add a Kubernetes cloud to the controller
1037 Similar to the `juju add-k8s` command in the CLI
1039 :param: name: Name for the K8s cloud
1040 :param: configuration: Kubernetes configuration object
1041 :param: storage_class: Storage Class to use in the cloud
1042 :param: credential_name: Storage Class to use in the cloud
1045 if not storage_class
:
1046 raise Exception("storage_class must be a non-empty string")
1048 raise Exception("name must be a non-empty string")
1049 if not configuration
:
1050 raise Exception("configuration must be provided")
1052 endpoint
= configuration
.host
1053 credential
= self
.get_k8s_cloud_credential(configuration
)
1055 [credential
.attrs
["ClientCertificateData"]]
1056 if "ClientCertificateData" in credential
.attrs
1059 cloud
= client
.Cloud(
1061 auth_types
=[credential
.auth_type
],
1063 ca_certificates
=ca_certificates
,
1065 "operator-storage": storage_class
,
1066 "workload-storage": storage_class
,
1070 return await self
.add_cloud(
1071 name
, cloud
, credential
, credential_name
=credential_name
1074 def get_k8s_cloud_credential(
1075 self
, configuration
: Configuration
,
1076 ) -> client
.CloudCredential
:
1078 ca_cert
= configuration
.ssl_ca_cert
or configuration
.cert_file
1079 key
= configuration
.key_file
1080 api_key
= configuration
.api_key
1082 username
= configuration
.username
1083 password
= configuration
.password
1085 if "authorization" in api_key
:
1086 authorization
= api_key
["authorization"]
1087 if "Bearer " in authorization
:
1088 bearer_list
= authorization
.split(" ")
1089 if len(bearer_list
) == 2:
1090 [_
, token
] = bearer_list
1092 raise JujuInvalidK8sConfiguration("unknown format of api_key")
1094 token
= authorization
1096 attrs
["ClientCertificateData"] = open(ca_cert
, "r").read()
1098 attrs
["ClientKeyData"] = open(key
, "r").read()
1100 if username
or password
:
1101 raise JujuInvalidK8sConfiguration("Cannot set both token and user/pass")
1102 attrs
["Token"] = token
1106 auth_type
= "oauth2"
1108 raise JujuInvalidK8sConfiguration(
1109 "missing token for auth type {}".format(auth_type
)
1114 "credential for user {} has empty password".format(username
)
1116 attrs
["username"] = username
1117 attrs
["password"] = password
1119 auth_type
= "userpasswithcert"
1121 auth_type
= "userpass"
1122 elif ca_cert
and token
:
1123 auth_type
= "certificate"
1125 raise JujuInvalidK8sConfiguration("authentication method not supported")
1126 return client
.CloudCredential(auth_type
=auth_type
, attrs
=attrs
,)
1128 async def add_cloud(
1132 credential
: CloudCredential
= None,
1133 credential_name
: str = None,
1136 Add cloud to the controller
1138 :param: name: Name of the cloud to be added
1139 :param: cloud: Cloud object
1140 :param: credential: CloudCredentials object for the cloud
1141 :param: credential_name: Credential name.
1142 If not defined, cloud of the name will be used.
1144 controller
= await self
.get_controller()
1146 _
= await controller
.add_cloud(name
, cloud
)
1148 await controller
.add_credential(
1149 credential_name
or name
, credential
=credential
, cloud
=name
1151 # Need to return the object returned by the controller.add_cloud() function
1152 # I'm returning the original value now until this bug is fixed:
1153 # https://github.com/juju/python-libjuju/issues/443
1156 await self
.disconnect_controller(controller
)
1158 async def remove_cloud(self
, name
: str):
1162 :param: name: Name of the cloud to be removed
1164 controller
= await self
.get_controller()
1166 await controller
.remove_cloud(name
)
1168 await self
.disconnect_controller(controller
)