f4081355c686b56ac3e5842c5e35e4101d4699cd
12 from http
.client
import HTTPSConnection
17 from juju
.errors
import JujuAPIError
, JujuConnectionError
, JujuError
19 log
= logging
.getLogger("websocket")
26 # Connect to an arbitrary api server
27 client = await Connection.connect(
28 api_endpoint, model_uuid, username, password, cacert)
30 # Connect using a controller/model name
31 client = await Connection.connect_model('local.local:default')
33 # Connect to the currently active model
34 client = await Connection.connect_current()
38 self
, endpoint
, uuid
, username
, password
, cacert
=None,
40 self
.endpoint
= endpoint
42 self
.username
= username
43 self
.password
= password
44 self
.macaroons
= macaroons
47 self
.__request
_id
__ = 0
58 def _get_ssl(self
, cert
=None):
59 return ssl
.create_default_context(
60 purpose
=ssl
.Purpose
.CLIENT_AUTH
, cadata
=cert
)
64 url
= "wss://{}/model/{}/api".format(self
.endpoint
, self
.uuid
)
66 url
= "wss://{}/api".format(self
.endpoint
)
69 kw
['ssl'] = self
._get
_ssl
(self
.cacert
)
71 self
.ws
= await websockets
.connect(url
, **kw
)
72 log
.info("Driver connected to juju %s", url
)
75 async def close(self
):
79 result
= await self
.ws
.recv()
80 if result
is not None:
81 result
= json
.loads(result
)
84 async def rpc(self
, msg
, encoder
=None):
85 self
.__request
_id
__ += 1
86 msg
['request-id'] = self
.__request
_id
__
87 if'params' not in msg
:
89 if "version" not in msg
:
90 msg
['version'] = self
.facades
[msg
['type']]
91 outgoing
= json
.dumps(msg
, indent
=2, cls
=encoder
)
92 await self
.ws
.send(outgoing
)
93 result
= await self
.recv()
100 raise JujuAPIError(result
)
102 if not 'response' in result
:
103 # This may never happen
106 if 'results' in result
['response']:
107 # Check for errors in a result list.
109 for res
in result
['response']['results']:
110 if res
.get('error', {}).get('message'):
111 errors
.append(res
['error']['message'])
113 raise JujuError(errors
)
115 elif result
['response'].get('error', {}).get('message'):
116 raise JujuError(result
['response']['error']['message'])
120 def http_headers(self
):
121 """Return dictionary of http headers necessary for making an http
122 connection to the endpoint of this Connection.
124 :return: Dictionary of headers
127 if not self
.username
:
130 creds
= u
'{}:{}'.format(
131 tag
.user(self
.username
),
134 token
= base64
.b64encode(creds
.encode())
136 'Authorization': 'Basic {}'.format(token
.decode())
139 def https_connection(self
):
140 """Return an https connection to this Connection's endpoint.
142 Returns a 3-tuple containing::
144 1. The :class:`HTTPSConnection` instance
145 2. Dictionary of auth headers to be used with the connection
146 3. The root url path (str) to be used for requests.
149 endpoint
= self
.endpoint
150 host
, remainder
= endpoint
.split(':', 1)
153 port
, _
= remainder
.split('/', 1)
155 conn
= HTTPSConnection(
157 context
=self
._get
_ssl
(self
.cacert
),
161 "/model/{}".format(self
.uuid
)
164 return conn
, self
.http_headers(), path
166 async def clone(self
):
167 """Return a new Connection, connected to the same websocket endpoint
171 return await Connection
.connect(
180 async def controller(self
):
181 """Return a Connection to the controller at self.endpoint
184 return await Connection
.connect(
195 cls
, endpoint
, uuid
, username
, password
, cacert
=None,
197 """Connect to the websocket.
199 If uuid is None, the connection will be to the controller. Otherwise it
200 will be to the model.
203 client
= cls(endpoint
, uuid
, username
, password
, cacert
, macaroons
)
206 redirect_info
= await client
.redirect_info()
207 if not redirect_info
:
208 await client
.login(username
, password
, macaroons
)
213 s
for servers
in redirect_info
['servers']
214 for s
in servers
if s
["scope"] == 'public'
216 for server
in servers
:
218 "{value}:{port}".format(**server
), uuid
, username
,
219 password
, redirect_info
['ca-cert'], macaroons
)
222 result
= await client
.login(username
, password
, macaroons
)
223 if 'discharge-required-error' in result
:
226 except Exception as e
:
231 "Couldn't authenticate to %s", endpoint
)
234 async def connect_current(cls
):
235 """Connect to the currently active model.
238 jujudata
= JujuData()
239 controller_name
= jujudata
.current_controller()
240 models
= jujudata
.models()[controller_name
]
241 model_name
= models
['current-model']
243 return await cls
.connect_model(
244 '{}:{}'.format(controller_name
, model_name
))
247 async def connect_current_controller(cls
):
248 """Connect to the currently active controller.
251 jujudata
= JujuData()
252 controller_name
= jujudata
.current_controller()
253 if not controller_name
:
254 raise JujuConnectionError('No current controller')
256 return await cls
.connect_controller(controller_name
)
259 async def connect_controller(cls
, controller_name
):
260 """Connect to a controller by name.
263 jujudata
= JujuData()
264 controller
= jujudata
.controllers()[controller_name
]
265 endpoint
= controller
['api-endpoints'][0]
266 cacert
= controller
.get('ca-cert')
267 accounts
= jujudata
.accounts()[controller_name
]
268 username
= accounts
['user']
269 password
= accounts
.get('password')
270 macaroons
= get_macaroons() if not password
else None
272 return await cls
.connect(
273 endpoint
, None, username
, password
, cacert
, macaroons
)
276 async def connect_model(cls
, model
):
277 """Connect to a model by name.
279 :param str model: <controller>:<model>
282 controller_name
, model_name
= model
.split(':')
284 jujudata
= JujuData()
285 controller
= jujudata
.controllers()[controller_name
]
286 endpoint
= controller
['api-endpoints'][0]
287 cacert
= controller
.get('ca-cert')
288 accounts
= jujudata
.accounts()[controller_name
]
289 username
= accounts
['user']
290 password
= accounts
.get('password')
291 models
= jujudata
.models()[controller_name
]
292 model_uuid
= models
['models'][model_name
]['uuid']
293 macaroons
= get_macaroons() if not password
else None
295 return await cls
.connect(
296 endpoint
, model_uuid
, username
, password
, cacert
, macaroons
)
298 def build_facades(self
, info
):
301 self
.facades
[facade
['name']] = facade
['versions'][-1]
303 async def login(self
, username
, password
, macaroons
=None):
308 if username
and not username
.startswith('user-'):
309 username
= 'user-{}'.format(username
)
311 result
= await self
.rpc({
316 "auth-tag": username
,
317 "credentials": password
,
318 "nonce": "".join(random
.sample(string
.printable
, 12)),
319 "macaroons": macaroons
or []
321 response
= result
['response']
322 self
.build_facades(response
.get('facades', {}))
323 self
.info
= response
.copy()
326 async def redirect_info(self
):
328 result
= await self
.rpc({
330 "request": "RedirectInfo",
333 except JujuAPIError
as e
:
334 if e
.message
== 'not redirected':
337 return result
['response']
342 self
.path
= os
.environ
.get('JUJU_DATA') or '~/.local/share/juju'
343 self
.path
= os
.path
.abspath(os
.path
.expanduser(self
.path
))
345 def current_controller(self
):
346 cmd
= shlex
.split('juju list-controllers --format yaml')
347 output
= subprocess
.check_output(cmd
)
348 output
= yaml
.safe_load(output
)
349 return output
.get('current-controller', '')
351 def controllers(self
):
352 return self
._load
_yaml
('controllers.yaml', 'controllers')
355 return self
._load
_yaml
('models.yaml', 'controllers')
358 return self
._load
_yaml
('accounts.yaml', 'controllers')
360 def _load_yaml(self
, filename
, key
):
361 filepath
= os
.path
.join(self
.path
, filename
)
362 with io
.open(filepath
, 'rt') as f
:
363 return yaml
.safe_load(f
)[key
]
367 """Decode and return macaroons from default ~/.go-cookies
371 cookie_file
= os
.path
.expanduser('~/.go-cookies')
372 with
open(cookie_file
, 'r') as f
:
373 cookies
= json
.load(f
)
374 except (OSError, ValueError) as e
:
375 log
.warn("Couldn't load macaroons from %s", cookie_file
)
379 c
['Value'] for c
in cookies
380 if c
['Name'].startswith('macaroon-') and c
['Value']
384 json
.loads(base64
.b64decode(value
).decode('utf-8'))
385 for value
in base64_macaroons