Use systemd to start/stop mysqlrouter

Systemd control of mysqlrouter
Actions for start/stop/restart

Change-Id: I05b0a5a5f3bab75f3c9c8ff88e12aa3e8ecb9ebb
This commit is contained in:
David Ames 2020-01-06 16:00:06 -08:00
parent 454f6e9f8b
commit 357070620c
14 changed files with 254 additions and 84 deletions

9
src/actions.yaml Normal file
View File

@ -0,0 +1,9 @@
stop-mysqlrouter:
description: |
Stop the mysqlrouter daemon
start-mysqlrouter:
description: |
Start the mysqlrouter daemon
restart-mysqlrouter:
description: |
Restart the mysqlrouter daemon

13
src/actions/__init__.py Normal file
View File

@ -0,0 +1,13 @@
# Copyright 2019 Canonical Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

142
src/actions/actions.py Executable file
View File

@ -0,0 +1,142 @@
#!/usr/local/sbin/charm-env python3
# Copyright 2019 Canonical Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import subprocess
import sys
import traceback
# Load modules from $CHARM_DIR/lib
_path = os.path.dirname(os.path.realpath(__file__))
_lib = os.path.abspath(os.path.join(_path, "../lib"))
_reactive = os.path.abspath(os.path.join(_path, "../reactive"))
def _add_path(path):
if path not in sys.path:
sys.path.insert(1, path)
_add_path(_lib)
_add_path(_reactive)
import charms_openstack.charm as charm
import charmhelpers.core as ch_core
import charms_openstack.bus
charms_openstack.bus.discover()
def stop_mysqlrouter(args):
"""Display cluster status
Return cluster.status() as a JSON encoded dictionary
:param args: sys.argv
:type args: sys.argv
:side effect: Calls instance.get_cluster_status
:returns: This function is called for its side effect
:rtype: None
:action return: Dictionary with command output
"""
with charm.provide_charm_instance() as instance:
try:
instance.stop_mysqlrouter()
instance.assess_status()
ch_core.hookenv.action_set({"outcome": "Success"})
except subprocess.CalledProcessError as e:
ch_core.hookenv.action_set({
"output": e.output,
"return-code": e.returncode,
"traceback": traceback.format_exc()})
ch_core.hookenv.action_fail("Stop MySQLRouter failed.")
def start_mysqlrouter(args):
"""Display cluster status
Return cluster.status() as a JSON encoded dictionary
:param args: sys.argv
:type args: sys.argv
:side effect: Calls instance.get_cluster_status
:returns: This function is called for its side effect
:rtype: None
:action return: Dictionary with command output
"""
with charm.provide_charm_instance() as instance:
try:
instance.start_mysqlrouter()
instance.assess_status()
ch_core.hookenv.action_set({"outcome": "Success"})
except subprocess.CalledProcessError as e:
ch_core.hookenv.action_set({
"output": e.output,
"return-code": e.returncode,
"traceback": traceback.format_exc()})
ch_core.hookenv.action_fail("Start MySQLRouter failed.")
def restart_mysqlrouter(args):
"""Display cluster status
Return cluster.status() as a JSON encoded dictionary
:param args: sys.argv
:type args: sys.argv
:side effect: Calls instance.get_cluster_status
:returns: This function is called for its side effect
:rtype: None
:action return: Dictionary with command output
"""
with charm.provide_charm_instance() as instance:
try:
instance.restart_mysqlrouter()
instance.assess_status()
ch_core.hookenv.action_set({"outcome": "Success"})
except subprocess.CalledProcessError as e:
ch_core.hookenv.action_set({
"output": e.output,
"return-code": e.returncode,
"traceback": traceback.format_exc()})
ch_core.hookenv.action_fail("Retart MySQLRouter failed.")
# A dictionary of all the defined actions to callables (which take
# parsed arguments).
ACTIONS = {"stop-mysqlrouter": stop_mysqlrouter,
"start-mysqlrouter": start_mysqlrouter,
"restart-mysqlrouter": restart_mysqlrouter}
def main(args):
action_name = os.path.basename(args[0])
try:
action = ACTIONS[action_name]
except KeyError:
return "Action {} undefined".format(action_name)
else:
try:
action(args)
except Exception as e:
ch_core.hookenv.action_set({
"output": e.output.decode("UTF-8"),
"return-code": e.returncode,
"traceback": traceback.format_exc()})
ch_core.hookenv.action_fail(
"{} action failed.".format(action_name))
if __name__ == "__main__":
sys.exit(main(sys.argv))

View File

@ -0,0 +1 @@
actions.py

View File

