Configure mysqlrouter.conf sections based on wildcards
Upon bootstrap, the mysqlrouter will create a mysqlrouter.conf file for
the cluster it is connecting to. This creates sections such as
metadata_cache:<cluster_name>, routing:<cluster_name>_rw,
routing:<cluster_name>_ro, etc. The cluster name is not provided on the
mysql-router interface so this information is not available in
determining the correct section name. Since the mysql-router is designed
to work with a single cluster, the need to update the interface which in
turn requires the user to update a number of deployed charms in the
environment, an approach is taken to allow regular expressions to be
used when matching the section name.
There is some risk to this in that it requires that future edits
carefully consider the possible section names when future sections are
added. However, this developer cost is traded off in order to ease the
burden of operators.
For the upgrade scenario, this patch also checks to see if the file
rendered on disk contains multiple 'metadata_cache' sections, and if so
rewrites the mysqlrouter.conf file with the hardcoded
metadata_cache:jujuCluster section removed.
Closes-Bug: #1927981
Change-Id: Iad44744ad01c0b6429fbafb041e6fc11887dbfb9
(cherry picked from commit 5a2da18002)
This commit is contained in:
committed by
Alex Kavanagh
parent
161d9c86e6
commit
3bbcaa0b37
@@ -15,6 +15,7 @@
|
|||||||
import configparser
|
import configparser
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import tenacity
|
import tenacity
|
||||||
|
|
||||||
@@ -38,6 +39,15 @@ MYSQL_ROUTER_STARTED = "charm.mysqlrouter.started"
|
|||||||
DB_ROUTER_AVAILABLE = "db-router.available"
|
DB_ROUTER_AVAILABLE = "db-router.available"
|
||||||
DB_ROUTER_PROXY_AVAILABLE = "db-router.available.proxy"
|
DB_ROUTER_PROXY_AVAILABLE = "db-router.available.proxy"
|
||||||
|
|
||||||
|
# Section configuration search keys for setting configuration values
|
||||||
|
# Note, mysql object names can contain alphanumeric and $ characters.
|
||||||
|
DEFAULT_SECTION = 'DEFAULT'
|
||||||
|
METADATA_CACHE_SECTION = r'metadata_cache:[\w$]+$'
|
||||||
|
ROUTING_RW_SECTION = r'routing:[\w$]+_rw(?<!_x_rw)$'
|
||||||
|
ROUTING_RO_SECTION = r'routing:[\w$]+_ro(?<!_x_ro)$'
|
||||||
|
ROUTING_X_RO_SECTION = r'routing:[\w$]+_x_ro$'
|
||||||
|
ROUTING_X_RW_SECTION = r'routing:[\w$]+_x_rw$'
|
||||||
|
|
||||||
|
|
||||||
@charms_openstack.adapters.config_property
|
@charms_openstack.adapters.config_property
|
||||||
def db_router_address(cls):
|
def db_router_address(cls):
|
||||||
@@ -319,6 +329,28 @@ class MySQLRouterCharm(charms_openstack.charm.OpenStackCharm):
|
|||||||
perms=0o644,
|
perms=0o644,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def upgrade_charm(self):
|
||||||
|
"""Custom upgrade charm function to handle special upgrade logic."""
|
||||||
|
# Bug 1927981 - For mysql-innodb-clusters which were deployed with a
|
||||||
|
# cluster name which was not 'jujuCluster', an extra section to the
|
||||||
|
# mysqrouter.conf file was written which causes the mysql router
|
||||||
|
# service to fail to start. Remove the extraneous section at charm
|
||||||
|
# upgrade time.
|
||||||
|
config = configparser.ConfigParser()
|
||||||
|
config.read(self.mysqlrouter_conf)
|
||||||
|
sections = list(filter(lambda x: x.startswith('metadata_cache'),
|
||||||
|
config.sections()))
|
||||||
|
if len(sections) > 1 and 'metadata_cache:jujuCluster' in sections:
|
||||||
|
ch_core.hookenv.log('Found multiple metadata_cache sections. '
|
||||||
|
'Removing hard-coded '
|
||||||
|
'metadata_cache:jujuCluster section', 'INFO')
|
||||||
|
config.remove_section('metadata_cache:jujuCluster')
|
||||||
|
ch_core.hookenv.log("Writing {}".format(self.mysqlrouter_conf))
|
||||||
|
with open(self.mysqlrouter_conf, 'w') as configfile:
|
||||||
|
config.write(configfile)
|
||||||
|
|
||||||
|
super(MySQLRouterCharm, self).upgrade_charm()
|
||||||
|
|
||||||
def get_db_helper(self):
|
def get_db_helper(self):
|
||||||
"""Get an instance of the MySQLDB8Helper class.
|
"""Get an instance of the MySQLDB8Helper class.
|
||||||
|
|
||||||
@@ -643,6 +675,13 @@ class MySQLRouterCharm(charms_openstack.charm.OpenStackCharm):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mysqlrouter.conf file will have some headings which are targeted at
|
||||||
|
the mysql cluster name. Headings specified in the parameters may be a
|
||||||
|
regular expression for matching mysqlrouter configuration sections
|
||||||
|
which have variable information included in the name. Capturing
|
||||||
|
groups are not supported as the check is just used to see if the
|
||||||
|
section matches the regular expression.
|
||||||
|
|
||||||
:param parameters: Dictionary of parameters
|
:param parameters: Dictionary of parameters
|
||||||
:type parameters: dict
|
:type parameters: dict
|
||||||
:side effect: Writes the mysqlrouter.conf file
|
:side effect: Writes the mysqlrouter.conf file
|
||||||
@@ -651,14 +690,23 @@ class MySQLRouterCharm(charms_openstack.charm.OpenStackCharm):
|
|||||||
"""
|
"""
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
config.read(self.mysqlrouter_conf)
|
config.read(self.mysqlrouter_conf)
|
||||||
for heading in parameters.keys():
|
|
||||||
for param in parameters[heading].keys():
|
for heading, settings in parameters.items():
|
||||||
|
for section in config.sections():
|
||||||
|
if re.match(heading, section):
|
||||||
|
translated = section
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
translated = heading
|
||||||
|
|
||||||
|
for param, value in settings.items():
|
||||||
# BUG LP#1927981 - heading may not exist during a charm upgrade
|
# BUG LP#1927981 - heading may not exist during a charm upgrade
|
||||||
# Handle missing heading via direct assignment in except.
|
# Handle missing heading via direct assignment in except.
|
||||||
try:
|
try:
|
||||||
config[heading][param] = parameters[heading][param]
|
config[translated][param] = value
|
||||||
except KeyError:
|
except KeyError:
|
||||||
config[heading] = {param: parameters[heading][param]}
|
config[translated] = {param: value}
|
||||||
|
|
||||||
ch_core.hookenv.log("Writing {}".format(
|
ch_core.hookenv.log("Writing {}".format(
|
||||||
self.mysqlrouter_conf))
|
self.mysqlrouter_conf))
|
||||||
with open(self.mysqlrouter_conf, 'w') as configfile:
|
with open(self.mysqlrouter_conf, 'w') as configfile:
|
||||||
@@ -683,13 +731,13 @@ class MySQLRouterCharm(charms_openstack.charm.OpenStackCharm):
|
|||||||
return
|
return
|
||||||
|
|
||||||
_parameters = {
|
_parameters = {
|
||||||
"metadata_cache:jujuCluster": {
|
METADATA_CACHE_SECTION: {
|
||||||
"ttl": str(self.options.ttl),
|
"ttl": str(self.options.ttl),
|
||||||
"auth_cache_ttl": str(self.options.auth_cache_ttl),
|
"auth_cache_ttl": str(self.options.auth_cache_ttl),
|
||||||
"auth_cache_refresh_interval":
|
"auth_cache_refresh_interval":
|
||||||
str(self.options.auth_cache_refresh_interval),
|
str(self.options.auth_cache_refresh_interval),
|
||||||
},
|
},
|
||||||
"DEFAULT": {
|
DEFAULT_SECTION: {
|
||||||
"pid_file": self.mysqlrouter_pid_file,
|
"pid_file": self.mysqlrouter_pid_file,
|
||||||
"max_connections": str(self.options.max_connections),
|
"max_connections": str(self.options.max_connections),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ applications:
|
|||||||
num_units: 3
|
num_units: 3
|
||||||
options:
|
options:
|
||||||
source: *openstack-origin
|
source: *openstack-origin
|
||||||
|
cluster-name: foo
|
||||||
channel: latest/edge
|
channel: latest/edge
|
||||||
keystone:
|
keystone:
|
||||||
charm: ch:keystone
|
charm: ch:keystone
|
||||||
|
|||||||
@@ -57,6 +57,21 @@ class FakeException(Exception):
|
|||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
class FakeConfigParser(dict):
|
||||||
|
|
||||||
|
def read(*args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def write(*args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def sections(self):
|
||||||
|
return self.keys()
|
||||||
|
|
||||||
|
def remove_section(self, section):
|
||||||
|
self.pop(section, None)
|
||||||
|
|
||||||
|
|
||||||
class TestMySQLRouterCharm(test_utils.PatchHelper):
|
class TestMySQLRouterCharm(test_utils.PatchHelper):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -663,13 +678,6 @@ class TestMySQLRouterCharm(test_utils.PatchHelper):
|
|||||||
|
|
||||||
def test_update_config_parameters_missing_heading(self):
|
def test_update_config_parameters_missing_heading(self):
|
||||||
# test fix for Bug LP#1927981
|
# test fix for Bug LP#1927981
|
||||||
class FakeConfigParser(dict):
|
|
||||||
def read(*args, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def write(*args, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
current_config = {"DEFAULT": {"client_ssl_mode": "NONE"}}
|
current_config = {"DEFAULT": {"client_ssl_mode": "NONE"}}
|
||||||
fake_config = FakeConfigParser(current_config)
|
fake_config = FakeConfigParser(current_config)
|
||||||
|
|
||||||
@@ -690,6 +698,54 @@ class TestMySQLRouterCharm(test_utils.PatchHelper):
|
|||||||
self.assertEqual(fake_config['metadata_cache:jujuCluster'],
|
self.assertEqual(fake_config['metadata_cache:jujuCluster'],
|
||||||
{"thing": "a-thing"})
|
{"thing": "a-thing"})
|
||||||
|
|
||||||
|
def test_update_config_parameters_regex(self):
|
||||||
|
# test fix for Bug LP#1927981
|
||||||
|
current_config = {
|
||||||
|
"DEFAULT": {"client_ssl_mode": "NONE"},
|
||||||
|
"metadata_cache:foo": {
|
||||||
|
"ttl": '5',
|
||||||
|
"auth_cache_ttl": '-1',
|
||||||
|
"auth_cache_refresh_interval": '2',
|
||||||
|
},
|
||||||
|
"routing:foo_x_rw": {
|
||||||
|
"test": 'yes',
|
||||||
|
},
|
||||||
|
"routing:foo_rw": {
|
||||||
|
"test": 'no',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fake_config = FakeConfigParser(current_config)
|
||||||
|
|
||||||
|
self.patch_object(mysql_router.configparser, "ConfigParser",
|
||||||
|
return_value=fake_config)
|
||||||
|
|
||||||
|
# metadata_cache:jujuCluster didn't exist in the previous config so the
|
||||||
|
# header needs to be created (c.f. BUG LP#1927981)
|
||||||
|
_params = {
|
||||||
|
"DEFAULT": {"client_ssl_mode": "PREFERRED"},
|
||||||
|
mysql_router.METADATA_CACHE_SECTION: {"thing": "a-thing"},
|
||||||
|
mysql_router.ROUTING_RW_SECTION: {
|
||||||
|
"test": True,
|
||||||
|
},
|
||||||
|
mysql_router.ROUTING_X_RW_SECTION: {
|
||||||
|
"test": False,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mrc = mysql_router.MySQLRouterCharm()
|
||||||
|
# should not throw a key error.
|
||||||
|
mrc.update_config_parameters(_params)
|
||||||
|
self.assertIn('metadata_cache:foo', fake_config)
|
||||||
|
self.assertNotIn(mysql_router.METADATA_CACHE_SECTION, fake_config)
|
||||||
|
self.assertEqual(fake_config['metadata_cache:foo'],
|
||||||
|
{"thing": "a-thing", "ttl": '5',
|
||||||
|
"auth_cache_ttl": '-1',
|
||||||
|
"auth_cache_refresh_interval": '2'})
|
||||||
|
self.assertEqual(fake_config['routing:foo_x_rw'],
|
||||||
|
{"test": False})
|
||||||
|
self.assertEqual(fake_config['routing:foo_rw'],
|
||||||
|
{"test": True})
|
||||||
|
|
||||||
def test_config_changed(self):
|
def test_config_changed(self):
|
||||||
_config_data = {
|
_config_data = {
|
||||||
"ttl": '5',
|
"ttl": '5',
|
||||||
@@ -715,8 +771,8 @@ class TestMySQLRouterCharm(test_utils.PatchHelper):
|
|||||||
_metadata_config = _config_data.copy()
|
_metadata_config = _config_data.copy()
|
||||||
_metadata_config.pop('max_connections')
|
_metadata_config.pop('max_connections')
|
||||||
_params = {
|
_params = {
|
||||||
'metadata_cache:jujuCluster': _metadata_config,
|
mysql_router.METADATA_CACHE_SECTION: _metadata_config,
|
||||||
'DEFAULT': {
|
mysql_router.DEFAULT_SECTION: {
|
||||||
'client_ssl_mode': "PASSTHROUGH",
|
'client_ssl_mode': "PASSTHROUGH",
|
||||||
'max_connections': _config_data['max_connections'],
|
'max_connections': _config_data['max_connections'],
|
||||||
'pid_file': '/run/mysql/mysqlrouter-foobar.pid'
|
'pid_file': '/run/mysql/mysqlrouter-foobar.pid'
|
||||||
@@ -755,3 +811,29 @@ class TestMySQLRouterCharm(test_utils.PatchHelper):
|
|||||||
self.service_stop.assert_called_once_with(self.service_name)
|
self.service_stop.assert_called_once_with(self.service_name)
|
||||||
self.service_start.assert_called_once_with(self.service_name)
|
self.service_start.assert_called_once_with(self.service_name)
|
||||||
_mock_check_mysql_connection.assert_called_once()
|
_mock_check_mysql_connection.assert_called_once()
|
||||||
|
|
||||||
|
def test_upgrade_charm_lp1927981(self):
|
||||||
|
# test fix for Bug LP#1927981
|
||||||
|
current_config = {
|
||||||
|
"DEFAULT": {"client_ssl_mode": "NONE"},
|
||||||
|
"metadata_cache:foo": {
|
||||||
|
"ttl": '5',
|
||||||
|
"auth_cache_ttl": '-1',
|
||||||
|
"auth_cache_refresh_interval": '2',
|
||||||
|
},
|
||||||
|
"metadata_cache:jujuCluster": {
|
||||||
|
"ttl": '5',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
fake_config = FakeConfigParser(current_config)
|
||||||
|
|
||||||
|
self.patch_object(mysql_router.charms_openstack.charm.OpenStackCharm,
|
||||||
|
'upgrade_charm')
|
||||||
|
self.patch_object(mysql_router.configparser, "ConfigParser",
|
||||||
|
return_value=fake_config)
|
||||||
|
|
||||||
|
mrc = mysql_router.MySQLRouterCharm()
|
||||||
|
# should not throw a key error.
|
||||||
|
mrc.upgrade_charm()
|
||||||
|
self.assertIn('metadata_cache:foo', fake_config)
|
||||||
|
self.assertNotIn('metadata_cache:.jujuCluster', fake_config)
|
||||||
|
|||||||
Reference in New Issue
Block a user