Implement Model.add_machine()
[osm/N2VC.git] / juju / client / connection.py
index 18111ce..9e8cb8f 100644 (file)
@@ -9,10 +9,12 @@ import ssl
 import string
 import subprocess
 import websockets
+from http.client import HTTPSConnection
 
 import yaml
 
-from juju.errors import JujuAPIError
+from juju import tag
+from juju.errors import JujuAPIError, JujuConnectionError
 
 log = logging.getLogger("websocket")
 
@@ -93,6 +95,52 @@ class Connection:
             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.
@@ -135,8 +183,7 @@ class Connection:
 
         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()
@@ -153,7 +200,6 @@ class Connection:
                 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()
@@ -175,6 +221,35 @@ class Connection:
         return await cls.connect_model(
             '{}:{}'.format(controller_name, model_name))
 
+    @classmethod
+    async def connect_current_controller(cls):
+        """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)
+
+    @classmethod
+    async def connect_controller(cls, controller_name):
+        """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)
+
     @classmethod
     async def connect_model(cls, model):
         """Connect to a model by name.
@@ -221,7 +296,10 @@ class Connection:
                 "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:
@@ -243,10 +321,10 @@ class JujuData:
         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)
-        return list(output.keys())[0]
+        return output.get('current-controller', '')
 
     def controllers(self):
         return self._load_yaml('controllers.yaml', 'controllers')