Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 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
67 changes: 31 additions & 36 deletions lib/charms/mysql/v0/async_replication.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,19 @@
MySQLPromoteClusterToPrimaryError,
MySQLRejoinClusterError,
)
from constants import (
BACKUPS_PASSWORD_KEY,
BACKUPS_USERNAME,
CLUSTER_ADMIN_PASSWORD_KEY,
CLUSTER_ADMIN_USERNAME,
MONITORING_PASSWORD_KEY,
MONITORING_USERNAME,
PEER,
ROOT_PASSWORD_KEY,
ROOT_USERNAME,
SERVER_CONFIG_PASSWORD_KEY,
SERVER_CONFIG_USERNAME,
)
from ops import (
ActionEvent,
ActiveStatus,
Expand All @@ -30,21 +43,6 @@
)
from ops.framework import Object
from tenacity import RetryError, Retrying, stop_after_attempt, wait_fixed
from typing_extensions import Optional

from constants import (
BACKUPS_PASSWORD_KEY,
BACKUPS_USERNAME,
CLUSTER_ADMIN_PASSWORD_KEY,
CLUSTER_ADMIN_USERNAME,
MONITORING_PASSWORD_KEY,
MONITORING_USERNAME,
PEER,
ROOT_PASSWORD_KEY,
ROOT_USERNAME,
SERVER_CONFIG_PASSWORD_KEY,
SERVER_CONFIG_USERNAME,
)

if typing.TYPE_CHECKING:
from charm import MySQLOperatorCharm
Expand All @@ -54,7 +52,7 @@
# The unique Charmhub library identifier, never change it
LIBID = "4de21f1a022c4e2c87ac8e672ec16f6a"
LIBAPI = 0
LIBPATCH = 7
LIBPATCH = 10

RELATION_OFFER = "replication-offer"
RELATION_CONSUMER = "replication"
Expand Down Expand Up @@ -126,28 +124,31 @@ def cluster_set_name(self) -> str:
return self._charm.app_peer_data["cluster-set-domain-name"]

@property
def relation(self) -> Optional[Relation]:
def relation(self) -> Relation | None:
"""Relation."""
return self.model.get_relation(RELATION_OFFER) or self.model.get_relation(
RELATION_CONSUMER
)

@property
def relation_data(self) -> Optional[RelationDataContent]:
def relation_data(self) -> RelationDataContent | None:
"""Relation data."""
if not self.relation:
return
return self.relation.data[self.model.app]

@property
def remote_relation_data(self) -> Optional[RelationDataContent]:
def remote_relation_data(self) -> RelationDataContent | None:
"""Remote relation data."""
if not self.relation or not self.relation.app:
return
return self.relation.data[self.relation.app]

def _on_promote_to_primary(self, event: ActionEvent) -> None:
"""Promote a standby cluster to primary."""
if event.params.get("scope") != "cluster":
return

if not self._charm.unit.is_leader():
event.fail("Only the leader unit can promote a standby cluster")
return
Expand Down Expand Up @@ -359,7 +360,7 @@ def __init__(self, charm: "MySQLOperatorCharm"):
self.framework.observe(self._charm.on.secret_changed, self._on_secret_change)

@property
def state(self) -> Optional[States]:
def state(self) -> States | None:
"""State of the relation, on primary side."""
if not self.relation:
return States.UNINITIALIZED
Expand Down Expand Up @@ -405,12 +406,10 @@ def idle(self) -> bool:
# transitional state between relation created and setup_action
return False

if self.state not in [States.READY, States.UNINITIALIZED]:
return False
return True
return self.state in [States.READY, States.UNINITIALIZED]

@property
def secret(self) -> Optional[Secret]:
def secret(self) -> Secret | None:
"""Return the async replication secret."""
if not self.relation:
return
Expand All @@ -428,7 +427,7 @@ def _get_secret(self) -> Secret:

def _on_create_replication(self, event: ActionEvent):
"""Promote the offer side to primary on initial setup."""
if not self._charm.app_peer_data.get("async-ready") == "true":
if self._charm.app_peer_data.get("async-ready") != "true":
event.fail("Relation created but not ready")
return

