Revert "Clean up commented or unused code" 44/10244/1
authorDavid Garcia <david.garcia@canonical.com>
Mon, 1 Feb 2021 09:39:56 +0000 (10:39 +0100)
committerDavid Garcia <david.garcia@canonical.com>
Mon, 1 Feb 2021 09:40:19 +0000 (10:40 +0100)
This reverts commit b4e7f5c425df48f7e946d792184d1d1b44879fe9.

Change-Id: I31a65516e65c3eb6528f241e36c1e45f3753f4c0
Signed-off-by: David Garcia <david.garcia@canonical.com>
71 files changed:
n2vc/k8s_juju_conn.py
n2vc/libjuju.py
n2vc/n2vc_juju_conn.py
tests/README.md [new file with mode: 0644]
tests/__init__.py [new file with mode: 0644]
tests/base.py [new file with mode: 0644]
tests/bundles/k8s-zookeeper-downgrade.yaml [new file with mode: 0644]
tests/bundles/k8s-zookeeper-upgrade.yaml [new file with mode: 0644]
tests/bundles/k8s-zookeeper.yaml [new file with mode: 0644]
tests/charms/layers/broken/README.md [new file with mode: 0644]
tests/charms/layers/broken/actions.yaml [new file with mode: 0644]
tests/charms/layers/broken/actions/touch [new file with mode: 0755]
tests/charms/layers/broken/config.yaml [new file with mode: 0644]
tests/charms/layers/broken/icon.svg [new file with mode: 0644]
tests/charms/layers/broken/layer.yaml [new file with mode: 0644]
tests/charms/layers/broken/metadata.yaml [new file with mode: 0644]
tests/charms/layers/broken/metrics.yaml [new file with mode: 0644]
tests/charms/layers/broken/reactive/simple.py [new file with mode: 0644]
tests/charms/layers/metrics-ci/README.md [new file with mode: 0755]
tests/charms/layers/metrics-ci/config.yaml [new file with mode: 0755]
tests/charms/layers/metrics-ci/layer.yaml [new file with mode: 0755]
tests/charms/layers/metrics-ci/metadata.yaml [new file with mode: 0755]
tests/charms/layers/metrics-ci/metrics.yaml [new file with mode: 0755]
tests/charms/layers/metrics-ci/reactive/metrics_ci.py [new file with mode: 0755]
tests/charms/layers/metrics-proxy-ci/README.md [new file with mode: 0644]
tests/charms/layers/metrics-proxy-ci/config.yaml [new file with mode: 0644]
tests/charms/layers/metrics-proxy-ci/layer.yaml [new file with mode: 0644]
tests/charms/layers/metrics-proxy-ci/metadata.yaml [new file with mode: 0644]
tests/charms/layers/metrics-proxy-ci/metrics.yaml [new file with mode: 0644]
tests/charms/layers/metrics-proxy-ci/reactive/metrics_ci.py [new file with mode: 0644]
tests/charms/layers/native-ci/README.md [new file with mode: 0644]
tests/charms/layers/native-ci/actions.yaml [new file with mode: 0644]
tests/charms/layers/native-ci/actions/test [new file with mode: 0755]
tests/charms/layers/native-ci/actions/testint [new file with mode: 0755]
tests/charms/layers/native-ci/layer.yaml [new file with mode: 0644]
tests/charms/layers/native-ci/metadata.yaml [new file with mode: 0644]
tests/charms/layers/native-ci/reactive/native-ci.py [new file with mode: 0644]
tests/charms/layers/proxy-ci/README.md [new file with mode: 0644]
tests/charms/layers/proxy-ci/actions.yaml [new file with mode: 0644]
tests/charms/layers/proxy-ci/actions/test [new file with mode: 0755]
tests/charms/layers/proxy-ci/layer.yaml [new file with mode: 0644]
tests/charms/layers/proxy-ci/metadata.yaml [new file with mode: 0644]
tests/charms/layers/proxy-ci/reactive/proxy_ci.py [new file with mode: 0644]
tests/charms/layers/simple/README.md [new file with mode: 0644]
tests/charms/layers/simple/actions.yaml [new file with mode: 0644]
tests/charms/layers/simple/actions/touch [new file with mode: 0755]
tests/charms/layers/simple/config.yaml [new file with mode: 0644]
tests/charms/layers/simple/icon.svg [new file with mode: 0644]
tests/charms/layers/simple/layer.yaml [new file with mode: 0644]
tests/charms/layers/simple/metadata.yaml [new file with mode: 0644]
tests/charms/layers/simple/metrics.yaml [new file with mode: 0644]
tests/charms/layers/simple/reactive/simple.py [new file with mode: 0644]
tests/integration/__init__.py [new file with mode: 0644]
tests/integration/test_broken_charm.py [new file with mode: 0644]
tests/integration/test_charm_native.py [new file with mode: 0644]
tests/integration/test_charm_proxy.py [new file with mode: 0644]
tests/integration/test_charm_relate.py [new file with mode: 0644]
tests/integration/test_hackfest_simple.py [new file with mode: 0644]
tests/integration/test_metrics_native.py [new file with mode: 0644]
tests/integration/test_metrics_proxy.py [new file with mode: 0644]
tests/integration/test_multivdu_multicharm.py [new file with mode: 0644]
tests/integration/test_no_initial_config_primitive.py [new file with mode: 0644]
tests/integration/test_no_parameter.py [new file with mode: 0644]
tests/integration/test_non_existent_primitive.py [new file with mode: 0644]
tests/integration/test_non_string_parameter.py [new file with mode: 0644]
tests/integration/test_simplecharm.py [new file with mode: 0644]
tests/test_k8s_juju_conn.py [new file with mode: 0644]
tests/test_libjuju.py [new file with mode: 0644]
tests/test_lxd.py [new file with mode: 0644]
tests/test_model.py [new file with mode: 0644]
tests/test_ssh_keygen.py [new file with mode: 0644]

index db34994..3d58385 100644 (file)
@@ -46,6 +46,11 @@ RBAC_LABEL_KEY_NAME = "rbac-id"
 ADMIN_NAMESPACE = "kube-system"
 RBAC_STACK_PREFIX = "juju-credential"
 
+# from juju.bundle import BundleHandler
+# import re
+# import ssl
+# from .vnf import N2VC
+
 
 def generate_rbac_id():
     return binascii.hexlify(os.urandom(4)).decode()
@@ -142,8 +147,97 @@ class K8sJujuConnector(K8sConnector):
             (on error, an exception will be raised)
         """
 
+        # """Bootstrapping
+
+        # Bootstrapping cannot be done, by design, through the API. We need to
+        # use the CLI tools.
+        # """
+
+        # """
+        # WIP: Workflow
+
+        # 1. Has the environment already been bootstrapped?
+        # - Check the database to see if we have a record for this env
+
+        # 2. If this is a new env, create it
+        # - Add the k8s cloud to Juju
+        # - Bootstrap
+        # - Record it in the database
+
+        # 3. Connect to the Juju controller for this cloud
+
+        # """
+        # cluster_uuid = reuse_cluster_uuid
+        # if not cluster_uuid:
+        #     cluster_uuid = str(uuid4())
+
+        ##################################################
+        # TODO: Pull info from db based on the namespace #
+        ##################################################
+
+        ###################################################
+        # TODO: Make it idempotent, calling add-k8s and   #
+        # bootstrap whenever reuse_cluster_uuid is passed #
+        # as parameter                                    #
+        # `init_env` is called to initialize the K8s      #
+        # cluster for juju. If this initialization fails, #
+        # it can be called again by LCM with the param    #
+        # reuse_cluster_uuid, e.g. to try to fix it.       #
+        ###################################################
+
+        # This is a new cluster, so bootstrap it
+
         cluster_uuid = reuse_cluster_uuid or str(uuid.uuid4())
 
+        # Is a local k8s cluster?
+        # localk8s = self.is_local_k8s(k8s_creds)
+
+        # If the k8s is external, the juju controller needs a loadbalancer
+        # loadbalancer = False if localk8s else True
+
+        # Name the new k8s cloud
+        # k8s_cloud = "k8s-{}".format(cluster_uuid)
+
+        # self.log.debug("Adding k8s cloud {}".format(k8s_cloud))
+        # await self.add_k8s(k8s_cloud, k8s_creds)
+
+        # Bootstrap Juju controller
+        # self.log.debug("Bootstrapping...")
+        # await self.bootstrap(k8s_cloud, cluster_uuid, loadbalancer)
+        # self.log.debug("Bootstrap done.")
+
+        # Get the controller information
+
+        # Parse ~/.local/share/juju/controllers.yaml
+        # controllers.testing.api-endpoints|ca-cert|uuid
+        # self.log.debug("Getting controller endpoints")
+        # with open(os.path.expanduser("~/.local/share/juju/controllers.yaml")) as f:
+        #     controllers = yaml.load(f, Loader=yaml.Loader)
+        #     controller = controllers["controllers"][cluster_uuid]
+        #     endpoints = controller["api-endpoints"]
+        #     juju_endpoint = endpoints[0]
+        #     juju_ca_cert = controller["ca-cert"]
+
+        # Parse ~/.local/share/juju/accounts
+        # controllers.testing.user|password
+        # self.log.debug("Getting accounts")
+        # with open(os.path.expanduser("~/.local/share/juju/accounts.yaml")) as f:
+        #     controllers = yaml.load(f, Loader=yaml.Loader)
+        #     controller = controllers["controllers"][cluster_uuid]
+
+        #     juju_user = controller["user"]
+        #     juju_secret = controller["password"]
+
+        # config = {
+        #     "endpoint": juju_endpoint,
+        #     "username": juju_user,
+        #     "secret": juju_secret,
+        #     "cacert": juju_ca_cert,
+        #     "loadbalancer": loadbalancer,
+        # }
+
+        # Store the cluster configuration so it
+        # can be used for subsequent calls
         kubecfg = tempfile.NamedTemporaryFile()
         with open(kubecfg.name, "w") as kubecfg_file:
             kubecfg_file.write(k8s_creds)
@@ -208,6 +302,32 @@ class K8sJujuConnector(K8sConnector):
                 storage_class=default_storage_class,
                 credential_name=self._get_credential_name(cluster_uuid),
             )
+            # self.log.debug("Setting config")
+            # await self.set_config(cluster_uuid, config)
+
+            # Test connection
+            # controller = await self.get_controller(cluster_uuid)
+            # await controller.disconnect()
+
+            # TODO: Remove these commented lines
+            # raise Exception("EOL")
+            # self.juju_public_key = None
+            # Login to the k8s cluster
+            # if not self.authenticated:
+            #     await self.login(cluster_uuid)
+
+            # We're creating a new cluster
+            # print("Getting model {}".format(self.get_namespace(cluster_uuid),
+            #    cluster_uuid=cluster_uuid))
+            # model = await self.get_model(
+            #    self.get_namespace(cluster_uuid),
+            #    cluster_uuid=cluster_uuid
+            # )
+
+            # Disconnect from the model
+            # if model and model.is_connected():
+            #    await model.disconnect()
+
             return cluster_uuid, True
         except Exception as e:
             self.log.error("Error initializing k8scluster: {}".format(e))
@@ -259,7 +379,26 @@ class K8sJujuConnector(K8sConnector):
         """
 
         try:
+            # Remove k8scluster from database
+            # self.log.debug("[reset] Removing k8scluster from juju database")
+            # juju_db = self.db.get_one("admin", {"_id": "juju"})
+
+            # for k in juju_db["k8sclusters"]:
+            #     if k["_id"] == cluster_uuid:
+            #         juju_db["k8sclusters"].remove(k)
+            #         self.db.set_one(
+            #             table="admin",
+            #             q_filter={"_id": "juju"},
+            #             update_dict={"k8sclusters": juju_db["k8sclusters"]},
+            #         )
+            #         break
+
+            # Destroy the controller (via CLI)
+            # self.log.debug("[reset] Destroying controller")
+            # await self.destroy_controller(cluster_uuid)
             self.log.debug("[reset] Removing k8s cloud")
+            # k8s_cloud = "k8s-{}".format(cluster_uuid)
+            # await self.remove_cloud(k8s_cloud)
 
             cloud_creds = await self.libjuju.get_cloud_credentials(
                 cluster_uuid,
@@ -296,6 +435,20 @@ class K8sJujuConnector(K8sConnector):
             self.log.debug("Caught exception during reset: {}".format(e))
             raise e
         return True
+        # TODO: Remove these commented lines
+        #     if not self.authenticated:
+        #         await self.login(cluster_uuid)
+
+        #     if self.controller.is_connected():
+        #         # Destroy the model
+        #         namespace = self.get_namespace(cluster_uuid)
+        #         if await self.has_model(namespace):
+        #             self.log.debug("[reset] Destroying model")
+        #             await self.controller.destroy_model(namespace, destroy_storage=True)
+
+        #         # Disconnect from the controller
+        #         self.log.debug("[reset] Disconnecting controller")
+        #         await self.logout()
 
     """Deployment"""
 
@@ -326,6 +479,12 @@ class K8sJujuConnector(K8sConnector):
         """
         bundle = kdu_model
 
+        # controller = await self.get_controller(cluster_uuid)
+
+        ##
+        # Get or create the model, based on the NS
+        # uuid.
+
         if not db_dict:
             raise K8sException("db_dict must be set")
         if not bundle:
@@ -376,7 +535,43 @@ class K8sJujuConnector(K8sConnector):
         await self.libjuju.deploy(
             bundle, model_name=kdu_instance, wait=atomic, timeout=timeout
         )
+
+        # Get the application
+        # if atomic:
+        #     # applications = model.applications
+        #     self.log.debug("[install] Applications: {}".format(model.applications))
+        #     for name in model.applications:
+        #         self.log.debug("[install] Waiting for {} to settle".format(name))
+        #         application = model.applications[name]
+        #         try:
+        #             # It's not enough to wait for all units to be active;
+        #             # the application status needs to be active as well.
+        #             self.log.debug("Waiting for all units to be active...")
+        #             await model.block_until(
+        #                 lambda: all(
+        #                     unit.agent_status == "idle"
+        #                     and application.status in ["active", "unknown"]
+        #                     and unit.workload_status in ["active", "unknown"]
+        #                     for unit in application.units
+        #                 ),
+        #                 timeout=timeout,
+        #             )
+        #             self.log.debug("All units active.")
+
+        #         # TODO use asyncio.TimeoutError
+        #         except concurrent.futures._base.TimeoutError:
+        #             os.chdir(previous_workdir)
+        #             self.log.debug("[install] Timeout exceeded; resetting cluster")
+        #             await self.reset(cluster_uuid)
+        #             return False
+
+        # Wait for the application to be active
+        # if model.is_connected():
+        #     self.log.debug("[install] Disconnecting model")
+        #     await model.disconnect()
+        # await controller.disconnect()
         os.chdir(previous_workdir)
+
         return kdu_instance
 
     async def instances_list(self, cluster_uuid: str) -> list:
@@ -420,6 +615,59 @@ class K8sJujuConnector(K8sConnector):
         initial release.
         """
         raise MethodNotImplemented()
+        # TODO: Remove these commented lines
+
+        # model = await self.get_model(namespace, cluster_uuid=cluster_uuid)
+
+        # model = None
+        # namespace = self.get_namespace(cluster_uuid)
+        # controller = await self.get_controller(cluster_uuid)
+
+        # try:
+        #     if namespace not in await controller.list_models():
+        #         raise N2VCNotFound(message="Model {} does not exist".format(namespace))
+
+        #     model = await controller.get_model(namespace)
+        #     with open(kdu_model, "r") as f:
+        #         bundle = yaml.safe_load(f)
+
+        #         """
+        #         {
+        #             'description': 'Test bundle',
+        #             'bundle': 'kubernetes',
+        #             'applications': {
+        #                 'mariadb-k8s': {
+        #                     'charm': 'cs:~charmed-osm/mariadb-k8s-20',
+        #                     'scale': 1,
+        #                     'options': {
+        #                         'password': 'manopw',
+        #                         'root_password': 'osm4u',
+        #                         'user': 'mano'
+        #                     },
+        #                     'series': 'kubernetes'
+        #                 }
+        #             }
+        #         }
+        #         """
+        #         # TODO: This should be returned in an agreed-upon format
+        #         for name in bundle["applications"]:
+        #             self.log.debug(model.applications)
+        #             application = model.applications[name]
+        #             self.log.debug(application)
+
+        #             path = bundle["applications"][name]["charm"]
+
+        #             try:
+        #                 await application.upgrade_charm(switch=path)
+        #             except juju.errors.JujuError as ex:
+        #                 if "already running charm" in str(ex):
+        #                     # We're already running this version
+        #                     pass
+        # finally:
+        #     if model:
+        #         await model.disconnect()
+        #     await controller.disconnect()
+        # return True
 
     """Rollback"""
 
@@ -452,6 +700,8 @@ class K8sJujuConnector(K8sConnector):
         :return: Returns True if successful, or raises an exception
         """
 
+        # controller = await self.get_controller(cluster_uuid)
+
         self.log.debug("[uninstall] Destroying model")
 
         await self.libjuju.destroy_model(kdu_instance, total_timeout=3600)
@@ -486,6 +736,8 @@ class K8sJujuConnector(K8sConnector):
         :return: Returns the output of the action
         """
 
