Major improvement in OSM charms

- Adapt all new operator charms to use the same pattern. They are all
using now this library that encapsulates the common logic for all
charms: https://github.com/davigar15/ops-lib-charmed-osm. That will be
eventually moved to gitlab, when it has a PyPI repository available
- Add unit tests to all charms
- Modify installer and bundles to point to the new charms
- Improve the build.sh script for building the charms

Change-Id: I0896ceb082d1b6a76b3560c07482a4135a220a3f
Signed-off-by: David Garcia <david.garcia@canonical.com>
diff --git a/installers/charm/ro/.gitignore b/installers/charm/ro/.gitignore
index aa3848a..2885df2 100644
--- a/installers/charm/ro/.gitignore
+++ b/installers/charm/ro/.gitignore
@@ -1,4 +1,4 @@
-# Copyright 2020 Canonical Ltd.
+# 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
@@ -22,7 +22,9 @@
 venv
 .vscode
 build
-ro.charm
+*.charm
 .coverage
+coverage.xml
 .stestr
 cover
+release
\ No newline at end of file
diff --git a/installers/charm/ro/.jujuignore b/installers/charm/ro/.jujuignore
new file mode 100644
index 0000000..bf04eb4
--- /dev/null
+++ b/installers/charm/ro/.jujuignore
@@ -0,0 +1,28 @@
+# 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.
+#
+# For those usages not covered by the Apache License, Version 2.0 please
+# contact: legal@canonical.com
+#
+# To get in touch with the maintainers, please contact:
+# osm-charmers@lists.launchpad.net
+##
+
+venv
+.vscode
+build
+prometheus.charm
+.coverage
+.stestr
+cover
diff --git a/installers/charm/ro/.yamllint.yaml b/installers/charm/ro/.yamllint.yaml
index c20ac8d..d71fb69 100644
--- a/installers/charm/ro/.yamllint.yaml
+++ b/installers/charm/ro/.yamllint.yaml
@@ -1,4 +1,4 @@
-# Copyright 2020 Canonical Ltd.
+# 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
@@ -28,6 +28,7 @@
   - ".yamllint"
 ignore: |
   .tox
+  cover/
   build/
-  mod/
-  lib/
+  venv
+  release/
diff --git a/installers/charm/ro/metadata.yaml b/installers/charm/ro/metadata.yaml
index f29f4bc..eea0e9e 100644
--- a/installers/charm/ro/metadata.yaml
+++ b/installers/charm/ro/metadata.yaml
@@ -40,11 +40,14 @@
     upstream-source: "opensourcemano/ro:8"
 provides:
   ro:
-    interface: osm-ro
+    interface: http
 requires:
   kafka:
     interface: kafka
+    limit: 1
   mongodb:
     interface: mongodb
+    limit: 1
   mysql:
     interface: mysql
+    limit: 1
diff --git a/installers/charm/ro/requirements-test.txt b/installers/charm/ro/requirements-test.txt
new file mode 100644
index 0000000..1da01c8
--- /dev/null
+++ b/installers/charm/ro/requirements-test.txt
@@ -0,0 +1,30 @@
+# 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.
+#
+# For those usages not covered by the Apache License, Version 2.0 please
+# contact: legal@canonical.com
+#
+# To get in touch with the maintainers, please contact:
+# osm-charmers@lists.launchpad.net
+-r requirements.txt
+coverage
+stestr
+mock
+black
+yamllint
+flake8
+safety
+requests-mock
+asynctest
+nose2
\ No newline at end of file
diff --git a/installers/charm/ro/requirements.txt b/installers/charm/ro/requirements.txt
index a26601f..f10a199 100644
--- a/installers/charm/ro/requirements.txt
+++ b/installers/charm/ro/requirements.txt
@@ -1,4 +1,4 @@
-# Copyright 2020 Canonical Ltd.
+# 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
@@ -19,5 +19,4 @@
 # osm-charmers@lists.launchpad.net
 ##
 
-ops
-git+https://github.com/juju-solutions/resource-oci-image/@c5778285d332edf3d9a538f9d0c06154b7ec1b0b#egg=oci-image
+git+https://github.com/davigar15/ops-lib-charmed-osm/@e7f26cd29b322e175a23cadbe4546b7f2bbf111c
\ No newline at end of file
diff --git a/installers/charm/ro/src/charm.py b/installers/charm/ro/src/charm.py
index 8e6d576..ae92d98 100755
--- a/installers/charm/ro/src/charm.py
+++ b/installers/charm/ro/src/charm.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python3
-# Copyright 2020 Canonical Ltd.
+# 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
@@ -20,116 +20,73 @@
 # osm-charmers@lists.launchpad.net
 ##
 
+# pylint: disable=E0213
+
 import logging
-from typing import Dict, List, NoReturn
+from typing import NoReturn
 
-from ops.charm import CharmBase
-from ops.framework import EventBase, StoredState
 from ops.main import main
-from ops.model import ActiveStatus, Application, BlockedStatus, MaintenanceStatus, Unit
-from oci_image import OCIImageResource, OCIImageResourceError
 
-from pod_spec import make_pod_spec
+from opslib.osm.charm import CharmedOsmBase, RelationsMissing
+
+from opslib.osm.pod import (
+    ContainerV3Builder,
+    PodSpecV3Builder,
+)
+
+
+from opslib.osm.validator import (
+    ModelValidator,
+    validator,
+)
+
+from opslib.osm.interfaces.kafka import KafkaClient
+from opslib.osm.interfaces.mysql import MysqlClient
+from opslib.osm.interfaces.mongo import MongoClient
+
 
 logger = logging.getLogger(__name__)
 
