Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions actions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ get-password:
username:
type: string
description: The username, the default value 'operator'.
Possible values - operator, backup.
Possible values - operator, backup, monitor.

set-password:
description: Change the admin user's password, which is used by charm.
Expand All @@ -20,7 +20,7 @@ set-password:
username:
type: string
description: The username, the default value 'operator'.
Possible values - operator, backup.
Possible values - operator, backup, monitor.
password:
type: string
description: The password will be auto-generated if this option is not specified.
Expand Down
22 changes: 11 additions & 11 deletions documentation/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,16 +131,16 @@ unit-mongodb-0:
UnitId: mongodb/0
id: "2"
results:
admin-password: <password>
password: <password>
status: completed
timing:
completed: 2022-12-02 11:30:01 +0000 UTC
enqueued: 2022-12-02 11:29:57 +0000 UTC
started: 2022-12-02 11:30:01 +0000 UTC
```
Use the password under the result: `admin-password`:
Use the password under the result: `password`:
```shell
export DB_PASSWORD=$(juju run-action mongodb/leader get-password --wait | grep admin-password| awk '{print $2}')
export DB_PASSWORD=$(juju run-action mongodb/leader get-password --wait | grep password| awk '{print $2}')
```

**Retrieving the hosts:** The hosts are the units hosting the MongoDB application. The host’s IP address can be found with `juju status`:
Expand Down Expand Up @@ -490,14 +490,14 @@ unit-mongodb-0:
UnitId: mongodb/0
id: "2"
results:
admin-password: <password>
password: <password>
status: completed
timing:
completed: 2022-12-02 11:30:01 +0000 UTC
enqueued: 2022-12-02 11:29:57 +0000 UTC
started: 2022-12-02 11:30:01 +0000 UTC
```
The admin password is under the result: `admin-password`.
The admin password is under the result: `password`.


### Rotate the admin password
Expand All @@ -511,18 +511,18 @@ unit-mongodb-0:
UnitId: mongodb/0
id: "4"
results:
admin-password: <new password>
password: <new password>
status: completed
timing:
completed: 2022-12-02 14:53:30 +0000 UTC
enqueued: 2022-12-02 14:53:25 +0000 UTC
started: 2022-12-02 14:53:28 +0000 UTC
```
The admin password is under the result: `admin-password`. It should be different from your previous password.
The admin password is under the result: `password`. It should be different from your previous password.

*Note when you change the admin password you will also need to update the admin password the in MongoDB URI; as the old password will no longer be valid.* Update the DB password used in the URI and update the URI:
```shell
export DB_PASSWORD=$(juju run-action mongodb/leader get-password --wait | grep admin-password| awk '{print $2}')
export DB_PASSWORD=$(juju run-action mongodb/leader get-password --wait | grep password| awk '{print $2}')
export URI=mongodb://$DB_USERNAME:$DB_PASSWORD@$HOST_IP/$DB_NAME?replicaSet=$REPL_SET_NAME
```

Expand All @@ -537,18 +537,18 @@ unit-mongodb-0:
UnitId: mongodb/0
id: "4"
results:
admin-password: <password>
password: <password>
status: completed
timing:
completed: 2022-12-02 14:53:30 +0000 UTC
enqueued: 2022-12-02 14:53:25 +0000 UTC
started: 2022-12-02 14:53:28 +0000 UTC
```
The admin password under the result: `admin-password` should match whatever you passed in when you entered the command.
The admin password under the result: `password` should match whatever you passed in when you entered the command.

*Note that when you change the admin password you will also need to update the admin password in the MongoDB URI, as the old password will no longer be valid.* To update the DB password used in the URI:
```shell
export DB_PASSWORD=$(juju run-action mongodb/leader get-password --wait | grep admin-password| awk '{print $2}')
export DB_PASSWORD=$(juju run-action mongodb/leader get-password --wait | grep password| awk '{print $2}')
export URI=mongodb://$DB_USERNAME:$DB_PASSWORD@$HOST_IP/$DB_NAME?replicaSet=$REPL_SET_NAME
```

Expand Down
7 changes: 6 additions & 1 deletion lib/charms/mongodb/v0/mongodb.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
logger = logging.getLogger(__name__)

# List of system usernames needed for correct work on the charm.
CHARM_USERS = ["operator", "backup"]
CHARM_USERS = ["operator", "backup", "monitor"]


