5ed073f8d61283428af72f61cf45e9886b571b91
15 from juju
.errors
import JujuAPIError
, JujuConnectionError
17 log
= logging
.getLogger("websocket")
24 # Connect to an arbitrary api server
25 client = await Connection.connect(
26 api_endpoint, model_uuid, username, password, cacert)
28 # Connect using a controller/model name
29 client = await Connection.connect_model('local.local:default')
31 # Connect to the currently active model
32 client = await Connection.connect_current()
36 self
, endpoint
, uuid
, username
, password
, cacert
=None,
38 self
.endpoint
= endpoint
40 self
.username
= username
41 self
.password
= password
42 self
.macaroons
= macaroons
45 self
.__request
_id
__ = 0
56 def _get_ssl(self
, cert
=None):
57 return ssl
.create_default_context(
58 purpose
=ssl
.Purpose
.CLIENT_AUTH
, cadata
=cert
)
62 url
= "wss://{}/model/{}/api".format(self
.endpoint
, self
.uuid
)
64 url
= "wss://{}/api".format(self
.endpoint
)
67 kw
['ssl'] = self
._get
_ssl
(self
.cacert
)
69 self
.ws
= await websockets
.connect(url
, **kw
)
70 log
.info("Driver connected to juju %s", url
)
73 async def close(self
):
77 result
= await self
.ws
.recv()
78 if result
is not None:
79 result
= json
.loads(result
)
82 async def rpc(self
, msg
, encoder
=None):
83 self
.__request
_id
__ += 1
84 msg
['request-id'] = self
.__request
_id
__
85 if'params' not in msg
:
87 if "version" not in msg
:
88 msg
['version'] = self
.facades
[msg
['type']]
89 outgoing
= json
.dumps(msg
, indent
=2, cls
=encoder
)
90 await self
.ws
.send(outgoing
)
91 result
= await self
.recv()
92 if result
and 'error' in result
:
93 raise JujuAPIError(result
)
96 async def clone(self
):
97 """Return a new Connection, connected to the same websocket endpoint
101 return await Connection
.connect(
110 async def controller(self
):
111 """Return a Connection to the controller at self.endpoint
114 return await Connection
.connect(
125 cls
, endpoint
, uuid
, username
, password
, cacert
=None,
127 """Connect to the websocket.
129 If uuid is None, the connection will be to the controller. Otherwise it
130 will be to the model.
133 client
= cls(endpoint
, uuid
, username
, password
, cacert
, macaroons
)
136 redirect_info
= await client
.redirect_info()
137 if not redirect_info
:
138 await client
.login(username
, password
, macaroons
)
143 s
for servers
in redirect_info
['servers']
144 for s
in servers
if s
["scope"] == 'public'
146 for server
in servers
:
148 "{value}:{port}".format(**server
), uuid
, username
,
149 password
, redirect_info
['ca-cert'], macaroons
)
152 result
= await client
.login(username
, password
, macaroons
)
153 if 'discharge-required-error' in result
:
156 except Exception as e
:
161 "Couldn't authenticate to %s", endpoint
)
164 async def connect_current(cls
):
165 """Connect to the currently active model.
168 jujudata
= JujuData()
169 controller_name
= jujudata
.current_controller()
170 models
= jujudata
.models()[controller_name
]
171 model_name
= models
['current-model']
173 return await cls
.connect_model(
174 '{}:{}'.format(controller_name
, model_name
))
177 async def connect_current_controller(cls
):
178 """Connect to the currently active controller.
181 jujudata
= JujuData()
182 controller_name
= jujudata
.current_controller()
183 if not controller_name
:
184 raise JujuConnectionError('No current controller')
186 return await cls
.connect_controller(controller_name
)
189 async def connect_controller(cls
, controller_name
):
190 """Connect to a controller by name.
193 jujudata
= JujuData()
194 controller
= jujudata
.controllers()[controller_name
]
195 endpoint
= controller
['api-endpoints'][0]
196 cacert
= controller
.get('ca-cert')
197 accounts
= jujudata
.accounts()[controller_name
]
198 username
= accounts
['user']
199 password
= accounts
.get('password')
200 macaroons
= get_macaroons() if not password
else None
202 return await cls
.connect(
203 endpoint
, None, username
, password
, cacert
, macaroons
)
206 async def connect_model(cls
, model
):
207 """Connect to a model by name.
209 :param str model: <controller>:<model>
212 controller_name
, model_name
= model
.split(':')
214 jujudata
= JujuData()
215 controller
= jujudata
.controllers()[controller_name
]
216 endpoint
= controller
['api-endpoints'][0]
217 cacert
= controller
.get('ca-cert')
218 accounts
= jujudata
.accounts()[controller_name
]
219 username
= accounts
['user']
220 password
= accounts
.get('password')
221 models
= jujudata
.models()[controller_name
]
222 model_uuid
= models
['models'][model_name
]['uuid']
223 macaroons
= get_macaroons() if not password
else None
225 return await cls
.connect(
226 endpoint
, model_uuid
, username
, password
, cacert
, macaroons
)
228 def build_facades(self
, info
):
231 self
.facades
[facade
['name']] = facade
['versions'][-1]
233 async def login(self
, username
, password
, macaroons
=None):
238 if username
and not username
.startswith('user-'):
239 username
= 'user-{}'.format(username
)
241 result
= await self
.rpc({
246 "auth-tag": username
,
247 "credentials": password
,
248 "nonce": "".join(random
.sample(string
.printable
, 12)),
249 "macaroons": macaroons
or []
251 response
= result
['response']
252 self
.build_facades(response
.get('facades', {}))
253 self
.info
= response
.copy()
256 async def redirect_info(self
):
258 result
= await self
.rpc({
260 "request": "RedirectInfo",
263 except JujuAPIError
as e
:
264 if e
.message
== 'not redirected':
267 return result
['response']
272 self
.path
= os
.environ
.get('JUJU_DATA') or '~/.local/share/juju'
273 self
.path
= os
.path
.abspath(os
.path
.expanduser(self
.path
))
275 def current_controller(self
):
276 cmd
= shlex
.split('juju list-controllers --format yaml')
277 output
= subprocess
.check_output(cmd
)
278 output
= yaml
.safe_load(output
)
279 return output
.get('current-controller', '')
281 def controllers(self
):
282 return self
._load
_yaml
('controllers.yaml', 'controllers')
285 return self
._load
_yaml
('models.yaml', 'controllers')
288 return self
._load
_yaml
('accounts.yaml', 'controllers')
290 def _load_yaml(self
, filename
, key
):
291 filepath
= os
.path
.join(self
.path
, filename
)
292 with io
.open(filepath
, 'rt') as f
:
293 return yaml
.safe_load(f
)[key
]
297 """Decode and return macaroons from default ~/.go-cookies
301 cookie_file
= os
.path
.expanduser('~/.go-cookies')
302 with
open(cookie_file
, 'r') as f
:
303 cookies
= json
.load(f
)
304 except (OSError, ValueError) as e
:
305 log
.warn("Couldn't load macaroons from %s", cookie_file
)
309 c
['Value'] for c
in cookies
310 if c
['Name'].startswith('macaroon-') and c
['Value']
314 json
.loads(base64
.b64decode(value
).decode('utf-8'))
315 for value
in base64_macaroons