Skip to content

Commit 551c3f8

Browse files
[DRAFT]
1 parent 1566086 commit 551c3f8

File tree

17 files changed

+918
-289
lines changed

17 files changed

+918
-289
lines changed

lib/charms/mysql/v0/mysql.py

Lines changed: 199 additions & 88 deletions
Large diffs are not rendered by default.

src/charm.py

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
MySQLAddInstanceToClusterError,
3333
MySQLCharmBase,
3434
MySQLConfigureInstanceError,
35+
MySQLConfigureMySQLRolesError,
3536
MySQLConfigureMySQLUsersError,
3637
MySQLCreateClusterError,
3738
MySQLCreateClusterSetError,
@@ -326,6 +327,9 @@ def _on_start(self, event: StartEvent) -> None:
326327

327328
try:
328329
self.workload_initialise()
330+
except MySQLConfigureMySQLRolesError:
331+
self.unit.status = BlockedStatus("Failed to initialize MySQL roles")
332+
return
329333
except MySQLConfigureMySQLUsersError:
330334
self.unit.status = BlockedStatus("Failed to initialize MySQL users")
331335
return
@@ -350,19 +354,7 @@ def _on_start(self, event: StartEvent) -> None:
350354
self.unit_peer_data["member-state"] = "waiting"
351355
return
352356

353-
try:
354-
# Create the cluster and cluster set from the leader unit
355-
logger.info(f"Creating cluster {self.app_peer_data['cluster-name']}")
356-
self.create_cluster()
357-
self._open_ports()
358-
self.unit.status = ActiveStatus(self.active_status_message)
359-
except (
360-
MySQLCreateClusterError,
361-
MySQLCreateClusterSetError,
362-
MySQLInitializeJujuOperationsTableError,
363-
) as e:
364-
logger.exception("Failed to create cluster")
365-
raise e
357+
self._create_cluster()
366358

367359
def _on_peer_relation_changed(self, event: RelationChangedEvent) -> None:
368360
"""Handle the peer relation changed event."""
@@ -769,7 +761,9 @@ def workload_initialise(self) -> None:
769761
self._mysql.write_mysqld_config()
770762
self.log_rotation_setup.setup()
771763
self._mysql.reset_root_password_and_start_mysqld()
772-
self._mysql.configure_mysql_users()
764+
self._mysql.configure_mysql_router_roles()
765+
self._mysql.configure_mysql_system_roles()
766+
self._mysql.configure_mysql_system_users()
773767

774768
if self.config.plugin_audit_enabled:
775769
self._mysql.install_plugins(["audit_log"])
@@ -805,16 +799,6 @@ def update_endpoints(self) -> None:
805799
self.database_relation._update_endpoints_all_relations(None)
806800
self._on_update_status(None)
807801

