diff --git a/src/actions.yaml b/src/actions.yaml new file mode 100644 index 0000000..9a7f3bb --- /dev/null +++ b/src/actions.yaml @@ -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 diff --git a/src/actions/__init__.py b/src/actions/__init__.py new file mode 100644 index 0000000..5705e5d --- /dev/null +++ b/src/actions/__init__.py @@ -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. diff --git a/src/actions/actions.py b/src/actions/actions.py new file mode 100755 index 0000000..b76cede --- /dev/null +++ b/src/actions/actions.py @@ -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)) diff --git a/src/actions/restart-mysqlrouter b/src/actions/restart-mysqlrouter new file mode 120000 index 0000000..405a394 --- /dev/null +++ b/src/actions/restart-mysqlrouter @@ -0,0 +1 @@ +actions.py \ No newline at end of file diff --git a/src/actions/start-mysqlrouter b/src/actions/start-mysqlrouter new file mode 120000 index 0000000..405a394 --- /dev/null +++ b/src/actions/start-mysqlrouter @@ -0,0 +1 @@ +actions.py \ No newline at end of file diff --git a/src/actions/stop-mysqlrouter b/src/actions/stop-mysqlrouter new file mode 120000 index 0000000..405a394 --- /dev/null +++ b/src/actions/stop-mysqlrouter @@ -0,0 +1 @@ +actions.py \ No newline at end of file diff --git a/src/files/mysqlrouter.service b/src/files/mysqlrouter.service new file mode 100644 index 0000000..3ad4a77 --- /dev/null +++ b/src/files/mysqlrouter.service @@ -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 diff --git a/src/lib/__init__.py b/src/lib/__init__.py new file mode 100644 index 0000000..5705e5d --- /dev/null +++ b/src/lib/__init__.py @@ -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. diff --git a/src/lib/charm/__init__.py b/src/lib/charm/__init__.py new file mode 100644 index 0000000..5705e5d --- /dev/null +++ b/src/lib/charm/__init__.py @@ -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. diff --git a/src/lib/charm/openstack/__init__.py b/src/lib/charm/openstack/__init__.py new file mode 100644 index 0000000..5705e5d --- /dev/null +++ b/src/lib/charm/openstack/__init__.py @@ -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. diff --git a/src/lib/charm/mysql_router.py b/src/lib/charm/openstack/mysql_router.py similarity index 91% rename from src/lib/charm/mysql_router.py rename to src/lib/charm/openstack/mysql_router.py index 35d5e77..48a1aa8 100644 --- a/src/lib/charm/mysql_router.py +++ b/src/lib/charm/openstack/mysql_router.py @@ -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): diff --git a/src/reactive/mysql_router_handlers.py b/src/reactive/mysql_router_handlers.py index ff73205..07ba2ff 100644 --- a/src/reactive/mysql_router_handlers.py +++ b/src/reactive/mysql_router_handlers.py @@ -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() diff --git a/unit_tests/test_lib_charm_openstack_mysql_router.py b/unit_tests/test_lib_charm_openstack_mysql_router.py index 91de1a5..e1cf872 100644 --- a/unit_tests/test_lib_charm_openstack_mysql_router.py +++ b/unit_tests/test_lib_charm_openstack_mysql_router.py @@ -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() diff --git a/unit_tests/test_mysql_router_handlers.py b/unit_tests/test_mysql_router_handlers.py index cac4523..b721fd4 100644 --- a/unit_tests/test_mysql_router_handlers.py +++ b/unit_tests/test_mysql_router_handlers.py @@ -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