Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DPE-2089 DPE-1685 DPE-2222 Use snap with ppa sources + other misc snap related improvements #249

Merged
merged 2 commits into from
Aug 4, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
36 changes: 31 additions & 5 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
ActionEvent,
CharmBase,
InstallEvent,
RelationBrokenEvent,
RelationChangedEvent,
RelationCreatedEvent,
StartEvent,
)
from ops.main import main
Expand All @@ -52,6 +54,7 @@
CHARMED_MYSQLD_SERVICE,
CLUSTER_ADMIN_PASSWORD_KEY,
CLUSTER_ADMIN_USERNAME,
COS_AGENT_RELATION_NAME,
GR_MAX_MEMBERS,
MONITORING_PASSWORD_KEY,
MONITORING_USERNAME,
Expand All @@ -70,7 +73,6 @@
MySQL,
MySQLCreateCustomMySQLDConfigError,
MySQLDataPurgeError,
MySQLExporterConnectError,
MySQLReconfigureError,
MySQLResetRootPasswordAndStartMySQLDError,
SnapServiceOperationError,
Expand Down Expand Up @@ -123,6 +125,12 @@ def __init__(self, *args):
logs_rules_dir="./src/alert_rules/loki",
log_slots=[f"{CHARMED_MYSQL_SNAP_NAME}:logs"],
)
self.framework.observe(
self.on[COS_AGENT_RELATION_NAME].relation_created, self._on_cos_agent_relation_created
)
self.framework.observe(
self.on[COS_AGENT_RELATION_NAME].relation_broken, self._on_cos_agent_relation_broken
)
self.s3_integrator = S3Requirer(self, S3_INTEGRATOR_RELATION_NAME)
self.backups = MySQLBackups(self, self.s3_integrator)
self.hostname_resolution = MySQLMachineHostnameResolution(self)
Expand Down Expand Up @@ -211,9 +219,6 @@ def _on_start(self, event: StartEvent) -> None:
except MySQLCreateCustomMySQLDConfigError:
self.unit.status = BlockedStatus("Failed to create custom mysqld config")
return
except MySQLExporterConnectError:
self.unit.status = BlockedStatus("Failed to connect to MySQL exporter")
return
except MySQLGetMySQLVersionError:
logger.debug("Fail to get MySQL version")

Expand Down Expand Up @@ -381,6 +386,28 @@ def _on_update_status(self, _) -> None:
# Set active status when primary is known
self.app.status = ActiveStatus()

def _on_cos_agent_relation_created(self, event: RelationCreatedEvent) -> None:
"""Handle the cos_agent relation created event.

Enable the mysqld-exporter snap service.
"""
if not self._is_peer_data_set:
logger.debug("Charm not yet set up. Deferring")
event.defer()
return

self._mysql.connect_mysql_exporter()

def _on_cos_agent_relation_broken(self, event: RelationBrokenEvent) -> None:
"""Handle the cos_agent relation broken event.

Disable the mysqld-exporter snap service.
"""
if not self._is_peer_data_set:
return

self._mysql.stop_mysql_exporter()

shayancanonical marked this conversation as resolved.
Show resolved Hide resolved
# =======================
# Custom Action Handlers
# =======================
Expand Down Expand Up @@ -581,7 +608,6 @@ def _workload_initialise(self) -> None:
self._mysql.configure_mysql_users()
self._mysql.configure_instance()
self._mysql.wait_until_mysql_connection()
self._mysql.connect_mysql_exporter()
self.unit_peer_data["unit-configured"] = "True"
self.unit_peer_data["instance-hostname"] = f"{instance_hostname()}:3306"
if workload_version := self._mysql.get_mysql_version():
Expand Down
3 changes: 2 additions & 1 deletion src/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
TLS_SSL_CERT_FILE = "custom-server-cert.pem"
MYSQL_EXPORTER_PORT = "9104"
CHARMED_MYSQL_SNAP_NAME = "charmed-mysql"
CHARMED_MYSQL_SNAP_REVISION = 51
CHARMED_MYSQL_SNAP_REVISION = 66 # MySQL v8.0.33
CHARMED_MYSQLD_EXPORTER_SERVICE = "mysqld-exporter"
CHARMED_MYSQLD_SERVICE = "mysqld"
CHARMED_MYSQL = "charmed-mysql.mysql"
Expand All @@ -52,3 +52,4 @@
ROOT_SYSTEM_USER = "root"
GR_MAX_MEMBERS = 9
HOSTNAME_DETAILS = "hostname-details"
COS_AGENT_RELATION_NAME = "cos-agent"
4 changes: 2 additions & 2 deletions src/hostname_resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ def _potentially_update_etc_hosts(self, _) -> None:
try:
self.charm._mysql.flush_host_cache()
except MySQLFlushHostCacheError:
self.unit.status = BlockedStatus("Unable to flush MySQL host cache")
self.charm.unit.status = BlockedStatus("Unable to flush MySQL host cache")

