Skip to content
Permalink

Comparing changes

This is a direct comparison between two commits made in this repository or its related repositories. View the default comparison for this range or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: canonical/mysql-router-operator
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 75bfa5148740b3e199518f76eaa117e35bfcfedc
Choose a base ref
..
head repository: canonical/mysql-router-operator
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: dac0cfbdd406ca07970a993549518eba3dec83ab
Choose a head ref
Showing with 89 additions and 39 deletions.
  1. +14 −10 src/charm.py
  2. +64 −21 src/mysql_shell.py
  3. +1 −1 src/relations/database_provides.py
  4. +9 −7 src/snap.py
  5. +1 −0 src/socket_workload.py
24 changes: 14 additions & 10 deletions src/charm.py
Original file line number Diff line number Diff line change
@@ -9,12 +9,14 @@
import logging
import socket

import charms.operator_libs_linux.v2.snap as snap
import charms.operator_libs_linux.v2.snap as snap_lib
import ops
import tenacity

import relations.database_provides
import relations.database_requires
import snap
import socket_workload
import workload

logger = logging.getLogger(__name__)
@@ -37,17 +39,19 @@ def __init__(self, *args) -> None:

def get_workload(self, *, event):
"""MySQL Router workload"""
container = snap.Snap()
if connection_info := self.database_requires.get_connection_info(event=event):
return workload.AuthenticatedWorkload(
_connection_info=connection_info,
_charm=self,
return socket_workload.AuthenticatedSocketWorkload(
container_=container,
connection_info=connection_info,
charm_=self,
)
return workload.Workload()
return socket_workload.SocketWorkload(container_=container)

@property
def _endpoint(self) -> str:
"""K8s endpoint for MySQL Router"""
# TODO: socket file
# TODO: remove
# Example: mysql-router-k8s.my-model.svc.cluster.local
return "foo"
# return f"{self.app.name}.{self.model_service_domain}"
@@ -156,21 +160,21 @@ def _on_install(self, _) -> None:
# TODO set workload version
_SNAP_NAME = "charmed-mysql"
_SNAP_REVISION = "51"
mysql_snap = snap.SnapCache()[_SNAP_NAME]
mysql_snap = snap_lib.SnapCache()[_SNAP_NAME]
if mysql_snap.present:
logger.error(f"{_SNAP_NAME} snap already installed on machine. Installation aborted")
raise Exception(f"Multiple {_SNAP_NAME} snap installs not supported on one machine")
logger.debug(f"Installing {_SNAP_NAME=}, {_SNAP_REVISION=}")
# TODO: set status
# TODO catch/retry on error?
mysql_snap.ensure(snap.SnapState.Present, revision=_SNAP_REVISION)
mysql_snap.ensure(snap_lib.SnapState.Present, revision=_SNAP_REVISION)
logger.debug(f"Installed {_SNAP_NAME=}, {_SNAP_REVISION=}")
self.unit.set_workload_version(self.get_workload(event=None).version)

def _on_remove(self, _) -> None:
_SNAP_NAME = "charmed-mysql"
mysql_snap = snap.SnapCache()[_SNAP_NAME]
mysql_snap.ensure(snap.SnapState.Absent)
mysql_snap = snap_lib.SnapCache()[_SNAP_NAME]
mysql_snap.ensure(snap_lib.SnapState.Absent)

def _on_start(self, _) -> None:
# Set status on first start if no relations active
85 changes: 64 additions & 21 deletions src/mysql_shell.py
Original file line number Diff line number Diff line change
@@ -9,59 +9,60 @@
import dataclasses
import json
import logging
import pathlib
import secrets
import string
import subprocess
import typing

import container

_PASSWORD_LENGTH = 24
logger = logging.getLogger(__name__)


# TODO python3.10 min version: Add `(kw_only=True)`
@dataclasses.dataclass
class RouterUserInformation:
"""MySQL Router user information"""

username: str
router_id: str


# TODO python3.10 min version: Add `(kw_only=True)`
@dataclasses.dataclass
class Shell:
"""MySQL Shell connected to MySQL cluster"""

_container: container.Container
username: str
_password: str
_host: str
_port: str

_TEMPORARY_SCRIPT_FILE = pathlib.Path("/tmp/snap-private-tmp/snap.charmed-mysql/tmp/script.py")

def _run_commands(self, commands: list[str]) -> None:
def _run_commands(self, commands: list[str]) -> str:
"""Connect to MySQL cluster and run commands."""
# Redact password from log
logged_commands = commands.copy()
# Password is still logged on user creation
# TODO: Password is still logged on user creation
logged_commands.insert(
0, f"shell.connect('{self.username}:***@{self._host}:{self._port}')"
)

commands.insert(
0, f"shell.connect('{self.username}:{self._password}@{self._host}:{self._port}')"
)
with open(self._TEMPORARY_SCRIPT_FILE, "w") as file:
file.write("\n".join(commands))
temporary_script_file = self._container.path("/tmp/script.py")
temporary_script_file.write_text("\n".join(commands))
try:
subprocess.run(
[
"charmed-mysql.mysqlsh",
"--no-wizard",
"--python",
"--file",
"/tmp/script.py",
],
capture_output=True,
check=True,
encoding="utf-8",
output = self._container.run_mysql_shell(
["--no-wizard", "--python", "--file", temporary_script_file]
)
except subprocess.CalledProcessError as e:
except container.CalledProcessError as e:
logger.exception(f"Failed to run {logged_commands=}\nstderr:\n{e.stderr}\n")
raise
finally:
self._TEMPORARY_SCRIPT_FILE.unlink()
temporary_script_file.unlink()
return output

def _run_sql(self, sql_statements: list[str]) -> None:
"""Connect to MySQL cluster and execute SQL."""
@@ -114,6 +115,48 @@ def add_attributes_to_mysql_router_user(
self._run_sql([f"ALTER USER `{username}` ATTRIBUTE '{attributes}'"])
logger.debug(f"Added {attributes=} to {username=}")

def get_mysql_router_user_for_unit(
self, unit_name: str
) -> typing.Optional[RouterUserInformation]:
"""Get MySQL Router user created by a previous instance of the unit.
Get username & router ID attribute.
Before container restart, the charm does not have an opportunity to delete the MySQL
Router user or cluster metadata created during MySQL Router bootstrap. After container
restart, the user and cluster metadata should be deleted before bootstrapping MySQL Router
again.
"""
logger.debug(f"Getting MySQL Router user for {unit_name=}")
rows = json.loads(
self._run_commands(
[
f"result = session.run_sql(\"SELECT USER, ATTRIBUTE->>'$.router_id' FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE ATTRIBUTE->'$.created_by_user'='{self.username}' AND ATTRIBUTE->'$.created_by_juju_unit'='{unit_name}'\")",
"print(result.fetch_all())",
]
)
)
if not rows:
logger.debug(f"No MySQL Router user found for {unit_name=}")
return
assert len(rows) == 1
username, router_id = rows[0]
user_info = RouterUserInformation(username=username, router_id=router_id)
logger.debug(f"MySQL Router user found for {unit_name=}: {user_info}")
return user_info

def remove_router_from_cluster_metadata(self, router_id: str) -> None:
"""Remove MySQL Router from InnoDB Cluster metadata.
On container restart, MySQL Router bootstrap will fail without `--force` if cluster
metadata already exists for the router ID.
"""
logger.debug(f"Removing {router_id=} from cluster metadata")
self._run_commands(
["cluster = dba.get_cluster()", f'cluster.remove_router_metadata("{router_id}")']
)
logger.debug(f"Removed {router_id=} from cluster metadata")

def delete_user(self, username: str) -> None:
"""Delete user."""
logger.debug(f"Deleting {username=}")
2 changes: 1 addition & 1 deletion src/relations/database_provides.py
Original file line number Diff line number Diff line change
@@ -75,7 +75,7 @@ def __init__(
def _set_databag(self, *, username: str, password: str, router_endpoint: str) -> None:
"""Share connection information with application charm."""
# TODO: remove `file://`?
# TODO: https://dev.mysql.com/doc/refman/8.0/en/connecting-using-uri-or-key-value-pairs.html#connecting-using-uri
# TODO: get socket path from variable
read_write_endpoint = (
"file:///var/snap/charmed-mysql/common/var/run/mysqlrouter/mysql.sock"
)
16 changes: 9 additions & 7 deletions src/snap.py
Original file line number Diff line number Diff line change
@@ -6,7 +6,8 @@

import container

_UNIX_USERNAME = "foo"
_UNIX_USERNAME = None # TODO
_SNAP_NAME = "charmed-mysql"


class _SnapPath(pathlib.PosixPath):
@@ -15,9 +16,11 @@ def __new__(cls, *args, **kwargs):
if str(path).startswith("/etc/mysqlrouter") or str(path).startswith(
"/var/lib/mysqlrouter"
):
parent = "/var/snap/charmed-mysql/current"
parent = f"/var/snap/{_SNAP_NAME}/current"
elif str(path).startswith("/run"):
parent = "/var/snap/charmed-mysql/common"
parent = f"/var/snap/{_SNAP_NAME}/common"
elif str(path).startswith("/tmp"):
parent = f"/tmp/snap-private-tmp/snap.{_SNAP_NAME}"
else:
return path
assert str(path).startswith("/")
@@ -44,22 +47,21 @@ def rmtree(self):

class Snap(container.Container):
UNIX_USERNAME = _UNIX_USERNAME
_SNAP_NAME = "charmed-mysql"
_SNAP_REVISION = "51"
_SERVICE_NAME = "mysqlrouter-service"

def __init__(self) -> None:
super().__init__(
mysql_router_command=f"{self._SNAP_NAME}.mysqlrouter",
mysql_shell_command=f"{self._SNAP_NAME}.mysqlsh",
mysql_router_command=f"{_SNAP_NAME}.mysqlrouter",
mysql_shell_command=f"{_SNAP_NAME}.mysqlsh",
)

def ready(self) -> bool:
return True

@property
def _snap(self) -> snap_lib.Snap:
return snap_lib.SnapCache()[self._SNAP_NAME]
return snap_lib.SnapCache()[_SNAP_NAME]

@property
def mysql_router_service_enabled(self) -> bool:
1 change: 1 addition & 0 deletions src/socket_workload.py
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@
import workload


# TODO: rename to Workload?
class SocketWorkload(workload.Workload):
pass