Feature 10239: Distributed VCA 16/10616/9
authorDavid Garcia <david.garcia@canonical.com>
Mon, 12 Apr 2021 10:07:37 +0000 (12:07 +0200)
committerDavid Garcia <david.garcia@canonical.com>
Mon, 3 May 2021 11:30:40 +0000 (13:30 +0200)
- Add vca_id in all calls that invoke libjuju. This is for being able to
talk to the default VCA or the VCA associated to the VIM
- Add store.py: Abstraction to talk to the database.
  - DBMongoStore: Use the db from common to talk to the database
  - MotorStore: Use motor, an asynchronous mongodb client to talk to the
database
- Add vca/connection.py: Represents the data needed to connect the VCA
- Add EnvironConfig in config.py: Class to get the environment config,
and avoid LCM from passing that

Change-Id: I28625e0c56ce408114022c83d4b7cacbb649434c
Signed-off-by: David Garcia <david.garcia@canonical.com>
26 files changed:
n2vc/config.py
n2vc/juju_watcher.py
n2vc/k8s_helm3_conn.py
n2vc/k8s_helm_base_conn.py
n2vc/k8s_helm_conn.py
n2vc/k8s_juju_conn.py
n2vc/libjuju.py
n2vc/n2vc_conn.py
n2vc/n2vc_juju_conn.py
n2vc/store.py [new file with mode: 0644]
n2vc/tests/unit/test_config.py [new file with mode: 0644]
n2vc/tests/unit/test_connection.py [new file with mode: 0644]
n2vc/tests/unit/test_juju_watcher.py
n2vc/tests/unit/test_k8s_juju_conn.py
n2vc/tests/unit/test_libjuju.py
n2vc/tests/unit/test_n2vc_juju_conn.py
n2vc/tests/unit/test_store.py [new file with mode: 0644]
n2vc/tests/unit/test_utils.py
n2vc/tests/unit/utils.py
n2vc/utils.py
n2vc/vca/__init__.py [new file with mode: 0644]
n2vc/vca/cloud.py [new file with mode: 0644]
n2vc/vca/connection.py [new file with mode: 0644]
n2vc/vca/connection_data.py [new file with mode: 0644]
requirements.in
requirements.txt

index 59a74be..374ec73 100644 (file)
 #     See the License for the specific language governing permissions and
 #     limitations under the License.
 
 #     See the License for the specific language governing permissions and
 #     limitations under the License.
 
+import os
+import typing
+
+
+class EnvironConfig(dict):
+    prefixes = ["OSMLCM_VCA_", "OSMMON_VCA_"]
+
+    def __init__(self, prefixes: typing.List[str] = None):
+        if prefixes:
+            self.prefixes = prefixes
+        for key, value in os.environ.items():
+            if any(key.startswith(prefix) for prefix in self.prefixes):
+                self.__setitem__(self._get_renamed_key(key), value)
+
+    def _get_renamed_key(self, key: str) -> str:
+        for prefix in self.prefixes:
+            key = key.replace(prefix, "")
+        return key.lower()
+
+
 MODEL_CONFIG_KEYS = [
     "agent-metadata-url",
     "agent-stream",
 MODEL_CONFIG_KEYS = [
     "agent-metadata-url",
     "agent-stream",
index 04ad10f..e206e06 100644 (file)
@@ -72,7 +72,10 @@ def application_ready(application: Application) -> bool:
 
 class JujuModelWatcher:
     @staticmethod
 
 class JujuModelWatcher:
     @staticmethod
-    async def wait_for_model(model: Model, timeout: float = 3600):
+    async def wait_for_model(
+        model: Model,
+        timeout: float = 3600
+    ):
         """
         Wait for all entities in model to reach its final state.
 
         """
         Wait for all entities in model to reach its final state.
 
@@ -121,6 +124,7 @@ class JujuModelWatcher:
         total_timeout: float = 3600,
         db_dict: dict = None,
         n2vc: N2VCConnector = None,
         total_timeout: float = 3600,
         db_dict: dict = None,
         n2vc: N2VCConnector = None,
+        vca_id: str = None,
     ):
         """
         Wait for entity to reach its final state.
     ):
         """
         Wait for entity to reach its final state.
@@ -131,6 +135,7 @@ class JujuModelWatcher:
         :param: total_timeout:      Timeout for the entity to be active
         :param: db_dict:            Dictionary with data of the DB to write the updates
         :param: n2vc:               N2VC Connector objector
         :param: total_timeout:      Timeout for the entity to be active
         :param: db_dict:            Dictionary with data of the DB to write the updates
         :param: n2vc:               N2VC Connector objector
+        :param: vca_id:             VCA ID
 
         :raises: asyncio.TimeoutError when timeout reaches
         """
 
         :raises: asyncio.TimeoutError when timeout reaches
         """
@@ -161,6 +166,7 @@ class JujuModelWatcher:
                 timeout=progress_timeout,
                 db_dict=db_dict,
                 n2vc=n2vc,
                 timeout=progress_timeout,
                 db_dict=db_dict,
                 n2vc=n2vc,
+                vca_id=vca_id,
             )
         )
 
             )
         )
 
@@ -182,6 +188,7 @@ class JujuModelWatcher:
         timeout: float,
         db_dict: dict = None,
         n2vc: N2VCConnector = None,
         timeout: float,
         db_dict: dict = None,
         n2vc: N2VCConnector = None,
+        vca_id: str = None,
     ):
         """
         Observes the changes related to an specific entity in a model
     ):
         """
         Observes the changes related to an specific entity in a model
@@ -192,6 +199,7 @@ class JujuModelWatcher:
         :param: timeout:        Maximum time between two updates in the model
         :param: db_dict:        Dictionary with data of the DB to write the updates
         :param: n2vc:           N2VC Connector objector
         :param: timeout:        Maximum time between two updates in the model
         :param: db_dict:        Dictionary with data of the DB to write the updates
         :param: n2vc:           N2VC Connector objector
+        :param: vca_id:         VCA ID
 
         :raises: asyncio.TimeoutError when timeout reaches
         """
 
         :raises: asyncio.TimeoutError when timeout reaches
         """
@@ -249,6 +257,7 @@ class JujuModelWatcher:
                                 detailed_status=status_message,
                                 vca_status=vca_status,
                                 entity_type=delta_entity,
                                 detailed_status=status_message,
                                 vca_status=vca_status,
                                 entity_type=delta_entity,
+                                vca_id=vca_id,
                             )
                 # Check if timeout
                 if time.time() > timeout_end:
                             )
                 # Check if timeout
                 if time.time() > timeout_end:
index 6afadbf..7d69168 100644 (file)
@@ -78,7 +78,25 @@ class K8sHelm3Connector(K8sHelmBaseConnector):
             db_dict: dict = None,
             kdu_name: str = None,
             namespace: str = None,
             db_dict: dict = None,
             kdu_name: str = None,
             namespace: str = None,
+            **kwargs,
     ):
     ):
+        """Install a helm chart
+
+        :param cluster_uuid str: The UUID of the cluster to install to
+        :param kdu_model str: The name or path of a bundle to install
+        :param kdu_instance: Kdu instance name
+        :param atomic bool: If set, waits until the model is active and resets
+                            the cluster on failure.
+        :param timeout int: The time, in seconds, to wait for the install
+                            to finish
+        :param params dict: Key-value pairs of instantiation parameters
+        :param kdu_name: Name of the KDU instance to be installed
+        :param namespace: K8s namespace to use for the KDU instance
+
+        :param kwargs: Additional parameters (None yet)
+
+        :return: True if successful
+        """
         _, cluster_id = self._get_namespace_cluster_id(cluster_uuid)
         self.log.debug("installing {} in cluster {}".format(kdu_model, cluster_id))
 
         _, cluster_id = self._get_namespace_cluster_id(cluster_uuid)
         self.log.debug("installing {} in cluster {}".format(kdu_model, cluster_id))
 