def _remove_host_from_etc_hosts(self, event: RelationDepartedEvent) -> None:
departing_unit_name = event.unit.name
Expand All @@ -152,4 +152,4 @@ def _remove_host_from_etc_hosts(self, event: RelationDepartedEvent) -> None:
try:
self.charm._mysql.flush_host_cache()
except MySQLFlushHostCacheError:
self.unit.status = BlockedStatus("Unable to flush MySQL host cache")
self.charm.unit.status = BlockedStatus("Unable to flush MySQL host cache")
42 changes: 28 additions & 14 deletions src/mysql_vm_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,15 +166,17 @@ def install_and_configure_mysql_dependencies() -> None:
mysqlsh_help_command = ["charmed-mysql.mysqlsh", "--help"]
subprocess.check_call(mysqlsh_help_command, stderr=subprocess.PIPE)

subprocess.run(["snap", "alias", "charmed-mysql.mysql", "mysql"], check=True)

installed_by_mysql_server_file.touch(exist_ok=True)
except subprocess.CalledProcessError as e:
logger.exception("Failed to execute subprocess command", exc_info=e)
except subprocess.CalledProcessError:
logger.exception("Failed to execute subprocess command")
raise
except (snap.SnapNotFoundError, snap.SnapError) as e:
logger.exception("Failed to install snaps", exc_info=e)
except (snap.SnapNotFoundError, snap.SnapError):
logger.exception("Failed to install snaps")
raise
except Exception as e:
logger.exception("Encountered an unexpected exception", exc_info=e)
except Exception:
logger.exception("Encountered an unexpected exception")
raise

def create_custom_mysqld_config(self, profile: str) -> None:
Expand Down Expand Up @@ -495,7 +497,7 @@ def start_mysqld(self) -> None:
)

try:
snap_service_operation(CHARMED_MYSQL_SNAP_NAME, CHARMED_MYSQLD_SERVICE, "start", True)
snap_service_operation(CHARMED_MYSQL_SNAP_NAME, CHARMED_MYSQLD_SERVICE, "start")
self.wait_until_mysql_connection()
except (
MySQLServiceNotRunningError,
Expand Down Expand Up @@ -539,11 +541,21 @@ def connect_mysql_exporter(self) -> None:
}
)
snap_service_operation(
CHARMED_MYSQL_SNAP_NAME, CHARMED_MYSQLD_EXPORTER_SERVICE, "start", True
CHARMED_MYSQL_SNAP_NAME, CHARMED_MYSQLD_EXPORTER_SERVICE, "start"
)
except snap.SnapError:
logger.exception("An exception occurred when setting up mysqld-exporter.")
raise MySQLExporterConnectError
raise MySQLExporterConnectError("Error setting up mysqld-exporter")

def stop_mysql_exporter(self) -> None:
"""Stop the mysqld exporter."""
try:
snap_service_operation(
CHARMED_MYSQL_SNAP_NAME, CHARMED_MYSQLD_EXPORTER_SERVICE, "stop"
)
shayancanonical marked this conversation as resolved.
Show resolved Hide resolved
except snap.SnapError:
logger.exception("An exception occurred when stopping mysqld-exporter")
raise MySQLExporterConnectError("Error stopping mysqld-exporter")

def _run_mysqlsh_script(self, script: str, timeout=None) -> str:
"""Execute a MySQL shell script.
Expand All @@ -561,11 +573,13 @@ def _run_mysqlsh_script(self, script: str, timeout=None) -> str:
_file.write(script)
_file.flush()

# Specify python as this is not the default in the deb version
# of the mysql-shell snap
command = [CHARMED_MYSQLSH, "--no-wizard", "--python", "-f", _file.name]

try:
# need to change permissions since charmed-mysql.mysqlsh runs as
# snap_daemon
shutil.chown(_file.name, user="snap_daemon", group="root")

return subprocess.check_output(
command, stderr=subprocess.PIPE, timeout=timeout
).decode("utf-8")
Expand Down Expand Up @@ -726,7 +740,7 @@ def instance_hostname():
return None


def snap_service_operation(snapname: str, service: str, operation: str, enable=False) -> bool:
def snap_service_operation(snapname: str, service: str, operation: str) -> bool:
"""Helper function to run an operation on a snap service.

