Adding manual external DB URI config 24/10724/4
authorsousaedu <eduardo.sousa@canonical.com>
Sun, 2 May 2021 22:22:43 +0000 (00:22 +0200)
committersousaedu <eduardo.sousa@canonical.com>
Thu, 6 May 2021 10:02:16 +0000 (12:02 +0200)
Change-Id: I24782d5b9594aebae2ec97b1196acfa2bafb6dbd
Signed-off-by: sousaedu <eduardo.sousa@canonical.com>
28 files changed:
installers/charm/keystone/config.yaml
installers/charm/keystone/src/charm.py
installers/charm/keystone/tests/test_charm.py
installers/charm/keystone/tox.ini
installers/charm/lcm/config.yaml
installers/charm/lcm/src/charm.py
installers/charm/lcm/tests/test_charm.py
installers/charm/lcm/tox.ini
installers/charm/mon/config.yaml
installers/charm/mon/src/charm.py
installers/charm/mon/tests/test_charm.py
installers/charm/mon/tox.ini
installers/charm/nbi/config.yaml
installers/charm/nbi/src/charm.py
installers/charm/nbi/tests/test_charm.py
installers/charm/nbi/tox.ini
installers/charm/pla/config.yaml
installers/charm/pla/src/charm.py
installers/charm/pla/tests/test_charm.py
installers/charm/pla/tox.ini
installers/charm/pol/config.yaml
installers/charm/pol/src/charm.py
installers/charm/pol/tests/test_charm.py
installers/charm/pol/tox.ini
installers/charm/ro/config.yaml
installers/charm/ro/src/charm.py
installers/charm/ro/tests/test_charm.py
installers/charm/ro/tox.ini

index 2758d1c..402096c 100644 (file)
@@ -45,6 +45,15 @@ options:
     type: string
     description: Keystone DB Password
     default: admin
+  mysql_host:
+    type: string
+    description: MySQL Host (external database)
+  mysql_port:
+    type: int
+    description: MySQL Port (external database)
+  mysql_root_password:
+    type: string
+    description: MySQL Root Password (external database)
   admin_username:
     type: string
     description: Admin username to be created when starting the service
index 5d8d317..b5ce0cc 100755 (executable)
@@ -80,6 +80,9 @@ class ConfigModel(ModelValidator):
     site_url: Optional[str]
     ingress_whitelist_source_range: Optional[str]
     tls_secret_name: Optional[str]
+    mysql_host: Optional[str]
+    mysql_port: Optional[int]
+    mysql_root_password: Optional[str]
 
     @validator("max_file_size")
     def validate_max_file_size(cls, v):
@@ -101,6 +104,12 @@ class ConfigModel(ModelValidator):
             ip_network(v)
         return v
 
+    @validator("mysql_port")
+    def validate_mysql_port(cls, v):
+        if v and (v <= 0 or v >= 65535):
+            raise ValueError("Mysql port out of range")
+        return v
+
 
 class ConfigLdapModel(ModelValidator):
     ldap_enabled: bool
@@ -170,11 +179,19 @@ class KeystoneCharm(CharmedOsmBase):
 
     def _check_missing_dependencies(self, config: ConfigModel):
         missing_relations = []
-        if self.mysql_client.is_missing_data_in_unit():
+        if not config.mysql_host and self.mysql_client.is_missing_data_in_unit():
             missing_relations.append("mysql")
         if missing_relations:
             raise RelationsMissing(missing_relations)
 
