Skip to content

Commit 7ce19f5

Browse files
Extend create-backup action checks
1 parent 6d2662f commit 7ce19f5

File tree

4 files changed

+98
-10
lines changed

4 files changed

+98
-10
lines changed

actions.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ set-tls-private-key:
4545
create-backup:
4646
description: Create a database backup using xtrabackup.
4747
S3 credentials are retrieved from a relation with the S3 integrator charm.
48+
params:
49+
force:
50+
type: boolean
51+
default: False
52+
description: |
53+
Whether to ignore cluster health concerns, and create the backup regardless.
4854
4955
list-backups:
5056
description: List available backup_ids in the S3 bucket and path provided by the S3 integrator charm.

lib/charms/mysql/v0/backups.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def is_unit_blocked(self) -> bool:
5757
S3Requirer,
5858
)
5959
from charms.mysql.v0.mysql import (
60+
MySQLClusterState,
6061
MySQLConfigureInstanceError,
6162
MySQLCreateClusterError,
6263
MySQLCreateClusterSetError,
@@ -111,7 +112,7 @@ def is_unit_blocked(self) -> bool:
111112

112113
# Increment this PATCH version before using `charmcraft publish-lib` or reset
113114
# to 0 if you are raising the major API version
114-
LIBPATCH = 16
115+
LIBPATCH = 17
115116

116117
ANOTHER_S3_CLUSTER_REPOSITORY_ERROR_MESSAGE = "S3 repository claimed by another cluster"
117118
MOVE_RESTORED_CLUSTER_TO_ANOTHER_S3_REPOSITORY_ERROR = (
@@ -280,6 +281,7 @@ def _pre_create_backup_checks(self, event: ActionEvent) -> bool:
280281
def _on_create_backup(self, event: ActionEvent) -> None:
281282
"""Handle the create backup action."""
282283
logger.info("A backup has been requested on unit")
284+
force = event.params.get("force", False)
283285

284286
if not self._pre_create_backup_checks(event):
285287
return
@@ -295,9 +297,16 @@ def _on_create_backup(self, event: ActionEvent) -> None:
295297

296298
backup_path = str(pathlib.Path(s3_parameters["path"]) / datetime_backup_requested)
297299

300+
# Check if this cluster can perform backup
301+
can_cluster_perform_backup, validation_message = self._can_cluster_perform_backup()
302+
if not (can_cluster_perform_backup or force):
303+
logger.error(f"Backup failed: {validation_message}")
304+
event.fail(validation_message or "")
305+
return
306+
298307
# Check if this unit can perform backup
299308
can_unit_perform_backup, validation_message = self._can_unit_perform_backup()
300-
if not can_unit_perform_backup:
309+
if not (can_unit_perform_backup or force):
301310
logger.error(f"Backup failed: {validation_message}")
302311
event.fail(validation_message or "")
303312
return
@@ -355,6 +364,21 @@ def _on_create_backup(self, event: ActionEvent) -> None:
355364
})
356365
self.charm._on_update_status(None)
357366

367+
def _can_cluster_perform_backup(self) -> tuple[bool, str | None]:
368+
"""Validates whether this cluster can perform a backup.
369+
370+
Returns: tuple of (success, error_message)
371+
"""
372+
cluster_status = self.charm._mysql.get_cluster_status()
373+
if not cluster_status:
374+
return False, "Cluster status unknown"
375+
376+
cluster_status = cluster_status["defaultreplicaset"]["status"]
377+
if cluster_status not in (MySQLClusterState.OK, MySQLClusterState.OK_PARTIAL):
378+
return False, "Cluster is not in a healthy state"
379+
380+
return True, None
381+
358382
def _can_unit_perform_backup(self) -> tuple[bool, str | None]:
359383
"""Validates whether this unit can perform a backup.
360384

lib/charms/mysql/v0/mysql.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def wait_until_mysql_connection(self) -> None:
127127
# Increment this major API version when introducing breaking changes
128128
LIBAPI = 0
129129

130-
LIBPATCH = 93
130+
LIBPATCH = 94
131131

132132
UNIT_TEARDOWN_LOCKNAME = "unit-teardown"
133133
UNIT_ADD_LOCKNAME = "unit-add"
@@ -916,11 +916,7 @@ def get_cluster_endpoints(self, relation_name: str) -> tuple[str, str, str]:
916916

917917
return ",".join(rw_endpoints), ",".join(ro_endpoints), ",".join(no_endpoints)
918918

919-
def get_secret(
920-
self,
921-
scope: Scopes,
922-
key: str,
923-
) -> str | None:
919+
def get_secret(self, scope: Scopes, key: str) -> str | None:
924920
"""Get secret from the secret storage.
925921
926922
Retrieve secret from juju secrets backend if secret exists there.
@@ -1012,7 +1008,18 @@ class MySQLMemberState(str, enum.Enum):
10121008
class MySQLClusterState(str, enum.Enum):
10131009
"""MySQL Cluster state."""
10141010

1011+
# TODO: python 3.11 has new enum.StrEnum
1012+
# that can remove str inheritance
1013+
10151014
OK = "ok"
1015+
OK_PARTIAL = "ok_partial"
1016+
OK_NO_TOLERANCE = "ok_no_tolerance"
1017+
OK_NO_TOLERANCE_PARTIAL = "ok_no_tolerance_partial"
1018+
NO_QUORUM = "no_quorum"
1019+
OFFLINE = "offline"
1020+
ERROR = "error"
1021+
UNREACHABLE = "unreachable"
1022+
UNKNOWN = "unknown"
10161023
FENCED = "fenced_writes"
10171024

10181025

