Remove dependency on vendored libjuju 18/8018/7
authorAdam Israel <adam.israel@canonical.com>
Thu, 3 Oct 2019 16:35:38 +0000 (12:35 -0400)
committerAdam Israel <adam.israel@canonical.com>
Wed, 9 Oct 2019 13:36:02 +0000 (09:36 -0400)
This patch removes the dependency on the embedded version of libjuju,
instead relying on the upstream library.

Change-Id: I88bd762006e5a182eaea74b5eba636ad03d524b0
Signed-off-by: Adam Israel <adam.israel@canonical.com>
n2vc/exceptions.py
n2vc/vnf.py
setup.py
tests/README.md
tests/base.py
tests/test_libjuju.py
tox.ini

index f5c9fb0..fd4a3ce 100644 (file)
@@ -38,3 +38,6 @@ class NoRouteToHost(Exception):
 
 class AuthenticationFailed(Exception):
     """The authentication for the specified user failed."""
+
+class InvalidCACertificate(Exception):
+    """The CA Certificate is not valid."""
\ No newline at end of file
index 17b44aa..ef7b967 100644 (file)
@@ -13,6 +13,8 @@
 #     limitations under the License.
 
 import asyncio
+import base64
+import binascii
 import logging
 import os
 import os.path
@@ -22,15 +24,16 @@ import ssl
 import subprocess
 import sys
 # import time
+import n2vc.exceptions
 from n2vc.provisioner import SSHProvisioner
 
 # FIXME: this should load the juju inside or modules without having to
 # explicitly install it. Check why it's not working.
 # Load our subtree of the juju library
-path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
-path = os.path.join(path, "modules/libjuju/")
-if path not in sys.path:
-    sys.path.insert(1, path)
+path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
+path = os.path.join(path, "modules/libjuju/")
+if path not in sys.path:
+    sys.path.insert(1, path)
 
 from juju.client import client
 from juju.controller import Controller
@@ -213,6 +216,11 @@ class N2VC:
         self.authenticated = False
         self.api_proxy = api_proxy
 