+    def _validate_mysql_config(self, config: ConfigModel):
+        invalid_values = []
+        if not config.mysql_root_password:
+            invalid_values.append("Mysql root password must be provided")
+
+        if invalid_values:
+            raise ValueError("Invalid values: " + ", ".join(invalid_values))
+
     def _generate_keys(self) -> Tuple[List[str], List[str]]:
         """Generating new fernet tokens.
 
@@ -229,13 +246,23 @@ class KeystoneCharm(CharmedOsmBase):
         # Validate config
         config = ConfigModel(**dict(self.config))
         config_ldap = ConfigLdapModel(**dict(self.config))
+
+        if config.mysql_host and not self.mysql_client.is_missing_data_in_unit():
+            raise Exception("Mysql data cannot be provided via config and relation")
+
+        if config.mysql_host:
+            self._validate_mysql_config(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)
+
         # Build files
         credential_files, fernet_files = self._build_files(config)
         container_builder.add_volume_config(
@@ -246,10 +273,11 @@ class KeystoneCharm(CharmedOsmBase):
         )
         container_builder.add_envs(
             {
-                "DB_HOST": self.mysql_client.host,
-                "DB_PORT": self.mysql_client.port,
+                "DB_HOST": config.mysql_host or self.mysql_client.host,
+                "DB_PORT": config.mysql_port or self.mysql_client.port,
                 "ROOT_DB_USER": "root",
-                "ROOT_DB_PASSWORD": self.mysql_client.root_password,
+                "ROOT_DB_PASSWORD": config.mysql_root_password
+                or self.mysql_client.root_password,
                 "KEYSTONE_DB_PASSWORD": config.keystone_db_password,
                 "REGION_ID": config.region_id,
                 "KEYSTONE_HOST": self.app.name,
@@ -263,7 +291,6 @@ class KeystoneCharm(CharmedOsmBase):
         )
 
         if config_ldap.ldap_enabled:
-
             container_builder.add_envs(
                 {
                     "LDAP_AUTHENTICATION_DOMAIN_NAME": config_ldap.ldap_authentication_domain_name,
@@ -325,8 +352,10 @@ class KeystoneCharm(CharmedOsmBase):
                     }
                 )
         container = container_builder.build()
+
         # Add container to pod spec
         pod_spec_builder.add_container(container)
+
         # Add ingress resources to pod spec if site url exists
         if config.site_url:
             parsed = urlparse(config.site_url)
index d16e75d..e281b20 100644 (file)
@@ -42,6 +42,9 @@ class TestCharm(unittest.TestCase):
         self.config = {
             "region_id": "str",
             "keystone_db_password": "str",
+            "mysql_host": "",
+            "mysql_port": 3306,
+            "mysql_root_password": "manopw",
             "admin_username": "str",
             "admin_password": "str",
             "admin_project": "str",
@@ -83,6 +86,12 @@ class TestCharm(unittest.TestCase):
         # Assertions
         self.assertIsInstance(self.harness.charm.unit.status, ActiveStatus)
 
+    def test_with_config(self) -> NoReturn:
+        "Test with mysql config"
+        self.initialize_mysql_config()
+        # Verifying status
+        self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
+
     def test_with_relations(
         self,
     ) -> NoReturn:
@@ -91,6 +100,24 @@ class TestCharm(unittest.TestCase):
         # Verifying status
         self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
 
+    def test_exception_mysql_relation_and_config(
+        self,
+    ) -> NoReturn:
+        "Test with relations and config. Must throw exception"
+        self.initialize_mysql_config()
+        self.initialize_mysql_relation()
+        # Verifying status
+        self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
+
+    def initialize_mysql_config(self):
+        self.harness.update_config(
+            {
+                "mysql_host": "mysql",
+                "mysql_port": 3306,
+                "mysql_root_password": "manopw",
+            }
+        )
+
     def initialize_mysql_relation(self):
         relation_id = self.harness.add_relation("db", "mysql")
         self.harness.add_relation_unit(relation_id, "mysql/0")
index d0d2570..cb37bc8 100644 (file)
@@ -108,6 +108,7 @@ commands =
 ignore =
         W291,
         W293,
+        W503,
         E123,
         E125,
         E226,
index 6301622..0f0cebb 100644 (file)
@@ -59,6 +59,9 @@ options:
     description: Database common key
     type: string
     default: osm
+  mongodb_uri:
+    type: string
+    description: MongoDB URI (external database)
   log_level:
     description: "Log Level"
     type: string
index e9552fd..52bc5cf 100755 (executable)
@@ -51,6 +51,7 @@ class ConfigModel(ModelValidator):
     vca_cloud: str
     vca_k8s_cloud: str
     database_commonkey: str
+    mongodb_uri: Optional[str]
     log_level: str
     vca_apiproxy: Optional[str]
     # Model-config options
@@ -114,6 +115,12 @@ class ConfigModel(ModelValidator):
             raise ValueError("value must be INFO or DEBUG")
         return v
 
+    @validator("mongodb_uri")
+    def validate_mongodb_uri(cls, v):
+        if v and not v.startswith("mongodb://"):
+            raise ValueError("mongodb_uri is not properly formed")
+        return v
+
 
 class LcmCharm(CharmedOsmBase):
     def __init__(self, *args) -> NoReturn:
@@ -136,7 +143,7 @@ class LcmCharm(CharmedOsmBase):
 
         if self.kafka_client.is_missing_data_in_unit():
             missing_relations.append("kafka")
-        if self.mongodb_client.is_missing_data_in_unit():
+        if not config.mongodb_uri and self.mongodb_client.is_missing_data_in_unit():
             missing_relations.append("mongodb")
         if self.ro_client.is_missing_data_in_app():
             missing_relations.append("ro")
@@ -147,10 +154,16 @@ class LcmCharm(CharmedOsmBase):
     def build_pod_spec(self, image_info):
         # Validate config
         config = ConfigModel(**dict(self.config))
+
+        if config.mongodb_uri and not self.mongodb_client.is_missing_data_in_unit():
+            raise Exception("Mongodb data cannot be provided via config and relation")
+
         # 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)
@@ -169,7 +182,8 @@ class LcmCharm(CharmedOsmBase):
                 "OSMLCM_MESSAGE_PORT": self.kafka_client.port,
                 # Database configuration
                 "OSMLCM_DATABASE_DRIVER": "mongo",
-                "OSMLCM_DATABASE_URI": self.mongodb_client.connection_string,
+                "OSMLCM_DATABASE_URI": config.mongodb_uri
+                or self.mongodb_client.connection_string,
                 "OSMLCM_DATABASE_COMMONKEY": config.database_commonkey,
                 # Storage configuration
                 "OSMLCM_STORAGE_DRIVER": "mongo",
@@ -195,11 +209,15 @@ class LcmCharm(CharmedOsmBase):
             for k, v in self.config.items()
             if k.startswith("vca_model_config")
         }
+
         if model_config_envs:
             container_builder.add_envs(model_config_envs)
+
         container = container_builder.build()
+
         # Add container to pod spec
         pod_spec_builder.add_container(container)
+
         return pod_spec_builder.build()
 
 
index 831e176..3e6b2a4 100644 (file)
@@ -49,6 +49,7 @@ class TestCharm(unittest.TestCase):
             "vca_cloud": "cloud",
             "vca_k8s_cloud": "k8scloud",
             "database_commonkey": "commonkey",
+            "mongodb_uri": "",
             "log_level": "INFO",
         }
         self.harness.update_config(self.config)
@@ -79,6 +80,16 @@ class TestCharm(unittest.TestCase):
         # Assertions
         self.assertIsInstance(self.harness.charm.unit.status, ActiveStatus)
 
+    def test_with_relations_and_mongodb_config(
+        self,
+    ) -> NoReturn:
+        "Test with relations and mongodb config"
+        self.initialize_kafka_relation()
+        self.initialize_mongo_config()
+        self.initialize_ro_relation()
+        # Verifying status
+        self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
+
     def test_with_relations(
         self,
     ) -> NoReturn:
@@ -89,6 +100,15 @@ class TestCharm(unittest.TestCase):
         # Verifying status
         self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
 
+    def test_exception_mongodb_relation_and_config(
+        self,
+    ) -> NoReturn:
+        "Test with all relations and config for mongodb. Must fail"
+        self.initialize_mongo_relation()
+        self.initialize_mongo_config()
+        # Verifying status
+        self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
+
     def test_build_pod_spec(
         self,
     ) -> NoReturn:
@@ -200,6 +220,9 @@ class TestCharm(unittest.TestCase):
             kafka_relation_id, "kafka/0", {"host": "kafka", "port": 9092}
         )
 
+    def initialize_mongo_config(self):
+        self.harness.update_config({"mongodb_uri": "mongodb://mongo:27017"})
+
     def initialize_mongo_relation(self):
         mongodb_relation_id = self.harness.add_relation("mongodb", "mongodb")
         self.harness.add_relation_unit(mongodb_relation_id, "mongodb/0")
index 30e0ea8..f207ac3 100644 (file)
@@ -108,6 +108,7 @@ commands =
 ignore =
         W291,
         W293,
+        W503,
         E123,
         E125,
         E226,
index f6d1ae1..d06b68d 100644 (file)
@@ -36,6 +36,9 @@ options:
     description: Database common key
     type: string
     default: osm
+  mongodb_uri:
+    type: string
+    description: MongoDB URI (external database)
   collector_interval:
     description: Collector interval
     type: int
index ab6dd2d..8c0a6bc 100755 (executable)
@@ -70,6 +70,7 @@ class ConfigModel(ModelValidator):
     vca_secret: str
     vca_cacert: str
     database_commonkey: str
+    mongodb_uri: Optional[str]
     log_level: str
     openstack_default_granularity: int
     global_request_timeout: int
@@ -92,6 +93,12 @@ class ConfigModel(ModelValidator):
         _extract_certificates(v)
         return v
 
+    @validator("mongodb_uri")
+    def validate_mongodb_uri(cls, v):
+        if v and not v.startswith("mongodb://"):
+            raise ValueError("mongodb_uri is not properly formed")
+        return v
+
     @property
     def certificates_dict(cls):
         return _extract_certificates(cls.certificates) if cls.certificates else {}
@@ -126,7 +133,7 @@ class MonCharm(CharmedOsmBase):
 
         if self.kafka_client.is_missing_data_in_unit():
             missing_relations.append("kafka")
-        if self.mongodb_client.is_missing_data_in_unit():
+        if not config.mongodb_uri and self.mongodb_client.is_missing_data_in_unit():
             missing_relations.append("mongodb")
         if self.prometheus_client.is_missing_data_in_app():
             missing_relations.append("prometheus")
@@ -149,15 +156,23 @@ class MonCharm(CharmedOsmBase):
     def build_pod_spec(self, image_info):
         # Validate config
         config = ConfigModel(**dict(self.config))
+
+        if config.mongodb_uri and not self.mongodb_client.is_missing_data_in_unit():
+            raise Exception("Mongodb data cannot be provided via config and relation")
+
         # 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)
         certs_files = self._build_cert_files(config)
+
         if certs_files:
             container_builder.add_volume_config("certs", "/certs", certs_files)
+
         container_builder.add_port(name=self.app.name, port=PORT)
         container_builder.add_envs(
             {
@@ -174,7 +189,8 @@ class MonCharm(CharmedOsmBase):
                 "OSMMON_MESSAGE_PORT": self.kafka_client.port,
                 # Database configuration
                 "OSMMON_DATABASE_DRIVER": "mongo",
-                "OSMMON_DATABASE_URI": self.mongodb_client.connection_string,
+                "OSMMON_DATABASE_URI": config.mongodb_uri
+                or self.mongodb_client.connection_string,
                 "OSMMON_DATABASE_COMMONKEY": config.database_commonkey,
                 # Prometheus configuration
                 "OSMMON_PROMETHEUS_URL": f"http://{self.prometheus_client.hostname}:{self.prometheus_client.port}",
@@ -200,10 +216,11 @@ class MonCharm(CharmedOsmBase):
                     "OSMMON_KEYSTONE_SERVICE_PROJECT": self.keystone_client.service,
                 }
             )
-
         container = container_builder.build()
+
         # Add container to pod spec
         pod_spec_builder.add_container(container)
+
         return pod_spec_builder.build()
 
 
index dcf74ed..a7cef20 100644 (file)
@@ -76,6 +76,7 @@ class TestCharm(unittest.TestCase):
             "vca_secret": "admin",
             "vca_cacert": "cacert",
             "database_commonkey": "commonkey",
+            "mongodb_uri": "",
             "log_level": "INFO",
             "openstack_default_granularity": 10,
             "global_request_timeout": 10,
@@ -112,6 +113,17 @@ class TestCharm(unittest.TestCase):
         # Assertions
         self.assertIsInstance(self.harness.charm.unit.status, ActiveStatus)
 
+    def test_with_relations_and_mongodb_config(
+        self,
+    ) -> NoReturn:
+        "Test with relations (internal)"
+        self.initialize_kafka_relation()
+        self.initialize_mongo_config()
+        self.initialize_prometheus_relation()
+        self.initialize_keystone_relation()
+        # Verifying status
+        self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
+
     def test_with_relations(
         self,
     ) -> NoReturn:
@@ -123,6 +135,15 @@ class TestCharm(unittest.TestCase):
         # Verifying status
         self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
 
+    def test_exception_mongodb_relation_and_config(
+        self,
+    ) -> NoReturn:
+        "Test with relations and config for mongodb. Must fail"
+        self.initialize_mongo_relation()
+        self.initialize_mongo_config()
+        # Verifying status
+        self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
+
     def initialize_kafka_relation(self):
         kafka_relation_id = self.harness.add_relation("kafka", "kafka")
         self.harness.add_relation_unit(kafka_relation_id, "kafka/0")
@@ -130,6 +151,9 @@ class TestCharm(unittest.TestCase):
             kafka_relation_id, "kafka/0", {"host": "kafka", "port": 9092}
         )
 
+    def initialize_mongo_config(self):
+        self.harness.update_config({"mongodb_uri": "mongodb://mongo:27017"})
+
     def initialize_mongo_relation(self):
         mongodb_relation_id = self.harness.add_relation("mongodb", "mongodb")
         self.harness.add_relation_unit(mongodb_relation_id, "mongodb/0")
index 30e0ea8..f207ac3 100644 (file)
@@ -108,6 +108,7 @@ commands =
 ignore =
         W291,
         W293,
+        W503,
         E123,
         E125,
         E226,
index ef0792b..a3d94bc 100644 (file)
@@ -64,3 +64,6 @@ options:
     type: boolean
     description: Enable test endpoints of NBI.
     default: false
+  mongodb_uri:
+    type: string
+    description: MongoDB URI (external database)
index 1f5812a..550c88b 100755 (executable)
@@ -59,6 +59,7 @@ class ConfigModel(ModelValidator):
     cluster_issuer: Optional[str]
     ingress_whitelist_source_range: Optional[str]
     tls_secret_name: Optional[str]
+    mongodb_uri: Optional[str]
 
     @validator("auth_backend")
     def validate_auth_backend(cls, v):
@@ -92,6 +93,12 @@ class ConfigModel(ModelValidator):
             ip_network(v)
         return v
 
+    @validator("mongodb_uri")
+    def validate_mongodb_uri(cls, v):
+        if v and not v.startswith("mongodb://"):
+            raise ValueError("mongodb_uri is not properly formed")
+        return v
+
 
 class NbiCharm(CharmedOsmBase):
     def __init__(self, *args) -> NoReturn:
@@ -134,7 +141,7 @@ class NbiCharm(CharmedOsmBase):
 
         if self.kafka_client.is_missing_data_in_unit():
             missing_relations.append("kafka")
-        if self.mongodb_client.is_missing_data_in_unit():
+        if not config.mongodb_uri and self.mongodb_client.is_missing_data_in_unit():
             missing_relations.append("mongodb")
         if self.prometheus_client.is_missing_data_in_app():
             missing_relations.append("prometheus")
@@ -148,10 +155,16 @@ class NbiCharm(CharmedOsmBase):
     def build_pod_spec(self, image_info):
         # Validate config
         config = ConfigModel(**dict(self.config))
+
+        if config.mongodb_uri and not self.mongodb_client.is_missing_data_in_unit():
+            raise Exception("Mongodb data cannot be provided via config and relation")
+
         # Check relations
         self._check_missing_dependencies(config)
+
         # Create Builder for the PodSpec
         pod_spec_builder = PodSpecV3Builder()
+
         # Build Init Container
         pod_spec_builder.add_init_container(
             {
@@ -164,6 +177,7 @@ class NbiCharm(CharmedOsmBase):
                 ],
             }
         )
+
         # Build Container
         container_builder = ContainerV3Builder(self.app.name, image_info)
         container_builder.add_port(name=self.app.name, port=PORT)
@@ -189,7 +203,8 @@ class NbiCharm(CharmedOsmBase):
                 "OSMNBI_MESSAGE_PORT": self.kafka_client.port,
                 # Database configuration
                 "OSMNBI_DATABASE_DRIVER": "mongo",
-                "OSMNBI_DATABASE_URI": self.mongodb_client.connection_string,
+                "OSMNBI_DATABASE_URI": config.mongodb_uri
+                or self.mongodb_client.connection_string,
                 "OSMNBI_DATABASE_COMMONKEY": config.database_commonkey,
                 # Storage configuration
                 "OSMNBI_STORAGE_DRIVER": "mongo",
@@ -219,8 +234,10 @@ class NbiCharm(CharmedOsmBase):
                 }
             )
         container = container_builder.build()
+
         # Add container to pod spec
         pod_spec_builder.add_container(container)
+
         # Add ingress resources to pod spec if site url exists
         if config.site_url:
             parsed = urlparse(config.site_url)
@@ -254,7 +271,9 @@ class NbiCharm(CharmedOsmBase):
             ingress_resource_builder.add_rule(parsed.hostname, self.app.name, PORT)
             ingress_resource = ingress_resource_builder.build()
             pod_spec_builder.add_ingress_resource(ingress_resource)
+
         logger.debug(pod_spec_builder.build())
+
         return pod_spec_builder.build()
 
 
index 6543335..116c06b 100644 (file)
@@ -43,6 +43,7 @@ class TestCharm(unittest.TestCase):
             "enable_test": False,
             "auth_backend": "internal",
             "database_commonkey": "key",
+            "mongodb_uri": "",
             "log_level": "INFO",
             "max_file_size": 0,
             "ingress_whitelist_source_range": "",
@@ -78,6 +79,16 @@ class TestCharm(unittest.TestCase):
         # Assertions
         self.assertIsInstance(self.harness.charm.unit.status, ActiveStatus)
 
+    def test_with_relations_internal_and_mongodb_config(
+        self,
+    ) -> NoReturn:
+        "Test with relations and mongodb config (internal)"
+        self.initialize_kafka_relation()
+        self.initialize_mongo_config()
+        self.initialize_prometheus_relation()
+        # Verifying status
+        self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
+
     def test_with_relations_internal(
         self,
     ) -> NoReturn:
@@ -88,6 +99,18 @@ class TestCharm(unittest.TestCase):
         # Verifying status
         self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
 
+    def test_with_relations_and_mongodb_config_with_keystone_missing(
+        self,
+    ) -> NoReturn:
+        "Test with relations and mongodb config (keystone)"
+        self.harness.update_config({"auth_backend": "keystone"})
+        self.initialize_kafka_relation()
+        self.initialize_mongo_config()
+        self.initialize_prometheus_relation()
+        # Verifying status
+        self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
+        self.assertTrue("keystone" in self.harness.charm.unit.status.message)
+
     def test_with_relations_keystone_missing(
         self,
     ) -> NoReturn:
@@ -100,6 +123,18 @@ class TestCharm(unittest.TestCase):
         self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
         self.assertTrue("keystone" in self.harness.charm.unit.status.message)
 
+    def test_with_relations_and_mongodb_config_with_keystone(
+        self,
+    ) -> NoReturn:
+        "Test with relations (keystone)"
+        self.harness.update_config({"auth_backend": "keystone"})
+        self.initialize_kafka_relation()
+        self.initialize_mongo_config()
+        self.initialize_prometheus_relation()
+        self.initialize_keystone_relation()
+        # Verifying status
+        self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
+
     def test_with_relations_keystone(
         self,
     ) -> NoReturn:
@@ -112,6 +147,14 @@ class TestCharm(unittest.TestCase):
         # Verifying status
         self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
 
+    def test_mongodb_exception_relation_and_config(
+        self,
+    ) -> NoReturn:
+        self.initialize_mongo_config()
+        self.initialize_mongo_relation()
+        # Verifying status
+        self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
+
     def initialize_kafka_relation(self):
         kafka_relation_id = self.harness.add_relation("kafka", "kafka")
         self.harness.add_relation_unit(kafka_relation_id, "kafka/0")
@@ -119,6 +162,9 @@ class TestCharm(unittest.TestCase):
             kafka_relation_id, "kafka/0", {"host": "kafka", "port": 9092}
         )
 
+    def initialize_mongo_config(self):
+        self.harness.update_config({"mongodb_uri": "mongodb://mongo:27017"})
+
     def initialize_mongo_relation(self):
         mongodb_relation_id = self.harness.add_relation("mongodb", "mongodb")
         self.harness.add_relation_unit(mongodb_relation_id, "mongodb/0")
index 30e0ea8..f207ac3 100644 (file)
@@ -108,6 +108,7 @@ commands =
 ignore =
         W291,
         W293,
+        W503,
         E123,
         E125,
         E226,
index ae90304..1820188 100644 (file)
@@ -24,3 +24,6 @@ options:
     description: Common Key for Mongo database
     type: string
     default: osm
+  mongodb_uri:
+    type: string
+    description: MongoDB URI (external database)
index 15ae095..d9dfaa4 100755 (executable)
@@ -24,7 +24,7 @@
 
 
 import logging
-from typing import NoReturn
+from typing import NoReturn, Optional
 
 from ops.main import main
 from opslib.osm.charm import CharmedOsmBase, RelationsMissing
@@ -44,6 +44,7 @@ PORT = 9999
 
 class ConfigModel(ModelValidator):
     database_commonkey: str
+    mongodb_uri: Optional[str]
     log_level: str
 
     @validator("log_level")
@@ -52,6 +53,12 @@ class ConfigModel(ModelValidator):
             raise ValueError("value must be INFO or DEBUG")
         return v
 
+    @validator("mongodb_uri")
+    def validate_mongodb_uri(cls, v):
+        if v and not v.startswith("mongodb://"):
+            raise ValueError("mongodb_uri is not properly formed")
+        return v
+
 
 class PlaCharm(CharmedOsmBase):
     def __init__(self, *args) -> NoReturn:
@@ -70,7 +77,7 @@ class PlaCharm(CharmedOsmBase):
 
         if self.kafka_client.is_missing_data_in_unit():
             missing_relations.append("kafka")
-        if self.mongodb_client.is_missing_data_in_unit():
+        if not config.mongodb_uri and self.mongodb_client.is_missing_data_in_unit():
             missing_relations.append("mongodb")
 
         if missing_relations:
@@ -79,10 +86,16 @@ class PlaCharm(CharmedOsmBase):
     def build_pod_spec(self, image_info):
         # Validate config
         config = ConfigModel(**dict(self.config))
+
+        if config.mongodb_uri and not self.mongodb_client.is_missing_data_in_unit():
+            raise Exception("Mongodb data cannot be provided via config and relation")
+
         # 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)
@@ -97,14 +110,17 @@ class PlaCharm(CharmedOsmBase):
                 "OSMPLA_MESSAGE_PORT": self.kafka_client.port,
                 # Database configuration
                 "OSMPLA_DATABASE_DRIVER": "mongo",
-                "OSMPLA_DATABASE_URI": self.mongodb_client.connection_string,
+                "OSMPLA_DATABASE_URI": config.mongodb_uri
+                or self.mongodb_client.connection_string,
                 "OSMPLA_DATABASE_COMMONKEY": config.database_commonkey,
             }
         )
 
         container = container_builder.build()
+
         # Add container to pod spec
         pod_spec_builder.add_container(container)
+
         return pod_spec_builder.build()
 
 
index 80cb315..debc378 100644 (file)
@@ -41,6 +41,7 @@ class TestCharm(unittest.TestCase):
         self.harness.begin()
         self.config = {
             "log_level": "INFO",
+            "mongodb_uri": "",
         }
         self.harness.update_config(self.config)
 
@@ -70,6 +71,15 @@ class TestCharm(unittest.TestCase):
         # Assertions
         self.assertIsInstance(self.harness.charm.unit.status, ActiveStatus)
 
+    def test_with_relations_and_mongodb_config(
+        self,
+    ) -> NoReturn:
+        "Test with relations and mongodb config (internal)"
+        self.initialize_kafka_relation()
+        self.initialize_mongo_config()
+        # Verifying status
+        self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
+
     def test_with_relations(
         self,
     ) -> NoReturn:
@@ -79,6 +89,15 @@ class TestCharm(unittest.TestCase):
         # Verifying status
         self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
 
+    def test_exception_mongodb_relation_and_config(
+        self,
+    ) -> NoReturn:
+        "Test with relation and config for Mongodb. Test must fail"
+        self.initialize_mongo_relation()
+        self.initialize_mongo_config()
+        # Verifying status
+        self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
+
     def initialize_kafka_relation(self):
         kafka_relation_id = self.harness.add_relation("kafka", "kafka")
         self.harness.add_relation_unit(kafka_relation_id, "kafka/0")
@@ -86,6 +105,9 @@ class TestCharm(unittest.TestCase):
             kafka_relation_id, "kafka/0", {"host": "kafka", "port": 9092}
         )
 
+    def initialize_mongo_config(self):
+        self.harness.update_config({"mongodb_uri": "mongodb://mongo:27017"})
+
     def initialize_mongo_relation(self):
         mongodb_relation_id = self.harness.add_relation("mongodb", "mongodb")
         self.harness.add_relation_unit(mongodb_relation_id, "mongodb/0")
index 30e0ea8..f207ac3 100644 (file)
@@ -108,6 +108,7 @@ commands =
 ignore =
         W291,
         W293,
+        W503,
         E123,
         E125,
         E226,
index 1ae26a4..59f695b 100644 (file)
@@ -24,3 +24,6 @@ options:
     description: "Log Level"
     type: string
     default: "INFO"
+  mongodb_uri:
+    type: string
+    description: MongoDB URI (external database)
index d339e1c..b2e5883 100755 (executable)
@@ -24,7 +24,7 @@
 
 
 import logging
-from typing import NoReturn
+from typing import NoReturn, Optional
 
 from ops.main import main
 from opslib.osm.charm import CharmedOsmBase, RelationsMissing
@@ -44,6 +44,7 @@ PORT = 9999
 
 class ConfigModel(ModelValidator):
     log_level: str
+    mongodb_uri: Optional[str]
 
     @validator("log_level")
     def validate_log_level(cls, v):
@@ -51,6 +52,12 @@ class ConfigModel(ModelValidator):
             raise ValueError("value must be INFO or DEBUG")
         return v
 
+    @validator("mongoddb_uri")
+    def validate_mongodb_uri(cls, v):
+        if v and not v.startswith("mongodb://"):
+            raise ValueError("mongodb_uri is not properly formed")
+        return v
+
 
 class PolCharm(CharmedOsmBase):
     def __init__(self, *args) -> NoReturn:
@@ -69,7 +76,7 @@ class PolCharm(CharmedOsmBase):
 
         if self.kafka_client.is_missing_data_in_unit():
             missing_relations.append("kafka")
-        if self.mongodb_client.is_missing_data_in_unit():
+        if not config.mongodb_uri and self.mongodb_client.is_missing_data_in_unit():
             missing_relations.append("mongodb")
 
         if missing_relations:
@@ -78,10 +85,16 @@ class PolCharm(CharmedOsmBase):
     def build_pod_spec(self, image_info):
         # Validate config
         config = ConfigModel(**dict(self.config))
+
+        if config.mongodb_uri and not self.mongodb_client.is_missing_data_in_unit():
+            raise Exception("Mongodb data cannot be provided via config and relation")
+
         # 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)
@@ -96,13 +109,15 @@ class PolCharm(CharmedOsmBase):
                 "OSMPOL_MESSAGE_PORT": self.kafka_client.port,
                 # Database configuration
                 "OSMPOL_DATABASE_DRIVER": "mongo",
-                "OSMPOL_DATABASE_URI": self.mongodb_client.connection_string,
+                "OSMPOL_DATABASE_URI": config.mongodb_uri
+                or self.mongodb_client.connection_string,
             }
         )
-
         container = container_builder.build()
+
         # Add container to pod spec
         pod_spec_builder.add_container(container)
+
         return pod_spec_builder.build()
 
 
index 32b1acd..089b6e5 100644 (file)
@@ -40,6 +40,7 @@ class TestCharm(unittest.TestCase):
         self.harness.begin()
         self.config = {
             "log_level": "INFO",
+            "mongodb_uri": "",
         }
         self.harness.update_config(self.config)
 
@@ -69,6 +70,15 @@ class TestCharm(unittest.TestCase):
         # Assertions
         self.assertIsInstance(self.harness.charm.unit.status, ActiveStatus)
 
+    def test_with_relations_and_mongodb_config(
+        self,
+    ) -> NoReturn:
+        "Test with relations and mongodb config (internal)"
+        self.initialize_kafka_relation()
+        self.initialize_mongo_config()
+        # Verifying status
+        self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
+
     def test_with_relations(
         self,
     ) -> NoReturn:
@@ -78,6 +88,15 @@ class TestCharm(unittest.TestCase):
         # Verifying status
         self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
 
+    def test_exception_mongodb_relation_and_config(
+        self,
+    ) -> NoReturn:
+        "Test with relation and config for Mongodb. Must fail"
+        self.initialize_mongo_relation()
+        self.initialize_mongo_config()
+        # Verifying status
+        self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
+
     def initialize_kafka_relation(self):
         kafka_relation_id = self.harness.add_relation("kafka", "kafka")
         self.harness.add_relation_unit(kafka_relation_id, "kafka/0")
@@ -85,6 +104,9 @@ class TestCharm(unittest.TestCase):
             kafka_relation_id, "kafka/0", {"host": "kafka", "port": 9092}
         )
 
+    def initialize_mongo_config(self):
+        self.harness.update_config({"mongodb_uri": "mongodb://mongo:27017"})
+
     def initialize_mongo_relation(self):
         mongodb_relation_id = self.harness.add_relation("mongodb", "mongodb")
         self.harness.add_relation_unit(mongodb_relation_id, "mongodb/0")
index 30e0ea8..f207ac3 100644 (file)
@@ -108,6 +108,7 @@ commands =
 ignore =
         W291,
         W293,
+        W503,
         E123,
         E125,
         E226,
index bb667d7..0d3c9a0 100644 (file)
@@ -28,10 +28,28 @@ options:
     description: Database COMMON KEY
     type: string
     default: osm
+  mongodb_uri:
+    type: string
+    description: MongoDB URI (external database)
   log_level:
     description: "Log Level"
     type: string
     default: "INFO"
+  mysql_host:
+    type: string
+    description: MySQL Host (external database)
+  mysql_port:
+    type: int
+    description: MySQL Port (external database)
+  mysql_user:
+    type: string
+    description: MySQL User (external database)
+  mysql_password:
+    type: string
+    description: MySQL Password (external database)
+  mysql_root_password:
+    type: string
+    description: MySQL Root Password (external database)
   vim_database:
     type: string
     description: "The database name."
index 5b40c16..951f281 100755 (executable)
@@ -62,7 +62,13 @@ def decode(content: str):
 class ConfigModel(ModelValidator):
     enable_ng_ro: bool
     database_commonkey: str
+    mongodb_uri: Optional[str]
     log_level: str
+    mysql_host: Optional[str]
+    mysql_port: Optional[int]
+    mysql_user: Optional[str]
+    mysql_password: Optional[str]
+    mysql_root_password: Optional[str]
     vim_database: str
     ro_database: str
     openmano_tenant: str
@@ -80,6 +86,18 @@ class ConfigModel(ModelValidator):
         _extract_certificates(v)
         return v
 
+    @validator("mongodb_uri")
+    def validate_mongodb_uri(cls, v):
+        if v and not v.startswith("mongodb://"):
+            raise ValueError("mongodb_uri is not properly formed")
+        return v
+
+    @validator("mysql_port")
+    def validate_mysql_port(cls, v):
+        if v and (v <= 0 or v >= 65535):
+            raise ValueError("Mysql port out of range")
+        return v
+
     @property
     def certificates_dict(cls):
         return _extract_certificates(cls.certificates) if cls.certificates else {}
@@ -126,14 +144,26 @@ class RoCharm(CharmedOsmBase):
         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():
+            if not config.mongodb_uri and self.mongodb_client.is_missing_data_in_unit():
                 missing_relations.append("mongodb")
         else:
-            if self.mysql_client.is_missing_data_in_unit():
+            if not config.mysql_host and self.mysql_client.is_missing_data_in_unit():
                 missing_relations.append("mysql")
         if missing_relations:
             raise RelationsMissing(missing_relations)
 
+    def _validate_mysql_config(self, config: ConfigModel):
+        invalid_values = []
+        if not config.mysql_user:
+            invalid_values.append("Mysql user is empty")
+        if not config.mysql_password:
+            invalid_values.append("Mysql password is empty")
+        if not config.mysql_root_password:
+            invalid_values.append("Mysql root password empty")
+
+        if invalid_values:
+            raise ValueError("Invalid values: " + ", ".join(invalid_values))
+
     def _build_cert_files(
         self,
         config: ConfigModel,
@@ -146,15 +176,32 @@ class RoCharm(CharmedOsmBase):
     def build_pod_spec(self, image_info):
         # Validate config
         config = ConfigModel(**dict(self.config))
+
+        if config.enable_ng_ro:
+            if config.mongodb_uri and not self.mongodb_client.is_missing_data_in_unit():
+                raise Exception(
+                    "Mongodb data cannot be provided via config and relation"
+                )
+        else:
+            if config.mysql_host and not self.mysql_client.is_missing_data_in_unit():
+                raise Exception("Mysql data cannot be provided via config and relation")
+
+            if config.mysql_host:
+                self._validate_mysql_config(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)
         certs_files = self._build_cert_files(config)
+
         if certs_files:
             container_builder.add_volume_config("certs", "/certs", certs_files)
+
         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",
@@ -177,6 +224,7 @@ class RoCharm(CharmedOsmBase):
                 "OSMRO_LOG_LEVEL": config.log_level,
             }
         )
+
         if config.enable_ng_ro:
             container_builder.add_envs(
                 {
@@ -185,7 +233,8 @@ class RoCharm(CharmedOsmBase):
                     "OSMRO_MESSAGE_PORT": self.kafka_client.port,
                     # MongoDB configuration
                     "OSMRO_DATABASE_DRIVER": "mongo",
-                    "OSMRO_DATABASE_URI": self.mongodb_client.connection_string,
+                    "OSMRO_DATABASE_URI": config.mongodb_uri
+                    or self.mongodb_client.connection_string,
                     "OSMRO_DATABASE_COMMONKEY": config.database_commonkey,
                 }
             )
@@ -193,24 +242,30 @@ class RoCharm(CharmedOsmBase):
         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_HOST": config.mysql_host or self.mysql_client.host,
+                    "RO_DB_OVIM_HOST": config.mysql_host or self.mysql_client.host,
+                    "RO_DB_PORT": config.mysql_port or self.mysql_client.port,
+                    "RO_DB_OVIM_PORT": config.mysql_port or self.mysql_client.port,
+                    "RO_DB_USER": config.mysql_user or self.mysql_client.user,
+                    "RO_DB_OVIM_USER": config.mysql_user or self.mysql_client.user,
+                    "RO_DB_PASSWORD": config.mysql_password
+                    or self.mysql_client.password,
+                    "RO_DB_OVIM_PASSWORD": config.mysql_password
+                    or self.mysql_client.password,
+                    "RO_DB_ROOT_PASSWORD": config.mysql_root_password
+                    or self.mysql_client.root_password,
+                    "RO_DB_OVIM_ROOT_PASSWORD": config.mysql_root_password
+                    or 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()
 
 
index 9cced45..2d317bd 100644 (file)
@@ -71,6 +71,7 @@ class TestCharm(unittest.TestCase):
         self.config = {
             "enable_ng_ro": True,
             "database_commonkey": "commonkey",
+            "mongodb_uri": "",
             "log_level": "INFO",
             "vim_database": "db_name",
             "ro_database": "ro_db_name",
@@ -115,6 +116,24 @@ class TestCharm(unittest.TestCase):
         # Assertions
         self.assertIsInstance(self.harness.charm.unit.status, ActiveStatus)
 
+    def test_with_relations_and_mongodb_config_ng(
+        self,
+    ) -> NoReturn:
+        "Test with relations (ng-ro)"
+
+        # Initializing the kafka relation
+        kafka_relation_id = self.harness.add_relation("kafka", "kafka")
+        self.harness.add_relation_unit(kafka_relation_id, "kafka/0")
+        self.harness.update_relation_data(
+            kafka_relation_id, "kafka/0", {"host": "kafka", "port": 9092}
+        )
+
+        # Initializing the mongodb config
+        self.harness.update_config({"mongodb_uri": "mongodb://mongo:27017"})
+
+        # Verifying status
+        self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
+
     def test_with_relations_ng(
         self,
     ) -> NoReturn:
@@ -139,6 +158,25 @@ class TestCharm(unittest.TestCase):
         # Verifying status
         self.assertNotIsInstance(self.harness.charm.unit.status, BlockedStatus)
 
+    def test_ng_exception_mongodb_relation_and_config(
+        self,
+    ) -> NoReturn:
+        "Test NG-RO mongodb relation and config. Must fail"
+        # 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(
+            mongodb_relation_id,
+            "mongodb/0",
+            {"connection_string": "mongodb://mongo:27017"},
+        )
+
+        # Initializing the mongodb config
+        self.harness.update_config({"mongodb_uri": "mongodb://mongo:27017"})
+
+        # Verifying status
+        self.assertIsInstance(self.harness.charm.unit.status, BlockedStatus)
+
 
 if __name__ == "__main__":
     unittest.main()
index 30e0ea8..f207ac3 100644 (file)
@@ -108,6 +108,7 @@ commands =
 ignore =
         W291,
         W293,
+        W503,
         E123,
         E125,
         E226,