X-Git-Url: https://osm.etsi.org/gitweb/?a=blobdiff_plain;f=juju%2Fclient%2Fconnection.py;h=18111ce11afed98b640d95e09596ed1aca43d3ef;hb=77cdb181bdeefd6f33b769dbed0d0c9ca2277006;hp=56f9e18fbb1e55c82671c7ac6e1a9f059d6223f7;hpb=769d19789b142ca3c1ffd0a172e9ac29cfb4b40e;p=osm%2FN2VC.git diff --git a/juju/client/connection.py b/juju/client/connection.py index 56f9e18..18111ce 100644 --- a/juju/client/connection.py +++ b/juju/client/connection.py @@ -1,15 +1,19 @@ -import asyncio +import base64 import io import json import logging import os import random +import shlex import ssl import string +import subprocess import websockets import yaml +from juju.errors import JujuAPIError + log = logging.getLogger("websocket") @@ -28,11 +32,14 @@ class Connection: client = await Connection.connect_current() """ - def __init__(self, endpoint, uuid, username, password, cacert=None): + def __init__( + self, endpoint, uuid, username, password, cacert=None, + macaroons=None): self.endpoint = endpoint self.uuid = uuid self.username = username self.password = password + self.macaroons = macaroons self.cacert = cacert self.__request_id__ = 0 @@ -40,16 +47,27 @@ class Connection: self.ws = None self.facades = {} - def _get_ssl(self, cert): + @property + def is_open(self): + if self.ws: + return self.ws.open + return False + + def _get_ssl(self, cert=None): return ssl.create_default_context( purpose=ssl.Purpose.CLIENT_AUTH, cadata=cert) - async def open(self, addr, cert=None): + async def open(self): + if self.uuid: + url = "wss://{}/model/{}/api".format(self.endpoint, self.uuid) + else: + url = "wss://{}/api".format(self.endpoint) + kw = dict() - if cert: - kw['ssl'] = self._get_ssl(cert) - self.addr = addr - self.ws = await websockets.connect(addr, **kw) + kw['ssl'] = self._get_ssl(self.cacert) + self.addr = url + self.ws = await websockets.connect(url, **kw) + log.info("Driver connected to juju %s", url) return self async def close(self): @@ -63,17 +81,16 @@ class Connection: async def rpc(self, msg, encoder=None): self.__request_id__ += 1 - msg['RequestId'] = self.__request_id__ - if'Params' not in msg: - msg['Params'] = {} - if "Version" not in msg: - msg['Version'] = self.facades[msg['Type']] + msg['request-id'] = self.__request_id__ + if'params' not in msg: + msg['params'] = {} + if "version" not in msg: + msg['version'] = self.facades[msg['type']] outgoing = json.dumps(msg, indent=2, cls=encoder) await self.ws.send(outgoing) result = await self.recv() - log.debug("send %s got %s", msg, result) - if result and 'Error' in result: - raise RuntimeError(result) + if result and 'error' in result: + raise JujuAPIError(result) return result async def clone(self): @@ -87,18 +104,63 @@ class Connection: self.username, self.password, self.cacert, + self.macaroons, + ) + + async def controller(self): + """Return a Connection to the controller at self.endpoint + + """ + return await Connection.connect( + self.endpoint, + None, + self.username, + self.password, + self.cacert, + self.macaroons, ) @classmethod - async def connect(cls, endpoint, uuid, username, password, cacert=None): - url = "wss://{}/model/{}/api".format(endpoint, uuid) - client = cls(endpoint, uuid, username, password, cacert) - await client.open(url, cacert) - server_info = await client.login(username, password) - client.build_facades(server_info['facades']) - log.info("Driver connected to juju %s", endpoint) + async def connect( + cls, endpoint, uuid, username, password, cacert=None, + macaroons=None): + """Connect to the websocket. + + If uuid is None, the connection will be to the controller. Otherwise it + will be to the model. - return client + """ + client = cls(endpoint, uuid, username, password, cacert, macaroons) + await client.open() + + redirect_info = await client.redirect_info() + if not redirect_info: + server_info = await client.login(username, password, macaroons) + client.build_facades(server_info['facades']) + return client + + await client.close() + servers = [ + s for servers in redirect_info['servers'] + for s in servers if s["scope"] == 'public' + ] + for server in servers: + client = cls( + "{value}:{port}".format(**server), uuid, username, + password, redirect_info['ca-cert'], macaroons) + await client.open() + try: + result = await client.login(username, password, macaroons) + if 'discharge-required-error' in result: + continue + client.build_facades(result['facades']) + return client + except Exception as e: + await client.close() + log.exception(e) + + raise Exception( + "Couldn't authenticate to %s", endpoint) @classmethod async def connect_current(cls): @@ -107,18 +169,11 @@ class Connection: """ jujudata = JujuData() controller_name = jujudata.current_controller() - controller = jujudata.controllers()[controller_name] - endpoint = controller['api-endpoints'][0] - cacert = controller.get('ca-cert') - accounts = jujudata.accounts()[controller_name] - username = accounts['current-account'] - password = accounts['accounts'][username]['password'] - models = jujudata.models()[controller_name]['accounts'][username] + models = jujudata.models()[controller_name] model_name = models['current-model'] - model_uuid = models['models'][model_name]['uuid'] - return await cls.connect( - endpoint, model_uuid, username, password, cacert) + return await cls.connect_model( + '{}:{}'.format(controller_name, model_name)) @classmethod async def connect_model(cls, model): @@ -134,33 +189,52 @@ class Connection: endpoint = controller['api-endpoints'][0] cacert = controller.get('ca-cert') accounts = jujudata.accounts()[controller_name] - username = accounts['current-account'] - password = accounts['accounts'][username]['password'] - models = jujudata.models()[controller_name]['accounts'][username] + username = accounts['user'] + password = accounts.get('password') + models = jujudata.models()[controller_name] model_uuid = models['models'][model_name]['uuid'] + macaroons = get_macaroons() if not password else None return await cls.connect( - endpoint, model_uuid, username, password, cacert) + endpoint, model_uuid, username, password, cacert, macaroons) def build_facades(self, info): self.facades.clear() for facade in info: - self.facades[facade['Name']] = facade['Versions'][-1] + self.facades[facade['name']] = facade['versions'][-1] + + async def login(self, username, password, macaroons=None): + if macaroons: + username = '' + password = '' - async def login(self, username, password): - if not username.startswith('user-'): + if username and not username.startswith('user-'): username = 'user-{}'.format(username) result = await self.rpc({ - "Type": "Admin", - "Request": "Login", - "Version": 3, - "Params": { + "type": "Admin", + "request": "Login", + "version": 3, + "params": { "auth-tag": username, "credentials": password, - "Nonce": "".join(random.sample(string.printable, 12)), + "nonce": "".join(random.sample(string.printable, 12)), + "macaroons": macaroons or [] }}) - return result['Response'] + return result['response'] + + async def redirect_info(self): + try: + result = await self.rpc({ + "type": "Admin", + "request": "RedirectInfo", + "version": 3, + }) + except JujuAPIError as e: + if e.message == 'not redirected': + return None + raise + return result['response'] class JujuData: @@ -169,13 +243,10 @@ class JujuData: self.path = os.path.abspath(os.path.expanduser(self.path)) def current_controller(self): - try: - filepath = os.path.join(self.path, 'current-controller') - with io.open(filepath, 'rt') as f: - return f.read().strip() - except OSError as e: - log.exception(e) - return None + cmd = shlex.split('juju show-controller --format yaml') + output = subprocess.check_output(cmd) + output = yaml.safe_load(output) + return list(output.keys())[0] def controllers(self): return self._load_yaml('controllers.yaml', 'controllers') @@ -190,3 +261,26 @@ class JujuData: filepath = os.path.join(self.path, filename) with io.open(filepath, 'rt') as f: return yaml.safe_load(f)[key] + + +def get_macaroons(): + """Decode and return macaroons from default ~/.go-cookies + + """ + try: + cookie_file = os.path.expanduser('~/.go-cookies') + with open(cookie_file, 'r') as f: + cookies = json.load(f) + except (OSError, ValueError) as e: + log.warn("Couldn't load macaroons from %s", cookie_file) + return [] + + base64_macaroons = [ + c['Value'] for c in cookies + if c['Name'].startswith('macaroon-') and c['Value'] + ] + + return [ + json.loads(base64.b64decode(value).decode('utf-8')) + for value in base64_macaroons + ]