index a79f038..0d001ee 100644 (file)
@@ -100,7 +100,7 @@ class K8sHelmBaseConnector(K8sConnector):
         return namespace, cluster_id
 
     async def init_env(
         return namespace, cluster_id
 
     async def init_env(
-            self, k8s_creds: str, namespace: str = "kube-system", reuse_cluster_uuid=None
+        self, k8s_creds: str, namespace: str = "kube-system", reuse_cluster_uuid=None, **kwargs,
     ) -> (str, bool):
         """
         It prepares a given K8s cluster environment to run Charts
     ) -> (str, bool):
         """
         It prepares a given K8s cluster environment to run Charts
@@ -110,6 +110,7 @@ class K8sHelmBaseConnector(K8sConnector):
         :param namespace: optional namespace to be used for helm. By default,
             'kube-system' will be used
         :param reuse_cluster_uuid: existing cluster uuid for reuse
         :param namespace: optional namespace to be used for helm. By default,
             'kube-system' will be used
         :param reuse_cluster_uuid: existing cluster uuid for reuse
+        :param kwargs: Additional parameters (None yet)
         :return: uuid of the K8s cluster and True if connector has installed some
             software in the cluster
         (on error, an exception will be raised)
         :return: uuid of the K8s cluster and True if connector has installed some
             software in the cluster
         (on error, an exception will be raised)
@@ -236,9 +237,18 @@ class K8sHelmBaseConnector(K8sConnector):
         self.fs.reverse_sync(from_path=cluster_id)
 
     async def reset(
         self.fs.reverse_sync(from_path=cluster_id)
 
     async def reset(
-            self, cluster_uuid: str, force: bool = False, uninstall_sw: bool = False
+            self, cluster_uuid: str, force: bool = False, uninstall_sw: bool = False, **kwargs
     ) -> bool:
     ) -> bool:
+        """Reset a cluster
 
 
+        Resets the Kubernetes cluster by removing the helm deployment that represents it.
+
+        :param cluster_uuid: The UUID of the cluster to reset
+        :param force: Boolean to force the reset
+        :param uninstall_sw: Boolean to force the reset
+        :param kwargs: Additional parameters (None yet)
+        :return: Returns True if successful or raises an exception.
+        """
         namespace, cluster_id = self._get_namespace_cluster_id(cluster_uuid)
         self.log.debug("Resetting K8s environment. cluster uuid: {} uninstall={}"
                        .format(cluster_id, uninstall_sw))
         namespace, cluster_id = self._get_namespace_cluster_id(cluster_uuid)
         self.log.debug("Resetting K8s environment. cluster uuid: {} uninstall={}"
                        .format(cluster_id, uninstall_sw))
@@ -569,7 +579,7 @@ class K8sHelmBaseConnector(K8sConnector):
         else:
             return 0
 
         else:
             return 0
 
-    async def uninstall(self, cluster_uuid: str, kdu_instance: str):
+    async def uninstall(self, cluster_uuid: str, kdu_instance: str, **kwargs):
         """
         Removes an existing KDU instance. It would implicitly use the `delete` or 'uninstall' call
         (this call should happen after all _terminate-config-primitive_ of the VNF
         """
         Removes an existing KDU instance. It would implicitly use the `delete` or 'uninstall' call
         (this call should happen after all _terminate-config-primitive_ of the VNF
@@ -577,6 +587,7 @@ class K8sHelmBaseConnector(K8sConnector):
 
         :param cluster_uuid: UUID of a K8s cluster known by OSM, or namespace:cluster_id
         :param kdu_instance: unique name for the KDU instance to be deleted
 
         :param cluster_uuid: UUID of a K8s cluster known by OSM, or namespace:cluster_id
         :param kdu_instance: unique name for the KDU instance to be deleted
+        :param kwargs: Additional parameters (None yet)
         :return: True if successful
         """
 
         :return: True if successful
         """
 
@@ -648,6 +659,7 @@ class K8sHelmBaseConnector(K8sConnector):
         timeout: float = 300,
         params: dict = None,
         db_dict: dict = None,
         timeout: float = 300,
         params: dict = None,
         db_dict: dict = None,
+        **kwargs,
     ) -> str:
         """Exec primitive (Juju action)
 
     ) -> str:
         """Exec primitive (Juju action)
 
@@ -657,6 +669,7 @@ class K8sHelmBaseConnector(K8sConnector):
         :param timeout: Timeout for action execution
         :param params: Dictionary of all the parameters needed for the action
         :db_dict: Dictionary for any additional data
         :param timeout: Timeout for action execution
         :param params: Dictionary of all the parameters needed for the action
         :db_dict: Dictionary for any additional data
+        :param kwargs: Additional parameters (None yet)
 
         :return: Returns the output of the action
         """
 
         :return: Returns the output of the action
         """
@@ -730,8 +743,30 @@ class K8sHelmBaseConnector(K8sConnector):
 
         return service
 
 
         return service
 
-    async def status_kdu(self, cluster_uuid: str, kdu_instance: str) -> str:
+    async def status_kdu(self, cluster_uuid: str, kdu_instance: str, **kwargs) -> str:
+        """
+        This call would retrieve tha current state of a given KDU instance. It would be
+        would allow to retrieve the _composition_ (i.e. K8s objects) and _specific
+        values_ of the configuration parameters applied to a given instance. This call
+        would be based on the `status` call.
 
 
+        :param cluster_uuid: UUID of a K8s cluster known by OSM
+        :param kdu_instance: unique name for the KDU instance
+        :param kwargs: Additional parameters (None yet)
+        :return: If successful, it will return the following vector of arguments:
+        - K8s `namespace` in the cluster where the KDU lives
+        - `state` of the KDU instance. It can be:
+              - UNKNOWN
+              - DEPLOYED
+              - DELETED
+              - SUPERSEDED
+              - FAILED or
+              - DELETING
+        - List of `resources` (objects) that this release consists of, sorted by kind,
+          and the status of those resources
+        - Last `deployment_time`.
+
+        """
         self.log.debug(
             "status_kdu: cluster_uuid: {}, kdu_instance: {}".format(
                 cluster_uuid, kdu_instance
         self.log.debug(
             "status_kdu: cluster_uuid: {}, kdu_instance: {}".format(
                 cluster_uuid, kdu_instance
index ad230b5..d443f8d 100644 (file)
@@ -99,7 +99,36 @@ class K8sHelmConnector(K8sHelmBaseConnector):
             db_dict: dict = None,
             kdu_name: str = None,
             namespace: str = None,
             db_dict: dict = None,
             kdu_name: str = None,
             namespace: str = None,
+            **kwargs,
     ):
     ):
+        """
+        Deploys of a new KDU instance. It would implicitly rely on the `install` call
+        to deploy the Chart/Bundle properly parametrized (in practice, this call would
+        happen before any _initial-config-primitive_of the VNF is called).
+
+        :param cluster_uuid: UUID of a K8s cluster known by OSM
+        :param kdu_model: chart/ reference (string), which can be either
+            of these options:
+            - a name of chart available via the repos known by OSM
+            - a path to a packaged chart
+            - a path to an unpacked chart directory or a URL
+        :param kdu_instance: Kdu instance name
+        :param atomic: If set, installation process purges chart/bundle on fail, also
+            will wait until all the K8s objects are active
+        :param timeout: Time in seconds to wait for the install of the chart/bundle
+            (defaults to Helm default timeout: 300s)
+        :param params: dictionary of key-value pairs for instantiation parameters
+            (overriding default values)
+        :param dict db_dict: where to write into database when the status changes.
+                        It contains a dict with {collection: <str>, filter: {},
+                        path: <str>},
+                            e.g. {collection: "nsrs", filter:
+                            {_id: <nsd-id>, path: "_admin.deployed.K8S.3"}
+        :param kdu_name: Name of the KDU instance to be installed
+        :param namespace: K8s namespace to use for the KDU instance
+        :param kwargs: Additional parameters (None yet)
+        :return: True if successful
+        """
         _, cluster_id = self._get_namespace_cluster_id(cluster_uuid)
         self.log.debug("installing {} in cluster {}".format(kdu_model, cluster_id))
 
         _, cluster_id = self._get_namespace_cluster_id(cluster_uuid)
         self.log.debug("installing {} in cluster {}".format(kdu_model, cluster_id))
 
index 3130216..e3ec17e 100644 (file)
@@ -20,15 +20,16 @@ import tempfile
 import binascii
 import base64
 
 import binascii
 import base64
 
-from n2vc.config import ModelConfig
-from n2vc.exceptions import K8sException, N2VCBadArgumentsException
+from n2vc.config import EnvironConfig
+from n2vc.exceptions import K8sException
 from n2vc.k8s_conn import K8sConnector
 from n2vc.kubectl import Kubectl, CORE_CLIENT, RBAC_CLIENT
 from .exceptions import MethodNotImplemented
 from n2vc.k8s_conn import K8sConnector
 from n2vc.kubectl import Kubectl, CORE_CLIENT, RBAC_CLIENT
 from .exceptions import MethodNotImplemented
-from n2vc.utils import base64_to_cacert
 from n2vc.libjuju import Libjuju
 from n2vc.utils import obj_to_dict, obj_to_yaml
 from n2vc.libjuju import Libjuju
 from n2vc.utils import obj_to_dict, obj_to_yaml
-
+from n2vc.store import MotorStore
+from n2vc.vca.cloud import Cloud
+from n2vc.vca.connection import get_connection
 from kubernetes.client.models import (
     V1ClusterRole,
     V1ObjectMeta,
 from kubernetes.client.models import (
     V1ClusterRole,
     V1ObjectMeta,
@@ -54,6 +55,8 @@ def generate_rbac_id():
 
 
 class K8sJujuConnector(K8sConnector):
 
 
 class K8sJujuConnector(K8sConnector):
+    libjuju = None
+
     def __init__(
         self,
         fs: object,
     def __init__(
         self,
         fs: object,
@@ -63,7 +66,6 @@ class K8sJujuConnector(K8sConnector):
         log: object = None,
         loop: object = None,
         on_update_db=None,
         log: object = None,
         loop: object = None,
         on_update_db=None,
-        vca_config: dict = None,
     ):
         """
         :param fs: file system for kubernetes and helm configuration
     ):
         """
         :param fs: file system for kubernetes and helm configuration
@@ -86,35 +88,10 @@ class K8sJujuConnector(K8sConnector):
         self.loop = loop or asyncio.get_event_loop()
         self.log.debug("Initializing K8S Juju connector")
 
         self.loop = loop or asyncio.get_event_loop()
         self.log.debug("Initializing K8S Juju connector")
 
-        required_vca_config = [
-            "host",
-            "user",
-            "secret",
-            "ca_cert",
-        ]
-        if not vca_config or not all(k in vca_config for k in required_vca_config):
-            raise N2VCBadArgumentsException(
-                message="Missing arguments in vca_config: {}".format(vca_config),
-                bad_args=required_vca_config,
-            )
-        port = vca_config["port"] if "port" in vca_config else 17070
-        url = "{}:{}".format(vca_config["host"], port)
-        model_config = ModelConfig(vca_config)
-        username = vca_config["user"]
-        secret = vca_config["secret"]
-        ca_cert = base64_to_cacert(vca_config["ca_cert"])
-
-        self.libjuju = Libjuju(
-            endpoint=url,
-            api_proxy=None,  # Not needed for k8s charms
-            model_config=model_config,
-            username=username,
-            password=secret,
-            cacert=ca_cert,
-            loop=self.loop,
-            log=self.log,
-            db=self.db,
-        )
+        db_uri = EnvironConfig(prefixes=["OSMLCM_", "OSMMON_"]).get("database_uri")
+        self._store = MotorStore(db_uri)
+        self.loading_libjuju = asyncio.Lock(loop=self.loop)
+
         self.log.debug("K8S Juju connector initialized")
         # TODO: Remove these commented lines:
         # self.authenticated = False
         self.log.debug("K8S Juju connector initialized")
         # TODO: Remove these commented lines:
         # self.authenticated = False
@@ -128,6 +105,7 @@ class K8sJujuConnector(K8sConnector):
         k8s_creds: str,
         namespace: str = "kube-system",
         reuse_cluster_uuid: str = None,
         k8s_creds: str,
         namespace: str = "kube-system",
         reuse_cluster_uuid: str = None,
+        **kwargs,
     ) -> (str, bool):
         """
         It prepares a given K8s cluster environment to run Juju bundles.
     ) -> (str, bool):
         """
         It prepares a given K8s cluster environment to run Juju bundles.
@@ -137,10 +115,14 @@ class K8sJujuConnector(K8sConnector):
         :param namespace: optional namespace to be used for juju. By default,
             'kube-system' will be used
         :param reuse_cluster_uuid: existing cluster uuid for reuse
         :param namespace: optional namespace to be used for juju. By default,
             'kube-system' will be used
         :param reuse_cluster_uuid: existing cluster uuid for reuse
+        :param: kwargs: Additional parameters
+            vca_id (str): VCA ID
+
         :return: uuid of the K8s cluster and True if connector has installed some
             software in the cluster
             (on error, an exception will be raised)
         """
         :return: uuid of the K8s cluster and True if connector has installed some
             software in the cluster
             (on error, an exception will be raised)
         """
+        libjuju = await self._get_libjuju(kwargs.get("vca_id"))
 
         cluster_uuid = reuse_cluster_uuid or str(uuid.uuid4())
 
 
         cluster_uuid = reuse_cluster_uuid or str(uuid.uuid4())
 
@@ -199,7 +181,7 @@ class K8sJujuConnector(K8sConnector):
             )
 
             default_storage_class = kubectl.get_default_storage_class()
             )
 
             default_storage_class = kubectl.get_default_storage_class()
-            await self.libjuju.add_k8s(
+            await libjuju.add_k8s(
                 name=cluster_uuid,
                 rbac_id=rbac_id,
                 token=token,
                 name=cluster_uuid,
                 rbac_id=rbac_id,
                 token=token,
@@ -248,25 +230,34 @@ class K8sJujuConnector(K8sConnector):
     """Reset"""
 
     async def reset(
     """Reset"""
 
     async def reset(
-        self, cluster_uuid: str, force: bool = False, uninstall_sw: bool = False
+        self,
+        cluster_uuid: str,
+        force: bool = False,
+        uninstall_sw: bool = False,
+        **kwargs,
     ) -> bool:
         """Reset a cluster
 
         Resets the Kubernetes cluster by removing the model that represents it.
 
         :param cluster_uuid str: The UUID of the cluster to reset
     ) -> bool:
         """Reset a cluster
 
         Resets the Kubernetes cluster by removing the model that represents it.
 
         :param cluster_uuid str: The UUID of the cluster to reset
+        :param force: Force reset
+        :param uninstall_sw: Boolean to uninstall sw
+        :param: kwargs: Additional parameters
+            vca_id (str): VCA ID
+
         :return: Returns True if successful or raises an exception.
         """
 
         try:
             self.log.debug("[reset] Removing k8s cloud")
         :return: Returns True if successful or raises an exception.
         """
 
         try:
             self.log.debug("[reset] Removing k8s cloud")
+            libjuju = await self._get_libjuju(kwargs.get("vca_id"))
 
 
-            cloud_creds = await self.libjuju.get_cloud_credentials(
-                cluster_uuid,
-                self._get_credential_name(cluster_uuid),
-            )
+            cloud = Cloud(cluster_uuid, self._get_credential_name(cluster_uuid))
+
+            cloud_creds = await libjuju.get_cloud_credentials(cloud)
 
 
-            await self.libjuju.remove_cloud(cluster_uuid)
+            await libjuju.remove_cloud(cluster_uuid)
 
             kubecfg = self.get_credentials(cluster_uuid=cluster_uuid)
 
 
             kubecfg = self.get_credentials(cluster_uuid=cluster_uuid)
 
@@ -310,6 +301,7 @@ class K8sJujuConnector(K8sConnector):
         db_dict: dict = None,
         kdu_name: str = None,
         namespace: str = None,
         db_dict: dict = None,
         kdu_name: str = None,
         namespace: str = None,
+        **kwargs,
     ) -> bool:
         """Install a bundle
 
     ) -> bool:
         """Install a bundle
 
@@ -323,9 +315,12 @@ class K8sJujuConnector(K8sConnector):
         :param params dict: Key-value pairs of instantiation parameters
         :param kdu_name: Name of the KDU instance to be installed
         :param namespace: K8s namespace to use for the KDU instance
         :param params dict: Key-value pairs of instantiation parameters
         :param kdu_name: Name of the KDU instance to be installed
         :param namespace: K8s namespace to use for the KDU instance
+        :param kwargs: Additional parameters
+            vca_id (str): VCA ID
 
         :return: If successful, returns ?
         """
 
         :return: If successful, returns ?
         """
+        libjuju = await self._get_libjuju(kwargs.get("vca_id"))
         bundle = kdu_model
 
         if not db_dict:
         bundle = kdu_model
 
         if not db_dict:
@@ -347,11 +342,8 @@ class K8sJujuConnector(K8sConnector):
 
         # Create the new model
         self.log.debug("Adding model: {}".format(kdu_instance))
 
         # Create the new model
         self.log.debug("Adding model: {}".format(kdu_instance))
-        await self.libjuju.add_model(
-            model_name=kdu_instance,
-            cloud_name=cluster_uuid,
-            credential_name=self._get_credential_name(cluster_uuid),
-        )
+        cloud = Cloud(cluster_uuid, self._get_credential_name(cluster_uuid))
+        await libjuju.add_model(kdu_instance, cloud)
 
         # if model:
         # TODO: Instantiation parameters
 
         # if model:
         # TODO: Instantiation parameters
@@ -370,12 +362,17 @@ class K8sJujuConnector(K8sConnector):
             previous_workdir = "/app/storage"
 
         self.log.debug("[install] deploying {}".format(bundle))
             previous_workdir = "/app/storage"
 
         self.log.debug("[install] deploying {}".format(bundle))
-        await self.libjuju.deploy(
+        await libjuju.deploy(
             bundle, model_name=kdu_instance, wait=atomic, timeout=timeout
         )
         os.chdir(previous_workdir)
         if self.on_update_db:
             bundle, model_name=kdu_instance, wait=atomic, timeout=timeout
         )
         os.chdir(previous_workdir)
         if self.on_update_db:
-            await self.on_update_db(cluster_uuid, kdu_instance, filter=db_dict["filter"])
+            await self.on_update_db(
+                cluster_uuid,
+                kdu_instance,
+                filter=db_dict["filter"],
+                vca_id=kwargs.get("vca_id")
+            )
         return True
 
     async def instances_list(self, cluster_uuid: str) -> list:
         return True
 
     async def instances_list(self, cluster_uuid: str) -> list:
@@ -442,18 +439,26 @@ class K8sJujuConnector(K8sConnector):
 
     """Deletion"""
 
 
     """Deletion"""
 
-    async def uninstall(self, cluster_uuid: str, kdu_instance: str) -> bool:
+    async def uninstall(
+        self,
+        cluster_uuid: str,
+        kdu_instance: str,
+        **kwargs,
+    ) -> bool:
         """Uninstall a KDU instance
 
         :param cluster_uuid str: The UUID of the cluster
         :param kdu_instance str: The unique name of the KDU instance
         """Uninstall a KDU instance
 
         :param cluster_uuid str: The UUID of the cluster
         :param kdu_instance str: The unique name of the KDU instance
+        :param kwargs: Additional parameters
+            vca_id (str): VCA ID
 
         :return: Returns True if successful, or raises an exception
         """
 
         self.log.debug("[uninstall] Destroying model")
 
         :return: Returns True if successful, or raises an exception
         """
 
         self.log.debug("[uninstall] Destroying model")
+        libjuju = await self._get_libjuju(kwargs.get("vca_id"))
 
 
-        await self.libjuju.destroy_model(kdu_instance, total_timeout=3600)
+        await libjuju.destroy_model(kdu_instance, total_timeout=3600)
 
         # self.log.debug("[uninstall] Model destroyed and disconnecting")
         # await controller.disconnect()
 
         # self.log.debug("[uninstall] Model destroyed and disconnecting")
         # await controller.disconnect()
@@ -472,6 +477,7 @@ class K8sJujuConnector(K8sConnector):
         timeout: float = 300,
         params: dict = None,
         db_dict: dict = None,
         timeout: float = 300,
         params: dict = None,
         db_dict: dict = None,
+        **kwargs,
     ) -> str:
         """Exec primitive (Juju action)
 
     ) -> str:
         """Exec primitive (Juju action)
 
@@ -480,10 +486,13 @@ class K8sJujuConnector(K8sConnector):
         :param primitive_name: Name of action that will be executed
         :param timeout: Timeout for action execution
         :param params: Dictionary of all the parameters needed for the action
         :param primitive_name: Name of action that will be executed
         :param timeout: Timeout for action execution
         :param params: Dictionary of all the parameters needed for the action
-        :db_dict: Dictionary for any additional data
+        :param db_dict: Dictionary for any additional data
+        :param kwargs: Additional parameters
+            vca_id (str): VCA ID
 
         :return: Returns the output of the action
         """
 
         :return: Returns the output of the action
         """
+        libjuju = await self._get_libjuju(kwargs.get("vca_id"))
 
         if not params or "application-name" not in params:
             raise K8sException(
 
         if not params or "application-name" not in params:
             raise K8sException(
@@ -496,10 +505,10 @@ class K8sJujuConnector(K8sConnector):
                 "kdu_instance: {}".format(kdu_instance)
             )
             application_name = params["application-name"]
                 "kdu_instance: {}".format(kdu_instance)
             )
             application_name = params["application-name"]
-            actions = await self.libjuju.get_actions(application_name, kdu_instance)
+            actions = await libjuju.get_actions(application_name, kdu_instance)
             if primitive_name not in actions:
                 raise K8sException("Primitive {} not found".format(primitive_name))
             if primitive_name not in actions:
                 raise K8sException("Primitive {} not found".format(primitive_name))
-            output, status = await self.libjuju.execute_action(
+            output, status = await libjuju.execute_action(
                 application_name, kdu_instance, primitive_name, **params
             )
 
                 application_name, kdu_instance, primitive_name, **params
             )
 
@@ -593,7 +602,8 @@ class K8sJujuConnector(K8sConnector):
         cluster_uuid: str,
         kdu_instance: str,
         complete_status: bool = False,
         cluster_uuid: str,
         kdu_instance: str,
         complete_status: bool = False,
-        yaml_format: bool = False
+        yaml_format: bool = False,
+        **kwargs,
     ) -> dict:
         """Get the status of the KDU
 
     ) -> dict:
         """Get the status of the KDU
 
@@ -603,13 +613,16 @@ class K8sJujuConnector(K8sConnector):
         :param kdu_instance str: The unique id of the KDU instance
         :param complete_status: To get the complete_status of the KDU
         :param yaml_format: To get the status in proper format for NSR record
         :param kdu_instance str: The unique id of the KDU instance
         :param complete_status: To get the complete_status of the KDU
         :param yaml_format: To get the status in proper format for NSR record
+        :param: kwargs: Additional parameters
+            vca_id (str): VCA ID
 
         :return: Returns a dictionary containing namespace, state, resources,
                  and deployment_time and returns complete_status if complete_status is True
         """
 
         :return: Returns a dictionary containing namespace, state, resources,
                  and deployment_time and returns complete_status if complete_status is True
         """
+        libjuju = await self._get_libjuju(kwargs.get("vca_id"))
         status = {}
 
         status = {}
 
-        model_status = await self.libjuju.get_model_status(kdu_instance)
+        model_status = await libjuju.get_model_status(kdu_instance)
 
         if not complete_status:
             for name in model_status.applications:
 
         if not complete_status:
             for name in model_status.applications:
@@ -623,28 +636,31 @@ class K8sJujuConnector(K8sConnector):
 
         return status
 
 
         return status
 
-    async def update_vca_status(self, vcastatus: dict, kdu_instance: str):
+    async def update_vca_status(self, vcastatus: dict, kdu_instance: str, **kwargs):
         """
         Add all configs, actions, executed actions of all applications in a model to vcastatus dict
 
         :param vcastatus dict: dict containing vcastatus
         :param kdu_instance str: The unique id of the KDU instance
         """
         Add all configs, actions, executed actions of all applications in a model to vcastatus dict
 
         :param vcastatus dict: dict containing vcastatus
         :param kdu_instance str: The unique id of the KDU instance
+        :param: kwargs: Additional parameters
+            vca_id (str): VCA ID
 
         :return: None
         """
 
         :return: None
         """
+        libjuju = await self._get_libjuju(kwargs.get("vca_id"))
         try:
             for model_name in vcastatus:
                 # Adding executed actions
                 vcastatus[model_name]["executedActions"] = \
         try:
             for model_name in vcastatus:
                 # Adding executed actions
                 vcastatus[model_name]["executedActions"] = \
-                    await self.libjuju.get_executed_actions(kdu_instance)
+                    await libjuju.get_executed_actions(kdu_instance)
 
                 for application in vcastatus[model_name]["applications"]:
                     # Adding application actions
                     vcastatus[model_name]["applications"][application]["actions"] = \
 
                 for application in vcastatus[model_name]["applications"]:
                     # Adding application actions
                     vcastatus[model_name]["applications"][application]["actions"] = \
-                        await self.libjuju.get_actions(application, kdu_instance)
+                        await libjuju.get_actions(application, kdu_instance)
                     # Adding application configs
                     vcastatus[model_name]["applications"][application]["configs"] = \
                     # Adding application configs
                     vcastatus[model_name]["applications"][application]["configs"] = \
-                        await self.libjuju.get_application_configs(kdu_instance, application)
+                        await libjuju.get_application_configs(kdu_instance, application)
 
         except Exception as e:
             self.log.debug("Error in updating vca status: {}".format(str(e)))
 
         except Exception as e:
             self.log.debug("Error in updating vca status: {}".format(str(e)))
@@ -853,3 +869,28 @@ class K8sJujuConnector(K8sConnector):
         else:
             kdu_instance = db_dict["filter"]["_id"]
         return kdu_instance
         else:
             kdu_instance = db_dict["filter"]["_id"]
         return kdu_instance
+
+    async def _get_libjuju(self, vca_id: str = None) -> Libjuju:
+        """
+        Get libjuju object
+
+        :param: vca_id: VCA ID
+                        If None, get a libjuju object with a Connection to the default VCA
+                        Else, geta libjuju object with a Connection to the specified VCA
+        """
+        if not vca_id:
+            while self.loading_libjuju.locked():
+                await asyncio.sleep(0.1)
+            if not self.libjuju:
+                async with self.loading_libjuju:
+                    vca_connection = await get_connection(self._store)
+                    self.libjuju = Libjuju(vca_connection, loop=self.loop, log=self.log)
+            return self.libjuju
+        else:
+            vca_connection = await get_connection(self._store, vca_id)
+            return Libjuju(
+                vca_connection,
+                loop=self.loop,
+                log=self.log,
+                n2vc=self,
+            )
index eb0fa72..a6fd8fe 100644 (file)
@@ -14,6 +14,7 @@
 
 import asyncio
 import logging
 
 import asyncio
 import logging
+import typing
 
 import time
 
 
 import time
 
@@ -32,7 +33,6 @@ from juju.controller import Controller
 from juju.client import client
 from juju import tag
 
 from juju.client import client
 from juju import tag
 
-from n2vc.config import ModelConfig
 from n2vc.juju_watcher import JujuModelWatcher
 from n2vc.provisioner import AsyncSSHProvisioner
 from n2vc.n2vc_conn import N2VCConnector
 from n2vc.juju_watcher import JujuModelWatcher
 from n2vc.provisioner import AsyncSSHProvisioner
 from n2vc.n2vc_conn import N2VCConnector
@@ -44,11 +44,13 @@ from n2vc.exceptions import (
     JujuControllerFailedConnecting,
     JujuApplicationExists,
     JujuInvalidK8sConfiguration,
     JujuControllerFailedConnecting,
     JujuApplicationExists,
     JujuInvalidK8sConfiguration,
-    JujuError
+    JujuError,
 )
 )
-from n2vc.utils import DB_DATA
-from osm_common.dbbase import DbException
+from n2vc.vca.cloud import Cloud as VcaCloud
+from n2vc.vca.connection import Connection
 from kubernetes.client.configuration import Configuration
 from kubernetes.client.configuration import Configuration
+from retrying_async import retry
+
 
 RBAC_LABEL_KEY_NAME = "rbac-id"
 
 
 RBAC_LABEL_KEY_NAME = "rbac-id"
 
@@ -56,63 +58,35 @@ RBAC_LABEL_KEY_NAME = "rbac-id"
 class Libjuju:
     def __init__(
         self,
 class Libjuju:
     def __init__(
         self,
-        endpoint: str,
-        api_proxy: str,
-        username: str,
-        password: str,
-        cacert: str,
+        vca_connection: Connection,
         loop: asyncio.AbstractEventLoop = None,
         log: logging.Logger = None,
         loop: asyncio.AbstractEventLoop = None,
         log: logging.Logger = None,
-        db: dict = None,
         n2vc: N2VCConnector = None,
         n2vc: N2VCConnector = None,
-        model_config: ModelConfig = {},
     ):
         """
         Constructor
 
     ):
         """
         Constructor
 
-        :param: endpoint:               Endpoint of the juju controller (host:port)
-        :param: api_proxy:              Endpoint of the juju controller - Reachable from the VNFs
-        :param: username:               Juju username
-        :param: password:               Juju password
-        :param: cacert:                 Juju CA Certificate
+        :param: vca_connection:         n2vc.vca.connection object
         :param: loop:                   Asyncio loop
         :param: log:                    Logger
         :param: loop:                   Asyncio loop
         :param: log:                    Logger
-        :param: db:                     DB object
         :param: n2vc:                   N2VC object
         :param: n2vc:                   N2VC object
-        :param: apt_mirror:             APT Mirror
-        :param: enable_os_upgrade:      Enable OS Upgrade
         """
 
         self.log = log or logging.getLogger("Libjuju")
         """
 
         self.log = log or logging.getLogger("Libjuju")
-        self.db = db
-        db_endpoints = self._get_api_endpoints_db()
-        self.endpoints = None
-        if (db_endpoints and endpoint not in db_endpoints) or not db_endpoints:
-            self.endpoints = [endpoint]
-            self._update_api_endpoints_db(self.endpoints)
-        else:
-            self.endpoints = db_endpoints
-        self.api_proxy = api_proxy
-        self.username = username
-        self.password = password
-        self.cacert = cacert
-        self.loop = loop or asyncio.get_event_loop()
         self.n2vc = n2vc
         self.n2vc = n2vc
+        self.vca_connection = vca_connection
 
 
-        # Generate config for models
-        self.model_config = model_config
-
+        self.loop = loop or asyncio.get_event_loop()
         self.loop.set_exception_handler(self.handle_exception)
         self.creating_model = asyncio.Lock(loop=self.loop)
 
         self.loop.set_exception_handler(self.handle_exception)
         self.creating_model = asyncio.Lock(loop=self.loop)
 
-        self.log.debug("Libjuju initialized!")
-
-        self.health_check_task = self._create_health_check_task()
+        if self.vca_connection.is_default:
+            self.health_check_task = self._create_health_check_task()
 
     def _create_health_check_task(self):
         return self.loop.create_task(self.health_check())
 
 
     def _create_health_check_task(self):
         return self.loop.create_task(self.health_check())
 
-    async def get_controller(self, timeout: float = 15.0) -> Controller:
+    async def get_controller(self, timeout: float = 60.0) -> Controller:
         """
         Get controller
 
         """
         Get controller
 
@@ -123,23 +97,27 @@ class Libjuju:
             controller = Controller(loop=self.loop)
             await asyncio.wait_for(
                 controller.connect(
             controller = Controller(loop=self.loop)
             await asyncio.wait_for(
                 controller.connect(
-                    endpoint=self.endpoints,
-                    username=self.username,
-                    password=self.password,
-                    cacert=self.cacert,
+                    endpoint=self.vca_connection.data.endpoints,
+                    username=self.vca_connection.data.user,
+                    password=self.vca_connection.data.secret,
+                    cacert=self.vca_connection.data.cacert,
                 ),
                 timeout=timeout,
             )
                 ),
                 timeout=timeout,
             )
-            endpoints = await controller.api_endpoints
-            if self.endpoints != endpoints:
-                self.endpoints = endpoints
-                self._update_api_endpoints_db(self.endpoints)
+            if self.vca_connection.is_default:
+                endpoints = await controller.api_endpoints
+                if not all(
+                    endpoint in self.vca_connection.endpoints for endpoint in endpoints
+                ):
+                    await self.vca_connection.update_endpoints(endpoints)
             return controller
         except asyncio.CancelledError as e:
             raise e
         except Exception as e:
             self.log.error(
             return controller
         except asyncio.CancelledError as e:
             raise e
         except Exception as e:
             self.log.error(
-                "Failed connecting to controller: {}...".format(self.endpoints)
+                "Failed connecting to controller: {}... {}".format(
+                    self.vca_connection.data.endpoints, e
+                )
             )
             if controller:
                 await self.disconnect_controller(controller)
             )
             if controller:
                 await self.disconnect_controller(controller)
@@ -168,14 +146,13 @@ class Libjuju:
         if controller:
             await controller.disconnect()
 
         if controller:
             await controller.disconnect()
 
-    async def add_model(self, model_name: str, cloud_name: str, credential_name=None):
+    @retry(attempts=3, delay=5, timeout=None)
+    async def add_model(self, model_name: str, cloud: VcaCloud):
         """
         Create model
 
         :param: model_name: Model name
         """
         Create model
 
         :param: model_name: Model name
-        :param: cloud_name: Cloud name
-        :param: credential_name: Credential name to use for adding the model
-                                 If not specified, same name as the cloud will be used.
+        :param: cloud: Cloud object
         """
 
         # Get controller
         """
 
         # Get controller
@@ -193,10 +170,15 @@ class Libjuju:
                 self.log.debug("Creating model {}".format(model_name))
                 model = await controller.add_model(
                     model_name,
                 self.log.debug("Creating model {}".format(model_name))
                 model = await controller.add_model(
                     model_name,
-                    config=self.model_config,
-                    cloud_name=cloud_name,
-                    credential_name=credential_name or cloud_name,
+                    config=self.vca_connection.data.model_config,
+                    cloud_name=cloud.name,
+                    credential_name=cloud.credential_name,
                 )
                 )
+        except JujuAPIError as e:
+            if "already exists" in e.message:
+                pass
+            else:
+                raise e
         finally:
             if model:
                 await self.disconnect_model(model)
         finally:
             if model:
                 await self.disconnect_model(model)
@@ -221,25 +203,35 @@ class Libjuju:
                 actions.update(application_actions)
             # Get status of all actions
             for application_action in actions:
                 actions.update(application_actions)
             # Get status of all actions
             for application_action in actions:
-                app_action_status_list = await model.get_action_status(name=application_action)
+                app_action_status_list = await model.get_action_status(
+                    name=application_action
+                )
                 for action_id, action_status in app_action_status_list.items():
                 for action_id, action_status in app_action_status_list.items():
-                    executed_action = {"id": action_id, "action": application_action,
-                                       "status": action_status}
+                    executed_action = {
+                        "id": action_id,
+                        "action": application_action,
+                        "status": action_status,
+                    }
                     # Get action output by id
                     action_status = await model.get_action_output(executed_action["id"])
                     for k, v in action_status.items():
                         executed_action[k] = v
                     executed_actions.append(executed_action)
         except Exception as e:
                     # Get action output by id
                     action_status = await model.get_action_output(executed_action["id"])
                     for k, v in action_status.items():
                         executed_action[k] = v
                     executed_actions.append(executed_action)
         except Exception as e:
-            raise JujuError("Error in getting executed actions for model: {}. Error: {}"
-                            .format(model_name, str(e)))
+            raise JujuError(
+                "Error in getting executed actions for model: {}. Error: {}".format(
+                    model_name, str(e)
+                )
+            )
         finally:
             if model:
                 await self.disconnect_model(model)
             await self.disconnect_controller(controller)
         return executed_actions
 
         finally:
             if model:
                 await self.disconnect_model(model)
             await self.disconnect_controller(controller)
         return executed_actions
 
-    async def get_application_configs(self, model_name: str, application_name: str) -> dict:
+    async def get_application_configs(
+        self, model_name: str, application_name: str
+    ) -> dict:
         """
         Get available configs for an application.
 
         """
         Get available configs for an application.
 
@@ -253,20 +245,24 @@ class Libjuju:
         controller = await self.get_controller()
         try:
             model = await self.get_model(controller, model_name)
         controller = await self.get_controller()
         try:
             model = await self.get_model(controller, model_name)
-            application = self._get_application(model, application_name=application_name)
+            application = self._get_application(
+                model, application_name=application_name
+            )
             application_configs = await application.get_config()
         except Exception as e:
             application_configs = await application.get_config()
         except Exception as e:
-            raise JujuError("Error in getting configs for application: {} in model: {}. Error: {}"
-                            .format(application_name, model_name, str(e)))
+            raise JujuError(
+                "Error in getting configs for application: {} in model: {}. Error: {}".format(
+                    application_name, model_name, str(e)
+                )
+            )
         finally:
             if model:
                 await self.disconnect_model(model)
             await self.disconnect_controller(controller)
         return application_configs
 
         finally:
             if model:
                 await self.disconnect_model(model)
             await self.disconnect_controller(controller)
         return application_configs
 
-    async def get_model(
-        self, controller: Controller, model_name: str, id=None
-    ) -> Model:
+    @retry(attempts=3, delay=5)
+    async def get_model(self, controller: Controller, model_name: str) -> Model:
         """
         Get model from controller
 
         """
         Get model from controller
 
@@ -277,9 +273,7 @@ class Libjuju:
         """
         return await controller.get_model(model_name)
 
         """
         return await controller.get_model(model_name)
 
-    async def model_exists(
-        self, model_name: str, controller: Controller = None
-    ) -> bool:
+    async def model_exists(self, model_name: str, controller: Controller = None) -> bool:
         """
         Check if model exists
 
         """
         Check if model exists
 
@@ -421,6 +415,7 @@ class Libjuju:
                         total_timeout=total_timeout,
                         db_dict=db_dict,
                         n2vc=self.n2vc,
                         total_timeout=total_timeout,
                         db_dict=db_dict,
                         n2vc=self.n2vc,
+                        vca_id=self.vca_connection._vca_id,
                     )
         finally:
             await self.disconnect_model(model)
                     )
         finally:
             await self.disconnect_model(model)
@@ -502,7 +497,7 @@ class Libjuju:
                     connection=connection,
                     nonce=params.nonce,
                     machine_id=machine_id,
                     connection=connection,
                     nonce=params.nonce,
                     machine_id=machine_id,
-                    proxy=self.api_proxy,
+                    proxy=self.vca_connection.data.api_proxy,
                     series=params.series,
                 )
             )
                     series=params.series,
                 )
             )
