Skip to content

Commit 12a236a

Browse files
[MISC] Update MySQL charm libs (#621)
1 parent c54085c commit 12a236a

File tree

10 files changed

+103
-96
lines changed

10 files changed

+103
-96
lines changed

lib/charms/mysql/v0/async_replication.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
# The unique Charmhub library identifier, never change it
5555
LIBID = "4de21f1a022c4e2c87ac8e672ec16f6a"
5656
LIBAPI = 0
57-
LIBPATCH = 6
57+
LIBPATCH = 7
5858

5959
RELATION_OFFER = "replication-offer"
6060
RELATION_CONSUMER = "replication"
@@ -553,7 +553,7 @@ def _on_offer_relation_changed(self, event):
553553
unit_label = remote_data["node-label"]
554554

555555
logger.debug("Looking for a donor node")
556-
_, ro, _ = self._charm._mysql.get_cluster_endpoints(get_ips=False)
556+
_, ro, _ = self._charm.get_cluster_endpoints(self.relation.name)
557557

558558
if not ro:
559559
logger.debug(f"Adding replica {cluster=} with {endpoint=}. Primary is the donor")
@@ -716,15 +716,16 @@ def _async_replication_credentials(self) -> dict[str, str]:
716716
secret = self._obtain_secret()
717717
return secret.peek_content()
718718

719-
def _get_endpoint(self) -> str:
719+
def _get_endpoint(self) -> Optional[str]:
720720
"""Get endpoint to be used by the primary cluster.
721721
722722
This is the address in which the unit must be reachable from the primary cluster.
723723
Not necessarily the locally resolved address, but an ingress address.
724724
"""
725-
# TODO: devise method to inform the real address
726-
# using unit informed address (fqdn or ip)
727-
return self._charm.unit_address
725+
if self.relation.name == RELATION_CONSUMER:
726+
return self._charm.get_unit_address(self._charm.unit, RELATION_CONSUMER)
727+
if self.relation.name == RELATION_OFFER:
728+
return self._charm.get_unit_address(self._charm.unit, RELATION_OFFER)
728729

729730
def _on_consumer_relation_created(self, event):
730731
"""Handle the async_replica relation being created on the leader unit."""

lib/charms/mysql/v0/backups.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ def is_unit_blocked(self) -> bool:
9595

9696
from constants import (
9797
MYSQL_DATA_DIR,
98+
PEER,
9899
SERVER_CONFIG_PASSWORD_KEY,
99100
SERVER_CONFIG_USERNAME,
100101
)
@@ -112,21 +113,21 @@ def is_unit_blocked(self) -> bool:
112113

113114
# Increment this PATCH version before using `charmcraft publish-lib` or reset
114115
# to 0 if you are raising the major API version
115-
LIBPATCH = 13
116+
LIBPATCH = 14
116117

117118
ANOTHER_S3_CLUSTER_REPOSITORY_ERROR_MESSAGE = "S3 repository claimed by another cluster"
118119
MOVE_RESTORED_CLUSTER_TO_ANOTHER_S3_REPOSITORY_ERROR = (
119120
"Move restored cluster to another S3 repository"
120121
)
121122

122123
if typing.TYPE_CHECKING:
123-
from charm import MySQLOperatorCharm
124+
from mysql import MySQLCharmBase
124125

125126

126127
class MySQLBackups(Object):
127128
"""Encapsulation of backups for MySQL."""
128129

129-
def __init__(self, charm: "MySQLOperatorCharm", s3_integrator: S3Requirer) -> None:
130+
def __init__(self, charm: "MySQLCharmBase", s3_integrator: S3Requirer) -> None:
130131
super().__init__(charm, MYSQL_BACKUPS)
131132

132133
self.charm = charm
@@ -715,7 +716,7 @@ def _pitr_restore(
715716
try:
716717
logger.info("Restoring point-in-time-recovery")
717718
stdout, stderr = self.charm._mysql.restore_pitr(
718-
host=self.charm.get_unit_address(self.charm.unit),
719+
host=self.charm.get_unit_address(self.charm.unit, PEER),
719720
mysql_user=self.charm._mysql.server_config_user,
720721
password=self.charm._mysql.server_config_password,
721722
s3_parameters=s3_parameters,
@@ -744,8 +745,7 @@ def _post_restore(self) -> Tuple[bool, str]:
744745

745746
try:
746747
logger.info("Creating cluster on restored node")
747-
unit_label = self.charm.unit.name.replace("/", "-")
748-
self.charm._mysql.create_cluster(unit_label)
748+
self.charm._mysql.create_cluster(self.charm.unit_label)
749749
self.charm._mysql.create_cluster_set()
750750
self.charm._mysql.initialize_juju_units_operations_table()
751751

lib/charms/mysql/v0/mysql.py

Lines changed: 58 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ def wait_until_mysql_connection(self) -> None:
7474
import logging
7575
import os
7676
import re
77-
import socket
7877
import sys
7978
import time
8079
from abc import ABC, abstractmethod
@@ -134,7 +133,7 @@ def wait_until_mysql_connection(self) -> None:
134133
# Increment this major API version when introducing breaking changes
135134
LIBAPI = 0
136135

137-
LIBPATCH = 87
136+
LIBPATCH = 88
138137

139138
UNIT_TEARDOWN_LOCKNAME = "unit-teardown"
140139
UNIT_ADD_LOCKNAME = "unit-add"
@@ -495,11 +494,16 @@ def get_unit_hostname(self):
495494
raise NotImplementedError
496495

497496
@abstractmethod
498-
def get_unit_address(self, unit: Unit) -> str:
497+
def get_unit_address(self, unit: Unit, relation_name: str) -> str:
499498
"""Return unit address."""
500499
# each platform has its own way to get an arbitrary unit address
501500
raise NotImplementedError
502501

502+
@staticmethod
503+
def get_unit_label(unit: Unit) -> str:
504+
"""Return unit label."""
505+
return unit.name.replace("/", "-")
506+
503507
def _on_get_password(self, event: ActionEvent) -> None:
504508
"""Action used to retrieve the system user's password."""
505509
username = event.params.get("username") or ROOT_USERNAME
@@ -633,7 +637,7 @@ def cluster_initialized(self) -> bool:
633637
for unit in self.app_units:
634638
try:
635639
if unit != self.unit and self._mysql.cluster_metadata_exists(
636-
self.get_unit_address(unit)
640+
self.get_unit_address(unit, PEER)
637641
):
638642
return True
639643
elif self._mysql.cluster_metadata_exists():
@@ -652,13 +656,14 @@ def only_one_cluster_node_thats_uninitialized(self) -> Optional[bool]:
652656
total_cluster_nodes = 0
653657
for unit in self.app_units:
654658
total_cluster_nodes += self._mysql.get_cluster_node_count(
655-
from_instance=self.get_unit_address(unit)
659+
from_instance=self.get_unit_address(unit, PEER)
656660
)
657661

658662
total_online_cluster_nodes = 0
659663
for unit in self.app_units:
660664
total_online_cluster_nodes += self._mysql.get_cluster_node_count(
661-
from_instance=self.get_unit_address(unit), node_status=MySQLMemberState["ONLINE"]
665+
from_instance=self.get_unit_address(unit, PEER),
666+
node_status=MySQLMemberState.ONLINE,
662667
)
663668

664669
return total_cluster_nodes == 1 and total_online_cluster_nodes == 0
@@ -669,15 +674,16 @@ def cluster_fully_initialized(self) -> bool:
669674
670675
Fully initialized means that all unit that can be joined are joined.
671676
"""
672-
return self._mysql.get_cluster_node_count(node_status=MySQLMemberState["ONLINE"]) == min(
677+
return self._mysql.get_cluster_node_count(node_status=MySQLMemberState.ONLINE) == min(
673678
GR_MAX_MEMBERS, self.app.planned_units()
674679
)
675680

676681
@property
677682
def unit_configured(self) -> bool:
678683
"""Check if the unit is configured to be part of the cluster."""
679684
return self._mysql.is_instance_configured_for_innodb(
680-
self.get_unit_address(self.unit), self.unit_label
685+
self.get_unit_address(self.unit, PEER),
686+
self.unit_label,
681687
)
682688

683689
@property
@@ -707,7 +713,7 @@ def app_units(self) -> set[Unit]:
707713
@property
708714
def unit_label(self):
709715
"""Return unit label."""
710-
return self.unit.name.replace("/", "-")
716+
return self.get_unit_label(self.unit)
711717

712718
@property
713719
def _is_peer_data_set(self) -> bool:
@@ -774,6 +780,37 @@ def peer_relation_data(self, scope: Scopes) -> DataPeerData:
774780
elif scope == UNIT_SCOPE:
775781
return self.peer_relation_unit
776782

783+
def get_cluster_endpoints(self, relation_name: str) -> Tuple[str, str, str]:
784+
"""Return (rw, ro, offline) endpoints tuple names or IPs."""
785+
repl_topology = self._mysql.get_cluster_topology()
786+
repl_cluster = self._mysql.is_cluster_replica()
787+
788+
if not repl_topology:
789+
raise MySQLGetClusterEndpointsError("Failed to get endpoints from cluster topology")
790+
791+
unit_labels = {self.get_unit_label(unit): unit for unit in self.app_units}
792+
793+
no_endpoints = set()
794+
ro_endpoints = set()
795+
rw_endpoints = set()
796+
797+
for k, v in repl_topology.items():
798+
address = f"{self.get_unit_address(unit_labels[k], relation_name)}:3306"
799+
800+
if v["status"] != MySQLMemberState.ONLINE:
801+
no_endpoints.add(address)
802+
if v["status"] == MySQLMemberState.ONLINE and v["mode"] == "r/o":
803+
ro_endpoints.add(address)
804+
if v["status"] == MySQLMemberState.ONLINE and v["mode"] == "r/w" and not repl_cluster:
805+
rw_endpoints.add(address)
806+
807+
# Replica return global primary address
808+
if repl_cluster:
809+
primary_address = f"{self._mysql.get_cluster_set_global_primary_address()}:3306"
810+
rw_endpoints.add(primary_address)
811+
812+
return ",".join(rw_endpoints), ",".join(ro_endpoints), ",".join(no_endpoints)
813+
777814
def get_secret(
778815
self,
779816
scope: Scopes,
@@ -2144,51 +2181,6 @@ def get_cluster_node_count(
21442181

21452182
return int(matches.group(1)) if matches else 0
21462183

2147-
def get_cluster_endpoints(self, get_ips: bool = True) -> Tuple[str, str, str]:
2148-
"""Return (rw, ro, ofline) endpoints tuple names or IPs."""
2149-
status = self.get_cluster_status()
2150-
2151-
if not status:
2152-
raise MySQLGetClusterEndpointsError("Failed to get endpoints from cluster status")
2153-
2154-
topology = status["defaultreplicaset"]["topology"]
2155-
2156-
def _get_host_ip(host: str) -> str:
2157-
try:
2158-
port = None
2159-
if ":" in host:
2160-
host, port = host.split(":")
2161-
2162-
host_ip = socket.gethostbyname(host)
2163-
return f"{host_ip}:{port}" if port else host_ip
2164-
except socket.gaierror:
2165-
raise MySQLGetClusterEndpointsError(f"Failed to query IP for host {host}")
2166-
2167-
ro_endpoints = {
2168-
_get_host_ip(v["address"]) if get_ips else v["address"]
2169-
for v in topology.values()
2170-
if v["mode"] == "r/o" and v["status"] == MySQLMemberState.ONLINE
2171-
}
2172-
2173-
if self.is_cluster_replica():
2174-
# replica return global primary address
2175-
global_primary = self.get_cluster_set_global_primary_address()
2176-
if not global_primary:
2177-
raise MySQLGetClusterEndpointsError("Failed to get global primary address")
2178-
rw_endpoints = {_get_host_ip(global_primary) if get_ips else global_primary}
2179-
else:
2180-
rw_endpoints = {
2181-
_get_host_ip(v["address"]) if get_ips else v["address"]
2182-
for v in topology.values()
2183-
if v["mode"] == "r/w" and v["status"] == MySQLMemberState.ONLINE
2184-
}
2185-
# won't get offline endpoints to IP as they maybe unreachable
2186-
no_endpoints = {
2187-
v["address"] for v in topology.values() if v["status"] != MySQLMemberState.ONLINE
2188-
}
2189-
2190-
return ",".join(rw_endpoints), ",".join(ro_endpoints), ",".join(no_endpoints)
2191-
21922184
def execute_remove_instance(
21932185
self, connect_instance: Optional[str] = None, force: bool = False
21942186
) -> None:
@@ -2478,12 +2470,21 @@ def get_cluster_set_global_primary_address(
24782470

24792471
return address
24802472

2481-
def get_primary_label(self) -> Optional[str]:
2482-
"""Get the label of the cluster's primary."""
2473+
def get_cluster_topology(self) -> Optional[dict]:
2474+
"""Get the cluster topology."""
24832475
status = self.get_cluster_status()
24842476
if not status:
24852477
return None
2486-
for label, value in status["defaultreplicaset"]["topology"].items():
2478+
2479+
return status["defaultreplicaset"]["topology"]
2480+
2481+
def get_primary_label(self) -> Optional[str]:
2482+
"""Get the label of the cluster's primary."""
2483+
topology = self.get_cluster_topology()
2484+
if not topology:
2485+
return None
2486+
2487+
for label, value in topology.items():
24872488
if value["memberrole"] == "primary":
24882489
return label
24892490

src/charm.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ def tracing_endpoint(self) -> Optional[str]:
210210
def _mysql(self) -> MySQL:
211211
"""Returns an instance of the MySQL object from mysql_k8s_helpers."""
212212
return MySQL(
213-
self.get_unit_address(),
213+
self.unit_address,
214214
self.app_peer_data["cluster-name"],
215215
self.app_peer_data["cluster-set-domain-name"],
216216
self.get_secret("app", ROOT_PASSWORD_KEY), # pyright: ignore [reportArgumentType]
@@ -292,7 +292,7 @@ def restart_peers(self) -> Optional[ops.model.Relation]:
292292
@property
293293
def unit_address(self) -> str:
294294
"""Return the address of this unit."""
295-
return self.get_unit_address()
295+
return self.get_unit_address(self.unit)
296296

297297
@property
298298
def is_new_unit(self) -> bool:
@@ -344,14 +344,11 @@ def get_unit_hostname(self, unit_name: Optional[str] = None) -> str:
344344
return f"{unit_name.replace('/', '-')}.{self.app.name}-endpoints"
345345

346346
@retry(reraise=True, stop=stop_after_delay(120), wait=wait_fixed(2))
347-
def get_unit_address(self, unit: Optional[Unit] = None) -> str:
347+
def get_unit_address(self, unit: Unit, relation_name: str = PEER) -> str:
348348
"""Get fqdn/address for a unit.
349349
350350
Translate juju unit name to resolvable hostname.
351351
"""
352-
if not unit:
353-
unit = self.unit
354-
355352
unit_hostname = self.get_unit_hostname(unit.name)
356353
unit_dns_domain = getfqdn(self.get_unit_hostname(unit.name))
357354

@@ -411,7 +408,7 @@ def join_unit_to_cluster(self) -> None:
411408
412409
Try to join the unit from the primary unit.
413410
"""
414-
instance_label = self.unit.name.replace("/", "-")
411+
instance_label = self.get_unit_label(self.unit)
415412
instance_address = self.get_unit_address(self.unit)
416413

417414
if not self._mysql.is_instance_in_cluster(instance_label):

src/k8s_helpers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def __init__(self, charm: "MySQLOperatorCharm"):
4141
Args:
4242
charm: a `CharmBase` parent object
4343
"""
44-
self.pod_name = charm.unit.name.replace("/", "-")
44+
self.pod_name = charm.get_unit_label(charm.unit)
4545
self.namespace = charm.model.name
4646
self.app_name = charm.model.app.name
4747
self.cluster_name = charm.app_peer_data.get("cluster-name")

src/mysql_k8s_helpers.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
MYSQLD_SERVICE,
4848
MYSQLD_SOCK_FILE,
4949
MYSQLSH_LOCATION,
50+
PEER,
5051
ROOT_SYSTEM_USER,
5152
XTRABACKUP_PLUGIN_DIR,
5253
)
@@ -898,11 +899,11 @@ def is_data_dir_initialised(self) -> bool:
898899
except ExecError:
899900
return False
900901

901-
def update_endpoints(self) -> None:
902+
def update_endpoints(self, relation_name: str) -> None:
902903
"""Updates pod labels to reflect role of the unit."""
903904
logger.debug("Updating pod labels")
904905
try:
905-
rw_endpoints, ro_endpoints, offline = self.get_cluster_endpoints(get_ips=False)
906+
rw_endpoints, ro_endpoints, offline = self.charm.get_cluster_endpoints(relation_name)
906907

907908
for endpoints, label in (
908909
(rw_endpoints, "primary"),
@@ -920,7 +921,7 @@ def update_endpoints(self) -> None:
920921
def set_cluster_primary(self, new_primary_address: str) -> None:
921922
"""Set the cluster primary and update pod labels."""
922923
super().set_cluster_primary(new_primary_address)
923-
self.update_endpoints()
924+
self.update_endpoints(PEER)
924925

925926
def fetch_error_log(self) -> Optional[str]:
926927
"""Fetch the MySQL error log."""
@@ -983,6 +984,6 @@ def get_cluster_members(self) -> list[str]:
983984
984985
Returns: list of cluster members in MySQL MEMBER_HOST format.
985986
"""
986-
return [self.charm.get_unit_address(self.charm.unit)] + [
987+
return [self.charm.unit_address] + [
987988
self.charm.get_unit_address(unit) for unit in self.charm.peers.units
988989
]

0 commit comments

Comments
 (0)