12 from http
.client
import HTTPSConnection
18 from juju
.errors
import JujuError
, JujuAPIError
, JujuConnectionError
20 log
= logging
.getLogger("websocket")
27 # Connect to an arbitrary api server
28 client = await Connection.connect(
29 api_endpoint, model_uuid, username, password, cacert)
31 # Connect using a controller/model name
32 client = await Connection.connect_model('local.local:default')
34 # Connect to the currently active model
35 client = await Connection.connect_current()
39 self
, endpoint
, uuid
, username
, password
, cacert
=None,
40 macaroons
=None, loop
=None):
41 self
.endpoint
= endpoint
43 self
.username
= username
44 self
.password
= password
45 self
.macaroons
= macaroons
47 self
.loop
= loop
or asyncio
.get_event_loop()
49 self
.__request
_id
__ = 0
60 def _get_ssl(self
, cert
=None):
61 return ssl
.create_default_context(
62 purpose
=ssl
.Purpose
.CLIENT_AUTH
, cadata
=cert
)
66 url
= "wss://{}/model/{}/api".format(self
.endpoint
, self
.uuid
)
68 url
= "wss://{}/api".format(self
.endpoint
)
71 kw
['ssl'] = self
._get
_ssl
(self
.cacert
)
72 kw
['loop'] = self
.loop
74 self
.ws
= await websockets
.connect(url
, **kw
)
75 log
.info("Driver connected to juju %s", url
)
78 async def close(self
):
82 result
= await self
.ws
.recv()
83 if result
is not None:
84 result
= json
.loads(result
)
87 async def rpc(self
, msg
, encoder
=None):
88 self
.__request
_id
__ += 1
89 msg
['request-id'] = self
.__request
_id
__
90 if'params' not in msg
:
92 if "version" not in msg
:
93 msg
['version'] = self
.facades
[msg
['type']]
94 outgoing
= json
.dumps(msg
, indent
=2, cls
=encoder
)
95 await self
.ws
.send(outgoing
)
96 result
= await self
.recv()
97 if result
and 'error' in result
:
98 raise JujuAPIError(result
)
101 def http_headers(self
):
102 """Return dictionary of http headers necessary for making an http
103 connection to the endpoint of this Connection.
105 :return: Dictionary of headers
108 if not self
.username
:
111 creds
= u
'{}:{}'.format(
112 tag
.user(self
.username
),
115 token
= base64
.b64encode(creds
.encode())
117 'Authorization': 'Basic {}'.format(token
.decode())
120 def https_connection(self
):
121 """Return an https connection to this Connection's endpoint.
123 Returns a 3-tuple containing::
125 1. The :class:`HTTPSConnection` instance
126 2. Dictionary of auth headers to be used with the connection
127 3. The root url path (str) to be used for requests.
130 endpoint
= self
.endpoint
131 host
, remainder
= endpoint
.split(':', 1)
134 port
, _
= remainder
.split('/', 1)
136 conn
= HTTPSConnection(
138 context
=self
._get
_ssl
(self
.cacert
),
142 "/model/{}".format(self
.uuid
)
145 return conn
, self
.http_headers(), path
147 async def clone(self
):
148 """Return a new Connection, connected to the same websocket endpoint
152 return await Connection
.connect(
162 async def controller(self
):
163 """Return a Connection to the controller at self.endpoint
166 return await Connection
.connect(
178 cls
, endpoint
, uuid
, username
, password
, cacert
=None,
179 macaroons
=None, loop
=None):
180 """Connect to the websocket.
182 If uuid is None, the connection will be to the controller. Otherwise it
183 will be to the model.
186 client
= cls(endpoint
, uuid
, username
, password
, cacert
, macaroons
,
190 redirect_info
= await client
.redirect_info()
191 if not redirect_info
:
192 await client
.login(username
, password
, macaroons
)
197 s
for servers
in redirect_info
['servers']
198 for s
in servers
if s
["scope"] == 'public'
200 for server
in servers
:
202 "{value}:{port}".format(**server
), uuid
, username
,
203 password
, redirect_info
['ca-cert'], macaroons
)
206 result
= await client
.login(username
, password
, macaroons
)
207 if 'discharge-required-error' in result
:
210 except Exception as e
:
215 "Couldn't authenticate to %s", endpoint
)
218 async def connect_current(cls
, loop
=None):
219 """Connect to the currently active model.
222 jujudata
= JujuData()
223 controller_name
= jujudata
.current_controller()
224 model_name
= jujudata
.current_model()
226 return await cls
.connect_model(
227 '{}:{}'.format(controller_name
, model_name
), loop
)
230 async def connect_current_controller(cls
, loop
=None):
231 """Connect to the currently active controller.
234 jujudata
= JujuData()
235 controller_name
= jujudata
.current_controller()
236 if not controller_name
:
237 raise JujuConnectionError('No current controller')
239 return await cls
.connect_controller(controller_name
, loop
)
242 async def connect_controller(cls
, controller_name
, loop
=None):
243 """Connect to a controller by name.
246 jujudata
= JujuData()
247 controller
= jujudata
.controllers()[controller_name
]
248 endpoint
= controller
['api-endpoints'][0]
249 cacert
= controller
.get('ca-cert')
250 accounts
= jujudata
.accounts()[controller_name
]
251 username
= accounts
['user']
252 password
= accounts
.get('password')
253 macaroons
= get_macaroons() if not password
else None
255 return await cls
.connect(
256 endpoint
, None, username
, password
, cacert
, macaroons
, loop
)
259 async def connect_model(cls
, model
, loop
=None):
260 """Connect to a model by name.
262 :param str model: [<controller>:]<model>
265 jujudata
= JujuData()
268 controller_name
, model_name
= model
.split(':')
270 controller_name
= jujudata
.current_controller()
273 controller
= jujudata
.controllers()[controller_name
]
274 endpoint
= controller
['api-endpoints'][0]
275 cacert
= controller
.get('ca-cert')
276 accounts
= jujudata
.accounts()[controller_name
]
277 username
= accounts
['user']
278 password
= accounts
.get('password')
279 models
= jujudata
.models()[controller_name
]
280 if '/' not in model_name
:
281 model_name
= '{}/{}'.format(username
, model_name
)
282 model_uuid
= models
['models'][model_name
]['uuid']
283 macaroons
= get_macaroons() if not password
else None
285 return await cls
.connect(
286 endpoint
, model_uuid
, username
, password
, cacert
, macaroons
, loop
)
288 def build_facades(self
, info
):
291 self
.facades
[facade
['name']] = facade
['versions'][-1]
293 async def login(self
, username
, password
, macaroons
=None):
298 if username
and not username
.startswith('user-'):
299 username
= 'user-{}'.format(username
)
301 result
= await self
.rpc({
306 "auth-tag": username
,
307 "credentials": password
,
308 "nonce": "".join(random
.sample(string
.printable
, 12)),
309 "macaroons": macaroons
or []
311 response
= result
['response']
312 self
.build_facades(response
.get('facades', {}))
313 self
.info
= response
.copy()
316 async def redirect_info(self
):
318 result
= await self
.rpc({
320 "request": "RedirectInfo",
323 except JujuAPIError
as e
:
324 if e
.message
== 'not redirected':
327 return result
['response']
332 self
.path
= os
.environ
.get('JUJU_DATA') or '~/.local/share/juju'
333 self
.path
= os
.path
.abspath(os
.path
.expanduser(self
.path
))
335 def current_controller(self
):
336 cmd
= shlex
.split('juju list-controllers --format yaml')
337 output
= subprocess
.check_output(cmd
)
338 output
= yaml
.safe_load(output
)
339 return output
.get('current-controller', '')
341 def current_model(self
, controller_name
=None):
342 models
= self
.models()[controller_name
or self
.current_controller()]
343 if 'current-model' not in models
:
344 raise JujuError('No current model')
345 return models
['current-model']
347 def controllers(self
):
348 return self
._load
_yaml
('controllers.yaml', 'controllers')
351 return self
._load
_yaml
('models.yaml', 'controllers')
354 return self
._load
_yaml
('accounts.yaml', 'controllers')
356 def _load_yaml(self
, filename
, key
):
357 filepath
= os
.path
.join(self
.path
, filename
)
358 with io
.open(filepath
, 'rt') as f
:
359 return yaml
.safe_load(f
)[key
]
363 """Decode and return macaroons from default ~/.go-cookies
367 cookie_file
= os
.path
.expanduser('~/.go-cookies')
368 with
open(cookie_file
, 'r') as f
:
369 cookies
= json
.load(f
)
370 except (OSError, ValueError):
371 log
.warn("Couldn't load macaroons from %s", cookie_file
)
375 c
['Value'] for c
in cookies
376 if c
['Name'].startswith('macaroon-') and c
['Value']
380 json
.loads(base64
.b64decode(value
).decode('utf-8'))
381 for value
in base64_macaroons