From cf522989dbdccab57144bc32cd2cfbb82e8925cf Mon Sep 17 00:00:00 2001 From: Mia Altieri Date: Wed, 18 Oct 2023 12:33:51 +0000 Subject: [PATCH 1/7] functional status reporting --- lib/charms/mongodb/v1/helpers.py | 29 ++++++++ lib/charms/mongodb/v1/mongos.py | 35 +++++++++- lib/charms/mongodb/v1/shards_interface.py | 82 +++++++++++++++++++++-- 3 files changed, 139 insertions(+), 7 deletions(-) diff --git a/lib/charms/mongodb/v1/helpers.py b/lib/charms/mongodb/v1/helpers.py index d34d5fc7f..a21cc951e 100644 --- a/lib/charms/mongodb/v1/helpers.py +++ b/lib/charms/mongodb/v1/helpers.py @@ -201,6 +201,35 @@ def generate_keyfile() -> str: return "".join([secrets.choice(choices) for _ in range(1024)]) +def prioritise_statuses(mongodb_status, pbm_status, shard_status, config_server_status): + """Returns the status with the highest priority of the given statuses. + + Note: it will never be the case that shard_status and config_server_status are both present + since the mongodb app can either be a shard or a config server, but not both. + """ + # failure in mongodb takes precedence over sharding and config server + if not isinstance(mongodb_status, ActiveStatus): + return mongodb_status + + if shard_status and not isinstance(shard_status, ActiveStatus): + return shard_status + + if config_server_status and not isinstance(config_server_status, ActiveStatus): + return config_server_status + + if pbm_status and not isinstance(pbm_status, ActiveStatus): + return pbm_status + + # if all statuses are active report sharding statuses over mongodb status + if isinstance(shard_status, ActiveStatus): + return shard_status + + if isinstance(config_server_status, ActiveStatus): + return config_server_status + + return mongodb_status + + def build_unit_status(mongodb_config: MongoDBConfiguration, unit_ip: str) -> StatusBase: """Generates the status of a unit based on its status reported by mongod.""" try: diff --git a/lib/charms/mongodb/v1/mongos.py b/lib/charms/mongodb/v1/mongos.py index b378f10a8..919e6e3d4 100644 --- a/lib/charms/mongodb/v1/mongos.py +++ b/lib/charms/mongodb/v1/mongos.py @@ -9,6 +9,7 @@ from charms.mongodb.v0.mongodb import NotReadyError from pymongo import MongoClient, collection +from tenacity import RetryError, Retrying, stop_after_delay, wait_fixed from config import Config @@ -20,7 +21,7 @@ # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 0 +LIBPATCH = 1 # path to store mongodb ketFile logger = logging.getLogger(__name__) @@ -194,7 +195,7 @@ def remove_shard(self, shard_name: str) -> None: removal_info = self.client.admin.command("removeShard", shard_name) # process removal status - self._log_removal_info(removal_info) + self._log_removal_info(removal_info, shard_name) def _is_shard_draining(self, shard_name: str) -> bool: """Reports if a given shard is currently in the draining state. @@ -286,3 +287,33 @@ def _log_removal_info(self, removal_info, shard_name): str(remaining_chunks), ",".join(dbs_to_move), ) + + @property + def is_ready(self) -> bool: + """Is mongos ready for services requests. + + Returns: + True if services is ready False otherwise. Retries over a period of 60 seconds times to + allow server time to start up. + + Raises: + ConfigurationError, ConfigurationError, OperationFailure + """ + try: + for attempt in Retrying(stop=stop_after_delay(60), wait=wait_fixed(3)): + with attempt: + # The ping command is cheap and does not require auth. + self.client.admin.command("ping") + except RetryError: + return False + + return True + + def is_shard_aware(self, shard_name: str) -> bool: + """Returns True if provided shard is shard aware.""" + sc_status = self.client.admin.command("listShards") + for shard in sc_status["shards"]: + if shard["_id"] == shard_name: + return shard["state"] == 1 + + return False diff --git a/lib/charms/mongodb/v1/shards_interface.py b/lib/charms/mongodb/v1/shards_interface.py index 7f50d0b17..bea990aff 100644 --- a/lib/charms/mongodb/v1/shards_interface.py +++ b/lib/charms/mongodb/v1/shards_interface.py @@ -18,6 +18,7 @@ PyMongoError, ) from charms.mongodb.v1.helpers import KEY_FILE +from charms.mongodb.v1.mongodb_provider import LEGACY_REL_NAME, REL_NAME from charms.mongodb.v1.mongos import ( MongosConnection, RemovePrimaryShardError, @@ -43,7 +44,7 @@ # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 0 +LIBPATCH = 1 KEYFILE_KEY = "key-file" HOSTS_KEY = "host" OPERATOR_PASSWORD_KEY = MongoDBUser.get_password_key_name_for_user(OperatorUser.get_username()) @@ -274,6 +275,10 @@ def update_mongos_hosts(self): for relation in self.charm.model.relations[self.relation_name]: self._update_relation_data(relation.id, {HOSTS_KEY: json.dumps(self.charm._unit_ips)}) + def get_config_server_status(self): + """TODO: Implement this function in a separate PR.""" + return None + def _update_relation_data(self, relation_id: int, data: dict) -> None: """Updates a set of key-value pairs in the relation. @@ -355,6 +360,8 @@ def _on_relation_changed(self, event): logger.info("Skipping relation joined event: hook checks re not passed") return + self.charm.unit.status = MaintenanceStatus("Adding shard to config-server") + # shards rely on the config server for secrets relation_data = event.relation.data[event.app] self.update_keyfile(key_file_contents=relation_data.get(KEYFILE_KEY)) @@ -367,8 +374,6 @@ def _on_relation_changed(self, event): event.defer() return - self.charm.unit.status = MaintenanceStatus("Adding shard to config-server") - if not self.charm.unit.is_leader(): return @@ -464,6 +469,42 @@ def wait_for_draining(self, mongos_hosts: List[str]): break + def get_shard_status(self): + """Returns the current status of the shard. + + Note: No need to report if currently draining, since that check block other hooks from + executing. + """ + if not self.charm.is_role(Config.Role.SHARD): + logger.info("skipping status check, charm is not running as a shard") + return None + + if not self.charm.db_initialised: + logger.info("No status for shard to report, waiting for db to be initalised.") + return None + + if self.model.get_relation(LEGACY_REL_NAME): + return BlockedStatus(f"relation {LEGACY_REL_NAME} to shard not supported.") + + if self.model.get_relation(REL_NAME): + return BlockedStatus(f"relation {REL_NAME} to shard not supported.") + + if not self.model.get_relation(self.relation_name) and not self.charm.drained: + return BlockedStatus("missing relation to config server") + + if not self.model.get_relation(self.relation_name) and self.charm.drained: + return ActiveStatus("Shard drained from cluster, ready for removal") + + # todo this check will fail if trying to restart + if not self._is_mongos_reachable(): + return BlockedStatus("Config server unreachable") + + if not self._is_shard_aware(): + return BlockedStatus("Shard is not yet shard aware") + + config_server_name = self.get_related_config_server() + return ActiveStatus(f"Shard connected to config-server: {config_server_name}") + def drained(self, mongos_hosts: Set[str], shard_name: str) -> bool: """Returns whether a shard has been drained from the cluster. @@ -569,13 +610,44 @@ def _update_relation_data(self, relation_id: int, data: dict) -> None: if relation: relation.data[self.charm.model.app].update(data) + def _is_mongos_reachable(self) -> bool: + """Returns True if mongos is reachable.""" + if not self.model.get_relation(self.relation_name): + logger.info("Mongos is not reachable, no relation to config-sever") + return False + + mongos_hosts = self.get_mongos_hosts() + self.charm.remote_mongos_config(set(mongos_hosts)) + config = self.charm.remote_mongos_config(set(mongos_hosts)) + + # use a URI that is not dependent on the operator password, as we are not gauranteed that + # the shard has recieved the password yet. + uri = f"mongodb://{','.join(mongos_hosts)}" + with MongosConnection(config, uri) as mongo: + return mongo.is_ready + + def _is_shard_aware(self) -> bool: + """Returns True if shard is in cluster and shard aware.""" + if not self.model.get_relation(self.relation_name): + logger.info( + "Mongos is not reachable, no relation to config-sever, cannot check shard status." + ) + return False + + mongos_hosts = self.get_mongos_hosts() + with MongosConnection(self.charm.remote_mongos_config(set(mongos_hosts))) as mongo: + return mongo.is_shard_aware(shard_name=self.charm.app.name) + def has_config_server(self) -> bool: """Returns True if currently related to config server.""" return len(self.charm.model.relations[self.relation_name]) > 0 - def get_related_config_server(self) -> List[str]: + def get_related_config_server(self) -> str: """Returns the related config server.""" - return [rel.app.name for rel in self.charm.model.relations[self.relation_name]] + config_server = [rel.app.name for rel in self.charm.model.relations[self.relation_name]] + + # metadata.yaml prevents having multiple config servers + return config_server[0] def get_mongos_hosts(self) -> List[str]: """Returns a list of IP addresses for the mongos hosts.""" From 1a85a8cc4042b3714be6a650672a060fe5f96bbe Mon Sep 17 00:00:00 2001 From: Mia Altieri Date: Wed, 18 Oct 2023 12:53:38 +0000 Subject: [PATCH 2/7] fmt + lint --- lib/charms/mongodb/v1/shards_interface.py | 6 ++--- src/charm.py | 32 ++++++++++++++--------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/lib/charms/mongodb/v1/shards_interface.py b/lib/charms/mongodb/v1/shards_interface.py index bea990aff..df27e8d86 100644 --- a/lib/charms/mongodb/v1/shards_interface.py +++ b/lib/charms/mongodb/v1/shards_interface.py @@ -480,7 +480,7 @@ def get_shard_status(self): return None if not self.charm.db_initialised: - logger.info("No status for shard to report, waiting for db to be initalised.") + logger.info("No status for shard to report, waiting for db to be initialised.") return None if self.model.get_relation(LEGACY_REL_NAME): @@ -620,8 +620,8 @@ def _is_mongos_reachable(self) -> bool: self.charm.remote_mongos_config(set(mongos_hosts)) config = self.charm.remote_mongos_config(set(mongos_hosts)) - # use a URI that is not dependent on the operator password, as we are not gauranteed that - # the shard has recieved the password yet. + # use a URI that is not dependent on the operator password, as we are not guaranteed that + # the shard has received the password yet. uri = f"mongodb://{','.join(mongos_hosts)}" with MongosConnection(config, uri) as mongo: return mongo.is_ready diff --git a/src/charm.py b/src/charm.py index 909330b89..c790dabd0 100755 --- a/src/charm.py +++ b/src/charm.py @@ -32,6 +32,7 @@ generate_keyfile, generate_password, get_create_user_cmd, + prioritise_statuses, ) from charms.mongodb.v1.mongodb_backups import S3_RELATION, MongoDBBackups from charms.mongodb.v1.mongodb_provider import MongoDBProvider @@ -574,20 +575,25 @@ def _on_update_status(self, event: UpdateStatusEvent): if self.unit.is_leader(): self._handle_reconfigure(event) - # update the units status based on it's replica set config and backup status. An error in - # the status of MongoDB takes precedence over pbm status. + # retrieve statuses of different services running on Charmed MongoDB mongodb_status = build_unit_status(self.mongodb_config, self._unit_ip(self.unit)) - pbm_status = self.backups.get_pbm_status() - if ( - not isinstance(mongodb_status, ActiveStatus) - or not self.model.get_relation( - S3_RELATION - ) # if s3 relation doesn't exist only report MongoDB status - or isinstance(pbm_status, ActiveStatus) # pbm is ready then report the MongoDB status - ): - self.unit.status = mongodb_status - else: - self.unit.status = pbm_status + shard_status = self.shard.get_shard_status() if self.is_role(Config.Role.SHARD) else None + config_server_status = ( + self.config_server.get_config_server_status() + if self.is_role(Config.Role.CONFIG_SERVER) + else None + ) + pbm_status = ( + self.backups.get_pbm_status() if self.model.get_relation(S3_RELATION) else None + ) + + # prioritize the statuses of the different services running on Charmed MongoDB + self.unit.status = prioritise_statuses( + mongodb_status, + pbm_status, + shard_status, + config_server_status, + ) def _on_get_primary_action(self, event: ActionEvent): event.set_results({"replica-set-primary": self._primary}) From e7cbdfbb044c4c41607ce35aba25cc5120d510a2 Mon Sep 17 00:00:00 2001 From: Mia Altieri Date: Thu, 19 Oct 2023 08:30:44 +0000 Subject: [PATCH 3/7] status reporting for sharding states complete --- lib/charms/mongodb/v1/shards_interface.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/charms/mongodb/v1/shards_interface.py b/lib/charms/mongodb/v1/shards_interface.py index df27e8d86..a5f2f0832 100644 --- a/lib/charms/mongodb/v1/shards_interface.py +++ b/lib/charms/mongodb/v1/shards_interface.py @@ -360,6 +360,8 @@ def _on_relation_changed(self, event): logger.info("Skipping relation joined event: hook checks re not passed") return + # if re-using an old shard, re-set drained flag. + self.charm.app_peer_data["drained"] = json.dumps(False) self.charm.unit.status = MaintenanceStatus("Adding shard to config-server") # shards rely on the config server for secrets @@ -388,7 +390,7 @@ def _on_relation_changed(self, event): ) return - # TODO future PR, leader unit verifies shard was added to cluster (update-status hook) + self.charm.app_peer_data["added_to_cluster"] = json.dumps(True) def pass_hook_checks(self, event): """Runs the pre-hooks checks for ConfigServerRequirer, returns True if all pass.""" @@ -436,8 +438,9 @@ def _on_relation_broken(self, event: RelationBrokenEvent) -> None: self.wait_for_draining(mongos_hosts) self.charm.unit.status = ActiveStatus("Shard drained from cluster, ready for removal") - # TODO future PR, leader unit displays this message in update-status hook - # TODO future PR, check for shard drainage when removing application + + if self.charm.unit.is_leader(): + self.charm.app_peer_data["added_to_cluster"] = json.dumps(False) def wait_for_draining(self, mongos_hosts: List[str]): """Waits for shards to be drained from sharded cluster.""" @@ -495,10 +498,12 @@ def get_shard_status(self): if not self.model.get_relation(self.relation_name) and self.charm.drained: return ActiveStatus("Shard drained from cluster, ready for removal") - # todo this check will fail if trying to restart if not self._is_mongos_reachable(): return BlockedStatus("Config server unreachable") + if not self._is_added_to_cluster(): + self.charm.unit.status = MaintenanceStatus("Adding shard to config-server") + if not self._is_shard_aware(): return BlockedStatus("Shard is not yet shard aware") @@ -626,6 +631,10 @@ def _is_mongos_reachable(self) -> bool: with MongosConnection(config, uri) as mongo: return mongo.is_ready + def _is_added_to_cluster(self) -> bool: + """Returns True if the shard has been added to the cluster.""" + return json.loads(self.charm.app_peer_data.get("added_to_cluster", "False")) + def _is_shard_aware(self) -> bool: """Returns True if shard is in cluster and shard aware.""" if not self.model.get_relation(self.relation_name): From 706dc404ab8f218cc62c35545d283336326f2ef0 Mon Sep 17 00:00:00 2001 From: Mia Altieri Date: Thu, 19 Oct 2023 09:32:46 +0000 Subject: [PATCH 4/7] status reporting for relations --- lib/charms/mongodb/v1/mongodb_provider.py | 43 ++++++++++++++----- .../{v0 => v1}/mongodb_vm_legacy_provider.py | 16 ++++++- lib/charms/mongodb/v1/shards_interface.py | 1 + src/charm.py | 2 +- 4 files changed, 48 insertions(+), 14 deletions(-) rename lib/charms/mongodb/{v0 => v1}/mongodb_vm_legacy_provider.py (91%) diff --git a/lib/charms/mongodb/v1/mongodb_provider.py b/lib/charms/mongodb/v1/mongodb_provider.py index d0484a479..144232a44 100644 --- a/lib/charms/mongodb/v1/mongodb_provider.py +++ b/lib/charms/mongodb/v1/mongodb_provider.py @@ -21,6 +21,8 @@ from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus, Relation from pymongo.errors import PyMongoError +from config import Config + # The unique Charmhub library identifier, never change it LIBID = "4067879ef7dd4261bf6c164bc29d94b1" @@ -82,6 +84,34 @@ def __init__(self, charm: CharmBase, substrate="k8s", relation_name: str = "data self.database_provides.on.database_requested, self._on_relation_event ) + def pass_hook_checks(self) -> bool: + """Runs the pre-hooks checks for MongoDBProvider, returns True if all pass.""" + if self.charm.is_role(Config.Role.SHARD) or self.charm.is_role(Config.Role.CONFIG_SERVER): + self.charm.unit.status = BlockedStatus( + "Sharding roles do not support mongodb_client interface." + ) + logger.error( + "Charm is in sharding role: %s. Does not support mongodb_client interface.", + self.charm.role, + ) + return False + + # legacy relations have auth disabled, which new relations require + if self.model.get_relation(LEGACY_REL_NAME): + self.charm.unit.status = BlockedStatus("cannot have both legacy and new relations") + logger.error("Auth disabled due to existing connections to legacy relations") + return False + + if not self.charm.unit.is_leader(): + return False + + # We shouldn't try to create or update users if the database is not + # initialised. We will create users as part of initialisation. + if not self.charm.db_initialised: + return False + + return True + def _on_relation_event(self, event): """Handle relation joined events. @@ -90,17 +120,8 @@ def _on_relation_event(self, event): data. As a result, related charm gets credentials for accessing the MongoDB database. """ - if not self.charm.unit.is_leader(): - return - # We shouldn't try to create or update users if the database is not - # initialised. We will create users as part of initialisation. - if "db_initialised" not in self.charm.app_peer_data: - return - - # legacy relations have auth disabled, which new relations require - if self.model.get_relation(LEGACY_REL_NAME): - self.charm.unit.status = BlockedStatus("cannot have both legacy and new relations") - logger.error("Auth disabled due to existing connections to legacy relations") + if not self.pass_hook_checks(): + logger.info("Skipping %s: hook checks did not pass", type(event)) return # If auth is disabled but there are no legacy relation users, this means that legacy diff --git a/lib/charms/mongodb/v0/mongodb_vm_legacy_provider.py b/lib/charms/mongodb/v1/mongodb_vm_legacy_provider.py similarity index 91% rename from lib/charms/mongodb/v0/mongodb_vm_legacy_provider.py rename to lib/charms/mongodb/v1/mongodb_vm_legacy_provider.py index ef6db7e12..8859302a5 100644 --- a/lib/charms/mongodb/v0/mongodb_vm_legacy_provider.py +++ b/lib/charms/mongodb/v1/mongodb_vm_legacy_provider.py @@ -13,15 +13,17 @@ from ops.framework import Object from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus +from config import Config + # The unique Charmhub library identifier, never change it LIBID = "896a48bc89b84d30839335bb37170509" # Increment this major API version when introducing breaking changes -LIBAPI = 0 +LIBAPI = 1 # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 4 +LIBPATCH = 0 logger = logging.getLogger(__name__) REL_NAME = "database" @@ -64,6 +66,16 @@ def _on_legacy_relation_created(self, event): ) return + if self.charm.is_role(Config.Role.SHARD) or self.charm.is_role(Config.Role.CONFIG_SERVER): + self.charm.unit.status = BlockedStatus( + "Sharding roles do not support mongodb_client interface." + ) + logger.error( + "Charm is in sharding role: %s. Does not support mongodb_client interface.", + self.charm.role, + ) + return + # If auth is already disabled its likely it has a connection with another legacy relation # user. Shutting down and restarting mongod would lead to downtime for the other legacy # relation user and hence shouldn't be done. Not to mention there is no need to disable diff --git a/lib/charms/mongodb/v1/shards_interface.py b/lib/charms/mongodb/v1/shards_interface.py index a5f2f0832..1d261d359 100644 --- a/lib/charms/mongodb/v1/shards_interface.py +++ b/lib/charms/mongodb/v1/shards_interface.py @@ -451,6 +451,7 @@ def wait_for_draining(self, mongos_hosts: List[str]): # no need to continuously check and abuse resources while shard is draining time.sleep(10) drained = self.drained(mongos_hosts, self.charm.app.name) + self.charm.unit.status = MaintenanceStatus("Draining shard from cluster") draining_status = ( "Shard is still draining" if not drained else "Shard is fully drained." ) diff --git a/src/charm.py b/src/charm.py index c790dabd0..04f71dafa 100755 --- a/src/charm.py +++ b/src/charm.py @@ -20,7 +20,6 @@ ) from charms.mongodb.v0.mongodb_secrets import SecretCache, generate_secret_label from charms.mongodb.v0.mongodb_tls import MongoDBTLS -from charms.mongodb.v0.mongodb_vm_legacy_provider import MongoDBLegacyProvider from charms.mongodb.v1.helpers import ( KEY_FILE, TLS_EXT_CA_FILE, @@ -36,6 +35,7 @@ ) from charms.mongodb.v1.mongodb_backups import S3_RELATION, MongoDBBackups from charms.mongodb.v1.mongodb_provider import MongoDBProvider +from charms.mongodb.v1.mongodb_vm_legacy_provider import MongoDBLegacyProvider from charms.mongodb.v1.mongos import MongosConfiguration from charms.mongodb.v1.shards_interface import ConfigServerRequirer, ShardingProvider from charms.mongodb.v1.users import ( From fd56c7b3a0dd23ef391fef36502662bcc20f7b61 Mon Sep 17 00:00:00 2001 From: Mia Altieri Date: Thu, 19 Oct 2023 09:49:11 +0000 Subject: [PATCH 5/7] small fixes --- lib/charms/mongodb/v1/helpers.py | 2 +- lib/charms/mongodb/v1/mongodb_provider.py | 2 +- lib/charms/mongodb/v1/mongodb_vm_legacy_provider.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/charms/mongodb/v1/helpers.py b/lib/charms/mongodb/v1/helpers.py index a21cc951e..1511c1407 100644 --- a/lib/charms/mongodb/v1/helpers.py +++ b/lib/charms/mongodb/v1/helpers.py @@ -29,7 +29,7 @@ # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 0 +LIBPATCH = 1 # path to store mongodb ketFile KEY_FILE = "keyFile" diff --git a/lib/charms/mongodb/v1/mongodb_provider.py b/lib/charms/mongodb/v1/mongodb_provider.py index 144232a44..ccac329cd 100644 --- a/lib/charms/mongodb/v1/mongodb_provider.py +++ b/lib/charms/mongodb/v1/mongodb_provider.py @@ -31,7 +31,7 @@ # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 1 +LIBPATCH = 2 logger = logging.getLogger(__name__) REL_NAME = "database" diff --git a/lib/charms/mongodb/v1/mongodb_vm_legacy_provider.py b/lib/charms/mongodb/v1/mongodb_vm_legacy_provider.py index 8859302a5..6942ad70f 100644 --- a/lib/charms/mongodb/v1/mongodb_vm_legacy_provider.py +++ b/lib/charms/mongodb/v1/mongodb_vm_legacy_provider.py @@ -68,7 +68,7 @@ def _on_legacy_relation_created(self, event): if self.charm.is_role(Config.Role.SHARD) or self.charm.is_role(Config.Role.CONFIG_SERVER): self.charm.unit.status = BlockedStatus( - "Sharding roles do not support mongodb_client interface." + "Sharding roles do not support mongodb interface." ) logger.error( "Charm is in sharding role: %s. Does not support mongodb_client interface.", From 624afb0cdfb7c70e0ee80911add668c50986c333 Mon Sep 17 00:00:00 2001 From: Mia Altieri Date: Fri, 27 Oct 2023 07:20:33 +0000 Subject: [PATCH 6/7] apply PR suggestions rd 1 --- lib/charms/mongodb/v1/helpers.py | 31 +------ lib/charms/mongodb/v1/mongodb_provider.py | 12 +-- .../mongodb/v1/mongodb_vm_legacy_provider.py | 13 +-- lib/charms/mongodb/v1/shards_interface.py | 36 ++++--- src/charm.py | 93 +++++++++++++++---- src/config.py | 1 + 6 files changed, 102 insertions(+), 84 deletions(-) diff --git a/lib/charms/mongodb/v1/helpers.py b/lib/charms/mongodb/v1/helpers.py index 1511c1407..d34d5fc7f 100644 --- a/lib/charms/mongodb/v1/helpers.py +++ b/lib/charms/mongodb/v1/helpers.py @@ -29,7 +29,7 @@ # Increment this PATCH version before using `charmcraft publish-lib` or reset # to 0 if you are raising the major API version -LIBPATCH = 1 +LIBPATCH = 0 # path to store mongodb ketFile KEY_FILE = "keyFile" @@ -201,35 +201,6 @@ def generate_keyfile() -> str: return "".join([secrets.choice(choices) for _ in range(1024)]) -def prioritise_statuses(mongodb_status, pbm_status, shard_status, config_server_status): - """Returns the status with the highest priority of the given statuses. - - Note: it will never be the case that shard_status and config_server_status are both present - since the mongodb app can either be a shard or a config server, but not both. - """ - # failure in mongodb takes precedence over sharding and config server - if not isinstance(mongodb_status, ActiveStatus): - return mongodb_status - - if shard_status and not isinstance(shard_status, ActiveStatus): - return shard_status - - if config_server_status and not isinstance(config_server_status, ActiveStatus): - return config_server_status - - if pbm_status and not isinstance(pbm_status, ActiveStatus): - return pbm_status - - # if all statuses are active report sharding statuses over mongodb status - if isinstance(shard_status, ActiveStatus): - return shard_status - - if isinstance(config_server_status, ActiveStatus): - return config_server_status - - return mongodb_status - - def build_unit_status(mongodb_config: MongoDBConfiguration, unit_ip: str) -> StatusBase: """Generates the status of a unit based on its status reported by mongod.""" try: diff --git a/lib/charms/mongodb/v1/mongodb_provider.py b/lib/charms/mongodb/v1/mongodb_provider.py index ccac329cd..00e052b1d 100644 --- a/lib/charms/mongodb/v1/mongodb_provider.py +++ b/lib/charms/mongodb/v1/mongodb_provider.py @@ -21,8 +21,6 @@ from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus, Relation from pymongo.errors import PyMongoError -from config import Config - # The unique Charmhub library identifier, never change it LIBID = "4067879ef7dd4261bf6c164bc29d94b1" @@ -86,14 +84,8 @@ def __init__(self, charm: CharmBase, substrate="k8s", relation_name: str = "data def pass_hook_checks(self) -> bool: """Runs the pre-hooks checks for MongoDBProvider, returns True if all pass.""" - if self.charm.is_role(Config.Role.SHARD) or self.charm.is_role(Config.Role.CONFIG_SERVER): - self.charm.unit.status = BlockedStatus( - "Sharding roles do not support mongodb_client interface." - ) - logger.error( - "Charm is in sharding role: %s. Does not support mongodb_client interface.", - self.charm.role, - ) + if not self.charm.is_relation_feasible(self.relation_name): + logger.info("Skipping code for relations.") return False # legacy relations have auth disabled, which new relations require diff --git a/lib/charms/mongodb/v1/mongodb_vm_legacy_provider.py b/lib/charms/mongodb/v1/mongodb_vm_legacy_provider.py index 6942ad70f..6eeb38600 100644 --- a/lib/charms/mongodb/v1/mongodb_vm_legacy_provider.py +++ b/lib/charms/mongodb/v1/mongodb_vm_legacy_provider.py @@ -13,8 +13,6 @@ from ops.framework import Object from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus -from config import Config - # The unique Charmhub library identifier, never change it LIBID = "896a48bc89b84d30839335bb37170509" @@ -43,6 +41,7 @@ def __init__(self, charm): """Manager of MongoDB client relations.""" super().__init__(charm, "client-relations") self.charm = charm + self.relation_name = LEGACY_REL_NAME self.framework.observe( self.charm.on[LEGACY_REL_NAME].relation_created, self._on_legacy_relation_created ) @@ -66,14 +65,8 @@ def _on_legacy_relation_created(self, event): ) return - if self.charm.is_role(Config.Role.SHARD) or self.charm.is_role(Config.Role.CONFIG_SERVER): - self.charm.unit.status = BlockedStatus( - "Sharding roles do not support mongodb interface." - ) - logger.error( - "Charm is in sharding role: %s. Does not support mongodb_client interface.", - self.charm.role, - ) + if not self.charm.is_relation_feasible(self.relation_name): + logger.info("Skipping code for legacy relations.") return # If auth is already disabled its likely it has a connection with another legacy relation diff --git a/lib/charms/mongodb/v1/shards_interface.py b/lib/charms/mongodb/v1/shards_interface.py index 088487aa3..b2ec45b8b 100644 --- a/lib/charms/mongodb/v1/shards_interface.py +++ b/lib/charms/mongodb/v1/shards_interface.py @@ -29,7 +29,13 @@ from charms.mongodb.v1.users import MongoDBUser, OperatorUser from ops.charm import CharmBase, EventBase, RelationBrokenEvent from ops.framework import Object -from ops.model import ActiveStatus, BlockedStatus, MaintenanceStatus, WaitingStatus +from ops.model import ( + ActiveStatus, + BlockedStatus, + MaintenanceStatus, + StatusBase, + WaitingStatus, +) from tenacity import RetryError, Retrying, stop_after_delay, wait_fixed from config import Config @@ -107,12 +113,8 @@ def _on_relation_joined(self, event): def pass_hook_checks(self, event: EventBase) -> bool: """Runs the pre-hooks checks for ShardingProvider, returns True if all pass.""" - if self.charm.is_role(Config.Role.REPLICATION): - self.charm.unit.status = BlockedStatus("role replication does not support sharding") - logger.error( - "Skipping %s. Sharding interface not supported with config role=replication.", - type(event), - ) + if not self.charm.is_relation_feasible(self.relation_name): + logger.info("Skipping event %s , relation not feasible.", type(event)) return False if not self.charm.is_role(Config.Role.CONFIG_SERVER): @@ -388,9 +390,8 @@ def _on_relation_changed(self, event): def pass_hook_checks(self, event): """Runs the pre-hooks checks for ConfigServerRequirer, returns True if all pass.""" - if self.charm.is_role(Config.Role.REPLICATION): - self.charm.unit.status = BlockedStatus("role replication does not support sharding") - logger.error("sharding interface not supported with config role=replication") + if not self.charm.is_relation_feasible(self.relation_name): + logger.info("Skipping event %s , relation not feasible.", type(event)) return False if not self.charm.is_role(Config.Role.SHARD): @@ -468,7 +469,7 @@ def wait_for_draining(self, mongos_hosts: List[str]): break - def get_shard_status(self): + def get_shard_status(self) -> StatusBase: """Returns the current status of the shard. Note: No need to report if currently draining, since that check block other hooks from @@ -498,7 +499,7 @@ def get_shard_status(self): return BlockedStatus("Config server unreachable") if not self._is_added_to_cluster(): - self.charm.unit.status = MaintenanceStatus("Adding shard to config-server") + return MaintenanceStatus("Adding shard to config-server") if not self._is_shard_aware(): return BlockedStatus("Shard is not yet shard aware") @@ -618,6 +619,9 @@ def _is_mongos_reachable(self) -> bool: return False mongos_hosts = self.get_mongos_hosts() + if not mongos_hosts: + return False + self.charm.remote_mongos_config(set(mongos_hosts)) config = self.charm.remote_mongos_config(set(mongos_hosts)) @@ -649,13 +653,17 @@ def has_config_server(self) -> bool: def get_related_config_server(self) -> str: """Returns the related config server.""" - config_server = [rel.app.name for rel in self.charm.model.relations[self.relation_name]] + if self.relation_name not in self.charm.model.relations: + return None # metadata.yaml prevents having multiple config servers - return config_server[0] + return self.charm.model.relations[self.relation_name][0].app.name def get_mongos_hosts(self) -> List[str]: """Returns a list of IP addresses for the mongos hosts.""" # only one related config-server is possible config_server_relation = self.charm.model.relations[self.relation_name][0] + if HOSTS_KEY not in config_server_relation.data[config_server_relation.app]: + return + return json.loads(config_server_relation.data[config_server_relation.app].get(HOSTS_KEY)) diff --git a/src/charm.py b/src/charm.py index 04f71dafa..ff325cb48 100755 --- a/src/charm.py +++ b/src/charm.py @@ -31,7 +31,6 @@ generate_keyfile, generate_password, get_create_user_cmd, - prioritise_statuses, ) from charms.mongodb.v1.mongodb_backups import S3_RELATION, MongoDBBackups from charms.mongodb.v1.mongodb_provider import MongoDBProvider @@ -67,6 +66,7 @@ BlockedStatus, MaintenanceStatus, Relation, + StatusBase, Unit, WaitingStatus, ) @@ -575,25 +575,7 @@ def _on_update_status(self, event: UpdateStatusEvent): if self.unit.is_leader(): self._handle_reconfigure(event) - # retrieve statuses of different services running on Charmed MongoDB - mongodb_status = build_unit_status(self.mongodb_config, self._unit_ip(self.unit)) - shard_status = self.shard.get_shard_status() if self.is_role(Config.Role.SHARD) else None - config_server_status = ( - self.config_server.get_config_server_status() - if self.is_role(Config.Role.CONFIG_SERVER) - else None - ) - pbm_status = ( - self.backups.get_pbm_status() if self.model.get_relation(S3_RELATION) else None - ) - - # prioritize the statuses of the different services running on Charmed MongoDB - self.unit.status = prioritise_statuses( - mongodb_status, - pbm_status, - shard_status, - config_server_status, - ) + self.unit.status = self.get_status() def _on_get_primary_action(self, event: ActionEvent): event.set_results({"replica-set-primary": self._primary}) @@ -1367,6 +1349,77 @@ def _is_removing_last_replica(self) -> bool: """Returns True if the last replica (juju unit) is getting removed.""" return self.app.planned_units() == 0 and len(self._peers.units) == 0 + def get_status(self) -> StatusBase: + """Returns the status with the highest priority from backups, sharding, and mongod. + + Note: it will never be the case that shard_status and config_server_status are both present + since the mongodb app can either be a shard or a config server, but not both. + """ + # retrieve statuses of different services running on Charmed MongoDB + mongodb_status = build_unit_status(self.mongodb_config, self._unit_ip(self.unit)) + shard_status = self.shard.get_shard_status() if self.is_role(Config.Role.SHARD) else None + config_server_status = ( + self.config_server.get_config_server_status() + if self.is_role(Config.Role.CONFIG_SERVER) + else None + ) + pbm_status = ( + self.backups.get_pbm_status() if self.model.get_relation(S3_RELATION) else None + ) + + # failure in mongodb takes precedence over sharding and config server + if not isinstance(mongodb_status, ActiveStatus): + return mongodb_status + + if shard_status and not isinstance(shard_status, ActiveStatus): + return shard_status + + if config_server_status and not isinstance(config_server_status, ActiveStatus): + return config_server_status + + if pbm_status and not isinstance(pbm_status, ActiveStatus): + return pbm_status + + # if all statuses are active report sharding statuses over mongodb status + if isinstance(shard_status, ActiveStatus): + return shard_status + + if isinstance(config_server_status, ActiveStatus): + return config_server_status + + return mongodb_status + + def is_relation_feasible(self, rel_interface) -> bool: + """Returns true if the proposed relation is feasible.""" + if self.is_sharding_component() and rel_interface in Config.Relations.DB_RELATIONS: + self.unit.status = BlockedStatus( + f"Sharding roles do not support {rel_interface} interface." + ) + logger.error( + "Charm is in sharding role: %s. Does not support %s interface.", + rel_interface, + self.role, + ) + return False + + if ( + not self.is_sharding_component() + and rel_interface != Config.Relations.SHARDING_RELATIONS_NAME + ): + self.unit.status = BlockedStatus("role replication does not support sharding") + logger.error( + "Charm is in sharding role: %s. Does not support %s interface.", + self.role, + rel_interface, + ) + return False + + return True + + def is_sharding_component(self) -> bool: + """Returns true if charm is running as a sharded component.""" + return self.is_role(Config.Role.SHARD) or self.is_role(Config.Role.CONFIG_SERVER) + # END: helper functions diff --git a/src/config.py b/src/config.py index 073065bee..a00d8b4d4 100644 --- a/src/config.py +++ b/src/config.py @@ -76,6 +76,7 @@ class Relations: CONFIG_SERVER_RELATIONS_NAME = "config-server" APP_SCOPE = "app" UNIT_SCOPE = "unit" + DB_RELATIONS = [OBSOLETE_RELATIONS_NAME, NAME] Scopes = Literal[APP_SCOPE, UNIT_SCOPE] class Secrets: From 3eb8b7265c4fa687a6fc04ea78aef29c622d666a Mon Sep 17 00:00:00 2001 From: Mia Altieri Date: Fri, 27 Oct 2023 11:42:54 +0000 Subject: [PATCH 7/7] small fix --- lib/charms/mongodb/v1/shards_interface.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/charms/mongodb/v1/shards_interface.py b/lib/charms/mongodb/v1/shards_interface.py index 4de1fe68f..6a053d30a 100644 --- a/lib/charms/mongodb/v1/shards_interface.py +++ b/lib/charms/mongodb/v1/shards_interface.py @@ -357,7 +357,9 @@ def _on_relation_changed(self, event): return # if re-using an old shard, re-set drained flag. - self.charm.app_peer_data["drained"] = json.dumps(False) + if self.charm.unit.is_leader(): + self.charm.app_peer_data["drained"] = json.dumps(False) + self.charm.unit.status = MaintenanceStatus("Adding shard to config-server") # shards rely on the config server for secrets