Feature 10239: Distributed VCA

- 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>
diff --git a/n2vc/vca/__init__.py b/n2vc/vca/__init__.py
new file mode 100644
index 0000000..aa5cee8
--- /dev/null
+++ b/n2vc/vca/__init__.py
@@ -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
index 0000000..970fd93
--- /dev/null
+++ b/n2vc/vca/cloud.py
@@ -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
index 0000000..98de0ff
--- /dev/null
+++ b/n2vc/vca/connection.py
@@ -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
index 0000000..a1eff21
--- /dev/null
+++ b/n2vc/vca/connection_data.py
@@ -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)