Tox + Integration testing

This commit implements a VNF Descriptor-driven integration
test framework, which will lead to integration tests being able
to run via jenkins, and more robust testing in general.

N2VC:

- Allow the use of an event loop passed when instantiating N2VC
- Refactor the execution of the initial-config-primitive so that it can
be easily re-run, such as the case of when a proxy charm is deployed
before the VNF's VM is created.
- Refactor GetPrimitiveStatus, to return the status (queued, running,
complete, failed) of a primitive.
- Add GetPrimitiveOutput, to return the output of a completed primitive
- Fix model disconnection when executing a primitive (it was happening
in the wrong scope)
- Fix wait_for_application, which was previously unused and broken
- Add support for parameter's 'data-type' field
- Add support for better SSH key management, allowing for a proxy charm
to be deployed before the VNF, so that it's public SSH key can be
injected when the VNF's VM is created.

Integration Tests:

The integration tests are intended to exercise the expected
functionality of a VNF/charm: deploy the charm, configure it as required
(i.e., ssh credentials), and execute the VNF's
initial-config-primitives.

- test_native_charm: deploy a native charm to a juju-managed machine and
verify primitive execution works
- test_proxy_charm: deploy a proxy charm, configured to talk to a remote
machine, and verify primitive execution works
- test_metrics_native: deploy a native charm and collect a metric
- test_metrics_proxy: deploy a proxy charm and collect a metric from the
vnf
- test_no_initial-config-primitive: deploy a vnf without an
initial-config-primitive
- test_non-string_parameter: deploy a vnf with a non-string parameter in
initial-config-primitive
- test_no_parameter: deploy a vnf with a primitive with no parameters

General:
- Add a build target to tox.ini so that a .deb is built via Jenkins

TODO (in a follow-up commit):
- test multi-vdu, multi-charm
- test deploying a native charm to a manually-provisioned machine
- Update inline pydoc
- Add more integration tests
- Add global per-test timeout to catch stalled tests

Signed-off-by: Adam Israel <adam.israel@canonical.com>
Change-Id: Id322b45d65c44714e8051fc5764f8c20b76d846c
diff --git a/tests/test_lxd.py b/tests/test_lxd.py
new file mode 100644
index 0000000..f68fa3a
--- /dev/null
+++ b/tests/test_lxd.py
@@ -0,0 +1,96 @@
+"""
+This test exercises LXD, to make sure that we can:
+1. Create a container profile
+2. Launch a container with a profile
+3. Stop a container
+4. Destroy a container
+5. Delete a container profile
+
+"""
+import logging
+# import os
+import pytest
+from . import base
+import subprocess
+import shlex
+import tempfile
+
+
+@pytest.mark.asyncio
+async def test_lxd():
+
+    container = base.create_lxd_container(name="test-lxd")
+    assert container is not None
+
+    # Get the hostname of the container
+    hostname = container.name
+
+    # Delete the container
+    base.destroy_lxd_container(container)
+
+    # Verify the container is deleted
+    client = base.get_lxd_client()
+    assert client.containers.exists(hostname) is False
+
+
+@pytest.mark.asyncio
+async def test_lxd_ssh():
+
+    with tempfile.TemporaryDirectory() as tmp:
+        try:
+            # Create a temporary keypair
+            cmd = shlex.split(
+                "ssh-keygen -t rsa -b 4096 -N '' -f {}/id_lxd_rsa".format(
+                    tmp,
+                )
+            )
+            subprocess.check_call(cmd)
+        except subprocess.CalledProcessError as e:
+            logging.debug(e)
+            assert False
+
+        # Slurp the public key
+        public_key = None
+        with open("{}/id_lxd_rsa.pub".format(tmp), "r") as f:
+            public_key = f.read()
+
+        assert public_key is not None
+
+        # Create the container with the keypair injected via profile
+        container = base.create_lxd_container(
+            public_key=public_key,
+            name="test-lxd"
+        )
+        assert container is not None
+
+        # Get the hostname of the container
+        hostname = container.name
+
+        addresses = container.state().network['eth0']['addresses']
+        # The interface may have more than one address, but we only need
+        # the first one for testing purposes.
+        ipaddr = addresses[0]['address']
+
+        # Verify we can SSH into container
+        try:
+            cmd = shlex.split(
+                "ssh -i {}/id_lxd_rsa {} root@{} hostname".format(
+                    tmp,
+                    "-oStrictHostKeyChecking=no",
+                    ipaddr,
+                )
+            )
+            subprocess.check_call(cmd)
+        except subprocess.CalledProcessError as e:
+            logging.debug(e)
+            assert False
+
+        # Delete the container
+        base.destroy_lxd_container(container)
+
+        # Verify the container is deleted
+        client = base.get_lxd_client()
+        assert client.containers.exists(hostname) is False
+
+        # Verify the container profile is deleted
+        assert client.profiles.exists(hostname) is False