Expand Down Expand Up @@ -489,10 +488,8 @@ def _on_offer_created(self, event: RelationCreatedEvent):
):
# Test for a broken relation on the primary side
logger.error(
(
"Cannot setup async relation with primary cluster in blocked/read-only state\n"
"Remove the relation."
)
"Cannot setup async relation with primary cluster in blocked/read-only state\n"
"Remove the relation."
)
message = f"Cluster is in a blocked state. Remove {RELATION_OFFER} relation"
self._charm.unit.status = BlockedStatus(message)
Expand All @@ -501,10 +498,8 @@ def _on_offer_created(self, event: RelationCreatedEvent):
if not self.model.get_relation(RELATION_OFFER):
# safeguard against a deferred event a previous relation.
logger.error(
(
"Relation created running against removed relation.\n"
f"Remove {RELATION_OFFER} relation and retry."
)
"Relation created running against removed relation.\n"
f"Remove {RELATION_OFFER} relation and retry."
)
self._charm.unit.status = BlockedStatus(f"Remove {RELATION_OFFER} relation and retry")
return
Expand Down Expand Up @@ -639,7 +634,7 @@ def __init__(self, charm: "MySQLOperatorCharm"):
self.framework.observe(self._charm.on.secret_changed, self._on_secret_change)

@property
def state(self) -> Optional[States]:
def state(self) -> States | None:
"""State of the relation, on consumer side."""
if not self.relation:
return None
Expand Down Expand Up @@ -716,7 +711,7 @@ def _async_replication_credentials(self) -> dict[str, str]:
secret = self._obtain_secret()
return secret.peek_content()

def _get_endpoint(self) -> Optional[str]:
def _get_endpoint(self) -> str | None:
"""Get endpoint to be used by the primary cluster.

This is the address in which the unit must be reachable from the primary cluster.
Expand Down Expand Up @@ -885,7 +880,7 @@ def _on_consumer_non_leader_created(self, _):
# set waiting state to inhibit auto recovery, only when not already set
if self._charm.unit.is_leader():
return
if not self._charm.unit_peer_data.get("member-state") == "waiting":
if self._charm.unit_peer_data.get("member-state") != "waiting":
self._charm.unit_peer_data["member-state"] = "waiting"
self._charm.unit.status = WaitingStatus("waiting replica cluster be configured")

Expand Down
46 changes: 21 additions & 25 deletions lib/charms/mysql/v0/backups.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ def is_unit_blocked(self) -> bool:
import pathlib
import re
import typing
from typing import Dict, List, Optional, Tuple

from charms.data_platform_libs.v0.s3 import (
CredentialsChangedEvent,
Expand Down Expand Up @@ -88,17 +87,16 @@ def is_unit_blocked(self) -> bool:
list_backups_in_s3_path,
upload_content_to_s3,
)
from ops.charm import ActionEvent
from ops.framework import Object
from ops.jujuversion import JujuVersion
from ops.model import BlockedStatus, MaintenanceStatus

from constants import (
MYSQL_DATA_DIR,
PEER,
SERVER_CONFIG_PASSWORD_KEY,
SERVER_CONFIG_USERNAME,
)
from ops.charm import ActionEvent
from ops.framework import Object
from ops.jujuversion import JujuVersion
from ops.model import BlockedStatus, MaintenanceStatus

logger = logging.getLogger(__name__)

Expand All @@ -113,7 +111,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 = 14
LIBPATCH = 16

ANOTHER_S3_CLUSTER_REPOSITORY_ERROR_MESSAGE = "S3 repository claimed by another cluster"
MOVE_RESTORED_CLUSTER_TO_ANOTHER_S3_REPOSITORY_ERROR = (
Expand Down Expand Up @@ -150,7 +148,7 @@ def _s3_integrator_relation_exists(self) -> bool:
"""Returns whether a relation with the s3-integrator exists."""
return bool(self.model.get_relation(S3_INTEGRATOR_RELATION_NAME))

def _retrieve_s3_parameters(self) -> Tuple[Dict[str, str], List[str]]:
def _retrieve_s3_parameters(self) -> tuple[dict[str, str], list[str]]:
"""Retrieve S3 parameters from the S3 integrator relation.

