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 JujuModelAlreadyExists
,
44 JujuControllerFailedConnecting
,
45 JujuApplicationExists
,
46 JujuInvalidK8sConfiguration
,
48 from n2vc
.utils
import DB_DATA
49 from osm_common
.dbbase
import DbException
50 from kubernetes
.client
.configuration
import Configuration
52 RBAC_LABEL_KEY_NAME
= "rbac-id"
63 loop
: asyncio
.AbstractEventLoop
= None,
64 log
: logging
.Logger
= None,
66 n2vc
: N2VCConnector
= None,
67 apt_mirror
: str = None,
68 enable_os_upgrade
: bool = True,
73 :param: endpoint: Endpoint of the juju controller (host:port)
74 :param: api_proxy: Endpoint of the juju controller - Reachable from the VNFs
75 :param: username: Juju username
76 :param: password: Juju password
77 :param: cacert: Juju CA Certificate
78 :param: loop: Asyncio loop
81 :param: n2vc: N2VC object
82 :param: apt_mirror: APT Mirror
83 :param: enable_os_upgrade: Enable OS Upgrade
86 self
.log
= log
or logging
.getLogger("Libjuju")
88 db_endpoints
= self
._get
_api
_endpoints
_db
()
90 if (db_endpoints
and endpoint
not in db_endpoints
) or not db_endpoints
:
91 self
.endpoints
= [endpoint
]
92 self
._update
_api
_endpoints
_db
(self
.endpoints
)
94 self
.endpoints
= db_endpoints
95 self
.api_proxy
= api_proxy
96 self
.username
= username
97 self
.password
= password
99 self
.loop
= loop
or asyncio
.get_event_loop()
102 # Generate config for models
103 self
.model_config
= {}
105 self
.model_config
["apt-mirror"] = apt_mirror
106 self
.model_config
["enable-os-refresh-update"] = enable_os_upgrade
107 self
.model_config
["enable-os-upgrade"] = enable_os_upgrade
109 self
.loop
.set_exception_handler(self
.handle_exception
)
110 self
.creating_model
= asyncio
.Lock(loop
=self
.loop
)
113 self
.log
.debug("Libjuju initialized!")
115 self
.health_check_task
= self
._create
_health
_check
_task
()
117 def _create_health_check_task(self
):
118 return self
.loop
.create_task(self
.health_check())
120 async def get_controller(self
, timeout
: float = 15.0) -> Controller
:
124 :param: timeout: Time in seconds to wait for controller to connect
128 controller
= Controller(loop
=self
.loop
)
129 await asyncio
.wait_for(
131 endpoint
=self
.endpoints
,
132 username
=self
.username
,
133 password
=self
.password
,
138 endpoints
= await controller
.api_endpoints
139 if self
.endpoints
!= endpoints
:
140 self
.endpoints
= endpoints
141 self
._update
_api
_endpoints
_db
(self
.endpoints
)
143 except asyncio
.CancelledError
as e
:
145 except Exception as e
:
147 "Failed connecting to controller: {}...".format(self
.endpoints
)
150 await self
.disconnect_controller(controller
)
151 raise JujuControllerFailedConnecting(e
)
153 async def disconnect(self
):
155 # Cancel health check task
156 self
.health_check_task
.cancel()
157 self
.log
.debug("Libjuju disconnected!")
159 async def disconnect_model(self
, model
: Model
):
163 :param: model: Model that will be disconnected
165 await model
.disconnect()
167 async def disconnect_controller(self
, controller
: Controller
):
169 Disconnect controller
171 :param: controller: Controller that will be disconnected
174 await controller
.disconnect()
176 async def add_model(self
, model_name
: str, cloud_name
: str, credential_name
=None):
180 :param: model_name: Model name
181 :param: cloud_name: Cloud name
182 :param: credential_name: Credential name to use for adding the model
183 If not specified, same name as the cloud will be used.
187 controller
= await self
.get_controller()
190 # Raise exception if model already exists
191 if await self
.model_exists(model_name
, controller
=controller
):
192 raise JujuModelAlreadyExists(
193 "Model {} already exists.".format(model_name
)
196 # Block until other workers have finished model creation
197 while self
.creating_model
.locked():
198 await asyncio
.sleep(0.1)
200 # If the model exists, return it from the controller
201 if model_name
in self
.models
:
205 async with self
.creating_model
:
206 self
.log
.debug("Creating model {}".format(model_name
))
207 model
= await controller
.add_model(
209 config
=self
.model_config
,
210 cloud_name
=cloud_name
,
211 credential_name
=credential_name
or cloud_name
,
213 self
.models
.add(model_name
)
216 await self
.disconnect_model(model
)
217 await self
.disconnect_controller(controller
)
220 self
, controller
: Controller
, model_name
: str, id=None
223 Get model from controller
225 :param: controller: Controller
226 :param: model_name: Model name
228 :return: Model: The created Juju model object
230 return await controller
.get_model(model_name
)
232 async def model_exists(
233 self
, model_name
: str, controller
: Controller
= None
236 Check if model exists
238 :param: controller: Controller
239 :param: model_name: Model name
243 need_to_disconnect
= False
245 # Get controller if not passed
247 controller
= await self
.get_controller()
248 need_to_disconnect
= True
250 # Check if model exists
252 return model_name
in await controller
.list_models()
254 if need_to_disconnect
:
255 await self
.disconnect_controller(controller
)
257 async def models_exist(self
, model_names
: [str]) -> (bool, list):
259 Check if models exists
261 :param: model_names: List of strings with model names
263 :return (bool, list[str]): (True if all models exists, List of model names that don't exist)
267 "model_names must be a non-empty array. Given value: {}".format(
271 non_existing_models
= []
272 models
= await self
.list_models()
273 existing_models
= list(set(models
).intersection(model_names
))
274 non_existing_models
= list(set(model_names
) - set(existing_models
))
277 len(non_existing_models
) == 0,
281 async def get_model_status(self
, model_name
: str) -> FullStatus
:
285 :param: model_name: Model name
287 :return: Full status object
289 controller
= await self
.get_controller()
290 model
= await self
.get_model(controller
, model_name
)
292 return await model
.get_status()
294 await self
.disconnect_model(model
)
295 await self
.disconnect_controller(controller
)
297 async def create_machine(
300 machine_id
: str = None,
301 db_dict
: dict = None,
302 progress_timeout
: float = None,
303 total_timeout
: float = None,
304 series
: str = "xenial",
306 ) -> (Machine
, bool):
310 :param: model_name: Model name
311 :param: machine_id: Machine id
312 :param: db_dict: Dictionary with data of the DB to write the updates
313 :param: progress_timeout: Maximum time between two updates in the model
314 :param: total_timeout: Timeout for the entity to be active
315 :param: series: Series of the machine (xenial, bionic, focal, ...)
316 :param: wait: Wait until machine is ready
318 :return: (juju.machine.Machine, bool): Machine object and a boolean saying
319 if the machine is new or it already existed
325 "Creating machine (id={}) in model: {}".format(machine_id
, model_name
)
329 controller
= await self
.get_controller()
332 model
= await self
.get_model(controller
, model_name
)
334 if machine_id
is not None:
336 "Searching machine (id={}) in model {}".format(
337 machine_id
, model_name
341 # Get machines from model and get the machine with machine_id if exists
342 machines
= await model
.get_machines()
343 if machine_id
in machines
:
345 "Machine (id={}) found in model {}".format(
346 machine_id
, model_name
349 machine
= machines
[machine_id
]
351 raise JujuMachineNotFound("Machine {} not found".format(machine_id
))
354 self
.log
.debug("Creating a new machine in model {}".format(model_name
))
357 machine
= await model
.add_machine(
358 spec
=None, constraints
=None, disks
=None, series
=series
362 # Wait until the machine is ready
364 "Wait until machine {} is ready in model {}".format(
365 machine
.entity_id
, model_name
369 await JujuModelWatcher
.wait_for(
372 progress_timeout
=progress_timeout
,
373 total_timeout
=total_timeout
,
378 await self
.disconnect_model(model
)
379 await self
.disconnect_controller(controller
)
382 "Machine {} ready at {} in model {}".format(
383 machine
.entity_id
, machine
.dns_name
, model_name
388 async def provision_machine(
393 private_key_path
: str,
394 db_dict
: dict = None,
395 progress_timeout
: float = None,
396 total_timeout
: float = None,
399 Manually provisioning of a machine
401 :param: model_name: Model name
402 :param: hostname: IP to access the machine
403 :param: username: Username to login to the machine
404 :param: private_key_path: Local path for the private key
405 :param: db_dict: Dictionary with data of the DB to write the updates
406 :param: progress_timeout: Maximum time between two updates in the model
407 :param: total_timeout: Timeout for the entity to be active
409 :return: (Entity): Machine id
412 "Provisioning machine. model: {}, hostname: {}, username: {}".format(
413 model_name
, hostname
, username
418 controller
= await self
.get_controller()
421 model
= await self
.get_model(controller
, model_name
)
425 provisioner
= AsyncSSHProvisioner(
428 private_key_path
=private_key_path
,
433 params
= await provisioner
.provision_machine()
435 params
.jobs
= ["JobHostUnits"]
437 self
.log
.debug("Adding machine to model")
438 connection
= model
.connection()
439 client_facade
= client
.ClientFacade
.from_connection(connection
)
441 results
= await client_facade
.AddMachines(params
=[params
])
442 error
= results
.machines
[0].error
445 msg
= "Error adding machine: {}".format(error
.message
)
446 self
.log
.error(msg
=msg
)
447 raise ValueError(msg
)
449 machine_id
= results
.machines
[0].machine
451 self
.log
.debug("Installing Juju agent into machine {}".format(machine_id
))
452 asyncio
.ensure_future(
453 provisioner
.install_agent(
454 connection
=connection
,
456 machine_id
=machine_id
,
457 proxy
=self
.api_proxy
,
463 machine_list
= await model
.get_machines()
464 if machine_id
in machine_list
:
465 self
.log
.debug("Machine {} found in model!".format(machine_id
))
466 machine
= model
.machines
.get(machine_id
)
468 await asyncio
.sleep(2)
471 msg
= "Machine {} not found in model".format(machine_id
)
472 self
.log
.error(msg
=msg
)
473 raise JujuMachineNotFound(msg
)
476 "Wait until machine {} is ready in model {}".format(
477 machine
.entity_id
, model_name
480 await JujuModelWatcher
.wait_for(
483 progress_timeout
=progress_timeout
,
484 total_timeout
=total_timeout
,
488 except Exception as e
:
491 await self
.disconnect_model(model
)
492 await self
.disconnect_controller(controller
)
495 "Machine provisioned {} in model {}".format(machine_id
, model_name
)
501 self
, uri
: str, model_name
: str, wait
: bool = True, timeout
: float = 3600
504 Deploy bundle or charm: Similar to the juju CLI command `juju deploy`
506 :param: uri: Path or Charm Store uri in which the charm or bundle can be found
507 :param: model_name: Model name
508 :param: wait: Indicates whether to wait or not until all applications are active
509 :param: timeout: Time in seconds to wait until all applications are active
511 controller
= await self
.get_controller()
512 model
= await self
.get_model(controller
, model_name
)
514 await model
.deploy(uri
)
516 await JujuModelWatcher
.wait_for_model(model
, timeout
=timeout
)
517 self
.log
.debug("All units active in model {}".format(model_name
))
519 await self
.disconnect_model(model
)
520 await self
.disconnect_controller(controller
)
522 async def deploy_charm(
524 application_name
: str,
528 db_dict
: dict = None,
529 progress_timeout
: float = None,
530 total_timeout
: float = None,
537 :param: application_name: Application name
538 :param: path: Local path to the charm
539 :param: model_name: Model name
540 :param: machine_id ID of the machine
541 :param: db_dict: Dictionary with data of the DB to write the updates
542 :param: progress_timeout: Maximum time between two updates in the model
543 :param: total_timeout: Timeout for the entity to be active
544 :param: config: Config for the charm
545 :param: series: Series of the charm
546 :param: num_units: Number of units
548 :return: (juju.application.Application): Juju application
551 "Deploying charm {} to machine {} in model ~{}".format(
552 application_name
, machine_id
, model_name
555 self
.log
.debug("charm: {}".format(path
))
558 controller
= await self
.get_controller()
561 model
= await self
.get_model(controller
, model_name
)
565 if application_name
not in model
.applications
:
567 if machine_id
is not None:
568 if machine_id
not in model
.machines
:
569 msg
= "Machine {} not found in model".format(machine_id
)
570 self
.log
.error(msg
=msg
)
571 raise JujuMachineNotFound(msg
)
572 machine
= model
.machines
[machine_id
]
573 series
= machine
.series
575 application
= await model
.deploy(
577 application_name
=application_name
,
586 "Wait until application {} is ready in model {}".format(
587 application_name
, model_name
591 for _
in range(num_units
- 1):
592 m
, _
= await self
.create_machine(model_name
, wait
=False)
593 await application
.add_unit(to
=m
.entity_id
)
595 await JujuModelWatcher
.wait_for(
598 progress_timeout
=progress_timeout
,
599 total_timeout
=total_timeout
,
604 "Application {} is ready in model {}".format(
605 application_name
, model_name
609 raise JujuApplicationExists(
610 "Application {} exists".format(application_name
)
613 await self
.disconnect_model(model
)
614 await self
.disconnect_controller(controller
)
618 def _get_application(self
, model
: Model
, application_name
: str) -> Application
:
621 :param: model: Model object
622 :param: application_name: Application name
624 :return: juju.application.Application (or None if it doesn't exist)
626 if model
.applications
and application_name
in model
.applications
:
627 return model
.applications
[application_name
]
629 async def execute_action(
631 application_name
: str,
634 db_dict
: dict = None,
635 progress_timeout
: float = None,
636 total_timeout
: float = None,
641 :param: application_name: Application name
642 :param: model_name: Model name
643 :param: action_name: Name of the action
644 :param: db_dict: Dictionary with data of the DB to write the updates
645 :param: progress_timeout: Maximum time between two updates in the model
646 :param: total_timeout: Timeout for the entity to be active
648 :return: (str, str): (output and status)
651 "Executing action {} using params {}".format(action_name
, kwargs
)
654 controller
= await self
.get_controller()
657 model
= await self
.get_model(controller
, model_name
)
661 application
= self
._get
_application
(
663 application_name
=application_name
,
665 if application
is None:
666 raise JujuApplicationNotFound("Cannot execute action")
670 # Ocassionally, self._get_leader_unit() will return None
671 # because the leader elected hook has not been triggered yet.
672 # Therefore, we are doing some retries. If it happens again,
675 time_between_retries
= 10
677 for _
in range(attempts
):
678 unit
= await self
._get
_leader
_unit
(application
)
680 await asyncio
.sleep(time_between_retries
)
684 raise JujuLeaderUnitNotFound(
685 "Cannot execute action: leader unit not found"
688 actions
= await application
.get_actions()
690 if action_name
not in actions
:
691 raise JujuActionNotFound(
692 "Action {} not in available actions".format(action_name
)
695 action
= await unit
.run_action(action_name
, **kwargs
)
698 "Wait until action {} is completed in application {} (model={})".format(
699 action_name
, application_name
, model_name
702 await JujuModelWatcher
.wait_for(
705 progress_timeout
=progress_timeout
,
706 total_timeout
=total_timeout
,
711 output
= await model
.get_action_output(action_uuid
=action
.entity_id
)
712 status
= await model
.get_action_status(uuid_or_prefix
=action
.entity_id
)
714 status
[action
.entity_id
] if action
.entity_id
in status
else "failed"
718 "Action {} completed with status {} in application {} (model={})".format(
719 action_name
, action
.status
, application_name
, model_name
723 await self
.disconnect_model(model
)
724 await self
.disconnect_controller(controller
)
726 return output
, status
728 async def get_actions(self
, application_name
: str, model_name
: str) -> dict:
729 """Get list of actions
731 :param: application_name: Application name
732 :param: model_name: Model name
734 :return: Dict with this format
736 "action_name": "Description of the action",
741 "Getting list of actions for application {}".format(application_name
)
745 controller
= await self
.get_controller()
748 model
= await self
.get_model(controller
, model_name
)
752 application
= self
._get
_application
(
754 application_name
=application_name
,
757 # Return list of actions
758 return await application
.get_actions()
761 # Disconnect from model and controller
762 await self
.disconnect_model(model
)
763 await self
.disconnect_controller(controller
)
765 async def get_metrics(self
, model_name
: str, application_name
: str) -> dict:
766 """Get the metrics collected by the VCA.
768 :param model_name The name or unique id of the network service
769 :param application_name The name of the application
771 if not model_name
or not application_name
:
772 raise Exception("model_name and application_name must be non-empty strings")
774 controller
= await self
.get_controller()
775 model
= await self
.get_model(controller
, model_name
)
777 application
= self
._get
_application
(model
, application_name
)
778 if application
is not None:
779 metrics
= await application
.get_metrics()
781 self
.disconnect_model(model
)
782 self
.disconnect_controller(controller
)
785 async def add_relation(
793 :param: model_name: Model name
794 :param: endpoint_1 First endpoint name
795 ("app:endpoint" format or directly the saas name)
796 :param: endpoint_2: Second endpoint name (^ same format)
799 self
.log
.debug("Adding relation: {} -> {}".format(endpoint_1
, endpoint_2
))
802 controller
= await self
.get_controller()
805 model
= await self
.get_model(controller
, model_name
)
809 await model
.add_relation(endpoint_1
, endpoint_2
)
810 except JujuAPIError
as e
:
811 if "not found" in e
.message
:
812 self
.log
.warning("Relation not found: {}".format(e
.message
))
814 if "already exists" in e
.message
:
815 self
.log
.warning("Relation already exists: {}".format(e
.message
))
817 # another exception, raise it
820 await self
.disconnect_model(model
)
821 await self
.disconnect_controller(controller
)
829 Adds a remote offer to the model. Relations can be created later using "juju relate".
831 :param: offer_url: Offer Url
832 :param: model_name: Model name
834 :raises ParseError if there's a problem parsing the offer_url
835 :raises JujuError if remote offer includes and endpoint
836 :raises JujuAPIError if the operation is not successful
838 controller
= await self
.get_controller()
839 model
= await controller
.get_model(model_name
)
842 await model
.consume(offer_url
)
844 await self
.disconnect_model(model
)
845 await self
.disconnect_controller(controller
)
847 async def destroy_model(self
, model_name
: str, total_timeout
: float):
851 :param: model_name: Model name
852 :param: total_timeout: Timeout
855 controller
= await self
.get_controller()
858 model
= await self
.get_model(controller
, model_name
)
859 self
.log
.debug("Destroying model {}".format(model_name
))
860 uuid
= model
.info
.uuid
862 # Destroy machines that are manually provisioned
863 # and still are in pending state
864 await self
._destroy
_pending
_machines
(model
, only_manual
=True)
867 await self
.disconnect_model(model
)
870 if model_name
in self
.models
:
871 self
.models
.remove(model_name
)
873 await controller
.destroy_model(uuid
, force
=True, max_wait
=0)
875 # Wait until model is destroyed
876 self
.log
.debug("Waiting for model {} to be destroyed...".format(model_name
))
878 if total_timeout
is None:
880 end
= time
.time() + total_timeout
881 while time
.time() < end
:
882 models
= await controller
.list_models()
883 if model_name
not in models
:
885 "The model {} ({}) was destroyed".format(model_name
, uuid
)
888 await asyncio
.sleep(5)
890 "Timeout waiting for model {} to be destroyed".format(model_name
)
892 except Exception as e
:
894 await self
.disconnect_model(model
)
897 await self
.disconnect_controller(controller
)
899 async def destroy_application(self
, model
: Model
, application_name
: str):
903 :param: model: Model object
904 :param: application_name: Application name
907 "Destroying application {} in model {}".format(
908 application_name
, model
.info
.name
911 application
= model
.applications
.get(application_name
)
913 await application
.destroy()
915 self
.log
.warning("Application not found: {}".format(application_name
))
917 async def _destroy_pending_machines(self
, model
: Model
, only_manual
: bool = False):
919 Destroy pending machines in a given model
921 :param: only_manual: Bool that indicates only manually provisioned
922 machines should be destroyed (if True), or that
923 all pending machines should be destroyed
925 status
= await model
.get_status()
926 for machine_id
in status
.machines
:
927 machine_status
= status
.machines
[machine_id
]
928 if machine_status
.agent_status
.status
== "pending":
929 if only_manual
and not machine_status
.instance_id
.startswith("manual:"):
931 machine
= model
.machines
[machine_id
]
932 await machine
.destroy(force
=True)
934 # async def destroy_machine(
935 # self, model: Model, machine_id: str, total_timeout: float = 3600
940 # :param: model: Model object
941 # :param: machine_id: Machine id
942 # :param: total_timeout: Timeout in seconds
944 # machines = await model.get_machines()
945 # if machine_id in machines:
946 # machine = machines[machine_id]
947 # await machine.destroy(force=True)
949 # end = time.time() + total_timeout
951 # # wait for machine removal
952 # machines = await model.get_machines()
953 # while machine_id in machines and time.time() < end:
954 # self.log.debug("Waiting for machine {} is destroyed".format(machine_id))
955 # await asyncio.sleep(0.5)
956 # machines = await model.get_machines()
957 # self.log.debug("Machine destroyed: {}".format(machine_id))
959 # self.log.debug("Machine not found: {}".format(machine_id))
961 async def configure_application(
962 self
, model_name
: str, application_name
: str, config
: dict = None
964 """Configure application
966 :param: model_name: Model name
967 :param: application_name: Application name
968 :param: config: Config to apply to the charm
970 self
.log
.debug("Configuring application {}".format(application_name
))
973 controller
= await self
.get_controller()
976 model
= await self
.get_model(controller
, model_name
)
977 application
= self
._get
_application
(
979 application_name
=application_name
,
981 await application
.set_config(config
)
984 await self
.disconnect_model(model
)
985 await self
.disconnect_controller(controller
)
987 def _get_api_endpoints_db(self
) -> [str]:
989 Get API Endpoints from DB
991 :return: List of API endpoints
993 self
.log
.debug("Getting endpoints from database")
995 juju_info
= self
.db
.get_one(
996 DB_DATA
.api_endpoints
.table
,
997 q_filter
=DB_DATA
.api_endpoints
.filter,
1000 if juju_info
and DB_DATA
.api_endpoints
.key
in juju_info
:
1001 return juju_info
[DB_DATA
.api_endpoints
.key
]
1003 def _update_api_endpoints_db(self
, endpoints
: [str]):
1005 Update API endpoints in Database
1007 :param: List of endpoints
1009 self
.log
.debug("Saving endpoints {} in database".format(endpoints
))
1011 juju_info
= self
.db
.get_one(
1012 DB_DATA
.api_endpoints
.table
,
1013 q_filter
=DB_DATA
.api_endpoints
.filter,
1014 fail_on_empty
=False,
1016 # If it doesn't, then create it
1020 DB_DATA
.api_endpoints
.table
,
1021 DB_DATA
.api_endpoints
.filter,
1023 except DbException
as e
:
1024 # Racing condition: check if another N2VC worker has created it
1025 juju_info
= self
.db
.get_one(
1026 DB_DATA
.api_endpoints
.table
,
1027 q_filter
=DB_DATA
.api_endpoints
.filter,
1028 fail_on_empty
=False,
1033 DB_DATA
.api_endpoints
.table
,
1034 DB_DATA
.api_endpoints
.filter,
1035 {DB_DATA
.api_endpoints
.key
: endpoints
},
1038 def handle_exception(self
, loop
, context
):
1039 # All unhandled exceptions by libjuju are handled here.
1042 async def health_check(self
, interval
: float = 300.0):
1044 Health check to make sure controller and controller_model connections are OK
1046 :param: interval: Time in seconds between checks
1051 controller
= await self
.get_controller()
1052 # self.log.debug("VCA is alive")
1053 except Exception as e
:
1054 self
.log
.error("Health check to VCA failed: {}".format(e
))
1056 await self
.disconnect_controller(controller
)
1057 await asyncio
.sleep(interval
)
1059 async def list_models(self
, contains
: str = None) -> [str]:
1060 """List models with certain names
1062 :param: contains: String that is contained in model name
1064 :retur: [models] Returns list of model names
1067 controller
= await self
.get_controller()
1069 models
= await controller
.list_models()
1071 models
= [model
for model
in models
if contains
in model
]
1074 await self
.disconnect_controller(controller
)
1076 async def list_offers(self
, model_name
: str) -> QueryApplicationOffersResults
:
1077 """List models with certain names
1079 :param: model_name: Model name
1081 :return: Returns list of offers
1084 controller
= await self
.get_controller()
1086 return await controller
.list_offers(model_name
)
1088 await self
.disconnect_controller(controller
)
1095 client_cert_data
: str,
1096 configuration
: Configuration
,
1098 credential_name
: str = None,
1101 Add a Kubernetes cloud to the controller
1103 Similar to the `juju add-k8s` command in the CLI
1105 :param: name: Name for the K8s cloud
1106 :param: configuration: Kubernetes configuration object
1107 :param: storage_class: Storage Class to use in the cloud
1108 :param: credential_name: Storage Class to use in the cloud
1111 if not storage_class
:
1112 raise Exception("storage_class must be a non-empty string")
1114 raise Exception("name must be a non-empty string")
1115 if not configuration
:
1116 raise Exception("configuration must be provided")
1118 endpoint
= configuration
.host
1119 credential
= self
.get_k8s_cloud_credential(
1124 credential
.attrs
[RBAC_LABEL_KEY_NAME
] = rbac_id
1125 cloud
= client
.Cloud(
1127 auth_types
=[credential
.auth_type
],
1129 ca_certificates
=[client_cert_data
],
1131 "operator-storage": storage_class
,
1132 "workload-storage": storage_class
,
1136 return await self
.add_cloud(
1137 name
, cloud
, credential
, credential_name
=credential_name
1140 def get_k8s_cloud_credential(
1142 configuration
: Configuration
,
1143 client_cert_data
: str,
1145 ) -> client
.CloudCredential
:
1147 # TODO: Test with AKS
1148 key
= None # open(configuration.key_file, "r").read()
1149 username
= configuration
.username
1150 password
= configuration
.password
1152 if client_cert_data
:
1153 attrs
["ClientCertificateData"] = client_cert_data
1155 attrs
["ClientKeyData"] = key
1157 if username
or password
:
1158 raise JujuInvalidK8sConfiguration("Cannot set both token and user/pass")
1159 attrs
["Token"] = token
1163 auth_type
= "oauth2"
1164 if client_cert_data
:
1165 auth_type
= "oauth2withcert"
1167 raise JujuInvalidK8sConfiguration(
1168 "missing token for auth type {}".format(auth_type
)
1173 "credential for user {} has empty password".format(username
)
1175 attrs
["username"] = username
1176 attrs
["password"] = password
1177 if client_cert_data
:
1178 auth_type
= "userpasswithcert"
1180 auth_type
= "userpass"
1181 elif client_cert_data
and token
:
1182 auth_type
= "certificate"
1184 raise JujuInvalidK8sConfiguration("authentication method not supported")
1185 return client
.CloudCredential(auth_type
=auth_type
, attrs
=attrs
)
1187 async def add_cloud(
1191 credential
: CloudCredential
= None,
1192 credential_name
: str = None,
1195 Add cloud to the controller
1197 :param: name: Name of the cloud to be added
1198 :param: cloud: Cloud object
1199 :param: credential: CloudCredentials object for the cloud
1200 :param: credential_name: Credential name.
1201 If not defined, cloud of the name will be used.
1203 controller
= await self
.get_controller()
1205 _
= await controller
.add_cloud(name
, cloud
)
1207 await controller
.add_credential(
1208 credential_name
or name
, credential
=credential
, cloud
=name
1210 # Need to return the object returned by the controller.add_cloud() function
1211 # I'm returning the original value now until this bug is fixed:
1212 # https://github.com/juju/python-libjuju/issues/443
1215 await self
.disconnect_controller(controller
)
1217 async def remove_cloud(self
, name
: str):
1221 :param: name: Name of the cloud to be removed
1223 controller
= await self
.get_controller()
1225 await controller
.remove_cloud(name
)
1227 await self
.disconnect_controller(controller
)
1229 async def _get_leader_unit(self
, application
: Application
) -> Unit
:
1231 for u
in application
.units
:
1232 if await u
.is_leader_from_status():
1237 async def get_cloud_credentials(self
, cloud_name
: str, credential_name
: str):
1238 controller
= await self
.get_controller()
1240 facade
= client
.CloudFacade
.from_connection(controller
.connection())
1241 cloud_cred_tag
= tag
.credential(cloud_name
, self
.username
, credential_name
)
1242 params
= [client
.Entity(cloud_cred_tag
)]
1243 return (await facade
.Credential(params
)).results
1245 await self
.disconnect_controller(controller
)