+        if log:
+            self.log = log
+        else:
+            self.log = logging.getLogger(__name__)
+
         # For debugging
         self.refcount = {
             'controller': 0,
@@ -229,20 +237,38 @@ class N2VC:
         self.port = 17070
         self.username = ""
         self.secret = ""
-        
+
         self.juju_public_key = juju_public_key
         if juju_public_key:
             self._create_juju_public_key(juju_public_key)
+        else:
+            self.juju_public_key = ''
 
         # TODO: Verify ca_cert is valid before using. VCA will crash
         # if the ca_cert isn't formatted correctly.
-        # self.ca_cert = ca_cert
-        self.ca_cert = None
+        def base64_to_cacert(b64string):
+            """Convert the base64-encoded string containing the VCA CACERT.
+
+            The input string....
+
+            """
+            try:
+                cacert = base64.b64decode(b64string).decode("utf-8")
+
+                cacert = re.sub(
+                    r'\\n',
+                    r'\n',
+                    cacert,
+                )
+            except binascii.Error as e:
+                self.log.debug("Caught binascii.Error: {}".format(e))
+                raise n2vc.exceptions.InvalidCACertificate("Invalid CA Certificate")
+
+            return cacert
+
+        self.ca_cert = base64_to_cacert(ca_cert)
+        # self.ca_cert = None
 
-        if log:
-            self.log = log
-        else:
-            self.log = logging.getLogger(__name__)
 
         # Quiet websocket traffic
         logging.getLogger('websockets.protocol').setLevel(logging.INFO)
@@ -950,16 +976,8 @@ class N2VC:
 
         models = await self.controller.list_models()
         if ns_uuid not in models:
-            try:
-                self.models[ns_uuid] = await self.controller.add_model(
-                    ns_uuid
-                )
-            except JujuError as e:
-                if "already exists" not in e.message:
-                    raise e
-
-            # Create an observer for this model
-            await self.create_model_monitor(ns_uuid)
+            # Get the new model
+            await self.get_model(ns_uuid)
 
         return True
 
@@ -1270,7 +1288,9 @@ class N2VC:
             if model_name not in models:
                 try:
                     self.models[model_name] = await self.controller.add_model(
-                        model_name
+                        model_name,
+                        config={'authorized-keys': self.juju_public_key}
+
                     )
                 except JujuError as e:
                     if "already exists" not in e.message:
@@ -1312,20 +1332,24 @@ class N2VC:
 
         if self.secret:
             self.log.debug(
-                "Connecting to controller... ws://{}:{} as {}/{}".format(
+                "Connecting to controller... ws://{} as {}/{}".format(
                     self.endpoint,
-                    self.port,
                     self.user,
                     self.secret,
                 )
             )
-            await self.controller.connect(
-                endpoint=self.endpoint,
-                username=self.user,
-                password=self.secret,
-                cacert=self.ca_cert,
-            )
-            self.refcount['controller'] += 1
+            try:
+                await self.controller.connect(
+                    endpoint=self.endpoint,
+                    username=self.user,
+                    password=self.secret,
+                    cacert=self.ca_cert,
+                )
+                self.refcount['controller'] += 1
+                self.authenticated = True
+                self.log.debug("JujuApi: Logged into controller")
+            except Exception as ex:
+                self.log.debug("Caught exception: {}".format(ex))
         else:
             # current_controller no longer exists
             # self.log.debug("Connecting to current controller...")
@@ -1336,9 +1360,8 @@ class N2VC:
             #     cacert=cacert,
             # )
             self.log.fatal("VCA credentials not configured.")
+            self.authenticated = False
 
-        self.authenticated = True
-        self.log.debug("JujuApi: Logged into controller")
 
     async def logout(self):
         """Logout of the Juju controller."""
index 2111150..1eb3f69 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -21,11 +21,7 @@ setup(
     packages=find_packages(
         exclude=["*.tests", "*.tests.*", "tests.*", "tests"]),
     install_requires=[
-        'macaroonbakery>=1.1,<2.0',
-        'pyRFC3339>=1.0,<2.0',
-        'pyyaml>=3.0,<4.0',
-        'theblues>=0.3.8,<1.0',
-        'websockets>=7.0,<8.0',
+        'juju',
         'paramiko',
         'pyasn1>=0.4.4',
     ],
index 56380a4..4d6e64e 100644 (file)
@@ -1,3 +1,19 @@
+<!--
+ Copyright 2019 Canonical Ltd.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
 # N2VC Testing
 
 
@@ -21,7 +37,8 @@ Run `juju status -m controller` and capture the IP address of machine 0. This is
 export VCA_HOST=1.2.3.4
 export VCA_USER=admin
 export VCA_SECRET=admin
-
+export VCA_CACERT=$(juju controllers --format json | jq -r '.controllers["osm"]["ca-cert"]'| base64 | tr -d \\n)
+export VCA_PUBLIC_KEY=$(cat ~/.local/share/juju/ssh/juju_id_rsa.pub)
 
 # Running tests
 
index ce95056..c7dad6d 100644 (file)
@@ -1,4 +1,18 @@
 #!/usr/bin/env python3
+# Copyright 2019 Canonical Ltd.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+#     Unless required by applicable law or agreed to in writing, software
+#     distributed under the License is distributed on an "AS IS" BASIS,
+#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#     See the License for the specific language governing permissions and
+#     limitations under the License.
+
 import asyncio
 import datetime
 import logging
@@ -118,6 +132,7 @@ def get_n2vc(loop=None):
     vca_user = os.getenv('VCA_USER', 'admin')
     vca_charms = os.getenv('VCA_CHARMS', None)
     vca_secret = os.getenv('VCA_SECRET', None)
+    vca_cacert = os.getenv('VCA_CACERT', None)
 
     # Get the Juju Public key
     juju_public_key = get_juju_public_key()
@@ -148,6 +163,7 @@ def get_n2vc(loop=None):
         artifacts=vca_charms,
         loop=loop,
         juju_public_key=juju_public_key,
+        ca_cert=vca_cacert,
     )
     return client
 
index 8adc202..cdce5bf 100644 (file)
-# A simple test to verify we're using the right libjuju module
+# Copyright 2019 Canonical Ltd.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+#     Unless required by applicable law or agreed to in writing, software
+#     distributed under the License is distributed on an "AS IS" BASIS,
+#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#     See the License for the specific language governing permissions and
+#     limitations under the License.
+
+import base64
+import juju
+import logging
+import n2vc.exceptions
 from n2vc.vnf import N2VC  # noqa: F401
+import os
+import pytest
+import re
+import ssl
 import sys
 
+MODEL_NAME = '5e4e7cb0-5678-4b82-97da-9e4a1b51f5d5'
+
+class TestN2VC(object):
+
+    @classmethod
+    def setup_class(self):
+        """ setup any state specific to the execution of the given class (which
+        usually contains tests).
+        """
+        # Initialize instance variable(s)
+        self.log = logging.getLogger()
+        self.log.level = logging.DEBUG
+
+    @classmethod
+    def teardown_class(self):
+        """ teardown any state that was previously setup with a call to
+        setup_class.
+        """
+        pass
+
+    """Utility functions"""
+    def get_n2vc(self, params={}):
+        """Return an instance of N2VC.VNF."""
+
+
+        # Extract parameters from the environment in order to run our test
+        vca_host = params['VCA_HOST']
+        vca_port = params['VCA_PORT']
+        vca_user = params['VCA_USER']
+        vca_charms = params['VCA_CHARMS']
+        vca_secret = params['VCA_SECRET']
+        vca_cacert = params['VCA_CACERT']
+        vca_public_key = params['VCA_PUBLIC_KEY']
+
+        client = n2vc.vnf.N2VC(
+            log=self.log,
+            server=vca_host,
+            port=vca_port,
+            user=vca_user,
+            secret=vca_secret,
+            artifacts=vca_charms,
+            juju_public_key=vca_public_key,
+            ca_cert=vca_cacert,
+        )
+        return client
+
+    """Tests"""
+
+    def test_vendored_libjuju(self):
+        """Test the module import for our vendored version of libjuju.
+
+        Test and verify that the version of libjuju being imported by N2VC is our
+        vendored version, not one installed externally.
+        """
+        for name in sys.modules:
+            if name.startswith("juju"):
+                module = sys.modules[name]
+                if getattr(module, "__file__"):
+                    print(getattr(module, "__file__"))
+                    assert re.search('n2vc', module.__file__, re.IGNORECASE)
+
+                    # assert module.__file__.find("N2VC")
+                    # assert False
+        return
+
+    @pytest.mark.asyncio
+    async def test_connect_invalid_cacert(self):
+        params = {
+            'VCA_HOST': os.getenv('VCA_HOST', '127.0.0.1'),
+            'VCA_PORT': os.getenv('VCA_PORT', 17070),
+            'VCA_USER': os.getenv('VCA_USER', 'admin'),
+            'VCA_SECRET': os.getenv('VCA_SECRET', 'admin'),
+            'VCA_CHARMS': os.getenv('VCA_CHARMS', None),
+            'VCA_PUBLIC_KEY': os.getenv('VCA_PUBLIC_KEY', None),
+            'VCA_CACERT': 'invalidcacert',
+        }
+        with pytest.raises(n2vc.exceptions.InvalidCACertificate):
+            client = self.get_n2vc(params)
+
+
+    @pytest.mark.asyncio
+    async def test_login(self):
+        """Test connecting to libjuju."""
+        params = {
+            'VCA_HOST': os.getenv('VCA_HOST', '127.0.0.1'),
+            'VCA_PORT': os.getenv('VCA_PORT', 17070),
+            'VCA_USER': os.getenv('VCA_USER', 'admin'),
+            'VCA_SECRET': os.getenv('VCA_SECRET', 'admin'),
+            'VCA_CHARMS': os.getenv('VCA_CHARMS', None),
+            'VCA_PUBLIC_KEY': os.getenv('VCA_PUBLIC_KEY', None),
+            'VCA_CACERT': os.getenv('VCA_CACERT', "invalidcacert"),
+        }
+
+        client = self.get_n2vc(params)
+
+        await client.login()
+        assert client.authenticated
+
+        await client.logout()
+        assert client.authenticated is False
+
+    @pytest.mark.asyncio
+    async def test_model(self):
+        """Test models."""
+        params = {
+            'VCA_HOST': os.getenv('VCA_HOST', '127.0.0.1'),
+            'VCA_PORT': os.getenv('VCA_PORT', 17070),
+            'VCA_USER': os.getenv('VCA_USER', 'admin'),
+            'VCA_SECRET': os.getenv('VCA_SECRET', 'admin'),
+            'VCA_CHARMS': os.getenv('VCA_CHARMS', None),
+            'VCA_PUBLIC_KEY': os.getenv('VCA_PUBLIC_KEY', None),
+            'VCA_CACERT': os.getenv('VCA_CACERT', "invalidcacert"),
+        }
+
+        client = self.get_n2vc(params)
+
+        await client.login()
+        assert client.authenticated
+
+        self.log.debug("Creating model {}".format(MODEL_NAME))
+        await client.CreateNetworkService(MODEL_NAME)
+
+        # assert that model exists
+        model = await client.controller.get_model(MODEL_NAME)
+        assert model
+
+        await client.DestroyNetworkService(MODEL_NAME)
 
-def test_libjuju():
-    """Test the module import for our vendored version of libjuju.
+        # Wait for model to be destroyed
+        import time
+        time.sleep(5)
 
-    Test and verify that the version of libjuju being imported by N2VC is our
-    vendored version, not one installed externally.
-    """
-    for name in sys.modules:
-        if name.startswith("juju"):
-            module = sys.modules[name]
-            if getattr(module, "__file__"):
-                assert module.__file__.find("N2VC/modules/libjuju/juju")
+        with pytest.raises(juju.errors.JujuAPIError):
+            model = await client.controller.get_model(MODEL_NAME)
 
-    return
+        await client.logout()
+        assert client.authenticated is False
diff --git a/tox.ini b/tox.ini
index 481bb9d..4685666 100644 (file)
--- a/tox.ini
+++ b/tox.ini
@@ -1,3 +1,17 @@
+# Copyright 2019 Canonical Ltd.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+#     Unless required by applicable law or agreed to in writing, software
+#     distributed under the License is distributed on an "AS IS" BASIS,
+#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#     See the License for the specific language governing permissions and
+#     limitations under the License.
+
 # Tox (http://tox.testrun.org/) is a tool for running tests
 # in multiple virtualenvs. This configuration file will run the
 # test suite on all supported python versions. To use it, "pip install tox"
@@ -26,6 +40,7 @@ passenv =
     VCA_SECRET
     LXD_HOST
     LXD_SECRET
+    VCA_CACERT
     # These are needed so executing `charm build` succeeds
     TERM
     TERMINFO
@@ -42,7 +57,7 @@ deps =
 [testenv:py3]
 # default tox env, excludes integration and serial tests
 commands =
-    pytest --ignore modules/ --ignore tests/charms/ --tb native -ra -v -s -n auto -k 'not integration' -m 'not serial' {posargs}
+    pytest --ignore modules/ --ignore tests/charms/ --tb native -ra -v -n auto -k 'not integration' -m 'not serial' {posargs}
 
 [testenv:lint]
 envdir = {toxworkdir}/py3