-RO_PORT = 9090
+PORT = 9090
 
 
-class RelationsMissing(Exception):
-    def __init__(self, missing_relations: List):
-        self.message = ""
-        if missing_relations and isinstance(missing_relations, list):
-            self.message += f'Waiting for {", ".join(missing_relations)} relation'
-            if "," in self.message:
-                self.message += "s"
+class ConfigModel(ModelValidator):
+    enable_ng_ro: bool
+    database_commonkey: str
+    log_level: str
+    vim_database: str
+    ro_database: str
+    openmano_tenant: str
+
+    @validator("log_level")
+    def validate_log_level(cls, v):
+        if v not in {"INFO", "DEBUG"}:
+            raise ValueError("value must be INFO or DEBUG")
+        return v
 
 
-class RelationDefinition:
-    def __init__(self, relation_name: str, keys: List, source_type):
-        if source_type != Application and source_type != Unit:
-            raise TypeError(
-                "source_type should be ops.model.Application or ops.model.Unit"
-            )
-        self.relation_name = relation_name
-        self.keys = keys
-        self.source_type = source_type
-
-
-def check_missing_relation_data(
-    data: Dict,
-    expected_relations_data: List[RelationDefinition],
-):
-    missing_relations = []
-    for relation_data in expected_relations_data:
-        if not all(
-            f"{relation_data.relation_name}_{k}" in data for k in relation_data.keys
-        ):
-            missing_relations.append(relation_data.relation_name)
-    if missing_relations:
-        raise RelationsMissing(missing_relations)
-
-
-def get_relation_data(
-    charm: CharmBase,
-    relation_data: RelationDefinition,
-) -> Dict:
-    data = {}
-    relation = charm.model.get_relation(relation_data.relation_name)
-    if relation:
-        self_app_unit = (
-            charm.app if relation_data.source_type == Application else charm.unit
-        )
-        expected_type = relation_data.source_type
-        for app_unit in relation.data:
-            if app_unit != self_app_unit and isinstance(app_unit, expected_type):
-                if all(k in relation.data[app_unit] for k in relation_data.keys):
-                    for k in relation_data.keys:
-                        data[f"{relation_data.relation_name}_{k}"] = relation.data[
-                            app_unit
-                        ].get(k)
-                    break
-    return data
-
-
-class RoCharm(CharmBase):
-    """RO Charm."""
-
-    state = StoredState()
+class RoCharm(CharmedOsmBase):
+    """GrafanaCharm Charm."""
 
     def __init__(self, *args) -> NoReturn:
-        """RO Charm constructor."""
-        super().__init__(*args)
+        """Prometheus Charm constructor."""
+        super().__init__(*args, oci_image="image")
 
-        # Internal state initialization
-        self.state.set_default(pod_spec=None)
+        self.kafka_client = KafkaClient(self, "kafka")
+        self.framework.observe(self.on["kafka"].relation_changed, self.configure_pod)
+        self.framework.observe(self.on["kafka"].relation_broken, self.configure_pod)
 
-        self.port = RO_PORT
-        self.image = OCIImageResource(self, "image")
+        self.mysql_client = MysqlClient(self, "mysql")
+        self.framework.observe(self.on["mysql"].relation_changed, self.configure_pod)
+        self.framework.observe(self.on["mysql"].relation_broken, self.configure_pod)
 
-        # Registering regular events
-        self.framework.observe(self.on.start, self.configure_pod)
-        self.framework.observe(self.on.config_changed, self.configure_pod)
+        self.mongodb_client = MongoClient(self, "mongodb")
+        self.framework.observe(self.on["mongodb"].relation_changed, self.configure_pod)
+        self.framework.observe(self.on["mongodb"].relation_broken, self.configure_pod)
 
-        # Registering required relation events
-        self.framework.observe(self.on.kafka_relation_changed, self.configure_pod)
-        self.framework.observe(self.on.mongodb_relation_changed, self.configure_pod)
-        self.framework.observe(self.on.mysql_relation_changed, self.configure_pod)
+        self.framework.observe(self.on["ro"].relation_joined, self._publish_ro_info)
 