@ -0,0 +1 @@
actions.py

View File

@ -0,0 +1 @@
actions.py

View File

@ -0,0 +1,15 @@
# MySQL Router systemd service file
[Unit]
Description=MySQL Router
After=network.target
[Service]
Type=exec
ExecStart=/var/lib/mysql/mysqlrouter/start.sh
ExecStop=/var/lib/mysql/mysqlrouter/stop.sh
RemainAfterExit=yes
Restart=on-failure
[Install]
WantedBy=multi-user.target

13
src/lib/__init__.py Normal file
View File

@ -0,0 +1,13 @@
# Copyright 2019 Canonical Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

13
src/lib/charm/__init__.py Normal file
View File

@ -0,0 +1,13 @@
# Copyright 2019 Canonical Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

View File

@ -0,0 +1,13 @@
# Copyright 2019 Canonical Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

View File

@ -14,6 +14,7 @@
import json
import os
import shutil
import subprocess
import charms_openstack.charm
@ -27,7 +28,7 @@ import charmhelpers.contrib.network.ip as ch_net_ip
import charmhelpers.contrib.database.mysql as mysql
MYSQLD_CNF = "/etc/mysql/mysql.conf.d/mysqld.cnf"
MYSQLROUTER_CNF = "/var/lib/mysql/mysqlrouter/mysqlrouter.conf"
# Flag Strings
MYSQL_ROUTER_BOOTSTRAPPED = "charm.mysqlrouter.bootstrapped"
@ -58,12 +59,10 @@ class MySQLRouterCharm(charms_openstack.charm.OpenStackCharm):
required_relations = ["db-router", "shared-db"]
source_config_key = "source"
# FIXME Can we have a non-systemd services?
# Create a systemd shim?
# services = ["mysqlrouter"]
services = []
# TODO Post bootstrap config management and restarts
restart_map = {}
services = ["mysqlrouter"]
restart_map = {
MYSQLROUTER_CNF: services,
}
# TODO Pick group owner
group = "mysql"
@ -236,6 +235,15 @@ class MySQLRouterCharm(charms_openstack.charm.OpenStackCharm):
group=self.mysqlrouter_group,
perms=0o755)
# Systemd File
_systemd_filename = "mysqlrouter.service"
_src_systemd = os.path.join(
ch_core.hookenv.charm_dir(), "files", _systemd_filename)
_dst_systemd = os.path.join("/etc/systemd/system", _systemd_filename)
shutil.copy(_src_systemd, _dst_systemd)
cmd = ["/usr/bin/systemctl", "enable", "mysqlrouter"]
subprocess.check_output(cmd, stderr=subprocess.STDOUT)
def get_db_helper(self):
"""Get an instance of the MySQLDB8Helper class.
@ -377,19 +385,7 @@ class MySQLRouterCharm(charms_openstack.charm.OpenStackCharm):
:returns: This function is called for its side effect
:rtype: None
"""
cmd = ["{}/start.sh".format(self.mysqlrouter_working_dir)]
try:
proc = subprocess.Popen(
cmd, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, bufsize=1,
universal_newlines=True)
proc.wait()
ch_core.hookenv.log("MySQL router started", "DEBUG")
except subprocess.CalledProcessError as e:
ch_core.hookenv.log(
"Failed to start mysqlrouter: {}"
.format(e.output.decode("UTF-8")), "ERROR")
return
ch_core.host.service_start("mysqlrouter")
reactive.flags.set_flag(MYSQL_ROUTER_STARTED)
def stop_mysqlrouter(self):
@ -403,20 +399,7 @@ class MySQLRouterCharm(charms_openstack.charm.OpenStackCharm):
:returns: This function is called for its side effect
:rtype: None
"""
cmd = ["{}/stop.sh".format(self.mysqlrouter_working_dir)]
try:
proc = subprocess.Popen(
cmd, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, bufsize=1,
universal_newlines=True)
proc.wait()
ch_core.hookenv.log("MySQL router stopped", "DEBUG")
except subprocess.CalledProcessError as e:
ch_core.hookenv.log(
"Failed to start mysqlrouter: {}"
.format(e.output.decode("UTF-8")), "ERROR")
return
reactive.flags.clear_flag(MYSQL_ROUTER_STARTED)
ch_core.host.service_stop("mysqlrouter")
def restart_mysqlrouter(self):
"""Restart MySQL Router.
@ -430,8 +413,7 @@ class MySQLRouterCharm(charms_openstack.charm.OpenStackCharm):
:returns: This function is called for its side effect
:rtype: None
"""
self.stop_mysqlrouter()
self.start_mysqlrouter()
ch_core.host.service_restart("mysqlrouter")
def proxy_db_and_user_requests(
self, receiving_interface, sending_interface):

View File

@ -3,7 +3,7 @@ import charms.reactive as reactive
import charms_openstack.bus
import charms_openstack.charm as charm
import charm.mysql_router as mysql_router # noqa
import charm.openstack.mysql_router as mysql_router # noqa
charms_openstack.bus.discover()

View File

@ -19,7 +19,7 @@ import mock
import charms_openstack.test_utils as test_utils
import charm.mysql_router as mysql_router
import charm.openstack.mysql_router as mysql_router
class TestMySQLRouterProperties(test_utils.PatchHelper):
@ -244,6 +244,7 @@ class TestMySQLRouterCharm(test_utils.PatchHelper):
self.patch_object(
mysql_router.charms_openstack.charm.OpenStackCharm,
"install", "super_install")
self.patch_object(mysql_router.shutil, "copy")
self.os.path.exists.return_value = False
self.group_exists.return_value = False
self.user_exists.return_value = False
@ -259,6 +260,11 @@ class TestMySQLRouterCharm(test_utils.PatchHelper):
self.mkdir.assert_called_once_with(
"/var/lib/mysql", group="mysql", owner="mysql", perms=0o755)
self.copy.assert_called_once()
self.subprocess.check_output.assert_called_once_with(
['/usr/bin/systemctl', 'enable', 'mysqlrouter'],
stderr=self.subprocess.STDOUT)
def test_get_db_helper(self):
self.patch_object(
mysql_router.mysql, "MySQL8Helper")
@ -387,66 +393,27 @@ class TestMySQLRouterCharm(test_utils.PatchHelper):
self.set_flag.assert_not_called()
def test_start_mysqlrouter(self):
_user = "mysql"
_port = "3006"
self.patch_object(mysql_router.ch_core.host, "service_start")
mrc = mysql_router.MySQLRouterCharm()
mrc.options.system_user = _user
mrc.options.base_port = _port
# Successful
mrc.start_mysqlrouter()
self.subprocess.Popen.assert_called_once_with(
["/var/lib/mysql/mysqlrouter/start.sh"],
bufsize=1,
stdout=self.stdout,
stderr=self.stdout,
universal_newlines=True)
self.service_start.assert_called_once_with("mysqlrouter")
self.set_flag.assert_called_once_with(
mysql_router.MYSQL_ROUTER_STARTED)
# Fail
self.subprocess.reset_mock()
self.set_flag.reset_mock()
self.subprocess.CalledProcessError = FakeException
self.subprocess.Popen.side_effect = self.subprocess.CalledProcessError
mrc.start_mysqlrouter()
self.set_flag.assert_not_called()
def test_stop_mysqlrouter(self):
_user = "ubuntu"
_port = "3306"
self.patch_object(mysql_router.ch_core.host, "service_stop")
mrc = mysql_router.MySQLRouterCharm()
mrc.options.system_user = _user
mrc.options.base_port = _port
# Successful
mrc.stop_mysqlrouter()
self.subprocess.Popen.assert_called_once_with(
["/var/lib/mysql/mysqlrouter/stop.sh"],
bufsize=1,
stdout=self.stdout,
stderr=self.stdout,
universal_newlines=True)
self.clear_flag.assert_called_once_with(
mysql_router.MYSQL_ROUTER_STARTED)
# Fail
self.subprocess.reset_mock()
self.clear_flag.reset_mock()
self.subprocess.CalledProcessError = FakeException
self.subprocess.Popen.side_effect = self.subprocess.CalledProcessError
mrc.stop_mysqlrouter()
self.clear_flag.assert_not_called()
self.service_stop.assert_called_once_with("mysqlrouter")
def test_restart_mysqlrouter(self):
mrc = mysql_router.MySQLRouterCharm()
mrc.stop_mysqlrouter = mock.MagicMock()
mrc.start_mysqlrouter = mock.MagicMock()
self.patch_object(mysql_router.ch_core.host, "service_restart")
mrc.restart_mysqlrouter()
mrc.stop_mysqlrouter.assert_called_once()
mrc.start_mysqlrouter.assert_called_once()
self.service_restart.assert_called_once_with("mysqlrouter")
def test_proxy_db_and_user_requests_no_prefix(self):
mrc = mysql_router.MySQLRouterCharm()

View File

@ -14,7 +14,7 @@
import mock
import charm.mysql_router as mysql_router
import charm.openstack.mysql_router as mysql_router
import reactive.mysql_router_handlers as handlers
import charms_openstack.test_utils as test_utils