@@ -533,6 +528,7 @@ class Libjuju:
                 total_timeout=total_timeout,
                 db_dict=db_dict,
                 n2vc=self.n2vc,
                 total_timeout=total_timeout,
                 db_dict=db_dict,
                 n2vc=self.n2vc,
+                vca_id=self.vca_connection._vca_id,
             )
         except Exception as e:
             raise e
             )
         except Exception as e:
             raise e
@@ -648,6 +644,7 @@ class Libjuju:
                     total_timeout=total_timeout,
                     db_dict=db_dict,
                     n2vc=self.n2vc,
                     total_timeout=total_timeout,
                     db_dict=db_dict,
                     n2vc=self.n2vc,
+                    vca_id=self.vca_connection._vca_id,
                 )
                 self.log.debug(
                     "Application {} is ready in model {}".format(
                 )
                 self.log.debug(
                     "Application {} is ready in model {}".format(
@@ -720,19 +717,7 @@ class Libjuju:
             #   because the leader elected hook has not been triggered yet.
             #   Therefore, we are doing some retries. If it happens again,
             #   re-open bug 1236
             #   because the leader elected hook has not been triggered yet.
             #   Therefore, we are doing some retries. If it happens again,
             #   re-open bug 1236
-            attempts = 3
-            time_between_retries = 10
-            unit = None
-            for _ in range(attempts):
-                unit = await self._get_leader_unit(application)
-                if unit is None:
-                    await asyncio.sleep(time_between_retries)
-                else:
-                    break
-            if unit is None:
-                raise JujuLeaderUnitNotFound(
-                    "Cannot execute action: leader unit not found"
-                )
+            unit = await self._get_leader_unit(application)
 
             actions = await application.get_actions()
 
 
             actions = await application.get_actions()
 
@@ -755,6 +740,7 @@ class Libjuju:
                 total_timeout=total_timeout,
                 db_dict=db_dict,
                 n2vc=self.n2vc,
                 total_timeout=total_timeout,
                 db_dict=db_dict,
                 n2vc=self.n2vc,
+                vca_id=self.vca_connection._vca_id,
             )
 
             output = await model.get_action_output(action_uuid=action.entity_id)
             )
 
             output = await model.get_action_output(action_uuid=action.entity_id)
@@ -1041,57 +1027,6 @@ class Libjuju:
                     await self.disconnect_model(model)
                 await self.disconnect_controller(controller)
 
                     await self.disconnect_model(model)
                 await self.disconnect_controller(controller)
 
-    def _get_api_endpoints_db(self) -> [str]:
-        """
-        Get API Endpoints from DB
-
-        :return: List of API endpoints
-        """
-        self.log.debug("Getting endpoints from database")
-
-        juju_info = self.db.get_one(
-            DB_DATA.api_endpoints.table,
-            q_filter=DB_DATA.api_endpoints.filter,
-            fail_on_empty=False,
-        )
-        if juju_info and DB_DATA.api_endpoints.key in juju_info:
-            return juju_info[DB_DATA.api_endpoints.key]
-
-    def _update_api_endpoints_db(self, endpoints: [str]):
-        """
-        Update API endpoints in Database
-
-        :param: List of endpoints
-        """
-        self.log.debug("Saving endpoints {} in database".format(endpoints))
-
-        juju_info = self.db.get_one(
-            DB_DATA.api_endpoints.table,
-            q_filter=DB_DATA.api_endpoints.filter,
-            fail_on_empty=False,
-        )
-        # If it doesn't, then create it
-        if not juju_info:
-            try:
-                self.db.create(
-                    DB_DATA.api_endpoints.table,
-                    DB_DATA.api_endpoints.filter,
-                )
-            except DbException as e:
-                # Racing condition: check if another N2VC worker has created it
-                juju_info = self.db.get_one(
-                    DB_DATA.api_endpoints.table,
-                    q_filter=DB_DATA.api_endpoints.filter,
-                    fail_on_empty=False,
-                )
-                if not juju_info:
-                    raise e
-        self.db.set_one(
-            DB_DATA.api_endpoints.table,
-            DB_DATA.api_endpoints.filter,
-            {DB_DATA.api_endpoints.key: endpoints},
-        )
-
     def handle_exception(self, loop, context):
         # All unhandled exceptions by libjuju are handled here.
         pass
     def handle_exception(self, loop, context):
         # All unhandled exceptions by libjuju are handled here.
         pass
@@ -1283,19 +1218,32 @@ class Libjuju:
         finally:
             await self.disconnect_controller(controller)
 
         finally:
             await self.disconnect_controller(controller)
 
+    @retry(attempts=20, delay=5, fallback=JujuLeaderUnitNotFound())
     async def _get_leader_unit(self, application: Application) -> Unit:
         unit = None
         for u in application.units:
             if await u.is_leader_from_status():
                 unit = u
                 break
     async def _get_leader_unit(self, application: Application) -> Unit:
         unit = None
         for u in application.units:
             if await u.is_leader_from_status():
                 unit = u
                 break
+        if not unit:
+            raise Exception()
         return unit
 
         return unit
 
-    async def get_cloud_credentials(self, cloud_name: str, credential_name: str):
+    async def get_cloud_credentials(self, cloud: Cloud) -> typing.List:
+        """
+        Get cloud credentials
+
+        :param: cloud: Cloud object. The returned credentials will be from this cloud.
+
+        :return: List of credentials object associated to the specified cloud
+
+        """
         controller = await self.get_controller()
         try:
             facade = client.CloudFacade.from_connection(controller.connection())
         controller = await self.get_controller()
         try:
             facade = client.CloudFacade.from_connection(controller.connection())
-            cloud_cred_tag = tag.credential(cloud_name, self.username, credential_name)
+            cloud_cred_tag = tag.credential(
+                cloud.name, self.vca_connection.data.user, cloud.credential_name
+            )
             params = [client.Entity(cloud_cred_tag)]
             return (await facade.Credential(params)).results
         finally:
             params = [client.Entity(cloud_cred_tag)]
             return (await facade.Credential(params)).results
         finally:
index 12704a3..bfdf460 100644 (file)
@@ -55,9 +55,6 @@ class N2VCConnector(abc.ABC, Loggable):
         fs: object,
         log: object,
         loop: object,
         fs: object,
         log: object,
         loop: object,
-        url: str,
-        username: str,
-        vca_config: dict,
         on_update_db=None,
         **kwargs,
     ):
         on_update_db=None,
         **kwargs,
     ):
@@ -68,14 +65,6 @@ class N2VCConnector(abc.ABC, Loggable):
             FsBase)
         :param object log: the logging object to log to
         :param object loop: the loop to use for asyncio (default current thread loop)
             FsBase)
         :param object log: the logging object to log to
         :param object loop: the loop to use for asyncio (default current thread loop)
-        :param str url: a string that how to connect to the VCA (if needed, IP and port
-            can be obtained from there)
-        :param str username: the username to authenticate with VCA
-        :param dict vca_config: Additional parameters for the specific VCA. For example,
-            for juju it will contain:
-            secret: The password to authenticate with
-            public_key: The contents of the juju public SSH key
-            ca_cert str: The CA certificate used to authenticate
         :param on_update_db: callback called when n2vc connector updates database.
             Received arguments:
             table: e.g. "nsrs"
         :param on_update_db: callback called when n2vc connector updates database.
             Received arguments:
             table: e.g. "nsrs"
@@ -93,26 +82,10 @@ class N2VCConnector(abc.ABC, Loggable):
         if fs is None:
             raise N2VCBadArgumentsException("Argument fs is mandatory", ["fs"])
 
         if fs is None:
             raise N2VCBadArgumentsException("Argument fs is mandatory", ["fs"])
 
-        self.log.info(
-            "url={}, username={}, vca_config={}".format(
-                url,
-                username,
-                {
-                    k: v
-                    for k, v in vca_config.items()
-                    if k
-                    not in ("host", "port", "user", "secret", "public_key", "ca_cert")
-                },
-            )
-        )
-
         # store arguments into self
         self.db = db
         self.fs = fs
         self.loop = loop or asyncio.get_event_loop()
         # store arguments into self
         self.db = db
         self.fs = fs
         self.loop = loop or asyncio.get_event_loop()
-        self.url = url
-        self.username = username
-        self.vca_config = vca_config
         self.on_update_db = on_update_db
 
         # generate private/public key-pair
         self.on_update_db = on_update_db
 
         # generate private/public key-pair
@@ -443,7 +416,18 @@ class N2VCConnector(abc.ABC, Loggable):
         detailed_status: str,
         vca_status: str,
         entity_type: str,
         detailed_status: str,
         vca_status: str,
         entity_type: str,
+        vca_id: str = None,
     ):
     ):
+        """
+        Write application status to database
+
+        :param: db_dict: DB dictionary
+        :param: status: Status of the application
+        :param: detailed_status: Detailed status
+        :param: vca_status: VCA status
+        :param: entity_type: Entity type ("application", "machine, and "action")
+        :param: vca_id: Id of the VCA. If None, the default VCA will be used.
+        """
         if not db_dict:
             self.log.debug("No db_dict => No database write")
             return
         if not db_dict:
             self.log.debug("No db_dict => No database write")
             return
@@ -477,10 +461,10 @@ class N2VCConnector(abc.ABC, Loggable):
             if self.on_update_db:
                 if asyncio.iscoroutinefunction(self.on_update_db):
                     await self.on_update_db(
             if self.on_update_db:
                 if asyncio.iscoroutinefunction(self.on_update_db):
                     await self.on_update_db(
-                        the_table, the_filter, the_path, update_dict
+                        the_table, the_filter, the_path, update_dict, vca_id=vca_id
                     )
                 else:
                     )
                 else:
-                    self.on_update_db(the_table, the_filter, the_path, update_dict)
+                    self.on_update_db(the_table, the_filter, the_path, update_dict, vca_id=vca_id)
 
         except DbException as e:
             if e.http_code == HTTPStatus.NOT_FOUND:
 
         except DbException as e:
             if e.http_code == HTTPStatus.NOT_FOUND:
index c09c263..99661f5 100644 (file)
@@ -22,9 +22,8 @@
 
 import asyncio
 import logging
 
 import asyncio
 import logging
-import os
 
 
-from n2vc.config import ModelConfig
+from n2vc.config import EnvironConfig
 from n2vc.exceptions import (
     N2VCBadArgumentsException,
     N2VCException,
 from n2vc.exceptions import (
     N2VCBadArgumentsException,
     N2VCException,
@@ -32,23 +31,24 @@ from n2vc.exceptions import (
     N2VCExecutionException,
     # N2VCNotFound,
     MethodNotImplemented,
     N2VCExecutionException,
     # N2VCNotFound,
     MethodNotImplemented,
-    JujuK8sProxycharmNotSupported,
 )
 from n2vc.n2vc_conn import N2VCConnector
 from n2vc.n2vc_conn import obj_to_dict, obj_to_yaml
 from n2vc.libjuju import Libjuju
 )
 from n2vc.n2vc_conn import N2VCConnector
 from n2vc.n2vc_conn import obj_to_dict, obj_to_yaml
 from n2vc.libjuju import Libjuju
-from n2vc.utils import base64_to_cacert
+from n2vc.store import MotorStore
+from n2vc.vca.connection import get_connection
 
 
 class N2VCJujuConnector(N2VCConnector):
 
     """
 
 
 class N2VCJujuConnector(N2VCConnector):
 
     """
-####################################################################################
-################################### P U B L I C ####################################
-####################################################################################
+    ####################################################################################
+    ################################### P U B L I C ####################################
+    ####################################################################################
     """
 
     BUILT_IN_CLOUDS = ["localhost", "microk8s"]
     """
 
     BUILT_IN_CLOUDS = ["localhost", "microk8s"]
+    libjuju = None
 
     def __init__(
         self,
 
     def __init__(
         self,
@@ -56,12 +56,16 @@ class N2VCJujuConnector(N2VCConnector):
         fs: object,
         log: object = None,
         loop: object = None,
         fs: object,
         log: object = None,
         loop: object = None,
-        url: str = "127.0.0.1:17070",
-        username: str = "admin",
-        vca_config: dict = None,
         on_update_db=None,
     ):
         on_update_db=None,
     ):
-        """Initialize juju N2VC connector
+        """
+        Constructor
+
+        :param: db: Database object from osm_common
+        :param: fs: Filesystem object from osm_common
+        :param: log: Logger
+        :param: loop: Asyncio loop
+        :param: on_update_db: Callback function to be called for updating the database.
         """
 
         # parent class constructor
         """
 
         # parent class constructor
@@ -71,9 +75,6 @@ class N2VCJujuConnector(N2VCConnector):
             fs=fs,
             log=log,
             loop=loop,
             fs=fs,
             log=log,
             loop=loop,
-            url=url,
-            username=username,
-            vca_config=vca_config,
             on_update_db=on_update_db,
         )
 
             on_update_db=on_update_db,
         )
 
@@ -84,113 +85,29 @@ class N2VCJujuConnector(N2VCConnector):
 
         self.log.info("Initializing N2VC juju connector...")
 
 
         self.log.info("Initializing N2VC juju connector...")
 
-        """
-        ##############################################################
-        # check arguments
-        ##############################################################
-        """
-
-        # juju URL
-        if url is None:
-            raise N2VCBadArgumentsException("Argument url is mandatory", ["url"])
-        url_parts = url.split(":")
-        if len(url_parts) != 2:
-            raise N2VCBadArgumentsException(
-                "Argument url: bad format (localhost:port) -> {}".format(url), ["url"]
-            )
-        self.hostname = url_parts[0]
-        try:
-            self.port = int(url_parts[1])
-        except ValueError:
-            raise N2VCBadArgumentsException(
-                "url port must be a number -> {}".format(url), ["url"]
-            )
-
-        # juju USERNAME
-        if username is None:
-            raise N2VCBadArgumentsException(
-                "Argument username is mandatory", ["username"]
-            )
-
-        # juju CONFIGURATION
-        if vca_config is None:
-            raise N2VCBadArgumentsException(
-                "Argument vca_config is mandatory", ["vca_config"]
-            )
-
-        if "secret" in vca_config:
-            self.secret = vca_config["secret"]
-        else:
-            raise N2VCBadArgumentsException(
-                "Argument vca_config.secret is mandatory", ["vca_config.secret"]
-            )
-
-        # pubkey of juju client in osm machine: ~/.local/share/juju/ssh/juju_id_rsa.pub
-        # if exists, it will be written in lcm container: _create_juju_public_key()
-        if "public_key" in vca_config:
-            self.public_key = vca_config["public_key"]
-        else:
-            self.public_key = None
-
-        # TODO: Verify ca_cert is valid before using. VCA will crash
-        # if the ca_cert isn't formatted correctly.
-
-        self.ca_cert = vca_config.get("ca_cert")
-        if self.ca_cert:
-            self.ca_cert = base64_to_cacert(vca_config["ca_cert"])
-
-        if "api_proxy" in vca_config and vca_config["api_proxy"] != "":
-            self.api_proxy = vca_config["api_proxy"]
-            self.log.debug(
-                "api_proxy for native charms configured: {}".format(self.api_proxy)
-            )
-        else:
-            self.warning(
-                "api_proxy is not configured"
-            )
-            self.api_proxy = None
-
-        model_config = ModelConfig(vca_config)
-
-        self.cloud = vca_config.get('cloud')
-        self.k8s_cloud = None
-        if "k8s_cloud" in vca_config:
-            self.k8s_cloud = vca_config.get("k8s_cloud")
-        self.log.debug('Arguments have been checked')
-
-        # juju data
-        self.controller = None  # it will be filled when connect to juju
-        self.juju_models = {}  # model objects for every model_name
-        self.juju_observers = {}  # model observers for every model_name
-        self._connecting = (
-            False  # while connecting to juju (to avoid duplicate connections)
-        )
-        self._authenticated = (
-            False  # it will be True when juju connection be stablished
-        )
-        self._creating_model = False  # True during model creation
-        self.libjuju = Libjuju(
-            endpoint=self.url,
-            api_proxy=self.api_proxy,
-            username=self.username,
-            password=self.secret,
-            cacert=self.ca_cert,
-            loop=self.loop,
-            log=self.log,
-            db=self.db,
-            n2vc=self,
-            model_config=model_config,
-        )
-
-        # create juju pub key file in lcm container at
-        # ./local/share/juju/ssh/juju_id_rsa.pub
-        self._create_juju_public_key()
+        db_uri = EnvironConfig(prefixes=["OSMLCM_", "OSMMON_"]).get("database_uri")
+        self._store = MotorStore(db_uri)
+        self.loading_libjuju = asyncio.Lock(loop=self.loop)
 
         self.log.info("N2VC juju connector initialized")
 
 
         self.log.info("N2VC juju connector initialized")
 
-    async def get_status(self, namespace: str, yaml_format: bool = True):
+    async def get_status(
+        self, namespace: str, yaml_format: bool = True, vca_id: str = None
+    ):
+        """
+        Get status from all juju models from a VCA
+
+        :param namespace: we obtain ns from namespace
+        :param yaml_format: returns a yaml string
+        :param: vca_id: VCA ID from which the status will be retrieved.
+        """
+        # TODO: Review where is this function used. It is not optimal at all to get the status
+        #       from all the juju models of a particular VCA. Additionally, these models might
+        #       not have been deployed by OSM, in that case we are getting information from
+        #       deployments outside of OSM's scope.
 
         # self.log.info('Getting NS status. namespace: {}'.format(namespace))
 
         # self.log.info('Getting NS status. namespace: {}'.format(namespace))
+        libjuju = await self._get_libjuju(vca_id)
 
         _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components(
             namespace=namespace
 
         _nsi_id, ns_id, _vnf_id, _vdu_id, _vdu_count = self._get_namespace_components(
             namespace=namespace
@@ -203,33 +120,38 @@ class N2VCJujuConnector(N2VCConnector):
             raise N2VCBadArgumentsException(msg, ["namespace"])
 
         status = {}
             raise N2VCBadArgumentsException(msg, ["namespace"])
 
         status = {}
-        models = await self.libjuju.list_models(contains=ns_id)
+        models = await libjuju.list_models(contains=ns_id)
 
         for m in models:
 
         for m in models:
-            status[m] = await self.libjuju.get_model_status(m)
+            status[m] = await libjuju.get_model_status(m)
+
         if yaml_format:
             return obj_to_yaml(status)
         else:
             return obj_to_dict(status)
 
         if yaml_format:
             return obj_to_yaml(status)
         else:
             return obj_to_dict(status)
 
-    async def update_vca_status(self, vcastatus: dict):
+    async def update_vca_status(self, vcastatus: dict, vca_id: str = None):
         """
         Add all configs, actions, executed actions of all applications in a model to vcastatus dict.
         """
         Add all configs, actions, executed actions of all applications in a model to vcastatus dict.
+
         :param vcastatus: dict containing vcaStatus
         :param vcastatus: dict containing vcaStatus
+        :param: vca_id: VCA ID
+
         :return: None
         """
         try:
         :return: None
         """
         try:
+            libjuju = await self._get_libjuju(vca_id)
             for model_name in vcastatus:
                 # Adding executed actions
                 vcastatus[model_name]["executedActions"] = \
             for model_name in vcastatus:
                 # Adding executed actions
                 vcastatus[model_name]["executedActions"] = \
-                    await self.libjuju.get_executed_actions(model_name)
+                    await libjuju.get_executed_actions(model_name)
                 for application in vcastatus[model_name]["applications"]:
                     # Adding application actions
                     vcastatus[model_name]["applications"][application]["actions"] = \
                 for application in vcastatus[model_name]["applications"]:
                     # Adding application actions
                     vcastatus[model_name]["applications"][application]["actions"] = \
-                        await self.libjuju.get_actions(application, model_name)
+                        await libjuju.get_actions(application, model_name)
                     # Adding application configs
                     vcastatus[model_name]["applications"][application]["configs"] = \
                     # Adding application configs
                     vcastatus[model_name]["applications"][application]["configs"] = \
-                        await self.libjuju.get_application_configs(model_name, application)
+                        await libjuju.get_application_configs(model_name, application)
         except Exception as e:
             self.log.debug("Error in updating vca status: {}".format(str(e)))
 
         except Exception as e:
             self.log.debug("Error in updating vca status: {}".format(str(e)))
 
@@ -240,15 +162,34 @@ class N2VCJujuConnector(N2VCConnector):
         reuse_ee_id: str = None,
         progress_timeout: float = None,
         total_timeout: float = None,
         reuse_ee_id: str = None,
         progress_timeout: float = None,
         total_timeout: float = None,
-        cloud_name: str = None,
-        credential_name: str = None,
+        vca_id: str = None,
     ) -> (str, dict):
     ) -> (str, dict):
+        """
+        Create an Execution Environment. Returns when it is created or raises an
+        exception on failing
+
+        :param: namespace: Contains a dot separate string.
+                    LCM will use: [<nsi-id>].<ns-id>.<vnf-id>.<vdu-id>[-<count>]
+        :param: db_dict: where to write to database when the status changes.
+            It contains a dictionary with {collection: str, filter: {},  path: str},
+                e.g. {collection: "nsrs", filter: {_id: <nsd-id>, path:
+                "_admin.deployed.VCA.3"}
+        :param: reuse_ee_id: ee id from an older execution. It allows us to reuse an
+                             older environment
+        :param: progress_timeout: Progress timeout
+        :param: total_timeout: Total timeout
+        :param: vca_id: VCA ID
+
+        :returns: id of the new execution environment and credentials for it
+                  (credentials can contains hostname, username, etc depending on underlying cloud)
+        """
 
         self.log.info(
             "Creating execution environment. namespace: {}, reuse_ee_id: {}".format(
                 namespace, reuse_ee_id
             )
         )
 
         self.log.info(
             "Creating execution environment. namespace: {}, reuse_ee_id: {}".format(
                 namespace, reuse_ee_id
             )
         )
+        libjuju = await self._get_libjuju(vca_id)
 
         machine_id = None
         if reuse_ee_id:
 
         machine_id = None
         if reuse_ee_id:
@@ -276,15 +217,12 @@ class N2VCJujuConnector(N2VCConnector):
 
         # create or reuse a new juju machine
         try:
 
         # create or reuse a new juju machine
         try:
-            if not await self.libjuju.model_exists(model_name):
-                cloud = cloud_name or self.cloud
-                credential = credential_name or cloud_name if cloud_name else self.cloud
-                await self.libjuju.add_model(
+            if not await libjuju.model_exists(model_name):
+                await libjuju.add_model(
                     model_name,
                     model_name,
-                    cloud_name=cloud,
-                    credential_name=credential
+                    libjuju.vca_connection.lxd_cloud,
                 )
                 )
-            machine, new = await self.libjuju.create_machine(
+            machine, new = await libjuju.create_machine(
                 model_name=model_name,
                 machine_id=machine_id,
                 db_dict=db_dict,
                 model_name=model_name,
                 machine_id=machine_id,
                 db_dict=db_dict,
@@ -328,15 +266,34 @@ class N2VCJujuConnector(N2VCConnector):
         db_dict: dict,
         progress_timeout: float = None,
         total_timeout: float = None,
         db_dict: dict,
         progress_timeout: float = None,
         total_timeout: float = None,
-        cloud_name: str = None,
-        credential_name: str = None,
+        vca_id: str = None,
     ) -> str:
     ) -> str:
