From: David Garcia Date: Mon, 1 Feb 2021 09:39:56 +0000 (+0100) Subject: Revert "Clean up commented or unused code" X-Git-Tag: branch-sol006v331-start~17 X-Git-Url: https://osm.etsi.org/gitweb/?a=commitdiff_plain;h=refs%2Fchanges%2F44%2F10244%2F1;p=osm%2FN2VC.git Revert "Clean up commented or unused code" This reverts commit b4e7f5c425df48f7e946d792184d1d1b44879fe9. Change-Id: I31a65516e65c3eb6528f241e36c1e45f3753f4c0 Signed-off-by: David Garcia --- diff --git a/n2vc/k8s_juju_conn.py b/n2vc/k8s_juju_conn.py index db34994..3d58385 100644 --- a/n2vc/k8s_juju_conn.py +++ b/n2vc/k8s_juju_conn.py @@ -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, diff --git a/n2vc/libjuju.py b/n2vc/libjuju.py index 6848e0f..a79d00d 100644 --- a/n2vc/libjuju.py +++ b/n2vc/libjuju.py @@ -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 ): diff --git a/n2vc/n2vc_juju_conn.py b/n2vc/n2vc_juju_conn.py index 6e78c47..2566b0c 100644 --- a/n2vc/n2vc_juju_conn.py +++ b/n2vc/n2vc_juju_conn.py @@ -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 index 0000000..4d6e64e --- /dev/null +++ b/tests/README.md @@ -0,0 +1,68 @@ + + +# 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 index 0000000..22ec1e3 --- /dev/null +++ b/tests/__init__.py @@ -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 index 0000000..aabf359 --- /dev/null +++ b/tests/base.py @@ -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 index 0000000..8a99e2c --- /dev/null +++ b/tests/bundles/k8s-zookeeper-downgrade.yaml @@ -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 index 0000000..8308f2f --- /dev/null +++ b/tests/bundles/k8s-zookeeper-upgrade.yaml @@ -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 index 0000000..689220e --- /dev/null +++ b/tests/bundles/k8s-zookeeper.yaml @@ -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 index 0000000..9234e57 --- /dev/null +++ b/tests/charms/layers/broken/README.md @@ -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 index 0000000..d224b5d --- /dev/null +++ b/tests/charms/layers/broken/actions.yaml @@ -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 index 0000000..7e30af4 --- /dev/null +++ b/tests/charms/layers/broken/actions/touch @@ -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 index 0000000..8bde313 --- /dev/null +++ b/tests/charms/layers/broken/config.yaml @@ -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 index 0000000..e092eef --- /dev/null +++ b/tests/charms/layers/broken/icon.svg @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/tests/charms/layers/broken/layer.yaml b/tests/charms/layers/broken/layer.yaml new file mode 100644 index 0000000..2ee67bf --- /dev/null +++ b/tests/charms/layers/broken/layer.yaml @@ -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 index 0000000..ed41942 --- /dev/null +++ b/tests/charms/layers/broken/metadata.yaml @@ -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 +subordinate: false +series: ['xenial'] diff --git a/tests/charms/layers/broken/metrics.yaml b/tests/charms/layers/broken/metrics.yaml new file mode 100644 index 0000000..e610b99 --- /dev/null +++ b/tests/charms/layers/broken/metrics.yaml @@ -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 index 0000000..3a018fb --- /dev/null +++ b/tests/charms/layers/broken/reactive/simple.py @@ -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 index 0000000..a765b72 --- /dev/null +++ b/tests/charms/layers/metrics-ci/README.md @@ -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 index 0000000..8bde313 --- /dev/null +++ b/tests/charms/layers/metrics-ci/config.yaml @@ -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 index 0000000..0726ddf --- /dev/null +++ b/tests/charms/layers/metrics-ci/layer.yaml @@ -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 index 0000000..8c7e081 --- /dev/null +++ b/tests/charms/layers/metrics-ci/metadata.yaml @@ -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: +maintainer: Adam Israel +description: | + +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 index 0000000..2326f6c --- /dev/null +++ b/tests/charms/layers/metrics-ci/metrics.yaml @@ -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 index 0000000..4c4756a --- /dev/null +++ b/tests/charms/layers/metrics-ci/reactive/metrics_ci.py @@ -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 index 0000000..e96f02f --- /dev/null +++ b/tests/charms/layers/metrics-proxy-ci/README.md @@ -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 index 0000000..8bde313 --- /dev/null +++ b/tests/charms/layers/metrics-proxy-ci/config.yaml @@ -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 index 0000000..186f1db --- /dev/null +++ b/tests/charms/layers/metrics-proxy-ci/layer.yaml @@ -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 index 0000000..78bf753 --- /dev/null +++ b/tests/charms/layers/metrics-proxy-ci/metadata.yaml @@ -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: +maintainer: Adam Israel +description: | + +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 index 0000000..2326f6c --- /dev/null +++ b/tests/charms/layers/metrics-proxy-ci/metrics.yaml @@ -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 index 0000000..ea5c544 --- /dev/null +++ b/tests/charms/layers/metrics-proxy-ci/reactive/metrics_ci.py @@ -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 index 0000000..d58b762 --- /dev/null +++ b/tests/charms/layers/native-ci/README.md @@ -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 index 0000000..46c6cb7 --- /dev/null +++ b/tests/charms/layers/native-ci/actions.yaml @@ -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 index 0000000..7e30af4 --- /dev/null +++ b/tests/charms/layers/native-ci/actions/test @@ -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 index 0000000..7e30af4 --- /dev/null +++ b/tests/charms/layers/native-ci/actions/testint @@ -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 index 0000000..4dd82f9 --- /dev/null +++ b/tests/charms/layers/native-ci/layer.yaml @@ -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 index 0000000..1c72d9e --- /dev/null +++ b/tests/charms/layers/native-ci/metadata.yaml @@ -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 +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 index 0000000..72365c0 --- /dev/null +++ b/tests/charms/layers/native-ci/reactive/native-ci.py @@ -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 index 0000000..c16d9d8 --- /dev/null +++ b/tests/charms/layers/proxy-ci/README.md @@ -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 index 0000000..304b520 --- /dev/null +++ b/tests/charms/layers/proxy-ci/actions.yaml @@ -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 index 0000000..7e30af4 --- /dev/null +++ b/tests/charms/layers/proxy-ci/actions/test @@ -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 index 0000000..186f1db --- /dev/null +++ b/tests/charms/layers/proxy-ci/layer.yaml @@ -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 index 0000000..37120e3 --- /dev/null +++ b/tests/charms/layers/proxy-ci/metadata.yaml @@ -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: +maintainer: Adam Israel +description: | + +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 index 0000000..cf2ed99 --- /dev/null +++ b/tests/charms/layers/proxy-ci/reactive/proxy_ci.py @@ -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 index 0000000..5504db4 --- /dev/null +++ b/tests/charms/layers/simple/README.md @@ -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 index 0000000..d224b5d --- /dev/null +++ b/tests/charms/layers/simple/actions.yaml @@ -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 index 0000000..7e30af4 --- /dev/null +++ b/tests/charms/layers/simple/actions/touch @@ -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 index 0000000..8bde313 --- /dev/null +++ b/tests/charms/layers/simple/config.yaml @@ -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 index 0000000..e092eef --- /dev/null +++ b/tests/charms/layers/simple/icon.svg @@ -0,0 +1,279 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/tests/charms/layers/simple/layer.yaml b/tests/charms/layers/simple/layer.yaml new file mode 100644 index 0000000..2ee67bf --- /dev/null +++ b/tests/charms/layers/simple/layer.yaml @@ -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 index 0000000..1cb3de9 --- /dev/null +++ b/tests/charms/layers/simple/metadata.yaml @@ -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 +subordinate: false +series: ['xenial'] diff --git a/tests/charms/layers/simple/metrics.yaml b/tests/charms/layers/simple/metrics.yaml new file mode 100644 index 0000000..e610b99 --- /dev/null +++ b/tests/charms/layers/simple/metrics.yaml @@ -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 index 0000000..6e1300e --- /dev/null +++ b/tests/charms/layers/simple/reactive/simple.py @@ -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 index 0000000..e0b5e8c --- /dev/null +++ b/tests/integration/__init__.py @@ -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 index 0000000..d77786a --- /dev/null +++ b/tests/integration/test_broken_charm.py @@ -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 index 0000000..1f48a6e --- /dev/null +++ b/tests/integration/test_charm_native.py @@ -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 index 0000000..608f94f --- /dev/null +++ b/tests/integration/test_charm_proxy.py @@ -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 index 0000000..714e2f2 --- /dev/null +++ b/tests/integration/test_charm_relate.py @@ -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 index 0000000..db65f06 --- /dev/null +++ b/tests/integration/test_hackfest_simple.py @@ -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: + - 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 index 0000000..e82f5df --- /dev/null +++ b/tests/integration/test_metrics_native.py @@ -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 index 0000000..58b0000 --- /dev/null +++ b/tests/integration/test_metrics_proxy.py @@ -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 index 0000000..ecad8db --- /dev/null +++ b/tests/integration/test_multivdu_multicharm.py @@ -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 index 0000000..477022e --- /dev/null +++ b/tests/integration/test_no_initial_config_primitive.py @@ -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 index 0000000..9416dbf --- /dev/null +++ b/tests/integration/test_no_parameter.py @@ -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 index 0000000..d6759bb --- /dev/null +++ b/tests/integration/test_non_existent_primitive.py @@ -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 index 0000000..7e15ae2 --- /dev/null +++ b/tests/integration/test_non_string_parameter.py @@ -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 index 0000000..0546bbb --- /dev/null +++ b/tests/integration/test_simplecharm.py @@ -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 index 0000000..95b5f38 --- /dev/null +++ b/tests/test_k8s_juju_conn.py @@ -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 index 0000000..cdce5bf --- /dev/null +++ b/tests/test_libjuju.py @@ -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 index 0000000..de77e4f --- /dev/null +++ b/tests/test_lxd.py @@ -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 index 0000000..e5d1f78 --- /dev/null +++ b/tests/test_model.py @@ -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 index 0000000..e821096 --- /dev/null +++ b/tests/test_ssh_keygen.py @@ -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)