1 # Copyright 2023 Canonical Ltd.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at
6 # http://www.apache.org/licenses/LICENSE-2.0
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
16 from dataclasses
import dataclass
17 from typing
import List
19 from juju
.controller
import Controller
20 from juju
.errors
import JujuError
21 from juju
.model
import Model
23 from n2vc
.exceptions
import (
24 JujuApplicationExists
,
25 JujuControllerFailedConnecting
,
26 JujuModelAlreadyExists
,
32 """Information to connect to juju controller"""
39 cloud_credentials
: str
42 return f
"{self.__class__.__name__}(endpoint: {self.endpoint}, user: {self.user}, password: ******, caert: ******)"
45 return f
"{self.__class__.__name__}(endpoint: {self.endpoint}, user: {self.user}, password: ******, caert: ******)"
49 def __init__(self
, connection_info
: ConnectionInfo
) -> None:
50 self
.logger
= logging
.getLogger("temporal_libjuju")
51 self
.connection_info
= connection_info
53 async def get_controller(self
) -> Controller
:
54 controller
= Controller()
56 await controller
.connect(
57 endpoint
=self
.connection_info
.endpoint
,
58 username
=self
.connection_info
.user
,
59 password
=self
.connection_info
.password
,
60 cacert
=self
.connection_info
.cacert
,
63 except Exception as e
:
65 "Error connecting to controller={}: {}".format(
66 self
.connection_info
.endpoint
, e
69 await self
.disconnect_controller(controller
)
70 raise JujuControllerFailedConnecting(str(e
))
72 async def disconnect_controller(self
, controller
: Controller
) -> None:
74 await controller
.disconnect()
76 async def disconnect_model(self
, model
: Model
):
78 await model
.disconnect()
80 async def add_model(self
, model_name
: str):
81 """Exception is raised if model_name already exists"""
85 controller
= await self
.get_controller()
86 if await self
.model_exists(model_name
, controller
=controller
):
87 raise JujuModelAlreadyExists(
88 "Cannot create model {}".format(model_name
)
90 self
.logger
.debug("Creating model {}".format(model_name
))
91 model
= await controller
.add_model(
93 cloud_name
=self
.connection_info
.cloud_name
,
94 credential_name
=self
.connection_info
.cloud_credentials
,
97 await self
.disconnect_model(model
)
98 await self
.disconnect_controller(controller
)
100 async def model_exists(
101 self
, model_name
: str, controller
: Controller
= None
103 """Returns True if model exists. False otherwhise."""
104 need_to_disconnect
= False
107 controller
= await self
.get_controller()
108 need_to_disconnect
= True
110 return model_name
in await controller
.list_models()
112 if need_to_disconnect
:
113 await self
.disconnect_controller(controller
)
115 async def get_model(self
, controller
: Controller
, model_name
: str) -> Model
:
116 return await controller
.get_model(model_name
)
118 async def list_models(self
) -> List
[str]:
119 """List models in controller."""
121 controller
= await self
.get_controller()
122 return await controller
.list_models()
124 await self
.disconnect_controller(controller
)
126 async def deploy_charm(
128 application_name
: str,
134 channel
: str = "stable",
138 application_name (str): Application name.
139 path (str): Local path to the charm.
140 model_name (str): Model name.
141 config (dict): Config for the charm.
142 series (str): Series of the charm.
143 num_units (str): Number of units to deploy.
144 channel (str): Charm store channel from which to retrieve the charm.
147 (juju.application.Application): Juju application
150 "Deploying charm {} in model {}".format(application_name
, model_name
)
152 self
.logger
.debug("charm: {}".format(path
))
156 controller
= await self
.get_controller()
157 model
= await self
.get_model(controller
, model_name
)
158 if application_name
in model
.applications
:
159 raise JujuApplicationExists(
160 "Application {} exists".format(application_name
)
162 application
= await model
.deploy(
164 application_name
=application_name
,
172 "Wait until application {} is ready in model {}".format(
173 application_name
, model_name
176 await self
.wait_app_deployment_completion(application_name
, model_name
)
178 except JujuError
as e
:
179 if "already exists" in e
.message
:
180 raise JujuApplicationExists(
181 "Application {} exists".format(application_name
)
186 await self
.disconnect_model(model
)
187 await self
.disconnect_controller(controller
)
191 async def wait_app_deployment_completion(
192 self
, application_name
: str, model_name
: str
195 "Application {} is ready in model {}".format(application_name
, model_name
)
198 async def destroy_model(self
, model_name
: str, force
=False) -> None:
203 controller
= await self
.get_controller()
204 if not await self
.model_exists(model_name
, controller
=controller
):
205 self
.logger
.warn(f
"Model {model_name} doesn't exist")
208 self
.logger
.debug(f
"Getting model {model_name} to destroy")
209 model
= await self
.get_model(controller
, model_name
)
210 await self
.disconnect_model(model
)
212 await controller
.destroy_model(
213 model_name
, destroy_storage
=True, force
=force
, max_wait
=60
216 except Exception as e
:
217 self
.logger
.warn(f
"Failed deleting model {model_name}: {e}")
220 await self
.disconnect_model(model
)
221 await self
.disconnect_controller(controller
)