Skip to content

Commit 5c5c2ae

Browse files
[DRAFT]
1 parent e0f134a commit 5c5c2ae

File tree

17 files changed

+912
-290
lines changed

17 files changed

+912
-290
lines changed

lib/charms/mysql/v0/mysql.py

Lines changed: 205 additions & 89 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
@@ -33,6 +33,7 @@
3333
MySQLAddInstanceToClusterError,
3434
MySQLCharmBase,
3535
MySQLConfigureInstanceError,
36+
MySQLConfigureMySQLRolesError,
3637
MySQLConfigureMySQLUsersError,
3738
MySQLCreateClusterError,
3839
MySQLCreateClusterSetError,
@@ -327,6 +328,9 @@ def _on_start(self, event: StartEvent) -> None:
327328

328329
try:
329330
self.workload_initialise()
331+
except MySQLConfigureMySQLRolesError:
332+
self.unit.status = BlockedStatus("Failed to initialize MySQL roles")
333+
return
330334
except MySQLConfigureMySQLUsersError:
331335
self.unit.status = BlockedStatus("Failed to initialize MySQL users")
332336
return
@@ -351,19 +355,7 @@ def _on_start(self, event: StartEvent) -> None:
351355
self.unit_peer_data["member-state"] = "waiting"
352356
return
353357

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

368360
def _on_peer_relation_changed(self, event: RelationChangedEvent) -> None:
369361
"""Handle the peer relation changed event."""
@@ -770,7 +762,9 @@ def workload_initialise(self) -> None:
770762
self._mysql.write_mysqld_config()
771763
self.log_rotation_setup.setup()
772764
self._mysql.reset_root_password_and_start_mysqld()
773-
self._mysql.configure_mysql_users()
765+
self._mysql.configure_mysql_router_roles()
766+
self._mysql.configure_mysql_system_roles()
767+
self._mysql.configure_mysql_system_users()
774768

775769
if self.config.plugin_audit_enabled:
776770
self._mysql.install_plugins(["audit_log"])
@@ -806,16 +800,6 @@ def update_endpoints(self) -> None:
806800
self.database_relation._update_endpoints_all_relations(None)
807801
self._on_update_status(None)
808802

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

866850
return True
867851

852+
def _create_cluster(self) -> None:
853+
"""Creates the InnoDB cluster and sets up the ports."""
854+
try:
855+
# Create the cluster and cluster set from the leader unit
856+
logger.info(f"Creating cluster {self.app_peer_data['cluster-name']}")
857+
self.create_cluster()
858+
self.unit.set_ports(3306, 33060)
859+
self.unit.status = ActiveStatus(self.active_status_message)
860+
except (
861+
MySQLCreateClusterError,
862+
MySQLCreateClusterSetError,
863+
MySQLInitializeJujuOperationsTableError,
864+
) as e:
865+
logger.exception("Failed to create cluster")
866+
raise e
867+
868868
def _is_unit_waiting_to_join_cluster(self) -> bool:
869869
"""Return if the unit is waiting to join the cluster."""
870870
# 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: Union[Tuple[Any, ...], List[Any]],
806-
user: str = "root",
807+
user: str = ROOT_USERNAME,
807808
password: Optional[str] = None,
808809
timeout: Optional[int] = 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
@@ -12,7 +12,8 @@
1212
from charms.mysql.v0.mysql import (
1313
MySQLCheckUserExistenceError,
1414
MySQLConfigureRouterUserError,
15-
MySQLCreateApplicationDatabaseAndScopedUserError,
15+
MySQLCreateApplicationDatabaseError,
16+
MySQLCreateApplicationScopedUserError,
1617
MySQLDeleteUsersForUnitError,
1718
MySQLGetClusterPrimaryAddressError,
1819
)
@@ -125,8 +126,8 @@ def _create_requested_users(
125126
Raises:
126127
MySQLCheckUserExistenceError if there is an issue checking a user's existence
127128
MySQLConfigureRouterUserError if there is an issue configuring the mysqlrouter user
128-
MySQLCreateApplicationDatabaseAndScopedUserError if there is an issue creating a
129-
user or said user scoped database
129+
MySQLCreateApplicationDatabaseError if there is an issue creating the database
130+
MySQLCreateApplicationScopedUserError if there is an issue creating the database user
130131
"""
131132
user_passwords = {}
132133
requested_user_applications = set()
@@ -142,7 +143,8 @@ def _create_requested_users(
142143
requested_user.username, password, requested_user.hostname, user_unit_name
143144
)
144145
else:
145-
self.charm._mysql.create_application_database_and_scoped_user(
146+
self.charm._mysql.create_database(requested_user.database)
147+
self.charm._mysql.create_scoped_user(
146148
requested_user.database,
147149
requested_user.username,
148150
password,
@@ -228,7 +230,8 @@ def _on_db_router_relation_changed(self, event: RelationChangedEvent) -> None:
228230
except (
229231
MySQLCheckUserExistenceError,
230232
MySQLConfigureRouterUserError,
231-
MySQLCreateApplicationDatabaseAndScopedUserError,
233+
MySQLCreateApplicationDatabaseError,
234+
MySQLCreateApplicationScopedUserError,
232235
):
233236
self.charm.unit.status = BlockedStatus("Failed to create app user or scoped database")
234237
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)