tests/unit/test_backups.py

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,12 @@ def test_on_list_backups_failure(self, _list_backups_in_s3_path, _retrieve_s3_pa
155155
return_value=({"path": "/path"}, []),
156156
)
157157
@patch(
158-
"charms.mysql.v0.backups.MySQLBackups._can_unit_perform_backup", return_value=(True, None)
158+
"charms.mysql.v0.backups.MySQLBackups._can_cluster_perform_backup",
159+
return_value=(True, None),
160+
)
161+
@patch(
162+
"charms.mysql.v0.backups.MySQLBackups._can_unit_perform_backup",
163+
return_value=(True, None),
159164
)
160165
@patch("charms.mysql.v0.backups.upload_content_to_s3")
161166
@patch("charms.mysql.v0.backups.MySQLBackups._pre_backup", return_value=(True, None))
@@ -170,6 +175,7 @@ def test_on_create_backup(
170175
_pre_backup,
171176
_upload_content_to_s3,
172177
_can_unit_perform_backup,
178+
_can_cluster_perform_backup,
173179
_retrieve_s3_parameters,
174180
_datetime,
175181
_update_status,
@@ -191,6 +197,7 @@ def test_on_create_backup(
191197
self.mysql_backups._on_create_backup(event)
192198

193199
_retrieve_s3_parameters.assert_called_once()
200+
_can_cluster_perform_backup.assert_called_once()
194201
_can_unit_perform_backup.assert_called_once()
195202
_upload_content_to_s3.assert_called_once_with(
196203
expected_metadata, f"{expected_backup_path}.metadata", expected_s3_params
@@ -208,7 +215,12 @@ def test_on_create_backup(
208215
return_value=({"path": "/path"}, []),
209216
)
210217
@patch(
211-
"charms.mysql.v0.backups.MySQLBackups._can_unit_perform_backup", return_value=(True, None)
218+
"charms.mysql.v0.backups.MySQLBackups._can_cluster_perform_backup",
219+
return_value=(True, None),
220+
)
221+
@patch(
222+
"charms.mysql.v0.backups.MySQLBackups._can_unit_perform_backup",
223+
return_value=(True, None),
212224
)
213225
@patch("charms.mysql.v0.backups.upload_content_to_s3")
214226
@patch("charms.mysql.v0.backups.MySQLBackups._pre_backup", return_value=(True, None))
@@ -223,6 +235,7 @@ def test_on_create_backup_failure(
223235
_pre_backup,
224236
_upload_content_to_s3,
225237
_can_unit_perform_backup,
238+
_can_cluster_perform_backup,
226239
_retrieve_s3_parameters,
227240
_datetime,
228241
):
@@ -272,13 +285,25 @@ def test_on_create_backup_failure(
272285
# test failure with _can_unit_perform_backup
273286
_can_unit_perform_backup.return_value = False, "can unit perform backup failure"
274287
event = MagicMock()
288+
type(event).params = PropertyMock(return_value={"force": False})
275289
self.charm.unit.status = ActiveStatus()
276290

277291
self.mysql_backups._on_create_backup(event)
278292
event.set_results.assert_not_called()
279293
event.fail.assert_called_once_with("can unit perform backup failure")
280294
self.assertTrue(isinstance(self.harness.model.unit.status, ActiveStatus))
281295

296+
# test failure with _can_cluster_perform_backup
297+
_can_cluster_perform_backup.return_value = False, "can cluster perform backup failure"
298+
event = MagicMock()
299+
type(event).params = PropertyMock(return_value={"force": False})
300+
self.charm.unit.status = ActiveStatus()
301+
302+
self.mysql_backups._on_create_backup(event)
303+
event.set_results.assert_not_called()
304+
event.fail.assert_called_once_with("can cluster perform backup failure")
305+
self.assertTrue(isinstance(self.harness.model.unit.status, ActiveStatus))
306+
282307
# test failure with _retrieve_s3_parameters
283308
_retrieve_s3_parameters.return_value = False, ["bucket"]
284309
event = MagicMock()
@@ -309,6 +334,32 @@ def test_on_create_backup_failure(
309334
event.fail.assert_called_once_with("Missing relation with S3 integrator charm")
310335
self.assertTrue(isinstance(self.harness.model.unit.status, ActiveStatus))
311336

337+
@patch("mysql_vm_helpers.MySQL.get_cluster_status")
338+
def test_can_cluster_perform_backup(self, _get_cluster_status):
339+
"""Test _can_cluster_perform_backup()."""
340+
_get_cluster_status.return_value = {"defaultreplicaset": {"status": "ok"}}
341+
342+
success, error_message = self.mysql_backups._can_cluster_perform_backup()
343+
self.assertTrue(success)
344+
self.assertIsNone(error_message)
345+
346+
@patch("mysql_vm_helpers.MySQL.get_cluster_status")
347+
def test_can_cluster_perform_backup_failure(self, _get_cluster_status):
348+
"""Test failure of _can_unit_perform_backup()."""
349+
# test unknown state
350+
_get_cluster_status.return_value = None
351+
352+
success, error_message = self.mysql_backups._can_cluster_perform_backup()
353+
self.assertFalse(success)
354+
self.assertEqual(error_message, "Cluster status unknown")
355+
356+
# test error state
357+
_get_cluster_status.return_value = {"defaultreplicaset": {"status": "error"}}
358+
359+
success, error_message = self.mysql_backups._can_cluster_perform_backup()
360+
self.assertFalse(success)
361+
self.assertEqual(error_message, "Cluster is not in a healthy state")
362+
312363
@patch("mysql_vm_helpers.MySQL.offline_mode_and_hidden_instance_exists", return_value=False)
313364
@patch("mysql_vm_helpers.MySQL.get_member_state", return_value=("online", "replica"))
314365
def test_can_unit_perform_backup(

0 commit comments

Comments
 (0)