diff --git a/lib/charms/mongodb/v0/helpers.py b/lib/charms/mongodb/v0/helpers.py deleted file mode 100644 index d34d5fc7f..000000000 --- a/lib/charms/mongodb/v0/helpers.py +++ /dev/null @@ -1,269 +0,0 @@ -"""Simple functions, which can be used in both K8s and VM charms.""" -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. -import json -import logging -import os -import secrets -import string -import subprocess -from typing import List - -from charms.mongodb.v0.mongodb import MongoDBConfiguration, MongoDBConnection -from ops.model import ( - ActiveStatus, - BlockedStatus, - MaintenanceStatus, - StatusBase, - WaitingStatus, -) -from pymongo.errors import AutoReconnect, ServerSelectionTimeoutError - -from config import Config - -# The unique Charmhub library identifier, never change it -LIBID = "b9a7fe0c38d8486a9d1ce94c27d4758e" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 0 - -# path to store mongodb ketFile -KEY_FILE = "keyFile" -TLS_EXT_PEM_FILE = "external-cert.pem" -TLS_EXT_CA_FILE = "external-ca.crt" -TLS_INT_PEM_FILE = "internal-cert.pem" -TLS_INT_CA_FILE = "internal-ca.crt" - -MONGODB_COMMON_DIR = "/var/snap/charmed-mongodb/common" -MONGODB_SNAP_DATA_DIR = "/var/snap/charmed-mongodb/current" - -MONGO_SHELL = "charmed-mongodb.mongosh" - -DATA_DIR = "/var/lib/mongodb" -CONF_DIR = "/etc/mongod" -MONGODB_LOG_FILENAME = "mongodb.log" -logger = logging.getLogger(__name__) - - -# noinspection GrazieInspection -def get_create_user_cmd(config: MongoDBConfiguration, mongo_path=MONGO_SHELL) -> List[str]: - """Creates initial admin user for MongoDB. - - Initial admin user can be created only through localhost connection. - see https://www.mongodb.com/docs/manual/core/localhost-exception/ - unfortunately, pymongo not able to create connection which considered - as local connection by MongoDB, even if socket connection used. - As result where are only hackish ways to create initial user. - It is needed to install mongodb-clients inside charm container to make - this function work correctly - """ - return [ - mongo_path, - "mongodb://localhost/admin", - "--quiet", - "--eval", - "db.createUser({" - f" user: '{config.username}'," - " pwd: passwordPrompt()," - " roles:[" - " {'role': 'userAdminAnyDatabase', 'db': 'admin'}, " - " {'role': 'readWriteAnyDatabase', 'db': 'admin'}, " - " {'role': 'clusterAdmin', 'db': 'admin'}, " - " ]," - " mechanisms: ['SCRAM-SHA-256']," - " passwordDigestor: 'server'," - "})", - ] - - -def get_mongos_args( - config: MongoDBConfiguration, - snap_install: bool = False, -) -> str: - """Returns the arguments used for starting mongos on a config-server side application. - - Returns: - A string representing the arguments to be passed to mongos. - """ - # mongos running on the config server communicates through localhost - # use constant for port - config_server_uri = f"{config.replset}/localhost:27017" - - full_conf_dir = f"{MONGODB_SNAP_DATA_DIR}{CONF_DIR}" if snap_install else CONF_DIR - cmd = [ - # mongos on config server side should run on 0.0.0.0 so it can be accessed by other units - # in the sharded cluster - "--bind_ip_all", - f"--configdb {config_server_uri}", - # config server is already using 27017 - f"--port {Config.MONGOS_PORT}", - f"--keyFile={full_conf_dir}/{KEY_FILE}", - "\n", - ] - - # TODO Future PR: support TLS on mongos - - return " ".join(cmd) - - -def get_mongod_args( - config: MongoDBConfiguration, - auth: bool = True, - snap_install: bool = False, - role: str = "replication", -) -> str: - """Construct the MongoDB startup command line. - - Returns: - A string representing the command used to start MongoDB. - """ - full_data_dir = f"{MONGODB_COMMON_DIR}{DATA_DIR}" if snap_install else DATA_DIR - full_conf_dir = f"{MONGODB_SNAP_DATA_DIR}{CONF_DIR}" if snap_install else CONF_DIR - # in k8s the default logging options that are used for the vm charm are ignored and logs are - # the output of the container. To enable logging to a file it must be set explicitly - logging_options = "" if snap_install else f"--logpath={full_data_dir}/{MONGODB_LOG_FILENAME}" - cmd = [ - # bind to localhost and external interfaces - "--bind_ip_all", - # part of replicaset - f"--replSet={config.replset}", - # db must be located within the snap common directory since the snap is strictly confined - f"--dbpath={full_data_dir}", - # for simplicity we run the mongod daemon on shards, configsvrs, and replicas on the same - # port - f"--port={Config.MONGODB_PORT}", - logging_options, - ] - if auth: - cmd.extend(["--auth"]) - - if auth and not config.tls_internal: - # keyFile cannot be used without auth and cannot be used in tandem with internal TLS - cmd.extend( - [ - "--clusterAuthMode=keyFile", - f"--keyFile={full_conf_dir}/{KEY_FILE}", - ] - ) - - if config.tls_external: - cmd.extend( - [ - f"--tlsCAFile={full_conf_dir}/{TLS_EXT_CA_FILE}", - f"--tlsCertificateKeyFile={full_conf_dir}/{TLS_EXT_PEM_FILE}", - # allow non-TLS connections - "--tlsMode=preferTLS", - ] - ) - - # internal TLS can be enabled only in external is enabled - if config.tls_internal and config.tls_external: - cmd.extend( - [ - "--clusterAuthMode=x509", - "--tlsAllowInvalidCertificates", - f"--tlsClusterCAFile={full_conf_dir}/{TLS_INT_CA_FILE}", - f"--tlsClusterFile={full_conf_dir}/{TLS_INT_PEM_FILE}", - ] - ) - - if role == Config.Role.CONFIG_SERVER: - cmd.append("--configsvr") - - if role == Config.Role.SHARD: - cmd.append("--shardsvr") - - cmd.append("\n") - return " ".join(cmd) - - -def generate_password() -> str: - """Generate a random password string. - - Returns: - A random password string. - """ - choices = string.ascii_letters + string.digits - return "".join([secrets.choice(choices) for _ in range(32)]) - - -def generate_keyfile() -> str: - """Key file used for authentication between replica set peers. - - Returns: - A maximum allowed random string. - """ - choices = string.ascii_letters + string.digits - return "".join([secrets.choice(choices) for _ in range(1024)]) - - -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: - with MongoDBConnection(mongodb_config) as mongo: - replset_status = mongo.get_replset_status() - - if unit_ip not in replset_status: - return WaitingStatus("Member being added..") - - replica_status = replset_status[unit_ip] - - if replica_status == "PRIMARY": - return ActiveStatus("Primary") - elif replica_status == "SECONDARY": - return ActiveStatus("") - elif replica_status in ["STARTUP", "STARTUP2", "ROLLBACK", "RECOVERING"]: - return WaitingStatus("Member is syncing...") - elif replica_status == "REMOVED": - return WaitingStatus("Member is removing...") - else: - return BlockedStatus(replica_status) - except ServerSelectionTimeoutError as e: - # ServerSelectionTimeoutError is commonly due to ReplicaSetNoPrimary - logger.debug("Got error: %s, while checking replica set status", str(e)) - return WaitingStatus("Waiting for primary re-election..") - except AutoReconnect as e: - # AutoReconnect is raised when a connection to the database is lost and an attempt to - # auto-reconnect will be made by pymongo. - logger.debug("Got error: %s, while checking replica set status", str(e)) - return WaitingStatus("Waiting to reconnect to unit..") - - -def copy_licenses_to_unit(): - """Copies licenses packaged in the snap to the charm's licenses directory.""" - os.makedirs("src/licenses", exist_ok=True) - subprocess.check_output("cp LICENSE src/licenses/LICENSE-charm", shell=True) - subprocess.check_output( - "cp -r /snap/charmed-mongodb/current/licenses/* src/licenses", shell=True - ) - - -def current_pbm_op(pbm_status: str) -> str: - """Parses pbm status for the operation that pbm is running.""" - pbm_status = json.loads(pbm_status) - return pbm_status["running"] if "running" in pbm_status else "" - - -def process_pbm_status(pbm_status: str) -> StatusBase: - """Parses current pbm operation and returns unit status.""" - current_op = current_pbm_op(pbm_status) - # no operations are currently running with pbm - if current_op == {}: - return ActiveStatus("") - - if current_op["type"] == "backup": - backup_id = current_op["name"] - return MaintenanceStatus(f"backup started/running, backup id:'{backup_id}'") - - if current_op["type"] == "restore": - backup_id = current_op["name"] - return MaintenanceStatus(f"restore started/running, backup id:'{backup_id}'") - - if current_op["type"] == "resync": - return WaitingStatus("waiting to sync s3 configurations.") - - return ActiveStatus() diff --git a/lib/charms/mongodb/v0/users.py b/lib/charms/mongodb/v0/users.py deleted file mode 100644 index 57883d730..000000000 --- a/lib/charms/mongodb/v0/users.py +++ /dev/null @@ -1,117 +0,0 @@ -"""Users configuration for MongoDB.""" -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. -from typing import Set - -# The unique Charmhub library identifier, never change it -LIBID = "b74007eda21c453a89e4dcc6382aa2b3" - -# Increment this major API version when introducing breaking changes -LIBAPI = 1 - -# Increment this PATCH version before using `charmcraft publish-lib` or reset -# to 0 if you are raising the major API version -LIBPATCH = 0 - - -class MongoDBUser: - """Base class for MongoDB users.""" - - _username = "" - _password_key_name = "" - _database_name = "" - _roles = [] - _privileges = {} - _mongodb_role = "" - _hosts = [] - - def get_username(self) -> str: - """Returns the username of the user.""" - return self._username - - def get_password_key_name(self) -> str: - """Returns the key name for the password of the user.""" - return self._password_key_name - - def get_database_name(self) -> str: - """Returns the database of the user.""" - return self._database_name - - def get_roles(self) -> Set[str]: - """Returns the role of the user.""" - return self._roles - - def get_mongodb_role(self) -> str: - """Returns the MongoDB role of the user.""" - return self._mongodb_role - - def get_privileges(self) -> dict: - """Returns the privileges of the user.""" - return self._privileges - - def get_hosts(self) -> list: - """Returns the hosts of the user.""" - return self._hosts - - @staticmethod - def get_password_key_name_for_user(username: str) -> str: - """Returns the key name for the password of the user.""" - if username == OperatorUser.get_username(): - return OperatorUser.get_password_key_name() - elif username == MonitorUser.get_username(): - return MonitorUser.get_password_key_name() - elif username == BackupUser.get_username(): - return BackupUser.get_password_key_name() - else: - raise ValueError(f"Unknown user: {username}") - - -class _OperatorUser(MongoDBUser): - """Operator user for MongoDB.""" - - _username = "operator" - _password_key_name = f"{_username}-password" - _database_name = "admin" - _roles = ["default"] - _hosts = [] - - -class _MonitorUser(MongoDBUser): - """Monitor user for MongoDB.""" - - _username = "monitor" - _password_key_name = f"{_username}-password" - _database_name = "admin" - _roles = ["monitor"] - _privileges = { - "resource": {"db": "", "collection": ""}, - "actions": ["listIndexes", "listCollections", "dbStats", "dbHash", "collStats", "find"], - } - _mongodb_role = "explainRole" - _hosts = [ - "127.0.0.1" - ] # MongoDB Exporter can only connect to one replica - not the entire set. - - -class _BackupUser(MongoDBUser): - """Backup user for MongoDB.""" - - _username = "backup" - _password_key_name = f"{_username}-password" - _database_name = "" - _roles = ["backup"] - _mongodb_role = "pbmAnyAction" - _privileges = {"resource": {"anyResource": True}, "actions": ["anyAction"]} - _hosts = ["127.0.0.1"] # pbm cannot make a direct connection if multiple hosts are used - - -OperatorUser = _OperatorUser() -MonitorUser = _MonitorUser() -BackupUser = _BackupUser() - -# List of system usernames needed for correct work on the charm. -CHARM_USERS = [ - OperatorUser.get_username(), - BackupUser.get_username(), - MonitorUser.get_username(), -]