@dataclass
Expand Down Expand Up @@ -331,6 +331,11 @@ def _get_roles(config: MongoDBConfiguration) -> List[dict]:
{"role": "readWriteAnyDatabase", "db": "admin"},
{"role": "userAdmin", "db": "admin"},
],
"monitor": [
{"role": "explainRole", "db": "admin"},
{"role": "clusterMonitor", "db": "admin"},
{"role": "read", "db": "local"},
],
"backup": [
{"db": "admin", "role": "readWrite", "collection": ""},
{"db": "admin", "role": "backup"},
Expand Down
6 changes: 3 additions & 3 deletions lib/charms/mongodb/v0/mongodb_backups.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,14 +429,14 @@ def _format_backup_list(self, backup_list) -> str:
@property
def _backup_config(self) -> MongoDBConfiguration:
"""Construct the config object for backup user and creates user if necessary."""
if not self.charm.get_secret("app", "backup_password"):
self.charm.set_secret("app", "backup_password", generate_password())
if not self.charm.get_secret("app", "backup-password"):
self.charm.set_secret("app", "backup-password", generate_password())

return MongoDBConfiguration(
replset=self.charm.app.name,
database="",
username="backup",
password=self.charm.get_secret("app", "backup_password"),
password=self.charm.get_secret("app", "backup-password"),
hosts=[
self.charm._unit_ip(self.charm.unit)
], # pbm cannot make a direct connection if multiple hosts are used
Expand Down
44 changes: 42 additions & 2 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@
MONGO_USER = "mongodb"
MONGO_DATA_DIR = "/data/db"
PBM_PRIVILEGES = {"resource": {"anyResource": True}, "actions": ["anyAction"]}
MONITOR_PRIVILEGES = {
"resource": {"db": "", "collection": ""},
"actions": ["listIndexes", "listCollections", "dbStats", "dbHash", "collStats", "find"],
}

# We expect the MongoDB container to use the default ports
MONGODB_PORT = 27017
Expand Down Expand Up @@ -441,7 +445,7 @@ def _on_get_password(self, event: ops.charm.ActionEvent) -> None:
)
return

event.set_results({f"{username}-password": self.get_secret("app", f"{username}-password")})
event.set_results({"password": self.get_secret("app", f"{username}-password")})

def _on_set_password(self, event: ops.charm.ActionEvent) -> None:
"""Set the password for the admin user."""
Expand Down Expand Up @@ -475,7 +479,7 @@ def _on_set_password(self, event: ops.charm.ActionEvent) -> None:
return

self.set_secret("app", f"{username}-password", new_password)
event.set_results({f"{username}-password": new_password})
event.set_results({"password": new_password})

def _open_port_tcp(self, port: int) -> None:
"""Open the given port.
Expand Down Expand Up @@ -624,6 +628,7 @@ def _initialise_replica_set(self, event: ops.charm.StartEvent) -> None:
logger.info("User initialization")
self._init_admin_user()
self._init_backup_user()
self._init_monitor_user()
logger.info("Manage relations")
self.client_relations.oversee_users(None, None)
except subprocess.CalledProcessError as e:
Expand Down Expand Up @@ -746,6 +751,23 @@ def mongodb_config(self) -> MongoDBConfiguration:
tls_internal=internal_ca is not None,
)

@property
def monitor_config(self) -> MongoDBConfiguration:
"""Generates a MongoDBConfiguration object for this deployment of MongoDB."""
if not self.get_secret("app", "monitor-password"):
self.set_secret("app", "monitor-password", generate_password())

return MongoDBConfiguration(
replset=self.app.name,
database="",
username="monitor",
password=self.get_secret("app", "monitor-password"),
hosts=set(self._unit_ips),
roles={"monitor"},
tls_external=self.tls.get_tls_files("unit") is not None,
tls_internal=self.tls.get_tls_files("app") is not None,
)

@property
def unit_peer_data(self) -> Dict:
"""Peer relation data object."""
Expand Down Expand Up @@ -803,6 +825,24 @@ def _init_admin_user(self) -> None:
logger.debug("User created")
self.app_peer_data["user_created"] = "True"

@retry(
stop=stop_after_attempt(3),
wait=wait_fixed(5),
reraise=True,
before=before_log(logger, logging.DEBUG),
)
def _init_monitor_user(self):
"""Creates the monitor user on the MongoDB database."""
if "monitor_user_created" in self.app_peer_data:
return

with MongoDBConnection(self.mongodb_config) as mongo:
logger.debug("creating the monitor user roles...")
mongo.create_role(role_name="explainRole", privileges=MONITOR_PRIVILEGES)
logger.debug("creating the monitor user...")
mongo.create_user(self.monitor_config)
self.app_peer_data["monitor_user_created"] = "True"

@retry(
stop=stop_after_attempt(3),
wait=wait_fixed(5),
Expand Down
2 changes: 1 addition & 1 deletion tests/integration/ha_tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ async def get_password(ops_test: OpsTest, app, down_unit=None) -> str:

action = await ops_test.model.units.get(f"{app}/{unit_id}").run_action("get-password")
action = await action.wait()
return action.results["operator-password"]
return action.results["password"]


async def fetch_primary(
Expand Down
8 changes: 5 additions & 3 deletions tests/integration/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def unit_uri(ip_address: str, password, app=APP_NAME) -> str:
return f"mongodb://operator:" f"{password}@" f"{ip_address}:{PORT}/admin?replicaSet={app}"


async def get_password(ops_test: OpsTest, app=APP_NAME) -> str:
async def get_password(ops_test: OpsTest, app=APP_NAME, username="operator") -> str:
"""Use the charm action to retrieve the password from provided unit.

Returns:
Expand All @@ -36,9 +36,11 @@ async def get_password(ops_test: OpsTest, app=APP_NAME) -> str:
unit_name = ops_test.model.applications[app].units[0].name
unit_id = unit_name.split("/")[1]

action = await ops_test.model.units.get(f"{app}/{unit_id}").run_action("get-password")
action = await ops_test.model.units.get(f"{app}/{unit_id}").run_action(
"get-password", **{"username": username}
)
action = await action.wait()
return action.results["operator-password"]
return action.results["password"]


@retry(
Expand Down
20 changes: 18 additions & 2 deletions tests/integration/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ async def test_set_password_action(ops_test: OpsTest) -> None:
unit = await find_unit(ops_test, leader=True)
action = await unit.run_action("set-password")
action = await action.wait()
new_password = action.results["operator-password"]
new_password = action.results["password"]
assert new_password != old_password
new_password_reported = await get_password(ops_test)
assert new_password == new_password_reported
Expand All @@ -152,7 +152,7 @@ async def test_set_password_action(ops_test: OpsTest) -> None:
old_password = await get_password(ops_test)
action = await unit.run_action("set-password", **{"password": "safe_pass"})
action = await action.wait()
new_password = action.results["operator-password"]
new_password = action.results["password"]
assert new_password != old_password
new_password_reported = await get_password(ops_test)
assert "safe_pass" == new_password_reported
Expand All @@ -167,6 +167,22 @@ async def test_set_password_action(ops_test: OpsTest) -> None:
client.close()


async def test_monitor_user(ops_test: OpsTest) -> None:
"""Test verifies that the monitor user can perform operations such as 'rs.conf()'."""
unit = ops_test.model.applications[APP_NAME].units[0]
password = await get_password(ops_test, "mongodb", "monitor")
replica_set_hosts = [
unit.public_address for unit in ops_test.model.applications["mongodb"].units
]
hosts = ",".join(replica_set_hosts)
replica_set_uri = f"mongodb://monitor:{password}@{hosts}/admin?replicaSet=mongodb"

admin_mongod_cmd = f"mongo '{replica_set_uri}' --eval 'rs.conf()'"
check_monitor_cmd = f"run --unit {unit.name} -- {admin_mongod_cmd}"
return_code, _, _ = await ops_test.juju(*check_monitor_cmd.split())
assert return_code == 0, "command rs.conf() on monitor user does not work"


async def test_exactly_one_primary_reported_by_juju(ops_test: OpsTest) -> None:
"""Tests that there is exactly one replica set primary unit reported by juju."""

Expand Down
4 changes: 2 additions & 2 deletions tests/unit/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -756,7 +756,7 @@ def test_set_password(self, connection):

# verify app data is updated and results are reported to user
self.assertNotEqual(original_password, new_password)
action_event.set_results.assert_called_with({"operator-password": new_password})
action_event.set_results.assert_called_with({"password": new_password})

@patch_network_get(private_address="1.1.1.1")
@patch("charm.MongoDBConnection")
Expand All @@ -770,7 +770,7 @@ def test_set_password_provided(self, connection):

# verify app data is updated and results are reported to user
self.assertEqual("canonical123", new_password)
action_event.set_results.assert_called_with({"operator-password": "canonical123"})
action_event.set_results.assert_called_with({"password": "canonical123"})

@patch_network_get(private_address="1.1.1.1")
@patch("charm.MongoDBConnection")
Expand Down