Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
6 changes: 6 additions & 0 deletions actions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ set-tls-private-key:
create-backup:
description: Create a database backup using xtrabackup.
S3 credentials are retrieved from a relation with the S3 integrator charm.
params:
force:
type: boolean
default: False
description: |
Whether to ignore cluster health concerns, and create the backup regardless.

list-backups:
description: List available backup_ids in the S3 bucket and path provided by the S3 integrator charm.
Expand Down
28 changes: 26 additions & 2 deletions lib/charms/mysql/v0/backups.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def is_unit_blocked(self) -> bool:
S3Requirer,
)
from charms.mysql.v0.mysql import (
MySQLClusterState,
MySQLConfigureInstanceError,
MySQLCreateClusterError,
MySQLCreateClusterSetError,
Expand Down Expand Up @@ -111,7 +112,7 @@ def is_unit_blocked(self) -> bool:

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

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

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

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

# Check if this cluster can perform backup
can_cluster_perform_backup, validation_message = self._can_cluster_perform_backup()
if not (can_cluster_perform_backup or force):
logger.error(f"Backup failed: {validation_message}")
event.fail(validation_message or "")
return

# Check if this unit can perform backup
can_unit_perform_backup, validation_message = self._can_unit_perform_backup()
if not can_unit_perform_backup:
if not (can_unit_perform_backup or force):
logger.error(f"Backup failed: {validation_message}")
event.fail(validation_message or "")
return
Expand Down Expand Up @@ -355,6 +364,21 @@ def _on_create_backup(self, event: ActionEvent) -> None:
})
self.charm._on_update_status(None)

def _can_cluster_perform_backup(self) -> tuple[bool, str | None]:
"""Validates whether this cluster can perform a backup.

Returns: tuple of (success, error_message)
"""
cluster_status = self.charm._mysql.get_cluster_status()
if not cluster_status:
return False, "Cluster status unknown"

cluster_status = cluster_status["defaultreplicaset"]["status"]
if cluster_status not in (MySQLClusterState.OK, MySQLClusterState.OK_PARTIAL):
return False, "Cluster is not in a healthy state"

return True, None

def _can_unit_perform_backup(self) -> tuple[bool, str | None]:
"""Validates whether this unit can perform a backup.

Expand Down
19 changes: 13 additions & 6 deletions lib/charms/mysql/v0/mysql.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def wait_until_mysql_connection(self) -> None:
# Increment this major API version when introducing breaking changes
LIBAPI = 0

LIBPATCH = 93
LIBPATCH = 94

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

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

def get_secret(
self,
scope: Scopes,
key: str,
) -> str | None:
def get_secret(self, scope: Scopes, key: str) -> str | None:
"""Get secret from the secret storage.

Retrieve secret from juju secrets backend if secret exists there.
Expand Down Expand Up @@ -1012,7 +1008,18 @@ class MySQLMemberState(str, enum.Enum):
class MySQLClusterState(str, enum.Enum):
"""MySQL Cluster state."""

# TODO: python 3.11 has new enum.StrEnum
# that can remove str inheritance

OK = "ok"
OK_PARTIAL = "ok_partial"
OK_NO_TOLERANCE = "ok_no_tolerance"
OK_NO_TOLERANCE_PARTIAL = "ok_no_tolerance_partial"
NO_QUORUM = "no_quorum"
OFFLINE = "offline"
ERROR = "error"
UNREACHABLE = "unreachable"
UNKNOWN = "unknown"
FENCED = "fenced_writes"


Expand Down
Loading