-
+        """
+        Register an existing execution environment at the VCA
+
+        :param: namespace: Contains a dot separate string.
+                    LCM will use: [<nsi-id>].<ns-id>.<vnf-id>.<vdu-id>[-<count>]
+        :param: credentials: credentials to access the existing execution environment
+                            (it can contains hostname, username, path to private key,
+                            etc depending on underlying cloud)
+        :param: db_dict: where to write to database when the status changes.
+            It contains a dictionary with {collection: str, filter: {},  path: str},
+                e.g. {collection: "nsrs", filter: {_id: <nsd-id>, path:
+                "_admin.deployed.VCA.3"}
+        :param: reuse_ee_id: ee id from an older execution. It allows us to reuse an
+                             older environment
+        :param: progress_timeout: Progress timeout
+        :param: total_timeout: Total timeout
+        :param: vca_id: VCA ID
+
+        :returns: id of the execution environment
+        """
         self.log.info(
             "Registering execution environment. namespace={}, credentials={}".format(
                 namespace, credentials
             )
         )
         self.log.info(
             "Registering execution environment. namespace={}, credentials={}".format(
                 namespace, credentials
             )
         )
+        libjuju = await self._get_libjuju(vca_id)
 
         if credentials is None:
             raise N2VCBadArgumentsException(
 
         if credentials is None:
             raise N2VCBadArgumentsException(
@@ -371,15 +328,12 @@ class N2VCJujuConnector(N2VCConnector):
 
         # register machine on juju
         try:
 
         # register machine on juju
         try:
-            if not await self.libjuju.model_exists(model_name):
-                cloud = cloud_name or self.cloud
-                credential = credential_name or cloud_name if cloud_name else self.cloud
-                await self.libjuju.add_model(
+            if not await libjuju.model_exists(model_name):
+                await libjuju.add_model(
                     model_name,
                     model_name,
-                    cloud_name=cloud,
-                    credential_name=credential
+                    libjuju.vca_connection.lxd_cloud,
                 )
                 )
-            machine_id = await self.libjuju.provision_machine(
+            machine_id = await libjuju.provision_machine(
                 model_name=model_name,
                 hostname=hostname,
                 username=username,
                 model_name=model_name,
                 hostname=hostname,
                 username=username,
@@ -416,7 +370,29 @@ class N2VCJujuConnector(N2VCConnector):
         total_timeout: float = None,
         config: dict = None,
         num_units: int = 1,
         total_timeout: float = None,
         config: dict = None,
         num_units: int = 1,
+        vca_id: str = None,
     ):
     ):
+        """
+        Install the software inside the execution environment identified by ee_id
+
+        :param: ee_id: the id of the execution environment returned by
+                          create_execution_environment or register_execution_environment
+        :param: artifact_path: where to locate the artifacts (parent folder) using
+                                  the self.fs
+                                  the final artifact path will be a combination of this
+                                  artifact_path and additional string from the config_dict
+                                  (e.g. charm name)
+        :param: db_dict: where to write into database when the status changes.
+                             It contains a dict with
+                                {collection: <str>, filter: {},  path: <str>},
+                                e.g. {collection: "nsrs", filter:
+                                    {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
+        :param: progress_timeout: Progress timeout
+        :param: total_timeout: Total timeout
+        :param: config: Dictionary with deployment config information.
+        :param: num_units: Number of units to deploy of a particular charm.
+        :param: vca_id: VCA ID
+        """
 
         self.log.info(
             (
 
         self.log.info(
             (
@@ -424,6 +400,7 @@ class N2VCJujuConnector(N2VCConnector):
                 "artifact path: {}, db_dict: {}"
             ).format(ee_id, artifact_path, db_dict)
         )
                 "artifact path: {}, db_dict: {}"
             ).format(ee_id, artifact_path, db_dict)
         )
+        libjuju = await self._get_libjuju(vca_id)
 
         # check arguments
         if ee_id is None or len(ee_id) == 0:
 
         # check arguments
         if ee_id is None or len(ee_id) == 0:
@@ -473,7 +450,7 @@ class N2VCJujuConnector(N2VCConnector):
             full_path = self.fs.path + "/" + artifact_path
 
         try:
             full_path = self.fs.path + "/" + artifact_path
 
         try:
-            await self.libjuju.deploy_charm(
+            await libjuju.deploy_charm(
                 model_name=model_name,
                 application_name=application_name,
                 path=full_path,
                 model_name=model_name,
                 application_name=application_name,
                 path=full_path,
@@ -500,8 +477,7 @@ class N2VCJujuConnector(N2VCConnector):
         progress_timeout: float = None,
         total_timeout: float = None,
         config: dict = None,
         progress_timeout: float = None,
         total_timeout: float = None,
         config: dict = None,
-        cloud_name: str = None,
-        credential_name: str = None,
+        vca_id: str = None,
     ) -> str:
         """
         Install a k8s proxy charm
     ) -> str:
         """
         Install a k8s proxy charm
@@ -517,56 +493,54 @@ class N2VCJujuConnector(N2VCConnector):
                             {collection: <str>, filter: {},  path: <str>},
                             e.g. {collection: "nsrs", filter:
                                 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
                             {collection: <str>, filter: {},  path: <str>},
                             e.g. {collection: "nsrs", filter:
                                 {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
-        :param float progress_timeout:
-        :param float total_timeout:
+        :param: progress_timeout: Progress timeout
+        :param: total_timeout: Total timeout
         :param config: Dictionary with additional configuration
         :param config: Dictionary with additional configuration
-        :param cloud_name: Cloud Name in which the charms will be deployed
-        :param credential_name: Credential Name to use in the cloud_name.
-                                If not set, cloud_name will be used as credential_name
+        :param vca_id: VCA ID
 
         :returns ee_id: execution environment id.
         """
 
         :returns ee_id: execution environment id.
         """
-        self.log.info('Installing k8s proxy charm: {}, artifact path: {}, db_dict: {}'
-                      .format(charm_name, artifact_path, db_dict))
-
-        if not self.k8s_cloud:
-            raise JujuK8sProxycharmNotSupported("There is not k8s_cloud available")
+        self.log.info(
+            "Installing k8s proxy charm: {}, artifact path: {}, db_dict: {}".format(
+                charm_name, artifact_path, db_dict
+            )
+        )
+        libjuju = await self._get_libjuju(vca_id)
 
         if artifact_path is None or len(artifact_path) == 0:
             raise N2VCBadArgumentsException(
                 message="artifact_path is mandatory", bad_args=["artifact_path"]
             )
         if db_dict is None:
 
         if artifact_path is None or len(artifact_path) == 0:
             raise N2VCBadArgumentsException(
                 message="artifact_path is mandatory", bad_args=["artifact_path"]
             )
         if db_dict is None:
-            raise N2VCBadArgumentsException(message='db_dict is mandatory', bad_args=['db_dict'])
+            raise N2VCBadArgumentsException(
+                message="db_dict is mandatory", bad_args=["db_dict"]
+            )
 
         # remove // in charm path
 
         # remove // in charm path
-        while artifact_path.find('//') >= 0:
-            artifact_path = artifact_path.replace('//', '/')
+        while artifact_path.find("//") >= 0:
+            artifact_path = artifact_path.replace("//", "/")
 
         # check charm path
         if not self.fs.file_exists(artifact_path, mode="dir"):
 
         # check charm path
         if not self.fs.file_exists(artifact_path, mode="dir"):
-            msg = 'artifact path does not exist: {}'.format(artifact_path)
-            raise N2VCBadArgumentsException(message=msg, bad_args=['artifact_path'])
+            msg = "artifact path does not exist: {}".format(artifact_path)
+            raise N2VCBadArgumentsException(message=msg, bad_args=["artifact_path"])
 
 
-        if artifact_path.startswith('/'):
+        if artifact_path.startswith("/"):
             full_path = self.fs.path + artifact_path
         else:
             full_path = self.fs.path + artifact_path
         else:
-            full_path = self.fs.path + '/' + artifact_path
+            full_path = self.fs.path + "/" + artifact_path
 
         _, ns_id, _, _, _ = self._get_namespace_components(namespace=namespace)
 
         _, ns_id, _, _, _ = self._get_namespace_components(namespace=namespace)
-        model_name = '{}-k8s'.format(ns_id)
-        if not await self.libjuju.model_exists(model_name):
-            cloud = cloud_name or self.k8s_cloud
-            credential = credential_name or cloud_name if cloud_name else self.k8s_cloud
-            await self.libjuju.add_model(
+        model_name = "{}-k8s".format(ns_id)
+        if not await libjuju.model_exists(model_name):
+            await libjuju.add_model(
                 model_name,
                 model_name,
-                cloud_name=cloud,
-                credential_name=credential
+                libjuju.vca_connection.k8s_cloud,
             )
         application_name = self._get_application_name(namespace)
 
         try:
             )
         application_name = self._get_application_name(namespace)
 
         try:
-            await self.libjuju.deploy_charm(
+            await libjuju.deploy_charm(
                 model_name=model_name,
                 application_name=application_name,
                 path=full_path,
                 model_name=model_name,
                 application_name=application_name,
                 path=full_path,
@@ -574,12 +548,12 @@ class N2VCJujuConnector(N2VCConnector):
                 db_dict=db_dict,
                 progress_timeout=progress_timeout,
                 total_timeout=total_timeout,
                 db_dict=db_dict,
                 progress_timeout=progress_timeout,
                 total_timeout=total_timeout,
-                config=config
+                config=config,
             )
         except Exception as e:
             )
         except Exception as e:
-            raise N2VCException(message='Error deploying charm: {}'.format(e))
+            raise N2VCException(message="Error deploying charm: {}".format(e))
 
 
-        self.log.info('K8s proxy charm installed')
+        self.log.info("K8s proxy charm installed")
         ee_id = N2VCJujuConnector._build_ee_id(
             model_name=model_name,
             application_name=application_name,
         ee_id = N2VCJujuConnector._build_ee_id(
             model_name=model_name,
             application_name=application_name,
@@ -596,13 +570,33 @@ class N2VCJujuConnector(N2VCConnector):
         db_dict: dict,
         progress_timeout: float = None,
         total_timeout: float = None,
         db_dict: dict,
         progress_timeout: float = None,
         total_timeout: float = None,
+        vca_id: str = None,
     ) -> str:
     ) -> str:
+        """
+        Get Execution environment ssh public key
+
+        :param: ee_id: the id of the execution environment returned by
+            create_execution_environment or register_execution_environment
+        :param: db_dict: where to write into database when the status changes.
+                            It contains a dict with
+                                {collection: <str>, filter: {},  path: <str>},
+                                e.g. {collection: "nsrs", filter:
+                                    {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
+        :param: progress_timeout: Progress timeout
+        :param: total_timeout: Total timeout
+        :param vca_id: VCA ID
+        :returns: public key of the execution environment
+                    For the case of juju proxy charm ssh-layered, it is the one
+                    returned by 'get-ssh-public-key' primitive.
+                    It raises a N2VC exception if fails
+        """
 
         self.log.info(
             (
                 "Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}"
             ).format(ee_id, db_dict)
         )
 
         self.log.info(
             (
                 "Generating priv/pub key pair and get pub key on ee_id: {}, db_dict: {}"
             ).format(ee_id, db_dict)
         )
+        libjuju = await self._get_libjuju(vca_id)
 
         # check arguments
         if ee_id is None or len(ee_id) == 0:
 
         # check arguments
         if ee_id is None or len(ee_id) == 0:
@@ -643,7 +637,7 @@ class N2VCJujuConnector(N2VCConnector):
 
         # execute action: generate-ssh-key
         try:
 
         # execute action: generate-ssh-key
         try:
-            output, _status = await self.libjuju.execute_action(
+            output, _status = await libjuju.execute_action(
                 model_name=model_name,
                 application_name=application_name,
                 action_name="generate-ssh-key",
                 model_name=model_name,
                 application_name=application_name,
                 action_name="generate-ssh-key",
@@ -660,7 +654,7 @@ class N2VCJujuConnector(N2VCConnector):
 
         # execute action: get-ssh-public-key
         try:
 
         # execute action: get-ssh-public-key
         try:
-            output, _status = await self.libjuju.execute_action(
+            output, _status = await libjuju.execute_action(
                 model_name=model_name,
                 application_name=application_name,
                 action_name="get-ssh-public-key",
                 model_name=model_name,
                 application_name=application_name,
                 action_name="get-ssh-public-key",
@@ -676,18 +670,44 @@ class N2VCJujuConnector(N2VCConnector):
         # return public key if exists
         return output["pubkey"] if "pubkey" in output else output
 
         # return public key if exists
         return output["pubkey"] if "pubkey" in output else output
 
-    async def get_metrics(self, model_name: str, application_name: str) -> dict:
-        return await self.libjuju.get_metrics(model_name, application_name)
+    async def get_metrics(
+        self, model_name: str, application_name: str, vca_id: str = None
+    ) -> dict:
+        """
+        Get metrics from application
+
+        :param: model_name: Model name
+        :param: application_name: Application name
+        :param: vca_id: VCA ID
+
+        :return: Dictionary with obtained metrics
+        """
+        libjuju = await self._get_libjuju(vca_id)
+        return await libjuju.get_metrics(model_name, application_name)
 
     async def add_relation(
 
     async def add_relation(
-        self, ee_id_1: str, ee_id_2: str, endpoint_1: str, endpoint_2: str
+        self,
+        ee_id_1: str,
+        ee_id_2: str,
+        endpoint_1: str,
+        endpoint_2: str,
+        vca_id: str = None,
     ):
     ):
+        """
+        Add relation between two charmed endpoints
 
 
+        :param: ee_id_1: The id of the first execution environment
+        :param: ee_id_2: The id of the second execution environment
+        :param: endpoint_1: The endpoint in the first execution environment
+        :param: endpoint_2: The endpoint in the second execution environment
+        :param: vca_id: VCA ID
+        """
         self.log.debug(
             "adding new relation between {} and {}, endpoints: {}, {}".format(
                 ee_id_1, ee_id_2, endpoint_1, endpoint_2
             )
         )
         self.log.debug(
             "adding new relation between {} and {}, endpoints: {}, {}".format(
                 ee_id_1, ee_id_2, endpoint_1, endpoint_2
             )
         )
+        libjuju = await self._get_libjuju(vca_id)
 
         # check arguments
         if not ee_id_1:
 
         # check arguments
         if not ee_id_1:
@@ -721,7 +741,7 @@ class N2VCJujuConnector(N2VCConnector):
 
         # add juju relations between two applications
         try:
 
         # add juju relations between two applications
         try:
-            await self.libjuju.add_relation(
+            await libjuju.add_relation(
                 model_name=model_1,
                 endpoint_1="{}:{}".format(app_1, endpoint_1),
                 endpoint_2="{}:{}".format(app_2, endpoint_2),
                 model_name=model_1,
                 endpoint_1="{}:{}".format(app_1, endpoint_1),
                 endpoint_2="{}:{}".format(app_2, endpoint_2),
@@ -743,9 +763,25 @@ class N2VCJujuConnector(N2VCConnector):
         raise MethodNotImplemented()
 
     async def delete_namespace(
         raise MethodNotImplemented()
 
     async def delete_namespace(
-        self, namespace: str, db_dict: dict = None, total_timeout: float = None
+        self,
+        namespace: str,
+        db_dict: dict = None,
+        total_timeout: float = None,
+        vca_id: str = None,
     ):
     ):
+        """
+        Remove a network scenario and its execution environments
+        :param: namespace: [<nsi-id>].<ns-id>
+        :param: db_dict: where to write into database when the status changes.
+                        It contains a dict with
+                            {collection: <str>, filter: {},  path: <str>},
+                            e.g. {collection: "nsrs", filter:
+                                {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
+        :param: total_timeout: Total timeout
+        :param: vca_id: VCA ID
+        """
         self.log.info("Deleting namespace={}".format(namespace))
         self.log.info("Deleting namespace={}".format(namespace))
+        libjuju = await self._get_libjuju(vca_id)
 
         # check arguments
         if namespace is None:
 
         # check arguments
         if namespace is None:
@@ -758,9 +794,9 @@ class N2VCJujuConnector(N2VCConnector):
         )
         if ns_id is not None:
             try:
         )
         if ns_id is not None:
             try:
-                models = await self.libjuju.list_models(contains=ns_id)
+                models = await libjuju.list_models(contains=ns_id)
                 for model in models:
                 for model in models:
-                    await self.libjuju.destroy_model(
+                    await libjuju.destroy_model(
                         model_name=model, total_timeout=total_timeout
                     )
             except Exception as e:
                         model_name=model, total_timeout=total_timeout
                     )
             except Exception as e:
@@ -775,10 +811,27 @@ class N2VCJujuConnector(N2VCConnector):
         self.log.info("Namespace {} deleted".format(namespace))
 
     async def delete_execution_environment(
         self.log.info("Namespace {} deleted".format(namespace))
 
     async def delete_execution_environment(
-        self, ee_id: str, db_dict: dict = None, total_timeout: float = None,
-            scaling_in: bool = False
+        self,
+        ee_id: str,
+        db_dict: dict = None,
+        total_timeout: float = None,
+        scaling_in: bool = False,
+        vca_id: str = None,
     ):
     ):
+        """
+        Delete an execution environment
+        :param str ee_id: id of the execution environment to delete
+        :param dict db_dict: where to write into database when the status changes.
+                        It contains a dict with
+                            {collection: <str>, filter: {},  path: <str>},
+                            e.g. {collection: "nsrs", filter:
+                                {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
+        :param: total_timeout: Total timeout
+        :param: scaling_in: Boolean to indicate if is it a scaling in operation
+        :param: vca_id: VCA ID
+        """
         self.log.info("Deleting execution environment ee_id={}".format(ee_id))
         self.log.info("Deleting execution environment ee_id={}".format(ee_id))
+        libjuju = await self._get_libjuju(vca_id)
 
         # check arguments
         if ee_id is None:
 
         # check arguments
         if ee_id is None:
@@ -793,13 +846,13 @@ class N2VCJujuConnector(N2VCConnector):
             if not scaling_in:
                 # destroy the model
                 # TODO: should this be removed?
             if not scaling_in:
                 # destroy the model
                 # TODO: should this be removed?
-                await self.libjuju.destroy_model(
+                await libjuju.destroy_model(
                     model_name=model_name,
                     total_timeout=total_timeout,
                 )
             else:
                 # destroy the application
                     model_name=model_name,
                     total_timeout=total_timeout,
                 )
             else:
                 # destroy the application
-                await self.libjuju.destroy_application(
+                await libjuju.destroy_application(
                     model_name=model_name,
                     application_name=application_name,
                     total_timeout=total_timeout,
                     model_name=model_name,
                     application_name=application_name,
                     total_timeout=total_timeout,
@@ -821,13 +874,34 @@ class N2VCJujuConnector(N2VCConnector):
         db_dict: dict = None,
         progress_timeout: float = None,
         total_timeout: float = None,
         db_dict: dict = None,
         progress_timeout: float = None,
         total_timeout: float = None,
+        vca_id: str = None,
     ) -> str:
     ) -> str:
+        """
+        Execute a primitive in the execution environment
+
+        :param: ee_id: the one returned by create_execution_environment or
+            register_execution_environment
+        :param: primitive_name: must be one defined in the software. There is one
+            called 'config', where, for the proxy case, the 'credentials' of VM are
+            provided
+        :param: params_dict: parameters of the action
+        :param: db_dict: where to write into database when the status changes.
+                        It contains a dict with
+                            {collection: <str>, filter: {},  path: <str>},
+                            e.g. {collection: "nsrs", filter:
+                                {_id: <nsd-id>, path: "_admin.deployed.VCA.3"}
+        :param: progress_timeout: Progress timeout
+        :param: total_timeout: Total timeout
+        :param: vca_id: VCA ID
+        :returns str: primitive result, if ok. It raises exceptions in case of fail
+        """
 
         self.log.info(
             "Executing primitive: {} on ee: {}, params: {}".format(
                 primitive_name, ee_id, params_dict
             )
         )
 
         self.log.info(
             "Executing primitive: {} on ee: {}, params: {}".format(
                 primitive_name, ee_id, params_dict
             )
         )
+        libjuju = await self._get_libjuju(vca_id)
 
         # check arguments
         if ee_id is None or len(ee_id) == 0:
 
         # check arguments
         if ee_id is None or len(ee_id) == 0:
@@ -858,13 +932,14 @@ class N2VCJujuConnector(N2VCConnector):
         if primitive_name == "config":
             # Special case: config primitive
             try:
         if primitive_name == "config":
             # Special case: config primitive
             try:
-                await self.libjuju.configure_application(
+                await libjuju.configure_application(
                     model_name=model_name,
                     application_name=application_name,
                     config=params_dict,
                 )
                     model_name=model_name,
                     application_name=application_name,
                     config=params_dict,
                 )
-                actions = await self.libjuju.get_actions(
-                    application_name=application_name, model_name=model_name,
+                actions = await libjuju.get_actions(
+                    application_name=application_name,
+                    model_name=model_name,
                 )
                 self.log.debug(
                     "Application {} has these actions: {}".format(
                 )
                 self.log.debug(
                     "Application {} has these actions: {}".format(
@@ -878,7 +953,7 @@ class N2VCJujuConnector(N2VCConnector):
                     for _ in range(num_retries):
                         try:
                             self.log.debug("Executing action verify-ssh-credentials...")
                     for _ in range(num_retries):
                         try:
                             self.log.debug("Executing action verify-ssh-credentials...")
-                            output, ok = await self.libjuju.execute_action(
+                            output, ok = await libjuju.execute_action(
                                 model_name=model_name,
                                 application_name=application_name,
                                 action_name="verify-ssh-credentials",
                                 model_name=model_name,
                                 application_name=application_name,
                                 action_name="verify-ssh-credentials",
@@ -920,7 +995,7 @@ class N2VCJujuConnector(N2VCConnector):
             return "CONFIG OK"
         else:
             try:
             return "CONFIG OK"
         else:
             try:
-                output, status = await self.libjuju.execute_action(
+                output, status = await libjuju.execute_action(
                     model_name=model_name,
                     application_name=application_name,
                     action_name=primitive_name,
                     model_name=model_name,
                     application_name=application_name,
                     action_name=primitive_name,
@@ -944,13 +1019,20 @@ class N2VCJujuConnector(N2VCConnector):
                     primitive_name=primitive_name,
                 )
 
                     primitive_name=primitive_name,
                 )
 
-    async def disconnect(self):
+    async def disconnect(self, vca_id: str = None):
+        """
+        Disconnect from VCA
+
+        :param: vca_id: VCA ID
+        """
         self.log.info("closing juju N2VC...")
         self.log.info("closing juju N2VC...")
+        libjuju = await self._get_libjuju(vca_id)
         try:
         try:
-            await self.libjuju.disconnect()
+            await libjuju.disconnect()
         except Exception as e:
             raise N2VCConnectionException(
         except Exception as e:
             raise N2VCConnectionException(
-                message="Error disconnecting controller: {}".format(e), url=self.url
+                message="Error disconnecting controller: {}".format(e),
+                url=libjuju.vca_connection.data.endpoints,
             )
 
     """
             )
 
     """
@@ -959,6 +1041,31 @@ class N2VCJujuConnector(N2VCConnector):
 ####################################################################################
     """
 
 ####################################################################################
     """
 
+    async def _get_libjuju(self, vca_id: str = None) -> Libjuju:
+        """
+        Get libjuju object
+
+        :param: vca_id: VCA ID
+                        If None, get a libjuju object with a Connection to the default VCA
+                        Else, geta libjuju object with a Connection to the specified VCA
+        """
+        if not vca_id:
+            while self.loading_libjuju.locked():
+                await asyncio.sleep(0.1)
+            if not self.libjuju:
+                async with self.loading_libjuju:
+                    vca_connection = await get_connection(self._store)
+                    self.libjuju = Libjuju(vca_connection, loop=self.loop, log=self.log)
+            return self.libjuju
+        else:
+            vca_connection = await get_connection(self._store, vca_id)
+            return Libjuju(
+                vca_connection,
+                loop=self.loop,
+                log=self.log,
+                n2vc=self,
+            )
+
     def _write_ee_id_db(self, db_dict: dict, ee_id: str):
 
         # write ee_id to database: _admin.deployed.VCA.x
     def _write_ee_id_db(self, db_dict: dict, ee_id: str):
 
         # write ee_id to database: _admin.deployed.VCA.x
@@ -1046,38 +1153,6 @@ class N2VCJujuConnector(N2VCConnector):
 
         return N2VCJujuConnector._format_app_name(application_name)
 
 
         return N2VCJujuConnector._format_app_name(application_name)
 
-    def _create_juju_public_key(self):
-        """Recreate the Juju public key on lcm container, if needed
-        Certain libjuju commands expect to be run from the same machine as Juju
-         is bootstrapped to. This method will write the public key to disk in
-         that location: ~/.local/share/juju/ssh/juju_id_rsa.pub
-        """
-
-        # Make sure that we have a public key before writing to disk
-        if self.public_key is None or len(self.public_key) == 0:
-            if "OSMLCM_VCA_PUBKEY" in os.environ:
-                self.public_key = os.getenv("OSMLCM_VCA_PUBKEY", "")
-                if len(self.public_key) == 0:
-                    return
-            else:
-                return
-
-        pk_path = "{}/.local/share/juju/ssh".format(os.path.expanduser("~"))
-        file_path = "{}/juju_id_rsa.pub".format(pk_path)
-        self.log.debug(
-            "writing juju public key to file:\n{}\npublic key: {}".format(
-                file_path, self.public_key
-            )
-        )
-        if not os.path.exists(pk_path):
-            # create path and write file
-            os.makedirs(pk_path)
-            with open(file_path, "w") as f:
-                self.log.debug("Creating juju public key file: {}".format(file_path))
-                f.write(self.public_key)
-        else:
-            self.log.debug("juju public key file already exists: {}".format(file_path))
-
     @staticmethod
     def _format_model_name(name: str) -> str:
         """Format the name of the model.
     @staticmethod
     def _format_model_name(name: str) -> str:
         """Format the name of the model.
@@ -1127,3 +1202,14 @@ class N2VCJujuConnector(N2VCConnector):
             app_name = "z" + app_name
 
         return app_name
             app_name = "z" + app_name
 
         return app_name
+
+    async def validate_vca(self, vca_id: str):
+        """
+        Validate a VCA by connecting/disconnecting to/from it
+
+        :param: vca_id: VCA ID
+        """
+        vca_connection = await get_connection(self._store, vca_id=vca_id)
+        libjuju = Libjuju(vca_connection, loop=self.loop, log=self.log, n2vc=self)
+        controller = await libjuju.get_controller()
+        await libjuju.disconnect_controller(controller)
diff --git a/n2vc/store.py b/n2vc/store.py
new file mode 100644 (file)
index 0000000..b827d51
--- /dev/null
@@ -0,0 +1,390 @@
+# Copyright 2021 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 abc
+import asyncio
+from base64 import b64decode
+import re
+import typing
+
+from Crypto.Cipher import AES
+from motor.motor_asyncio import AsyncIOMotorClient
+from n2vc.config import EnvironConfig
+from n2vc.vca.connection_data import ConnectionData
+from osm_common.dbmongo import DbMongo, DbException
+
+DB_NAME = "osm"
+
+
+class Store(abc.ABC):
+    @abc.abstractmethod
+    async def get_vca_connection_data(self, vca_id: str) -> ConnectionData:
+        """
+        Get VCA connection data
+
+        :param: vca_id: VCA ID
+
+        :returns: ConnectionData with the information of the database
+        """
+
+    @abc.abstractmethod
+    async def update_vca_endpoints(self, hosts: typing.List[str], vca_id: str):
+        """
+        Update VCA endpoints
+
+        :param: endpoints: List of endpoints to write in the database
+        :param: vca_id: VCA ID
+        """
+
+    @abc.abstractmethod
+    async def get_vca_endpoints(self, vca_id: str = None) -> typing.List[str]:
+        """
+        Get list if VCA endpoints
+
+        :param: vca_id: VCA ID
+
+        :returns: List of endpoints
+        """
+
+    @abc.abstractmethod
+    async def get_vca_id(self, vim_id: str = None) -> str:
+        """
+        Get VCA id for a VIM account
+
+        :param: vim_id: Vim account ID
+        """
+
+
+class DbMongoStore(Store):
+    def __init__(self, db: DbMongo):
+        """
+        Constructor
+
+        :param: db: osm_common.dbmongo.DbMongo object
+        """
+        self.db = db
+
+    async def get_vca_connection_data(self, vca_id: str) -> ConnectionData:
+        """
+        Get VCA connection data
+
+        :param: vca_id: VCA ID
+
+        :returns: ConnectionData with the information of the database
+        """
+        data = self.db.get_one("vca", q_filter={"_id": vca_id})
+        self.db.encrypt_decrypt_fields(
+            data,
+            "decrypt",
+            ["secret", "cacert"],
+            schema_version=data["schema_version"],
+            salt=data["_id"],
+        )
+        return ConnectionData(**data)
+
+    async def update_vca_endpoints(
+        self, endpoints: typing.List[str], vca_id: str = None
+    ):
+        """
+        Update VCA endpoints
+
+        :param: endpoints: List of endpoints to write in the database
+        :param: vca_id: VCA ID
+        """
+        if vca_id:
+            data = self.db.get_one("vca", q_filter={"_id": vca_id})
+            data["endpoints"] = endpoints
+            self._update("vca", vca_id, data)
+        else:
+            # The default VCA. Data for the endpoints is in a different place
+            juju_info = self._get_juju_info()
+            # If it doesn't, then create it
+            if not juju_info:
+                try:
+                    self.db.create(
+                        "vca",
+                        {"_id": "juju"},
+                    )
+                except DbException as e:
+                    # Racing condition: check if another N2VC worker has created it
+                    juju_info = self._get_juju_info()
+                    if not juju_info:
+                        raise e
+            self.db.set_one(
+                "vca",
+                {"_id": "juju"},
+                {"api_endpoints": endpoints},
+            )
+
+    async def get_vca_endpoints(self, vca_id: str = None) -> typing.List[str]:
+        """
+        Get list if VCA endpoints
+
+        :param: vca_id: VCA ID
+
+        :returns: List of endpoints
+        """
+        endpoints = []
+        if vca_id:
+            endpoints = self.get_vca_connection_data(vca_id).endpoints
+        else:
+            juju_info = self._get_juju_info()
+            if juju_info and "api_endpoints" in juju_info:
+                endpoints = juju_info["api_endpoints"]
+        return endpoints
+
+    async def get_vca_id(self, vim_id: str = None) -> str:
+        """
+        Get VCA ID from the database for a given VIM account ID
+
+        :param: vim_id: VIM account ID
+        """
+        return (
+            self.db.get_one(
+                "vim_accounts",
+                q_filter={"_id": vim_id},
+                fail_on_empty=False,
+            ).get("vca")
+            if vim_id
+            else None
+        )
+
+    def _update(self, collection: str, id: str, data: dict):
+        """
+        Update object in database
+
+        :param: collection: Collection name
+        :param: id: ID of the object
+        :param: data: Object data
+        """
+        self.db.replace(
+            collection,
+            id,
+            data,
+        )
+
+    def _get_juju_info(self):
+        """Get Juju information (the default VCA) from the admin collection"""
+        return self.db.get_one(
+            "vca",
+            q_filter={"_id": "juju"},
+            fail_on_empty=False,
+        )
+
+
+class MotorStore(Store):
+    def __init__(self, uri: str, loop=None):
+        """
+        Constructor
+
+        :param: uri: Connection string to connect to the database.
+        :param: loop: Asyncio Loop
+        """
+        self._client = AsyncIOMotorClient(uri)
+        self.loop = loop or asyncio.get_event_loop()
+        self._secret_key = None
+        self._config = EnvironConfig(prefixes=["OSMLCM_", "OSMMON_"])
+
+    @property
+    def _database(self):
+        return self._client[DB_NAME]
+
+    @property
+    def _vca_collection(self):
+        return self._database["vca"]
+
+    @property
+    def _admin_collection(self):
+        return self._database["admin"]
+
+    @property
+    def _vim_accounts_collection(self):
+        return self._database["vim_accounts"]
+
+    async def get_vca_connection_data(self, vca_id: str) -> ConnectionData:
+        """
+        Get VCA connection data
+
+        :param: vca_id: VCA ID
+
+        :returns: ConnectionData with the information of the database
+        """
+        data = await self._vca_collection.find_one({"_id": vca_id})
+        if not data:
+            raise Exception("vca with id {} not found".format(vca_id))
+        await self.decrypt_fields(
+            data,
+            ["secret", "cacert"],
+            schema_version=data["schema_version"],
+            salt=data["_id"],
+        )
+        return ConnectionData(**data)
+
+    async def update_vca_endpoints(
+        self, endpoints: typing.List[str], vca_id: str = None
+    ):
+        """
+        Update VCA endpoints
+
+        :param: endpoints: List of endpoints to write in the database
+        :param: vca_id: VCA ID
+        """
+        if vca_id:
+            data = await self._vca_collection.find_one({"_id": vca_id})
+            data["endpoints"] = endpoints
+            await self._vca_collection.replace_one({"_id": vca_id}, data)
+        else:
+            # The default VCA. Data for the endpoints is in a different place
+            juju_info = await self._get_juju_info()
+            # If it doesn't, then create it
+            if not juju_info:
+                try:
+                    await self._admin_collection.insert_one({"_id": "juju"})
+                except Exception as e:
+                    # Racing condition: check if another N2VC worker has created it
+                    juju_info = await self._get_juju_info()
+                    if not juju_info:
+                        raise e
+
+            await self._admin_collection.replace_one(
+                {"_id": "juju"}, {"api_endpoints": endpoints}
+            )
+
+    async def get_vca_endpoints(self, vca_id: str = None) -> typing.List[str]:
+        """
+        Get list if VCA endpoints
+
+        :param: vca_id: VCA ID
+
+        :returns: List of endpoints
+        """
+        endpoints = []
+        if vca_id:
+            endpoints = (await self.get_vca_connection_data(vca_id)).endpoints
+        else:
+            juju_info = await self._get_juju_info()
+            if juju_info and "api_endpoints" in juju_info:
+                endpoints = juju_info["api_endpoints"]
+        return endpoints
+
+    async def get_vca_id(self, vim_id: str = None) -> str:
+        """
+        Get VCA ID from the database for a given VIM account ID
+
+        :param: vim_id: VIM account ID
+        """
+        vca_id = None
+        if vim_id:
+            vim_account = await self._vim_accounts_collection.find_one({"_id": vim_id})
+            if vim_account and "vca" in vim_account:
+                vca_id = vim_account["vca"]
+        return vca_id
+
+    async def _get_juju_info(self):
+        """Get Juju information (the default VCA) from the admin collection"""
+        return await self._admin_collection.find_one({"_id": "juju"})
+
+    # DECRYPT METHODS
+    async def decrypt_fields(
+        self,
+        item: dict,
+        fields: typing.List[str],
+        schema_version: str = None,
+        salt: str = None,
+    ):
+        """
+        Decrypt fields
+
+        Decrypt fields from a dictionary. Follows the same logic as in osm_common.
+
+        :param: item: Dictionary with the keys to be decrypted
+        :param: fields: List of keys to decrypt
+        :param: schema version: Schema version. (i.e. 1.11)
+        :param: salt: Salt for the decryption
+        """
+        flags = re.I
+
+        async def process(_item):
+            if isinstance(_item, list):
+                for elem in _item:
+                    await process(elem)
+            elif isinstance(_item, dict):
+                for key, val in _item.items():
+                    if isinstance(val, str):
+                        if any(re.search(f, key, flags) for f in fields):
+                            _item[key] = await self.decrypt(val, schema_version, salt)
+                    else:
+                        await process(val)
+
+        await process(item)
+
+    async def decrypt(self, value, schema_version=None, salt=None):
+        """
+        Decrypt an encrypted value
+        :param value: value to be decrypted. It is a base64 string
+        :param schema_version: used for known encryption method used. If None or '1.0' no encryption has been done.
+               If '1.1' symmetric AES encryption has been done
+        :param salt: optional salt to be used
+        :return: Plain content of value
+        """
+        if not await self.secret_key or not schema_version or schema_version == "1.0":
+            return value
+        else:
+            secret_key = self._join_secret_key(salt)
+            encrypted_msg = b64decode(value)
+            cipher = AES.new(secret_key)
+            decrypted_msg = cipher.decrypt(encrypted_msg)
+            try:
+                unpadded_private_msg = decrypted_msg.decode().rstrip("\0")
+            except UnicodeDecodeError:
+                raise DbException(
+                    "Cannot decrypt information. Are you using same COMMONKEY in all OSM components?",
+                    http_code=500,
+                )
+            return unpadded_private_msg
+
+    def _join_secret_key(self, update_key: typing.Any):
+        """
+        Join secret key
+
+        :param: update_key: str or bytes with the to update
+        """
+        if isinstance(update_key, str):
+            update_key_bytes = update_key.encode()
+        else:
+            update_key_bytes = update_key
+        new_secret_key = (
+            bytearray(self._secret_key) if self._secret_key else bytearray(32)
+        )
+        for i, b in enumerate(update_key_bytes):
+            new_secret_key[i % 32] ^= b
+        return bytes(new_secret_key)
+
+    @property
+    async def secret_key(self):
+        if self._secret_key:
+            return self._secret_key
+        else:
+            if self.database_key:
+                self._secret_key = self._join_secret_key(self.database_key)
+            version_data = await self._admin_collection.find_one({"_id": "version"})
+            if version_data and version_data.get("serial"):
+                self._secret_key = self._join_secret_key(
+                    b64decode(version_data["serial"])
+                )
+            return self._secret_key
+
+    @property
+    def database_key(self):
+        return self._config["database_commonkey"]
diff --git a/n2vc/tests/unit/test_config.py b/n2vc/tests/unit/test_config.py
new file mode 100644 (file)
index 0000000..9a4af07
--- /dev/null
@@ -0,0 +1,58 @@
+# Copyright 2020 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 unittest import TestCase
+from unittest.mock import patch
+
+
+from n2vc.config import EnvironConfig, ModelConfig, MODEL_CONFIG_KEYS
+
+
+def generate_os_environ_dict(config, prefix):
+    return {f"{prefix}{k.upper()}": v for k, v in config.items()}
+
+
+class TestEnvironConfig(TestCase):
+    def setUp(self):
+        self.config = {"host": "1.2.3.4", "port": "17070", "k8s_cloud": "k8s"}
+
+    @patch("os.environ.items")
+    def test_environ_config_lcm(self, mock_environ_items):
+        envs = generate_os_environ_dict(self.config, "OSMLCM_VCA_")
+        envs["not_valid_env"] = "something"
+        mock_environ_items.return_value = envs.items()
+        config = EnvironConfig()
+        self.assertEqual(config, self.config)
+
+    @patch("os.environ.items")
+    def test_environ_config_mon(self, mock_environ_items):
+        envs = generate_os_environ_dict(self.config, "OSMMON_VCA_")
+        envs["not_valid_env"] = "something"
+        mock_environ_items.return_value = envs.items()
+        config = EnvironConfig()
+        self.assertEqual(config, self.config)
+
+
+class TestModelConfig(TestCase):
+    def setUp(self):
+        self.config = {
+            f'model_config_{model_key.replace("-", "_")}': "somevalue"
+            for model_key in MODEL_CONFIG_KEYS
+        }
+        self.config["model_config_invalid"] = "something"
+        self.model_config = {model_key: "somevalue" for model_key in MODEL_CONFIG_KEYS}
+
+    def test_model_config(self):
+        model_config = ModelConfig(self.config)
+        self.assertEqual(model_config, self.model_config)
diff --git a/n2vc/tests/unit/test_connection.py b/n2vc/tests/unit/test_connection.py
new file mode 100644 (file)
index 0000000..c7f0bb4
--- /dev/null
@@ -0,0 +1,68 @@
+# Copyright 2020 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
+from unittest import TestCase
+from unittest.mock import Mock, patch
+
+
+from n2vc.tests.unit.utils import AsyncMock
+from n2vc.vca import connection
+
+
+class TestConnection(TestCase):
+    def setUp(self):
+        self.loop = asyncio.get_event_loop()
+        self.store = AsyncMock()
+
+    def test_load_from_store(self):
+        self.loop.run_until_complete(connection.get_connection(self.store, "vim_id"))
+
+        self.store.get_vca_connection_data.assert_called_once()
+
+    def test_cloud_properties(self):
+        conn = self.loop.run_until_complete(
+            connection.get_connection(self.store, "vim_id")
+        )
+        conn._data = Mock()
+        conn._data.lxd_cloud = "name"
+        conn._data.k8s_cloud = "name"
+        conn._data.lxd_credentials = "credential"
+        conn._data.k8s_credentials = "credential"
+
+        self.assertEqual(conn.lxd_cloud.name, "name")
+        self.assertEqual(conn.lxd_cloud.credential_name, "credential")
+        self.assertEqual(conn.k8s_cloud.name, "name")
+        self.assertEqual(conn.k8s_cloud.credential_name, "credential")
+
+    @patch("n2vc.vca.connection.EnvironConfig")
+    @patch("n2vc.vca.connection_data.base64_to_cacert")
+    def test_load_from_env(self, mock_base64_to_cacert, mock_env):
+        mock_base64_to_cacert.return_value = "cacert"
+        mock_env.return_value = {
+            "endpoints": "1.2.3.4:17070",
+            "user": "user",
+            "secret": "secret",
+            "cacert": "cacert",
+            "pubkey": "pubkey",
+            "cloud": "cloud",
+            "credentials": "credentials",
+            "k8s_cloud": "k8s_cloud",
+            "k8s_credentials": "k8s_credentials",
+            "model_config": {},
+            "api-proxy": "api_proxy",
+        }
+        self.store.get_vca_endpoints.return_value = ["1.2.3.5:17070"]
+        self.loop.run_until_complete(connection.get_connection(self.store))
+        self.store.get_vca_connection_data.assert_not_called()
index d333b33..5f81274 100644 (file)
@@ -45,6 +45,7 @@ class JujuWatcherTest(asynctest.TestCase):
     def test_model_watcher(self, allwatcher):
         tests = Deltas
         allwatcher.return_value = FakeWatcher()
     def test_model_watcher(self, allwatcher):
         tests = Deltas
         allwatcher.return_value = FakeWatcher()
+        n2vc = AsyncMock()
         for test in tests:
             with self.assertRaises(asyncio.TimeoutError):
                 allwatcher.return_value.delta_to_return = [test.delta]
         for test in tests:
             with self.assertRaises(asyncio.TimeoutError):
                 allwatcher.return_value.delta_to_return = [test.delta]
@@ -55,12 +56,12 @@ class JujuWatcherTest(asynctest.TestCase):
                         test.filter.entity_type,
                         timeout=0,
                         db_dict={"something"},
                         test.filter.entity_type,
                         timeout=0,
                         db_dict={"something"},
-                        n2vc=self.n2vc,
+                        n2vc=n2vc,
+                        vca_id=None,
                     )
                 )
 
                     )
                 )
 
-            self.assertEqual(self.n2vc.last_written_values, test.db.data)
-            self.n2vc.last_written_values = None
+            n2vc.write_app_status_to_db.assert_called()
 
     @mock.patch("n2vc.juju_watcher.asyncio.wait")
     def test_wait_for(self, wait):
 
     @mock.patch("n2vc.juju_watcher.asyncio.wait")
     def test_wait_for(self, wait):
index f454380..208c849 100644 (file)
@@ -23,172 +23,58 @@ from .utils import kubeconfig, FakeModel, FakeFileWrapper, AsyncMock
 from n2vc.exceptions import (
     MethodNotImplemented,
     K8sException,
 from n2vc.exceptions import (
     MethodNotImplemented,
     K8sException,
-    N2VCBadArgumentsException,
 )
 )
+from n2vc.vca.connection_data import ConnectionData
 
 
 class K8sJujuConnTestCase(asynctest.TestCase):
 
 
 class K8sJujuConnTestCase(asynctest.TestCase):
-    @asynctest.mock.patch("juju.controller.Controller.update_endpoints")
-    @asynctest.mock.patch("juju.client.connector.Connector.connect")
-    @asynctest.mock.patch("juju.controller.Controller.connection")
-    @asynctest.mock.patch("n2vc.k8s_juju_conn.base64_to_cacert")
     @asynctest.mock.patch("n2vc.k8s_juju_conn.Libjuju")
     @asynctest.mock.patch("n2vc.k8s_juju_conn.Libjuju")
+    @asynctest.mock.patch("n2vc.k8s_juju_conn.MotorStore")
+    @asynctest.mock.patch("n2vc.k8s_juju_conn.get_connection")
+    @asynctest.mock.patch("n2vc.vca.connection_data.base64_to_cacert")
     def setUp(
         self,
     def setUp(
         self,
-        mock_libjuju=None,
         mock_base64_to_cacert=None,
         mock_base64_to_cacert=None,
-        mock_connection=None,
-        mock_connect=None,
-        mock_update_endpoints=None,
+        mock_get_connection=None,
+        mock_store=None,
+        mock_libjuju=None,
     ):
         self.loop = asyncio.get_event_loop()
     ):
         self.loop = asyncio.get_event_loop()
-        mock_libjuju.return_value = AsyncMock()
-        db = Mock()
-        vca_config = {
-            "secret": "secret",
-            "api_proxy": "api_proxy",
-            "cloud": "cloud",
-            "k8s_cloud": "k8s_cloud",
-            "user": "user",
-            "host": "1.1.1.1",
-            "port": 17070,
-            "ca_cert": "cacert",
-        }
-
+        self.db = Mock()
+        mock_base64_to_cacert.return_value = """
+    -----BEGIN CERTIFICATE-----
+    SOMECERT
+    -----END CERTIFICATE-----"""
+        mock_libjuju.return_value = Mock()
+        mock_store.return_value = AsyncMock()
+        mock_vca_connection = Mock()
+        mock_get_connection.return_value = mock_vca_connection
+        mock_vca_connection.data.return_value = ConnectionData(
+            **{
+                "endpoints": ["1.2.3.4:17070"],
+                "user": "user",
+                "secret": "secret",
+                "cacert": "cacert",
+                "pubkey": "pubkey",
+                "lxd-cloud": "cloud",
+                "lxd-credentials": "credentials",
+                "k8s-cloud": "k8s_cloud",
+                "k8s-credentials": "k8s_credentials",
+                "model-config": {},
+                "api-proxy": "api_proxy",
+            }
+        )
         logging.disable(logging.CRITICAL)
 
         self.k8s_juju_conn = K8sJujuConnector(
             fs=fslocal.FsLocal(),
         logging.disable(logging.CRITICAL)
 
         self.k8s_juju_conn = K8sJujuConnector(
             fs=fslocal.FsLocal(),
-            db=db,
+            db=self.db,
             log=None,
             loop=self.loop,
             log=None,
             loop=self.loop,
-            vca_config=vca_config,
-            on_update_db=None,
-        )
-
-
-class K8sJujuConnInitSuccessTestCase(asynctest.TestCase):
-    def setUp(
-        self,
-    ):
-        logging.disable(logging.CRITICAL)
-
-    @asynctest.mock.patch("juju.controller.Controller.update_endpoints")
-    @asynctest.mock.patch("juju.client.connector.Connector.connect")
-    @asynctest.mock.patch("juju.controller.Controller.connection")
-    @asynctest.mock.patch("n2vc.k8s_juju_conn.base64_to_cacert")
-    @asynctest.mock.patch("n2vc.libjuju.Libjuju.__init__")
-    def test_success(
-        self,
-        mock_libjuju=None,
-        mock_base64_to_cacert=None,
-        mock_connection=None,
-        mock_connect=None,
-        mock_update_endpoints=None,
-    ):
-        mock_libjuju.return_value = None
-        loop = asyncio.get_event_loop()
-        log = logging.getLogger()
-        db = Mock()
-        vca_config = {
-            "secret": "secret",
-            "cloud": "cloud",
-            "k8s_cloud": "k8s_cloud",
-            "user": "user",
-            "host": "1.1.1.1",
-            "port": 17070,
-            "ca_cert": "cacert",
-        }
-        K8sJujuConnector(
-            fs=fslocal.FsLocal(),
-            db=db,
-            log=log,
-            loop=self.loop,
-            vca_config=vca_config,
             on_update_db=None,
         )
             on_update_db=None,
         )
-
-        mock_libjuju.assert_called_once_with(
-            endpoint="1.1.1.1:17070",
-            api_proxy=None,  # Not needed for k8s charms
-            model_config={},
-            username="user",
-            password="secret",
-            cacert=mock_base64_to_cacert.return_value,
-            loop=loop,
-            log=log,
-            db=db,
-        )
-
-
-class K8sJujuConnectorInitFailureTestCase(asynctest.TestCase):
-    def setUp(
-        self,
-    ):
-        self.loop = asyncio.get_event_loop()
-        logging.disable(logging.CRITICAL)
-        self.vca_config = {
-            "secret": "secret",
-            "api_proxy": "api_proxy",
-            "cloud": "cloud",
-            "k8s_cloud": "k8s_cloud",
-            "user": "user",
-            "host": "1.1.1.1",
-            "port": 17070,
-            "ca_cert": "cacert",
-        }
-
-    def test_missing_vca_config_host(self):
-        db = Mock()
-        self.vca_config.pop("host")
-        with self.assertRaises(N2VCBadArgumentsException):
-            self.k8s_juju_conn = K8sJujuConnector(
-                fs=fslocal.FsLocal(),
-                db=db,
-                log=None,
-                loop=self.loop,
-                vca_config=self.vca_config,
-                on_update_db=None,
-            )
-
-    def test_missing_vca_config_user(self):
-        db = Mock()
-        self.vca_config.pop("user")
-        with self.assertRaises(N2VCBadArgumentsException):
-            self.k8s_juju_conn = K8sJujuConnector(
-                fs=fslocal.FsLocal(),
-                db=db,
-                log=None,
-                loop=self.loop,
-                vca_config=self.vca_config,
-                on_update_db=None,
-            )
-
-    def test_missing_vca_config_secret(self):
-        db = Mock()
-        self.vca_config.pop("secret")
-        with self.assertRaises(N2VCBadArgumentsException):
-            self.k8s_juju_conn = K8sJujuConnector(
-                fs=fslocal.FsLocal(),
-                db=db,
-                log=None,
-                loop=self.loop,
-                vca_config=self.vca_config,
-                on_update_db=None,
-            )
-
-    def test_missing_vca_config_ca_cert(self):
-        db = Mock()
-        self.vca_config.pop("ca_cert")
-        with self.assertRaises(N2VCBadArgumentsException):
-            self.k8s_juju_conn = K8sJujuConnector(
-                fs=fslocal.FsLocal(),
-                db=db,
-                log=None,
-                loop=self.loop,
-                vca_config=self.vca_config,
-                on_update_db=None,
-            )
+        self.k8s_juju_conn._store.get_vca_id.return_value = None
+        self.k8s_juju_conn.libjuju = Mock()
 
 
 @asynctest.mock.patch("n2vc.kubectl.Kubectl.get_default_storage_class")
 
 
 @asynctest.mock.patch("n2vc.kubectl.Kubectl.get_default_storage_class")
@@ -344,11 +230,7 @@ class InstallTest(K8sJujuConnTestCase):
             )
         )
         self.assertEqual(mock_chdir.call_count, 2)
             )
         )
         self.assertEqual(mock_chdir.call_count, 2)
-        self.k8s_juju_conn.libjuju.add_model.assert_called_once_with(
-            model_name=self.kdu_instance,
-            cloud_name=self.cluster_uuid,
-            credential_name="cred-{}".format(self.cluster_uuid),
-        )
+        self.k8s_juju_conn.libjuju.add_model.assert_called_once()
         self.k8s_juju_conn.libjuju.deploy.assert_called_once_with(
             "local:{}".format(self.local_bundle),
             model_name=self.kdu_instance,
         self.k8s_juju_conn.libjuju.deploy.assert_called_once_with(
             "local:{}".format(self.local_bundle),
             model_name=self.kdu_instance,
@@ -368,11 +250,7 @@ class InstallTest(K8sJujuConnTestCase):
                 timeout=1800,
             )
         )
                 timeout=1800,
             )
         )
-        self.k8s_juju_conn.libjuju.add_model.assert_called_once_with(
-            model_name=self.kdu_instance,
-            cloud_name=self.cluster_uuid,
-            credential_name="cred-{}".format(self.cluster_uuid),
-        )
+        self.k8s_juju_conn.libjuju.add_model.assert_called_once()
         self.k8s_juju_conn.libjuju.deploy.assert_called_once_with(
             self.cs_bundle,
             model_name=self.kdu_instance,
         self.k8s_juju_conn.libjuju.deploy.assert_called_once_with(
             self.cs_bundle,
             model_name=self.kdu_instance,
@@ -392,11 +270,7 @@ class InstallTest(K8sJujuConnTestCase):
                 timeout=1800,
             )
         )
                 timeout=1800,
             )
         )
-        self.k8s_juju_conn.libjuju.add_model.assert_called_once_with(
-            model_name=self.kdu_instance,
-            cloud_name=self.cluster_uuid,
-            credential_name="cred-{}".format(self.cluster_uuid),
-        )
+        self.k8s_juju_conn.libjuju.add_model.assert_called_once()
         self.k8s_juju_conn.libjuju.deploy.assert_called_once_with(
             self.http_bundle,
             model_name=self.kdu_instance,
         self.k8s_juju_conn.libjuju.deploy.assert_called_once_with(
             self.http_bundle,
             model_name=self.kdu_instance,
@@ -415,11 +289,7 @@ class InstallTest(K8sJujuConnTestCase):
                 timeout=1800,
             )
         )
                 timeout=1800,
             )
         )
-        self.k8s_juju_conn.libjuju.add_model.assert_called_once_with(
-            model_name=self.kdu_instance,
-            cloud_name=self.cluster_uuid,
-            credential_name="cred-{}".format(self.cluster_uuid),
-        )
+        self.k8s_juju_conn.libjuju.add_model.assert_called_once()
         self.k8s_juju_conn.libjuju.deploy.assert_called_once_with(
             self.cs_bundle,
             model_name=self.kdu_instance,
         self.k8s_juju_conn.libjuju.deploy.assert_called_once_with(
             self.cs_bundle,
             model_name=self.kdu_instance,
@@ -458,11 +328,7 @@ class InstallTest(K8sJujuConnTestCase):
                 timeout=1800,
             )
         )
                 timeout=1800,
             )
         )
-        self.k8s_juju_conn.libjuju.add_model.assert_called_once_with(
-            model_name=self.kdu_instance,
-            cloud_name=self.cluster_uuid,
-            credential_name="cred-{}".format(self.cluster_uuid),
-        )
+        self.k8s_juju_conn.libjuju.add_model.assert_called_once()
         self.k8s_juju_conn.libjuju.deploy.assert_called_once_with(
             self.cs_bundle,
             model_name=self.kdu_instance,
         self.k8s_juju_conn.libjuju.deploy.assert_called_once_with(
             self.cs_bundle,
             model_name=self.kdu_instance,
@@ -500,11 +366,7 @@ class InstallTest(K8sJujuConnTestCase):
                     timeout=1800,
                 )
             )
                     timeout=1800,
                 )
             )
