Skip to content

Commit 63785fc

Browse files
[DRAFT]
1 parent 411fb45 commit 63785fc

File tree

17 files changed

+904
-290
lines changed

17 files changed

+904
-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,
@@ -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."""
@@ -748,7 +740,9 @@ def workload_initialise(self) -> None:
748740
self._mysql.write_mysqld_config()
749741
self.log_rotation_setup.setup()
750742
self._mysql.reset_root_password_and_start_mysqld()
751-
self._mysql.configure_mysql_users()
743+
self._mysql.configure_mysql_router_roles()
744+
self._mysql.configure_mysql_system_roles()
745+
self._mysql.configure_mysql_system_users()
752746

753747
if self.config.plugin_audit_enabled:
754748
self._mysql.install_plugins(["audit_log"])
@@ -779,16 +773,6 @@ def get_unit_address(self, unit: Unit, relation_name: str) -> str:
779773
except KeyError:
780774
return ""
781775

782-
def _open_ports(self) -> None:
783-
"""Open ports.
784-
785-
Used if `juju expose` ran on application
786-
"""
787-
try:
788-
self.unit.set_ports(3306, 33060)
789-
except ops.ModelError:
790-
logger.exception("failed to open port")
791-
792776
def _can_start(self, event: StartEvent) -> bool:
793777
"""Check if the unit can start.
794778
@@ -838,6 +822,22 @@ def _can_start(self, event: StartEvent) -> bool:
838822

839823
return True
840824

825+
def _create_cluster(self) -> None:
826+
"""Creates the InnoDB cluster and sets up the ports."""
827+
try:
828+
# Create the cluster and cluster set from the leader unit
829+
logger.info(f"Creating cluster {self.app_peer_data['cluster-name']}")
830+
self.create_cluster()
831+
self.unit.set_ports(3306, 33060)
832+
self.unit.status = ActiveStatus(self.active_status_message)
833+
except (
834+
MySQLCreateClusterError,
835+
MySQLCreateClusterSetError,
836+
MySQLInitializeJujuOperationsTableError,
837+
) as e:
838+
logger.exception("Failed to create cluster")
839+
raise e
840+
841841
def _is_unit_waiting_to_join_cluster(self) -> bool:
842842
"""Return if the unit is waiting to join the cluster."""
843843
# 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
@@ -218,16 +220,17 @@ def _on_database_requested(self, event: DatabaseRequestedEvent):
218220

219221
# get base relation data
220222
relation_id = event.relation.id
223+
app_name = event.app.name
221224
db_name = event.database
225+
222226
extra_user_roles = []
223227
if event.extra_user_roles:
224228
extra_user_roles = event.extra_user_roles.split(",")
229+
225230
# user name is derived from the relation id
226231
db_user = self._get_username(relation_id)
227232
db_pass = self._get_or_set_password(event.relation)
228233

229-
remote_app = event.app.name
230-
231234
try:
232235
db_version = self.charm._mysql.get_mysql_version()
233236
rw_endpoints, ro_endpoints, _ = self.charm.get_cluster_endpoints(DB_RELATION_NAME)
@@ -237,31 +240,26 @@ def _on_database_requested(self, event: DatabaseRequestedEvent):
237240
self.database.set_version(relation_id, db_version)
238241
self.database.set_read_only_endpoints(relation_id, ro_endpoints)
239242