Args:
Expand All @@ -752,10 +766,10 @@ def snap_service_operation(snapname: str, service: str, operation: str, enable=F
selected_snap.restart(services=[service])
return selected_snap.services[service]["active"]
elif operation == "start":
selected_snap.start(services=[service], enable=enable)
selected_snap.start(services=[service], enable=True)
return selected_snap.services[service]["active"]
else:
selected_snap.stop(services=[service])
selected_snap.stop(services=[service], disable=True)
return not selected_snap.services[service]["active"]
except snap.SnapError:
error_message = f"Failed to run snap service operation, snap={snapname}, service={service}, operation={operation}"
Expand Down
47 changes: 47 additions & 0 deletions tests/integration/high_availability/test_replication.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@


import logging
import time
from pathlib import Path

import pytest
import urllib3
import yaml
from pytest_operator.plugin import OpsTest
from tenacity import RetryError, Retrying, stop_after_attempt, wait_fixed

from ..helpers import (
cluster_name,
execute_queries_on_unit,
fetch_credentials,
generate_random_string,
get_primary_unit,
get_primary_unit_wrapper,
Expand Down Expand Up @@ -53,6 +56,50 @@ async def test_exporter_endpoints(ops_test: OpsTest, mysql_charm_series: str) ->
http = urllib3.PoolManager()

for unit in application.units:
_, output, _ = await ops_test.juju(
"ssh", unit.name, "sudo", "snap", "services", "charmed-mysql.mysqld-exporter"
)
assert output.split("\n")[1].split()[2] == "inactive"

return_code, _, _ = await ops_test.juju(
"ssh", unit.name, "sudo", "snap", "set", "charmed-mysql", "exporter.user=monitoring"
)
assert return_code == 0

monitoring_credentials = await fetch_credentials(unit, "monitoring")
return_code, _, _ = await ops_test.juju(
"ssh",
unit.name,
"sudo",
"snap",
"set",
"charmed-mysql",
f"exporter.password={monitoring_credentials['password']}",
)
assert return_code == 0

return_code, _, _ = await ops_test.juju(
"ssh", unit.name, "sudo", "snap", "start", "charmed-mysql.mysqld-exporter"
)
assert return_code == 0
carlcsaposs-canonical marked this conversation as resolved.
Show resolved Hide resolved

try:
for attempt in Retrying(stop=stop_after_attempt(45), wait=wait_fixed(2)):
with attempt:
_, output, _ = await ops_test.juju(
"ssh",
unit.name,
"sudo",
"snap",
"services",
"charmed-mysql.mysqld-exporter",
)
assert output.split("\n")[1].split()[2] == "active"
except RetryError:
raise Exception("Failed to start the mysqld-exporter snap service")

time.sleep(30)

unit_address = await unit.get_public_address()
mysql_exporter_url = f"http://{unit_address}:9104/metrics"

Expand Down
15 changes: 10 additions & 5 deletions tests/unit/test_mysqlsh_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,22 @@ def setUp(self):

@patch("tempfile.NamedTemporaryFile")
@patch("subprocess.check_output")
def test_run_mysqlsh_script(self, _check_output, _):
@patch("shutil.chown")
def test_run_mysqlsh_script(self, _chown, _check_output, _):
"""Test a successful execution of run_mysqlsh_script."""
_check_output.return_value = b"stdout"

self.mysql._run_mysqlsh_script("script")

_check_output.assert_called_once()
_chown.assert_called_once()

@patch("tempfile.NamedTemporaryFile")
@patch("subprocess.check_output")
def test_run_mysqlsh_script_exception(self, _check_output, _):
@patch("shutil.chown")
def test_run_mysqlsh_script_exception(self, _, _check_output, __):
"""Test a failed execution of run_mysqlsh_script."""
_check_output.side_effect = subprocess.CalledProcessError(cmd="", returncode=-1)
_check_output.side_effect = subprocess.CalledProcessError(cmd="", returncode=1)

with self.assertRaises(MySQLClientError):
self.mysql._run_mysqlsh_script("script")
Expand Down Expand Up @@ -405,7 +408,7 @@ def test_start_mysqld(
self.mysql.start_mysqld()

_snap_service_operation.assert_called_once_with(
CHARMED_MYSQL_SNAP_NAME, CHARMED_MYSQLD_SERVICE, "start", True
CHARMED_MYSQL_SNAP_NAME, CHARMED_MYSQLD_SERVICE, "start"
)
_wait_until_mysql_connection.assert_called_once()

Expand All @@ -430,9 +433,10 @@ def test_start_mysqld_failure(

@patch("pathlib.Path")
@patch("subprocess.check_call")
@patch("subprocess.run")
@patch("os.path.exists", return_value=True)
@patch("mysql_vm_helpers.snap.SnapCache")
def test_install_snap(self, _cache, _path_exists, _check_call, _pathlib):
def test_install_snap(self, _cache, _path_exists, _run, _check_call, _pathlib):
"""Test execution of install_snap()."""
_mysql_snap = MagicMock()
_cache.return_value = {CHARMED_MYSQL_SNAP_NAME: _mysql_snap}
Expand All @@ -443,3 +447,4 @@ def test_install_snap(self, _cache, _path_exists, _check_call, _pathlib):
self.mysql.install_and_configure_mysql_dependencies()

_check_call.assert_called_once_with(["charmed-mysql.mysqlsh", "--help"], stderr=-1)
_run.assert_called_once_with(["snap", "alias", "charmed-mysql.mysql", "mysql"], check=True)