Returns: tuple of (s3_parameters, missing_required_parameters)
Expand Down Expand Up @@ -196,7 +194,7 @@ def _upload_logs_to_s3(
stdout: str,
stderr: str,
log_filename: str,
s3_parameters: Dict[str, str],
s3_parameters: dict[str, str],
) -> bool:
"""Upload logs to S3 at the specified location.

Expand All @@ -219,7 +217,7 @@ def _upload_logs_to_s3(
# ------------------ List Backups ------------------

@staticmethod
def _format_backups_list(backup_list: List[Tuple[str, str]]) -> str:
def _format_backups_list(backup_list: list[tuple[str, str]]) -> str:
"""Formats the provided list of backups as a table."""
backups = [f"{'backup-id':<21} | {'backup-type':<12} | backup-status"]

Expand Down Expand Up @@ -250,9 +248,7 @@ def _on_list_backups(self, event: ActionEvent) -> None:
event.set_results({"backups": self._format_backups_list(backups)})
except Exception as e:
error_message = (
getattr(e, "message")
if hasattr(e, "message")
else "Failed to retrieve backup ids from S3"
e.message if hasattr(e, "message") else "Failed to retrieve backup ids from S3"
)
logger.error(error_message)
event.fail(error_message)
Expand Down Expand Up @@ -313,7 +309,7 @@ def _on_create_backup(self, event: ActionEvent) -> None:
f"Model Name: {self.model.name}\n"
f"Application Name: {self.model.app.name}\n"
f"Unit Name: {self.charm.unit.name}\n"
f"Juju Version: {str(juju_version)}\n"
f"Juju Version: {juju_version!s}\n"
)

if not upload_content_to_s3(metadata, f"{backup_path}.metadata", s3_parameters):
Expand Down Expand Up @@ -359,7 +355,7 @@ def _on_create_backup(self, event: ActionEvent) -> None:
})
self.charm._on_update_status(None)

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

Returns: tuple of (success, error_message)
Expand Down Expand Up @@ -390,7 +386,7 @@ def _can_unit_perform_backup(self) -> Tuple[bool, Optional[str]]:

return True, None

def _pre_backup(self) -> Tuple[bool, Optional[str]]:
def _pre_backup(self) -> tuple[bool, str | None]:
"""Runs operations required before performing a backup.

Returns: tuple of (success, error_message)
Expand All @@ -415,7 +411,7 @@ def _pre_backup(self) -> Tuple[bool, Optional[str]]:

return True, None

def _backup(self, backup_path: str, s3_parameters: Dict) -> Tuple[bool, Optional[str]]:
def _backup(self, backup_path: str, s3_parameters: dict) -> tuple[bool, str | None]:
"""Runs the backup operations.

Args:
Expand Down Expand Up @@ -450,7 +446,7 @@ def _backup(self, backup_path: str, s3_parameters: Dict) -> Tuple[bool, Optional

return True, None

def _post_backup(self) -> Tuple[bool, Optional[str]]:
def _post_backup(self) -> tuple[bool, str | None]:
"""Runs operations required after performing a backup.

Returns: tuple of (success, error_message)
Expand Down Expand Up @@ -613,7 +609,7 @@ def _on_restore(self, event: ActionEvent) -> None: # noqa: C901
# update status as soon as possible
self.charm._on_update_status(None)

def _pre_restore(self) -> Tuple[bool, str]:
def _pre_restore(self) -> tuple[bool, str]:
"""Perform operations that need to be done before performing a restore.

Returns: tuple of (success, error_message)
Expand All @@ -635,7 +631,7 @@ def _pre_restore(self) -> Tuple[bool, str]:

return True, ""

def _restore(self, backup_id: str, s3_parameters: Dict[str, str]) -> Tuple[bool, bool, str]:
def _restore(self, backup_id: str, s3_parameters: dict[str, str]) -> tuple[bool, bool, str]:
"""Run the restore operations.

Args:
Expand Down Expand Up @@ -687,7 +683,7 @@ def _restore(self, backup_id: str, s3_parameters: Dict[str, str]) -> Tuple[bool,

return True, True, ""

def _clean_data_dir_and_start_mysqld(self) -> Tuple[bool, str]:
def _clean_data_dir_and_start_mysqld(self) -> tuple[bool, str]:
"""Run idempotent operations run after restoring a backup.

Returns tuple of (success, error_message)
Expand All @@ -711,8 +707,8 @@ def _clean_data_dir_and_start_mysqld(self) -> Tuple[bool, str]:
return True, ""

def _pitr_restore(
self, restore_to_time: str, s3_parameters: Dict[str, str]
) -> Tuple[bool, str]:
self, restore_to_time: str, s3_parameters: dict[str, str]
) -> tuple[bool, str]:
try:
logger.info("Restoring point-in-time-recovery")
stdout, stderr = self.charm._mysql.restore_pitr(
Expand All @@ -728,7 +724,7 @@ def _pitr_restore(
return False, f"Failed to restore point-in-time-recovery to the {restore_to_time}"
return True, ""

def _post_restore(self) -> Tuple[bool, str]:
def _post_restore(self) -> tuple[bool, str]:
"""Run operations required after restoring a backup.

Returns: tuple of (success, error_message)
Expand Down Expand Up @@ -836,7 +832,7 @@ def _on_s3_credentials_gone(self, event: CredentialsGoneEvent) -> None:
"Exception is occurred when trying to stop binlogs collecting after S3 relation depart. It may be a leader departure"
)

def get_binlogs_collector_config(self) -> Dict[str, str]:
def get_binlogs_collector_config(self) -> dict[str, str]:
"""Return binlogs collector service config file.

Returns: dict of binlogs collector service config
Expand Down
Loading