+        # controller = await self.get_controller(cluster_uuid)
+
         if not params or "application-name" not in params:
             raise K8sException(
                 "Missing application-name argument, \
@@ -503,6 +755,33 @@ class K8sJujuConnector(K8sConnector):
             output, status = await self.libjuju.execute_action(
                 application_name, kdu_instance, primitive_name, **params
             )
+            # model = await self.get_model(kdu_instance, controller=controller)
+
+            # application_name = params["application-name"]
+            # application = model.applications[application_name]
+
+            # actions = await application.get_actions()
+            # if primitive_name not in actions:
+            #     raise K8sException("Primitive {} not found".format(primitive_name))
+
+            # unit = None
+            # for u in application.units:
+            #     if await u.is_leader_from_status():
+            #         unit = u
+            #         break
+
+            # if unit is None:
+            #     raise K8sException("No leader unit found to execute action")
+
+            # self.log.debug("[exec_primitive] Running action: {}".format(primitive_name))
+            # action = await unit.run_action(primitive_name, **params)
+
+            # output = await model.get_action_output(action_uuid=action.entity_id)
+            # status = await model.get_action_status(uuid_or_prefix=action.entity_id)
+
+            # status = (
+            #     status[action.entity_id] if action.entity_id in status else "failed"
+            # )
 
             if status != "completed":
                 raise K8sException(
@@ -515,6 +794,12 @@ class K8sJujuConnector(K8sConnector):
             error_msg = "Error executing primitive {}: {}".format(primitive_name, e)
             self.log.error(error_msg)
             raise K8sException(message=error_msg)
+        # finally:
+        #     await controller.disconnect()
+        # TODO: Remove these commented lines:
+        # if not self.authenticated:
+        #     self.log.debug("[exec_primitive] Connecting to controller")
+        #     await self.login(cluster_uuid)
 
     """Introspection"""
 
@@ -603,11 +888,19 @@ class K8sJujuConnector(K8sConnector):
                  and deployment_time.
         """
         status = {}
+        # controller = await self.get_controller(cluster_uuid)
+        # model = await self.get_model(kdu_instance, controller=controller)
+
+        # model_status = await model.get_status()
+        # status = model_status.applications
         model_status = await self.libjuju.get_model_status(kdu_instance)
         for name in model_status.applications:
             application = model_status.applications[name]
             status[name] = {"status": application["status"]["status"]}
 
+        # await model.disconnect()
+        # await controller.disconnect()
+
         return status
 
     async def get_services(
@@ -617,6 +910,14 @@ class K8sJujuConnector(K8sConnector):
 
         credentials = self.get_credentials(cluster_uuid=cluster_uuid)
 
+        # config_path = "/tmp/{}".format(cluster_uuid)
+        # config_file = "{}/config".format(config_path)
+
+        # if not os.path.exists(config_path):
+        #     os.makedirs(config_path)
+        # with open(config_file, "w") as f:
+        #     f.write(credentials)
+
         kubecfg = tempfile.NamedTemporaryFile()
         with open(kubecfg.name, "w") as kubecfg_file:
             kubecfg_file.write(credentials)
@@ -633,6 +934,14 @@ class K8sJujuConnector(K8sConnector):
 
         credentials = self.get_credentials(cluster_uuid=cluster_uuid)
 
+        # config_path = "/tmp/{}".format(cluster_uuid)
+        # config_file = "{}/config".format(config_path)
+
+        # if not os.path.exists(config_path):
+        #     os.makedirs(config_path)
+        # with open(config_file, "w") as f:
+        #     f.write(credentials)
+
         kubecfg = tempfile.NamedTemporaryFile()
         with open(kubecfg.name, "w") as kubecfg_file:
             kubecfg_file.write(credentials)
@@ -644,6 +953,156 @@ class K8sJujuConnector(K8sConnector):
             )
         )[0]
 
+    # Private methods
+    # async def add_k8s(self, cloud_name: str, credentials: str,) -> bool:
+    #     """Add a k8s cloud to Juju
+
+    #     Adds a Kubernetes cloud to Juju, so it can be bootstrapped with a
+    #     Juju Controller.
+
+    #     :param cloud_name str: The name of the cloud to add.
+    #     :param credentials dict: A dictionary representing the output of
+    #         `kubectl config view --raw`.
+
+    #     :returns: True if successful, otherwise raises an exception.
+    #     """
+
+    #     cmd = [self.juju_command, "add-k8s", "--local", cloud_name]
+    #     self.log.debug(cmd)
+
+    #     process = await asyncio.create_subprocess_exec(
+    #         *cmd,
+    #         stdout=asyncio.subprocess.PIPE,
+    #         stderr=asyncio.subprocess.PIPE,
+    #         stdin=asyncio.subprocess.PIPE,
+    #     )
+
+    #     # Feed the process the credentials
+    #     process.stdin.write(credentials.encode("utf-8"))
+    #     await process.stdin.drain()
+    #     process.stdin.close()
+
+    #     _stdout, stderr = await process.communicate()
+
+    #     return_code = process.returncode
+
+    #     self.log.debug("add-k8s return code: {}".format(return_code))
+
+    #     if return_code > 0:
+    #         raise Exception(stderr)
+
+    #     return True
+
+    # async def add_model(
+    #     self, model_name: str, cluster_uuid: str, controller: Controller
+    # ) -> Model:
+    #     """Adds a model to the controller
+
+    #     Adds a new model to the Juju controller
+
+    #     :param model_name str: The name of the model to add.
+    #     :param cluster_uuid str: ID of the cluster.
+    #     :param controller: Controller object in which the model will be added
+    #     :returns: The juju.model.Model object of the new model upon success or
+    #               raises an exception.
+    #     """
+
+    #     self.log.debug(
+    #         "Adding model '{}' to cluster_uuid '{}'".format(model_name, cluster_uuid)
+    #     )
+    #     model = None
+    #     try:
+    #         if self.juju_public_key is not None:
+    #             model = await controller.add_model(
+    #                 model_name, config={"authorized-keys": self.juju_public_key}
+    #             )
+    #         else:
+    #             model = await controller.add_model(model_name)
+    #     except Exception as ex:
+    #         self.log.debug(ex)
+    #         self.log.debug("Caught exception: {}".format(ex))
+    #         pass
+
+    #     return model
+
+    # async def bootstrap(
+    #     self, cloud_name: str, cluster_uuid: str, loadbalancer: bool
+    # ) -> bool:
+    #     """Bootstrap a Kubernetes controller
+
+    #     Bootstrap a Juju controller inside the Kubernetes cluster
+
+    #     :param cloud_name str: The name of the cloud.
+    #     :param cluster_uuid str: The UUID of the cluster to bootstrap.
+    #     :param loadbalancer bool: If the controller should use loadbalancer or not.
+    #     :returns: True upon success or raises an exception.
+    #     """
+
+    #     if not loadbalancer:
+    #         cmd = [self.juju_command, "bootstrap", cloud_name, cluster_uuid]
+    #     else:
+    #         """
+    #         For public clusters, specify that the controller service is using a
+    #         LoadBalancer.
+    #         """
+    #         cmd = [
+    #             self.juju_command,
+    #             "bootstrap",
+    #             cloud_name,
+    #             cluster_uuid,
+    #             "--config",
+    #             "controller-service-type=loadbalancer",
+    #         ]
+
+    #     self.log.debug(
+    #         "Bootstrapping controller {} in cloud {}".format(cluster_uuid, cloud_name)
+    #     )
+
+    #     process = await asyncio.create_subprocess_exec(
+    #         *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
+    #     )
+
+    #     _stdout, stderr = await process.communicate()
+
+    #     return_code = process.returncode
+
+    #     if return_code > 0:
+    #         #
+    #         if b"already exists" not in stderr:
+    #             raise Exception(stderr)
+
+    #     return True
+
+    # async def destroy_controller(self, cluster_uuid: str) -> bool:
+    #     """Destroy a Kubernetes controller
+
+    #     Destroy an existing Kubernetes controller.
+
+    #     :param cluster_uuid str: The UUID of the cluster to bootstrap.
+    #     :returns: True upon success or raises an exception.
+    #     """
+    #     cmd = [
+    #         self.juju_command,
+    #         "destroy-controller",
+    #         "--destroy-all-models",
+    #         "--destroy-storage",
+    #         "-y",
+    #         cluster_uuid,
+    #     ]
+
+    #     process = await asyncio.create_subprocess_exec(
+    #         *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
+    #     )
+
+    #     _stdout, stderr = await process.communicate()
+
+    #     return_code = process.returncode
+
+    #     if return_code > 0:
+    #         #
+    #         if "already exists" not in stderr:
+    #             raise Exception(stderr)
+
     def get_credentials(self, cluster_uuid: str) -> str:
         """
         Get Cluster Kubeconfig
@@ -677,6 +1136,51 @@ class K8sJujuConnector(K8sConnector):
         """
         return "cred-{}".format(cluster_uuid)
 
+    # def get_config(self, cluster_uuid: str,) -> dict:
+    #     """Get the cluster configuration
+
+    #     Gets the configuration of the cluster
+
+    #     :param cluster_uuid str: The UUID of the cluster.
+    #     :return: A dict upon success, or raises an exception.
+    #     """
+
+    #     juju_db = self.db.get_one("admin", {"_id": "juju"})
+    #     config = None
+    #     for k in juju_db["k8sclusters"]:
+    #         if k["_id"] == cluster_uuid:
+    #             config = k["config"]
+    #             self.db.encrypt_decrypt_fields(
+    #                 config,
+    #                 "decrypt",
+    #                 ["secret", "cacert"],
+    #                 schema_version="1.1",
+    #                 salt=k["_id"],
+    #             )
+    #             break
+    #     if not config:
+    #         raise Exception(
+    #             "Unable to locate configuration for cluster {}".format(cluster_uuid)
+    #         )
+    #     return config
+
+    # async def get_model(self, model_name: str, controller: Controller) -> Model:
+    #     """Get a model from the Juju Controller.
+
+    #     Note: Model objects returned must call disconnected() before it goes
+    #     out of scope.
+
+    #     :param model_name str: The name of the model to get
+    #     :param controller Controller: Controller object
+    #     :return The juju.model.Model object if found, or None.
+    #     """
+
+    #     models = await controller.list_models()
+    #     if model_name not in models:
+    #         raise N2VCNotFound("Model {} not found".format(model_name))
+    #     self.log.debug("Found model: {}".format(model_name))
+    #     return await controller.get_model(model_name)
+
     def get_namespace(
         self,
         cluster_uuid: str,
@@ -687,8 +1191,177 @@ class K8sJujuConnector(K8sConnector):
         :param cluster_uuid str: The UUID of the cluster
         :returns: The namespace UUID, or raises an exception
         """
+        # config = self.get_config(cluster_uuid)
+
+        # Make sure the name is in the config
+        # if "namespace" not in config:
+        #     raise Exception("Namespace not found.")
+
+        # TODO: We want to make sure this is unique to the cluster, in case
+        # the cluster is being reused.
+        # Consider pre/appending the cluster id to the namespace string
         pass
 
+    # TODO: Remove these lines of code
+    # async def has_model(self, model_name: str) -> bool:
+    #     """Check if a model exists in the controller
+
+    #     Checks to see if a model exists in the connected Juju controller.
+
+    #     :param model_name str: The name of the model
+    #     :return: A boolean indicating if the model exists
+    #     """
+    #     models = await self.controller.list_models()
+
+    #     if model_name in models:
+    #         return True
+    #     return False
+
+    # def is_local_k8s(self, credentials: str,) -> bool:
+    #     """Check if a cluster is local
+
+    #     Checks if a cluster is running in the local host
+
+    #     :param credentials dict: A dictionary containing the k8s credentials
+    #     :returns: A boolean if the cluster is running locally
+    #     """
+
+    #     creds = yaml.safe_load(credentials)
+
+    #     if creds and os.getenv("OSMLCM_VCA_APIPROXY"):
+    #         for cluster in creds["clusters"]:
+    #             if "server" in cluster["cluster"]:
+    #                 if os.getenv("OSMLCM_VCA_APIPROXY") in cluster["cluster"]["server"]:
+    #                     return True
+
+    #     return False
+
+    # async def get_controller(self, cluster_uuid):
+    #     """Login to the Juju controller."""
+
+    #     config = self.get_config(cluster_uuid)
+
+    #     juju_endpoint = config["endpoint"]
+    #     juju_user = config["username"]
+    #     juju_secret = config["secret"]
+    #     juju_ca_cert = config["cacert"]
+
+    #     controller = Controller()
+
+    #     if juju_secret:
+    #         self.log.debug(
+    #             "Connecting to controller... ws://{} as {}".format(
+    #                 juju_endpoint, juju_user,
+    #             )
+    #         )
+    #         try:
+    #             await controller.connect(
+    #                 endpoint=juju_endpoint,
+    #                 username=juju_user,
+    #                 password=juju_secret,
+    #                 cacert=juju_ca_cert,
+    #             )
+    #             self.log.debug("JujuApi: Logged into controller")
+    #             return controller
+    #         except Exception as ex:
+    #             self.log.debug(ex)
+    #             self.log.debug("Caught exception: {}".format(ex))
+    #     else:
+    #         self.log.fatal("VCA credentials not configured.")
+
+    # TODO: Remove these commented lines
+    #         self.authenticated = False
+    # if self.authenticated:
+    #         return
+
+    #     self.connecting = True
+    #     juju_public_key = None
+    #     self.authenticated = True
+    #     Test: Make sure we have the credentials loaded
+    # async def logout(self):
+    #     """Logout of the Juju controller."""
+    #     self.log.debug("[logout]")
+    #     if not self.authenticated:
+    #         return False
+
+    #     for model in self.models:
+    #         self.log.debug("Logging out of model {}".format(model))
+    #         await self.models[model].disconnect()
+
+    #     if self.controller:
+    #         self.log.debug("Disconnecting controller {}".format(self.controller))
+    #         await self.controller.disconnect()
+    #         self.controller = None
+
+    #     self.authenticated = False
+
+    # async def remove_cloud(self, cloud_name: str,) -> bool:
+    #     """Remove a k8s cloud from Juju
+
+    #     Removes a Kubernetes cloud from Juju.
+
+    #     :param cloud_name str: The name of the cloud to add.
+
+    #     :returns: True if successful, otherwise raises an exception.
+    #     """
+
+    #     # Remove the bootstrapped controller
+    #     cmd = [self.juju_command, "remove-k8s", "--client", cloud_name]
+    #     process = await asyncio.create_subprocess_exec(
+    #         *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
+    #     )
+
+    #     _stdout, stderr = await process.communicate()
+
+    #     return_code = process.returncode
+
+    #     if return_code > 0:
+    #         raise Exception(stderr)
+
+    #     # Remove the cloud from the local config
+    #     cmd = [self.juju_command, "remove-cloud", "--client", cloud_name]
+    #     process = await asyncio.create_subprocess_exec(
+    #         *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
+    #     )
+
+    #     _stdout, stderr = await process.communicate()
+
+    #     return_code = process.returncode
+
+    #     if return_code > 0:
+    #         raise Exception(stderr)
+
+    #     return True
+
+    # async def set_config(self, cluster_uuid: str, config: dict,) -> bool:
+    #     """Save the cluster configuration
+
+    #     Saves the cluster information to the Mongo database
+
+    #     :param cluster_uuid str: The UUID of the cluster
+    #     :param config dict: A dictionary containing the cluster configuration
+    #     """
+
+    #     juju_db = self.db.get_one("admin", {"_id": "juju"})
+
+    #     k8sclusters = juju_db["k8sclusters"] if "k8sclusters" in juju_db else []
+    #     self.db.encrypt_decrypt_fields(
+    #         config,
+    #         "encrypt",
+    #         ["secret", "cacert"],
+    #         schema_version="1.1",
+    #         salt=cluster_uuid,
+    #     )
+    #     k8sclusters.append({"_id": cluster_uuid, "config": config})
+    #     self.db.set_one(
+    #         table="admin",
+    #         q_filter={"_id": "juju"},
+    #         update_dict={"k8sclusters": k8sclusters},
+    #     )
+
+    # Private methods to create/delete needed resources in the
+    # Kubernetes cluster to create the K8s cloud in Juju
+
     def _create_cluster_role(
         self,
         kubectl: Kubectl,
index 6848e0f..a79d00d 100644 (file)
@@ -926,6 +926,33 @@ class Libjuju:
                 machine = model.machines[machine_id]
                 await machine.destroy(force=True)
 
+    # async def destroy_machine(
+    #     self, model: Model, machine_id: str, total_timeout: float = 3600
+    # ):
+    #     """
+    #     Destroy machine
+
+    #     :param: model:          Model object
+    #     :param: machine_id:     Machine id
+    #     :param: total_timeout:  Timeout in seconds
+    #     """
+    #     machines = await model.get_machines()
+    #     if machine_id in machines:
+    #         machine = machines[machine_id]
+    #         await machine.destroy(force=True)
+    #         # max timeout
+    #         end = time.time() + total_timeout
+
+    #         # wait for machine removal
+    #         machines = await model.get_machines()
+    #         while machine_id in machines and time.time() < end:
+    #             self.log.debug("Waiting for machine {} is destroyed".format(machine_id))
+    #             await asyncio.sleep(0.5)
+    #             machines = await model.get_machines()
+    #         self.log.debug("Machine destroyed: {}".format(machine_id))
+    #     else:
+    #         self.log.debug("Machine not found: {}".format(machine_id))
+
     async def configure_application(
         self, model_name: str, application_name: str, config: dict = None
     ):
index 6e78c47..2566b0c 100644 (file)
@@ -788,6 +788,18 @@ class N2VCJujuConnector(N2VCConnector):
                 ).format(ee_id, application_name, e)
             )
 
+        # destroy the machine
+        # try:
+        #     await self._juju_destroy_machine(
+        #         model_name=model_name,
+        #         machine_id=machine_id,
+        #         total_timeout=total_timeout
+        #     )
+        # except Exception as e:
+        #     raise N2VCException(
+        #        message='Error deleting execution environment {} (machine {}) : {}'
+        #                .format(ee_id, machine_id, e))
+
         self.log.info("Execution environment {} deleted".format(ee_id))
 
     async def exec_primitive(
diff --git a/tests/README.md b/tests/README.md
new file mode 100644 (file)
index 0000000..4d6e64e
--- /dev/null
@@ -0,0 +1,68 @@
+<!--
+ 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
+
+
+# Preparation
+## Environment variables
+
+The test currently requires some environment variables set in order to run, but these will be deprecated as soon as possible.
+
+## LXD
+
+LXD should be installed via snap.
+
+The connection to the LXD API server expects to use a self-signed SSL certificate, generated by lxc (`lxc list`, et al) is first one.
+
+## Juju
+
+Juju is expected to be installed via snap and bootstrapped.
+
+Run `juju status -m controller` and capture the IP address of machine 0. This is the Juju controller, specified in VCA_HOST
+
+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
+
+Tests are written with pytest, driven by tox. All tests are run from the root directory of the repository.
+
+## Run one test
+
+To run a single integration test, we tell tox which environment we need, and then the path to the test.
+
+```bash
+tox -e integration -- tests/integration/test_non-string_parameter.py
+```
+
+## Running all tests
+
+`make test` will invoke tox to run all unit tests. Alternatively, you can limit this to a specific type of test by invoking tox manually:
+```bash
+tox -e integration -- tests/integration/
+```
+
+# TODO
+- Update CI environment to have Juju and LXD available via snap
+- Investigate running via Docker
+- Remove the requirement for setting environment variables
+- Integrate into Jenkins so that tests run against every commit
+- Add global timeout to abort tests that are hung
+- Only build a charm once per test run, i.e., if two or more tests use the same charm, we should only call `charm build` once.
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644 (file)
index 0000000..22ec1e3
--- /dev/null
@@ -0,0 +1,14 @@
+#  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.
+
diff --git a/tests/base.py b/tests/base.py
new file mode 100644 (file)
index 0000000..aabf359
--- /dev/null
@@ -0,0 +1,1279 @@
+#!/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
+import n2vc.vnf
+import pylxd
+import pytest
+import os
+import shlex
+import subprocess
+import time
+import uuid
+import yaml
+
+from juju.controller import Controller
+
+# Disable InsecureRequestWarning w/LXD
+import urllib3
+urllib3.disable_warnings()
+logging.getLogger("urllib3").setLevel(logging.WARNING)
+
+here = os.path.dirname(os.path.realpath(__file__))
+
+
+class CleanController():
+    """
+    Context manager that automatically connects and disconnects from
+    the currently active controller.
+
+    Note: Unlike CleanModel, this will not create a new controller for you,
+    and an active controller must already be available.
+    """
+    def __init__(self):
+        self._controller = None
+
+    async def __aenter__(self):
+        self._controller = Controller()
+        await self._controller.connect()
+        return self._controller
+
+    async def __aexit__(self, exc_type, exc, tb):
+        await self._controller.disconnect()
+
+
+def debug(msg):
+    """Format debug messages in a consistent way."""
+    now = datetime.datetime.now()
+
+    # TODO: Decide on the best way to log. Output from `logging.debug` shows up
+    # when a test fails, but print() will always show up when running tox with
+    # `-s`, which is really useful for debugging single tests without having to
+    # insert a False assert to see the log.
+    logging.debug(
+        "[{}] {}".format(now.strftime('%Y-%m-%dT%H:%M:%S'), msg)
+    )
+    print(
+        "[{}] {}".format(now.strftime('%Y-%m-%dT%H:%M:%S'), msg)
+    )
+
+
+def get_charm_path():
+    return "{}/charms".format(here)
+
+
+def get_layer_path():
+    return "{}/charms/layers".format(here)
+
+
+def collect_metrics(application):
+    """Invoke Juju's metrics collector.
+
+    Caveat: this shells out to the `juju collect-metrics` command, rather than
+    making an API call. At the time of writing, that API is not exposed through
+    the client library.
+    """
+
+    try:
+        subprocess.check_call(['juju', 'collect-metrics', application])
+    except subprocess.CalledProcessError as e:
+        raise Exception("Unable to collect metrics: {}".format(e))
+
+
+def has_metrics(charm):
+    """Check if a charm has metrics defined."""
+    metricsyaml = "{}/{}/metrics.yaml".format(
+        get_layer_path(),
+        charm,
+    )
+    if os.path.exists(metricsyaml):
+        return True
+    return False
+
+
+def get_descriptor(descriptor):
+    desc = None
+    try:
+        tmp = yaml.safe_load(descriptor)
+
+        # Remove the envelope
+        root = list(tmp.keys())[0]
+        if root == "nsd:nsd-catalog":
+            desc = tmp['nsd:nsd-catalog']['nsd'][0]
+        elif root == "vnfd:vnfd-catalog":
+            desc = tmp['vnfd:vnfd-catalog']['vnfd'][0]
+    except ValueError:
+        assert False
+    return desc
+
+
+def get_n2vc(loop=None):
+    """Return an instance of N2VC.VNF."""
+    log = logging.getLogger()
+    log.level = logging.DEBUG
+
+    # Extract parameters from the environment in order to run our test
+    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_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()
+    if juju_public_key:
+        debug("Reading Juju public key @ {}".format(juju_public_key))
+        with open(juju_public_key, 'r') as f:
+            juju_public_key = f.read()
+        debug("Found public key: {}".format(juju_public_key))
+    else:
+        raise Exception("No Juju Public Key found")
+
+    # Get the ca-cert
+    # os.path.expanduser("~/.config/lxc")
+    # with open("{}/agent.conf".format(AGENT_PATH), "r") as f:
+    #     try:
+    #         y = yaml.safe_load(f)
+    #         self.cacert = y['cacert']
+    #     except yaml.YAMLError as exc:
+    #         log("Unable to find Juju ca-cert.")
+    #         raise exc
+
+    client = n2vc.vnf.N2VC(
+        log=log,
+        server=vca_host,
+        port=vca_port,
+        user=vca_user,
+        secret=vca_secret,
+        artifacts=vca_charms,
+        loop=loop,
+        juju_public_key=juju_public_key,
+        ca_cert=vca_cacert,
+    )
+    return client
+
+
+def create_lxd_container(public_key=None, name="test_name"):
+    """
+    Returns a container object
+
+    If public_key isn't set, we'll use the Juju ssh key
+
+    :param public_key: The public key to inject into the container
+    :param name: The name of the test being run
+    """
+    container = None
+
+    # Format name so it's valid
+    name = name.replace("_", "-").replace(".", "")
+
+    client = get_lxd_client()
+    if not client:
+        raise Exception("Unable to connect to LXD")
+
+    test_machine = "test-{}-{}".format(
+        uuid.uuid4().hex[-4:],
+        name,
+    )
+
+    private_key_path, public_key_path = find_n2vc_ssh_keys()
+
+    try:
+        # create profile w/cloud-init and juju ssh key
+        if not public_key:
+            public_key = ""
+            with open(public_key_path, "r") as f:
+                public_key = f.readline()
+
+        client.profiles.create(
+            test_machine,
+            config={
+                'user.user-data': '#cloud-config\nssh_authorized_keys:\n- {}'.format(public_key)},
+            devices={
+                'root': {'path': '/', 'pool': 'default', 'type': 'disk'},
+                'eth0': {
+                    'nictype': 'bridged',
+                    'parent': 'lxdbr0',
+                    'type': 'nic'
+                }
+            }
+        )
+    except Exception as ex:
+        debug("Error creating lxd profile {}: {}".format(test_machine, ex))
+        raise ex
+
+    try:
+        # create lxc machine
+        config = {
+            'name': test_machine,
+            'source': {
+                'type': 'image',
+                'alias': 'xenial',
+                'mode': 'pull',
+                'protocol': 'simplestreams',
+                'server': 'https://cloud-images.ubuntu.com/releases',
+            },
+            'profiles': [test_machine],
+        }
+        container = client.containers.create(config, wait=True)
+        container.start(wait=True)
+    except Exception as ex:
+        debug("Error creating lxd container {}: {}".format(test_machine, ex))
+        # This is a test-ending failure.
+        raise ex
+
+    def wait_for_network(container, timeout=30):
+        """Wait for eth0 to have an ipv4 address."""
+        starttime = time.time()
+        while(time.time() < starttime + timeout):
+            time.sleep(1)
+            if 'eth0' in container.state().network:
+                addresses = container.state().network['eth0']['addresses']
+                if len(addresses) > 0:
+                    if addresses[0]['family'] == 'inet':
+                        return addresses[0]
+        return None
+
+    try:
+        wait_for_network(container)
+    except Exception as ex:
+        debug(
+            "Error waiting for container {} network: {}".format(
+                test_machine,
+                ex,
+            )
+        )
+
+    try:
+        waitcount = 0
+        while waitcount <= 5:
+            if is_sshd_running(container):
+                break
+            waitcount += 1
+            time.sleep(1)
+        if waitcount >= 5:
+            debug("couldn't detect sshd running")
+            raise Exception("Unable to verify container sshd")
+
+    except Exception as ex:
+        debug(
+            "Error checking sshd status on {}: {}".format(
+                test_machine,
+                ex,
+            )
+        )
+
+    # HACK: We need to give sshd a chance to bind to the interface,
+    # and pylxd's container.execute seems to be broken and fails and/or
+    # hangs trying to properly check if the service is up.
+    (exit_code, stdout, stderr) = container.execute([
+        'ping',
+        '-c', '5',   # Wait for 5 ECHO_REPLY
+        '8.8.8.8',   # Ping Google's public DNS
+        '-W', '15',  # Set a 15 second deadline
+    ])
+    if exit_code > 0:
+        # The network failed
+        raise Exception("Unable to verify container network")
+
+    return container
+
+
+def is_sshd_running(container):
+    """Check if sshd is running in the container.
+
+    Check to see if the sshd process is running and listening on port 22.
+
+    :param container: The container to check
+    :return boolean: True if sshd is running.
+    """
+    debug("Container: {}".format(container))
+    try:
+        (rc, stdout, stderr) = container.execute(
+            ["service", "ssh", "status"]
+        )
+        # If the status is a) found and b) running, the exit code will be 0
+        if rc == 0:
+            return True
+    except Exception as ex:
+        debug("Failed to check sshd service status: {}".format(ex))
+
+    return False
+
+
+def destroy_lxd_container(container):
+    """Stop and delete a LXD container.
+
+    Sometimes we see errors talking to LXD -- ephemerial issues like
+    load or a bug that's killed the API. We'll do our best to clean
+    up here, and we should run a cleanup after all tests are finished
+    to remove any extra containers and profiles belonging to us.
+    """
+
+    if type(container) is bool:
+        return
+
+    name = container.name
+    debug("Destroying container {}".format(name))
+
+    client = get_lxd_client()
+
+    def wait_for_stop(timeout=30):
+        """Wait for eth0 to have an ipv4 address."""
+        starttime = time.time()
+        while(time.time() < starttime + timeout):
+            time.sleep(1)
+            if container.state == "Stopped":
+                return
+
+    def wait_for_delete(timeout=30):
+        starttime = time.time()
+        while(time.time() < starttime + timeout):
+            time.sleep(1)
+            if client.containers.exists(name) is False:
+                return
+
+    try:
+        container.stop(wait=False)
+        wait_for_stop()
+    except Exception as ex:
+        debug(
+            "Error stopping container {}: {}".format(
+                name,
+                ex,
+            )
+        )
+
+    try:
+        container.delete(wait=False)
+        wait_for_delete()
+    except Exception as ex:
+        debug(
+            "Error deleting container {}: {}".format(
+                name,
+                ex,
+            )
+        )
+
+    try:
+        # Delete the profile created for this container
+        profile = client.profiles.get(name)
+        if profile:
+            profile.delete()
+    except Exception as ex:
+        debug(
+            "Error deleting profile {}: {}".format(
+                name,
+                ex,
+            )
+        )
+
+
+def find_lxd_config():
+    """Find the LXD configuration directory."""
+    paths = []
+    paths.append(os.path.expanduser("~/.config/lxc"))
+    paths.append(os.path.expanduser("~/snap/lxd/current/.config/lxc"))
+
+    for path in paths:
+        if os.path.exists(path):
+            crt = os.path.expanduser("{}/client.crt".format(path))
+            key = os.path.expanduser("{}/client.key".format(path))
+            if os.path.exists(crt) and os.path.exists(key):
+                return (crt, key)
+    return (None, None)
+
+
+def find_n2vc_ssh_keys():
+    """Find the N2VC ssh keys."""
+
+    paths = []
+    paths.append(os.path.expanduser("~/.ssh/"))
+
+    for path in paths:
+        if os.path.exists(path):
+            private = os.path.expanduser("{}/id_n2vc_rsa".format(path))
+            public = os.path.expanduser("{}/id_n2vc_rsa.pub".format(path))
+            if os.path.exists(private) and os.path.exists(public):
+                return (private, public)
+    return (None, None)
+
+
+def find_juju_ssh_keys():
+    """Find the Juju ssh keys."""
+
+    paths = []
+    paths.append(os.path.expanduser("~/.local/share/juju/ssh"))
+
+    for path in paths:
+        if os.path.exists(path):
+            private = os.path.expanduser("{}/juju_id_rsa".format(path))
+            public = os.path.expanduser("{}/juju_id_rsa.pub".format(path))
+            if os.path.exists(private) and os.path.exists(public):
+                return (private, public)
+    return (None, None)
+
+
+def get_juju_private_key():
+    keys = find_juju_ssh_keys()
+    return keys[0]
+
+
+def get_juju_public_key():
+    """Find the Juju public key."""
+    paths = []
+
+    if 'VCA_PATH' in os.environ:
+        paths.append("{}/ssh".format(os.environ["VCA_PATH"]))
+
+    paths.append(os.path.expanduser("~/.local/share/juju/ssh"))
+    paths.append("/root/.local/share/juju/ssh")
+
+    for path in paths:
+        if os.path.exists(path):
+            public = os.path.expanduser("{}/juju_id_rsa.pub".format(path))
+            if os.path.exists(public):
+                return public
+    return None
+
+
+def get_lxd_client(host=None, port="8443", verify=False):
+    """ Get the LXD client."""
+
+    if host is None:
+        if 'LXD_HOST' in os.environ:
+            host = os.environ['LXD_HOST']
+        else:
+            host = '127.0.0.1'
+
+    passwd = None
+    if 'LXD_SECRET' in os.environ:
+        passwd = os.environ['LXD_SECRET']
+
+    # debug("Connecting to LXD remote {} w/authentication ({})".format(
+    #     host,
+    #     passwd
+    # ))
+    client = None
+    (crt, key) = find_lxd_config()
+
+    if crt and key:
+        client = pylxd.Client(
+            endpoint="https://{}:{}".format(host, port),
+            cert=(crt, key),
+            verify=verify,
+        )
+
+        # If the LXD server has a pasword set, authenticate with it.
+        if not client.trusted and passwd:
+            try:
+                client.authenticate(passwd)
+                if not client.trusted:
+                    raise Exception("Unable to authenticate with LXD remote")
+            except pylxd.exceptions.LXDAPIException as ex:
+                if 'Certificate already in trust store' in ex:
+                    pass
+
+    return client
+
+
+# TODO: This is marked serial but can be run in parallel with work, including:
+# - Fixing an event loop issue; seems that all tests stop when one test stops?
+
+
+@pytest.mark.serial
+class TestN2VC(object):
+    """TODO:
+    1. Validator Validation
+
+    Automatically validate the descriptors we're using here, unless the test
+    author explicitly wants to skip them. Useful to make sure tests aren't
+    being run against invalid descriptors, validating functionality that may
+    fail against a properly written descriptor.
+
+    We need to have a flag (instance variable) that controls this behavior. It
+    may be necessary to skip validation and run against a descriptor
+    implementing features that have not yet been released in the Information
+    Model.
+    """
+
+    """
+    The six phases of integration testing, for the test itself and each charm?:
+
+    setup/teardown_class:
+    1. Prepare      - Verify the environment and create a new model
+    2. Deploy       - Mark the test as ready to execute
+    3. Configure    - Configuration to reach Active state
+    4. Test         - Execute primitive(s) to verify success
+    5. Collect      - Collect any useful artifacts for debugging (charm, logs)
+    6. Destroy      - Destroy the model
+
+
+    1. Prepare      - Building of charm
+    2. Deploy       - Deploying charm
+    3. Configure    - Configuration to reach Active state
+    4. Test         - Execute primitive(s) to verify success
+    5. Collect      - Collect any useful artifacts for debugging (charm, logs)
+    6. Destroy      - Destroy the charm
+
+    """
+    @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.n2vc = None
+
+        # Track internal state for each test run
+        self.state = {}
+
+        # Parse the test's descriptors
+        self.nsd = get_descriptor(self.NSD_YAML)
+        self.vnfd = get_descriptor(self.VNFD_YAML)
+
+        self.ns_name = self.nsd['name']
+        self.vnf_name = self.vnfd['name']
+
+        self.charms = {}
+        self.parse_vnf_descriptor()
+        assert self.charms is not {}
+
+        # Track artifacts, like compiled charms, that will need to be removed
+        self.artifacts = {}
+
+        # Build the charm(s) needed for this test
+        for charm in self.get_charm_names():
+            # debug("Building charm {}".format(charm))
+            self.get_charm(charm)
+
+        # A bit of a hack, in order to allow the N2VC callback to run parallel
+        # to pytest. Test(s) should wait for this flag to change to False
+        # before returning.
+        self._running = True
+        self._stopping = False
+
+    @classmethod
+    def teardown_class(self):
+        """ teardown any state that was previously setup with a call to
+        setup_class.
+        """
+        debug("Running teardown_class...")
+        try:
+
+            debug("Destroying LXD containers...")
+            for application in self.state:
+                if self.state[application]['container']:
+                    destroy_lxd_container(self.state[application]['container'])
+            debug("Destroying LXD containers...done.")
+
+            # Logout of N2VC
+            if self.n2vc:
+                debug("teardown_class(): Logging out of N2VC...")
+                yield from self.n2vc.logout()
+                debug("teardown_class(): Logging out of N2VC...done.")
+
+            debug("Running teardown_class...done.")
+        except Exception as ex:
+            debug("Exception in teardown_class: {}".format(ex))
+
+    @classmethod
+    def all_charms_active(self):
+        """Determine if the all deployed charms are active."""
+        active = 0
+
+        for application in self.state:
+            if 'status' in self.state[application]:
+                debug("status of {} is '{}'".format(
+                    application,
+                    self.state[application]['status'],
+                ))
+                if self.state[application]['status'] == 'active':
+                    active += 1
+
+        debug("Active charms: {}/{}".format(
+            active,
+            len(self.charms),
+        ))
+
+        if active == len(self.charms):
+            return True
+
+        return False
+
+    @classmethod
+    def are_tests_finished(self):
+        appcount = len(self.state)
+
+        # If we don't have state yet, keep running.
+        if appcount == 0:
+            debug("No applications")
+            return False
+
+        if self._stopping:
+            debug("_stopping is True")
+            return True
+
+        appdone = 0
+        for application in self.state:
+            if self.state[application]['done']:
+                appdone += 1
+
+        debug("{}/{} charms tested".format(appdone, appcount))
+
+        if appcount == appdone:
+            return True
+
+        return False
+
+    @classmethod
+    async def running(self, timeout=600):
+        """Returns if the test is still running.
+
+        @param timeout The time, in seconds, to wait for the test to complete.
+        """
+        if self.are_tests_finished():
+            await self.stop()
+            return False
+
+        await asyncio.sleep(30)
+
+        return self._running
+
+    @classmethod
+    def get_charm(self, charm):
+        """Build and return the path to the test charm.
+
+        Builds one of the charms in tests/charms/layers and returns the path
+        to the compiled charm. The charm will automatically be removed when
+        when the test is complete.
+
+        Returns: The path to the built charm or None if `charm build` failed.
+        """
+        # Make sure the charm snap is installed
+        charm_cmd = None
+        try:
+            subprocess.check_call(['which', 'charm'])
+            charm_cmd = "charm build"
+        except subprocess.CalledProcessError:
+            # charm_cmd = "charm-build"
+            # debug("Using legacy charm-build")
+            raise Exception("charm snap not installed.")
+
+        if charm not in self.artifacts:
+            try:
+                # Note: This builds the charm under N2VC/tests/charms/builds/
+                # Currently, the snap-installed command only has write access
+                # to the $HOME (changing in an upcoming release) so writing to
+                # /tmp isn't possible at the moment.
+
+                builds = get_charm_path()
+                if not os.path.exists("{}/builds/{}".format(builds, charm)):
+                    cmd = "{} --no-local-layers {}/{} -o {}/".format(
+                        charm_cmd,
+                        get_layer_path(),
+                        charm,
+                        builds,
+                    )
+                    # debug(cmd)
+
+                    env = os.environ.copy()
+                    env["CHARM_BUILD_DIR"] = builds
+
+                    subprocess.check_call(shlex.split(cmd), env=env)
+
+            except subprocess.CalledProcessError as e:
+                # charm build will return error code 100 if the charm fails
+                # the auto-run of charm proof, which we can safely ignore for
+                # our CI charms.
+                if e.returncode != 100:
+                    raise Exception("charm build failed: {}.".format(e))
+
+            self.artifacts[charm] = {
+                'tmpdir': builds,
+                'charm': "{}/builds/{}".format(builds, charm),
+            }
+
+        return self.artifacts[charm]['charm']
+
+    @classmethod
+    async def deploy(self, vnf_index, charm, params, loop):
+        """An inner function to do the deployment of a charm from
+        either a vdu or vnf.
+        """
+
+        if not self.n2vc:
+            self.n2vc = get_n2vc(loop=loop)
+
+        debug("Creating model for Network Service {}".format(self.ns_name))
+        await self.n2vc.CreateNetworkService(self.ns_name)
+
+        application = self.n2vc.FormatApplicationName(
+            self.ns_name,
+            self.vnf_name,
+            str(vnf_index),
+        )
+
+        # Initialize the state of the application
+        self.state[application] = {
+            'status': None,     # Juju status
+            'container': None,  # lxd container, for proxy charms
+            'actions': {},      # Actions we've executed
+            'done': False,      # Are we done testing this charm?
+            'phase': "deploy",  # What phase is this application in?
+        }
+
+        debug("Deploying charm at {}".format(self.artifacts[charm]))
+
+        # If this is a native charm, we need to provision the underlying
+        # machine ala an LXC container.
+        machine_spec = {}
+
+        if not self.isproxy(application):
+            debug("Creating container for native charm")
+            # args = ("default", application, None, None)
+            self.state[application]['container'] = create_lxd_container(
+                name=os.path.basename(__file__)
+            )
+
+            hostname = self.get_container_ip(
+                self.state[application]['container'],
+            )
+
+            machine_spec = {
+                'hostname': hostname,
+                'username': 'ubuntu',
+            }
+
+        await self.n2vc.DeployCharms(
+            self.ns_name,
+            application,
+            self.vnfd,
+            self.get_charm(charm),
+            params,
+            machine_spec,
+            self.n2vc_callback,
+        )
+
+    @classmethod
+    def parse_vnf_descriptor(self):
+        """Parse the VNF descriptor to make running tests easier.
+
+        Parse the charm information in the descriptor to make it easy to write
+        tests to run again it.
+
+        Each charm becomes a dictionary in a list:
+        [
+            'is-proxy': True,
+            'vnf-member-index': 1,
+            'vnf-name': '',
+            'charm-name': '',
+            'initial-config-primitive': {},
+            'config-primitive': {}
+        ]
+        - charm name
+        - is this a proxy charm?
+        - what are the initial-config-primitives (day 1)?
+        - what are the config primitives (day 2)?
+
+        """
+        charms = {}
+
+        # You'd think this would be explicit, but it's just an incremental
+        # value that should be consistent.
+        vnf_member_index = 0
+
+        """Get all vdu and/or vdu config in a descriptor."""
+        config = self.get_config()
+        for cfg in config:
+            if 'juju' in cfg:
+
+                # Get the name to be used for the deployed application
+                application_name = n2vc.vnf.N2VC().FormatApplicationName(
+                    self.ns_name,
+                    self.vnf_name,
+                    str(vnf_member_index),
+                )
+
+                charm = {
+                    'application-name': application_name,
+                    'proxy': True,
+                    'vnf-member-index': vnf_member_index,
+                    'vnf-name': self.vnf_name,
+                    'name': None,
+                    'initial-config-primitive': {},
+                    'config-primitive': {},
+                }
+
+                juju = cfg['juju']
+                charm['name'] = juju['charm']
+
+                if 'proxy' in juju:
+                    charm['proxy'] = juju['proxy']
+
+                if 'initial-config-primitive' in cfg:
+                    charm['initial-config-primitive'] = \
+                        cfg['initial-config-primitive']
+
+                if 'config-primitive' in cfg:
+                    charm['config-primitive'] = cfg['config-primitive']
+
+                charms[application_name] = charm
+
+            # Increment the vnf-member-index
+            vnf_member_index += 1
+
+        self.charms = charms
+
+    @classmethod
+    def isproxy(self, application_name):
+
+        assert application_name in self.charms
+        assert 'proxy' in self.charms[application_name]
+        assert type(self.charms[application_name]['proxy']) is bool
+
+        # debug(self.charms[application_name])
+        return self.charms[application_name]['proxy']
+
+    @classmethod
+    def get_config(self):
+        """Return an iterable list of config items (vdu and vnf).
+
+        As far as N2VC is concerned, the config section for vdu and vnf are
+        identical. This joins them together so tests only need to iterate
+        through one list.
+        """
+        configs = []
+
+        """Get all vdu and/or vdu config in a descriptor."""
+        vnf_config = self.vnfd.get("vnf-configuration")
+        if vnf_config:
+            juju = vnf_config['juju']
+            if juju:
+                configs.append(vnf_config)
+
+        for vdu in self.vnfd['vdu']:
+            vdu_config = vdu.get('vdu-configuration')
+            if vdu_config:
+                juju = vdu_config['juju']
+                if juju:
+                    configs.append(vdu_config)
+
+        return configs
+
+    @classmethod
+    def get_charm_names(self):
+        """Return a list of charms used by the test descriptor."""
+
+        charms = {}
+
+        # Check if the VDUs in this VNF have a charm
+        for config in self.get_config():
+            juju = config['juju']
+
+            name = juju['charm']
+            if name not in charms:
+                charms[name] = 1
+
+        return charms.keys()
+
+    @classmethod
+    def get_phase(self, application):
+        return self.state[application]['phase']
+
+    @classmethod
+    def set_phase(self, application, phase):
+        self.state[application]['phase'] = phase
+
+    @classmethod
+    async def configure_proxy_charm(self, *args):
+        """Configure a container for use via ssh."""
+        (model, application, _, _) = args
+
+        try:
+            if self.get_phase(application) == "deploy":
+                self.set_phase(application, "configure")
+
+                debug("Start CreateContainer for {}".format(application))
+                self.state[application]['container'] = \
+                    await self.CreateContainer(*args)
+                debug("Done CreateContainer for {}".format(application))
+
+                if self.state[application]['container']:
+                    debug("Configure {} for container".format(application))
+                    if await self.configure_ssh_proxy(application):
+                        await asyncio.sleep(0.1)
+                        return True
+                else:
+                    debug("Failed to configure container for {}".format(application))
+            else:
+                debug("skipping CreateContainer for {}: {}".format(
+                    application,
+                    self.get_phase(application),
+                ))
+
+        except Exception as ex:
+            debug("configure_proxy_charm exception: {}".format(ex))
+        finally:
+            await asyncio.sleep(0.1)
+
+        return False
+
+    @classmethod
+    async def execute_charm_tests(self, *args):
+        (model, application, _, _) = args
+
+        debug("Executing charm test(s) for {}".format(application))
+
+        if self.state[application]['done']:
+            debug("Trying to execute tests against finished charm...aborting")
+            return False
+
+        try:
+            phase = self.get_phase(application)
+            # We enter the test phase when after deploy (for native charms) or
+            # configure, for proxy charms.
+            if phase in ["deploy", "configure"]:
+                self.set_phase(application, "test")
+                if self.are_tests_finished():
+                    raise Exception("Trying to execute init-config on finished test")
+
+                if await self.execute_initial_config_primitives(application):
+                    # check for metrics
+                    await self.check_metrics(application)
+
+                    debug("Done testing {}".format(application))
+                    self.state[application]['done'] = True
+
+        except Exception as ex:
+            debug("Exception in execute_charm_tests: {}".format(ex))
+        finally:
+            await asyncio.sleep(0.1)
+
+        return True
+
+    @classmethod
+    async def CreateContainer(self, *args):
+        """Create a LXD container for use with a proxy charm.abs
+
+        1. Get the public key from the charm via `get-ssh-public-key` action
+        2. Create container with said key injected for the ubuntu user
+
+        Returns a Container object
+        """
+        # Create and configure a LXD container for use with a proxy charm.
+        (model, application, _, _) = args
+
+        debug("[CreateContainer] {}".format(args))
+        container = None
+
+        try:
+            # Execute 'get-ssh-public-key' primitive and get returned value
+            uuid = await self.n2vc.ExecutePrimitive(
+                model,
+                application,
+                "get-ssh-public-key",
+                None,
+            )
+
+            result = await self.n2vc.GetPrimitiveOutput(model, uuid)
+            pubkey = result['pubkey']
+
+            container = create_lxd_container(
+                public_key=pubkey,
+                name=os.path.basename(__file__)
+            )
+
+            return container
+        except Exception as ex:
+            debug("Error creating container: {}".format(ex))
+            pass
+
+        return None
+
+    @classmethod
+    async def stop(self):
+        """Stop the test.
+
+        - Remove charms
+        - Stop and delete containers
+        - Logout of N2VC
+
+        TODO: Clean up duplicate code between teardown_class() and stop()
+        """
+        debug("stop() called")
+
+        if self.n2vc and self._running and not self._stopping:
+            self._running = False
+            self._stopping = True
+
+            # Destroy the network service
+            try:
+                await self.n2vc.DestroyNetworkService(self.ns_name)
+            except Exception as e:
+                debug(
+                    "Error Destroying Network Service \"{}\": {}".format(
+                        self.ns_name,
+                        e,
+                    )
+                )
+
+            # Wait for the applications to be removed and delete the containers
+            for application in self.charms:
+                try:
+
+                    while True:
+                        # Wait for the application to be removed
+                        await asyncio.sleep(10)
+                        if not await self.n2vc.HasApplication(
+                            self.ns_name,
+                            application,
+                        ):
+                            break
+
+                    # Need to wait for the charm to finish, because native charms
+                    if self.state[application]['container']:
+                        debug("Deleting LXD container...")
+                        destroy_lxd_container(
+                            self.state[application]['container']
+                        )
+                        self.state[application]['container'] = None
+                        debug("Deleting LXD container...done.")
+                    else:
+                        debug("No container found for {}".format(application))
+                except Exception as e:
+                    debug("Error while deleting container: {}".format(e))
+
+            # Logout of N2VC
+            try:
+                debug("stop(): Logging out of N2VC...")
+                await self.n2vc.logout()
+                self.n2vc = None
+                debug("stop(): Logging out of N2VC...Done.")
+            except Exception as ex:
+                debug(ex)
+
+            # Let the test know we're finished.
+            debug("Marking test as finished.")
+            # self._running = False
+        else:
+            debug("Skipping stop()")
+
+    @classmethod
+    def get_container_ip(self, container):
+        """Return the IPv4 address of container's eth0 interface."""
+        ipaddr = None
+        if container:
+            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']
+
+        return ipaddr
+
+    @classmethod
+    async def configure_ssh_proxy(self, application, task=None):
+        """Configure the proxy charm to use the lxd container.
+
+        Configure the charm to use a LXD container as it's VNF.
+        """
+        debug("Configuring ssh proxy for {}".format(application))
+
+        mgmtaddr = self.get_container_ip(
+            self.state[application]['container'],
+        )
+
+        debug(
+            "Setting ssh-hostname for {} to {}".format(
+                application,
+                mgmtaddr,
+            )
+        )
+
+        await self.n2vc.ExecutePrimitive(
+            self.ns_name,
+            application,
+            "config",
+            None,
+            params={
+                'ssh-hostname': mgmtaddr,
+                'ssh-username': 'ubuntu',
+            }
+        )
+
+        return True
+
+    @classmethod
+    async def execute_initial_config_primitives(self, application, task=None):
+        debug("Executing initial_config_primitives for {}".format(application))
+        try:
+            init_config = self.charms[application]
+
+            """
+            The initial-config-primitive is run during deploy but may fail
+             on some steps because proxy charm access isn't configured.
+
+            Re-run those actions so we can inspect the status.
+            """
+            uuids = await self.n2vc.ExecuteInitialPrimitives(
+                self.ns_name,
+                application,
+                init_config,
+            )
+
+            """
+            ExecutePrimitives will return a list of uuids. We need to check the
+             status of each. The test continues if all Actions succeed, and
+             fails if any of them fail.
+            """
+            await self.wait_for_uuids(application, uuids)
+            debug("Primitives for {} finished.".format(application))
+
+            return True
+        except Exception as ex:
+            debug("execute_initial_config_primitives exception: {}".format(ex))
+            raise ex
+            
+        return False
+
+    @classmethod
+    async def check_metrics(self, application, task=None):
+        """Check and run metrics, if present.
+
+        Checks to see if metrics are specified by the charm. If so, collects
+        the metrics.
+
+        If no metrics, then mark the test as finished.
+        """
+        if has_metrics(self.charms[application]['name']):
+            debug("Collecting metrics for {}".format(application))
+
+            metrics = await self.n2vc.GetMetrics(
+                self.ns_name,
+                application,
+            )
+
+            return await self.verify_metrics(application, metrics)
+
+    @classmethod
+    async def verify_metrics(self, application, metrics):
+        """Verify the charm's metrics.
+
+        Verify that the charm has sent metrics successfully.
+
+        Stops the test when finished.
+        """
+        debug("Verifying metrics for {}: {}".format(application, metrics))
+
+        if len(metrics):
+            return True
+
+        else:
+            # TODO: Ran into a case where it took 9 attempts before metrics
+            # were available; the controller is slow sometimes.
+            await asyncio.sleep(30)
+            return await self.check_metrics(application)
+
+    @classmethod
+    async def wait_for_uuids(self, application, uuids):
+        """Wait for primitives to execute.
+
+        The task will provide a list of uuids representing primitives that are
+        queued to run.
+        """
+        debug("Waiting for uuids for {}: {}".format(application, uuids))
+        waitfor = len(uuids)
+        finished = 0
+
+        while waitfor > finished:
+            for uid in uuids:
+                await asyncio.sleep(10)
+
+                if uuid not in self.state[application]['actions']:
+                    self.state[application]['actions'][uid] = "pending"
+
+                status = self.state[application]['actions'][uid]
+
+                # Have we already marked this as done?
+                if status in ["pending", "running"]:
+
+                    debug("Getting status of {} ({})...".format(uid, status))
+                    status = await self.n2vc.GetPrimitiveStatus(
+                        self.ns_name,
+                        uid,
+                    )
+                    debug("...state of {} is {}".format(uid, status))
+                    self.state[application]['actions'][uid] = status
+
+                    if status in ['completed', 'failed']:
+                        finished += 1
+
+            debug("{}/{} actions complete".format(finished, waitfor))
+
+            # Wait for the primitive to finish and try again
+            if waitfor > finished:
+                debug("Waiting 10s for action to finish...")
+                await asyncio.sleep(10)
+
+    @classmethod
+    def n2vc_callback(self, *args, **kwargs):
+        (model, application, status, message) = args
+        # debug("callback: {}".format(args))
+
+        if application not in self.state:
+            # Initialize the state of the application
+            self.state[application] = {
+                'status': None,     # Juju status
+                'container': None,  # lxd container, for proxy charms
+                'actions': {},      # Actions we've executed
+                'done': False,      # Are we done testing this charm?
+                'phase': "deploy",  # What phase is this application in?
+            }
+
+        self.state[application]['status'] = status
+
+        if status in ['waiting', 'maintenance', 'unknown']:
+            # Nothing to do for these
+            return
+
+        debug("callback: {}".format(args))
+
+        if self.state[application]['done']:
+            debug("{} is done".format(application))
+            return
+
+        if status in ['error']:
+            # To test broken charms, if a charm enters an error state we should
+            # end the test
+            debug("{} is in an error state, stop the test.".format(application))
+            # asyncio.ensure_future(self.stop())
+            self.state[application]['done'] = True
+            assert False
+
+        if status in ["blocked"] and self.isproxy(application):
+            if self.state[application]['phase'] == "deploy":
+                debug("Configuring proxy charm for {}".format(application))
+                asyncio.ensure_future(self.configure_proxy_charm(*args))
+
+        elif status in ["active"]:
+            """When a charm is active, we can assume that it has been properly
+            configured (not blocked), regardless of if it's a proxy or not.
+
+            All primitives should be complete by init_config_primitive
+            """
+            asyncio.ensure_future(self.execute_charm_tests(*args))
diff --git a/tests/bundles/k8s-zookeeper-downgrade.yaml b/tests/bundles/k8s-zookeeper-downgrade.yaml
new file mode 100644 (file)
index 0000000..8a99e2c
--- /dev/null
@@ -0,0 +1,21 @@
+# 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.
+
+description: Test bundle
+bundle: kubernetes
+applications:
+  zookeeper-k8s:
+    charm: 'cs:~charmed-osm/zookeeper-k8s-29'
+    scale: 1
+    series: kubernetes
diff --git a/tests/bundles/k8s-zookeeper-upgrade.yaml b/tests/bundles/k8s-zookeeper-upgrade.yaml
new file mode 100644 (file)
index 0000000..8308f2f
--- /dev/null
@@ -0,0 +1,21 @@
+# 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.
+
+description: Test bundle
+bundle: kubernetes
+applications:
+  zookeeper-k8s:
+    charm: 'cs:~charmed-osm/zookeeper-k8s-31'
+    scale: 1
+    series: kubernetes
diff --git a/tests/bundles/k8s-zookeeper.yaml b/tests/bundles/k8s-zookeeper.yaml
new file mode 100644 (file)
index 0000000..689220e
--- /dev/null
@@ -0,0 +1,21 @@
+# 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.
+
+description: Test bundle
+bundle: kubernetes
+applications:
+  zookeeper-k8s:
+    charm: 'cs:~charmed-osm/zookeeper-k8s-30'
+    scale: 1
+    series: kubernetes
diff --git a/tests/charms/layers/broken/README.md b/tests/charms/layers/broken/README.md
new file mode 100644 (file)
index 0000000..9234e57
--- /dev/null
@@ -0,0 +1,3 @@
+# Overview
+
+This charm is intended to install and break, requiring it to be removed.
diff --git a/tests/charms/layers/broken/actions.yaml b/tests/charms/layers/broken/actions.yaml
new file mode 100644 (file)
index 0000000..d224b5d
--- /dev/null
@@ -0,0 +1,23 @@
+# 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.
+
+touch:
+    description: "Touch a file on the VNF."
+    params:
+        filename:
+            description: "The name of the file to touch."
+            type: string
+            default: ""
+    required:
+        - filename
diff --git a/tests/charms/layers/broken/actions/touch b/tests/charms/layers/broken/actions/touch
new file mode 100755 (executable)
index 0000000..7e30af4
--- /dev/null
@@ -0,0 +1,33 @@
+#!/usr/bin/env python3
+##
+# Copyright 2016 Canonical Ltd.
+# All rights reserved.
+#
+# 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 sys
+sys.path.append('lib')
+
+from charms.reactive import main, set_flag
+from charmhelpers.core.hookenv import action_fail, action_name
+
+"""
+`set_state` only works here because it's flushed to disk inside the `main()`
+loop. remove_state will need to be called inside the action method.
+"""
+set_flag('actions.{}'.format(action_name()))
+
+try:
+    main()
+except Exception as e:
+    action_fail(repr(e))
diff --git a/tests/charms/layers/broken/config.yaml b/tests/charms/layers/broken/config.yaml
new file mode 100644 (file)
index 0000000..8bde313
--- /dev/null
@@ -0,0 +1,28 @@
+# 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.
+
+options:
+  string-option:
+    type: string
+    default: "Default Value"
+    description: "A short description of the configuration option"
+  boolean-option:
+    type: boolean
+    default: False
+    description: "A short description of the configuration option"
+  int-option:
+    type: int
+    default: 9001
+    description: "A short description of the configuration option"
+
diff --git a/tests/charms/layers/broken/icon.svg b/tests/charms/layers/broken/icon.svg
new file mode 100644 (file)
index 0000000..e092eef
--- /dev/null
@@ -0,0 +1,279 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>\r
+<!-- Created with Inkscape (http://www.inkscape.org/) -->\r
+\r
+<svg\r
+   xmlns:dc="http://purl.org/dc/elements/1.1/"\r
+   xmlns:cc="http://creativecommons.org/ns#"\r
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"\r
+   xmlns:svg="http://www.w3.org/2000/svg"\r
+   xmlns="http://www.w3.org/2000/svg"\r
+   xmlns:xlink="http://www.w3.org/1999/xlink"\r
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"\r
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"\r
+   width="96"\r
+   height="96"\r
+   id="svg6517"\r
+   version="1.1"\r
+   inkscape:version="0.48+devel r12274"\r
+   sodipodi:docname="Juju_charm_icon_template.svg">\r
+  <defs\r
+     id="defs6519">\r
+    <linearGradient\r
+       inkscape:collect="always"\r
+       xlink:href="#Background"\r
+       id="linearGradient6461"\r
+       gradientUnits="userSpaceOnUse"\r
+       x1="0"\r
+       y1="970.29498"\r
+       x2="144"\r
+       y2="970.29498"\r
+       gradientTransform="matrix(0,-0.66666669,0.6660448,0,-866.25992,731.29077)" />\r
+    <linearGradient\r
+       id="Background">\r
+      <stop\r
+         id="stop4178"\r
+         offset="0"\r
+         style="stop-color:#b8b8b8;stop-opacity:1" />\r
+      <stop\r
+         id="stop4180"\r
+         offset="1"\r
+         style="stop-color:#c9c9c9;stop-opacity:1" />\r
+    </linearGradient>\r
+    <filter\r
+       style="color-interpolation-filters:sRGB;"\r
+       inkscape:label="Inner Shadow"\r
+       id="filter1121">\r
+      <feFlood\r
+         flood-opacity="0.59999999999999998"\r
+         flood-color="rgb(0,0,0)"\r
+         result="flood"\r
+         id="feFlood1123" />\r
+      <feComposite\r
+         in="flood"\r
+         in2="SourceGraphic"\r
+         operator="out"\r
+         result="composite1"\r
+         id="feComposite1125" />\r
+      <feGaussianBlur\r
+         in="composite1"\r
+         stdDeviation="1"\r
+         result="blur"\r
+         id="feGaussianBlur1127" />\r
+      <feOffset\r
+         dx="0"\r
+         dy="2"\r
+         result="offset"\r
+         id="feOffset1129" />\r
+      <feComposite\r
+         in="offset"\r
+         in2="SourceGraphic"\r
+         operator="atop"\r
+         result="composite2"\r
+         id="feComposite1131" />\r
+    </filter>\r
+    <filter\r
+       style="color-interpolation-filters:sRGB;"\r
+       inkscape:label="Drop Shadow"\r
+       id="filter950">\r
+      <feFlood\r
+         flood-opacity="0.25"\r
+         flood-color="rgb(0,0,0)"\r
+         result="flood"\r
+         id="feFlood952" />\r
+      <feComposite\r
+         in="flood"\r
+         in2="SourceGraphic"\r
+         operator="in"\r
+         result="composite1"\r
+         id="feComposite954" />\r
+      <feGaussianBlur\r
+         in="composite1"\r
+         stdDeviation="1"\r
+         result="blur"\r
+         id="feGaussianBlur956" />\r
+      <feOffset\r
+         dx="0"\r
+         dy="1"\r
+         result="offset"\r
+         id="feOffset958" />\r
+      <feComposite\r
+         in="SourceGraphic"\r
+         in2="offset"\r
+         operator="over"\r
+         result="composite2"\r
+         id="feComposite960" />\r
+    </filter>\r
+    <clipPath\r
+       clipPathUnits="userSpaceOnUse"\r
+       id="clipPath873">\r
+      <g\r
+         transform="matrix(0,-0.66666667,0.66604479,0,-258.25992,677.00001)"\r
+         id="g875"\r
+         inkscape:label="Layer 1"\r
+         style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline">\r
+        <path\r
+           style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline"\r
+           d="m 46.702703,898.22775 50.594594,0 C 138.16216,898.22775 144,904.06497 144,944.92583 l 0,50.73846 c 0,40.86071 -5.83784,46.69791 -46.702703,46.69791 l -50.594594,0 C 5.8378378,1042.3622 0,1036.525 0,995.66429 L 0,944.92583 C 0,904.06497 5.8378378,898.22775 46.702703,898.22775 Z"\r
+           id="path877"\r
+           inkscape:connector-curvature="0"\r
+           sodipodi:nodetypes="sssssssss" />\r
+      </g>\r
+    </clipPath>\r
+    <filter\r
+       inkscape:collect="always"\r
+       id="filter891"\r
+       inkscape:label="Badge Shadow">\r
+      <feGaussianBlur\r
+         inkscape:collect="always"\r
+         stdDeviation="0.71999962"\r
+         id="feGaussianBlur893" />\r
+    </filter>\r
+  </defs>\r
+  <sodipodi:namedview\r
+     id="base"\r
+     pagecolor="#ffffff"\r
+     bordercolor="#666666"\r
+     borderopacity="1.0"\r
+     inkscape:pageopacity="0.0"\r
+     inkscape:pageshadow="2"\r
+     inkscape:zoom="4.0745362"\r
+     inkscape:cx="18.514671"\r
+     inkscape:cy="49.018169"\r
+     inkscape:document-units="px"\r
+     inkscape:current-layer="layer1"\r
+     showgrid="true"\r
+     fit-margin-top="0"\r
+     fit-margin-left="0"\r
+     fit-margin-right="0"\r
+     fit-margin-bottom="0"\r
+     inkscape:window-width="1920"\r
+     inkscape:window-height="1029"\r
+     inkscape:window-x="0"\r
+     inkscape:window-y="24"\r
+     inkscape:window-maximized="1"\r
+     showborder="true"\r
+     showguides="true"\r
+     inkscape:guide-bbox="true"\r
+     inkscape:showpageshadow="false">\r
+    <inkscape:grid\r
+       type="xygrid"\r
+       id="grid821" />\r
+    <sodipodi:guide\r
+       orientation="1,0"\r
+       position="16,48"\r
+       id="guide823" />\r
+    <sodipodi:guide\r
+       orientation="0,1"\r
+       position="64,80"\r
+       id="guide825" />\r
+    <sodipodi:guide\r
+       orientation="1,0"\r
+       position="80,40"\r
+       id="guide827" />\r
+    <sodipodi:guide\r
+       orientation="0,1"\r
+       position="64,16"\r
+       id="guide829" />\r
+  </sodipodi:namedview>\r
+  <metadata\r
+     id="metadata6522">\r
+    <rdf:RDF>\r
+      <cc:Work\r
+         rdf:about="">\r
+        <dc:format>image/svg+xml</dc:format>\r
+        <dc:type\r
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />\r
+        <dc:title></dc:title>\r
+      </cc:Work>\r
+    </rdf:RDF>\r
+  </metadata>\r
+  <g\r
+     inkscape:label="BACKGROUND"\r
+     inkscape:groupmode="layer"\r
+     id="layer1"\r
+     transform="translate(268,-635.29076)"\r
+     style="display:inline">\r
+    <path\r
+       style="fill:url(#linearGradient6461);fill-opacity:1;stroke:none;display:inline;filter:url(#filter1121)"\r
+       d="m -268,700.15563 0,-33.72973 c 0,-27.24324 3.88785,-31.13513 31.10302,-31.13513 l 33.79408,0 c 27.21507,0 31.1029,3.89189 31.1029,31.13513 l 0,33.72973 c 0,27.24325 -3.88783,31.13514 -31.1029,31.13514 l -33.79408,0 C -264.11215,731.29077 -268,727.39888 -268,700.15563 Z"\r
+       id="path6455"\r
+       inkscape:connector-curvature="0"\r
+       sodipodi:nodetypes="sssssssss" />\r
+  </g>\r
+  <g\r
+     inkscape:groupmode="layer"\r
+     id="layer3"\r
+     inkscape:label="PLACE YOUR PICTOGRAM HERE"\r
+     style="display:inline" />\r
+  <g\r
+     inkscape:groupmode="layer"\r
+     id="layer2"\r
+     inkscape:label="BADGE"\r
+     style="display:none"\r
+     sodipodi:insensitive="true">\r
+    <g\r
+       style="display:inline"\r
+       transform="translate(-340.00001,-581)"\r
+       id="g4394"\r
+       clip-path="none">\r
+      <g\r
+         id="g855">\r
+        <g\r
+           inkscape:groupmode="maskhelper"\r
+           id="g870"\r
+           clip-path="url(#clipPath873)"\r
+           style="opacity:0.6;filter:url(#filter891)">\r
+          <path\r
+             transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-237.54282)"\r
+             d="m 264,552.36218 a 12,12 0 1 1 -24,0 A 12,12 0 1 1 264,552.36218 Z"\r
+             sodipodi:ry="12"\r
+             sodipodi:rx="12"\r
+             sodipodi:cy="552.36218"\r
+             sodipodi:cx="252"\r
+             id="path844"\r
+             style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"\r
+             sodipodi:type="arc" />\r
+        </g>\r
+        <g\r
+           id="g862">\r
+          <path\r
+             sodipodi:type="arc"\r
+             style="color:#000000;fill:#f5f5f5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"\r
+             id="path4398"\r
+             sodipodi:cx="252"\r
+             sodipodi:cy="552.36218"\r
+             sodipodi:rx="12"\r
+             sodipodi:ry="12"\r
+             d="m 264,552.36218 a 12,12 0 1 1 -24,0 A 12,12 0 1 1 264,552.36218 Z"\r
+             transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-238.54282)" />\r
+          <path\r
+             transform="matrix(1.25,0,0,1.25,33,-100.45273)"\r
+             d="m 264,552.36218 a 12,12 0 1 1 -24,0 A 12,12 0 1 1 264,552.36218 Z"\r
+             sodipodi:ry="12"\r
+             sodipodi:rx="12"\r
+             sodipodi:cy="552.36218"\r
+             sodipodi:cx="252"\r
+             id="path4400"\r
+             style="color:#000000;fill:#dd4814;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"\r
+             sodipodi:type="arc" />\r
+          <path\r
+             sodipodi:type="star"\r
+             style="color:#000000;fill:#f5f5f5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"\r
+             id="path4459"\r
+             sodipodi:sides="5"\r
+             sodipodi:cx="666.19574"\r
+             sodipodi:cy="589.50385"\r
+             sodipodi:r1="7.2431178"\r
+             sodipodi:r2="4.3458705"\r
+             sodipodi:arg1="1.0471976"\r
+             sodipodi:arg2="1.6755161"\r
+             inkscape:flatsided="false"\r
+             inkscape:rounded="0.1"\r
+             inkscape:randomized="0"\r
+             d="m 669.8173,595.77657 c -0.39132,0.22593 -3.62645,-1.90343 -4.07583,-1.95066 -0.44938,-0.0472 -4.05653,1.36297 -4.39232,1.06062 -0.3358,-0.30235 0.68963,-4.03715 0.59569,-4.47913 -0.0939,-0.44198 -2.5498,-3.43681 -2.36602,-3.8496 0.18379,-0.41279 4.05267,-0.59166 4.44398,-0.81759 0.39132,-0.22593 2.48067,-3.48704 2.93005,-3.4398 0.44938,0.0472 1.81505,3.67147 2.15084,3.97382 0.3358,0.30236 4.08294,1.2817 4.17689,1.72369 0.0939,0.44198 -2.9309,2.86076 -3.11469,3.27355 C 669.9821,591.68426 670.20862,595.55064 669.8173,595.77657 Z"\r
+             transform="matrix(1.511423,-0.16366377,0.16366377,1.511423,-755.37346,-191.93651)" />\r
+        </g>\r
+      </g>\r
+    </g>\r
+  </g>\r
+</svg>\r
diff --git a/tests/charms/layers/broken/layer.yaml b/tests/charms/layers/broken/layer.yaml
new file mode 100644 (file)
index 0000000..2ee67bf
--- /dev/null
@@ -0,0 +1,18 @@
+# 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.
+
+includes: ['layer:basic', 'layer:vnfproxy']
+options:
+        basic:
+                use_venv: false
diff --git a/tests/charms/layers/broken/metadata.yaml b/tests/charms/layers/broken/metadata.yaml
new file mode 100644 (file)
index 0000000..ed41942
--- /dev/null
@@ -0,0 +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.
+
+name: broken
+summary: A (broken) simple VNF proxy charm
+maintainer: Adam Israel <adam.israel@canonical.com>
+subordinate: false
+series: ['xenial']
diff --git a/tests/charms/layers/broken/metrics.yaml b/tests/charms/layers/broken/metrics.yaml
new file mode 100644 (file)
index 0000000..e610b99
--- /dev/null
@@ -0,0 +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.
+
+metrics:
+  uptime:
+    type: gauge
+    description: "Uptime of the VNF"
+    command: awk '{print $1}' /proc/uptime
diff --git a/tests/charms/layers/broken/reactive/simple.py b/tests/charms/layers/broken/reactive/simple.py
new file mode 100644 (file)
index 0000000..3a018fb
--- /dev/null
@@ -0,0 +1,59 @@
+# 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.
+
+from charmhelpers.core.hookenv import (
+    action_get,
+    action_fail,
+    action_set,
+    status_set,
+)
+from charms.reactive import (
+    clear_flag,
+    set_flag,
+    when,
+    when_not,
+)
+import charms.sshproxy
+
+
+@when('sshproxy.configured')
+@when_not('simple.installed')
+def install_simple_proxy_charm():
+    """Post-install actions.
+
+    This function will run when two conditions are met:
+    1. The 'sshproxy.configured' state is set
+    2. The 'simple.installed' state is not set
+
+    This ensures that the workload status is set to active only when the SSH
+    proxy is properly configured.
+    """
+    set_flag('simple.installed')
+    status_set('active', 'Ready!')
+
+
+@when('actions.touch')
+def touch():
+    raise Exception("I am broken.")
+    err = ''
+    try:
+        filename = action_get('filename')
+        cmd = ['touch {}'.format(filename)]
+        result, err = charms.sshproxy._run(cmd)
+    except Exception:
+        action_fail('command failed:' + err)
+    else:
+        action_set({'output': result})
+    finally:
+        clear_flag('actions.touch')
diff --git a/tests/charms/layers/metrics-ci/README.md b/tests/charms/layers/metrics-ci/README.md
new file mode 100755 (executable)
index 0000000..a765b72
--- /dev/null
@@ -0,0 +1,3 @@
+# Overview
+
+Metrics collection via machine charm
\ No newline at end of file
diff --git a/tests/charms/layers/metrics-ci/config.yaml b/tests/charms/layers/metrics-ci/config.yaml
new file mode 100755 (executable)
index 0000000..8bde313
--- /dev/null
@@ -0,0 +1,28 @@
+# 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.
+
+options:
+  string-option:
+    type: string
+    default: "Default Value"
+    description: "A short description of the configuration option"
+  boolean-option:
+    type: boolean
+    default: False
+    description: "A short description of the configuration option"
+  int-option:
+    type: int
+    default: 9001
+    description: "A short description of the configuration option"
+
diff --git a/tests/charms/layers/metrics-ci/layer.yaml b/tests/charms/layers/metrics-ci/layer.yaml
new file mode 100755 (executable)
index 0000000..0726ddf
--- /dev/null
@@ -0,0 +1,15 @@
+# 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.
+
+includes: ['layer:basic', 'layer:metrics']  # if you use any interfaces, add them here
diff --git a/tests/charms/layers/metrics-ci/metadata.yaml b/tests/charms/layers/metrics-ci/metadata.yaml
new file mode 100755 (executable)
index 0000000..8c7e081
--- /dev/null
@@ -0,0 +1,26 @@
+# 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.
+
+name: metrics-ci
+summary: <Fill in summary here>
+maintainer: Adam Israel <Adam.Israel@ronin>
+description: |
+  <Multi-line description here>
+tags:
+  # Replace "misc" with one or more whitelisted tags from this list:
+  # https://jujucharms.com/docs/stable/authors-charm-metadata
+  - misc
+subordinate: false
+series:
+  - xenial
diff --git a/tests/charms/layers/metrics-ci/metrics.yaml b/tests/charms/layers/metrics-ci/metrics.yaml
new file mode 100755 (executable)
index 0000000..2326f6c
--- /dev/null
@@ -0,0 +1,23 @@
+# 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.
+
+metrics:
+  users:
+    type: gauge
+    description: "# of users"
+    command: who|wc -l
+  load:
+    type: gauge
+    description: "5 minute load average"
+    command: cat /proc/loadavg |awk '{print $1}'
diff --git a/tests/charms/layers/metrics-ci/reactive/metrics_ci.py b/tests/charms/layers/metrics-ci/reactive/metrics_ci.py
new file mode 100755 (executable)
index 0000000..4c4756a
--- /dev/null
@@ -0,0 +1,27 @@
+# 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.
+
+from charmhelpers.core.hookenv import (
+    status_set,
+)
+from charms.reactive import (
+    set_flag,
+    when_not,
+)
+
+
+@when_not('metrics-ci.installed')
+def install_metrics_ci():
+    status_set('active', "Ready!")
+    set_flag('metrics-ci.installed')
diff --git a/tests/charms/layers/metrics-proxy-ci/README.md b/tests/charms/layers/metrics-proxy-ci/README.md
new file mode 100644 (file)
index 0000000..e96f02f
--- /dev/null
@@ -0,0 +1,3 @@
+# Overview
+
+Test charm for metrics collection via proxy.
\ No newline at end of file
diff --git a/tests/charms/layers/metrics-proxy-ci/config.yaml b/tests/charms/layers/metrics-proxy-ci/config.yaml
new file mode 100644 (file)
index 0000000..8bde313
--- /dev/null
@@ -0,0 +1,28 @@
+# 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.
+
+options:
+  string-option:
+    type: string
+    default: "Default Value"
+    description: "A short description of the configuration option"
+  boolean-option:
+    type: boolean
+    default: False
+    description: "A short description of the configuration option"
+  int-option:
+    type: int
+    default: 9001
+    description: "A short description of the configuration option"
+
diff --git a/tests/charms/layers/metrics-proxy-ci/layer.yaml b/tests/charms/layers/metrics-proxy-ci/layer.yaml
new file mode 100644 (file)
index 0000000..186f1db
--- /dev/null
@@ -0,0 +1,18 @@
+# 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.
+
+includes:
+    - 'layer:basic'
+    - 'layer:vnfproxy'
+    - 'layer:sshproxy'
diff --git a/tests/charms/layers/metrics-proxy-ci/metadata.yaml b/tests/charms/layers/metrics-proxy-ci/metadata.yaml
new file mode 100644 (file)
index 0000000..78bf753
--- /dev/null
@@ -0,0 +1,26 @@
+# 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.
+
+name: metrics-proxy-ci
+summary: <Fill in summary here>
+maintainer: Adam Israel <Adam.Israel@ronin>
+description: |
+  <Multi-line description here>
+tags:
+  # Replace "misc" with one or more whitelisted tags from this list:
+  # https://jujucharms.com/docs/stable/authors-charm-metadata
+  - misc
+subordinate: false
+series:
+  - xenial
diff --git a/tests/charms/layers/metrics-proxy-ci/metrics.yaml b/tests/charms/layers/metrics-proxy-ci/metrics.yaml
new file mode 100644 (file)
index 0000000..2326f6c
--- /dev/null
@@ -0,0 +1,23 @@
+# 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.
+
+metrics:
+  users:
+    type: gauge
+    description: "# of users"
+    command: who|wc -l
+  load:
+    type: gauge
+    description: "5 minute load average"
+    command: cat /proc/loadavg |awk '{print $1}'
diff --git a/tests/charms/layers/metrics-proxy-ci/reactive/metrics_ci.py b/tests/charms/layers/metrics-proxy-ci/reactive/metrics_ci.py
new file mode 100644 (file)
index 0000000..ea5c544
--- /dev/null
@@ -0,0 +1,27 @@
+# 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.
+
+from charmhelpers.core.hookenv import (
+    status_set,
+)
+from charms.reactive import (
+    set_flag,
+    when_not,
+)
+
+
+@when_not('metrics-ci.installed')
+def install_metrics_ci():
+    status_set('blocked', "Waiting for SSH credentials.")
+    set_flag('metrics-ci.installed')
diff --git a/tests/charms/layers/native-ci/README.md b/tests/charms/layers/native-ci/README.md
new file mode 100644 (file)
index 0000000..d58b762
--- /dev/null
@@ -0,0 +1,3 @@
+# Overview
+
+A native charm.
diff --git a/tests/charms/layers/native-ci/actions.yaml b/tests/charms/layers/native-ci/actions.yaml
new file mode 100644 (file)
index 0000000..46c6cb7
--- /dev/null
@@ -0,0 +1,22 @@
+# 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.
+
+test:
+    description: "Verify that the action can run."
+testint:
+    description: "Test a primitive with a non-string parameter"
+    params:
+        intval:
+            type: integer
+            default: 0
diff --git a/tests/charms/layers/native-ci/actions/test b/tests/charms/layers/native-ci/actions/test
new file mode 100755 (executable)
index 0000000..7e30af4
--- /dev/null
@@ -0,0 +1,33 @@
+#!/usr/bin/env python3
+##
+# Copyright 2016 Canonical Ltd.
+# All rights reserved.
+#
+# 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 sys
+sys.path.append('lib')
+
+from charms.reactive import main, set_flag
+from charmhelpers.core.hookenv import action_fail, action_name
+
+"""
+`set_state` only works here because it's flushed to disk inside the `main()`
+loop. remove_state will need to be called inside the action method.
+"""
+set_flag('actions.{}'.format(action_name()))
+
+try:
+    main()
+except Exception as e:
+    action_fail(repr(e))
diff --git a/tests/charms/layers/native-ci/actions/testint b/tests/charms/layers/native-ci/actions/testint
new file mode 100755 (executable)
index 0000000..7e30af4
--- /dev/null
@@ -0,0 +1,33 @@
+#!/usr/bin/env python3
+##
+# Copyright 2016 Canonical Ltd.
+# All rights reserved.
+#
+# 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 sys
+sys.path.append('lib')
+
+from charms.reactive import main, set_flag
+from charmhelpers.core.hookenv import action_fail, action_name
+
+"""
+`set_state` only works here because it's flushed to disk inside the `main()`
+loop. remove_state will need to be called inside the action method.
+"""
+set_flag('actions.{}'.format(action_name()))
+
+try:
+    main()
+except Exception as e:
+    action_fail(repr(e))
diff --git a/tests/charms/layers/native-ci/layer.yaml b/tests/charms/layers/native-ci/layer.yaml
new file mode 100644 (file)
index 0000000..4dd82f9
--- /dev/null
@@ -0,0 +1,21 @@
+# 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.
+
+includes:
+    - 'layer:basic'
+    - 'interface:mysql'
+    
+options:
+    basic:
+        use_venv: false
diff --git a/tests/charms/layers/native-ci/metadata.yaml b/tests/charms/layers/native-ci/metadata.yaml
new file mode 100644 (file)
index 0000000..1c72d9e
--- /dev/null
@@ -0,0 +1,26 @@
+# 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.
+
+name: native-ci
+summary: A native VNF charm
+description: A native VNF charm
+maintainer: Adam Israel <adam.israel@canonical.com>
+subordinate: false
+series: ['xenial']
+# provides:
+#     db:
+#         interface: mysql
+# requires:
+#     app:
+#         interface: mysql
diff --git a/tests/charms/layers/native-ci/reactive/native-ci.py b/tests/charms/layers/native-ci/reactive/native-ci.py
new file mode 100644 (file)
index 0000000..72365c0
--- /dev/null
@@ -0,0 +1,76 @@
+# 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.
+
+from charmhelpers.core.hookenv import (
+    action_fail,
+    action_set,
+    action_get,
+    status_set,
+)
+from charms.reactive import (
+    clear_flag,
+    set_flag,
+    when,
+    when_not,
+)
+
+
+@when_not('native-ci.installed')
+def install_native_ci_charm():
+    set_flag('native-ci.installed')
+    status_set('active', 'Ready!')
+
+
+@when('actions.test', 'native-ci.installed')
+def test():
+    try:
+        result = True
+    except Exception as e:
+        action_fail('command failed: {}'.format(e))
+    else:
+        action_set({'output': result})
+    finally:
+        clear_flag('actions.test')
+
+
+@when('actions.testint', 'native-ci.installed')
+def testint():
+    try:
+        # Test the value is an int by performing a mathmatical operation on it.
+        intval = action_get('intval')
+        intval = intval + 1
+    except Exception as e:
+        action_fail('command failed: {}'.format(e))
+    else:
+        action_set({'output': intval})
+    finally:
+        clear_flag('actions.testint')
+
+
+# @when('db.joined')
+# def provides_db(db):
+#     """Simulate providing database credentials."""
+#     db.configure(
+#         database="mydb",
+#         user="myuser",
+#         password="mypassword",
+#         host="myhost",
+#         slave="myslave",
+#     )
+
+
+# @when('db.available')
+# def requires_db(db):
+#     """Simulate receiving database credentials."""
+#     pass
diff --git a/tests/charms/layers/proxy-ci/README.md b/tests/charms/layers/proxy-ci/README.md
new file mode 100644 (file)
index 0000000..c16d9d8
--- /dev/null
@@ -0,0 +1,3 @@
+# Overview
+
+A `charm layer` to test the functionality of proxy charms.
diff --git a/tests/charms/layers/proxy-ci/actions.yaml b/tests/charms/layers/proxy-ci/actions.yaml
new file mode 100644 (file)
index 0000000..304b520
--- /dev/null
@@ -0,0 +1,16 @@
+# 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.
+
+test:
+    description: "Verify that the action can run."
diff --git a/tests/charms/layers/proxy-ci/actions/test b/tests/charms/layers/proxy-ci/actions/test
new file mode 100755 (executable)
index 0000000..7e30af4
--- /dev/null
@@ -0,0 +1,33 @@
+#!/usr/bin/env python3
+##
+# Copyright 2016 Canonical Ltd.
+# All rights reserved.
+#
+# 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 sys
+sys.path.append('lib')
+
+from charms.reactive import main, set_flag
+from charmhelpers.core.hookenv import action_fail, action_name
+
+"""
+`set_state` only works here because it's flushed to disk inside the `main()`
+loop. remove_state will need to be called inside the action method.
+"""
+set_flag('actions.{}'.format(action_name()))
+
+try:
+    main()
+except Exception as e:
+    action_fail(repr(e))
diff --git a/tests/charms/layers/proxy-ci/layer.yaml b/tests/charms/layers/proxy-ci/layer.yaml
new file mode 100644 (file)
index 0000000..186f1db
--- /dev/null
@@ -0,0 +1,18 @@
+# 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.
+
+includes:
+    - 'layer:basic'
+    - 'layer:vnfproxy'
+    - 'layer:sshproxy'
diff --git a/tests/charms/layers/proxy-ci/metadata.yaml b/tests/charms/layers/proxy-ci/metadata.yaml
new file mode 100644 (file)
index 0000000..37120e3
--- /dev/null
@@ -0,0 +1,32 @@
+# 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.
+
+name: proxy-ci
+summary: <Fill in summary here>
+maintainer: Adam Israel <Adam.Israel@ronin>
+description: |
+  <Multi-line description here>
+tags:
+  # Replace "misc" with one or more whitelisted tags from this list:
+  # https://jujucharms.com/docs/stable/authors-charm-metadata
+  - misc
+subordinate: false
+series:
+  - xenial
+provides:
+    db:
+        interface: mysql
+requires:
+    app:
+        interface: mysql
diff --git a/tests/charms/layers/proxy-ci/reactive/proxy_ci.py b/tests/charms/layers/proxy-ci/reactive/proxy_ci.py
new file mode 100644 (file)
index 0000000..cf2ed99
--- /dev/null
@@ -0,0 +1,66 @@
+# 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.
+
+from charmhelpers.core.hookenv import (
+    action_fail,
+    action_set,
+    status_set,
+)
+from charms.reactive import (
+    set_flag,
+    clear_flag,
+    when_not,
+    when,
+)
+import charms.sshproxy
+
+
+@when_not('proxy-ci.installed')
+def install_proxy_ci():
+    status_set('blocked', "Waiting for SSH credentials.")
+    set_flag('proxy-ci.installed')
+
+
+@when('actions.test', 'proxy-ci.installed')
+def test():
+    err = ''
+    try:
+        cmd = ['hostname']
+        result, err = charms.sshproxy._run(cmd)
+        if len(result) == 0:
+            raise Exception("Proxy failed")
+    except Exception as e:
+        action_fail('command failed: {}'.format(e))
+    else:
+        action_set({'output': result})
+    finally:
+        clear_flag('actions.test')
+
+
+@when('db.joined')
+def provides_db(db):
+    """Simulate providing database credentials."""
+    db.configure(
+        database="mydb",
+        user="myuser",
+        password="mypassword",
+        host="myhost",
+        slave="myslave",
+    )
+
+
+@when('db.available')
+def requires_db(db):
+    """Simulate receiving database credentials."""
+    pass
diff --git a/tests/charms/layers/simple/README.md b/tests/charms/layers/simple/README.md
new file mode 100644 (file)
index 0000000..5504db4
--- /dev/null
@@ -0,0 +1,3 @@
+# Overview
+
+A simple charm
\ No newline at end of file
diff --git a/tests/charms/layers/simple/actions.yaml b/tests/charms/layers/simple/actions.yaml
new file mode 100644 (file)
index 0000000..d224b5d
--- /dev/null
@@ -0,0 +1,23 @@
+# 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.
+
+touch:
+    description: "Touch a file on the VNF."
+    params:
+        filename:
+            description: "The name of the file to touch."
+            type: string
+            default: ""
+    required:
+        - filename
diff --git a/tests/charms/layers/simple/actions/touch b/tests/charms/layers/simple/actions/touch
new file mode 100755 (executable)
index 0000000..7e30af4
--- /dev/null
@@ -0,0 +1,33 @@
+#!/usr/bin/env python3
+##
+# Copyright 2016 Canonical Ltd.
+# All rights reserved.
+#
+# 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 sys
+sys.path.append('lib')
+
+from charms.reactive import main, set_flag
+from charmhelpers.core.hookenv import action_fail, action_name
+
+"""
+`set_state` only works here because it's flushed to disk inside the `main()`
+loop. remove_state will need to be called inside the action method.
+"""
+set_flag('actions.{}'.format(action_name()))
+
+try:
+    main()
+except Exception as e:
+    action_fail(repr(e))
diff --git a/tests/charms/layers/simple/config.yaml b/tests/charms/layers/simple/config.yaml
new file mode 100644 (file)
index 0000000..8bde313
--- /dev/null
@@ -0,0 +1,28 @@
+# 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.
+
+options:
+  string-option:
+    type: string
+    default: "Default Value"
+    description: "A short description of the configuration option"
+  boolean-option:
+    type: boolean
+    default: False
+    description: "A short description of the configuration option"
+  int-option:
+    type: int
+    default: 9001
+    description: "A short description of the configuration option"
+
diff --git a/tests/charms/layers/simple/icon.svg b/tests/charms/layers/simple/icon.svg
new file mode 100644 (file)
index 0000000..e092eef
--- /dev/null
@@ -0,0 +1,279 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>\r
+<!-- Created with Inkscape (http://www.inkscape.org/) -->\r
+\r
+<svg\r
+   xmlns:dc="http://purl.org/dc/elements/1.1/"\r
+   xmlns:cc="http://creativecommons.org/ns#"\r
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"\r
+   xmlns:svg="http://www.w3.org/2000/svg"\r
+   xmlns="http://www.w3.org/2000/svg"\r
+   xmlns:xlink="http://www.w3.org/1999/xlink"\r
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"\r
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"\r
+   width="96"\r
+   height="96"\r
+   id="svg6517"\r
+   version="1.1"\r
+   inkscape:version="0.48+devel r12274"\r
+   sodipodi:docname="Juju_charm_icon_template.svg">\r
+  <defs\r
+     id="defs6519">\r
+    <linearGradient\r
+       inkscape:collect="always"\r
+       xlink:href="#Background"\r
+       id="linearGradient6461"\r
+       gradientUnits="userSpaceOnUse"\r
+       x1="0"\r
+       y1="970.29498"\r
+       x2="144"\r
+       y2="970.29498"\r
+       gradientTransform="matrix(0,-0.66666669,0.6660448,0,-866.25992,731.29077)" />\r
+    <linearGradient\r
+       id="Background">\r
+      <stop\r
+         id="stop4178"\r
+         offset="0"\r
+         style="stop-color:#b8b8b8;stop-opacity:1" />\r
+      <stop\r
+         id="stop4180"\r
+         offset="1"\r
+         style="stop-color:#c9c9c9;stop-opacity:1" />\r
+    </linearGradient>\r
+    <filter\r
+       style="color-interpolation-filters:sRGB;"\r
+       inkscape:label="Inner Shadow"\r
+       id="filter1121">\r
+      <feFlood\r
+         flood-opacity="0.59999999999999998"\r
+         flood-color="rgb(0,0,0)"\r
+         result="flood"\r
+         id="feFlood1123" />\r
+      <feComposite\r
+         in="flood"\r
+         in2="SourceGraphic"\r
+         operator="out"\r
+         result="composite1"\r
+         id="feComposite1125" />\r
+      <feGaussianBlur\r
+         in="composite1"\r
+         stdDeviation="1"\r
+         result="blur"\r
+         id="feGaussianBlur1127" />\r
+      <feOffset\r
+         dx="0"\r
+         dy="2"\r
+         result="offset"\r
+         id="feOffset1129" />\r
+      <feComposite\r
+         in="offset"\r
+         in2="SourceGraphic"\r
+         operator="atop"\r
+         result="composite2"\r
+         id="feComposite1131" />\r
+    </filter>\r
+    <filter\r
+       style="color-interpolation-filters:sRGB;"\r
+       inkscape:label="Drop Shadow"\r
+       id="filter950">\r
+      <feFlood\r
+         flood-opacity="0.25"\r
+         flood-color="rgb(0,0,0)"\r
+         result="flood"\r
+         id="feFlood952" />\r
+      <feComposite\r
+         in="flood"\r
+         in2="SourceGraphic"\r
+         operator="in"\r
+         result="composite1"\r
+         id="feComposite954" />\r
+      <feGaussianBlur\r
+         in="composite1"\r
+         stdDeviation="1"\r
+         result="blur"\r
+         id="feGaussianBlur956" />\r
+      <feOffset\r
+         dx="0"\r
+         dy="1"\r
+         result="offset"\r
+         id="feOffset958" />\r
+      <feComposite\r
+         in="SourceGraphic"\r
+         in2="offset"\r
+         operator="over"\r
+         result="composite2"\r
+         id="feComposite960" />\r
+    </filter>\r
+    <clipPath\r
+       clipPathUnits="userSpaceOnUse"\r
+       id="clipPath873">\r
+      <g\r
+         transform="matrix(0,-0.66666667,0.66604479,0,-258.25992,677.00001)"\r
+         id="g875"\r
+         inkscape:label="Layer 1"\r
+         style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline">\r
+        <path\r
+           style="fill:#ff00ff;fill-opacity:1;stroke:none;display:inline"\r
+           d="m 46.702703,898.22775 50.594594,0 C 138.16216,898.22775 144,904.06497 144,944.92583 l 0,50.73846 c 0,40.86071 -5.83784,46.69791 -46.702703,46.69791 l -50.594594,0 C 5.8378378,1042.3622 0,1036.525 0,995.66429 L 0,944.92583 C 0,904.06497 5.8378378,898.22775 46.702703,898.22775 Z"\r
+           id="path877"\r
+           inkscape:connector-curvature="0"\r
+           sodipodi:nodetypes="sssssssss" />\r
+      </g>\r
+    </clipPath>\r
+    <filter\r
+       inkscape:collect="always"\r
+       id="filter891"\r
+       inkscape:label="Badge Shadow">\r
+      <feGaussianBlur\r
+         inkscape:collect="always"\r
+         stdDeviation="0.71999962"\r
+         id="feGaussianBlur893" />\r
+    </filter>\r
+  </defs>\r
+  <sodipodi:namedview\r
+     id="base"\r
+     pagecolor="#ffffff"\r
+     bordercolor="#666666"\r
+     borderopacity="1.0"\r
+     inkscape:pageopacity="0.0"\r
+     inkscape:pageshadow="2"\r
+     inkscape:zoom="4.0745362"\r
+     inkscape:cx="18.514671"\r
+     inkscape:cy="49.018169"\r
+     inkscape:document-units="px"\r
+     inkscape:current-layer="layer1"\r
+     showgrid="true"\r
+     fit-margin-top="0"\r
+     fit-margin-left="0"\r
+     fit-margin-right="0"\r
+     fit-margin-bottom="0"\r
+     inkscape:window-width="1920"\r
+     inkscape:window-height="1029"\r
+     inkscape:window-x="0"\r
+     inkscape:window-y="24"\r
+     inkscape:window-maximized="1"\r
+     showborder="true"\r
+     showguides="true"\r
+     inkscape:guide-bbox="true"\r
+     inkscape:showpageshadow="false">\r
+    <inkscape:grid\r
+       type="xygrid"\r
+       id="grid821" />\r
+    <sodipodi:guide\r
+       orientation="1,0"\r
+       position="16,48"\r
+       id="guide823" />\r
+    <sodipodi:guide\r
+       orientation="0,1"\r
+       position="64,80"\r
+       id="guide825" />\r
+    <sodipodi:guide\r
+       orientation="1,0"\r
+       position="80,40"\r
+       id="guide827" />\r
+    <sodipodi:guide\r
+       orientation="0,1"\r
+       position="64,16"\r
+       id="guide829" />\r
+  </sodipodi:namedview>\r
+  <metadata\r
+     id="metadata6522">\r
+    <rdf:RDF>\r
+      <cc:Work\r
+         rdf:about="">\r
+        <dc:format>image/svg+xml</dc:format>\r
+        <dc:type\r
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />\r
+        <dc:title></dc:title>\r
+      </cc:Work>\r
+    </rdf:RDF>\r
+  </metadata>\r
+  <g\r
+     inkscape:label="BACKGROUND"\r
+     inkscape:groupmode="layer"\r
+     id="layer1"\r
+     transform="translate(268,-635.29076)"\r
+     style="display:inline">\r
+    <path\r
+       style="fill:url(#linearGradient6461);fill-opacity:1;stroke:none;display:inline;filter:url(#filter1121)"\r
+       d="m -268,700.15563 0,-33.72973 c 0,-27.24324 3.88785,-31.13513 31.10302,-31.13513 l 33.79408,0 c 27.21507,0 31.1029,3.89189 31.1029,31.13513 l 0,33.72973 c 0,27.24325 -3.88783,31.13514 -31.1029,31.13514 l -33.79408,0 C -264.11215,731.29077 -268,727.39888 -268,700.15563 Z"\r
+       id="path6455"\r
+       inkscape:connector-curvature="0"\r
+       sodipodi:nodetypes="sssssssss" />\r
+  </g>\r
+  <g\r
+     inkscape:groupmode="layer"\r
+     id="layer3"\r
+     inkscape:label="PLACE YOUR PICTOGRAM HERE"\r
+     style="display:inline" />\r
+  <g\r
+     inkscape:groupmode="layer"\r
+     id="layer2"\r
+     inkscape:label="BADGE"\r
+     style="display:none"\r
+     sodipodi:insensitive="true">\r
+    <g\r
+       style="display:inline"\r
+       transform="translate(-340.00001,-581)"\r
+       id="g4394"\r
+       clip-path="none">\r
+      <g\r
+         id="g855">\r
+        <g\r
+           inkscape:groupmode="maskhelper"\r
+           id="g870"\r
+           clip-path="url(#clipPath873)"\r
+           style="opacity:0.6;filter:url(#filter891)">\r
+          <path\r
+             transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-237.54282)"\r
+             d="m 264,552.36218 a 12,12 0 1 1 -24,0 A 12,12 0 1 1 264,552.36218 Z"\r
+             sodipodi:ry="12"\r
+             sodipodi:rx="12"\r
+             sodipodi:cy="552.36218"\r
+             sodipodi:cx="252"\r
+             id="path844"\r
+             style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"\r
+             sodipodi:type="arc" />\r
+        </g>\r
+        <g\r
+           id="g862">\r
+          <path\r
+             sodipodi:type="arc"\r
+             style="color:#000000;fill:#f5f5f5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"\r
+             id="path4398"\r
+             sodipodi:cx="252"\r
+             sodipodi:cy="552.36218"\r
+             sodipodi:rx="12"\r
+             sodipodi:ry="12"\r
+             d="m 264,552.36218 a 12,12 0 1 1 -24,0 A 12,12 0 1 1 264,552.36218 Z"\r
+             transform="matrix(1.4999992,0,0,1.4999992,-29.999795,-238.54282)" />\r
+          <path\r
+             transform="matrix(1.25,0,0,1.25,33,-100.45273)"\r
+             d="m 264,552.36218 a 12,12 0 1 1 -24,0 A 12,12 0 1 1 264,552.36218 Z"\r
+             sodipodi:ry="12"\r
+             sodipodi:rx="12"\r
+             sodipodi:cy="552.36218"\r
+             sodipodi:cx="252"\r
+             id="path4400"\r
+             style="color:#000000;fill:#dd4814;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"\r
+             sodipodi:type="arc" />\r
+          <path\r
+             sodipodi:type="star"\r
+             style="color:#000000;fill:#f5f5f5;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"\r
+             id="path4459"\r
+             sodipodi:sides="5"\r
+             sodipodi:cx="666.19574"\r
+             sodipodi:cy="589.50385"\r
+             sodipodi:r1="7.2431178"\r
+             sodipodi:r2="4.3458705"\r
+             sodipodi:arg1="1.0471976"\r
+             sodipodi:arg2="1.6755161"\r
+             inkscape:flatsided="false"\r
+             inkscape:rounded="0.1"\r
+             inkscape:randomized="0"\r
+             d="m 669.8173,595.77657 c -0.39132,0.22593 -3.62645,-1.90343 -4.07583,-1.95066 -0.44938,-0.0472 -4.05653,1.36297 -4.39232,1.06062 -0.3358,-0.30235 0.68963,-4.03715 0.59569,-4.47913 -0.0939,-0.44198 -2.5498,-3.43681 -2.36602,-3.8496 0.18379,-0.41279 4.05267,-0.59166 4.44398,-0.81759 0.39132,-0.22593 2.48067,-3.48704 2.93005,-3.4398 0.44938,0.0472 1.81505,3.67147 2.15084,3.97382 0.3358,0.30236 4.08294,1.2817 4.17689,1.72369 0.0939,0.44198 -2.9309,2.86076 -3.11469,3.27355 C 669.9821,591.68426 670.20862,595.55064 669.8173,595.77657 Z"\r
+             transform="matrix(1.511423,-0.16366377,0.16366377,1.511423,-755.37346,-191.93651)" />\r
+        </g>\r
+      </g>\r
+    </g>\r
+  </g>\r
+</svg>\r
diff --git a/tests/charms/layers/simple/layer.yaml b/tests/charms/layers/simple/layer.yaml
new file mode 100644 (file)
index 0000000..2ee67bf
--- /dev/null
@@ -0,0 +1,18 @@
+# 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.
+
+includes: ['layer:basic', 'layer:vnfproxy']
+options:
+        basic:
+                use_venv: false
diff --git a/tests/charms/layers/simple/metadata.yaml b/tests/charms/layers/simple/metadata.yaml
new file mode 100644 (file)
index 0000000..1cb3de9
--- /dev/null
@@ -0,0 +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.
+
+name: simple
+summary: A simple VNF proxy charm
+maintainer: Adam Israel <adam.israel@canonical.com>
+subordinate: false
+series: ['xenial']
diff --git a/tests/charms/layers/simple/metrics.yaml b/tests/charms/layers/simple/metrics.yaml
new file mode 100644 (file)
index 0000000..e610b99
--- /dev/null
@@ -0,0 +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.
+
+metrics:
+  uptime:
+    type: gauge
+    description: "Uptime of the VNF"
+    command: awk '{print $1}' /proc/uptime
diff --git a/tests/charms/layers/simple/reactive/simple.py b/tests/charms/layers/simple/reactive/simple.py
new file mode 100644 (file)
index 0000000..6e1300e
--- /dev/null
@@ -0,0 +1,68 @@
+# 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.
+
+from charmhelpers.core.hookenv import (
+    action_get,
+    action_fail,
+    action_set,
+    status_set,
+)
+from charms.reactive import (
+    clear_flag,
+    set_flag,
+    when,
+    when_not,
+)
+import charms.sshproxy
+import os
+
+
+@when('sshproxy.configured')
+@when_not('simple.installed')
+def install_simple_proxy_charm():
+    """Post-install actions.
+
+    This function will run when two conditions are met:
+    1. The 'sshproxy.configured' state is set
+    2. The 'simple.installed' state is not set
+
+    This ensures that the workload status is set to active only when the SSH
+    proxy is properly configured.
+    """
+    set_flag('simple.installed')
+    status_set('active', 'Ready!')
+
+
+@when('actions.touch')
+def touch():
+    if not in_action_context():
+        clear_flag('actions.touch')
+        return
+
+    err = ''
+    try:
+        filename = action_get('filename')
+        cmd = ['touch {}'.format(filename)]
+        result, err = charms.sshproxy._run(cmd)
+    except Exception:
+        action_fail('command failed:' + err)
+    else:
+        action_set({'output': result})
+    finally:
+        clear_flag('actions.touch')
+
+
+def in_action_context():
+    """Determine whether we're running on an action context."""
+    return 'JUJU_ACTION_UUID' in os.environ
diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py
new file mode 100644 (file)
index 0000000..e0b5e8c
--- /dev/null
@@ -0,0 +1,13 @@
+# 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
+#
diff --git a/tests/integration/test_broken_charm.py b/tests/integration/test_broken_charm.py
new file mode 100644 (file)
index 0000000..d77786a
--- /dev/null
@@ -0,0 +1,191 @@
+# 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
+#
+
+"""
+Test a charm that breaks post-deployment
+"""
+
+import asyncio
+import logging
+import pytest
+from .. import base
+
+
+# @pytest.mark.serial
+class TestCharm(base.TestN2VC):
+
+    NSD_YAML = """
+    nsd:nsd-catalog:
+        nsd:
+        -   id: brokencharm-ns
+            name: brokencharm-ns
+            short-name: brokencharm-ns
+            description: NS with 1 VNF connected by datanet and mgmtnet VLs
+            version: '1.0'
+            logo: osm.png
+            constituent-vnfd:
+            -   vnfd-id-ref: charmproxy-vnf
+                member-vnf-index: '1'
+            vld:
+            -   id: mgmtnet
+                name: mgmtnet
+                short-name: mgmtnet
+                type: ELAN
+                mgmt-network: 'true'
+                vim-network-name: mgmt
+                vnfd-connection-point-ref:
+                -   vnfd-id-ref: charmproxy-vnf
+                    member-vnf-index-ref: '1'
+                    vnfd-connection-point-ref: vnf-mgmt
+                -   vnfd-id-ref: charmproxy-vnf
+                    member-vnf-index-ref: '2'
+                    vnfd-connection-point-ref: vnf-mgmt
+            -   id: datanet
+                name: datanet
+                short-name: datanet
+                type: ELAN
+                vnfd-connection-point-ref:
+                -   vnfd-id-ref: charmproxy-vnf
+                    member-vnf-index-ref: '1'
+                    vnfd-connection-point-ref: vnf-data
+                -   vnfd-id-ref: charmproxy-vnf
+                    member-vnf-index-ref: '2'
+                    vnfd-connection-point-ref: vnf-data
+    """
+
+    VNFD_YAML = """
+    vnfd:vnfd-catalog:
+        vnfd:
+        -   id: hackfest-simplecharm-vnf
+            name: hackfest-simplecharm-vnf
+            short-name: hackfest-simplecharm-vnf
+            version: '1.0'
+            description: A VNF consisting of 2 VDUs connected to an internal VL, and one VDU with cloud-init
+            logo: osm.png
+            connection-point:
+            -   id: vnf-mgmt
+                name: vnf-mgmt
+                short-name: vnf-mgmt
+                type: VPORT
+            -   id: vnf-data
+                name: vnf-data
+                short-name: vnf-data
+                type: VPORT
+            mgmt-interface:
+                cp: vnf-mgmt
+            internal-vld:
+            -   id: internal
+                name: internal
+                short-name: internal
+                type: ELAN
+                internal-connection-point:
+                -   id-ref: mgmtVM-internal
+                -   id-ref: dataVM-internal
+            vdu:
+            -   id: mgmtVM
+                name: mgmtVM
+                image: hackfest3-mgmt
+                count: '1'
+                vm-flavor:
+                    vcpu-count: '1'
+                    memory-mb: '1024'
+                    storage-gb: '10'
+                interface:
+                -   name: mgmtVM-eth0
+                    position: '1'
+                    type: EXTERNAL
+                    virtual-interface:
+                        type: PARAVIRT
+                    external-connection-point-ref: vnf-mgmt
+                -   name: mgmtVM-eth1
+                    position: '2'
+                    type: INTERNAL
+                    virtual-interface:
+                        type: PARAVIRT
+                    internal-connection-point-ref: mgmtVM-internal
+                internal-connection-point:
+                -   id: mgmtVM-internal
+                    name: mgmtVM-internal
+                    short-name: mgmtVM-internal
+                    type: VPORT
+                cloud-init-file: cloud-config.txt
+            -   id: dataVM
+                name: dataVM
+                image: hackfest3-mgmt
+                count: '1'
+                vm-flavor:
+                    vcpu-count: '1'
+                    memory-mb: '1024'
+                    storage-gb: '10'
+                interface:
+                -   name: dataVM-eth0
+                    position: '1'
+                    type: INTERNAL
+                    virtual-interface:
+                        type: PARAVIRT
+                    internal-connection-point-ref: dataVM-internal
+                -   name: dataVM-xe0
+                    position: '2'
+                    type: EXTERNAL
+                    virtual-interface:
+                        type: PARAVIRT
+                    external-connection-point-ref: vnf-data
+                internal-connection-point:
+                -   id: dataVM-internal
+                    name: dataVM-internal
+                    short-name: dataVM-internal
+                    type: VPORT
+            vnf-configuration:
+                juju:
+                    charm: broken
+                    proxy: true
+                initial-config-primitive:
+                -   seq: '1'
+                    name: touch
+                    parameter:
+                    -   name: filename
+                        value: '/home/ubuntu/first-touch'
+                config-primitive:
+                -   name: touch
+                    parameter:
+                    -   name: filename
+                        data-type: STRING
+                        default-value: '/home/ubuntu/touched'
+    """
+
+    # @pytest.mark.serial
+    @pytest.mark.asyncio
+    async def test_charm_proxy(self, event_loop):
+        """Deploy and execute the initial-config-primitive of a VNF."""
+
+        if self.nsd and self.vnfd:
+            vnf_index = 0
+
+            for config in self.get_config():
+                juju = config['juju']
+                charm = juju['charm']
+
+                await self.deploy(
+                    vnf_index,
+                    charm,
+                    config,
+                    event_loop,
+                )
+
+            while await self.running():
+                print("Waiting for test to finish...")
+                await asyncio.sleep(15)
+            logging.debug("test_charm_proxy stopped")
+
+        return 'ok'
diff --git a/tests/integration/test_charm_native.py b/tests/integration/test_charm_native.py
new file mode 100644 (file)
index 0000000..1f48a6e
--- /dev/null
@@ -0,0 +1,154 @@
+# 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
+#
+
+"""
+Deploy a native charm (to LXD) and execute a primitive
+"""
+
+import asyncio
+import pytest
+from .. import base
+
+
+# @pytest.mark.serial
+class TestCharm(base.TestN2VC):
+
+    NSD_YAML = """
+    nsd:nsd-catalog:
+        nsd:
+        -   id: charmnative-ns
+            name: charmnative-ns
+            short-name: charmnative-ns
+            description: NS with 1 VNFs charmnative-vnf connected by datanet and mgmtnet VLs
+            version: '1.0'
+            logo: osm.png
+            constituent-vnfd:
+            -   vnfd-id-ref: charmnative-vnf
+                member-vnf-index: '1'
+            vld:
+            -   id: mgmtnet
+                name: mgmtnet
+                short-name: mgmtnet
+                type: ELAN
+                mgmt-network: 'true'
+                vim-network-name: mgmt
+                vnfd-connection-point-ref:
+                -   vnfd-id-ref: charmnative-vnf
+                    member-vnf-index-ref: '1'
+                    vnfd-connection-point-ref: vnf-mgmt
+                -   vnfd-id-ref: charmnative-vnf
+                    member-vnf-index-ref: '2'
+                    vnfd-connection-point-ref: vnf-mgmt
+            -   id: datanet
+                name: datanet
+                short-name: datanet
+                type: ELAN
+                vnfd-connection-point-ref:
+                -   vnfd-id-ref: charmnative-vnf
+                    member-vnf-index-ref: '1'
+                    vnfd-connection-point-ref: vnf-data
+                -   vnfd-id-ref: charmnative-vnf
+                    member-vnf-index-ref: '2'
+                    vnfd-connection-point-ref: vnf-data
+    """
+
+    VNFD_YAML = """
+    vnfd:vnfd-catalog:
+        vnfd:
+        -   id: charmnative-vnf
+            name: charmnative-vnf
+            short-name: charmnative-vnf
+            version: '1.0'
+            description: A VNF consisting of 2 VDUs w/charms connected to an internal VL, and one VDU with cloud-init
+            logo: osm.png
+            connection-point:
+            -   id: vnf-mgmt
+                name: vnf-mgmt
+                short-name: vnf-mgmt
+                type: VPORT
+            -   id: vnf-data
+                name: vnf-data
+                short-name: vnf-data
+                type: VPORT
+            mgmt-interface:
+                cp: vnf-mgmt
+            internal-vld:
+            -   id: internal
+                name: internal
+                short-name: internal
+                type: ELAN
+                internal-connection-point:
+                -   id-ref: mgmtVM-internal
+                -   id-ref: dataVM-internal
+            vdu:
+            -   id: mgmtVM
+                name: mgmtVM
+                image: xenial
+                count: '1'
+                vm-flavor:
+                    vcpu-count: '1'
+                    memory-mb: '1024'
+                    storage-gb: '10'
+                interface:
+                -   name: mgmtVM-eth0
+                    position: '1'
+                    type: EXTERNAL
+                    virtual-interface:
+                        type: VIRTIO
+                    external-connection-point-ref: vnf-mgmt
+                -   name: mgmtVM-eth1
+                    position: '2'
+                    type: INTERNAL
+                    virtual-interface:
+                        type: VIRTIO
+                    internal-connection-point-ref: mgmtVM-internal
+                internal-connection-point:
+                -   id: mgmtVM-internal
+                    name: mgmtVM-internal
+                    short-name: mgmtVM-internal
+                    type: VPORT
+                cloud-init-file: cloud-config.txt
+                vdu-configuration:
+                    juju:
+                        charm: native-ci
+                        proxy: false
+                    initial-config-primitive:
+                    -   seq: '1'
+                        name: test
+    """
+
+    @pytest.mark.asyncio
+    async def test_charm_native(self, event_loop):
+        """Deploy and execute the initial-config-primitive of a VNF."""
+
+        if self.nsd and self.vnfd:
+            vnf_index = 0
+
+            for config in self.get_config():
+                juju = config['juju']
+                charm = juju['charm']
+
+                await self.deploy(
+                    vnf_index,
+                    charm,
+                    config,
+                    loop=event_loop,
+                )
+
+            while await self.running():
+                print("Waiting for test to finish...")
+                await asyncio.sleep(15)
+
+            print("test_charm_native stopped")
+        return 'ok'
diff --git a/tests/integration/test_charm_proxy.py b/tests/integration/test_charm_proxy.py
new file mode 100644 (file)
index 0000000..608f94f
--- /dev/null
@@ -0,0 +1,156 @@
+# 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
+#
+
+"""
+Deploy a VNF with a proxy charm, executing an initial-config-primitive
+"""
+
+import asyncio
+import logging
+import pytest
+from .. import base
+
+
+# @pytest.mark.serial
+class TestCharm(base.TestN2VC):
+
+    NSD_YAML = """
+    nsd:nsd-catalog:
+        nsd:
+        -   id: charmproxy-ns
+            name: charmproxy-ns
+            short-name: charmproxy-ns
+            description: NS with 1 VNF connected by datanet and mgmtnet VLs
+            version: '1.0'
+            logo: osm.png
+            constituent-vnfd:
+            -   vnfd-id-ref: charmproxy-vnf
+                member-vnf-index: '1'
+            vld:
+            -   id: mgmtnet
+                name: mgmtnet
+                short-name: mgmtnet
+                type: ELAN
+                mgmt-network: 'true'
+                vim-network-name: mgmt
+                vnfd-connection-point-ref:
+                -   vnfd-id-ref: charmproxy-vnf
+                    member-vnf-index-ref: '1'
+                    vnfd-connection-point-ref: vnf-mgmt
+                -   vnfd-id-ref: charmproxy-vnf
+                    member-vnf-index-ref: '2'
+                    vnfd-connection-point-ref: vnf-mgmt
+            -   id: datanet
+                name: datanet
+                short-name: datanet
+                type: ELAN
+                vnfd-connection-point-ref:
+                -   vnfd-id-ref: charmproxy-vnf
+                    member-vnf-index-ref: '1'
+                    vnfd-connection-point-ref: vnf-data
+                -   vnfd-id-ref: charmproxy-vnf
+                    member-vnf-index-ref: '2'
+                    vnfd-connection-point-ref: vnf-data
+    """
+
+    VNFD_YAML = """
+    vnfd:vnfd-catalog:
+        vnfd:
+        -   id: charmproxy-vnf
+            name: charmproxy-vnf
+            short-name: charmproxy-vnf
+            version: '1.0'
+            description: A VNF consisting of 1 VDUs w/proxy charm
+            logo: osm.png
+            connection-point:
+            -   id: vnf-mgmt
+                name: vnf-mgmt
+                short-name: vnf-mgmt
+                type: VPORT
+            -   id: vnf-data
+                name: vnf-data
+                short-name: vnf-data
+                type: VPORT
+            mgmt-interface:
+                cp: vnf-mgmt
+            internal-vld:
+            -   id: internal
+                name: internal
+                short-name: internal
+                type: ELAN
+                internal-connection-point:
+                -   id-ref: mgmtVM-internal
+                -   id-ref: dataVM-internal
+            vdu:
+            -   id: mgmtVM
+                name: mgmtVM
+                image: xenial
+                count: '1'
+                vm-flavor:
+                    vcpu-count: '1'
+                    memory-mb: '1024'
+                    storage-gb: '10'
+                interface:
+                -   name: mgmtVM-eth0
+                    position: '1'
+                    type: EXTERNAL
+                    virtual-interface:
+                        type: VIRTIO
+                    external-connection-point-ref: vnf-mgmt
+                -   name: mgmtVM-eth1
+                    position: '2'
+                    type: INTERNAL
+                    virtual-interface:
+                        type: VIRTIO
+                    internal-connection-point-ref: mgmtVM-internal
+                internal-connection-point:
+                -   id: mgmtVM-internal
+                    name: mgmtVM-internal
+                    short-name: mgmtVM-internal
+                    type: VPORT
+                cloud-init-file: cloud-config.txt
+                vdu-configuration:
+                    juju:
+                        charm: proxy-ci
+                        proxy: true
+                    initial-config-primitive:
+                    -   seq: '1'
+                        name: test
+    """
+
+    # @pytest.mark.serial
+    @pytest.mark.asyncio
+    async def test_charm_proxy(self, event_loop):
+        """Deploy and execute the initial-config-primitive of a VNF."""
+
+        if self.nsd and self.vnfd:
+            vnf_index = 0
+
+            for config in self.get_config():
+                juju = config['juju']
+                charm = juju['charm']
+
+                await self.deploy(
+                    vnf_index,
+                    charm,
+                    config,
+                    event_loop,
+                )
+
+            while await self.running():
+                print("Waiting for test to finish...")
+                await asyncio.sleep(15)
+            logging.debug("test_charm_proxy stopped")
+
+        return 'ok'
diff --git a/tests/integration/test_charm_relate.py b/tests/integration/test_charm_relate.py
new file mode 100644 (file)
index 0000000..714e2f2
--- /dev/null
@@ -0,0 +1,200 @@
+# 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
+#
+
+"""
+Deploy a multi-vdu, multi-charm VNF
+"""
+
+import asyncio
+import logging
+import pytest
+from .. import base
+
+
+# @pytest.mark.serial
+class TestCharm(base.TestN2VC):
+
+    NSD_YAML = """
+    nsd:nsd-catalog:
+        nsd:
+        -   id: multivdurelate-ns
+            name: multivdurelate-ns
+            short-name: multivdurelate-ns
+            description: NS with 1 VNF connected by datanet and mgmtnet VLs
+            version: '1.0'
+            logo: osm.png
+            constituent-vnfd:
+            -   vnfd-id-ref: multivdurelate-vnf
+                member-vnf-index: '1'
+            vld:
+            -   id: mgmtnet
+                name: mgmtnet
+                short-name: mgmtnet
+                type: ELAN
+                mgmt-network: 'true'
+                vim-network-name: mgmt
+                vnfd-connection-point-ref:
+                -   vnfd-id-ref: multivdurelate-vnf
+                    member-vnf-index-ref: '1'
+                    vnfd-connection-point-ref: vnf-mgmt
+                -   vnfd-id-ref: multivdurelate-vnf
+                    member-vnf-index-ref: '2'
+                    vnfd-connection-point-ref: vnf-mgmt
+            -   id: datanet
+                name: datanet
+                short-name: datanet
+                type: ELAN
+                vnfd-connection-point-ref:
+                -   vnfd-id-ref: multivdurelate-vnf
+                    member-vnf-index-ref: '1'
+                    vnfd-connection-point-ref: vnf-data
+                -   vnfd-id-ref: multivdurelate-vnf
+                    member-vnf-index-ref: '2'
+                    vnfd-connection-point-ref: vnf-data
+    """
+
+    VNFD_YAML = """
+    vnfd:vnfd-catalog:
+        vnfd:
+        -   id: multivdurelate-vnf
+            name: multivdurelate-vnf
+            short-name: multivdurelate-vnf
+            version: '1.0'
+            description: A VNF consisting of 1 VDUs w/proxy charm
+            logo: osm.png
+            connection-point:
+            -   id: vnf-mgmt
+                name: vnf-mgmt
+                short-name: vnf-mgmt
+                type: VPORT
+            -   id: vnf-data
+                name: vnf-data
+                short-name: vnf-data
+                type: VPORT
+            mgmt-interface:
+                cp: vnf-mgmt
+            internal-vld:
+            -   id: internal
+                name: internal
+                short-name: internal
+                type: ELAN
+                internal-connection-point:
+                -   id-ref: mgmtVM-internal
+                -   id-ref: dataVM-internal
+            vdu:
+            -   id: mgmtVM
+                name: mgmtVM
+                image: xenial
+                count: '1'
+                vm-flavor:
+                    vcpu-count: '1'
+                    memory-mb: '1024'
+                    storage-gb: '10'
+                interface:
+                -   name: mgmtVM-eth0
+                    position: '1'
+                    type: EXTERNAL
+                    virtual-interface:
+                        type: VIRTIO
+                    external-connection-point-ref: vnf-mgmt
+                -   name: mgmtVM-eth1
+                    position: '2'
+                    type: INTERNAL
+                    virtual-interface:
+                        type: VIRTIO
+                    internal-connection-point-ref: mgmtVM-internal
+                internal-connection-point:
+                -   id: mgmtVM-internal
+                    name: mgmtVM-internal
+                    short-name: mgmtVM-internal
+                    type: VPORT
+                cloud-init-file: cloud-config.txt
+                vdu-configuration:
+                    juju:
+                        charm: proxy-ci
+                        proxy: true
+                        vca-relationships:
+                            # Relation needs to map to the vdu providing or
+                            # requiring, so that we can map to the deployed app.
+                            relation:
+                            -   provides: dataVM:db
+                                requires: mgmtVM:app
+                    initial-config-primitive:
+                    -   seq: '1'
+                        name: test
+            -   id: dataVM
+                name: dataVM
+                image: xenial
+                count: '1'
+                vm-flavor:
+                    vcpu-count: '1'
+                    memory-mb: '1024'
+                    storage-gb: '10'
+                interface:
+                -   name: dataVM-eth0
+                    position: '1'
+                    type: EXTERNAL
+                    virtual-interface:
+                        type: VIRTIO
+                    external-connection-point-ref: vnf-mgmt
+                -   name: dataVM-eth1
+                    position: '2'
+                    type: INTERNAL
+                    virtual-interface:
+                        type: VIRTIO
+                    internal-connection-point-ref: dataVM-internal
+                internal-connection-point:
+                -   id: dataVM-internal
+                    name: dataVM-internal
+                    short-name: dataVM-internal
+                    type: VPORT
+                cloud-init-file: cloud-config.txt
+                vdu-configuration:
+                    juju:
+                        charm: proxy-ci
+                        proxy: true
+                    initial-config-primitive:
+                    -   seq: '1'
+                        name: test
+
+    """
+
+    # @pytest.mark.serial
+    @pytest.mark.asyncio
+    async def test_multivdu_relate(self, event_loop):
+        """Deploy and execute the initial-config-primitive of a VNF."""
+
+        if self.nsd and self.vnfd:
+            vnf_index = 0
+
+            for config in self.get_config():
+                juju = config['juju']
+                charm = juju['charm']
+
+                await self.deploy(
+                    vnf_index,
+                    charm,
+                    config,
+                    event_loop,
+                )
+                vnf_index += 1
+
+            while await self.running():
+                logging.debug("Waiting for test to finish...")
+                await asyncio.sleep(15)
+
+            # assert False
+            logging.debug("test_multivdu_relate stopped")
+
+        return 'ok'
diff --git a/tests/integration/test_hackfest_simple.py b/tests/integration/test_hackfest_simple.py
new file mode 100644 (file)
index 0000000..db65f06
--- /dev/null
@@ -0,0 +1,203 @@
+# 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
+#
+
+"""
+Deploy a multi-vdu, multi-charm VNF
+"""
+
+import asyncio
+import logging
+import pytest
+from .. import base
+
+
+# @pytest.mark.serial
+class TestCharm(base.TestN2VC):
+
+    NSD_YAML = """
+nsd:nsd-catalog:
+    nsd:
+    -   id: hackfest-simplecharm-ns
+        name: hackfest-simplecharm-ns
+        short-name: hackfest-simplecharm-ns
+        description: NS with 2 VNFs hackfest-simplecharm-vnf connected by datanet and mgmtnet VLs
+        version: '1.0'
+        logo: osm.png
+        constituent-vnfd:
+        -   vnfd-id-ref: hackfest-simplecharm-vnf
+            member-vnf-index: '1'
+        -   vnfd-id-ref: hackfest-simplecharm-vnf
+            member-vnf-index: '2'
+        vld:
+        -   id: mgmtnet
+            name: mgmtnet
+            short-name: mgmtnet
+            type: ELAN
+            mgmt-network: 'true'
+            vim-network-name: mgmt
+            vnfd-connection-point-ref:
+            -   vnfd-id-ref: hackfest-simplecharm-vnf
+                member-vnf-index-ref: '1'
+                vnfd-connection-point-ref: vnf-mgmt
+            -   vnfd-id-ref: hackfest-simplecharm-vnf
+                member-vnf-index-ref: '2'
+                vnfd-connection-point-ref: vnf-mgmt
+        -   id: datanet
+            name: datanet
+            short-name: datanet
+            type: ELAN
+            vnfd-connection-point-ref:
+            -   vnfd-id-ref: hackfest-simplecharm-vnf
+                member-vnf-index-ref: '1'
+                vnfd-connection-point-ref: vnf-data
+            -   vnfd-id-ref: hackfest-simplecharm-vnf
+                member-vnf-index-ref: '2'
+                vnfd-connection-point-ref: vnf-data
+    """
+
+    VNFD_YAML = """
+vnfd:vnfd-catalog:
+    vnfd:
+    -   id: hackfest-simplecharm-vnf
+        name: hackfest-simplecharm-vnf
+        short-name: hackfest-simplecharm-vnf
+        version: '1.0'
+        description: A VNF consisting of 2 VDUs connected to an internal VL, and one VDU with cloud-init
+        logo: osm.png
+        connection-point:
+        -   id: vnf-mgmt
+            name: vnf-mgmt
+            short-name: vnf-mgmt
+            type: VPORT
+        -   id: vnf-data
+            name: vnf-data
+            short-name: vnf-data
+            type: VPORT
+        mgmt-interface:
+            cp: vnf-mgmt
+        internal-vld:
+        -   id: internal
+            name: internal
+            short-name: internal
+            type: ELAN
+            internal-connection-point:
+            -   id-ref: mgmtVM-internal
+            -   id-ref: dataVM-internal
+        vdu:
+        -   id: mgmtVM
+            name: mgmtVM
+            image: hackfest3-mgmt
+            count: '1'
+            vm-flavor:
+                vcpu-count: '1'
+                memory-mb: '1024'
+                storage-gb: '10'
+            interface:
+            -   name: mgmtVM-eth0
+                position: '1'
+                type: EXTERNAL
+                virtual-interface:
+                    type: PARAVIRT
+                external-connection-point-ref: vnf-mgmt
+            -   name: mgmtVM-eth1
+                position: '2'
+                type: INTERNAL
+                virtual-interface:
+                    type: PARAVIRT
+                internal-connection-point-ref: mgmtVM-internal
+            internal-connection-point:
+            -   id: mgmtVM-internal
+                name: mgmtVM-internal
+                short-name: mgmtVM-internal
+                type: VPORT
+            cloud-init-file: cloud-config.txt
+        -   id: dataVM
+            name: dataVM
+            image: hackfest3-mgmt
+            count: '1'
+            vm-flavor:
+                vcpu-count: '1'
+                memory-mb: '1024'
+                storage-gb: '10'
+            interface:
+            -   name: dataVM-eth0
+                position: '1'
+                type: INTERNAL
+                virtual-interface:
+                    type: PARAVIRT
+                internal-connection-point-ref: dataVM-internal
+            -   name: dataVM-xe0
+                position: '2'
+                type: EXTERNAL
+                virtual-interface:
+                    type: PARAVIRT
+                external-connection-point-ref: vnf-data
+            internal-connection-point:
+            -   id: dataVM-internal
+                name: dataVM-internal
+                short-name: dataVM-internal
+                type: VPORT
+        vnf-configuration:
+            juju:
+                charm: simple
+            initial-config-primitive:
+            -   seq: '1'
+                name: config
+                parameter:
+                -   name: ssh-hostname
+                    value: <rw_mgmt_ip>
+                -   name: ssh-username
+                    value: ubuntu
+                -   name: ssh-password
+                    value: osm4u
+            -   seq: '2'
+                name: touch
+                parameter:
+                -   name: filename
+                    value: '/home/ubuntu/first-touch'
+            config-primitive:
+            -   name: touch
+                parameter:
+                -   name: filename
+                    data-type: STRING
+                    default-value: '/home/ubuntu/touched'
+    """
+
+    # @pytest.mark.serial
+    @pytest.mark.asyncio
+    async def test_multivdu_multicharm(self, event_loop):
+        """Deploy and execute the initial-config-primitive of a VNF."""
+
+        if self.nsd and self.vnfd:
+            vnf_index = 0
+
+            for config in self.get_config():
+                juju = config['juju']
+                charm = juju['charm']
+
+                await self.deploy(
+                    vnf_index,
+                    charm,
+                    config,
+                    event_loop,
+                )
+                vnf_index += 1
+
+            while await self.running():
+                logging.debug("Waiting for test to finish...")
+                await asyncio.sleep(15)
+            # assert False
+            logging.debug("test_multivdu_multicharm stopped")
+
+        return 'ok'
diff --git a/tests/integration/test_metrics_native.py b/tests/integration/test_metrics_native.py
new file mode 100644 (file)
index 0000000..e82f5df
--- /dev/null
@@ -0,0 +1,159 @@
+# 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
+#
+
+"""
+Deploy a VNF w/native charm that collects metrics
+"""
+import asyncio
+import logging
+import pytest
+from .. import base
+
+
+# @pytest.mark.serial
+class TestCharm(base.TestN2VC):
+
+    NSD_YAML = """
+    nsd:nsd-catalog:
+        nsd:
+        -   id: metricsnative-ns
+            name: metricsnative-ns
+            short-name: metricsnative-ns
+            description: NS with 1 VNFs metricsnative-vnf connected by datanet and mgmtnet VLs
+            version: '1.0'
+            logo: osm.png
+            constituent-vnfd:
+            -   vnfd-id-ref: metricsnative-vnf
+                member-vnf-index: '1'
+            vld:
+            -   id: mgmtnet
+                name: mgmtnet
+                short-name: mgmtnet
+                type: ELAN
+                mgmt-network: 'true'
+                vim-network-name: mgmt
+                vnfd-connection-point-ref:
+                -   vnfd-id-ref: metricsnative-vnf
+                    member-vnf-index-ref: '1'
+                    vnfd-connection-point-ref: vnf-mgmt
+                -   vnfd-id-ref: metricsnative-vnf
+                    member-vnf-index-ref: '2'
+                    vnfd-connection-point-ref: vnf-mgmt
+            -   id: datanet
+                name: datanet
+                short-name: datanet
+                type: ELAN
+                vnfd-connection-point-ref:
+                -   vnfd-id-ref: metricsnative-vnf
+                    member-vnf-index-ref: '1'
+                    vnfd-connection-point-ref: vnf-data
+                -   vnfd-id-ref: metricsnative-vnf
+                    member-vnf-index-ref: '2'
+                    vnfd-connection-point-ref: vnf-data
+    """
+
+    VNFD_YAML = """
+    vnfd:vnfd-catalog:
+        vnfd:
+        -   id: metricsnative-vnf
+            name: metricsnative-vnf
+            short-name: metricsnative-vnf
+            version: '1.0'
+            description: A VNF consisting of 2 VDUs w/charms connected to an internal VL, and one VDU with cloud-init
+            logo: osm.png
+            connection-point:
+            -   id: vnf-mgmt
+                name: vnf-mgmt
+                short-name: vnf-mgmt
+                type: VPORT
+            -   id: vnf-data
+                name: vnf-data
+                short-name: vnf-data
+                type: VPORT
+            mgmt-interface:
+                cp: vnf-mgmt
+            internal-vld:
+            -   id: internal
+                name: internal
+                short-name: internal
+                type: ELAN
+                internal-connection-point:
+                -   id-ref: mgmtVM-internal
+                -   id-ref: dataVM-internal
+            vdu:
+            -   id: mgmtVM
+                name: mgmtVM
+                image: xenial
+                count: '1'
+                vm-flavor:
+                    vcpu-count: '1'
+                    memory-mb: '1024'
+                    storage-gb: '10'
+                interface:
+                -   name: mgmtVM-eth0
+                    position: '1'
+                    type: EXTERNAL
+                    virtual-interface:
+                        type: VIRTIO
+                    external-connection-point-ref: vnf-mgmt
+                -   name: mgmtVM-eth1
+                    position: '2'
+                    type: INTERNAL
+                    virtual-interface:
+                        type: VIRTIO
+                    internal-connection-point-ref: mgmtVM-internal
+                internal-connection-point:
+                -   id: mgmtVM-internal
+                    name: mgmtVM-internal
+                    short-name: mgmtVM-internal
+                    type: VPORT
+                cloud-init-file: cloud-config.txt
+            vnf-configuration:
+                juju:
+                    charm: metrics-ci
+                    proxy: false
+                config-primitive:
+                -   name: touch
+                    parameter:
+                    -   name: filename
+                        data-type: STRING
+                        default-value: '/home/ubuntu/touched'
+    """
+
+    # @pytest.mark.serial
+    @pytest.mark.asyncio
+    async def test_metrics_native(self, event_loop):
+        """Deploy and execute the initial-config-primitive of a VNF."""
+
+        if self.nsd and self.vnfd:
+            vnf_index = 0
+
+            for config in self.get_config():
+                juju = config['juju']
+                charm = juju['charm']
+
+                await self.deploy(
+                    vnf_index,
+                    charm,
+                    config,
+                    event_loop,
+                )
+
+            while await self.running():
+                print("Waiting for test to finish...")
+                await asyncio.sleep(15)
+
+            logging.debug("test_metrics_native stopped")
+
+        return 'ok'
diff --git a/tests/integration/test_metrics_proxy.py b/tests/integration/test_metrics_proxy.py
new file mode 100644 (file)
index 0000000..58b0000
--- /dev/null
@@ -0,0 +1,160 @@
+# 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
+#
+
+"""
+Deploy a VNF w/proxy charm that collects metrics
+"""
+import asyncio
+import logging
+import pytest
+from .. import base
+
+
+# @pytest.mark.serial
+class TestCharm(base.TestN2VC):
+
+    NSD_YAML = """
+    nsd:nsd-catalog:
+        nsd:
+        -   id: metricsproxy-ns
+            name: metricsproxy-ns
+            short-name: metricsproxy-ns
+            description: NS with 1 VNFs metricsproxy-vnf connected by datanet and mgmtnet VLs
+            version: '1.0'
+            logo: osm.png
+            constituent-vnfd:
+            -   vnfd-id-ref: metricsproxy-vnf
+                member-vnf-index: '1'
+            vld:
+            -   id: mgmtnet
+                name: mgmtnet
+                short-name: mgmtnet
+                type: ELAN
+                mgmt-network: 'true'
+                vim-network-name: mgmt
+                vnfd-connection-point-ref:
+                -   vnfd-id-ref: metricsproxy-vnf
+                    member-vnf-index-ref: '1'
+                    vnfd-connection-point-ref: vnf-mgmt
+                -   vnfd-id-ref: metricsproxy-vnf
+                    member-vnf-index-ref: '2'
+                    vnfd-connection-point-ref: vnf-mgmt
+            -   id: datanet
+                name: datanet
+                short-name: datanet
+                type: ELAN
+                vnfd-connection-point-ref:
+                -   vnfd-id-ref: metricsproxy-vnf
+                    member-vnf-index-ref: '1'
+                    vnfd-connection-point-ref: vnf-data
+                -   vnfd-id-ref: metricsproxy-vnf
+                    member-vnf-index-ref: '2'
+                    vnfd-connection-point-ref: vnf-data
+    """
+
+    VNFD_YAML = """
+    vnfd:vnfd-catalog:
+        vnfd:
+        -   id: metricsproxy-vnf
+            name: metricsproxy-vnf
+            short-name: metricsproxy-vnf
+            version: '1.0'
+            description: A VNF consisting of 2 VDUs w/charms connected to an internal VL, and one VDU with cloud-init
+            logo: osm.png
+            connection-point:
+            -   id: vnf-mgmt
+                name: vnf-mgmt
+                short-name: vnf-mgmt
+                type: VPORT
+            -   id: vnf-data
+                name: vnf-data
+                short-name: vnf-data
+                type: VPORT
+            mgmt-interface:
+                cp: vnf-mgmt
+            internal-vld:
+            -   id: internal
+                name: internal
+                short-name: internal
+                type: ELAN
+                internal-connection-point:
+                -   id-ref: mgmtVM-internal
+                -   id-ref: dataVM-internal
+            vdu:
+            -   id: mgmtVM
+                name: mgmtVM
+                image: xenial
+                count: '1'
+                vm-flavor:
+                    vcpu-count: '1'
+                    memory-mb: '1024'
+                    storage-gb: '10'
+                interface:
+                -   name: mgmtVM-eth0
+                    position: '1'
+                    type: EXTERNAL
+                    virtual-interface:
+                        type: VIRTIO
+                    external-connection-point-ref: vnf-mgmt
+                -   name: mgmtVM-eth1
+                    position: '2'
+                    type: INTERNAL
+                    virtual-interface:
+                        type: VIRTIO
+                    internal-connection-point-ref: mgmtVM-internal
+                internal-connection-point:
+                -   id: mgmtVM-internal
+                    name: mgmtVM-internal
+                    short-name: mgmtVM-internal
+                    type: VPORT
+                cloud-init-file: cloud-config.txt
+            vnf-configuration:
+                juju:
+                    charm: metrics-proxy-ci
+                    proxy: true
+                initial-config-primitive:
+                -   seq: '1'
+                    name: run
+                    parameter:
+                    -   name: command
+                        data-type: STRING
+                        value: hostname
+"""
+
+    # @pytest.mark.serial
+    @pytest.mark.asyncio
+    async def test_metrics_proxy(self, event_loop):
+        """Deploy and execute the initial-config-primitive of a VNF."""
+
+        if self.nsd and self.vnfd:
+            vnf_index = 0
+
+            for config in self.get_config():
+                juju = config['juju']
+                charm = juju['charm']
+
+                await self.deploy(
+                    vnf_index,
+                    charm,
+                    config,
+                    event_loop,
+                )
+
+            while await self.running():
+                print("Waiting for test to finish...")
+                await asyncio.sleep(15)
+
+            logging.debug("test_metrics_proxy stopped")
+
+        return 'ok'
diff --git a/tests/integration/test_multivdu_multicharm.py b/tests/integration/test_multivdu_multicharm.py
new file mode 100644 (file)
index 0000000..ecad8db
--- /dev/null
@@ -0,0 +1,198 @@
+# 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
+#
+
+"""
+Deploy a multi-vdu, multi-charm VNF
+"""
+
+import asyncio
+import logging
+import pytest
+from .. import base
+
+
+# @pytest.mark.serial
+class TestCharm(base.TestN2VC):
+
+    NSD_YAML = """
+    nsd:nsd-catalog:
+        nsd:
+        -   id: multivdumulticharm-ns
+            name: multivdumulticharm-ns
+            short-name: multivdumulticharm-ns
+            description: NS with 1 VNF connected by datanet and mgmtnet VLs
+            version: '1.0'
+            logo: osm.png
+            constituent-vnfd:
+            -   vnfd-id-ref: multivdumulticharm-vnf
+                member-vnf-index: '1'
+            vld:
+            -   id: mgmtnet
+                name: mgmtnet
+                short-name: mgmtnet
+                type: ELAN
+                mgmt-network: 'true'
+                vim-network-name: mgmt
+                vnfd-connection-point-ref:
+                -   vnfd-id-ref: multivdumulticharm-vnf
+                    member-vnf-index-ref: '1'
+                    vnfd-connection-point-ref: vnf-mgmt
+                -   vnfd-id-ref: multivdumulticharm-vnf
+                    member-vnf-index-ref: '2'
+                    vnfd-connection-point-ref: vnf-mgmt
+            -   id: datanet
+                name: datanet
+                short-name: datanet
+                type: ELAN
+                vnfd-connection-point-ref:
+                -   vnfd-id-ref: multivdumulticharm-vnf
+                    member-vnf-index-ref: '1'
+                    vnfd-connection-point-ref: vnf-data
+                -   vnfd-id-ref: multivdumulticharm-vnf
+                    member-vnf-index-ref: '2'
+                    vnfd-connection-point-ref: vnf-data
+    """
+
+    VNFD_YAML = """
+    vnfd:vnfd-catalog:
+        vnfd:
+        -   id: multivdumulticharm-vnf
+            name: multivdumulticharm-vnf
+            short-name: multivdumulticharm-vnf
+            version: '1.0'
+            description: A VNF consisting of 1 VDUs w/proxy charm
+            logo: osm.png
+            connection-point:
+            -   id: vnf-mgmt
+                name: vnf-mgmt
+                short-name: vnf-mgmt
+                type: VPORT
+            -   id: vnf-data
+                name: vnf-data
+                short-name: vnf-data
+                type: VPORT
+            mgmt-interface:
+                cp: vnf-mgmt
+            internal-vld:
+            -   id: internal
+                name: internal
+                short-name: internal
+                type: ELAN
+                internal-connection-point:
+                -   id-ref: mgmtVM-internal
+                -   id-ref: dataVM-internal
+            vdu:
+            -   id: mgmtVM
+                name: mgmtVM
+                image: xenial
+                count: '1'
+                vm-flavor:
+                    vcpu-count: '1'
+                    memory-mb: '1024'
+                    storage-gb: '10'
+                interface:
+                -   name: mgmtVM-eth0
+                    position: '1'
+                    type: EXTERNAL
+                    virtual-interface:
+                        type: VIRTIO
+                    external-connection-point-ref: vnf-mgmt
+                -   name: mgmtVM-eth1
+                    position: '2'
+                    type: INTERNAL
+                    virtual-interface:
+                        type: VIRTIO
+                    internal-connection-point-ref: mgmtVM-internal
+                internal-connection-point:
+                -   id: mgmtVM-internal
+                    name: mgmtVM-internal
+                    short-name: mgmtVM-internal
+                    type: VPORT
+                cloud-init-file: cloud-config.txt
+                vdu-configuration:
+                    juju:
+                        charm: proxy-ci
+                        proxy: true
+                    initial-config-primitive:
+                    -   seq: '1'
+                        name: test
+            -   id: dataVM
+                name: dataVM
+                image: xenial
+                count: '1'
+                vm-flavor:
+                    vcpu-count: '1'
+                    memory-mb: '1024'
+                    storage-gb: '10'
+                interface:
+                -   name: dataVM-eth0
+                    position: '1'
+                    type: EXTERNAL
+                    virtual-interface:
+                        type: VIRTIO
+                    external-connection-point-ref: vnf-mgmt
+                -   name: dataVM-eth1
+                    position: '2'
+                    type: INTERNAL
+                    virtual-interface:
+                        type: VIRTIO
+                    internal-connection-point-ref: dataVM-internal
+                internal-connection-point:
+                -   id: dataVM-internal
+                    name: dataVM-internal
+                    short-name: dataVM-internal
+                    type: VPORT
+                cloud-init-file: cloud-config.txt
+                vdu-configuration:
+                    juju:
+                        charm: proxy-ci
+                        proxy: true
+                        # Relation needs to map to the vdu providing or
+                        # requiring, so that we can map to the deployed app.
+                        relation:
+                        -   provides: dataVM:db
+                            requires: mgmtVM:app
+                    initial-config-primitive:
+                    -   seq: '1'
+                        name: test
+
+    """
+
+    # @pytest.mark.serial
+    @pytest.mark.asyncio
+    async def test_multivdu_multicharm(self, event_loop):
+        """Deploy and execute the initial-config-primitive of a VNF."""
+
+        if self.nsd and self.vnfd:
+            vnf_index = 0
+
+            for config in self.get_config():
+                juju = config['juju']
+                charm = juju['charm']
+
+                await self.deploy(
+                    vnf_index,
+                    charm,
+                    config,
+                    event_loop,
+                )
+                vnf_index += 1
+
+            while await self.running():
+                logging.debug("Waiting for test to finish...")
+                await asyncio.sleep(15)
+            # assert False
+            logging.debug("test_multivdu_multicharm stopped")
+
+        return 'ok'
diff --git a/tests/integration/test_no_initial_config_primitive.py b/tests/integration/test_no_initial_config_primitive.py
new file mode 100644 (file)
index 0000000..477022e
--- /dev/null
@@ -0,0 +1,156 @@
+# 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
+#
+
+"""
+Test N2VC when the VNF descriptor does not contain an initial-config-primitive.
+"""
+import asyncio
+import logging
+import pytest
+from .. import base
+
+
+# @pytest.mark.serial
+class TestCharm(base.TestN2VC):
+
+    NSD_YAML = """
+    nsd:nsd-catalog:
+        nsd:
+        -   id: noinitconfig-ns
+            name: noinitconfig-ns
+            short-name: noinitconfig-ns
+            description: NS with 1 VNFs noinitconfig-vnf connected by datanet and mgmtnet VLs
+            version: '1.0'
+            logo: osm.png
+            constituent-vnfd:
+            -   vnfd-id-ref: noinitconfig-vnf
+                member-vnf-index: '1'
+            vld:
+            -   id: mgmtnet
+                name: mgmtnet
+                short-name: mgmtnet
+                type: ELAN
+                mgmt-network: 'true'
+                vim-network-name: mgmt
+                vnfd-connection-point-ref:
+                -   vnfd-id-ref: noinitconfig-vnf
+                    member-vnf-index-ref: '1'
+                    vnfd-connection-point-ref: vnf-mgmt
+                -   vnfd-id-ref: noinitconfig-vnf
+                    member-vnf-index-ref: '2'
+                    vnfd-connection-point-ref: vnf-mgmt
+            -   id: datanet
+                name: datanet
+                short-name: datanet
+                type: ELAN
+                vnfd-connection-point-ref:
+                -   vnfd-id-ref: noinitconfig-vnf
+                    member-vnf-index-ref: '1'
+                    vnfd-connection-point-ref: vnf-data
+                -   vnfd-id-ref: noinitconfig-vnf
+                    member-vnf-index-ref: '2'
+                    vnfd-connection-point-ref: vnf-data
+    """
+
+    VNFD_YAML = """
+    vnfd:vnfd-catalog:
+        vnfd:
+        -   id: noinitconfig-vnf
+            name: noinitconfig-vnf
+            short-name: noinitconfig-vnf
+            version: '1.0'
+            description: A VNF consisting of 2 VDUs w/charms connected to an internal VL, and one VDU with cloud-init
+            logo: osm.png
+            connection-point:
+            -   id: vnf-mgmt
+                name: vnf-mgmt
+                short-name: vnf-mgmt
+                type: VPORT
+            -   id: vnf-data
+                name: vnf-data
+                short-name: vnf-data
+                type: VPORT
+            mgmt-interface:
+                cp: vnf-mgmt
+            internal-vld:
+            -   id: internal
+                name: internal
+                short-name: internal
+                type: ELAN
+                internal-connection-point:
+                -   id-ref: mgmtVM-internal
+                -   id-ref: dataVM-internal
+            vdu:
+            -   id: mgmtVM
+                name: mgmtVM
+                image: xenial
+                count: '1'
+                vm-flavor:
+                    vcpu-count: '1'
+                    memory-mb: '1024'
+                    storage-gb: '10'
+                interface:
+                -   name: mgmtVM-eth0
+                    position: '1'
+                    type: EXTERNAL
+                    virtual-interface:
+                        type: VIRTIO
+                    external-connection-point-ref: vnf-mgmt
+                -   name: mgmtVM-eth1
+                    position: '2'
+                    type: INTERNAL
+                    virtual-interface:
+                        type: VIRTIO
+                    internal-connection-point-ref: mgmtVM-internal
+                internal-connection-point:
+                -   id: mgmtVM-internal
+                    name: mgmtVM-internal
+                    short-name: mgmtVM-internal
+                    type: VPORT
+                cloud-init-file: cloud-config.txt
+                vdu-configuration:
+                    juju:
+                        charm: native-ci
+                        proxy: false
+                    config-primitive:
+                    -   name: test
+
+    """
+
+    # @pytest.mark.serial
+    @pytest.mark.asyncio
+    async def test_charm_no_initial_config_primitive(self, event_loop):
+        """Deploy and execute the initial-config-primitive of a VNF."""
+
+        if self.nsd and self.vnfd:
+            vnf_index = 0
+
+            for config in self.get_config():
+                juju = config['juju']
+                charm = juju['charm']
+
+                await self.deploy(
+                    vnf_index,
+                    charm,
+                    config,
+                    event_loop,
+                )
+
+            while await self.running():
+                print("Waiting for test to finish...")
+                await asyncio.sleep(15)
+
+            logging.debug("test_charm_no_initial_config_primitive stopped")
+
+        return 'ok'
diff --git a/tests/integration/test_no_parameter.py b/tests/integration/test_no_parameter.py
new file mode 100644 (file)
index 0000000..9416dbf
--- /dev/null
@@ -0,0 +1,154 @@
+# 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
+#
+
+"""
+Describe what this test is meant to do.
+"""
+import asyncio
+import logging
+import pytest
+from .. import base
+
+
+# @pytest.mark.serial
+class TestCharm(base.TestN2VC):
+
+    NSD_YAML = """
+    nsd:nsd-catalog:
+        nsd:
+        -   id: noparam-ns
+            name: noparam-ns
+            short-name: noparam-ns
+            description: NS with 1 VNFs noparam-vnf connected by datanet and mgmtnet VLs
+            version: '1.0'
+            logo: osm.png
+            constituent-vnfd:
+            -   vnfd-id-ref: noparam-vnf
+                member-vnf-index: '1'
+            vld:
+            -   id: mgmtnet
+                name: mgmtnet
+                short-name: mgmtnet
+                type: ELAN
+                mgmt-network: 'true'
+                vim-network-name: mgmt
+                vnfd-connection-point-ref:
+                -   vnfd-id-ref: noparam-vnf
+                    member-vnf-index-ref: '1'
+                    vnfd-connection-point-ref: vnf-mgmt
+                -   vnfd-id-ref: noparam-vnf
+                    member-vnf-index-ref: '2'
+                    vnfd-connection-point-ref: vnf-mgmt
+            -   id: datanet
+                name: datanet
+                short-name: datanet
+                type: ELAN
+                vnfd-connection-point-ref:
+                -   vnfd-id-ref: noparam-vnf
+                    member-vnf-index-ref: '1'
+                    vnfd-connection-point-ref: vnf-data
+                -   vnfd-id-ref: noparam-vnf
+                    member-vnf-index-ref: '2'
+                    vnfd-connection-point-ref: vnf-data
+    """
+
+    VNFD_YAML = """
+    vnfd:vnfd-catalog:
+        vnfd:
+        -   id: noparam-vnf
+            name: noparam-vnf
+            short-name: noparam-vnf
+            version: '1.0'
+            description: A VNF consisting of 2 VDUs w/charms connected to an internal VL, and one VDU with cloud-init
+            logo: osm.png
+            connection-point:
+            -   id: vnf-mgmt
+                name: vnf-mgmt
+                short-name: vnf-mgmt
+                type: VPORT
+            -   id: vnf-data
+                name: vnf-data
+                short-name: vnf-data
+                type: VPORT
+            mgmt-interface:
+                cp: vnf-mgmt
+            internal-vld:
+            -   id: internal
+                name: internal
+                short-name: internal
+                type: ELAN
+                internal-connection-point:
+                -   id-ref: mgmtVM-internal
+                -   id-ref: dataVM-internal
+            vdu:
+            -   id: mgmtVM
+                name: mgmtVM
+                image: xenial
+                count: '1'
+                vm-flavor:
+                    vcpu-count: '1'
+                    memory-mb: '1024'
+                    storage-gb: '10'
+                interface:
+                -   name: mgmtVM-eth0
+                    position: '1'
+                    type: EXTERNAL
+                    virtual-interface:
+                        type: VIRTIO
+                    external-connection-point-ref: vnf-mgmt
+                -   name: mgmtVM-eth1
+                    position: '2'
+                    type: INTERNAL
+                    virtual-interface:
+                        type: VIRTIO
+                    internal-connection-point-ref: mgmtVM-internal
+                internal-connection-point:
+                -   id: mgmtVM-internal
+                    name: mgmtVM-internal
+                    short-name: mgmtVM-internal
+                    type: VPORT
+                cloud-init-file: cloud-config.txt
+                vdu-configuration:
+                    juju:
+                        charm: native-ci
+                        proxy: false
+                    initial-config-primitive:
+                    -   seq: '1'
+                        name: test
+    """
+
+    # @pytest.mark.serial
+    @pytest.mark.asyncio
+    async def test_charm_no_parameter(self, event_loop):
+        """Deploy and execute the initial-config-primitive of a VNF."""
+        logging.warning("event_loop: {}".format(event_loop))
+        if self.nsd and self.vnfd:
+            vnf_index = 0
+
+            for config in self.get_config():
+                juju = config['juju']
+                charm = juju['charm']
+
+                await self.deploy(
+                    vnf_index,
+                    charm,
+                    config,
+                    event_loop,
+                )
+
+            while await self.running():
+                print("Waiting for test to finish...")
+                await asyncio.sleep(15)
+
+        return 'ok'
diff --git a/tests/integration/test_non_existent_primitive.py b/tests/integration/test_non_existent_primitive.py
new file mode 100644 (file)
index 0000000..d6759bb
--- /dev/null
@@ -0,0 +1,159 @@
+# 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
+#
+
+"""
+Deploy a VNF and execute a non-existent primitive
+"""
+import asyncio
+import logging
+import pytest
+from .. import base
+# import n2vc.vnf
+
+# @pytest.mark.serial
+class TestCharm(base.TestN2VC):
+
+    NSD_YAML = """
+    nsd:nsd-catalog:
+        nsd:
+        -   id: nonexistent-ns
+            name: nonexistent-ns
+            short-name: nonexistent-ns
+            description: NS with 1 VNFs charmnative-vnf connected by datanet and mgmtnet VLs
+            version: '1.0'
+            logo: osm.png
+            constituent-vnfd:
+            -   vnfd-id-ref: charmnative-vnf
+                member-vnf-index: '1'
+            vld:
+            -   id: mgmtnet
+                name: mgmtnet
+                short-name: mgmtnet
+                type: ELAN
+                mgmt-network: 'true'
+                vim-network-name: mgmt
+                vnfd-connection-point-ref:
+                -   vnfd-id-ref: charmnative-vnf
+                    member-vnf-index-ref: '1'
+                    vnfd-connection-point-ref: vnf-mgmt
+                -   vnfd-id-ref: charmnative-vnf
+                    member-vnf-index-ref: '2'
+                    vnfd-connection-point-ref: vnf-mgmt
+            -   id: datanet
+                name: datanet
+                short-name: datanet
+                type: ELAN
+                vnfd-connection-point-ref:
+                -   vnfd-id-ref: charmnative-vnf
+                    member-vnf-index-ref: '1'
+                    vnfd-connection-point-ref: vnf-data
+                -   vnfd-id-ref: charmnative-vnf
+                    member-vnf-index-ref: '2'
+                    vnfd-connection-point-ref: vnf-data
+    """
+
+    VNFD_YAML = """
+    vnfd:vnfd-catalog:
+        vnfd:
+        -   id: charmnative-vnf
+            name: charmnative-vnf
+            short-name: charmnative-vnf
+            version: '1.0'
+            description: A VNF consisting of 2 VDUs w/charms connected to an internal VL, and one VDU with cloud-init
+            logo: osm.png
+            connection-point:
+            -   id: vnf-mgmt
+                name: vnf-mgmt
+                short-name: vnf-mgmt
+                type: VPORT
+            -   id: vnf-data
+                name: vnf-data
+                short-name: vnf-data
+                type: VPORT
+            mgmt-interface:
+                cp: vnf-mgmt
+            internal-vld:
+            -   id: internal
+                name: internal
+                short-name: internal
+                type: ELAN
+                internal-connection-point:
+                -   id-ref: mgmtVM-internal
+                -   id-ref: dataVM-internal
+            vdu:
+            -   id: mgmtVM
+                name: mgmtVM
+                image: xenial
+                count: '1'
+                vm-flavor:
+                    vcpu-count: '1'
+                    memory-mb: '1024'
+                    storage-gb: '10'
+                interface:
+                -   name: mgmtVM-eth0
+                    position: '1'
+                    type: EXTERNAL
+                    virtual-interface:
+                        type: VIRTIO
+                    external-connection-point-ref: vnf-mgmt
+                -   name: mgmtVM-eth1
+                    position: '2'
+                    type: INTERNAL
+                    virtual-interface:
+                        type: VIRTIO
+                    internal-connection-point-ref: mgmtVM-internal
+                internal-connection-point:
+                -   id: mgmtVM-internal
+                    name: mgmtVM-internal
+                    short-name: mgmtVM-internal
+                    type: VPORT
+                cloud-init-file: cloud-config.txt
+                vdu-configuration:
+                    juju:
+                        charm: native-ci
+                        proxy: false
+                    initial-config-primitive:
+                    -   seq: '1'
+                        name: idonotexist
+                    -   seq: '2'
+                        name: test
+    """
+
+    # @pytest.mark.serial
+    @pytest.mark.asyncio
+    async def test_charm_non_existent_primitive(self, event_loop):
+        """Deploy and execute the initial-config-primitive of a VNF."""
+
+        if self.nsd and self.vnfd:
+            vnf_index = 0
+
+            for config in self.get_config():
+                juju = config['juju']
+                charm = juju['charm']
+
+                await self.deploy(
+                    vnf_index,
+                    charm,
+                    config,
+                    event_loop,
+                )
+
+            while await self.running():
+                print("Waiting for test to finish...")
+                await asyncio.sleep(15)
+            logging.debug("test_charm_non_string_parameter stopped")
+
+            
+            # assert False
+        return 'ok'
diff --git a/tests/integration/test_non_string_parameter.py b/tests/integration/test_non_string_parameter.py
new file mode 100644 (file)
index 0000000..7e15ae2
--- /dev/null
@@ -0,0 +1,161 @@
+# 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
+#
+
+"""
+Deploy a VNF with a non-string parameter passed to a primitive
+"""
+import asyncio
+import logging
+import pytest
+from .. import base
+
+
+# @pytest.mark.serial
+class TestCharm(base.TestN2VC):
+
+    NSD_YAML = """
+    nsd:nsd-catalog:
+        nsd:
+        -   id: nonstring-ns
+            name: nonstring-ns
+            short-name: nonstring-ns
+            description: NS with 1 VNFs charmnative-vnf connected by datanet and mgmtnet VLs
+            version: '1.0'
+            logo: osm.png
+            constituent-vnfd:
+            -   vnfd-id-ref: charmnative-vnf
+                member-vnf-index: '1'
+            vld:
+            -   id: mgmtnet
+                name: mgmtnet
+                short-name: mgmtnet
+                type: ELAN
+                mgmt-network: 'true'
+                vim-network-name: mgmt
+                vnfd-connection-point-ref:
+                -   vnfd-id-ref: charmnative-vnf
+                    member-vnf-index-ref: '1'
+                    vnfd-connection-point-ref: vnf-mgmt
+                -   vnfd-id-ref: charmnative-vnf
+                    member-vnf-index-ref: '2'
+                    vnfd-connection-point-ref: vnf-mgmt
+            -   id: datanet
+                name: datanet
+                short-name: datanet
+                type: ELAN
+                vnfd-connection-point-ref:
+                -   vnfd-id-ref: charmnative-vnf
+                    member-vnf-index-ref: '1'
+                    vnfd-connection-point-ref: vnf-data
+                -   vnfd-id-ref: charmnative-vnf
+                    member-vnf-index-ref: '2'
+                    vnfd-connection-point-ref: vnf-data
+    """
+
+    VNFD_YAML = """
+    vnfd:vnfd-catalog:
+        vnfd:
+        -   id: charmnative-vnf
+            name: charmnative-vnf
+            short-name: charmnative-vnf
+            version: '1.0'
+            description: A VNF consisting of 2 VDUs w/charms connected to an internal VL, and one VDU with cloud-init
+            logo: osm.png
+            connection-point:
+            -   id: vnf-mgmt
+                name: vnf-mgmt
+                short-name: vnf-mgmt
+                type: VPORT
+            -   id: vnf-data
+                name: vnf-data
+                short-name: vnf-data
+                type: VPORT
+            mgmt-interface:
+                cp: vnf-mgmt
+            internal-vld:
+            -   id: internal
+                name: internal
+                short-name: internal
+                type: ELAN
+                internal-connection-point:
+                -   id-ref: mgmtVM-internal
+                -   id-ref: dataVM-internal
+            vdu:
+            -   id: mgmtVM
+                name: mgmtVM
+                image: xenial
+                count: '1'
+                vm-flavor:
+                    vcpu-count: '1'
+                    memory-mb: '1024'
+                    storage-gb: '10'
+                interface:
+                -   name: mgmtVM-eth0
+                    position: '1'
+                    type: EXTERNAL
+                    virtual-interface:
+                        type: VIRTIO
+                    external-connection-point-ref: vnf-mgmt
+                -   name: mgmtVM-eth1
+                    position: '2'
+                    type: INTERNAL
+                    virtual-interface:
+                        type: VIRTIO
+                    internal-connection-point-ref: mgmtVM-internal
+                internal-connection-point:
+                -   id: mgmtVM-internal
+                    name: mgmtVM-internal
+                    short-name: mgmtVM-internal
+                    type: VPORT
+                cloud-init-file: cloud-config.txt
+                vdu-configuration:
+                    juju:
+                        charm: native-ci
+                        proxy: false
+                    initial-config-primitive:
+                    -   seq: '1'
+                        name: test
+                    -   seq: '2'
+                        name: testint
+                        parameter:
+                        -   name: intval
+                            data-type: INTEGER
+                            value: 1
+    """
+
+    # @pytest.mark.serial
+    @pytest.mark.asyncio
+    async def test_charm_non_string_parameter(self, event_loop):
+        """Deploy and execute the initial-config-primitive of a VNF."""
+
+        if self.nsd and self.vnfd:
+            vnf_index = 0
+
+            for config in self.get_config():
+                juju = config['juju']
+                charm = juju['charm']
+
+                await self.deploy(
+                    vnf_index,
+                    charm,
+                    config,
+                    event_loop,
+                )
+
+            while await self.running():
+                print("Waiting for test to finish...")
+                await asyncio.sleep(15)
+            logging.debug("test_charm_non_string_parameter stopped")
+            # assert False
+        return 'ok'
diff --git a/tests/integration/test_simplecharm.py b/tests/integration/test_simplecharm.py
new file mode 100644 (file)
index 0000000..0546bbb
--- /dev/null
@@ -0,0 +1,192 @@
+# 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
+#
+
+"""
+Exercise the simplecharm hackfest example:
+https://osm-download.etsi.org/ftp/osm-4.0-four/4th-hackfest/packages/hackfest_simplecharm_vnf.tar.gz
+"""
+
+import asyncio
+import logging
+import pytest
+from .. import base
+
+
+# @pytest.mark.serial
+class TestCharm(base.TestN2VC):
+
+    NSD_YAML = """
+    nsd:nsd-catalog:
+        nsd:
+        -   id: charmproxy-ns
+            name: charmproxy-ns
+            short-name: charmproxy-ns
+            description: NS with 1 VNF connected by datanet and mgmtnet VLs
+            version: '1.0'
+            logo: osm.png
+            constituent-vnfd:
+            -   vnfd-id-ref: charmproxy-vnf
+                member-vnf-index: '1'
+            vld:
+            -   id: mgmtnet
+                name: mgmtnet
+                short-name: mgmtnet
+                type: ELAN
+                mgmt-network: 'true'
+                vim-network-name: mgmt
+                vnfd-connection-point-ref:
+                -   vnfd-id-ref: charmproxy-vnf
+                    member-vnf-index-ref: '1'
+                    vnfd-connection-point-ref: vnf-mgmt
+                -   vnfd-id-ref: charmproxy-vnf
+                    member-vnf-index-ref: '2'
+                    vnfd-connection-point-ref: vnf-mgmt
+            -   id: datanet
+                name: datanet
+                short-name: datanet
+                type: ELAN
+                vnfd-connection-point-ref:
+                -   vnfd-id-ref: charmproxy-vnf
+                    member-vnf-index-ref: '1'
+                    vnfd-connection-point-ref: vnf-data
+                -   vnfd-id-ref: charmproxy-vnf
+                    member-vnf-index-ref: '2'
+                    vnfd-connection-point-ref: vnf-data
+    """
+
+    VNFD_YAML = """
+    vnfd:vnfd-catalog:
+        vnfd:
+        -   id: hackfest-simplecharm-vnf
+            name: hackfest-simplecharm-vnf
+            short-name: hackfest-simplecharm-vnf
+            version: '1.0'
+            description: A VNF consisting of 2 VDUs connected to an internal VL, and one VDU with cloud-init
+            logo: osm.png
+            connection-point:
+            -   id: vnf-mgmt
+                name: vnf-mgmt
+                short-name: vnf-mgmt
+                type: VPORT
+            -   id: vnf-data
+                name: vnf-data
+                short-name: vnf-data
+                type: VPORT
+            mgmt-interface:
+                cp: vnf-mgmt
+            internal-vld:
+            -   id: internal
+                name: internal
+                short-name: internal
+                type: ELAN
+                internal-connection-point:
+                -   id-ref: mgmtVM-internal
+                -   id-ref: dataVM-internal
+            vdu:
+            -   id: mgmtVM
+                name: mgmtVM
+                image: hackfest3-mgmt
+                count: '1'
+                vm-flavor:
+                    vcpu-count: '1'
+                    memory-mb: '1024'
+                    storage-gb: '10'
+                interface:
+                -   name: mgmtVM-eth0
+                    position: '1'
+                    type: EXTERNAL
+                    virtual-interface:
+                        type: PARAVIRT
+                    external-connection-point-ref: vnf-mgmt
+                -   name: mgmtVM-eth1
+                    position: '2'
+                    type: INTERNAL
+                    virtual-interface:
+                        type: PARAVIRT
+                    internal-connection-point-ref: mgmtVM-internal
+                internal-connection-point:
+                -   id: mgmtVM-internal
+                    name: mgmtVM-internal
+                    short-name: mgmtVM-internal
+                    type: VPORT
+                cloud-init-file: cloud-config.txt
+            -   id: dataVM
+                name: dataVM
+                image: hackfest3-mgmt
+                count: '1'
+                vm-flavor:
+                    vcpu-count: '1'
+                    memory-mb: '1024'
+                    storage-gb: '10'
+                interface:
+                -   name: dataVM-eth0
+                    position: '1'
+                    type: INTERNAL
+                    virtual-interface:
+                        type: PARAVIRT
+                    internal-connection-point-ref: dataVM-internal
+                -   name: dataVM-xe0
+                    position: '2'
+                    type: EXTERNAL
+                    virtual-interface:
+                        type: PARAVIRT
+                    external-connection-point-ref: vnf-data
+                internal-connection-point:
+                -   id: dataVM-internal
+                    name: dataVM-internal
+                    short-name: dataVM-internal
+                    type: VPORT
+            vnf-configuration:
+                juju:
+                    charm: simple
+                    proxy: true
+                initial-config-primitive:
+                -   seq: '1'
+                    name: touch
+                    parameter:
+                    -   name: filename
+                        value: '/home/ubuntu/first-touch'
+                config-primitive:
+                -   name: touch
+                    parameter:
+                    -   name: filename
+                        data-type: STRING
+                        default-value: '/home/ubuntu/touched'
+    """
+
+    # @pytest.mark.serial
+    @pytest.mark.asyncio
+    async def test_charm_proxy(self, event_loop):
+        """Deploy and execute the initial-config-primitive of a VNF."""
+
+        if self.nsd and self.vnfd:
+            vnf_index = 0
+
+            for config in self.get_config():
+                juju = config['juju']
+                charm = juju['charm']
+
+                await self.deploy(
+                    vnf_index,
+                    charm,
+                    config,
+                    event_loop,
+                )
+
+            while await self.running():
+                print("Waiting for test to finish...")
+                await asyncio.sleep(15)
+            logging.debug("test_charm_proxy stopped")
+
+        return 'ok'
diff --git a/tests/test_k8s_juju_conn.py b/tests/test_k8s_juju_conn.py
new file mode 100644 (file)
index 0000000..95b5f38
--- /dev/null
@@ -0,0 +1,132 @@
+#  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 argparse
+import asyncio
+import logging
+import n2vc.k8s_juju_conn
+from base import get_juju_public_key
+import os
+from osm_common.fslocal import FsLocal
+import subprocess
+import yaml
+
+
+def get_args():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--cluster_uuid", help='The UUID of an existing cluster to use', default=None)
+    parser.add_argument("--reset", action="store_true")
+    return parser.parse_args()
+
+async def main():
+
+    args = get_args()
+
+    reuse_cluster_uuid = args.cluster_uuid
+
+    log = logging.getLogger()
+    log.level = logging.DEBUG
+
+    # Extract parameters from the environment in order to run our tests
+    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_charms = os.getenv('VCA_CHARMS', None)
+    vca_secret = os.getenv('VCA_SECRET', None)
+    vca_ca_cert = os.getenv('VCA_CACERT', None)
+
+    # Get the Juju Public key
+    juju_public_key = get_juju_public_key()
+    if juju_public_key:
+        with open(juju_public_key, 'r') as f:
+            juju_public_key = f.read()
+    else:
+        raise Exception("No Juju Public Key found")
+
+    storage = {
+        'driver': 'local',
+        'path': '/srv/app/storage'
+    }
+    fs = FsLocal()
+    fs.fs_connect(storage)
+
+    client = n2vc.k8s_juju_conn.K8sJujuConnector(
+        kubectl_command='/snap/bin/kubectl',
+        juju_command='/snap/bin/juju',
+        fs=fs,
+        db=None,
+    )
+
+    # kubectl config view --raw
+    # microk8s.config
+
+    # if microk8s then
+    # kubecfg = subprocess.getoutput('microk8s.config')
+    # else
+    kubecfg = subprocess.getoutput('kubectl config view --raw')
+    # print(kubecfg)
+
+    # k8screds = yaml.load(kubecfg, Loader=yaml.FullLoader)
+    namespace = 'testing'
+    kdu_model = "./tests/bundles/k8s-zookeeper.yaml"
+
+    """init_env"""
+    cluster_uuid, _ = await client.init_env(kubecfg, namespace, reuse_cluster_uuid=reuse_cluster_uuid)
+    print(cluster_uuid)
+
+    if not reuse_cluster_uuid:
+        # This is a new cluster, so install to it
+
+        """install"""
+        # async def install(self, cluster_uuid, kdu_model, atomic=True, timeout=None, params=None):
+        # TODO: Re-add storage declaration to bundle. The API doesn't support the storage option yet. David is investigating.
+
+        # Deploy the bundle
+        kdu_instance = await client.install(cluster_uuid, kdu_model, atomic=True, timeout=600)
+
+        if kdu_instance:
+            # Inspect
+            print("Getting status")
+            status = await client.status_kdu(cluster_uuid, kdu_instance)
+            print(status)
+
+    # Inspect the bundle
+    config = await client.inspect_kdu(kdu_model)
+    print(config)
+
+    readme = await client.help_kdu(kdu_model)
+    # print(readme)
+
+
+    """upgrade
+    Upgrade to a newer version of the bundle
+    """
+    kdu_model_upgrade = "./tests/bundles/k8s-zookeeper-upgrade.yaml"
+    upgraded = await client.upgrade(cluster_uuid, namespace, kdu_model=kdu_model_upgrade)
+
+    kdu_model_upgrade = "./tests/bundles/k8s-zookeeper-downgrade.yaml"
+    upgraded = await client.upgrade(cluster_uuid, namespace, kdu_model=kdu_model_upgrade)
+
+    """uninstall"""
+
+    """reset"""
+    if args.reset:
+        await client.reset(cluster_uuid)
+
+    await client.logout()
+
+    print("Done")
+
+if __name__ == "__main__":
+    asyncio.run(main())
diff --git a/tests/test_libjuju.py b/tests/test_libjuju.py
new file mode 100644 (file)
index 0000000..cdce5bf
--- /dev/null
@@ -0,0 +1,162 @@
+# 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)
+
+        # Wait for model to be destroyed
+        import time
+        time.sleep(5)
+
+        with pytest.raises(juju.errors.JujuAPIError):
+            model = await client.controller.get_model(MODEL_NAME)
+
+        await client.logout()
+        assert client.authenticated is False
diff --git a/tests/test_lxd.py b/tests/test_lxd.py
new file mode 100644 (file)
index 0000000..de77e4f
--- /dev/null
@@ -0,0 +1,110 @@
+#  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.
+
+"""
+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
diff --git a/tests/test_model.py b/tests/test_model.py
new file mode 100644 (file)
index 0000000..e5d1f78
--- /dev/null
@@ -0,0 +1,69 @@
+#  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.
+
+"""
+Test N2VC's ssh key generation
+"""
+import n2vc
+import pytest
+from . import base
+import uuid
+
+
+@pytest.mark.asyncio
+async def test_model_create():
+    """Test the creation of a new model."""
+    client = base.get_n2vc()
+
+    model_name = "test-{}".format(
+        uuid.uuid4().hex[-4:],
+    )
+
+    pytest.assume(await client.CreateNetworkService(model_name))
+    pytest.assume(await client.DestroyNetworkService(model_name))
+    pytest.assume(await client.logout())
+
+
+@pytest.mark.asyncio
+async def test_destroy_non_existing_network_service():
+    """Destroy a model that doesn't exist."""
+
+    client = base.get_n2vc()
+
+    model_name = "test-{}".format(
+        uuid.uuid4().hex[-4:],
+    )
+
+    with pytest.raises(n2vc.vnf.NetworkServiceDoesNotExist):
+        pytest.assume(await client.DestroyNetworkService(model_name))
+
+    pytest.assume(await client.logout())
+
+
+@pytest.mark.asyncio
+async def test_model_create_duplicate():
+    """Create a new model, and try to create the same model."""
+    client = base.get_n2vc()
+
+    model_name = "test-{}".format(
+        uuid.uuid4().hex[-4:],
+    )
+
+    # Try to recreate bug 628
+    for x in range(0, 1000):
+        model = await client.get_model(model_name)
+        pytest.assume(model)
+
+    pytest.assume(await client.DestroyNetworkService(model_name))
+    pytest.assume(await client.logout())
diff --git a/tests/test_ssh_keygen.py b/tests/test_ssh_keygen.py
new file mode 100644 (file)
index 0000000..e821096
--- /dev/null
@@ -0,0 +1,32 @@
+#  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.
+
+"""
+Test N2VC's ssh key generation
+"""
+import os
+import pytest
+from . import base
+import tempfile
+
+
+@pytest.mark.asyncio
+async def test_ssh_keygen(monkeypatch):
+    with tempfile.TemporaryDirectory() as tmpdirname:
+        monkeypatch.setitem(os.environ, "HOME", tmpdirname)
+
+        client = base.get_n2vc()
+
+        public_key = await client.GetPublicKey()
+        assert len(public_key)