Pass through event loop
[osm/N2VC.git] / juju / client / connection.py
index 18111ce..3011a8a 100644 (file)
@@ -9,10 +9,13 @@ import ssl
 import string
 import subprocess
 import websockets
 import string
 import subprocess
 import websockets
+from http.client import HTTPSConnection
 
 
+import asyncio
 import yaml
 
 import yaml
 
-from juju.errors import JujuAPIError
+from juju import tag
+from juju.errors import JujuAPIError, JujuConnectionError
 
 log = logging.getLogger("websocket")
 
 
 log = logging.getLogger("websocket")
 
@@ -34,13 +37,14 @@ class Connection:
     """
     def __init__(
             self, endpoint, uuid, username, password, cacert=None,
     """
     def __init__(
             self, endpoint, uuid, username, password, cacert=None,
-            macaroons=None):
+            macaroons=None, loop=None):
         self.endpoint = endpoint
         self.uuid = uuid
         self.username = username
         self.password = password
         self.macaroons = macaroons
         self.cacert = cacert
         self.endpoint = endpoint
         self.uuid = uuid
         self.username = username
         self.password = password
         self.macaroons = macaroons
         self.cacert = cacert
+        self.loop = loop or asyncio.get_event_loop()
 
         self.__request_id__ = 0
         self.addr = None
 
         self.__request_id__ = 0
         self.addr = None
@@ -65,6 +69,7 @@ class Connection:
 
         kw = dict()
         kw['ssl'] = self._get_ssl(self.cacert)
 
         kw = dict()
         kw['ssl'] = self._get_ssl(self.cacert)
+        kw['loop'] = self.loop
         self.addr = url
         self.ws = await websockets.connect(url, **kw)
         log.info("Driver connected to juju %s", url)
         self.addr = url
         self.ws = await websockets.connect(url, **kw)
         log.info("Driver connected to juju %s", url)
@@ -93,6 +98,52 @@ class Connection:
             raise JujuAPIError(result)
         return result
 
             raise JujuAPIError(result)
         return result
 
+    def http_headers(self):
+        """Return dictionary of http headers necessary for making an http
+        connection to the endpoint of this Connection.
+
+        :return: Dictionary of headers
+
+        """
+        if not self.username:
+            return {}
+
+        creds = u'{}:{}'.format(
+            tag.user(self.username),
+            self.password or ''
+        )
+        token = base64.b64encode(creds.encode())
+        return {
+            'Authorization': 'Basic {}'.format(token.decode())
+        }
+
+    def https_connection(self):
+        """Return an https connection to this Connection's endpoint.
+
+        Returns a 3-tuple containing::
+
+            1. The :class:`HTTPSConnection` instance
+            2. Dictionary of auth headers to be used with the connection
+            3. The root url path (str) to be used for requests.
+
+        """
+        endpoint = self.endpoint
+        host, remainder = endpoint.split(':', 1)
+        port = remainder
+        if '/' in remainder:
+            port, _ = remainder.split('/', 1)
+
+        conn = HTTPSConnection(
+            host, int(port),
+            context=self._get_ssl(self.cacert),
+        )
+
+        path = (
+            "/model/{}".format(self.uuid)
+            if self.uuid else ""
+        )
+        return conn, self.http_headers(), path
+
     async def clone(self):
         """Return a new Connection, connected to the same websocket endpoint
         as this one.
     async def clone(self):
         """Return a new Connection, connected to the same websocket endpoint
         as this one.
@@ -105,6 +156,7 @@ class Connection:
             self.password,
             self.cacert,
             self.macaroons,
             self.password,
             self.cacert,
             self.macaroons,
+            self.loop,
         )
 
     async def controller(self):
         )
 
     async def controller(self):
@@ -118,25 +170,26 @@ class Connection:
             self.password,
             self.cacert,
             self.macaroons,
             self.password,
             self.cacert,
             self.macaroons,
+            self.loop,
         )
 
     @classmethod
     async def connect(
             cls, endpoint, uuid, username, password, cacert=None,
         )
 
     @classmethod
     async def connect(
             cls, endpoint, uuid, username, password, cacert=None,
-            macaroons=None):
+            macaroons=None, loop=None):
         """Connect to the websocket.
 
         If uuid is None, the connection will be to the controller. Otherwise it
         will be to the model.
 
         """
         """Connect to the websocket.
 
         If uuid is None, the connection will be to the controller. Otherwise it
         will be to the model.
 
         """
-        client = cls(endpoint, uuid, username, password, cacert, macaroons)
+        client = cls(endpoint, uuid, username, password, cacert, macaroons,
+                     loop)
         await client.open()
 
         redirect_info = await client.redirect_info()
         if not redirect_info:
         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'])