-        self.k8s_juju_conn.libjuju.add_model.assert_called_once_with(
-            model_name=self.kdu_instance,
-            cloud_name=self.cluster_uuid,
-            credential_name="cred-{}".format(self.cluster_uuid),
-        )
+        self.k8s_juju_conn.libjuju.add_model.assert_called_once()
         self.k8s_juju_conn.libjuju.deploy.assert_called_once_with(
             "local:{}".format(self.local_bundle),
             model_name=self.kdu_instance,
         self.k8s_juju_conn.libjuju.deploy.assert_called_once_with(
             "local:{}".format(self.local_bundle),
             model_name=self.kdu_instance,
index 29bcb7b..fde6817 100644 (file)
 import asyncio
 import asynctest
 import tempfile
 import asyncio
 import asynctest
 import tempfile
-from unittest import mock
+from unittest.mock import Mock, patch
 import juju
 import kubernetes
 from juju.errors import JujuAPIError
 import logging
 import juju
 import kubernetes
 from juju.errors import JujuAPIError
 import logging
-from .utils import FakeN2VC, FakeMachine, FakeApplication
+from .utils import FakeMachine, FakeApplication
 from n2vc.libjuju import Libjuju
 from n2vc.exceptions import (
     JujuControllerFailedConnecting,
 from n2vc.libjuju import Libjuju
 from n2vc.exceptions import (
     JujuControllerFailedConnecting,
@@ -33,122 +33,46 @@ from n2vc.exceptions import (
     JujuError,
 )
 from n2vc.k8s_juju_conn import generate_rbac_id
     JujuError,
 )
 from n2vc.k8s_juju_conn import generate_rbac_id
+from n2vc.tests.unit.utils import AsyncMock
+from n2vc.vca.connection import Connection
+from n2vc.vca.connection_data import ConnectionData
 
 
 
 
+cacert = """-----BEGIN CERTIFICATE-----
+SOMECERT
+-----END CERTIFICATE-----"""
+
+
+@asynctest.mock.patch("n2vc.libjuju.Controller")
 class LibjujuTestCase(asynctest.TestCase):
 class LibjujuTestCase(asynctest.TestCase):
-    @asynctest.mock.patch("juju.controller.Controller.update_endpoints")
-    @asynctest.mock.patch("juju.client.connector.Connector.connect")
-    @asynctest.mock.patch("juju.controller.Controller.connection")
-    @asynctest.mock.patch("n2vc.libjuju.Libjuju._get_api_endpoints_db")
+    @asynctest.mock.patch("n2vc.vca.connection_data.base64_to_cacert")
     def setUp(
         self,
     def setUp(
         self,
-        mock__get_api_endpoints_db=None,
-        mock_connection=None,
-        mock_connect=None,
-        mock_update_endpoints=None,
-    ):
-        loop = asyncio.get_event_loop()
-        n2vc = FakeN2VC()
-        mock__get_api_endpoints_db.return_value = ["127.0.0.1:17070"]
-        endpoints = "127.0.0.1:17070"
-        username = "admin"
-        password = "secret"
-        cacert = """
-    -----BEGIN CERTIFICATE-----
-    SOMECERT
-    -----END CERTIFICATE-----"""
-        self.libjuju = Libjuju(
-            endpoints,
-            "192.168.0.155:17070",
-            username,
-            password,
-            cacert,
-            loop,
-            log=None,
-            db={"get_one": []},
-            n2vc=n2vc,
-        )
-        logging.disable(logging.CRITICAL)
-        loop.run_until_complete(self.libjuju.disconnect())
-
-
-@asynctest.mock.patch("n2vc.libjuju.Libjuju._create_health_check_task")
-@asynctest.mock.patch("n2vc.libjuju.Libjuju._update_api_endpoints_db")
-@asynctest.mock.patch("n2vc.libjuju.Libjuju._get_api_endpoints_db")
-class LibjujuInitTestCase(asynctest.TestCase):
-    def setUp(self):
+        mock_base64_to_cacert=None,
+    ):
         self.loop = asyncio.get_event_loop()
         self.loop = asyncio.get_event_loop()
-        self.n2vc = FakeN2VC()
-        self.endpoint = "192.168.100.100:17070"
-        self.username = "admin"
-        self.password = "secret"
-        self.cacert = """
-    -----BEGIN CERTIFICATE-----
-    SOMECERT
-    -----END CERTIFICATE-----"""
-
-    def test_endpoint_not_in_db(
-        self,
-        mock__get_api_endpoints_db,
-        mock_update_endpoints,
-        mock_create_health_check_task,
-    ):
-        mock__get_api_endpoints_db.return_value = ["another_ip"]
-        Libjuju(
-            self.endpoint,
-            "192.168.0.155:17070",
-            self.username,
-            self.password,
-            self.cacert,
-            self.loop,
-            log=None,
-            db={"get_one": []},
-            n2vc=self.n2vc,
+        self.db = Mock()
+        mock_base64_to_cacert.return_value = cacert
+        Connection._load_vca_connection_data = Mock()
+        vca_connection = Connection(AsyncMock())
+        vca_connection._data = ConnectionData(
+            **{
+                "endpoints": ["1.2.3.4:17070"],
+                "user": "user",
+                "secret": "secret",
+                "cacert": "cacert",
+                "pubkey": "pubkey",
+                "lxd-cloud": "cloud",
+                "lxd-credentials": "credentials",
+                "k8s-cloud": "k8s_cloud",
+                "k8s-credentials": "k8s_credentials",
+                "model-config": {},
+                "api-proxy": "api_proxy",
+            }
         )
         )
-        mock_update_endpoints.assert_called_once_with([self.endpoint])
-        mock__get_api_endpoints_db.assert_called_once()
-
-    def test_endpoint_in_db(
-        self,
-        mock__get_api_endpoints_db,
-        mock_update_endpoints,
-        mock_create_health_check_task,
-    ):
-        mock__get_api_endpoints_db.return_value = [self.endpoint, "another_ip"]
-        Libjuju(
-            self.endpoint,
-            "192.168.0.155:17070",
-            self.username,
-            self.password,
-            self.cacert,
-            self.loop,
-            log=None,
-            db={"get_one": []},
-            n2vc=self.n2vc,
-        )
-        mock_update_endpoints.assert_not_called()
-        mock__get_api_endpoints_db.assert_called_once()
-
-    def test_no_db_endpoints(
-        self,
-        mock__get_api_endpoints_db,
-        mock_update_endpoints,
-        mock_create_health_check_task,
-    ):
-        mock__get_api_endpoints_db.return_value = None
-        Libjuju(
-            self.endpoint,
-            "192.168.0.155:17070",
-            self.username,
-            self.password,
-            self.cacert,
-            self.loop,
-            log=None,
-            db={"get_one": []},
-            n2vc=self.n2vc,
-        )
-        mock_update_endpoints.assert_called_once_with([self.endpoint])
-        mock__get_api_endpoints_db.assert_called_once()
+        logging.disable(logging.CRITICAL)
+        self.libjuju = Libjuju(vca_connection, self.loop)
+        self.loop.run_until_complete(self.libjuju.disconnect())
 
 
 @asynctest.mock.patch("juju.controller.Controller.connect")
 
 
 @asynctest.mock.patch("juju.controller.Controller.connect")
@@ -156,41 +80,34 @@ class LibjujuInitTestCase(asynctest.TestCase):
     "juju.controller.Controller.api_endpoints",
     new_callable=asynctest.CoroutineMock(return_value=["127.0.0.1:17070"]),
 )
     "juju.controller.Controller.api_endpoints",
     new_callable=asynctest.CoroutineMock(return_value=["127.0.0.1:17070"]),
 )
-@asynctest.mock.patch("n2vc.libjuju.Libjuju._update_api_endpoints_db")
 class GetControllerTest(LibjujuTestCase):
     def setUp(self):
         super(GetControllerTest, self).setUp()
 
 class GetControllerTest(LibjujuTestCase):
     def setUp(self):
         super(GetControllerTest, self).setUp()
 
-    def test_diff_endpoint(
-        self, mock__update_api_endpoints_db, mock_api_endpoints, mock_connect
-    ):
+    def test_diff_endpoint(self, mock_api_endpoints, mock_connect):
         self.libjuju.endpoints = []
         controller = self.loop.run_until_complete(self.libjuju.get_controller())
         self.libjuju.endpoints = []
         controller = self.loop.run_until_complete(self.libjuju.get_controller())
-        mock__update_api_endpoints_db.assert_called_once_with(["127.0.0.1:17070"])
         self.assertIsInstance(controller, juju.controller.Controller)
 
     @asynctest.mock.patch("n2vc.libjuju.Libjuju.disconnect_controller")
     def test_exception(
         self,
         mock_disconnect_controller,
         self.assertIsInstance(controller, juju.controller.Controller)
 
     @asynctest.mock.patch("n2vc.libjuju.Libjuju.disconnect_controller")
     def test_exception(
         self,
         mock_disconnect_controller,
-        mock__update_api_endpoints_db,
         mock_api_endpoints,
         mock_connect,
     ):
         self.libjuju.endpoints = []
         mock_api_endpoints,
         mock_connect,
     ):
         self.libjuju.endpoints = []
-        mock__update_api_endpoints_db.side_effect = Exception()
+
+        mock_connect.side_effect = Exception()
         controller = None
         with self.assertRaises(JujuControllerFailedConnecting):
             controller = self.loop.run_until_complete(self.libjuju.get_controller())
         self.assertIsNone(controller)
         mock_disconnect_controller.assert_called_once()
 
         controller = None
         with self.assertRaises(JujuControllerFailedConnecting):
             controller = self.loop.run_until_complete(self.libjuju.get_controller())
         self.assertIsNone(controller)
         mock_disconnect_controller.assert_called_once()
 
-    def test_same_endpoint_get_controller(
-        self, mock__update_api_endpoints_db, mock_api_endpoints, mock_connect
-    ):
+    def test_same_endpoint_get_controller(self, mock_api_endpoints, mock_connect):
         self.libjuju.endpoints = ["127.0.0.1:17070"]
         controller = self.loop.run_until_complete(self.libjuju.get_controller())
         self.libjuju.endpoints = ["127.0.0.1:17070"]
         controller = self.loop.run_until_complete(self.libjuju.get_controller())
-        mock__update_api_endpoints_db.assert_not_called()
         self.assertIsInstance(controller, juju.controller.Controller)
 
 
         self.assertIsInstance(controller, juju.controller.Controller)
 
 
@@ -231,9 +148,7 @@ class AddModelTest(LibjujuTestCase):
         mock_model_exists.return_value = True
 
         # This should not raise an exception
         mock_model_exists.return_value = True
 
         # This should not raise an exception
-        self.loop.run_until_complete(
-            self.libjuju.add_model("existing_model", "cloud")
-        )
+        self.loop.run_until_complete(self.libjuju.add_model("existing_model", "cloud"))
 
         mock_disconnect_controller.assert_called()
 
 
         mock_disconnect_controller.assert_called()
 
@@ -251,7 +166,7 @@ class AddModelTest(LibjujuTestCase):
         mock_get_controller.return_value = juju.controller.Controller()
 
         self.loop.run_until_complete(
         mock_get_controller.return_value = juju.controller.Controller()
 
         self.loop.run_until_complete(
-            self.libjuju.add_model("nonexisting_model", "cloud")
+            self.libjuju.add_model("nonexisting_model", Mock())
         )
 
         mock_add_model.assert_called_once()
         )
 
         mock_add_model.assert_called_once()
@@ -1863,7 +1778,7 @@ class GetK8sCloudCredentials(LibjujuTestCase):
         mock_configuration.key_file = None
         self.token = None
         self.cert_data = None
         mock_configuration.key_file = None
         self.token = None
         self.cert_data = None
-        with mock.patch.object(self.libjuju.log, "debug") as mock_debug:
+        with patch.object(self.libjuju.log, "debug") as mock_debug:
             credential = self.libjuju.get_k8s_cloud_credential(
                 mock_configuration,
                 self.cert_data,
             credential = self.libjuju.get_k8s_cloud_credential(
                 mock_configuration,
                 self.cert_data,
index e5e26be..d89de3f 100644 (file)
@@ -22,143 +22,145 @@ import asynctest
 from n2vc.n2vc_juju_conn import N2VCJujuConnector
 from osm_common import fslocal
 from n2vc.exceptions import (
 from n2vc.n2vc_juju_conn import N2VCJujuConnector
 from osm_common import fslocal
 from n2vc.exceptions import (
-    JujuK8sProxycharmNotSupported,
     N2VCBadArgumentsException,
     N2VCException,
 )
     N2VCBadArgumentsException,
     N2VCException,
 )
+from n2vc.tests.unit.utils import AsyncMock
+from n2vc.vca.connection_data import ConnectionData
 
 
 class N2VCJujuConnTestCase(asynctest.TestCase):
 
 
 class N2VCJujuConnTestCase(asynctest.TestCase):
-    @asynctest.mock.patch("n2vc.libjuju.Libjuju._create_health_check_task")
-    @asynctest.mock.patch("juju.controller.Controller.update_endpoints")
-    @asynctest.mock.patch("juju.client.connector.Connector.connect")
-    @asynctest.mock.patch("juju.controller.Controller.connection")
-    @asynctest.mock.patch("n2vc.libjuju.Libjuju._get_api_endpoints_db")
+    @asynctest.mock.patch("n2vc.n2vc_juju_conn.MotorStore")
+    @asynctest.mock.patch("n2vc.n2vc_juju_conn.get_connection")
+    @asynctest.mock.patch("n2vc.vca.connection_data.base64_to_cacert")
     def setUp(
         self,
     def setUp(
         self,
-        mock__get_api_endpoints_db=None,
-        mock_connection=None,
-        mock_connect=None,
-        mock_update_endpoints=None,
-        mock__create_health_check_task=None,
+        mock_base64_to_cacert=None,
+        mock_get_connection=None,
+        mock_store=None,
     ):
     ):
-        mock__get_api_endpoints_db.return_value = ["2.2.2.2:17070"]
-        loop = asyncio.get_event_loop()
-        db = {}
-        vca_config = {
-            "secret": "secret",
-            "api_proxy": "api_proxy",
-            "cloud": "cloud",
-            "k8s_cloud": "k8s_cloud",
-        }
-
+        self.loop = asyncio.get_event_loop()
+        self.db = Mock()
+        mock_base64_to_cacert.return_value = """
+    -----BEGIN CERTIFICATE-----
+    SOMECERT
+    -----END CERTIFICATE-----"""
+        mock_store.return_value = AsyncMock()
+        mock_vca_connection = Mock()
+        mock_get_connection.return_value = mock_vca_connection
+        mock_vca_connection.data.return_value = ConnectionData(
+            **{
+                "endpoints": ["1.2.3.4:17070"],
+                "user": "user",
+                "secret": "secret",
+                "cacert": "cacert",
+                "pubkey": "pubkey",
+                "lxd-cloud": "cloud",
+                "lxd-credentials": "credentials",
+                "k8s-cloud": "k8s_cloud",
+                "k8s-credentials": "k8s_credentials",
+                "model-config": {},
+                "api-proxy": "api_proxy",
+            }
+        )
         logging.disable(logging.CRITICAL)
 
         N2VCJujuConnector.get_public_key = Mock()
         self.n2vc = N2VCJujuConnector(
         logging.disable(logging.CRITICAL)
 
         N2VCJujuConnector.get_public_key = Mock()
         self.n2vc = N2VCJujuConnector(
-            db=db,
+            db=self.db,
             fs=fslocal.FsLocal(),
             log=None,
             fs=fslocal.FsLocal(),
             log=None,
-            loop=loop,
-            url="2.2.2.2:17070",
-            username="admin",
-            vca_config=vca_config,
+            loop=self.loop,
             on_update_db=None,
         )
         N2VCJujuConnector.get_public_key.assert_not_called()
             on_update_db=None,
         )
         N2VCJujuConnector.get_public_key.assert_not_called()
+        self.n2vc.libjuju = Mock()
 
 
 
 
-@asynctest.mock.patch("n2vc.libjuju.Libjuju.get_metrics")
 class GetMetricssTest(N2VCJujuConnTestCase):
     def setUp(self):
         super(GetMetricssTest, self).setUp()
 class GetMetricssTest(N2VCJujuConnTestCase):
     def setUp(self):
         super(GetMetricssTest, self).setUp()
+        self.n2vc.libjuju.get_metrics = AsyncMock()
 
 
-    def test_success(self, mock_get_metrics):
+    def test_success(self):
         _ = self.loop.run_until_complete(self.n2vc.get_metrics("model", "application"))
         _ = self.loop.run_until_complete(self.n2vc.get_metrics("model", "application"))
-        mock_get_metrics.assert_called_once()
+        self.n2vc.libjuju.get_metrics.assert_called_once()
 
 
-    def test_except(self, mock_get_metrics):
-        mock_get_metrics.side_effect = Exception()
+    def test_except(self):
+        self.n2vc.libjuju.get_metrics.side_effect = Exception()
         with self.assertRaises(Exception):
             _ = self.loop.run_until_complete(
                 self.n2vc.get_metrics("model", "application")
             )
         with self.assertRaises(Exception):
             _ = self.loop.run_until_complete(
                 self.n2vc.get_metrics("model", "application")
             )
-        mock_get_metrics.assert_called_once()
+        self.n2vc.libjuju.get_metrics.assert_called_once()
 
 
 
 
-@asynctest.mock.patch("n2vc.libjuju.Libjuju.get_controller")
-@asynctest.mock.patch("n2vc.libjuju.Libjuju.get_model")
-@asynctest.mock.patch("n2vc.libjuju.Libjuju.get_executed_actions")
-@asynctest.mock.patch("n2vc.libjuju.Libjuju.get_actions")
-@asynctest.mock.patch("n2vc.libjuju.Libjuju.get_application_configs")
-@asynctest.mock.patch("n2vc.libjuju.Libjuju._get_application")
 class UpdateVcaStatusTest(N2VCJujuConnTestCase):
     def setUp(self):
         super(UpdateVcaStatusTest, self).setUp()
 class UpdateVcaStatusTest(N2VCJujuConnTestCase):
     def setUp(self):
         super(UpdateVcaStatusTest, self).setUp()
+        self.n2vc.libjuju.get_controller = AsyncMock()
+        self.n2vc.libjuju.get_model = AsyncMock()
+        self.n2vc.libjuju.get_executed_actions = AsyncMock()
+        self.n2vc.libjuju.get_actions = AsyncMock()
+        self.n2vc.libjuju.get_application_configs = AsyncMock()
+        self.n2vc.libjuju._get_application = AsyncMock()
 
     def test_success(
         self,
 
     def test_success(
         self,
-        mock_get_application,
-        mock_get_application_configs,
-        mock_get_actions,
-        mock_get_executed_actions,
-        mock_get_model,
-        mock_get_controller,
     ):
     ):
-        self.loop.run_until_complete(self.n2vc.update_vca_status(
-            {"model": {"applications": {"app": {"actions": {}}}}}))
-        mock_get_executed_actions.assert_called_once()
-        mock_get_actions.assert_called_once()
-        mock_get_application_configs.assert_called_once()
+        self.loop.run_until_complete(
+            self.n2vc.update_vca_status(
+                {"model": {"applications": {"app": {"actions": {}}}}}
+            )
+        )
+        self.n2vc.libjuju.get_executed_actions.assert_called_once()
+        self.n2vc.libjuju.get_actions.assert_called_once()
+        self.n2vc.libjuju.get_application_configs.assert_called_once()
 
 
-    def test_exception(
-        self,
-        mock_get_application,
-        mock_get_application_configs,
-        mock_get_actions,
-        mock_get_executed_actions,
-        mock_get_model,
-        mock_get_controller,
-    ):
-        mock_get_model.return_value = None
-        mock_get_executed_actions.side_effect = Exception()
+    def test_exception(self):
+        self.n2vc.libjuju.get_model.return_value = None
+        self.n2vc.libjuju.get_executed_actions.side_effect = Exception()
         with self.assertRaises(Exception):
         with self.assertRaises(Exception):
-            self.loop.run_until_complete(self.n2vc.update_vca_status(
-                {"model": {"applications": {"app": {"actions": {}}}}}))
-            mock_get_executed_actions.assert_not_called()
-            mock_get_actions.assert_not_called_once()
-            mock_get_application_configs.assert_not_called_once()
+            self.loop.run_until_complete(
+                self.n2vc.update_vca_status(
+                    {"model": {"applications": {"app": {"actions": {}}}}}
+                )
+            )
+            self.n2vc.libjuju.get_executed_actions.assert_not_called()
+            self.n2vc.libjuju.get_actions.assert_not_called_once()
+            self.n2vc.libjuju.get_application_configs.assert_not_called_once()
 
 
 
 
-@asynctest.mock.patch("n2vc.libjuju.Libjuju.model_exists")
 @asynctest.mock.patch("osm_common.fslocal.FsLocal.file_exists")
 @asynctest.mock.patch(
     "osm_common.fslocal.FsLocal.path", new_callable=asynctest.PropertyMock, create=True
 )
 @asynctest.mock.patch("osm_common.fslocal.FsLocal.file_exists")
 @asynctest.mock.patch(
     "osm_common.fslocal.FsLocal.path", new_callable=asynctest.PropertyMock, create=True
 )
-@asynctest.mock.patch("n2vc.libjuju.Libjuju.deploy_charm")
-@asynctest.mock.patch("n2vc.libjuju.Libjuju.add_model")
 class K8sProxyCharmsTest(N2VCJujuConnTestCase):
     def setUp(self):
         super(K8sProxyCharmsTest, self).setUp()
 class K8sProxyCharmsTest(N2VCJujuConnTestCase):
     def setUp(self):
         super(K8sProxyCharmsTest, self).setUp()
+        self.n2vc.libjuju.model_exists = AsyncMock()
+        self.n2vc.libjuju.add_model = AsyncMock()
+        self.n2vc.libjuju.deploy_charm = AsyncMock()
+        self.n2vc.libjuju.model_exists.return_value = False
 
     def test_success(
 
     def test_success(
-        self, mock_add_model, mock_deploy_charm, mock_path, mock_file_exists, mock_model_exists
+        self,
+        mock_path,
+        mock_file_exists,
     ):
     ):
-        mock_model_exists.return_value = None
         mock_file_exists.return_value = True
         mock_path.return_value = "/path"
         ee_id = self.loop.run_until_complete(
             self.n2vc.install_k8s_proxy_charm(
         mock_file_exists.return_value = True
         mock_path.return_value = "/path"
         ee_id = self.loop.run_until_complete(
             self.n2vc.install_k8s_proxy_charm(
-                "charm", "nsi-id.ns-id.vnf-id.vdu", "////path/", {},
+                "charm",
+                "nsi-id.ns-id.vnf-id.vdu",
+                "////path/",
+                {},
             )
         )
 
             )
         )
 
-        mock_add_model.assert_called_once_with(
-            "ns-id-k8s",
-            cloud_name=self.n2vc.k8s_cloud,
-            credential_name=self.n2vc.k8s_cloud
-        )
-        mock_deploy_charm.assert_called_once_with(
+        self.n2vc.libjuju.add_model.assert_called_once()
+        self.n2vc.libjuju.deploy_charm.assert_called_once_with(
             model_name="ns-id-k8s",
             application_name="app-vnf-vnf-id-vdu-vdu",
             path="/path/path/",
             model_name="ns-id-k8s",
             application_name="app-vnf-vnf-id-vdu-vdu",
             path="/path/path/",
@@ -170,74 +172,70 @@ class K8sProxyCharmsTest(N2VCJujuConnTestCase):
         )
         self.assertEqual(ee_id, "ns-id-k8s.app-vnf-vnf-id-vdu-vdu.k8s")
 
         )
         self.assertEqual(ee_id, "ns-id-k8s.app-vnf-vnf-id-vdu-vdu.k8s")
 
-    @asynctest.mock.patch(
-        "n2vc.n2vc_juju_conn.N2VCJujuConnector.k8s_cloud",
-        new_callable=asynctest.PropertyMock,
-        create=True,
-    )
-    def test_no_k8s_cloud(
+    def test_no_artifact_path(
         self,
         self,
-        mock_k8s_cloud,
-        mock_add_model,
-        mock_deploy_charm,
         mock_path,
         mock_file_exists,
         mock_path,
         mock_file_exists,
-        mock_model_exists,
-    ):
-        mock_k8s_cloud.return_value = None
-        with self.assertRaises(JujuK8sProxycharmNotSupported):
-            ee_id = self.loop.run_until_complete(
-                self.n2vc.install_k8s_proxy_charm(
-                    "charm", "nsi-id.ns-id.vnf-id.vdu", "/path/", {},
-                )
-            )
-            self.assertIsNone(ee_id)
-
-    def test_no_artifact_path(
-        self, mock_add_model, mock_deploy_charm, mock_path, mock_file_exists, mock_model_exists,
     ):
         with self.assertRaises(N2VCBadArgumentsException):
             ee_id = self.loop.run_until_complete(
                 self.n2vc.install_k8s_proxy_charm(
     ):
         with self.assertRaises(N2VCBadArgumentsException):
             ee_id = self.loop.run_until_complete(
                 self.n2vc.install_k8s_proxy_charm(
-                    "charm", "nsi-id.ns-id.vnf-id.vdu", "", {},
+                    "charm",
+                    "nsi-id.ns-id.vnf-id.vdu",
+                    "",
+                    {},
                 )
             )
             self.assertIsNone(ee_id)
 
     def test_no_db(
                 )
             )
             self.assertIsNone(ee_id)
 
     def test_no_db(
-        self, mock_add_model, mock_deploy_charm, mock_path, mock_file_exists, mock_model_exists,
+        self,
+        mock_path,
+        mock_file_exists,
     ):
         with self.assertRaises(N2VCBadArgumentsException):
             ee_id = self.loop.run_until_complete(
                 self.n2vc.install_k8s_proxy_charm(
     ):
         with self.assertRaises(N2VCBadArgumentsException):
             ee_id = self.loop.run_until_complete(
                 self.n2vc.install_k8s_proxy_charm(
-                    "charm", "nsi-id.ns-id.vnf-id.vdu", "/path/", None,
+                    "charm",
+                    "nsi-id.ns-id.vnf-id.vdu",
+                    "/path/",
+                    None,
                 )
             )
             self.assertIsNone(ee_id)
 
     def test_file_not_exists(
                 )
             )
             self.assertIsNone(ee_id)
 
     def test_file_not_exists(
-        self, mock_add_model, mock_deploy_charm, mock_path, mock_file_exists, mock_model_exists,
+        self,
+        mock_path,
+        mock_file_exists,
     ):
         mock_file_exists.return_value = False
         with self.assertRaises(N2VCBadArgumentsException):
             ee_id = self.loop.run_until_complete(
                 self.n2vc.install_k8s_proxy_charm(
     ):
         mock_file_exists.return_value = False
         with self.assertRaises(N2VCBadArgumentsException):
             ee_id = self.loop.run_until_complete(
                 self.n2vc.install_k8s_proxy_charm(
-                    "charm", "nsi-id.ns-id.vnf-id.vdu", "/path/", {},
+                    "charm",
+                    "nsi-id.ns-id.vnf-id.vdu",
+                    "/path/",
+                    {},
                 )
             )
             self.assertIsNone(ee_id)
 
     def test_exception(
                 )
             )
             self.assertIsNone(ee_id)
 
     def test_exception(
-        self, mock_add_model, mock_deploy_charm, mock_path, mock_file_exists, mock_model_exists,
+        self,
+        mock_path,
+        mock_file_exists,
     ):
     ):
-        mock_model_exists.return_value = None
         mock_file_exists.return_value = True
         mock_path.return_value = "/path"
         mock_file_exists.return_value = True
         mock_path.return_value = "/path"
-        mock_deploy_charm.side_effect = Exception()
+        self.n2vc.libjuju.deploy_charm.side_effect = Exception()
         with self.assertRaises(N2VCException):
             ee_id = self.loop.run_until_complete(
                 self.n2vc.install_k8s_proxy_charm(
         with self.assertRaises(N2VCException):
             ee_id = self.loop.run_until_complete(
                 self.n2vc.install_k8s_proxy_charm(
-                    "charm", "nsi-id.ns-id.vnf-id.vdu", "path/", {},
+                    "charm",
+                    "nsi-id.ns-id.vnf-id.vdu",
+                    "path/",
+                    {},
                 )
             )
             self.assertIsNone(ee_id)
                 )
             )
             self.assertIsNone(ee_id)
diff --git a/n2vc/tests/unit/test_store.py b/n2vc/tests/unit/test_store.py
new file mode 100644 (file)
index 0000000..c7aa2d6
--- /dev/null
@@ -0,0 +1,288 @@
+# Copyright 2020 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
+from base64 import b64decode
+from unittest import TestCase
+from unittest.mock import Mock, patch
+
+
+from n2vc.store import DbMongoStore, MotorStore
+from n2vc.vca.connection_data import ConnectionData
+from n2vc.tests.unit.utils import AsyncMock
+from osm_common.dbmongo import DbException
+
+
+class TestDbMongoStore(TestCase):
+    def setUp(self):
+        self.store = DbMongoStore(Mock())
+        self.loop = asyncio.get_event_loop()
+
+    @patch("n2vc.vca.connection_data.base64_to_cacert")
+    def test_get_vca_connection_data(self, mock_base64_to_cacert):
+        mock_base64_to_cacert.return_value = "cacert"
+        conn_data = {
+            "endpoints": ["1.2.3.4:17070"],
+            "user": "admin",
+            "secret": "1234",
+            "cacert": "cacert",
+            "pubkey": "pubkey",
+            "lxd-cloud": "lxd-cloud",
+            "lxd-credentials": "lxd-credentials",
+            "k8s-cloud": "k8s-cloud",
+            "k8s-credentials": "k8s-credentials",
+            "model-config": {},
+            "api-proxy": None,
+        }
+        db_get_one = conn_data.copy()
+        db_get_one.update({"schema_version": "1.1", "_id": "id"})
+        self.store.db.get_one.return_value = db_get_one
+        connection_data = self.loop.run_until_complete(
+            self.store.get_vca_connection_data("vca_id")
+        )
+        self.assertTrue(
+            all(
+                connection_data.__dict__[k.replace("-", "_")] == v
+                for k, v in conn_data.items()
+            )
+        )
+
+    def test_update_vca_endpoints(self):
+        endpoints = ["1.2.3.4:17070"]
+        self.store.db.get_one.side_effect = [None, {"api_endpoints": []}]
+        self.store.db.create.side_effect = DbException("already exists")
+        self.loop.run_until_complete(self.store.update_vca_endpoints(endpoints))
+        self.assertEqual(self.store.db.get_one.call_count, 2)
+        Mock()
+        self.store.db.set_one.assert_called_once_with(
+            "vca", {"_id": "juju"}, {"api_endpoints": endpoints}
+        )
+
+    def test_update_vca_endpoints_exception(self):
+        endpoints = ["1.2.3.4:17070"]
+        self.store.db.get_one.side_effect = [None, None]
+        self.store.db.create.side_effect = DbException("already exists")
+        with self.assertRaises(DbException):
+            self.loop.run_until_complete(self.store.update_vca_endpoints(endpoints))
+        self.assertEqual(self.store.db.get_one.call_count, 2)
+        self.store.db.set_one.assert_not_called()
+
+    def test_update_vca_endpoints_with_vca_id(self):
+        endpoints = ["1.2.3.4:17070"]
+        self.store.db.get_one.return_value = {}
+        self.loop.run_until_complete(
+            self.store.update_vca_endpoints(endpoints, "vca_id")
+        )
+        self.store.db.get_one.assert_called_once_with("vca", q_filter={"_id": "vca_id"})
+        self.store.db.replace.assert_called_once_with(
+            "vca", "vca_id", {"endpoints": endpoints}
+        )
+
+    def test_get_vca_endpoints(self):
+        endpoints = ["1.2.3.4:17070"]
+        db_data = {"api_endpoints": endpoints}
+        db_returns = [db_data, None]
+        expected_returns = [endpoints, []]
+        returns = []
+        self.store._get_juju_info = Mock()
+        self.store._get_juju_info.side_effect = db_returns
+        for _ in range(len(db_returns)):
+            e = self.loop.run_until_complete(self.store.get_vca_endpoints())
+            returns.append(e)
+        self.assertEqual(expected_returns, returns)
+
+    @patch("n2vc.vca.connection_data.base64_to_cacert")
+    def test_get_vca_endpoints_with_vca_id(self, mock_base64_to_cacert):
+        expected_endpoints = ["1.2.3.4:17070"]
+        mock_base64_to_cacert.return_value = "cacert"
+        self.store.get_vca_connection_data = Mock()
+        self.store.get_vca_connection_data.return_value = ConnectionData(
+            **{
+                "endpoints": expected_endpoints,
+                "user": "admin",
+                "secret": "1234",
+                "cacert": "cacert",
+            }
+        )
+        endpoints = self.loop.run_until_complete(self.store.get_vca_endpoints("vca_id"))
+        self.store.get_vca_connection_data.assert_called_with("vca_id")
+        self.assertEqual(expected_endpoints, endpoints)
+
+    def test_get_vca_id(self):
+        self.assertIsNone(self.loop.run_until_complete(self.store.get_vca_id()))
+
+    def test_get_vca_id_with_vim_id(self):
+        self.store.db.get_one.return_value = {"vca": "vca_id"}
+        vca_id = self.loop.run_until_complete(self.store.get_vca_id("vim_id"))
+        self.store.db.get_one.assert_called_once_with(
+            "vim_accounts", q_filter={"_id": "vim_id"}, fail_on_empty=False
+        )
+        self.assertEqual(vca_id, "vca_id")
+
+
+class TestMotorStore(TestCase):
+    def setUp(self):
+        self.store = MotorStore("uri")
+        self.vca_collection = Mock()
+        self.vca_collection.find_one = AsyncMock()
+        self.vca_collection.insert_one = AsyncMock()
+        self.vca_collection.replace_one = AsyncMock()
+        self.admin_collection = Mock()
+        self.admin_collection.find_one = AsyncMock()
+        self.admin_collection.insert_one = AsyncMock()
+        self.admin_collection.replace_one = AsyncMock()
+        self.vim_accounts_collection = Mock()
+        self.vim_accounts_collection.find_one = AsyncMock()
+        self.store._client = {
+            "osm": {
+                "vca": self.vca_collection,
+                "admin": self.admin_collection,
+                "vim_accounts": self.vim_accounts_collection,
+            }
+        }
+        self.store._config = {"database_commonkey": "osm"}
+        # self.store.decrypt_fields = Mock()
+        self.loop = asyncio.get_event_loop()
+
+    @patch("n2vc.vca.connection_data.base64_to_cacert")
+    def test_get_vca_connection_data(self, mock_base64_to_cacert):
+        mock_base64_to_cacert.return_value = "cacert"
+        conn_data = {
+            "endpoints": ["1.2.3.4:17070"],
+            "user": "admin",
+            "secret": "1234",
+            "cacert": "cacert",
+            "pubkey": "pubkey",
+            "lxd-cloud": "lxd-cloud",
+            "lxd-credentials": "lxd-credentials",
+            "k8s-cloud": "k8s-cloud",
+            "k8s-credentials": "k8s-credentials",
+            "model-config": {},
+            "api-proxy": None,
+        }
+        db_find_one = conn_data.copy()
+        db_find_one.update({"schema_version": "1.1", "_id": "id"})
+        self.vca_collection.find_one.return_value = db_find_one
+        self.store.decrypt_fields = AsyncMock()
+        connection_data = self.loop.run_until_complete(
+            self.store.get_vca_connection_data("vca_id")
+        )
+        self.assertTrue(
+            all(
+                connection_data.__dict__[k.replace("-", "_")] == v
+                for k, v in conn_data.items()
+            )
+        )
+
+    @patch("n2vc.vca.connection_data.base64_to_cacert")
+    def test_get_vca_connection_data_exception(self, mock_base64_to_cacert):
+        mock_base64_to_cacert.return_value = "cacert"
+        self.vca_collection.find_one.return_value = None
+        with self.assertRaises(Exception):
+            self.loop.run_until_complete(self.store.get_vca_connection_data("vca_id"))
+
+    def test_update_vca_endpoints(self):
+        endpoints = ["1.2.3.4:17070"]
+        self.admin_collection.find_one.side_effect = [None, {"api_endpoints": []}]
+        self.admin_collection.insert_one.side_effect = DbException("already exists")
+        self.loop.run_until_complete(self.store.update_vca_endpoints(endpoints))
+        self.assertEqual(self.admin_collection.find_one.call_count, 2)
+        self.admin_collection.replace_one.assert_called_once_with(
+            {"_id": "juju"}, {"api_endpoints": ["1.2.3.4:17070"]}
+        )
+
+    def test_get_vca_connection_data_with_id(self):
+        secret = "e7b253af37785045d1ca08b8d929e556"
+        encrypted_secret = "kI46kRJh828ExSNpr16OG/q5a5/qTsE0bsHrv/W/2/g="
+        cacert = "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUQ4ekNDQWx1Z0F3SUJBZ0lVRWlzTTBoQWxiYzQ0Z1ZhZWh6bS80ZUsyNnRZd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0lURU5NQXNHQTFVRUNoTUVTblZxZFRFUU1BNEdBMVVFQXhNSGFuVnFkUzFqWVRBZUZ3MHlNVEEwTWpNeApNRFV3TXpSYUZ3MHpNVEEwTWpNeE1EVTFNelJhTUNFeERUQUxCZ05WQkFvVEJFcDFhblV4RURBT0JnTlZCQU1UCkIycDFhblV0WTJFd2dnR2lNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUJqd0F3Z2dHS0FvSUJnUUNhTmFvNGZab2gKTDJWYThtdy9LdCs3RG9tMHBYTlIvbEUxSHJyVmZvbmZqZFVQV01zSHpTSjJZZXlXcUNSd3BiaHlLaE82N1c1dgpUY2RsV3Y3WGFLTGtsdVkraDBZY3BQT3BFTmZZYmxrNGk0QkV1L0wzYVY5MFFkUFFrMG94S01CS2R5QlBNZVNNCkJmS2pPWXdyOGgzM0ZWUWhmVkJnMXVGZ2tGaDdTamNuNHczUFdvc1BCMjNiVHBCbGR3VE9zemN4Qm9TaDNSVTkKTzZjb3lQdDdEN0drOCtHRlA3RGRUQTdoV1RkaUM4cDBkeHp2RUNmY0psMXNFeFEyZVprS1QvVzZyelNtVDhUTApCM0ErM1FDRDhEOEVsQU1IVy9zS25SeHphYU8welpNVmVlQnRnNlFGZ1F3M0dJMGo2ZTY0K2w3VExoOW8wSkZVCjdpUitPY01xUzVDY0NROGpWV3JPSk9Xc2dEbDZ4T2FFREczYnR5SVJHY29jbVcvcEZFQjNZd1A2S1BRTUIrNXkKWDdnZExEWmFGRFBVakZmblhkMnhHdUZlMnpRTDNVbXZEUkZuUlBBaW02QlpQbWo1OFh2emFhZXROa3lyaUZLZwp4Z0Z1dVpTcDUwV2JWdjF0MkdzOTMrRE53NlhFZHRFYnlWWUNBa28xTTY0MkozczFnN3NoQnRFQ0F3RUFBYU1qCk1DRXdEZ1lEVlIwUEFRSC9CQVFEQWdLa01BOEdBMVVkRXdFQi93UUZNQU1CQWY4d0RRWUpLb1pJaHZjTkFRRUwKQlFBRGdnR0JBRXYxM2o2ZGFVbDBqeERPSnNTV1ZJZS9JdXNXVTRpN2ZXSWlqMHAwRU1GNS9LTE8yemRndTR5SQoreVd2T3N5aVFPanEzMlRYVlo2bTRDSnBkR1dGVE5HK2lLdXVOU3M0N3g3Q3dmVUNBWm5VVzhyamd3ZWJyS3BmCkJMNEVQcTZTcW0rSmltN0VPankyMWJkY2cyUXdZb3A3eUhvaHcveWEvL0l6RTMzVzZxNHlJeEFvNDBVYUhPTEMKTGtGbnNVYitjcFZBeFlPZGp6bjFzNWhnclpuWXlETEl3WmtIdFdEWm94alUzeC9jdnZzZ1FzLytzTWYrRFU4RgpZMkJKRHJjQ1VQM2xzclc0QVpFMFplZkEwOTlncFEvb3dSN0REYnMwSjZUeFM4NGt6Tldjc1FuWnRraXZheHJNClkyVHNnaWVndFExVFdGRWpxLy9sUFV4emJCdmpnd1FBZm5CQXZGeVNKejdTa0VuVm5rUXJGaUlUQVArTHljQVIKMlg4UFI2ZGI1bEt0SitBSENDM3kvZmNQS2k0ZzNTL3djeXRRdmdvOXJ6ODRFalp5YUNTaGJXNG9jNzNrMS9RcAowQWtHRDU0ZGVDWWVPYVJNbW96c0w3ZzdxWkpFekhtODdOcVBYSy9EZFoweWNxaVFhMXY2T3QxNjdXNUlzMUkzCjBWb0IzUzloSlE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCgo="  # noqa: E501
+        encrypted_cacert = "QeV4evTLXzcKwZZvmXQ/OvSHToXH3ISwfoLmU+Q9JlQWAFUHSJ9IhO0ewaQrJmx3NkfFb7NCxsQhh+wE57zDW4rWgn4w/SWkzvwSi1h2xYOO3ECEHzzVqgUm15Sk0xaj1Fv9Ed4hipf6PRijeOZ7A1G9zekr1w9WIvebMyJZrK+f6QJ8AP20NUZqG/3k+MeJr3kjrl+8uwU5aPOrHAexSQGAqSKTkWzW7glmlyMWTjwkuSgNVgFg0ctdWTZ5JnNwxXbpjwIKrC4E4sIHcxko2vsTeLF8pZFPk+3QUZIg8BrgtyM3lJC2kO1g3emPQhCIk3VDb5GBgssc/GyFyRXNS651d5BNgcABOKZ4Rv/gGnprB35zP7TKJKkST44XJTEBiugWMkSZg+T9H98/l3eE34O6thfTZXgIyG+ZM6uGlW2XOce0OoEIyJiEL039WJe3izjbD3b9sCCdgQc0MgS+hTaayJI6oCUWPsJLmRji19jLi/wjOsU5gPItCFWw3pBye/A4Zf8Hxm+hShvqBnk8R2yx1fPTiyw/Zx4Jn8m49XQJyjDSZnhIck0PVHR9xWzKCr++PKljLMLdkdFxVRVPFQk/FBbesqofjSXsq9DASY6ACTL3Jmignx2OXD6ac4SlBqCTjV2dIM0yEgZF7zwMNCtppRdXTV8S29JP4W2mfaiqXCUSRTggv8EYU+9diCE+8sPB6HjuLrsfiySbFlYR2m4ysDGXjsVx5CDAf0Nh4IRfcSceYnnBGIQ2sfgGcJFOZoJqr/QeE2NWz6jlWYbWT7MjS/0decpKxP7L88qrR+F48WXQvfsvjWgKjlMKw7lHmFF8FeY836VWWICTRZx+y6IlY1Ys2ML4kySF27Hal4OPhOOoBljMNMVwUEvBulOnKUWw4BGz8eGCl8Hw6tlyJdC7kcBj/aCyNCR/NnuDk4Wck6e//He8L6mS83OJi/hIFc8vYQxnCJMXj9Ou7wr5hxtBnvxXzZM3kFHxCDO24Cd5UyBV9GD8TiQJfBGAy7a2BCBMb5ESVX8NOkyyv2hXMHOjpnKhUM9yP3Ke4CBImO7mCKJNHdFVtAmuyVKJ+jT6ooAAArkX2xwEAvBEpvGNmW2jgs6wxSuKY0h5aUm0rA4v/s8fqSZhzdInB54sMldyAnt9G+9e+g933DfyA/tkc56Ed0vZ/XEvTkThVHyUbfYR/Gjsoab1RpnDBi4aZ2E7iceoBshy+L6NXdL0jlWEs4ZubiWlbVNWlN/MqJcjV/quLU7q4HtkG0MDEFm6To3o48x7xpv8otih6YBduNqBFnwQ6Qz9rM2chFgOR4IgNSZKPxHO0AGCi1gnK/CeCvrSfWYAMn+2rmw0hMZybqKMStG28+rXsKDdqmy6vAwL/+dJwkAW+ix68rWRXpeqHlWidu4SkIBELuwEkFIC/GJU/DRvcN2GG9uP1m+VFifCIS2UdiO4OVrP6PVoW1O+jBJvFH3K1YT7CRqevb9OzjS9fO1wjkOff0W8zZyJK9Mp25aynpf0k3oMpZDpjnlOsFXFUb3N6SvXD1Yi95szIlmsr5yRYaeGUJH7/SAmMr8R6RqsCR0ANptL2dtRoGPi/qcDQE15vnjJ+QMYCg9KbCdV+Qq5di93XAjmwPj6tKZv0aXQuaTZgYR7bdLmAnJaFLbHWcQG1k6F/vdKNEb7llLsoAD9KuKXPZT/LErIyKcI0RZySy9yvhTZb4jQWn17b83yfvqfd5/2NpcyaY4gNERhDRJHw7VhoS5Leai5ZnFaO3C1vU9tIJ85XgCUASTsBLoQWVCKPSQZGxzF7PVLnHui3YA5OsOQpVqAPtgGZ12tP9XkEKj+u2/Atj2bgYrqBF7zUL64X/AQpwr/UElWDhJLSD/KStVeDOUx3AwAVVi9eTUJr6NiNMutCE1sqUf9XVIddgZ/BaG5t3NV2L+T+11QzAl+Xrh8wH/XeUCTmnU3NGkvCz/9Y7PMS+qQL7T7WeGdYmEhb5s/5p/yjSYeqybr5sANOHs83OdeSXbop9cLWW+JksHmS//rHHcrrJhZgCb3P0EOpEoEMCarT6sJq0V1Hwf/YNFdJ9V7Ac654ALS+a9ffNthMUEJeY21QMtNOrEg3QH5RWBPn+yOYN/f38tzwlT1k6Ec94y/sBmeQVv8rRzkkiMSXeAL5ATdJntq8NQq5JbvLQDNnZnHQthZt+uhcUf08mWlRrxxBUaE6xLppgMqFdYSjLGvgn/d8FZ9y7UCg5ZBhgP1rrRQL1COpNKKlJLf5laqwiGAucIDmzSbhO+MidSauDLWuv+fsdd2QYk98PHxqNrPYLrlAlABFi3JEApBm4IlrGbHxKg6dRiy7L1c9xWnAD7E3XrZrSc6DXvGRsjMXWoQdlp4CX5H3cdH9sjIE6akWqiwwrOP6QTbJcxmJGv/MVhsDVrVKmrKSn2H0/Us1fyYCHCOyCSc2L96uId8i9wQO1NXj+1PJmUq3tJ8U0TUwTblOEQdYej99xEI8EzsXLjNJHCgbDygtHBYd/SHToXH3ISwfoLmU+Q9JlS1woaUpVa5sdvbsr4BXR6J"  # noqa: E501
+
+        self.vca_collection.find_one.return_value = {
+            "_id": "2ade7f0e-9b58-4dbd-93a3-4ec076185d39",
+            "schema_version": "1.11",
+            "endpoints": [],
+            "user": "admin",
+            "secret": encrypted_secret,
+            "cacert": encrypted_cacert,
+        }
+        self.admin_collection.find_one.return_value = {
+            "serial": b"l+U3HDp9td+UjQ+AN+Ypj/Uh7n3C+rMJueQNNxkIpWI="
+        }
+        connection_data = self.loop.run_until_complete(
+            self.store.get_vca_connection_data("vca_id")
+        )
+        self.assertEqual(connection_data.endpoints, [])
+        self.assertEqual(connection_data.user, "admin")
+        self.assertEqual(connection_data.secret, secret)
+        self.assertEqual(
+            connection_data.cacert, b64decode(cacert.encode("utf-8")).decode("utf-8")
+        )
+
+    def test_update_vca_endpoints_exception(self):
+        endpoints = ["1.2.3.4:17070"]
+        self.admin_collection.find_one.side_effect = [None, None]
+        self.admin_collection.insert_one.side_effect = DbException("already exists")
+        with self.assertRaises(DbException):
+            self.loop.run_until_complete(self.store.update_vca_endpoints(endpoints))
+        self.assertEqual(self.admin_collection.find_one.call_count, 2)
+        self.admin_collection.replace_one.assert_not_called()
+
+    def test_update_vca_endpoints_with_vca_id(self):
+        endpoints = ["1.2.3.4:17070"]
+        self.vca_collection.find_one.return_value = {}
+        self.loop.run_until_complete(
+            self.store.update_vca_endpoints(endpoints, "vca_id")
+        )
+        self.vca_collection.find_one.assert_called_once_with({"_id": "vca_id"})
+        self.vca_collection.replace_one.assert_called_once_with(
+            {"_id": "vca_id"}, {"endpoints": endpoints}
+        )
+
+    def test_get_vca_endpoints(self):
+        endpoints = ["1.2.3.4:17070"]
+        db_data = {"api_endpoints": endpoints}
+        db_returns = [db_data, None]
+        expected_returns = [endpoints, []]
+        returns = []
+        self.admin_collection.find_one.side_effect = db_returns
+        for _ in range(len(db_returns)):
+            e = self.loop.run_until_complete(self.store.get_vca_endpoints())
+            returns.append(e)
+        self.assertEqual(expected_returns, returns)
+
+    @patch("n2vc.vca.connection_data.base64_to_cacert")
+    def test_get_vca_endpoints_with_vca_id(self, mock_base64_to_cacert):
+        expected_endpoints = ["1.2.3.4:17070"]
+        mock_base64_to_cacert.return_value = "cacert"
+        self.store.get_vca_connection_data = AsyncMock()
+        self.store.get_vca_connection_data.return_value = ConnectionData(
+            **{
+                "endpoints": expected_endpoints,
+                "user": "admin",
+                "secret": "1234",
+                "cacert": "cacert",
+            }
+        )
+        endpoints = self.loop.run_until_complete(self.store.get_vca_endpoints("vca_id"))
+        self.store.get_vca_connection_data.assert_called_with("vca_id")
+        self.assertEqual(expected_endpoints, endpoints)
+
+    def test_get_vca_id(self):
+        self.assertIsNone(self.loop.run_until_complete((self.store.get_vca_id())))
+
+    def test_get_vca_id_with_vim_id(self):
+        self.vim_accounts_collection.find_one.return_value = {"vca": "vca_id"}
+        vca_id = self.loop.run_until_complete(self.store.get_vca_id("vim_id"))
+        self.vim_accounts_collection.find_one.assert_called_once_with({"_id": "vim_id"})
+        self.assertEqual(vca_id, "vca_id")
index c5ab84f..bffbc29 100644 (file)
@@ -14,7 +14,7 @@
 
 from unittest import TestCase
 
 
 from unittest import TestCase
 
-from n2vc.utils import Dict, EntityType, JujuStatusToOSM, N2VCDeploymentStatus, DB_DATA
+from n2vc.utils import Dict, EntityType, JujuStatusToOSM, N2VCDeploymentStatus
 from juju.machine import Machine
 from juju.application import Application
 from juju.action import Action
 from juju.machine import Machine
 from juju.application import Application
 from juju.action import Action
@@ -84,8 +84,3 @@ class UtilsTest(TestCase):
                 osm_status = status["osm"]
                 self.assertTrue(juju_status in JujuStatusToOSM[entity_type])
                 self.assertEqual(osm_status, JujuStatusToOSM[entity_type][juju_status])
                 osm_status = status["osm"]
                 self.assertTrue(juju_status in JujuStatusToOSM[entity_type])
                 self.assertEqual(osm_status, JujuStatusToOSM[entity_type][juju_status])
-
-    def test_db_data(self):
-        self.assertEqual(DB_DATA.api_endpoints.table, "admin")
-        self.assertEqual(DB_DATA.api_endpoints.filter, {"_id": "juju"})
-        self.assertEqual(DB_DATA.api_endpoints.key, "api_endpoints")
index a727072..2f107a7 100644 (file)
@@ -82,7 +82,18 @@ class FakeN2VC(MagicMock):
         detailed_status: str,
         vca_status: str,
         entity_type: str,
         detailed_status: str,
         vca_status: str,
         entity_type: str,
+        vca_id: str = None,
     ):
     ):
+        """
+        Write application status to database
+
+        :param: db_dict: DB dictionary
+        :param: status: Status of the application
+        :param: detailed_status: Detailed status
+        :param: vca_status: VCA status
+        :param: entity_type: Entity type ("application", "machine, and "action")
+        :param: vca_id: Id of the VCA. If None, the default VCA will be used.
+        """
         self.last_written_values = Dict(
             {
                 "n2vc_status": status,
         self.last_written_values = Dict(
             {
                 "n2vc_status": status,
index 6e0f2c0..f0146a0 100644 (file)
@@ -114,14 +114,6 @@ JujuStatusToOSM = {
     },
 }
 
     },
 }
 
-DB_DATA = Dict(
-    {
-        "api_endpoints": Dict(
-            {"table": "admin", "filter": {"_id": "juju"}, "key": "api_endpoints"}
-        )
-    }
-)
-
 
 def obj_to_yaml(obj: object) -> str:
     """
 
 def obj_to_yaml(obj: object) -> str:
     """
diff --git a/n2vc/vca/__init__.py b/n2vc/vca/__init__.py
new file mode 100644 (file)
index 0000000..aa5cee8
--- /dev/null
@@ -0,0 +1,13 @@
+# Copyright 2021 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/n2vc/vca/cloud.py b/n2vc/vca/cloud.py
new file mode 100644 (file)
index 0000000..970fd93
--- /dev/null
@@ -0,0 +1,25 @@
+# Copyright 2021 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.
+
+
+class Cloud:
+    def __init__(self, name: str, credential_name: str):
+        """
+        Constructor
+
+        :param: name: Name of the Cloud
+        :param: credential_name: Credential name for the Cloud
+        """
+        self.name = name
+        self.credential_name = credential_name
diff --git a/n2vc/vca/connection.py b/n2vc/vca/connection.py
new file mode 100644 (file)
index 0000000..98de0ff
--- /dev/null
@@ -0,0 +1,113 @@
+# Copyright 2021 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 typing
+
+from n2vc.config import EnvironConfig, ModelConfig
+from n2vc.store import Store
+from n2vc.vca.cloud import Cloud
+from n2vc.vca.connection_data import ConnectionData
+
+
+class Connection:
+    def __init__(self, store: Store, vca_id: str = None):
+        """
+        Contructor
+
+        :param: store: Store object. Used to communicate wuth the DB
+        :param: vca_id: Id of the VCA. If none specified, the default VCA will be used.
+        """
+        self._data = None
+        self.default = vca_id is None
+        self._vca_id = vca_id
+        self._store = store
+
+    async def load(self):
+        """Load VCA connection data"""
+        await self._load_vca_connection_data()
+
+    @property
+    def is_default(self):
+        return self._vca_id is None
+
+    @property
+    def data(self) -> ConnectionData:
+        return self._data
+
+    async def _load_vca_connection_data(self) -> typing.NoReturn:
+        """
+        Load VCA connection data
+
+        If self._vca_id is None, it will get the VCA data from the Environment variables,
+        and the default VCA will be used. If it is not None, then it means that it will
+        load the credentials from the database (A non-default VCA will be used).
+        """
+        if self._vca_id:
+            self._data = await self._store.get_vca_connection_data(self._vca_id)
+        else:
+            envs = EnvironConfig()
+            # Get endpoints from the DB and ENV. Check if update in the database is needed or not.
+            db_endpoints = await self._store.get_vca_endpoints()
+            env_endpoints = (
+                envs["endpoints"].split(",")
+                if "endpoints" in envs
+                else ["{}:{}".format(envs["host"], envs.get("port", 17070))]
+            )
+
+            db_update_needed = not all(e in db_endpoints for e in env_endpoints)
+
+            endpoints = env_endpoints if db_update_needed else db_endpoints
+            config = {
+                "endpoints": endpoints,
+                "user": envs["user"],
+                "secret": envs["secret"],
+                "cacert": envs["cacert"],
+                "pubkey": envs["pubkey"],
+                "lxd-cloud": envs["cloud"],
+                "lxd-credentials": envs.get("credentials", envs["cloud"]),
+                "k8s-cloud": envs["k8s_cloud"],
+                "k8s-credentials": envs.get("k8s_credentials", envs["k8s_cloud"]),
+                "model-config": ModelConfig(envs),
+                "api-proxy": envs.get("api_proxy", None),
+            }
+            self._data = ConnectionData(**config)
+            if db_update_needed:
+                await self.update_endpoints(endpoints)
+
+    @property
+    def endpoints(self):
+        return self._data.endpoints
+
+    async def update_endpoints(self, endpoints: typing.List[str]):
+        await self._store.update_vca_endpoints(endpoints, self._vca_id)
+        self._data.endpoints = endpoints
+
+    @property
+    def lxd_cloud(self) -> Cloud:
+        return Cloud(self.data.lxd_cloud, self.data.lxd_credentials)
+
+    @property
+    def k8s_cloud(self) -> Cloud:
+        return Cloud(self.data.k8s_cloud, self.data.k8s_credentials)
+
+
+async def get_connection(store: Store, vca_id: str = None) -> Connection:
+    """
+    Get Connection
+
+    Method to get a Connection object with the VCA information loaded
+    """
+    connection = Connection(store, vca_id=vca_id)
+    await connection.load()
+    return connection
diff --git a/n2vc/vca/connection_data.py b/n2vc/vca/connection_data.py
new file mode 100644 (file)
index 0000000..a1eff21
--- /dev/null
@@ -0,0 +1,51 @@
+# Copyright 2021 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 n2vc.utils import base64_to_cacert
+
+
+class ConnectionData:
+    def __init__(self, **kwargs):
+        """
+        Constructor
+
+        :param: kwargs:
+            endpoints (list): Endpoints of all the Juju controller units
+            user (str): Username for authenticating to the controller
+            secret (str): Secret for authenticating to the controller
+            cacert (str): Base64 encoded CA certificate for authenticating to the controller
+            (optional) pubkey (str): Public key to insert to the charm.
+                            This is useful to do `juju ssh`.
+                            It is not very useful though.
+                            TODO: Test it.
+            (optional) lxd-cloud (str): Name of the cloud to use for lxd proxy charms
+            (optional) lxd-credentials (str): Name of the lxd-cloud credentials
+            (optional) k8s-cloud (str): Name of the cloud to use for k8s proxy charms
+            (optional) k8s-credentials (str): Name of the k8s-cloud credentials
+            (optional) model-config (n2vc.config.ModelConfig): Config to apply in all Juju models
+            (deprecated, optional) api-proxy (str): Proxy IP to reach the controller.
+                                                    Used in case native charms cannot react the controller.
+        """
+        self.endpoints = kwargs["endpoints"]
+        self.user = kwargs["user"]
+        self.secret = kwargs["secret"]
+        self.cacert = base64_to_cacert(kwargs["cacert"])
+        self.pubkey = kwargs.get("pubkey", "")
+        self.lxd_cloud = kwargs.get("lxd-cloud", None)
+        self.lxd_credentials = kwargs.get("lxd-credentials", None)
+        self.k8s_cloud = kwargs.get("k8s-cloud", None)
+        self.k8s_credentials = kwargs.get("k8s-credentials", None)
+        self.model_config = kwargs.get("model-config", {})
+        self.model_config.update({"authorized-keys": self.pubkey})
+        self.api_proxy = kwargs.get("api-proxy", None)
index ea82d48..eb8534a 100644 (file)
@@ -14,4 +14,6 @@
 
 juju==2.8.4
 kubernetes==10.0.1
 
 juju==2.8.4
 kubernetes==10.0.1
-pyasn1
\ No newline at end of file
+pyasn1
+motor==1.3.1
+retrying-async
index 8ff8c08..2e394a5 100644 (file)
@@ -1,3 +1,5 @@
+async-timeout==3.0.1
+    # via retrying-async
 bcrypt==3.2.0
     # via paramiko
 cachetools==4.2.1
 bcrypt==3.2.0
     # via paramiko
 cachetools==4.2.1
@@ -29,6 +31,8 @@ macaroonbakery==1.3.1
     # via
     #   juju
     #   theblues
     # via
     #   juju
     #   theblues
+motor==1.3.1
+    # via -r requirements.in
 mypy-extensions==0.4.3
     # via typing-inspect
 oauthlib==3.1.0
 mypy-extensions==0.4.3
     # via typing-inspect
 oauthlib==3.1.0
@@ -49,6 +53,8 @@ pycparser==2.20
     # via cffi
 pymacaroons==0.13.0
     # via macaroonbakery
     # via cffi
 pymacaroons==0.13.0
     # via macaroonbakery
+pymongo==3.11.3
+    # via motor
 pynacl==1.4.0
     # via
     #   macaroonbakery
 pynacl==1.4.0
     # via
     #   macaroonbakery
@@ -75,6 +81,8 @@ requests==2.25.1
     #   macaroonbakery
     #   requests-oauthlib
     #   theblues
     #   macaroonbakery
     #   requests-oauthlib
     #   theblues
+retrying-async==1.2.0
+    # via -r requirements.in
 rsa==4.7.2
     # via google-auth
 six==1.15.0
 rsa==4.7.2
     # via google-auth
 six==1.15.0