Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
1177a7f
Fix metadata
astrojuanlu Feb 11, 2026
f2659b7
Add simple test
astrojuanlu Feb 12, 2026
f62e0b2
Properly parametrize MySQL datadir
astrojuanlu Feb 13, 2026
6aae614
Separate temporary tablespace storage
astrojuanlu Feb 13, 2026
b1da318
Separate binlogs storage
astrojuanlu Feb 13, 2026
970a9f8
Separate redologs storage
astrojuanlu Feb 13, 2026
66a0e8f
Separate logs storage
astrojuanlu Feb 13, 2026
2ac5bcb
Refactor
astrojuanlu Feb 13, 2026
dc0b444
Add spread task
astrojuanlu Feb 13, 2026
1bacabf
Fix mysql data dir contents check ahead of initialization
astrojuanlu Feb 17, 2026
cdab4e0
Proper cleanup of other extra data directories before restoring backups
astrojuanlu Feb 18, 2026
db1a0bf
[K8S] Consolidate all logs under one volume
astrojuanlu Apr 10, 2026
c0467b6
[VM] Implement separation of storage
astrojuanlu Apr 10, 2026
8656b7f
[VM] Clear contents without deleting
astrojuanlu Apr 14, 2026
3ba7dd8
[VM] Synchronize initialization flags with runtime configuration
astrojuanlu Apr 14, 2026
d5045a7
[VM] Rework file retrieval in storage test
astrojuanlu Apr 14, 2026
e98a4c7
[K8s] Synchronize initialization flags with runtime configuration
astrojuanlu Apr 15, 2026
e4ba4b7
[K8s] Adjust storage test
astrojuanlu Apr 15, 2026
641820f
[VM] Adjust storage test
astrojuanlu Apr 15, 2026
41661bc
[VM] Add archive storage for rotated logs
astrojuanlu Apr 15, 2026
5b8be75
[K8s] Add archive storage for rotated logs
astrojuanlu Apr 15, 2026
9b8dc92
[K8s] Cleanup
astrojuanlu Apr 15, 2026
cc71139
[VM] Cleanup
astrojuanlu Apr 15, 2026
2711c12
[VM] Temporarily disable upgrade test
astrojuanlu Apr 15, 2026
3f09ce7
[K8s] Temporarily disable upgrade test
astrojuanlu Apr 15, 2026
8dfacaf
[VM] Fix directory permissions and contents for backups
astrojuanlu Apr 15, 2026
6f94652
[VM] Add better logging to stop_mysql_process_gracefully
astrojuanlu Apr 15, 2026
b614f69
[K8s] Remove unneeded comments
astrojuanlu Apr 15, 2026
1d97adc
[K8s] Remove unneeded cluster configuration from test
astrojuanlu Apr 15, 2026
e7096c8
[VM] Remove unneeded comments
astrojuanlu Apr 15, 2026
534e912
[VM] Remove unneeded cluster configuration from test
astrojuanlu Apr 15, 2026
a14a9eb
[K8s] Reduce nesting
astrojuanlu Apr 15, 2026
04f2200
[VM] Update MySQL charm lib to match K8s
astrojuanlu Apr 15, 2026
7cb5348
[VM] Consolidate is_volume_mounted
astrojuanlu Apr 15, 2026
7fe9635
[K8s] Listen to any storage detaching
astrojuanlu Apr 15, 2026
63e3dc8
[K8s] Force literal filenames
astrojuanlu Apr 16, 2026
7990735
[VM] Add missing spread task for test_storage.py
astrojuanlu Apr 16, 2026
5838ea5
[VM] Remove unnecessary line break
astrojuanlu Apr 16, 2026
7e801cd
[K8s] Remove unnecessary line break
astrojuanlu Apr 16, 2026
4ac7171
[VM] Arrange MySQL config options logically
astrojuanlu Apr 16, 2026
a02a9c2
[K8s] Arrange MySQL config options logically
astrojuanlu Apr 16, 2026
848d466
[VM] Improve observability of backup command
astrojuanlu Apr 16, 2026
bbb2620
[VM] Make Ceph backup test amenable for local development
astrojuanlu Apr 17, 2026
edcf10f
[K8s] Make Ceph backup test amenable for local development
astrojuanlu Apr 17, 2026
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
44 changes: 34 additions & 10 deletions kubernetes/lib/charms/mysql/v0/mysql.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ def __init__(
import ops
from charms.data_platform_libs.v0.data_interfaces import DataPeerData, DataPeerUnitData
from constants import (
MYSQL_DATA_DIR,
MYSQL_LOGS_DIR,
MYSQL_TEMP_DIR,
BACKUPS_PASSWORD_KEY,
BACKUPS_USERNAME,
CHARMED_MYSQL_PITR_HELPER,
Expand Down Expand Up @@ -1105,7 +1108,6 @@ def render_mysqld_configuration( # noqa: C901
memory_limit: int | None = None,
experimental_max_connections: int | None = None,
binlog_retention_days: int,
snap_common: str = "",
) -> tuple[str, dict]:
"""Render mysqld ini configuration file."""
max_connections = None
Expand Down Expand Up @@ -1154,26 +1156,31 @@ def render_mysqld_configuration( # noqa: C901
# disable memory instruments if we have less than 2GiB of RAM
performance_schema_instrument = "'memory/%=OFF'"

logging_path = f"{snap_common}/var/log/mysql"
binlog_retention_seconds = binlog_retention_days * 24 * 60 * 60
config = configparser.ConfigParser(interpolation=None)

# do not enable slow query logs, but specify a log file path in case
# the admin enables them manually
config["mysqld"] = {
base_config = {
"datadir": MYSQL_DATA_DIR,
# All interfaces bind expected
"bind_address": "0.0.0.0", # noqa: S104
"mysqlx_bind_address": "0.0.0.0", # noqa: S104
"admin_address": self.instance_address,
"report_host": self.instance_address,
"max_connections": max_connections,
"innodb_buffer_pool_size": innodb_buffer_pool_size,
"innodb_log_group_home_dir": MYSQL_LOGS_DIR,
"innodb_temp_tablespaces_dir": MYSQL_TEMP_DIR,
"innodb_undo_directory": MYSQL_LOGS_DIR,
"log_bin": f"{MYSQL_LOGS_DIR}/binlog",
"log_bin_index": f"{MYSQL_LOGS_DIR}/binlog.index",
"log_error_services": "log_filter_internal;log_sink_internal",
"log_error": f"{logging_path}/error.log",
"log_error": f"{MYSQL_LOGS_DIR}/error.log",
"general_log": "OFF",
"general_log_file": f"{logging_path}/general.log",
"general_log_file": f"{MYSQL_LOGS_DIR}/general.log",
"loose-group_replication_paxos_single_leader": "ON",
"slow_query_log_file": f"{logging_path}/slow.log",
"slow_query_log_file": f"{MYSQL_LOGS_DIR}/slow.log",
"binlog_expire_logs_seconds": f"{binlog_retention_seconds}",
"gtid_mode": "ON",
"enforce_gtid_consistency": "ON",
Expand All @@ -1187,9 +1194,10 @@ def render_mysqld_configuration( # noqa: C901
"loose-validate_password.policy": "MEDIUM",
"loose-validate_password.special_char_count": 0,
}
config["mysqld"] = base_config # ty:ignore[invalid-assignment]

if audit_log_enabled:
config["mysqld"]["loose-audit_log_filter.file"] = f"{logging_path}/audit.log"
config["mysqld"]["loose-audit_log_filter.file"] = f"{MYSQL_LOGS_DIR}/audit.log"
config["mysqld"]["loose-audit_log_filter.format"] = "JSON"
config["mysqld"]["loose-audit_log_filter.policy"] = audit_log_policy.upper()
if audit_log_strategy == "async":
Expand Down Expand Up @@ -2712,8 +2720,9 @@ def empty_data_files(
mysql_data_directory: str,
user: str | None = None,
group: str | None = None,
extra_dirs: list[str] | None = None,
) -> None:
"""Empty the mysql data directory in preparation of backup restore."""
"""Empty the mysql data directories in preparation of backup restore."""
empty_data_files_command = [
"find",
mysql_data_directory,
Expand All @@ -2733,11 +2742,26 @@ def empty_data_files(
user=user,
group=group,
)

for extra_dir in (extra_dirs or []):
logger.debug(f"Emptying extra directory {extra_dir}")
self._execute_commands(
[
"find",
extra_dir,
"-not",
"-path",
extra_dir,
"-delete",
],
user=user,
group=group,
)
except MySQLExecError as e:
logger.error("Failed to empty data directory in prep for backup restore")
logger.error("Failed to empty data directories in prep for backup restore")
raise MySQLEmptyDataDirectoryError(e.message) from e
except Exception as e:
logger.error("Failed to empty data directory in prep for backup restore")
logger.error("Failed to empty data directories in prep for backup restore")
raise MySQLEmptyDataDirectoryError from e

def restore_backup(
Expand Down
23 changes: 19 additions & 4 deletions kubernetes/metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ docs: https://canonical-charmed-mysql-k8s.readthedocs-hosted.com/
source: https://github.com/canonical/mysql-operators
issues: https://github.com/canonical/mysql-operators/issues
website:
- https://ubuntu.com/data/mysql
- https://canonical.com/data/mysql
- https://charmhub.io/mysql-k8s
- https://github.com/canonical/mysql-operators
maintainers:
Expand All @@ -27,8 +27,14 @@ containers:
gid: 584788
resource: mysql-image
mounts:
- storage: database
location: /var/lib/mysql
- storage: archive
location: /var/lib/mysql/archive
- storage: data
location: /var/lib/mysql/data
- storage: logs
location: /var/lib/mysql/logs
- storage: temp
location: /var/lib/mysql/temp

resources:
mysql-image:
Expand Down Expand Up @@ -81,9 +87,18 @@ requires:
optional: true

storage:
database:
archive:
type: filesystem
description: Persistent storage for rotated logs and other archival purposes
data:
type: filesystem
description: Persistent storage for MySQL data
temp:
type: filesystem
description: Persistent storage for InnoDB temporary tablespaces
logs:
type: filesystem
description: Persistent storage for MySQL error logs, general query logs, slow query logs, binary logs, redo logs and undo logs

assumes:
- k8s-api
Expand Down
13 changes: 8 additions & 5 deletions kubernetes/src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
MONITORING_PASSWORD_KEY,
MONITORING_USERNAME,
MYSQL_BINLOGS_COLLECTOR_SERVICE,
MYSQL_DATA_DIR,
MYSQL_LOG_ERROR,
MYSQL_LOG_FILES,
MYSQL_LOG_SERVICE,
Expand Down Expand Up @@ -128,9 +129,11 @@ def __init__(self, *args):
self.framework.observe(self.on.leader_elected, self._on_leader_elected)
self.framework.observe(self.on.config_changed, self._on_config_changed)
self.framework.observe(self.on.update_status, self._on_update_status)
self.framework.observe(
self.on.database_storage_detaching, self._on_database_storage_detaching
)

self.framework.observe(self.on.archive_storage_detaching, self._on_storage_detaching)
Comment thread
paulomach marked this conversation as resolved.
self.framework.observe(self.on.data_storage_detaching, self._on_storage_detaching)
self.framework.observe(self.on.logs_storage_detaching, self._on_storage_detaching)
self.framework.observe(self.on.temp_storage_detaching, self._on_storage_detaching)

self.framework.observe(self.on[PEER].relation_joined, self._on_peer_relation_joined)
self.framework.observe(self.on[PEER].relation_changed, self._on_peer_relation_changed)
Expand Down Expand Up @@ -227,7 +230,7 @@ def _pebble_layer(self) -> Layer:
mysqld_cmd = [
MYSQLD_LOCATION,
"--basedir=/usr",
"--datadir=/var/lib/mysql",
f"--datadir={MYSQL_DATA_DIR}",
"--plugin-dir=/usr/lib/mysql/plugin",
f"--log-error={MYSQL_LOG_ERROR}",
f"--pid-file={self.unit_label}.pid",
Expand Down Expand Up @@ -1064,7 +1067,7 @@ def _on_peer_relation_departed(self, event: RelationDepartedEvent) -> None:
if not self._mysql.reconcile_binlogs_collection(force_restart=True):
logger.error("Failed to reconcile binlogs collection during peer departed event")

def _on_database_storage_detaching(self, _) -> None:
def _on_storage_detaching(self, _) -> None:
"""Handle the database storage detaching event."""
# Only executes if the unit was initialised
if not self.unit_initialized():
Expand Down
12 changes: 7 additions & 5 deletions kubernetes/src/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,18 @@
TLS_SSL_CERT_FILE = "custom-server-cert.pem"
MYSQL_CLI_LOCATION = "/usr/bin/mysql"
MYSQLSH_LOCATION = "/usr/bin/mysqlsh"
MYSQL_DATA_DIR = "/var/lib/mysql"
MYSQL_ARCHIVE_DIR = "/var/lib/mysql/archive" # Corresponds to the archive storage mount
MYSQL_DATA_DIR = "/var/lib/mysql/data" # Corresponds to the data storage mount
MYSQL_LOGS_DIR = "/var/lib/mysql/logs" # Corresponds to the logs storage mount
MYSQL_TEMP_DIR = "/var/lib/mysql/temp" # Corresponds to the temp storage mount
MYSQLD_SOCK_FILE = "/var/run/mysqld/mysqld.sock"
MYSQLD_CONFIG_FILE = "/etc/mysql/mysql.conf.d/z-custom.cnf"
MYSQLD_INIT_CONFIG_FILE = "/etc/mysql/mysql.conf.d/z-custom-init-file.cnf"
MYSQL_LOG_DIR = "/var/log/mysql"
MYSQL_LOG_ERROR = f"{MYSQL_LOG_DIR}/error.log"
MYSQL_LOG_ERROR = f"{MYSQL_LOGS_DIR}/error.log"
MYSQL_LOG_FILES = [
MYSQL_LOG_ERROR,
f"{MYSQL_LOG_DIR}/audit.log",
f"{MYSQL_LOG_DIR}/general.log",
f"{MYSQL_LOGS_DIR}/audit.log",
f"{MYSQL_LOGS_DIR}/general.log",
]
MYSQL_SYSTEM_USER = "mysql"
MYSQL_SYSTEM_GROUP = "mysql"
Expand Down
25 changes: 19 additions & 6 deletions kubernetes/src/mysql_k8s_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,14 @@
CHARMED_MYSQL_XTRABACKUP_LOCATION,
CONTAINER_NAME,
LOG_ROTATE_CONFIG_FILE,
MYSQL_ARCHIVE_DIR,
MYSQL_BINLOGS_COLLECTOR_SERVICE,
MYSQL_DATA_DIR,
MYSQL_LOG_DIR,
MYSQL_LOG_ERROR,
MYSQL_LOGS_DIR,
MYSQL_SYSTEM_GROUP,
MYSQL_SYSTEM_USER,
MYSQL_TEMP_DIR,
MYSQLD_DEFAULTS_CONFIG_FILE,
MYSQLD_INIT_CONFIG_FILE,
MYSQLD_LOCATION,
Expand Down Expand Up @@ -184,6 +186,14 @@ def initialise_mysqld(self) -> None:
"--initialize",
"-u",
MYSQL_SYSTEM_USER,
"--datadir",
MYSQL_DATA_DIR,
"--innodb-log-group-home-dir",
MYSQL_LOGS_DIR,
"--innodb-undo-directory",
MYSQL_LOGS_DIR,
"--innodb-temp-tablespaces-dir",
MYSQL_TEMP_DIR,
]

try:
Expand Down Expand Up @@ -277,7 +287,8 @@ def setup_logrotate_config(
rendered = template.render(
system_user=MYSQL_SYSTEM_USER,
system_group=MYSQL_SYSTEM_GROUP,
log_dir=MYSQL_LOG_DIR,
log_dir=MYSQL_LOGS_DIR,
archive_dir=MYSQL_ARCHIVE_DIR,
logs_retention_period=logs_retention_period,
logs_rotations=logs_rotations,
logs_compression_enabled=logs_compression,
Expand Down Expand Up @@ -368,6 +379,7 @@ def empty_data_files(self) -> None:
MYSQL_DATA_DIR,
user=MYSQL_SYSTEM_USER,
group=MYSQL_SYSTEM_GROUP,
extra_dirs=[MYSQL_TEMP_DIR, MYSQL_LOGS_DIR],
)

def restore_backup(self, backup_location: str) -> tuple[str, str]:
Expand Down Expand Up @@ -577,8 +589,8 @@ def is_data_dir_initialised(self) -> bool:

# minimal expected content for an integral mysqld data-dir
expected_content = {
"#innodb_redo",
"#innodb_temp",
# "#innodb_redo", # stored separately
# "#innodb_temp", # stored separately
"auto.cnf",
"ca-key.pem",
"ca.pem",
Expand All @@ -593,9 +605,10 @@ def is_data_dir_initialised(self) -> bool:
"server-cert.pem",
"server-key.pem",
"sys",
"undo_001",
"undo_002",
# "undo_001", # stored separately
# "undo_002", # stored separately
}
logger.debug("mysql data dir contents: %s", content_set)

return expected_content <= content_set
except (ExecError, APIError):
Expand Down
7 changes: 5 additions & 2 deletions kubernetes/templates/logrotate.j2
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ ifempty
missingok
nomail
nosharedscripts
nocopytruncate
# Needed because rotated logs might be in a different filesystem,
# `nocopytruncate` fails with `failed to rename ...: Invalid cross-device link`
copy
copytruncate

{% for log in enabled_log_files %}
{{ log_dir }}/{{ log }}.log {
olddir archive_{{ log }}
olddir {{ archive_dir }}/archive_{{ log }}
}
{% endfor %}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
wait_fixed,
)

from constants import CONTAINER_NAME, MYSQL_LOG_DIR
from constants import CONTAINER_NAME, MYSQL_ARCHIVE_DIR, MYSQL_LOGS_DIR

from ... import architecture
from ...helpers_ha import (
Expand Down Expand Up @@ -76,7 +76,7 @@ def test_deploy_highly_available_cluster(juju: Juju, charm: str) -> None:

def test_log_rotation(juju: Juju) -> None:
"""Test the log rotation of text files."""
log_types = ["error", "audit"]
log_types = ["audit", "error"]

mysql_app_leader = get_app_leader(juju, MYSQL_APP_NAME)
mysql_app_leader_label = get_mysql_instance_label(mysql_app_leader)
Expand All @@ -94,20 +94,22 @@ def test_log_rotation(juju: Juju) -> None:
stop_log_rotate_dispatcher(juju, mysql_app_leader)

for log_type in log_types:
archive_log_dir = f"{MYSQL_ARCHIVE_DIR}/archive_{log_type}"
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed

logging.info("Removing existing archive directories")
delete_unit_file(
juju=juju,
unit_name=mysql_app_leader,
container=CONTAINER_NAME,
file_path=f"{MYSQL_LOG_DIR}/archive_{log_type}",
file_path=archive_log_dir,
)

logging.info("Writing some data to the text log files")
write_unit_file(
juju=juju,
unit_name=mysql_app_leader,
container=CONTAINER_NAME,
file_path=f"{MYSQL_LOG_DIR}/{log_type}.log",
file_path=f"{MYSQL_LOGS_DIR}/{log_type}.log",
file_data=f"{log_type} content",
)

Expand All @@ -120,11 +122,11 @@ def test_log_rotation(juju: Juju) -> None:
juju=juju,
unit_name=mysql_app_leader,
container=CONTAINER_NAME,
file_path=f"{MYSQL_LOG_DIR}/{log_type}.log",
file_path=f"{MYSQL_LOGS_DIR}/{log_type}.log",
)
assert f"{log_type} content" not in active_log_file_data

archive_log_dir = f"{MYSQL_LOG_DIR}/archive_{log_type}"
archive_log_dir = f"{MYSQL_ARCHIVE_DIR}/archive_{log_type}"
archive_log_files_listed = list_unit_files(
juju=juju,
unit_name=mysql_app_leader,
Expand Down
Loading
Loading