+            await client.login(username, password, macaroons)
             return client
 
         await client.close()
             return client
 
         await client.close()
@@ -153,7 +206,6 @@ class Connection:
                 result = await client.login(username, password, macaroons)
                 if 'discharge-required-error' in result:
                     continue
                 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()
                 return client
             except Exception as e:
                 await client.close()
@@ -163,7 +215,7 @@ class Connection:
             "Couldn't authenticate to %s", endpoint)
 
     @classmethod
             "Couldn't authenticate to %s", endpoint)
 
     @classmethod
-    async def connect_current(cls):
+    async def connect_current(cls, loop=None):
         """Connect to the currently active model.
 
         """
         """Connect to the currently active model.
 
         """
@@ -173,10 +225,39 @@ class Connection:
         model_name = models['current-model']
 
         return await cls.connect_model(
         model_name = models['current-model']
 
         return await cls.connect_model(
-            '{}:{}'.format(controller_name, model_name))
+            '{}:{}'.format(controller_name, model_name), loop)
+
+    @classmethod
+    async def connect_current_controller(cls, loop=None):
+        """Connect to the currently active controller.
+
+        """
+        jujudata = JujuData()
+        controller_name = jujudata.current_controller()
+        if not controller_name:
+            raise JujuConnectionError('No current controller')
+
+        return await cls.connect_controller(controller_name, loop)
+
+    @classmethod
+    async def connect_controller(cls, controller_name, loop=None):
+        """Connect to a controller by name.
+
+        """
+        jujudata = JujuData()
+        controller = jujudata.controllers()[controller_name]
+        endpoint = controller['api-endpoints'][0]
+        cacert = controller.get('ca-cert')
+        accounts = jujudata.accounts()[controller_name]
+        username = accounts['user']
+        password = accounts.get('password')
+        macaroons = get_macaroons() if not password else None
+
+        return await cls.connect(
+            endpoint, None, username, password, cacert, macaroons, loop)
 
     @classmethod
 
     @classmethod
-    async def connect_model(cls, model):
+    async def connect_model(cls, model, loop=None):
         """Connect to a model by name.
 
         :param str model: <controller>:<model>
         """Connect to a model by name.
 
         :param str model: <controller>:<model>
@@ -192,11 +273,12 @@ class Connection:
         username = accounts['user']
         password = accounts.get('password')
         models = jujudata.models()[controller_name]
         username = accounts['user']
         password = accounts.get('password')
         models = jujudata.models()[controller_name]
+        model_name = '{}/{}'.format(username, model_name)
         model_uuid = models['models'][model_name]['uuid']
         macaroons = get_macaroons() if not password else None
 
         return await cls.connect(
         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, macaroons)
+            endpoint, model_uuid, username, password, cacert, macaroons, loop)
 
     def build_facades(self, info):
         self.facades.clear()
 
     def build_facades(self, info):
         self.facades.clear()
@@ -221,7 +303,10 @@ class Connection:
                 "nonce": "".join(random.sample(string.printable, 12)),
                 "macaroons": macaroons or []
             }})
                 "nonce": "".join(random.sample(string.printable, 12)),
                 "macaroons": macaroons or []
             }})
-        return result['response']
+        response = result['response']
+        self.build_facades(response.get('facades', {}))
+        self.info = response.copy()
+        return response
 
     async def redirect_info(self):
         try:
 
     async def redirect_info(self):
         try:
@@ -243,10 +328,10 @@ class JujuData:
         self.path = os.path.abspath(os.path.expanduser(self.path))
 
     def current_controller(self):
         self.path = os.path.abspath(os.path.expanduser(self.path))
 
     def current_controller(self):
-        cmd = shlex.split('juju show-controller --format yaml')
+        cmd = shlex.split('juju list-controllers --format yaml')
         output = subprocess.check_output(cmd)
         output = yaml.safe_load(output)
         output = subprocess.check_output(cmd)
         output = yaml.safe_load(output)
-        return list(output.keys())[0]
+        return output.get('current-controller', '')
 
     def controllers(self):
         return self._load_yaml('controllers.yaml', 'controllers')
 
     def controllers(self):
         return self._load_yaml('controllers.yaml', 'controllers')
@@ -271,7 +356,7 @@ def get_macaroons():
         cookie_file = os.path.expanduser('~/.go-cookies')
         with open(cookie_file, 'r') as f:
             cookies = json.load(f)
         cookie_file = os.path.expanduser('~/.go-cookies')
         with open(cookie_file, 'r') as f:
             cookies = json.load(f)
-    except (OSError, ValueError) as e:
+    except (OSError, ValueError):
         log.warn("Couldn't load macaroons from %s", cookie_file)
         return []
 
         log.warn("Couldn't load macaroons from %s", cookie_file)
         return []