From 403216591530f6330aa75c583e988471dac1948a Mon Sep 17 00:00:00 2001 From: David Ames Date: Mon, 4 May 2020 14:39:20 -0700 Subject: [PATCH] Allow for multiple instantiations of mysql-router Before this change it was not possible to run more than one instantiation of mysql-router on a single unit. The Nova cells deployments require nova-cloud-controller to be connected to two different databases, therefore, requiring two instantiations of mysql-router. This change includes all the necessary steps to run multiple instantiations of mysql-router on a single unit. Review and merge charm-helpers PR: https://github.com/juju/charm-helpers/pull/469 Closes-Bug: #1876188 Depends-On: Iafcc106fca44479e89b4b66a0a3988ffeee01f04 Change-Id: I76d1bfdccf9a551188c9f86c06ee0a22c07d47cf --- src/lib/charm/openstack/mysql_router.py | 47 +++++++++++------ src/{files => templates}/.gitkeep | 0 src/{files => templates}/mysqlrouter.service | 4 +- .../test_lib_charm_openstack_mysql_router.py | 51 ++++++++++++------- 4 files changed, 67 insertions(+), 35 deletions(-) rename src/{files => templates}/.gitkeep (100%) rename src/{files => templates}/mysqlrouter.service (58%) diff --git a/src/lib/charm/openstack/mysql_router.py b/src/lib/charm/openstack/mysql_router.py index 3ab7a47..e222385 100644 --- a/src/lib/charm/openstack/mysql_router.py +++ b/src/lib/charm/openstack/mysql_router.py @@ -14,7 +14,6 @@ import json import os -import shutil import subprocess import charms_openstack.charm @@ -27,6 +26,8 @@ import charmhelpers.contrib.network.ip as ch_net_ip import charmhelpers.contrib.database.mysql as mysql +import charmhelpers.contrib.openstack.templating as os_templating + MYSQLROUTER_CNF = "/var/lib/mysql/mysqlrouter/mysqlrouter.conf" @@ -52,14 +53,18 @@ def shared_db_address(cls): class MySQLRouterCharm(charms_openstack.charm.OpenStackCharm): """Charm class for the MySQLRouter charm.""" - name = "mysqlrouter" + name = ch_core.hookenv.service_name() packages = ["mysql-router"] release = "stein" release_pkg = "mysql-router" required_relations = ["db-router", "shared-db"] source_config_key = "source" - services = ["mysqlrouter"] + systemd_file = os.path.join( + "/etc/systemd/system", + "{}.service".format(name)) + + services = [name] restart_map = { MYSQLROUTER_CNF: services, } @@ -170,6 +175,10 @@ class MySQLRouterCharm(charms_openstack.charm.OpenStackCharm): """ return self.options.shared_db_address + @property + def mysqlrouter_port(self): + return self.options.base_port + @property def mysqlrouter_working_dir(self): """Determine the path to the mysqlrouter working directory. @@ -179,7 +188,7 @@ class MySQLRouterCharm(charms_openstack.charm.OpenStackCharm): :returns: Path to the directory :rtype: str """ - return "{}/mysqlrouter".format(self.mysqlrouter_home_dir) + return "{}/{}".format(self.mysqlrouter_home_dir, self.name) @property def mysqlrouter_home_dir(self): @@ -236,12 +245,16 @@ class MySQLRouterCharm(charms_openstack.charm.OpenStackCharm): 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"] + ch_core.templating.render( + source="mysqlrouter.service", + template_loader=os_templating.get_loader( + 'templates/', self.release), + target=self.systemd_file, + context=self.adapters_instance, + group=self.group, + perms=0o755, + ) + cmd = ["/usr/bin/systemctl", "enable", self.name] subprocess.check_output(cmd, stderr=subprocess.STDOUT) def get_db_helper(self): @@ -305,7 +318,8 @@ class MySQLRouterCharm(charms_openstack.charm.OpenStackCharm): try: m_helper.connect(self.db_router_user, self.db_router_password, - self.shared_db_address) + self.shared_db_address, + port=self.mysqlrouter_port) return True except mysql.MySQLdb._exceptions.OperationalError: ch_core.hookenv.log("Could not connect to db", "DEBUG") @@ -363,7 +377,7 @@ class MySQLRouterCharm(charms_openstack.charm.OpenStackCharm): self.cluster_address), "--directory", self.mysqlrouter_working_dir, "--conf-use-sockets", - "--conf-base-port", str(self.options.base_port)] + "--conf-base-port", str(self.mysqlrouter_port)] try: output = subprocess.check_output(cmd, stderr=subprocess.STDOUT) ch_core.hookenv.log(output, "DEBUG") @@ -385,7 +399,7 @@ class MySQLRouterCharm(charms_openstack.charm.OpenStackCharm): :returns: This function is called for its side effect :rtype: None """ - ch_core.host.service_start("mysqlrouter") + ch_core.host.service_start(self.name) reactive.flags.set_flag(MYSQL_ROUTER_STARTED) def stop_mysqlrouter(self): @@ -399,7 +413,7 @@ class MySQLRouterCharm(charms_openstack.charm.OpenStackCharm): :returns: This function is called for its side effect :rtype: None """ - ch_core.host.service_stop("mysqlrouter") + ch_core.host.service_stop(self.name) def restart_mysqlrouter(self): """Restart MySQL Router. @@ -413,7 +427,7 @@ class MySQLRouterCharm(charms_openstack.charm.OpenStackCharm): :returns: This function is called for its side effect :rtype: None """ - ch_core.host.service_restart("mysqlrouter") + ch_core.host.service_restart(self.name) def proxy_db_and_user_requests( self, receiving_interface, sending_interface): @@ -492,4 +506,5 @@ class MySQLRouterCharm(charms_openstack.charm.OpenStackCharm): _password, allowed_units=_allowed_hosts, prefix=prefix, - wait_timeout=_wait_timeout) + wait_timeout=_wait_timeout, + db_port=self.mysqlrouter_port) diff --git a/src/files/.gitkeep b/src/templates/.gitkeep similarity index 100% rename from src/files/.gitkeep rename to src/templates/.gitkeep diff --git a/src/files/mysqlrouter.service b/src/templates/mysqlrouter.service similarity index 58% rename from src/files/mysqlrouter.service rename to src/templates/mysqlrouter.service index 3ad4a77..9b6b549 100644 --- a/src/files/mysqlrouter.service +++ b/src/templates/mysqlrouter.service @@ -6,8 +6,8 @@ After=network.target [Service] Type=exec -ExecStart=/var/lib/mysql/mysqlrouter/start.sh -ExecStop=/var/lib/mysql/mysqlrouter/stop.sh +ExecStart=/var/lib/mysql/{{ options.charm_instance.name }}/start.sh +ExecStop=/var/lib/mysql/{{ options.charm_instance.name }}/stop.sh RemainAfterExit=yes Restart=on-failure diff --git a/unit_tests/test_lib_charm_openstack_mysql_router.py b/unit_tests/test_lib_charm_openstack_mysql_router.py index c8fe72f..56a06d5 100644 --- a/unit_tests/test_lib_charm_openstack_mysql_router.py +++ b/unit_tests/test_lib_charm_openstack_mysql_router.py @@ -218,9 +218,11 @@ class TestMySQLRouterCharm(test_utils.PatchHelper): def test_mysqlrouter_working_dir(self): mrc = mysql_router.MySQLRouterCharm() + _name = "keystone-mysql-router" + mrc.name = _name self.assertEqual( mrc.mysqlrouter_working_dir, - "/var/lib/mysql/mysqlrouter") + "/var/lib/mysql/{}".format(_name)) def test_mysqlrouter_home_dir(self): mrc = mysql_router.MySQLRouterCharm() @@ -244,12 +246,14 @@ class TestMySQLRouterCharm(test_utils.PatchHelper): self.patch_object( mysql_router.charms_openstack.charm.OpenStackCharm, "install", "super_install") - self.patch_object(mysql_router.shutil, "copy") + _name = "keystone-mysql-router" + self.patch_object(mysql_router.ch_core.templating, "render") self.os.path.exists.return_value = False self.group_exists.return_value = False self.user_exists.return_value = False mrc = mysql_router.MySQLRouterCharm() mrc.configure_source = mock.MagicMock() + mrc.name = _name mrc.install() self.super_install.assert_called_once() mrc.configure_source.assert_called_once() @@ -260,9 +264,9 @@ 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.render.assert_called_once() self.subprocess.check_output.assert_called_once_with( - ['/usr/bin/systemctl', 'enable', 'mysqlrouter'], + ['/usr/bin/systemctl', 'enable', _name], stderr=self.subprocess.STDOUT) def test_get_db_helper(self): @@ -304,6 +308,7 @@ class TestMySQLRouterCharm(test_utils.PatchHelper): _pass = "clusterpass" _user = "mysqlrouteruser" _addr = "127.0.0.1" + _port = 3316 self.endpoint_from_flag.return_value = self.db_router self.db_router.password.return_value = _json_pass @@ -312,20 +317,21 @@ class TestMySQLRouterCharm(test_utils.PatchHelper): self._exceptions.OperationalError = Exception _helper = mock.MagicMock() mrc = mysql_router.MySQLRouterCharm() + mrc.options.base_port = _port mrc.get_db_helper = mock.MagicMock() mrc.get_db_helper.return_value = _helper # Connects self.assertTrue(mrc.check_mysql_connection()) _helper.connect.assert_called_once_with( - _user, _pass, _addr) + _user, _pass, _addr, port=_port) # Fails _helper.reset_mock() _helper.connect.side_effect = self._exceptions.OperationalError self.assertFalse(mrc.check_mysql_connection()) _helper.connect.assert_called_once_with( - _user, _pass, _addr) + _user, _pass, _addr, port=_port) def test_custom_assess_status_check(self): _check = mock.MagicMock() @@ -394,26 +400,32 @@ class TestMySQLRouterCharm(test_utils.PatchHelper): def test_start_mysqlrouter(self): self.patch_object(mysql_router.ch_core.host, "service_start") + _name = "keystone-mysql-router" mrc = mysql_router.MySQLRouterCharm() + mrc.name = _name mrc.start_mysqlrouter() - self.service_start.assert_called_once_with("mysqlrouter") + self.service_start.assert_called_once_with(_name) self.set_flag.assert_called_once_with( mysql_router.MYSQL_ROUTER_STARTED) def test_stop_mysqlrouter(self): + _name = "keystone-mysql-router" self.patch_object(mysql_router.ch_core.host, "service_stop") mrc = mysql_router.MySQLRouterCharm() + mrc.name = _name mrc.stop_mysqlrouter() - self.service_stop.assert_called_once_with("mysqlrouter") + self.service_stop.assert_called_once_with(_name) def test_restart_mysqlrouter(self): + _name = "keystone-mysql-router" mrc = mysql_router.MySQLRouterCharm() + mrc.name = _name self.patch_object(mysql_router.ch_core.host, "service_restart") mrc.restart_mysqlrouter() - self.service_restart.assert_called_once_with("mysqlrouter") + self.service_restart.assert_called_once_with(_name) def test_proxy_db_and_user_requests_no_prefix(self): mrc = mysql_router.MySQLRouterCharm() @@ -440,10 +452,12 @@ class TestMySQLRouterCharm(test_utils.PatchHelper): _json_pass = '"pass"' _pass = json.loads(_json_pass) _local_unit = "kmr/5" + _port = 3316 self.db_router.password.return_value = _json_pass self.local_unit.return_value = _local_unit mrc = mysql_router.MySQLRouterCharm() + mrc.options.base_port = _port self.db_router.get_prefixes.return_value = [ mrc._unprefixed, mrc.db_prefix] @@ -455,7 +469,8 @@ class TestMySQLRouterCharm(test_utils.PatchHelper): self.db_router, self.keystone_shared_db) self.keystone_shared_db.set_db_connection_info.assert_called_once_with( self.keystone_shared_db.relation_id, mrc.shared_db_address, - _pass, allowed_units=None, prefix=None, wait_timeout=None) + _pass, allowed_units=None, prefix=None, wait_timeout=None, + db_port=_port) # Allowed Units and wait time set correctly self.db_router.wait_timeout.return_value = _json_wait_time @@ -467,7 +482,7 @@ class TestMySQLRouterCharm(test_utils.PatchHelper): self.keystone_shared_db.set_db_connection_info.assert_called_once_with( self.keystone_shared_db.relation_id, mrc.shared_db_address, _pass, allowed_units=self.keystone_unit_name, prefix=None, - wait_timeout=_wait_time) + wait_timeout=_wait_time, db_port=_port) # Confirm msyqlrouter credentials are not sent over the shared-db # relation @@ -483,10 +498,12 @@ class TestMySQLRouterCharm(test_utils.PatchHelper): _nova = "nova" _novaapi = "novaapi" _novacell0 = "novacell0" + _port = 3316 self.db_router.password.return_value = _json_pass self.local_unit.return_value = _local_unit mrc = mysql_router.MySQLRouterCharm() + mrc.options.base_port = _port self.db_router.get_prefixes.return_value = [ mrc.db_prefix, _nova, _novaapi, _novacell0] @@ -498,15 +515,15 @@ class TestMySQLRouterCharm(test_utils.PatchHelper): mock.call( self.nova_shared_db.relation_id, mrc.shared_db_address, _pass, allowed_units=None, prefix=_nova, - wait_timeout=None), + wait_timeout=None, db_port=_port), mock.call( self.nova_shared_db.relation_id, mrc.shared_db_address, _pass, allowed_units=None, prefix=_novaapi, - wait_timeout=None), + wait_timeout=None, db_port=_port), mock.call( self.nova_shared_db.relation_id, mrc.shared_db_address, _pass, allowed_units=None, prefix=_novacell0, - wait_timeout=None), + wait_timeout=None, db_port=_port), ] self.nova_shared_db.set_db_connection_info.assert_has_calls( _calls, any_order=True) @@ -520,15 +537,15 @@ class TestMySQLRouterCharm(test_utils.PatchHelper): mock.call( self.nova_shared_db.relation_id, mrc.shared_db_address, _pass, allowed_units=self.nova_unit_name, prefix=_nova, - wait_timeout=_wait_time), + wait_timeout=_wait_time, db_port=_port), mock.call( self.nova_shared_db.relation_id, mrc.shared_db_address, _pass, allowed_units=self.nova_unit_name, prefix=_novaapi, - wait_timeout=_wait_time), + wait_timeout=_wait_time, db_port=_port), mock.call( self.nova_shared_db.relation_id, mrc.shared_db_address, _pass, allowed_units=self.nova_unit_name, prefix=_novacell0, - wait_timeout=_wait_time), + wait_timeout=_wait_time, db_port=_port), ] self.nova_shared_db.set_db_connection_info.assert_has_calls( _calls, any_order=True)