808-
def _open_ports(self) -> None:
809-
"""Open ports.
810-
811-
Used if `juju expose` ran on application
812-
"""
813-
try:
814-
self.unit.set_ports(3306, 33060)
815-
except ops.ModelError:
816-
logger.exception("failed to open port")
817-
818802
def _can_start(self, event: StartEvent) -> bool:
819803
"""Check if the unit can start.
820804
@@ -864,6 +848,22 @@ def _can_start(self, event: StartEvent) -> bool:
864848

865849
return True
866850

851+
def _create_cluster(self) -> None:
852+
"""Creates the InnoDB cluster and sets up the ports."""
853+
try:
854+
# Create the cluster and cluster set from the leader unit
855+
logger.info(f"Creating cluster {self.app_peer_data['cluster-name']}")
856+
self.create_cluster()
857+
self.unit.set_ports(3306, 33060)
858+
self.unit.status = ActiveStatus(self.active_status_message)
859+
except (
860+
MySQLCreateClusterError,
861+
MySQLCreateClusterSetError,
862+
MySQLInitializeJujuOperationsTableError,
863+
) as e:
864+
logger.exception("Failed to create cluster")
865+
raise e
866+
867867
def _is_unit_waiting_to_join_cluster(self) -> bool:
868868
"""Return if the unit is waiting to join the cluster."""
869869
# alternatively, we could check if the instance is configured

src/mysql_vm_helpers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
MYSQLD_DEFAULTS_CONFIG_FILE,
5454
MYSQLD_SOCK_FILE,
5555
ROOT_SYSTEM_USER,
56+
ROOT_USERNAME,
5657
XTRABACKUP_PLUGIN_DIR,
5758
)
5859

@@ -803,7 +804,7 @@ def _run_mysqlsh_script(
803804
def _run_mysqlcli_script(
804805
self,
805806
script: tuple[Any, ...] | list[Any],
806-
user: str = "root",
807+
user: str = ROOT_USERNAME,
807808
password: str | None = None,
808809
timeout: int | None = None,
809810
exception_as_warning: bool = False,

src/relations/db_router.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
from charms.mysql.v0.mysql import (
1212
MySQLCheckUserExistenceError,
1313
MySQLConfigureRouterUserError,
14-
MySQLCreateApplicationDatabaseAndScopedUserError,
14+
MySQLCreateApplicationDatabaseError,
15+
MySQLCreateApplicationScopedUserError,
1516
MySQLDeleteUsersForUnitError,
1617
MySQLGetClusterPrimaryAddressError,
1718
)
@@ -124,8 +125,8 @@ def _create_requested_users(
124125
Raises:
125126
MySQLCheckUserExistenceError if there is an issue checking a user's existence
126127
MySQLConfigureRouterUserError if there is an issue configuring the mysqlrouter user
127-
MySQLCreateApplicationDatabaseAndScopedUserError if there is an issue creating a
128-
user or said user scoped database
128+
MySQLCreateApplicationDatabaseError if there is an issue creating the database
129+
MySQLCreateApplicationScopedUserError if there is an issue creating the database user
129130
"""
130131
user_passwords = {}
131132
requested_user_applications = set()
@@ -141,7 +142,8 @@ def _create_requested_users(
141142
requested_user.username, password, requested_user.hostname, user_unit_name
142143
)
143144
else:
144-
self.charm._mysql.create_application_database_and_scoped_user(
145+
self.charm._mysql.create_database(requested_user.database)
146+
self.charm._mysql.create_scoped_user(
145147
requested_user.database,
146148
requested_user.username,
147149
password,
@@ -227,7 +229,8 @@ def _on_db_router_relation_changed(self, event: RelationChangedEvent) -> None:
227229
except (
228230
MySQLCheckUserExistenceError,
229231
MySQLConfigureRouterUserError,
230-
MySQLCreateApplicationDatabaseAndScopedUserError,
232+
MySQLCreateApplicationDatabaseError,
233+
MySQLCreateApplicationScopedUserError,
231234
):
232235
self.charm.unit.status = BlockedStatus("Failed to create app user or scoped database")
233236
return

src/relations/mysql.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010

1111
from charms.mysql.v0.mysql import (
1212
MySQLCheckUserExistenceError,
13-
MySQLCreateApplicationDatabaseAndScopedUserError,
13+
MySQLCreateApplicationDatabaseError,
14+
MySQLCreateApplicationScopedUserError,
1415
MySQLDeleteUsersForUnitError,
1516
MySQLGetClusterPrimaryAddressError,
1617
)
@@ -194,7 +195,8 @@ def _on_mysql_relation_created(self, event: RelationCreatedEvent) -> None:
194195
password = self._get_or_set_password_in_peer_secrets(username)
195196

196197
try:
197-
self.charm._mysql.create_application_database_and_scoped_user(
198+
self.charm._mysql.create_database(database)
199+
self.charm._mysql.create_scoped_user(
198200
database,
199201
username,
200202
password,
@@ -203,9 +205,9 @@ def _on_mysql_relation_created(self, event: RelationCreatedEvent) -> None:
203205
)
204206

205207
primary_address = self.charm._mysql.get_cluster_primary_address()
206-
207208
except (
208-
MySQLCreateApplicationDatabaseAndScopedUserError,
209+
MySQLCreateApplicationDatabaseError,
210+
MySQLCreateApplicationScopedUserError,
209211
MySQLGetClusterPrimaryAddressError,
210212
):
211213
self.charm.unit.status = BlockedStatus("Failed to initialize `mysql` relation")

src/relations/mysql_provider.py

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,21 @@
88

99
from charms.data_platform_libs.v0.data_interfaces import DatabaseProvides, DatabaseRequestedEvent
1010
from charms.mysql.v0.mysql import (
11+
LEGACY_ROLE_ROUTER,
12+
MODERN_ROLE_ROUTER,
1113
MySQLClientError,
12-
MySQLCreateApplicationDatabaseAndScopedUserError,
14+
MySQLCreateApplicationDatabaseError,
15+
MySQLCreateApplicationScopedUserError,
1316
MySQLDeleteUserError,
1417
MySQLDeleteUsersForRelationError,
1518
MySQLGetClusterEndpointsError,
1619
MySQLGetClusterMembersAddressesError,
1720
MySQLGetMySQLVersionError,
18-
MySQLGrantPrivilegesToUserError,
1921
MySQLRemoveRouterFromMetadataError,
2022
)
2123
from ops.charm import RelationBrokenEvent, RelationDepartedEvent, RelationJoinedEvent
2224
from ops.framework import Object
23-
from ops.model import BlockedStatus
25+
from ops.model import ActiveStatus, BlockedStatus
2426

2527
from constants import DB_RELATION_NAME, PASSWORD_LENGTH, PEER
2628
from utils import generate_random_password
@@ -220,16 +222,17 @@ def _on_database_requested(self, event: DatabaseRequestedEvent):
220222

221223
# get base relation data
222224
relation_id = event.relation.id
225+
app_name = event.app.name
223226
db_name = event.database
227+
224228
extra_user_roles = []
225229
if event.extra_user_roles:
226230
extra_user_roles = event.extra_user_roles.split(",")
231+
227232
# user name is derived from the relation id
228233
db_user = self._get_username(relation_id)
229234
db_pass = self._get_or_set_password(event.relation)
230235

231-
remote_app = event.app.name
232-
233236
# Update endpoint addresses
234237
self.charm.update_endpoint_address(DB_RELATION_NAME)
235238

@@ -242,31 +245,26 @@ def _on_database_requested(self, event: DatabaseRequestedEvent):
242245
self.database.set_version(relation_id, db_version)
243246
self.database.set_read_only_endpoints(relation_id, ro_endpoints)
244247

245-
if "mysqlrouter" in extra_user_roles:
246-
self.charm._mysql.create_application_database_and_scoped_user(
247-
db_name,
248-
db_user,
249-
db_pass,
250-
"%",
251-
# MySQL Router charm does not need a new database
252-
create_database=False,
253-
)
254-
self.charm._mysql.grant_privileges_to_user(
255-
db_user, "%", ["ALL PRIVILEGES"], with_grant_option=True
256-
)
257-
else:
258-
# TODO:
259-
# add setup of tls, tls_ca and status
260-
self.charm._mysql.create_application_database_and_scoped_user(
261-
db_name, db_user, db_pass, "%"
262-
)
263-
264-
logger.info(f"Created user for app {remote_app}")
248+
if not any([
249+
LEGACY_ROLE_ROUTER in extra_user_roles,
250+
MODERN_ROLE_ROUTER in extra_user_roles,
251+
]):
252+
self.charm._mysql.create_database(db_name)
253+
254+
self.charm._mysql.create_scoped_user(
255+
db_name,
256+
db_user,
257+
db_pass,
258+
"%",
259+
extra_roles=extra_user_roles,
260+
)
261+
logger.info(f"Created user for app {app_name}")
262+
self.charm.unit.status = ActiveStatus()
265263
except (
266-
MySQLCreateApplicationDatabaseAndScopedUserError,
264+
MySQLCreateApplicationDatabaseError,
265+
MySQLCreateApplicationScopedUserError,
267266
MySQLGetMySQLVersionError,
268267
MySQLGetClusterMembersAddressesError,
269-
MySQLGrantPrivilegesToUserError,
270268
MySQLClientError,
271269
) as e:
272270
logger.exception("Failed to set up database relation", exc_info=e)

src/relations/shared_db.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
import typing
88

99
from charms.mysql.v0.mysql import (
10-
MySQLCreateApplicationDatabaseAndScopedUserError,
10+
MySQLCreateApplicationDatabaseError,
11+
MySQLCreateApplicationScopedUserError,
1112
MySQLGetClusterPrimaryAddressError,
1213
)
1314
from ops.charm import LeaderElectedEvent, RelationChangedEvent, RelationDepartedEvent
@@ -153,8 +154,13 @@ def _on_shared_db_relation_changed(self, event: RelationChangedEvent) -> None:
153154
remote_host = event.relation.data[event.unit].get("private-address")
154155

155156
try:
156-
self._charm._mysql.create_application_database_and_scoped_user(
157-
database_name, database_user, password, remote_host, unit_name=joined_unit
157+
self._charm._mysql.create_database(database_name)
158+
self._charm._mysql.create_scoped_user(
159+
database_name,
160+
database_user,
161+
password,
162+
remote_host,
163+
unit_name=joined_unit,
158164
)
159165

160166
# set the relation data for consumption
@@ -179,7 +185,10 @@ def _on_shared_db_relation_changed(self, event: RelationChangedEvent) -> None:
179185
allowed_units_set
180186
)
181187

182-
except MySQLCreateApplicationDatabaseAndScopedUserError:
188+
except (
189+
MySQLCreateApplicationDatabaseError,
190+
MySQLCreateApplicationScopedUserError,
191+
):
183192
self._charm.unit.status = BlockedStatus("Failed to initialize shared_db relation")
184193
return
185194

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2025 Canonical Ltd.
3+
# See LICENSE file for licensing details.
4+
5+
import asyncio
6+
from pathlib import Path
7+
8+
import pytest
9+
import yaml
10+
from pytest_operator.plugin import OpsTest
11+
12+
from . import juju_
13+
from .helpers import (
14+
execute_queries_on_unit,
15+
get_primary_unit,
16+
get_server_config_credentials,
17+
)
18+
19+
METADATA = yaml.safe_load(Path("./metadata.yaml").read_text())
20+
21+
DATABASE_APP_NAME = METADATA["name"]
22+
INTEGRATOR_APP_NAME = "data-integrator"
23+
24+
25+
@pytest.mark.abort_on_fail
26+
async def test_build_and_deploy(ops_test: OpsTest, charm) -> None:
27+
"""Simple test to ensure that the mysql and data-integrator charms get deployed."""
28+
async with ops_test.fast_forward("10s"):
29+
await asyncio.gather(
30+
ops_test.model.deploy(
31+
charm,
32+
application_name=DATABASE_APP_NAME,
33+
num_units=3,
34+
35+
config={"profile": "testing"},
36+
),
37+
ops_test.model.deploy(
38+
INTEGRATOR_APP_NAME,
39+
40+
),
41+
)
42+
43+
await ops_test.model.wait_for_idle(apps=[DATABASE_APP_NAME], status="active")
44+
await ops_test.model.wait_for_idle(apps=[INTEGRATOR_APP_NAME], status="blocked")
45+
46+
47+
@pytest.mark.abort_on_fail
48+
async def test_charmed_dba_role(ops_test: OpsTest):
49+
"""Test the DBA predefined role."""
50+
await ops_test.model.applications[INTEGRATOR_APP_NAME].set_config({
51+
"database-name": "charmed_dba_database",
52+
"extra-user-roles": "charmed_dba",
53+
})
54+
await ops_test.model.add_relation(INTEGRATOR_APP_NAME, DATABASE_APP_NAME)
55+
await ops_test.model.wait_for_idle(
56+
apps=[INTEGRATOR_APP_NAME, DATABASE_APP_NAME], status="active"
57+
)
58+
59+
mysql_unit = ops_test.model.applications[DATABASE_APP_NAME].units[0]
60+
primary_unit = await get_primary_unit(ops_test, mysql_unit, DATABASE_APP_NAME)
61+
primary_unit_address = await primary_unit.get_public_address()
62+
server_config_credentials = await get_server_config_credentials(primary_unit)
63+
64+
await execute_queries_on_unit(
65+
primary_unit_address,
66+
server_config_credentials["username"],
67+
server_config_credentials["password"],
68+
["CREATE DATABASE IF NOT EXISTS test"],
69+
commit=True,
70+
)
71+
72+
data_integrator_unit = ops_test.model.applications[INTEGRATOR_APP_NAME].units[0]
73+
results = await juju_.run_action(data_integrator_unit, "get-credentials")
74+
75+
rows = await execute_queries_on_unit(
76+
primary_unit_address,
77+
results["mysql"]["username"],
78+
results["mysql"]["password"],
79+
["SHOW DATABASES"],
80+
commit=True,
81+
)
82+
83+
assert "test" in rows, "Database is not visible to DBA user"

0 commit comments

Comments
 (0)