240-
if "mysqlrouter" in extra_user_roles:
241-
self.charm._mysql.create_application_database_and_scoped_user(
242-
db_name,
243-
db_user,
244-
db_pass,
245-
"%",
246-
# MySQL Router charm does not need a new database
247-
create_database=False,
248-
)
249-
self.charm._mysql.grant_privileges_to_user(
250-
db_user, "%", ["ALL PRIVILEGES"], with_grant_option=True
251-
)
252-
else:
253-
# TODO:
254-
# add setup of tls, tls_ca and status
255-
self.charm._mysql.create_application_database_and_scoped_user(
256-
db_name, db_user, db_pass, "%"
257-
)
258-
259-
logger.info(f"Created user for app {remote_app}")
243+
if not any([
244+
LEGACY_ROLE_ROUTER in extra_user_roles,
245+
MODERN_ROLE_ROUTER in extra_user_roles,
246+
]):
247+
self.charm._mysql.create_database(db_name)
248+
249+
self.charm._mysql.create_scoped_user(
250+
db_name,
251+
db_user,
252+
db_pass,
253+
"%",
254+
extra_roles=extra_user_roles,
255+
)
256+
logger.info(f"Created user for app {app_name}")
257+
self.charm.unit.status = ActiveStatus()
260258
except (
261-
MySQLCreateApplicationDatabaseAndScopedUserError,
259+
MySQLCreateApplicationDatabaseError,
260+
MySQLCreateApplicationScopedUserError,
262261
MySQLGetMySQLVersionError,
263262
MySQLGetClusterMembersAddressesError,
264-
MySQLGrantPrivilegesToUserError,
265263
MySQLClientError,
266264
) as e:
267265
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: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2025 Canonical Ltd.
3+
# See LICENSE file for licensing details.
4+
5+
import asyncio
6+
7+
import pytest
8+
from pytest_operator.plugin import OpsTest
9+
10+
from . import juju_
11+
from .helpers import (
12+
execute_queries_on_unit,
13+
get_primary_unit,
14+
get_server_config_credentials,
15+
)
16+
from .relations.test_database import DATABASE_APP_NAME
17+
18+
DATA_INTEGRATOR_APP_NAME = "data-integrator"
19+
20+
21+
@pytest.mark.abort_on_fail
22+
async def test_build_and_deploy(ops_test: OpsTest, charm) -> None:
23+
"""Simple test to ensure that the mysql and data-integrator charms get deployed."""
24+
async with ops_test.fast_forward("10s"):
25+
await asyncio.gather(
26+
ops_test.model.deploy(
27+
charm,
28+
application_name=DATABASE_APP_NAME,
29+
num_units=3,
30+
31+
config={"profile": "testing"},
32+
),
33+
ops_test.model.deploy(
34+
DATA_INTEGRATOR_APP_NAME,
35+
36+
),
37+
)
38+
39+
await ops_test.model.wait_for_idle(apps=[DATABASE_APP_NAME], status="active")
40+
await ops_test.model.wait_for_idle(apps=[DATA_INTEGRATOR_APP_NAME], status="blocked")
41+
42+
43+
@pytest.mark.abort_on_fail
44+
async def test_charmed_dba_role(ops_test: OpsTest):
45+
"""Test the DBA predefined role."""
46+
await ops_test.model.applications[DATA_INTEGRATOR_APP_NAME].set_config({
47+
"database-name": "charmed_dba_database",
48+
"extra-user-roles": "charmed_dba",
49+
})
50+
await ops_test.model.add_relation(DATA_INTEGRATOR_APP_NAME, DATABASE_APP_NAME)
51+
await ops_test.model.wait_for_idle(
52+
apps=[DATA_INTEGRATOR_APP_NAME, DATABASE_APP_NAME], status="active"
53+
)
54+
55+
mysql_unit = ops_test.model.applications[DATABASE_APP_NAME].units[0]
56+
primary_unit = await get_primary_unit(ops_test, mysql_unit, DATABASE_APP_NAME)
57+
primary_unit_address = await primary_unit.get_public_address()
58+
server_config_credentials = await get_server_config_credentials(primary_unit)
59+
60+
await execute_queries_on_unit(
61+
primary_unit_address,
62+
server_config_credentials["username"],
63+
server_config_credentials["password"],
64+
["CREATE DATABASE IF NOT EXISTS test"],
65+
commit=True,
66+
)
67+
68+
data_integrator_unit = ops_test.model.applications[DATA_INTEGRATOR_APP_NAME].units[0]
69+
results = await juju_.run_action(data_integrator_unit, "get-credentials")
70+
71+
rows = await execute_queries_on_unit(
72+
primary_unit_address,
73+
results["mysql"]["username"],
74+
results["mysql"]["password"],
75+
["SHOW DATABASES"],
76+
commit=True,
77+
)
78+
79+
assert "test" in rows, "Database is not visible to DBA user"

0 commit comments

Comments
 (0)