-        # Registering required relation departed events
-        self.framework.observe(self.on.kafka_relation_departed, self.configure_pod)
-        self.framework.observe(self.on.mongodb_relation_departed, self.configure_pod)
-        self.framework.observe(self.on.mysql_relation_departed, self.configure_pod)
-
-        # Registering required relation broken events
-        self.framework.observe(self.on.kafka_relation_broken, self.configure_pod)
-        self.framework.observe(self.on.mongodb_relation_broken, self.configure_pod)
-        self.framework.observe(self.on.mysql_relation_broken, self.configure_pod)
-
-        # Registering provided relation events
-        self.framework.observe(self.on.ro_relation_joined, self._publish_ro_info)
-
-    def _publish_ro_info(self, event: EventBase) -> NoReturn:
+    def _publish_ro_info(self, event):
         """Publishes RO information.
 
         Args:
@@ -138,80 +95,91 @@
         if self.unit.is_leader():
             rel_data = {
                 "host": self.model.app.name,
-                "port": str(RO_PORT),
+                "port": str(PORT),
             }
             for k, v in rel_data.items():
                 event.relation.data[self.app][k] = v
 
-    @property
-    def relations_requirements(self):
-        if self.model.config["enable_ng_ro"]:
-            return [
-                RelationDefinition("kafka", ["host", "port"], Unit),
-                RelationDefinition("mongodb", ["connection_string"], Unit),
-            ]
+    def _check_missing_dependencies(self, config: ConfigModel):
+        missing_relations = []
+
+        if config.enable_ng_ro:
+            if self.kafka_client.is_missing_data_in_unit():
+                missing_relations.append("kafka")
+            if self.mongodb_client.is_missing_data_in_unit():
+                missing_relations.append("mongodb")
         else:
-            return [
-                RelationDefinition(
-                    "mysql", ["host", "port", "user", "password", "root_password"], Unit
-                )
-            ]
+            if self.mysql_client.is_missing_data_in_unit():
+                missing_relations.append("mysql")
+        if missing_relations:
+            raise RelationsMissing(missing_relations)
 
-    def get_relation_state(self):
-        relation_state = {}
-        for relation_requirements in self.relations_requirements:
-            data = get_relation_data(self, relation_requirements)
-            relation_state = {**relation_state, **data}
-        check_missing_relation_data(relation_state, self.relations_requirements)
-        return relation_state
-
-    def configure_pod(self, _=None) -> NoReturn:
-        """Assemble the pod spec and apply it, if possible.
-
-        Args:
-            event (EventBase): Hook or Relation event that started the
-                               function.
-        """
-        if not self.unit.is_leader():
-            self.unit.status = ActiveStatus("ready")
-            return
-
-        relation_state = None
-        try:
-            relation_state = self.get_relation_state()
-        except RelationsMissing as exc:
-            logger.exception("Relation missing error")
-            self.unit.status = BlockedStatus(exc.message)
-            return
-
-        self.unit.status = MaintenanceStatus("Assembling pod spec")
-
-        # Fetch image information
-        try:
-            self.unit.status = MaintenanceStatus("Fetching image information")
-            image_info = self.image.fetch()
-        except OCIImageResourceError:
-            self.unit.status = BlockedStatus("Error fetching image information")
-            return
-
-        try:
-            pod_spec = make_pod_spec(
-                image_info,
-                self.model.config,
-                relation_state,
-                self.model.app.name,
-                self.port,
+    def build_pod_spec(self, image_info):
+        # Validate config
+        config = ConfigModel(**dict(self.config))
+        # Check relations
+        self._check_missing_dependencies(config)
+        # Create Builder for the PodSpec
+        pod_spec_builder = PodSpecV3Builder()
+        # Build Container
+        container_builder = ContainerV3Builder(self.app.name, image_info)
+        container_builder.add_port(name=self.app.name, port=PORT)
+        container_builder.add_http_readiness_probe(
+            "/ro/" if config.enable_ng_ro else "/openmano/tenants",
+            PORT,
+            initial_delay_seconds=10,
+            period_seconds=10,
+            timeout_seconds=5,
+            failure_threshold=3,
+        )
+        container_builder.add_http_liveness_probe(
+            "/ro/" if config.enable_ng_ro else "/openmano/tenants",
+            PORT,
+            initial_delay_seconds=600,
+            period_seconds=10,
+            timeout_seconds=5,
+            failure_threshold=3,
+        )
+        container_builder.add_envs(
+            {
+                "OSMRO_LOG_LEVEL": config.log_level,
+            }
+        )
+        if config.enable_ng_ro:
+            container_builder.add_envs(
+                {
+                    "OSMRO_MESSAGE_DRIVER": "kafka",
+                    "OSMRO_MESSAGE_HOST": self.kafka_client.host,
+                    "OSMRO_MESSAGE_PORT": self.kafka_client.port,
+                    # MongoDB configuration
+                    "OSMRO_DATABASE_DRIVER": "mongo",
+                    "OSMRO_DATABASE_URI": self.mongodb_client.connection_string,
+                    "OSMRO_DATABASE_COMMONKEY": config.database_commonkey,
+                }
             )
-        except ValueError as exc:
-            logger.exception("Config/Relation data validation error")
-            self.unit.status = BlockedStatus(str(exc))
-            return
 
-        if self.state.pod_spec != pod_spec:
-            self.model.pod.set_spec(pod_spec)
-            self.state.pod_spec = pod_spec
-
-        self.unit.status = ActiveStatus("ready")
+        else:
+            container_builder.add_envs(
+                {
+                    "RO_DB_HOST": self.mysql_client.host,
+                    "RO_DB_OVIM_HOST": self.mysql_client.host,
+                    "RO_DB_PORT": self.mysql_client.port,
+                    "RO_DB_OVIM_PORT": self.mysql_client.port,
+                    "RO_DB_USER": self.mysql_client.user,
+                    "RO_DB_OVIM_USER": self.mysql_client.user,
+                    "RO_DB_PASSWORD": self.mysql_client.password,
+                    "RO_DB_OVIM_PASSWORD": self.mysql_client.password,
+                    "RO_DB_ROOT_PASSWORD": self.mysql_client.root_password,
+                    "RO_DB_OVIM_ROOT_PASSWORD": self.mysql_client.root_password,
+                    "RO_DB_NAME": config.ro_database,
+                    "RO_DB_OVIM_NAME": config.vim_database,
+                    "OPENMANO_TENANT": config.openmano_tenant,
+                }
+            )
+        container = container_builder.build()
+        # Add container to pod spec
+        pod_spec_builder.add_container(container)
+        return pod_spec_builder.build()
 
 
 if __name__ == "__main__":
diff --git a/installers/charm/ro/src/pod_spec.py b/installers/charm/ro/src/pod_spec.py
index 6c91baf..1beba17 100644
--- a/installers/charm/ro/src/pod_spec.py
+++ b/installers/charm/ro/src/pod_spec.py
@@ -45,8 +45,9 @@
         )
         if values.get("enable_ng_ro", True)
         else True,
-        "log_level": lambda value, _: isinstance(value, str)
-        and value in ("INFO", "DEBUG"),
+        "log_level": lambda value, _: (
+            isinstance(value, str) and value in ("INFO", "DEBUG")
+        ),
         "vim_database": lambda value, values: (
             isinstance(value, str) and len(value) > 0
         )
diff --git a/installers/charm/ro/tests/test_charm.py b/installers/charm/ro/tests/test_charm.py
index 0aa9b7d..4f14aff 100644
--- a/installers/charm/ro/tests/test_charm.py
+++ b/installers/charm/ro/tests/test_charm.py
@@ -20,333 +20,416 @@
 # osm-charmers@lists.launchpad.net
 ##
 
+import sys
 from typing import NoReturn
 import unittest
-from ops.model import BlockedStatus
-
+from ops.model import ActiveStatus, BlockedStatus
 from ops.testing import Harness
 
 from charm import RoCharm
 
 
 class TestCharm(unittest.TestCase):
-    """RO Charm unit tests."""
+    """Prometheus Charm unit tests."""
 
     def setUp(self) -> NoReturn:
         """Test setup"""
+        self.image_info = sys.modules["oci_image"].OCIImageResource().fetch()
         self.harness = Harness(RoCharm)
         self.harness.set_leader(is_leader=True)
         self.harness.begin()
-
-    def test_on_start_without_relations_ng_ro(self) -> NoReturn:
-        """Test installation without any relation."""
-        self.harness.charm.on.start.emit()
-
-        # Verifying status
-        self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
-
-        # Verifying status message
-        self.assertGreater(len(self.harness.charm.unit.status.message), 0)
-        self.assertTrue(
-            self.harness.charm.unit.status.message.startswith("Waiting for ")
-        )
-        self.assertIn("kafka", self.harness.charm.unit.status.message)
-        self.assertIn("mongodb", self.harness.charm.unit.status.message)
-        self.assertTrue(self.harness.charm.unit.status.message.endswith(" relations"))
-
-    def test_on_start_without_relations_no_ng_ro(self) -> NoReturn:
-        """Test installation without any relation."""
-        self.harness.update_config({"enable_ng_ro": False})
-
-        self.harness.charm.on.start.emit()
-
-        # Verifying status
-        self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
-
-        # Verifying status message
-        self.assertGreater(len(self.harness.charm.unit.status.message), 0)
-        self.assertTrue(
-            self.harness.charm.unit.status.message.startswith("Waiting for ")
-        )
-        self.assertIn("mysql", self.harness.charm.unit.status.message)
-        self.assertTrue(self.harness.charm.unit.status.message.endswith(" relation"))
-
-    def test_on_start_with_relations_ng_ro(self) -> NoReturn:
-        """Test deployment with NG-RO."""
-        expected_result = {
-            "version": 3,
-            "containers": [
-                {
-                    "name": "ro",
-                    "imageDetails": self.harness.charm.image.fetch(),
-                    "imagePullPolicy": "Always",
-                    "ports": [
-                        {
-                            "name": "ro",
-                            "containerPort": 9090,
-                            "protocol": "TCP",
-                        }
-                    ],
-                    "envConfig": {
-                        "OSMRO_LOG_LEVEL": "INFO",
-                        "OSMRO_MESSAGE_DRIVER": "kafka",
-                        "OSMRO_MESSAGE_HOST": "kafka",
-                        "OSMRO_MESSAGE_PORT": "9090",
-                        "OSMRO_DATABASE_DRIVER": "mongo",
-                        "OSMRO_DATABASE_URI": "mongodb://mongo",
-                        "OSMRO_DATABASE_COMMONKEY": "osm",
-                    },
-                    "kubernetes": {
-                        "startupProbe": {
-                            "exec": {"command": ["/usr/bin/pgrep", "python3"]},
-                            "initialDelaySeconds": 60,
-                            "timeoutSeconds": 5,
-                        },
-                        "readinessProbe": {
-                            "httpGet": {
-                                "path": "/openmano/tenants",
-                                "port": 9090,
-                            },
-                            "periodSeconds": 10,
-                            "timeoutSeconds": 5,
-                            "successThreshold": 1,
-                            "failureThreshold": 3,
-                        },
-                        "livenessProbe": {
-                            "httpGet": {
-                                "path": "/openmano/tenants",
-                                "port": 9090,
-                            },
-                            "initialDelaySeconds": 600,
-                            "periodSeconds": 10,
-                            "timeoutSeconds": 5,
-                            "successThreshold": 1,
-                            "failureThreshold": 3,
-                        },
-                    },
-                }
-            ],
-            "kubernetesResources": {"ingressResources": []},
+        self.config = {
+            "enable_ng_ro": True,
+            "database_commonkey": "commonkey",
+            "log_level": "INFO",
+            "vim_database": "db_name",
+            "ro_database": "ro_db_name",
+            "openmano_tenant": "mano",
         }
+        self.harness.update_config(self.config)
 
-        self.harness.charm.on.start.emit()
+    def test_config_changed_no_relations(
+        self,
+    ) -> NoReturn:
+        """Test ingress resources without HTTP."""
+
+        self.harness.charm.on.config_changed.emit()
+
+        # Assertions
+        self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
+        self.assertTrue(
+            all(
+                relation in self.harness.charm.unit.status.message
+                for relation in ["mongodb", "kafka"]
+            )
+        )
+
+        # Disable ng-ro
+        self.harness.update_config({"enable_ng_ro": False})
+        self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
+        self.assertTrue(
+            all(
+                relation in self.harness.charm.unit.status.message
+                for relation in ["mysql"]
+            )
+        )
+
+    def test_config_changed_non_leader(
+        self,
+    ) -> NoReturn:
+        """Test ingress resources without HTTP."""
+        self.harness.set_leader(is_leader=False)
+        self.harness.charm.on.config_changed.emit()
+
+        # Assertions
+        self.assertIsInstance(self.harness.charm.unit.status, ActiveStatus)
+
+    def test_with_relations_ng(
+        self,
+    ) -> NoReturn:
+        "Test with relations (ng-ro)"
 
         # Initializing the kafka relation
-        relation_id = self.harness.add_relation("kafka", "kafka")
-        self.harness.add_relation_unit(relation_id, "kafka/0")
+        kafka_relation_id = self.harness.add_relation("kafka", "kafka")
+        self.harness.add_relation_unit(kafka_relation_id, "kafka/0")
         self.harness.update_relation_data(
-            relation_id,
-            "kafka/0",
-            {
-                "host": "kafka",
-                "port": "9090",
-            },
+            kafka_relation_id, "kafka/0", {"host": "kafka", "port": 9092}
         )
 
-        # Initializing the mongodb relation
-        relation_id = self.harness.add_relation("mongodb", "mongodb")
-        self.harness.add_relation_unit(relation_id, "mongodb/0")
+        # Initializing the mongo relation
+        mongodb_relation_id = self.harness.add_relation("mongodb", "mongodb")
+        self.harness.add_relation_unit(mongodb_relation_id, "mongodb/0")
         self.harness.update_relation_data(
-            relation_id,
+            mongodb_relation_id,
             "mongodb/0",
-            {
-                "connection_string": "mongodb://mongo",
-            },
+            {"connection_string": "mongodb://mongo:27017"},
         )
 
         # Verifying status
         self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
 
-        pod_spec, _ = self.harness.get_pod_spec()
 
-        self.assertDictEqual(expected_result, pod_spec)
+if __name__ == "__main__":
+    unittest.main()
 
-    def test_on_start_with_relations_no_ng_ro(self) -> NoReturn:
-        """Test deployment with old RO."""
-        self.harness.update_config({"enable_ng_ro": False})
+# class TestCharm(unittest.TestCase):
+#     """RO Charm unit tests."""
 
-        expected_result = {
-            "version": 3,
-            "containers": [
-                {
-                    "name": "ro",
-                    "imageDetails": self.harness.charm.image.fetch(),
-                    "imagePullPolicy": "Always",
-                    "ports": [
-                        {
-                            "name": "ro",
-                            "containerPort": 9090,
-                            "protocol": "TCP",
-                        }
-                    ],
-                    "envConfig": {
-                        "OSMRO_LOG_LEVEL": "INFO",
-                        "RO_DB_HOST": "mysql",
-                        "RO_DB_OVIM_HOST": "mysql",
-                        "RO_DB_PORT": 3306,
-                        "RO_DB_OVIM_PORT": 3306,
-                        "RO_DB_USER": "mano",
-                        "RO_DB_OVIM_USER": "mano",
-                        "RO_DB_PASSWORD": "manopw",
-                        "RO_DB_OVIM_PASSWORD": "manopw",
-                        "RO_DB_ROOT_PASSWORD": "rootmanopw",
-                        "RO_DB_OVIM_ROOT_PASSWORD": "rootmanopw",
-                        "RO_DB_NAME": "mano_db",
-                        "RO_DB_OVIM_NAME": "mano_vim_db",
-                        "OPENMANO_TENANT": "osm",
-                    },
-                    "kubernetes": {
-                        "startupProbe": {
-                            "exec": {"command": ["/usr/bin/pgrep", "python3"]},
-                            "initialDelaySeconds": 60,
-                            "timeoutSeconds": 5,
-                        },
-                        "readinessProbe": {
-                            "httpGet": {
-                                "path": "/openmano/tenants",
-                                "port": 9090,
-                            },
-                            "periodSeconds": 10,
-                            "timeoutSeconds": 5,
-                            "successThreshold": 1,
-                            "failureThreshold": 3,
-                        },
-                        "livenessProbe": {
-                            "httpGet": {
-                                "path": "/openmano/tenants",
-                                "port": 9090,
-                            },
-                            "initialDelaySeconds": 600,
-                            "periodSeconds": 10,
-                            "timeoutSeconds": 5,
-                            "successThreshold": 1,
-                            "failureThreshold": 3,
-                        },
-                    },
-                }
-            ],
-            "kubernetesResources": {"ingressResources": []},
-        }
+#     def setUp(self) -> NoReturn:
+#         """Test setup"""
+#         self.harness = Harness(RoCharm)
+#         self.harness.set_leader(is_leader=True)
+#         self.harness.begin()
 
-        self.harness.charm.on.start.emit()
+#     def test_on_start_without_relations_ng_ro(self) -> NoReturn:
+#         """Test installation without any relation."""
+#         self.harness.charm.on.start.emit()
 
-        # Initializing the mysql relation
-        relation_id = self.harness.add_relation("mysql", "mysql")
-        self.harness.add_relation_unit(relation_id, "mysql/0")
-        self.harness.update_relation_data(
-            relation_id,
-            "mysql/0",
-            {
-                "host": "mysql",
-                "port": 3306,
-                "user": "mano",
-                "password": "manopw",
-                "root_password": "rootmanopw",
-            },
-        )
+#         # Verifying status
+#         self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
 
-        # Verifying status
-        self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
+#         # Verifying status message
+#         self.assertGreater(len(self.harness.charm.unit.status.message), 0)
+#         self.assertTrue(
+#             self.harness.charm.unit.status.message.startswith("Waiting for ")
+#         )
+#         self.assertIn("kafka", self.harness.charm.unit.status.message)
+#         self.assertIn("mongodb", self.harness.charm.unit.status.message)
+#         self.assertTrue(self.harness.charm.unit.status.message.endswith(" relations"))
 
-        pod_spec, _ = self.harness.get_pod_spec()
+#     def test_on_start_without_relations_no_ng_ro(self) -> NoReturn:
+#         """Test installation without any relation."""
+#         self.harness.update_config({"enable_ng_ro": False})
 
-        self.assertDictEqual(expected_result, pod_spec)
+#         self.harness.charm.on.start.emit()
 
-    def test_on_kafka_unit_relation_changed(self) -> NoReturn:
-        """Test to see if kafka relation is updated."""
-        self.harness.charm.on.start.emit()
+#         # Verifying status
+#         self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
 
-        relation_id = self.harness.add_relation("kafka", "kafka")
-        self.harness.add_relation_unit(relation_id, "kafka/0")
-        self.harness.update_relation_data(
-            relation_id,
-            "kafka/0",
-            {
-                "host": "kafka",
-                "port": 9090,
-            },
-        )
+#         # Verifying status message
+#         self.assertGreater(len(self.harness.charm.unit.status.message), 0)
+#         self.assertTrue(
+#             self.harness.charm.unit.status.message.startswith("Waiting for ")
+#         )
+#         self.assertIn("mysql", self.harness.charm.unit.status.message)
+#         self.assertTrue(self.harness.charm.unit.status.message.endswith(" relation"))
 
-        # Verifying status
-        self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
+#     def test_on_start_with_relations_ng_ro(self) -> NoReturn:
+#         """Test deployment with NG-RO."""
+#         expected_result = {
+#             "version": 3,
+#             "containers": [
+#                 {
+#                     "name": "ro",
+#                     "imageDetails": self.harness.charm.image.fetch(),
+#                     "imagePullPolicy": "Always",
+#                     "ports": [
+#                         {
+#                             "name": "ro",
+#                             "containerPort": 9090,
+#                             "protocol": "TCP",
+#                         }
+#                     ],
+#                     "envConfig": {
+#                         "OSMRO_LOG_LEVEL": "INFO",
+#                         "OSMRO_MESSAGE_DRIVER": "kafka",
+#                         "OSMRO_MESSAGE_HOST": "kafka",
+#                         "OSMRO_MESSAGE_PORT": "9090",
+#                         "OSMRO_DATABASE_DRIVER": "mongo",
+#                         "OSMRO_DATABASE_URI": "mongodb://mongo",
+#                         "OSMRO_DATABASE_COMMONKEY": "osm",
+#                     },
+#                     "kubernetes": {
+#                         "startupProbe": {
+#                             "exec": {"command": ["/usr/bin/pgrep", "python3"]},
+#                             "initialDelaySeconds": 60,
+#                             "timeoutSeconds": 5,
+#                         },
+#                         "readinessProbe": {
+#                             "httpGet": {
+#                                 "path": "/openmano/tenants",
+#                                 "port": 9090,
+#                             },
+#                             "periodSeconds": 10,
+#                             "timeoutSeconds": 5,
+#                             "successThreshold": 1,
+#                             "failureThreshold": 3,
+#                         },
+#                         "livenessProbe": {
+#                             "httpGet": {
+#                                 "path": "/openmano/tenants",
+#                                 "port": 9090,
+#                             },
+#                             "initialDelaySeconds": 600,
+#                             "periodSeconds": 10,
+#                             "timeoutSeconds": 5,
+#                             "successThreshold": 1,
+#                             "failureThreshold": 3,
+#                         },
+#                     },
+#                 }
+#             ],
+#             "kubernetesResources": {"ingressResources": []},
+#         }
 
-        # Verifying status message
-        self.assertGreater(len(self.harness.charm.unit.status.message), 0)
-        self.assertTrue(
-            self.harness.charm.unit.status.message.startswith("Waiting for ")
-        )
-        self.assertIn("mongodb", self.harness.charm.unit.status.message)
-        self.assertTrue(self.harness.charm.unit.status.message.endswith(" relation"))
+#         self.harness.charm.on.start.emit()
 
-    def test_on_mongodb_unit_relation_changed(self) -> NoReturn:
-        """Test to see if mongodb relation is updated."""
-        self.harness.charm.on.start.emit()
+#         # Initializing the kafka relation
+#         relation_id = self.harness.add_relation("kafka", "kafka")
+#         self.harness.add_relation_unit(relation_id, "kafka/0")
+#         self.harness.update_relation_data(
+#             relation_id,
+#             "kafka/0",
+#             {
+#                 "host": "kafka",
+#                 "port": "9090",
+#             },
+#         )
 
-        relation_id = self.harness.add_relation("mongodb", "mongodb")
-        self.harness.add_relation_unit(relation_id, "mongodb/0")
-        self.harness.update_relation_data(
-            relation_id,
-            "mongodb/0",
-            {
-                "connection_string": "mongodb://mongo",
-            },
-        )
+#         # Initializing the mongodb relation
+#         relation_id = self.harness.add_relation("mongodb", "mongodb")
+#         self.harness.add_relation_unit(relation_id, "mongodb/0")
+#         self.harness.update_relation_data(
+#             relation_id,
+#             "mongodb/0",
+#             {
+#                 "connection_string": "mongodb://mongo",
+#             },
+#         )
 
-        # Verifying status
-        self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
+#         # Verifying status
+#         self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
 
-        # Verifying status message
-        self.assertGreater(len(self.harness.charm.unit.status.message), 0)
-        self.assertTrue(
-            self.harness.charm.unit.status.message.startswith("Waiting for ")
-        )
-        self.assertIn("kafka", self.harness.charm.unit.status.message)
-        self.assertTrue(self.harness.charm.unit.status.message.endswith(" relation"))
+#         pod_spec, _ = self.harness.get_pod_spec()
 
-    def test_on_mysql_unit_relation_changed(self) -> NoReturn:
-        """Test to see if mysql relation is updated."""
-        self.harness.charm.on.start.emit()
+#         self.assertDictEqual(expected_result, pod_spec)
 
-        relation_id = self.harness.add_relation("mysql", "mysql")
-        self.harness.add_relation_unit(relation_id, "mysql/0")
-        self.harness.update_relation_data(
-            relation_id,
-            "mysql/0",
-            {
-                "host": "mysql",
-                "port": 3306,
-                "user": "mano",
-                "password": "manopw",
-                "root_password": "rootmanopw",
-            },
-        )
+#     def test_on_start_with_relations_no_ng_ro(self) -> NoReturn:
+#         """Test deployment with old RO."""
+#         self.harness.update_config({"enable_ng_ro": False})
 
-        # Verifying status
-        self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
+#         expected_result = {
+#             "version": 3,
+#             "containers": [
+#                 {
+#                     "name": "ro",
+#                     "imageDetails": self.harness.charm.image.fetch(),
+#                     "imagePullPolicy": "Always",
+#                     "ports": [
+#                         {
+#                             "name": "ro",
+#                             "containerPort": 9090,
+#                             "protocol": "TCP",
+#                         }
+#                     ],
+#                     "envConfig": {
+#                         "OSMRO_LOG_LEVEL": "INFO",
+#                         "RO_DB_HOST": "mysql",
+#                         "RO_DB_OVIM_HOST": "mysql",
+#                         "RO_DB_PORT": 3306,
+#                         "RO_DB_OVIM_PORT": 3306,
+#                         "RO_DB_USER": "mano",
+#                         "RO_DB_OVIM_USER": "mano",
+#                         "RO_DB_PASSWORD": "manopw",
+#                         "RO_DB_OVIM_PASSWORD": "manopw",
+#                         "RO_DB_ROOT_PASSWORD": "rootmanopw",
+#                         "RO_DB_OVIM_ROOT_PASSWORD": "rootmanopw",
+#                         "RO_DB_NAME": "mano_db",
+#                         "RO_DB_OVIM_NAME": "mano_vim_db",
+#                         "OPENMANO_TENANT": "osm",
+#                     },
+#                     "kubernetes": {
+#                         "startupProbe": {
+#                             "exec": {"command": ["/usr/bin/pgrep", "python3"]},
+#                             "initialDelaySeconds": 60,
+#                             "timeoutSeconds": 5,
+#                         },
+#                         "readinessProbe": {
+#                             "httpGet": {
+#                                 "path": "/openmano/tenants",
+#                                 "port": 9090,
+#                             },
+#                             "periodSeconds": 10,
+#                             "timeoutSeconds": 5,
+#                             "successThreshold": 1,
+#                             "failureThreshold": 3,
+#                         },
+#                         "livenessProbe": {
+#                             "httpGet": {
+#                                 "path": "/openmano/tenants",
+#                                 "port": 9090,
+#                             },
+#                             "initialDelaySeconds": 600,
+#                             "periodSeconds": 10,
+#                             "timeoutSeconds": 5,
+#                             "successThreshold": 1,
+#                             "failureThreshold": 3,
+#                         },
+#                     },
+#                 }
+#             ],
+#             "kubernetesResources": {"ingressResources": []},
+#         }
 
-        # Verifying status message
-        self.assertGreater(len(self.harness.charm.unit.status.message), 0)
-        self.assertTrue(
-            self.harness.charm.unit.status.message.startswith("Waiting for ")
-        )
-        self.assertIn("kafka", self.harness.charm.unit.status.message)
-        self.assertIn("mongodb", self.harness.charm.unit.status.message)
-        self.assertTrue(self.harness.charm.unit.status.message.endswith(" relations"))
+#         self.harness.charm.on.start.emit()
 
-    def test_publish_ro_info(self) -> NoReturn:
-        """Test to see if ro relation is updated."""
-        expected_result = {
-            "host": "ro",
-            "port": "9090",
-        }
+#         # Initializing the mysql relation
+#         relation_id = self.harness.add_relation("mysql", "mysql")
+#         self.harness.add_relation_unit(relation_id, "mysql/0")
+#         self.harness.update_relation_data(
+#             relation_id,
+#             "mysql/0",
+#             {
+#                 "host": "mysql",
+#                 "port": 3306,
+#                 "user": "mano",
+#                 "password": "manopw",
+#                 "root_password": "rootmanopw",
+#             },
+#         )
 
-        self.harness.charm.on.start.emit()
+#         # Verifying status
+#         self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
 
-        relation_id = self.harness.add_relation("ro", "lcm")
-        self.harness.add_relation_unit(relation_id, "lcm/0")
-        relation_data = self.harness.get_relation_data(relation_id, "ro")
+#         pod_spec, _ = self.harness.get_pod_spec()
 
-        self.assertDictEqual(expected_result, relation_data)
+#         self.assertDictEqual(expected_result, pod_spec)
+
+#     def test_on_kafka_unit_relation_changed(self) -> NoReturn:
+#         """Test to see if kafka relation is updated."""
+#         self.harness.charm.on.start.emit()
+
+#         relation_id = self.harness.add_relation("kafka", "kafka")
+#         self.harness.add_relation_unit(relation_id, "kafka/0")
+#         self.harness.update_relation_data(
+#             relation_id,
+#             "kafka/0",
+#             {
+#                 "host": "kafka",
+#                 "port": 9090,
+#             },
+#         )
+
+#         # Verifying status
+#         self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
+
+#         # Verifying status message
+#         self.assertGreater(len(self.harness.charm.unit.status.message), 0)
+#         self.assertTrue(
+#             self.harness.charm.unit.status.message.startswith("Waiting for ")
+#         )
+#         self.assertIn("mongodb", self.harness.charm.unit.status.message)
+#         self.assertTrue(self.harness.charm.unit.status.message.endswith(" relation"))
+
+#     def test_on_mongodb_unit_relation_changed(self) -> NoReturn:
+#         """Test to see if mongodb relation is updated."""
+#         self.harness.charm.on.start.emit()
+
+#         relation_id = self.harness.add_relation("mongodb", "mongodb")
+#         self.harness.add_relation_unit(relation_id, "mongodb/0")
+#         self.harness.update_relation_data(
+#             relation_id,
+#             "mongodb/0",
+#             {
+#                 "connection_string": "mongodb://mongo",
+#             },
+#         )
+
+#         # Verifying status
+#         self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
+
+#         # Verifying status message
+#         self.assertGreater(len(self.harness.charm.unit.status.message), 0)
+#         self.assertTrue(
+#             self.harness.charm.unit.status.message.startswith("Waiting for ")
+#         )
+#         self.assertIn("kafka", self.harness.charm.unit.status.message)
+#         self.assertTrue(self.harness.charm.unit.status.message.endswith(" relation"))
+
+#     def test_on_mysql_unit_relation_changed(self) -> NoReturn:
+#         """Test to see if mysql relation is updated."""
+#         self.harness.charm.on.start.emit()
+
+#         relation_id = self.harness.add_relation("mysql", "mysql")
+#         self.harness.add_relation_unit(relation_id, "mysql/0")
+#         self.harness.update_relation_data(
+#             relation_id,
+#             "mysql/0",
+#             {
+#                 "host": "mysql",
+#                 "port": 3306,
+#                 "user": "mano",
+#                 "password": "manopw",
+#                 "root_password": "rootmanopw",
+#             },
+#         )
+
+#         # Verifying status
+#         self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
+
+#         # Verifying status message
+#         self.assertGreater(len(self.harness.charm.unit.status.message), 0)
+#         self.assertTrue(
+#             self.harness.charm.unit.status.message.startswith("Waiting for ")
+#         )
+#         self.assertIn("kafka", self.harness.charm.unit.status.message)
+#         self.assertIn("mongodb", self.harness.charm.unit.status.message)
+#         self.assertTrue(self.harness.charm.unit.status.message.endswith(" relations"))
+
+#     def test_publish_ro_info(self) -> NoReturn:
+#         """Test to see if ro relation is updated."""
+#         expected_result = {
+#             "host": "ro",
+#             "port": "9090",
+#         }
+
+#         self.harness.charm.on.start.emit()
+
+#         relation_id = self.harness.add_relation("ro", "lcm")
+#         self.harness.add_relation_unit(relation_id, "lcm/0")
+#         relation_data = self.harness.get_relation_data(relation_id, "ro")
+
+#         self.assertDictEqual(expected_result, relation_data)
 
 
 if __name__ == "__main__":
diff --git a/installers/charm/ro/tox.ini b/installers/charm/ro/tox.ini
index 8fd07d3..1f9442e 100644
--- a/installers/charm/ro/tox.ini
+++ b/installers/charm/ro/tox.ini
@@ -1,4 +1,4 @@
-# Copyright 2020 Canonical Ltd.
+# 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
@@ -18,64 +18,98 @@
 # To get in touch with the maintainers, please contact:
 # osm-charmers@lists.launchpad.net
 ##
+#######################################################################################
 
 [tox]
+envlist = flake8, cover, pylint, safety, yamllint
 skipsdist = True
-envlist = unit, lint
-sitepackages = False
-skip_missing_interpreters = False
 
 [testenv]
 basepython = python3.8
 setenv =
+  VIRTUAL_ENV={envdir}
   PYTHONHASHSEED=0
   PYTHONPATH = {toxinidir}/src
-  CHARM_NAME = ro
+deps =  -r{toxinidir}/requirements.txt
 
+#######################################################################################
+[testenv:cover]
+deps =  {[testenv]deps}
+        -r{toxinidir}/requirements-test.txt
+commands =
+        sh -c 'rm -f nosetests.xml'
+        coverage erase
+        nose2 -C --coverage src
+        coverage report --omit='*tests*'
+        coverage html -d ./cover --omit='*tests*'
+        coverage xml -o coverage.xml --omit=*tests*
+whitelist_externals = sh
+
+#######################################################################################
+[testenv:safety]
+setenv =
+        LC_ALL=C.UTF-8
+        LANG=C.UTF-8
+deps =  {[testenv]deps}
+        -r{toxinidir}/requirements-test.txt
+commands =
+        - safety check --full-report
+
+#######################################################################################
+[testenv:flake8]
+deps = flake8
+commands =
+        flake8 src/ tests/
+
+#######################################################################################
+[testenv:pylint]
+deps =  {[testenv]deps}
+        -r{toxinidir}/requirements-test.txt
+        pylint
+commands =
+    pylint -E src
+
+#######################################################################################
+[testenv:black]
+deps =  {[testenv]deps}
+        -r{toxinidir}/requirements-test.txt
+        black
+commands =  black --check --diff . --exclude "build/|.tox/|mod/|lib/"
+
+#######################################################################################
+[testenv:yamllint]
+deps =  {[testenv]deps}
+        -r{toxinidir}/requirements-test.txt
+        yamllint
+commands = yamllint .
+
+#######################################################################################
 [testenv:build]
 passenv=HTTP_PROXY HTTPS_PROXY NO_PROXY
+deps =  {[testenv]deps}
+        -r{toxinidir}/requirements-test.txt
+        charmcraft
 whitelist_externals =
   charmcraft
-  rm
-  unzip
+  cp
 commands =
-  rm -rf release ro.charm
   charmcraft build
-  unzip ro.charm -d release
+  cp -r build release
 
-[testenv:unit]
-commands =
-  coverage erase
-  stestr run --slowest --test-path=./tests --top-dir=./
-  coverage combine
-  coverage html -d cover
-  coverage xml -o cover/coverage.xml
-  coverage report
-deps =
-  coverage
-  stestr
-  mock
-  ops
-setenv =
-  {[testenv]setenv}
-  PYTHON=coverage run
+#######################################################################################
+[flake8]
+ignore =
+        W291,
+        W293,
+        E123,
+        E125,
+        E226,
+        E241,
+exclude =
+        .git,
+        __pycache__,
+        .tox,
+max-line-length = 120
+show-source = True
+builtins = _
 
-[testenv:lint]
-deps =
-  black
-  yamllint
-  flake8
-commands =
-  black --check --diff . --exclude "build/|.tox/|mod/|lib/"
-  yamllint .
-  flake8 . --max-line-length=100 --ignore="E501,W503,W504,F722" --exclude "build/ .tox/ mod/ lib/"
-
-[coverage:run]
-branch = True
-concurrency = multiprocessing
-parallel = True
-source =
-  .
-omit =
-  .tox/*
-  tests/*