From 19c5cfca317615597be6bf1051e9d2fa903adb97 Mon Sep 17 00:00:00 2001 From: Adam Israel Date: Thu, 3 Oct 2019 12:35:38 -0400 Subject: [PATCH] Remove dependency on vendored libjuju 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 --- n2vc/exceptions.py | 3 + n2vc/vnf.py | 89 +++++++++++++--------- setup.py | 6 +- tests/README.md | 19 ++++- tests/base.py | 16 ++++ tests/test_libjuju.py | 168 +++++++++++++++++++++++++++++++++++++++--- tox.ini | 17 ++++- 7 files changed, 266 insertions(+), 52 deletions(-) diff --git a/n2vc/exceptions.py b/n2vc/exceptions.py index f5c9fb0..fd4a3ce 100644 --- a/n2vc/exceptions.py +++ b/n2vc/exceptions.py @@ -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 diff --git a/n2vc/vnf.py b/n2vc/vnf.py index 17b44aa..ef7b967 100644 --- a/n2vc/vnf.py +++ b/n2vc/vnf.py @@ -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.""" diff --git a/setup.py b/setup.py index 2111150..1eb3f69 100644 --- 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', ], diff --git a/tests/README.md b/tests/README.md index 56380a4..4d6e64e 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,3 +1,19 @@ + + # 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 diff --git a/tests/base.py b/tests/base.py index ce95056..c7dad6d 100644 --- a/tests/base.py +++ b/tests/base.py @@ -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 diff --git a/tests/test_libjuju.py b/tests/test_libjuju.py index 8adc202..cdce5bf 100644 --- a/tests/test_libjuju.py +++ b/tests/test_libjuju.py @@ -1,18 +1,162 @@ -# 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 --- 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 -- 2.17.1