2282 lines
94 KiB
Python
2282 lines
94 KiB
Python
# 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 copy
|
|
import collections
|
|
import re
|
|
import subprocess
|
|
from unittest import mock
|
|
|
|
import charms_openstack.test_utils as test_utils
|
|
|
|
import charm.openstack.mysql_innodb_cluster as mysql_innodb_cluster
|
|
import charm.openstack.exceptions as exceptions
|
|
|
|
|
|
class FakeException(Exception):
|
|
|
|
def __init__(self, code, message):
|
|
self.code = code
|
|
self.message = message
|
|
|
|
|
|
class TestMySQLInnoDBClusterUtils(test_utils.PatchHelper):
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.cls = mock.MagicMock()
|
|
|
|
def test_make_cluster_instance_configured_key(self):
|
|
_addr = "10.10.10.10"
|
|
_expect = (
|
|
"cluster-instance-configured-{}"
|
|
.format(_addr.replace(".", "-")))
|
|
self.assertEqual(
|
|
_expect,
|
|
mysql_innodb_cluster.make_cluster_instance_configured_key(_addr))
|
|
|
|
def test_make_cluster_instance_clustered_key(self):
|
|
_addr = "10.10.10.10"
|
|
_expect = (
|
|
"cluster-instance-clustered-{}"
|
|
.format(_addr.replace(".", "-")))
|
|
self.assertEqual(
|
|
_expect,
|
|
mysql_innodb_cluster.make_cluster_instance_clustered_key(_addr))
|
|
|
|
|
|
class TestMySQLInnoDBClusterProperties(test_utils.PatchHelper):
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.cls = mock.MagicMock()
|
|
self.patch_object(mysql_innodb_cluster.ch_core.hookenv, "local_unit")
|
|
self.patch_object(mysql_innodb_cluster.ch_net_ip, "get_relation_ip")
|
|
|
|
def test_server_id(self):
|
|
self.local_unit.return_value = "unit/5"
|
|
self.assertEqual(mysql_innodb_cluster.server_id(self.cls), "1005")
|
|
|
|
def test_cluster_address(self):
|
|
_addr = "10.10.10.10"
|
|
self.get_relation_ip.return_value = _addr
|
|
self.assertEqual(mysql_innodb_cluster.cluster_address(self.cls), _addr)
|
|
self.get_relation_ip.assert_called_once_with("cluster")
|
|
|
|
def test_shared_db_address(self):
|
|
_addr = "10.10.10.20"
|
|
self.get_relation_ip.return_value = _addr
|
|
self.assertEqual(
|
|
mysql_innodb_cluster.shared_db_address(self.cls), _addr)
|
|
self.get_relation_ip.assert_called_once_with("shared-db")
|
|
|
|
def test_db_router_address(self):
|
|
_addr = "10.10.10.30"
|
|
self.get_relation_ip.return_value = _addr
|
|
self.assertEqual(
|
|
mysql_innodb_cluster.db_router_address(self.cls), _addr)
|
|
self.get_relation_ip.assert_called_once_with("db-router")
|
|
|
|
|
|
class TestMySQLInnoDBClusterCharm(test_utils.PatchHelper):
|
|
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.subprocess = mock.MagicMock()
|
|
self.patch_object(mysql_innodb_cluster.uuid, "uuid4")
|
|
self.uuid_of_cluster = "uuid-of-cluster"
|
|
self.uuid4.return_value = self.uuid_of_cluster
|
|
self.patch_object(mysql_innodb_cluster.reactive, "is_flag_set")
|
|
self.patch_object(mysql_innodb_cluster.reactive, "set_flag")
|
|
self.patch_object(mysql_innodb_cluster.ch_net_ip, "get_relation_ip")
|
|
self.patch_object(mysql_innodb_cluster.ch_core.host, "pwgen")
|
|
self.patch_object(mysql_innodb_cluster.ch_core.hookenv, "is_leader")
|
|
self.patch_object(mysql_innodb_cluster.ch_core.hookenv, "relation_set")
|
|
self.patch_object(mysql_innodb_cluster.leadership, "leader_set")
|
|
self.patch_object(mysql_innodb_cluster.ch_core.hookenv, "leader_get")
|
|
self.patch_object(mysql_innodb_cluster.ch_core.hookenv, "config")
|
|
self.patch_object(mysql_innodb_cluster.ch_core.hookenv, "log")
|
|
self.patch_object(
|
|
mysql_innodb_cluster.ch_core.hookenv, "application_version_set")
|
|
self.leader_get.side_effect = self._fake_leader_data
|
|
self.config.side_effect = self._fake_config_data
|
|
self.leader_data = {}
|
|
self.config_data = {}
|
|
self.data = {}
|
|
self.stdin = mock.MagicMock()
|
|
self.filename = "script.py"
|
|
self.file = mock.MagicMock()
|
|
self.file.name = self.filename
|
|
self.ntf = mock.MagicMock()
|
|
self.ntf.__enter__.return_value = self.file
|
|
self.ntf.__enter__.name.return_value = self.filename
|
|
self.wait_until = mock.MagicMock()
|
|
self.patch_object(mysql_innodb_cluster.tempfile, "NamedTemporaryFile")
|
|
self.NamedTemporaryFile.return_value = self.ntf
|
|
self.subprocess.STDOUT = self.stdin
|
|
|
|
# Complex setup for create_databases_and_users tests
|
|
# mimics a reactive env
|
|
self.mock_unprefixed = "UNPREFIXED"
|
|
self.keystone_shared_db = mock.MagicMock()
|
|
self.keystone_shared_db.relation_id = "shared-db:5"
|
|
self.nova_shared_db = mock.MagicMock()
|
|
self.nova_shared_db.relation_id = "shared-db:20"
|
|
self.kmr_db_router = mock.MagicMock()
|
|
self.kmr_db_router.relation_id = "db-router:7"
|
|
self.nmr_db_router = mock.MagicMock()
|
|
self.nmr_db_router.relation_id = "db-router:10"
|
|
# Keystone shared-db
|
|
self.keystone_unit5_name = "keystone/5"
|
|
self.keystone_unit5_ip = "10.10.10.50"
|
|
self.keystone_unit5 = mock.MagicMock()
|
|
self.keystone_unit5.received = {
|
|
"database": "keystone", "username": "keystone",
|
|
"hostname": self.keystone_unit5_ip}
|
|
self.keystone_unit5.unit_name = self.keystone_unit5_name
|
|
self.keystone_unit5.relation = self.keystone_shared_db
|
|
self.keystone_unit7_name = "keystone/7"
|
|
self.keystone_unit7_ip = "10.10.10.70"
|
|
self.keystone_unit7 = mock.MagicMock()
|
|
self.keystone_unit7.received = {
|
|
"database": "keystone", "username": "keystone",
|
|
"hostname": self.keystone_unit7_ip}
|
|
self.keystone_unit7.unit_name = self.keystone_unit7_name
|
|
self.keystone_unit7.relation = self.keystone_shared_db
|
|
self.keystone_shared_db.joined_units = [
|
|
self.keystone_unit5, self.keystone_unit7]
|
|
# Nova shared-db
|
|
self.nova_unit5_name = "nova/5"
|
|
self.nova_unit5_ip = "10.20.20.50"
|
|
self.nova_unit5 = mock.MagicMock()
|
|
self.nova_unit5.unit_name = self.nova_unit5_name
|
|
self.nova_unit5.relation = self.nova_shared_db
|
|
self.nova_unit5.received = {
|
|
"nova_database": "nova", "nova_username": "nova",
|
|
"nova_hostname": self.nova_unit5_ip,
|
|
"novaapi_database": "nova_api", "novaapi_username": "nova",
|
|
"novaapi_hostname": self.nova_unit5_ip,
|
|
"novacell0_database": "nova_cell0", "novacell0_username": "nova",
|
|
"novacell0_hostname": self.nova_unit5_ip}
|
|
self.nova_unit7_name = "nova/7"
|
|
self.nova_unit7_ip = "10.20.20.70"
|
|
self.nova_unit7 = mock.MagicMock()
|
|
self.nova_unit7.unit_name = self.nova_unit7_name
|
|
self.nova_unit7.received = {
|
|
"nova_database": "nova", "nova_username": "nova",
|
|
"nova_hostname": self.nova_unit7_ip,
|
|
"novaapi_database": "nova_api", "novaapi_username": "nova",
|
|
"novaapi_hostname": self.nova_unit7_ip,
|
|
"novacell0_database": "nova_cell0", "novacell0_username": "nova",
|
|
"novacell0_hostname": self.nova_unit7_ip}
|
|
self.nova_unit7.relation = self.nova_shared_db
|
|
self.nova_shared_db.joined_units = [self.nova_unit5, self.nova_unit7]
|
|
# Keystone db-router
|
|
self.kmr_unit5_name = "kmr/5"
|
|
self.kmr_unit5_ip = "10.30.30.50"
|
|
self.kmr_unit5 = mock.MagicMock()
|
|
self.kmr_unit5.unit_name = self.kmr_unit5_name
|
|
self.kmr_unit5.relation = self.kmr_db_router
|
|
self.kmr_unit5.received = {
|
|
"{}_database".format(self.mock_unprefixed): "keystone",
|
|
"{}_username".format(self.mock_unprefixed): "keystone",
|
|
"{}_hostname".format(self.mock_unprefixed): self.kmr_unit5_ip,
|
|
"mysqlrouter_username": "mysqlrouteruser",
|
|
"mysqlrouter_hostname": self.kmr_unit5_ip}
|
|
self.kmr_unit7_name = "kmr/7"
|
|
self.kmr_unit7_ip = "10.30.30.70"
|
|
self.kmr_unit7 = mock.MagicMock()
|
|
self.kmr_unit7.unit_name = self.kmr_unit7_name
|
|
self.kmr_unit7.relation = self.kmr_db_router
|
|
self.kmr_db_router.joined_units = [self.kmr_unit5, self.kmr_unit7]
|
|
self.kmr_unit7.received = {
|
|
"{}_database".format(self.mock_unprefixed): "keystone",
|
|
"{}_username".format(self.mock_unprefixed): "keystone",
|
|
"{}_hostname".format(self.mock_unprefixed): self.kmr_unit7_ip,
|
|
"mysqlrouter_username": "mysqlrouteruser",
|
|
"mysqlrouter_hostname": self.kmr_unit7_ip}
|
|
# Nova Router db-router
|
|
self.nmr_unit5_name = "nmr/5"
|
|
self.nmr_unit5_ip = "10.40.40.50"
|
|
self.nmr_unit5 = mock.MagicMock()
|
|
self.nmr_unit5.unit_name = self.nmr_unit5_name
|
|
self.nmr_unit5.relation = self.nmr_db_router
|
|
self.nmr_unit5.received = {
|
|
"nova_database": "nova", "nova_username": "nova",
|
|
"nova_hostname": self.nmr_unit5_ip,
|
|
"novaapi_database": "nova_api", "novaapi_username": "nova",
|
|
"novaapi_hostname": self.nmr_unit5_ip,
|
|
"novacell0_database": "nova_cell0",
|
|
"novacell0_username": "nova",
|
|
"novacell0_hostname": self.nmr_unit5_ip,
|
|
"mysqlrouter_username": "mysqlrouteruser",
|
|
"mysqlrouter_hostname": self.nmr_unit5_ip}
|
|
self.nmr_unit7_name = "nmr/7"
|
|
self.nmr_unit7_ip = "10.40.40.70"
|
|
self.nmr_unit7 = mock.MagicMock()
|
|
self.nmr_unit7.unit_name = self.nmr_unit7_name
|
|
self.nmr_unit7.relation = self.nmr_db_router
|
|
self.nmr_db_router.joined_units = [self.nmr_unit5, self.nmr_unit7]
|
|
self.nmr_unit7.received = {
|
|
"nova_database": "nova", "nova_username": "nova",
|
|
"nova_hostname": self.nmr_unit7_ip,
|
|
"novaapi_database": "nova_api", "novaapi_username": "nova",
|
|
"novaapi_hostname": self.nmr_unit7_ip,
|
|
"novacell0_database": "nova_cell0",
|
|
"novacell0_username": "nova",
|
|
"novacell0_hostname": self.nmr_unit7_ip,
|
|
"mysqlrouter_username": "mysqlrouteruser",
|
|
"mysqlrouter_hostname": self.nmr_unit7_ip}
|
|
|
|
self.unit1 = mock.MagicMock(name="FakeUnit")
|
|
self.unit1.received.__getitem__.side_effect = self._fake_data
|
|
self.cluster = mock.MagicMock()
|
|
self.certificates = mock.MagicMock()
|
|
self.cluster.all_joined_units = [self.unit1]
|
|
|
|
# Generic interface
|
|
self.interface = mock.MagicMock()
|
|
|
|
def _fake_leader_data(self, key=None):
|
|
if key:
|
|
return self.leader_data.get(key)
|
|
else:
|
|
return self.leader_data.copy()
|
|
|
|
def _fake_config_data(self, key=None):
|
|
if key is None:
|
|
return {}
|
|
return self.config_data.get(key)
|
|
|
|
def _fake_data(self, key):
|
|
return self.data.get(key)
|
|
|
|
def _fake_configure(self, *args, **kwargs):
|
|
# For use mocking configure_db_router and configure_db_for_hosts
|
|
# Return the same password for the same username
|
|
if len(args) == 3:
|
|
# configure_db_for_hosts
|
|
return "{}-pwd".format(args[2])
|
|
elif len(args) == 2:
|
|
# configure_db_router
|
|
return "{}-pwd".format(args[1])
|
|
|
|
def _fake_get_allowed_units(self, *args, **kwargs):
|
|
return " ".join(
|
|
[x.unit_name for x in
|
|
self.interface.relations[args[2]].joined_units])
|
|
|
|
def _fake_get_db_data(self, relation_data, unprefixed=None):
|
|
# This "fake" get_db_data looks a lot like the real thing.
|
|
# Charmhelpers is mocked out entirely and attempting to
|
|
# mock the output made the test setup more difficult.
|
|
settings = copy.deepcopy(relation_data)
|
|
databases = collections.OrderedDict()
|
|
|
|
singleset = {"database", "username", "hostname"}
|
|
if singleset.issubset(settings):
|
|
settings["{}_{}".format(unprefixed, "hostname")] = (
|
|
settings["hostname"])
|
|
settings.pop("hostname")
|
|
settings["{}_{}".format(unprefixed, "database")] = (
|
|
settings["database"])
|
|
settings.pop("database")
|
|
settings["{}_{}".format(unprefixed, "username")] = (
|
|
settings["username"])
|
|
settings.pop("username")
|
|
|
|
for k, v in settings.items():
|
|
db = k.split("_")[0]
|
|
x = "_".join(k.split("_")[1:])
|
|
if db not in databases:
|
|
databases[db] = collections.OrderedDict()
|
|
databases[db][x] = v
|
|
|
|
return databases
|
|
|
|
def test_mysqlsh_bin(self):
|
|
self.patch_object(mysql_innodb_cluster.os.path, "exists")
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
self.exists.return_value = True
|
|
self.assertEqual(
|
|
midbc.mysqlsh_bin,
|
|
"/snap/bin/mysqlsh")
|
|
self.exists.return_value = False
|
|
self.assertEqual(
|
|
midbc.mysqlsh_bin,
|
|
"/snap/bin/mysql-shell")
|
|
|
|
def test_mysqlsh_common_dir(self):
|
|
self.patch_object(mysql_innodb_cluster.os.path, "exists")
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
self.assertEqual(
|
|
midbc.mysqlsh_common_dir,
|
|
"/root/snap/mysql-shell/common")
|
|
|
|
def test_mysql_password(self):
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc._get_password = mock.MagicMock()
|
|
midbc._get_password.side_effect = self._fake_data
|
|
_pass = "pass123"
|
|
self.data = {"mysql.passwd": _pass}
|
|
self.assertEqual(
|
|
midbc.mysql_password,
|
|
_pass)
|
|
|
|
def test_cluster_name(self):
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
_name = "jujuCluster"
|
|
midbc.options.cluster_name = _name
|
|
self.assertEqual(
|
|
midbc.cluster_name,
|
|
_name)
|
|
|
|
def test_cluster_password(self):
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc._get_password = mock.MagicMock()
|
|
midbc._get_password.side_effect = self._fake_data
|
|
_pass = "pass321"
|
|
self.data = {"cluster-password": _pass}
|
|
self.assertEqual(
|
|
midbc.cluster_password,
|
|
_pass)
|
|
|
|
def test_cluster_address(self):
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
_addr = "10.10.10.50"
|
|
self.get_relation_ip.return_value = _addr
|
|
self.assertEqual(
|
|
midbc.cluster_address,
|
|
_addr)
|
|
|
|
def test_cluster_user(self):
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
self.assertEqual(
|
|
midbc.cluster_user,
|
|
"clusteruser")
|
|
|
|
def test_shared_db_address(self):
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
_addr = "10.10.10.60"
|
|
self.get_relation_ip.return_value = _addr
|
|
self.assertEqual(
|
|
midbc.shared_db_address,
|
|
_addr)
|
|
|
|
def test_db_router_address(self):
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
_addr = "10.10.10.70"
|
|
self.get_relation_ip.return_value = _addr
|
|
self.assertEqual(
|
|
midbc.db_router_address,
|
|
_addr)
|
|
|
|
def test__get_password(self):
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
# Pwgen
|
|
_pwgenpass = "pwgenpass"
|
|
self.pwgen.return_value = _pwgenpass
|
|
self.assertEqual(
|
|
midbc._get_password("pwgenpw"),
|
|
_pwgenpass)
|
|
# Config
|
|
_configpass = "configpass"
|
|
self.config_data = {"configpw": _configpass}
|
|
self.assertEqual(
|
|
midbc._get_password("configpw"),
|
|
_configpass)
|
|
# Leader settings
|
|
_leaderpass = "leaderpass"
|
|
self.leader_data = {"leaderpw": _leaderpass}
|
|
self.assertEqual(
|
|
midbc._get_password("leaderpw"),
|
|
_leaderpass)
|
|
|
|
def test_configure_mysql_password(self):
|
|
_pass = "mysql-pass"
|
|
self.data = {"mysql.passwd": _pass}
|
|
_debconf = mock.MagicMock()
|
|
self.subprocess.Popen.return_value = _debconf
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc._get_password = mock.MagicMock()
|
|
midbc._get_password.side_effect = self._fake_data
|
|
mysql_innodb_cluster.subprocess = self.subprocess
|
|
midbc.configure_mysql_password()
|
|
_calls = []
|
|
for package in ["mysql-server", "mysql-server-8.0"]:
|
|
_calls.append(
|
|
mock.call("{} {}/root_password password {}\n"
|
|
.format(package, package, _pass).encode("UTF-8")))
|
|
_calls.append(
|
|
mock.call("{} {}/root_password_again password {}\n"
|
|
.format(package, package, _pass).encode("UTF-8")))
|
|
_debconf.stdin.write.assert_has_calls(_calls, any_order=True)
|
|
|
|
def test_install(self):
|
|
self.patch_object(
|
|
mysql_innodb_cluster.charms_openstack.charm.OpenStackCharm,
|
|
"install", "super_install")
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.configure_mysql_password = mock.MagicMock()
|
|
midbc.configure_source = mock.MagicMock()
|
|
midbc.render_all_configs = mock.MagicMock()
|
|
midbc.configure_tls = mock.MagicMock()
|
|
midbc.install()
|
|
self.super_install.assert_called_once()
|
|
midbc.configure_mysql_password.assert_called_once()
|
|
midbc.configure_source.assert_called_once()
|
|
midbc.configure_tls.assert_called_once()
|
|
midbc.render_all_configs.assert_called_once()
|
|
|
|
def test_get_db_helper(self):
|
|
_helper = mock.MagicMock()
|
|
self.patch_object(
|
|
mysql_innodb_cluster.mysql, "MySQL8Helper")
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
self.MySQL8Helper.return_value = _helper
|
|
self.assertEqual(_helper, midbc.get_db_helper())
|
|
self.MySQL8Helper.assert_called_once()
|
|
|
|
def test_get_cluster_rw_db_helper(self):
|
|
_addr = "10.5.50.41"
|
|
_helper = mock.MagicMock()
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.get_db_helper = mock.MagicMock()
|
|
midbc.get_db_helper.return_value = _helper
|
|
midbc.get_cluster_primary_address = mock.MagicMock()
|
|
|
|
# No primary address found
|
|
midbc.get_cluster_primary_address.return_value = None
|
|
self.assertEqual(None, midbc.get_cluster_rw_db_helper())
|
|
|
|
# Return helper
|
|
midbc.get_cluster_primary_address.return_value = _addr
|
|
self.assertEqual(_helper, midbc.get_cluster_rw_db_helper())
|
|
_helper.connect.assert_called_once_with(
|
|
user=midbc.cluster_user,
|
|
password=midbc.cluster_password,
|
|
host=_addr)
|
|
|
|
def test_grant_cluster_user_permissions(self):
|
|
_user = "user"
|
|
_addr = "10.10.20.20"
|
|
mock_helper = mock.MagicMock()
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
|
|
# All privileges
|
|
midbc._grant_user_privileges(mock_helper, _addr, _user, "all")
|
|
mock_helper.execute.assert_has_calls([
|
|
mock.call("REVOKE ALL PRIVILEGES ON *.* FROM '{}'@'{}'"
|
|
.format(_user, _addr)),
|
|
mock.call("GRANT ALL PRIVILEGES ON *.* TO '{}'@'{}' "
|
|
"WITH GRANT OPTION".format(_user, _addr)),
|
|
mock.call("FLUSH PRIVILEGES"),
|
|
])
|
|
mock_helper.reset_mock()
|
|
|
|
# read-only privileges
|
|
midbc._grant_user_privileges(mock_helper, _addr, _user, "read_only")
|
|
mock_helper.execute.assert_has_calls([
|
|
mock.call("REVOKE ALL PRIVILEGES ON *.* FROM '{}'@'{}'"
|
|
.format(_user, _addr)),
|
|
mock.call("GRANT SELECT, SHOW VIEW ON *.* TO '{}'@'{}'"
|
|
.format(_user, _addr)),
|
|
mock.call("FLUSH PRIVILEGES"),
|
|
])
|
|
mock_helper.reset_mock()
|
|
|
|
@mock.patch(('charm.openstack.mysql_innodb_cluster.'
|
|
'MySQLInnoDBClusterCharm.cluster_address'),
|
|
new_callable=mock.PropertyMock)
|
|
def test_create_user(self, mock_cluster_address):
|
|
_user = "user"
|
|
_pass = "pass"
|
|
_addr = "10.10.20.20"
|
|
_helper = mock.MagicMock()
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.get_db_helper = mock.MagicMock()
|
|
self.leader_data = {
|
|
'cluster-instance-configured-10-1-2-3': 'True',
|
|
'cluster-instance-clustered-10-1-2-3': 'False',
|
|
}
|
|
midbc.get_db_helper.return_value = _helper
|
|
midbc.get_cluster_rw_db_helper = mock.MagicMock(return_value=None)
|
|
|
|
# test no cluster rw_db_helper, configured, but not yet in cluster
|
|
mock_cluster_address.return_value = "10.1.2.3"
|
|
self.assertIsNone(midbc.create_user(_addr, _user, _pass, "all"))
|
|
|
|
self.leader_data = {
|
|
'cluster-instance-configured-10-1-2-3': 'True',
|
|
'cluster-instance-clustered-10-1-2-3': 'True',
|
|
}
|
|
|
|
# Non-local
|
|
midbc.create_user(_addr, _user, _pass, "all")
|
|
_calls = [
|
|
mock.call("CREATE USER '{}'@'{}' IDENTIFIED BY '{}'"
|
|
.format(_user, _addr, _pass)),
|
|
mock.call("REVOKE ALL PRIVILEGES ON *.* FROM '{}'@'{}'"
|
|
.format(_user, _addr)),
|
|
mock.call("GRANT ALL PRIVILEGES ON *.* TO '{}'@'{}' "
|
|
"WITH GRANT OPTION".format(_user, _addr)),
|
|
mock.call("FLUSH PRIVILEGES")]
|
|
_helper.execute.assert_has_calls(
|
|
_calls, any_order=True)
|
|
|
|
# Local
|
|
_localhost = "localhost"
|
|
_helper.reset_mock()
|
|
self.get_relation_ip.return_value = _addr
|
|
midbc.create_user(_localhost, _user, _pass, "all")
|
|
_calls = [
|
|
mock.call("CREATE USER '{}'@'{}' IDENTIFIED BY '{}'"
|
|
.format(_user, _localhost, _pass)),
|
|
mock.call("REVOKE ALL PRIVILEGES ON *.* FROM '{}'@'{}'"
|
|
.format(_user, _localhost)),
|
|
mock.call("GRANT ALL PRIVILEGES ON *.* TO '{}'@'{}' "
|
|
"WITH GRANT OPTION".format(_user, _localhost)),
|
|
mock.call("FLUSH PRIVILEGES")]
|
|
_helper.execute.assert_has_calls(
|
|
_calls, any_order=True)
|
|
|
|
# Read only privileges
|
|
midbc.create_user(_localhost, _user, _pass, "read_only")
|
|
_calls = [
|
|
mock.call("CREATE USER '{}'@'{}' IDENTIFIED BY '{}'"
|
|
.format(_user, _localhost, _pass)),
|
|
mock.call("REVOKE ALL PRIVILEGES ON *.* FROM '{}'@'{}'"
|
|
.format(_user, _localhost)),
|
|
mock.call("GRANT SELECT, SHOW VIEW ON *.* TO '{}'@'{}'"
|
|
.format(_user, _localhost)),
|
|
mock.call("FLUSH PRIVILEGES")]
|
|
_helper.execute.assert_has_calls(
|
|
_calls, any_order=True)
|
|
|
|
# Exception handling
|
|
self.patch_object(
|
|
mysql_innodb_cluster.mysql.MySQLdb, "_exceptions")
|
|
self._exceptions.OperationalError = FakeException
|
|
|
|
# _helper.connect raises midbc._local_socket_connection_error
|
|
_helper.reset_mock()
|
|
# _helper.connect.side_effect = _error
|
|
_helper.connect.side_effect = FakeException(2002, "Failed connection")
|
|
self.assertIsNone(midbc.create_user(_localhost, _user, _pass, "all"))
|
|
_helper.connect.assert_called_once_with(password=mock.ANY)
|
|
|
|
# _helper.connect raises a different error.
|
|
_helper.connect.side_effect = FakeException(9999, "Unknown exception")
|
|
with self.assertRaises(FakeException) as e:
|
|
midbc.create_user(_localhost, _user, _pass, "all")
|
|
self.assertEqual(e.exception.code, 9999)
|
|
|
|
# remove the exception to move on to _helper.execute exceptions
|
|
_helper.connect.side_effect = None
|
|
|
|
# User Exists
|
|
_helper.reset_mock()
|
|
_helper.execute.side_effect = [
|
|
self._exceptions.OperationalError(1396, "User exists"),
|
|
mock.MagicMock(), # revoke all privileges
|
|
mock.MagicMock(), # gran privileges
|
|
mock.MagicMock(), # flush privileges
|
|
]
|
|
self.assertTrue(midbc.create_user(_localhost, _user, _pass, "all"))
|
|
|
|
# Read only node
|
|
_helper.reset_mock()
|
|
_helper.execute.side_effect = (
|
|
self._exceptions.OperationalError(1290, "Super read only"))
|
|
self.assertEqual(midbc.create_user(_localhost, _user, _pass, "all"),
|
|
False)
|
|
|
|
# before commit error (3100)
|
|
_helper.reset_mock()
|
|
_helper.execute.side_effect = (
|
|
self._exceptions.OperationalError(3100, "Before commit error"))
|
|
self.assertEqual(midbc.create_user(_localhost, _user, _pass, "all"),
|
|
False)
|
|
|
|
# Unhandled Exception
|
|
_helper.reset_mock()
|
|
_helper.execute.side_effect = (
|
|
self._exceptions.OperationalError(99999, "BROKEN"))
|
|
with self.assertRaises(FakeException):
|
|
midbc.create_user(_localhost, _user, _pass, "all")
|
|
|
|
def test_configure_instance(self):
|
|
_pass = "clusterpass"
|
|
_addr = "10.10.30.30"
|
|
self.data = {"cluster-password": _pass}
|
|
self.leader_data = {
|
|
'cluster-instance-configured-10-10-30-30': 'False',
|
|
}
|
|
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc._get_password = mock.MagicMock()
|
|
midbc._get_password.side_effect = self._fake_data
|
|
midbc.wait_until_connectable = mock.MagicMock()
|
|
midbc.run_mysqlsh_script = mock.MagicMock()
|
|
_script = (
|
|
"dba.configure_instance('{}:{}@{}')\n"
|
|
.format(midbc.cluster_user, midbc.cluster_password, _addr))
|
|
|
|
midbc.configure_instance(_addr)
|
|
midbc.run_mysqlsh_script.assert_called_once_with(_script)
|
|
midbc.wait_until_connectable.assert_called_once_with(
|
|
address=_addr, username=midbc.cluster_user,
|
|
password=midbc.cluster_password)
|
|
self.leader_set.assert_called_once_with(
|
|
{"cluster-instance-configured-{}"
|
|
.format(_addr.replace(".", "-")): True})
|
|
|
|
def test__contains_in_leader_settings(self):
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
|
|
# simple
|
|
self.leader_data = {
|
|
'cluster-instance-configured-10-10-30-30': 'True',
|
|
'foo': 'bar',
|
|
}
|
|
test = {
|
|
'cluster-instance-configured-10-10-30-30': 'True'
|
|
}
|
|
|
|
self.assertTrue(midbc._contains_in_leader_settings(test))
|
|
|
|
# double
|
|
self.leader_data = {
|
|
'cluster-instance-configured-10-10-30-30': 'True',
|
|
'cluster-instance-clustered-10-10-30-30': 'False',
|
|
'foo': 'bar',
|
|
}
|
|
test = {
|
|
'cluster-instance-configured-10-10-30-30': 'True',
|
|
'cluster-instance-clustered-10-10-30-30': 'False',
|
|
}
|
|
|
|
self.assertTrue(midbc._contains_in_leader_settings(test))
|
|
|
|
# False
|
|
self.leader_data = {
|
|
'cluster-instance-configured-10-10-30-30': 'True',
|
|
'cluster-instance-clustered-10-10-30-30': 'False',
|
|
'foo': 'bar',
|
|
}
|
|
test = {
|
|
'cluster-instance-configured-10-10-30-30': 'True',
|
|
'cluster-instance-clustered-10-10-30-30': 'True',
|
|
}
|
|
|
|
self.assertFalse(midbc._contains_in_leader_settings(test))
|
|
|
|
def test_configure_instance_already_configured(self):
|
|
self.leader_data = {
|
|
'cluster-instance-configured-10-10-30-30': 'True',
|
|
}
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.run_mysqlsh_script = mock.MagicMock()
|
|
|
|
midbc.configure_instance('10.10.30.30')
|
|
midbc.run_mysqlsh_script.assert_not_called()
|
|
|
|
def test_configure_instance_error(self):
|
|
_pass = "clusterpass"
|
|
_addr = "10.10.30.30"
|
|
self.data = {"cluster-password": _pass}
|
|
self.leader_data = {
|
|
'cluster-instance-configured-10-10-30-30': 'False',
|
|
}
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc._get_password = mock.MagicMock()
|
|
midbc._get_password.side_effect = self._fake_data
|
|
midbc.wait_until_connectable = mock.MagicMock()
|
|
midbc.run_mysqlsh_script = mock.MagicMock(
|
|
side_effect=[subprocess.CalledProcessError(
|
|
2, 'foo', b'output', b'error')])
|
|
_script = (
|
|
"dba.configure_instance('{}:{}@{}')\n"
|
|
.format(midbc.cluster_user, midbc.cluster_password, _addr))
|
|
|
|
with self.assertRaises(subprocess.CalledProcessError):
|
|
midbc.configure_instance(_addr)
|
|
midbc.run_mysqlsh_script.assert_called_once_with(_script)
|
|
midbc.wait_until_connectable.assert_not_called()
|
|
self.leader_set.assert_not_called()
|
|
|
|
@mock.patch(('charm.openstack.mysql_innodb_cluster.'
|
|
'MySQLInnoDBClusterCharm.cluster_peer_addresses'),
|
|
new_callable=mock.PropertyMock)
|
|
@mock.patch(('charm.openstack.mysql_innodb_cluster.'
|
|
'MySQLInnoDBClusterCharm.cluster_address'),
|
|
new_callable=mock.PropertyMock)
|
|
def test_get_cluster_addresses(self, cluster_address,
|
|
cluster_peer_addresses):
|
|
self.patch_object(
|
|
mysql_innodb_cluster.ch_net_ip,
|
|
"resolve_network_cidr",
|
|
side_effect=lambda x: '{}.{}.0.0/24'.format(
|
|
x.split('.')[0],
|
|
x.split('.')[1]))
|
|
cluster_peer_addresses.return_value = [
|
|
'10.0.0.13', '10.0.0.11', '10.10.0.10']
|
|
cluster_address.return_value = '10.0.0.12'
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
self.assertEqual(
|
|
midbc.get_cluster_addresses(),
|
|
['10.0.0.0/24', '10.0.0.11', '10.0.0.12',
|
|
'10.0.0.13', '10.10.0.10'])
|
|
|
|
def test_generate_ip_allowlist_str(self):
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.get_cluster_addresses = lambda: ['10.0.0.10', '10.0.0.11']
|
|
self.assertEqual(
|
|
midbc.generate_ip_allowlist_str(),
|
|
'127.0.0.1,::1,10.0.0.10,10.0.0.11')
|
|
|
|
def test_reached_quorum(self):
|
|
self.patch_object(
|
|
mysql_innodb_cluster.ch_core.hookenv, "expected_peer_units",
|
|
return_value=['u1'])
|
|
self.patch_object(
|
|
mysql_innodb_cluster.reactive, "endpoint_from_flag",
|
|
return_value=self.cluster)
|
|
self.data = {
|
|
"cluster-address": "10.0.0.11"}
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
self.assertTrue(midbc.reached_quorum())
|
|
|
|
def test_reached_quorum_fail(self):
|
|
self.patch_object(
|
|
mysql_innodb_cluster.ch_core.hookenv, "expected_peer_units",
|
|
return_value=['u1', 'u2'])
|
|
self.patch_object(
|
|
mysql_innodb_cluster.reactive, "endpoint_from_flag",
|
|
return_value=self.cluster)
|
|
self.data = {
|
|
"cluster-address": "10.0.0.11"}
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
self.assertFalse(midbc.reached_quorum())
|
|
|
|
def test_restart_instance(self):
|
|
_pass = "clusterpass"
|
|
_addr = "10.10.30.30"
|
|
self.data = {"cluster-password": _pass}
|
|
self.is_flag_set.return_value = False
|
|
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc._get_password = mock.MagicMock()
|
|
midbc._get_password.side_effect = self._fake_data
|
|
midbc.wait_until_connectable = mock.MagicMock()
|
|
midbc.run_mysqlsh_script = mock.MagicMock()
|
|
_script = (
|
|
"myshell = shell.connect('{}:{}@{}')\n"
|
|
"myshell.run_sql('RESTART;')"
|
|
.format(midbc.cluster_user, midbc.cluster_password, _addr))
|
|
|
|
midbc.restart_instance(_addr)
|
|
midbc.run_mysqlsh_script.assert_called_once_with(_script)
|
|
midbc.wait_until_connectable.assert_called_once_with(
|
|
address=_addr, username=midbc.cluster_user,
|
|
password=midbc.cluster_password)
|
|
|
|
def test_create_cluster(self):
|
|
_pass = "clusterpass"
|
|
_addr = "10.10.40.40"
|
|
_name = "jujuCluster"
|
|
_tries = 500
|
|
_allowlist = '10.0.0.0/24'
|
|
_expel_timeout = 5
|
|
self.get_relation_ip.return_value = _addr
|
|
self.data = {"cluster-password": _pass}
|
|
self.leader_data = {
|
|
'cluster-instance-configured-10-10-40-40': 'True',
|
|
}
|
|
self.is_flag_set.return_value = False
|
|
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc._get_password = mock.MagicMock()
|
|
midbc._get_password.side_effect = self._fake_data
|
|
midbc.wait_until_connectable = mock.MagicMock()
|
|
midbc.run_mysqlsh_script = mock.MagicMock()
|
|
midbc.options.cluster_name = _name
|
|
midbc.options.auto_rejoin_tries = _tries
|
|
midbc.options.expel_timeout = _expel_timeout
|
|
midbc.generate_ip_allowlist_str = lambda: _allowlist
|
|
_script = (
|
|
"shell.connect('{}:{}@{}')\n"
|
|
"cluster = dba.create_cluster('{}', {{'autoRejoinTries': '{}', "
|
|
"'expelTimeout': '{}', 'ipAllowlist': '{}'}})"
|
|
.format(
|
|
midbc.cluster_user, midbc.cluster_password,
|
|
midbc.cluster_address, midbc.cluster_name, _tries,
|
|
_expel_timeout, _allowlist))
|
|
|
|
midbc.create_cluster()
|
|
self.is_flag_set.assert_called_once_with(
|
|
"leadership.set.cluster-created")
|
|
midbc.run_mysqlsh_script.assert_called_once_with(_script)
|
|
_leader_set_calls = [
|
|
mock.call({"cluster-instance-clustered-{}"
|
|
.format(_addr.replace(".", "-")): True}),
|
|
mock.call({"cluster-created": self.uuid_of_cluster})]
|
|
self.leader_set.assert_has_calls(_leader_set_calls, any_order=True)
|
|
|
|
def test_add_instance_to_cluster_already_clustered(self):
|
|
self.leader_data = {
|
|
'cluster-instance-clustered-10-10-60-60': 'True',
|
|
}
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.get_cluster_primary_address = mock.MagicMock()
|
|
|
|
midbc.add_instance_to_cluster('10.10.60.60')
|
|
midbc.get_cluster_primary_address.assert_not_called()
|
|
|
|
def test_add_instance_to_cluster(self):
|
|
_pass = "clusterpass"
|
|
_local_addr = "10.10.50.50"
|
|
_remote_addr = "10.10.60.60"
|
|
_name = "theCluster"
|
|
_allowlist = '10.0.0.0/24'
|
|
self.get_relation_ip.return_value = _local_addr
|
|
self.get_relation_ip.return_value = _local_addr
|
|
self.data = {"cluster-password": _pass}
|
|
self.leader_data = {
|
|
'cluster-instance-clustered-10-10-60-60': 'False',
|
|
}
|
|
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.get_cluster_primary_address = mock.MagicMock(
|
|
return_value=_local_addr)
|
|
midbc._get_password = mock.MagicMock()
|
|
midbc._get_password.side_effect = self._fake_data
|
|
midbc.wait_until_connectable = mock.MagicMock()
|
|
midbc.run_mysqlsh_script = mock.MagicMock()
|
|
midbc.options.cluster_name = _name
|
|
midbc.is_address_in_replication_ip_allowlist = lambda x: True
|
|
midbc.generate_ip_allowlist_str = lambda: _allowlist
|
|
_script = (
|
|
"shell.connect('{}:{}@{}')\n"
|
|
"cluster = dba.get_cluster('{}')\n"
|
|
"cluster.add_instance("
|
|
"{{'user': '{}', 'host': '{}', 'password': '{}', 'port': '3306'}},"
|
|
"{{'recoveryMethod': 'clone', 'waitRecovery': '2', "
|
|
"'interactive': False, 'ipAllowlist': '{}'}})"
|
|
.format(
|
|
midbc.cluster_user, midbc.cluster_password,
|
|
midbc.cluster_address, midbc.cluster_name,
|
|
midbc.cluster_user, _remote_addr, midbc.cluster_password,
|
|
_allowlist))
|
|
|
|
midbc.add_instance_to_cluster(_remote_addr)
|
|
midbc.run_mysqlsh_script.assert_called_once_with(_script)
|
|
self.leader_set.assert_called_once_with(
|
|
{"cluster-instance-clustered-{}"
|
|
.format(_remote_addr.replace(".", "-")): True})
|
|
|
|
def test_add_instance_to_cluster_error(self):
|
|
_pass = "clusterpass"
|
|
_local_addr = "10.10.50.50"
|
|
_remote_addr = "10.10.60.60"
|
|
_name = "theCluster"
|
|
_allowlist = '10.0.0.0/24'
|
|
self.get_relation_ip.return_value = _local_addr
|
|
self.get_relation_ip.return_value = _local_addr
|
|
self.data = {"cluster-password": _pass}
|
|
self.leader_data = {
|
|
'cluster-instance-configured-10-10-60-60': 'False',
|
|
}
|
|
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.get_cluster_primary_address = mock.MagicMock(
|
|
return_value=_local_addr)
|
|
midbc._get_password = mock.MagicMock()
|
|
midbc._get_password.side_effect = self._fake_data
|
|
midbc.wait_until_connectable = mock.MagicMock()
|
|
midbc.run_mysqlsh_script = mock.MagicMock(
|
|
side_effect=[subprocess.CalledProcessError(
|
|
2, 'foo', b'output', b'error')])
|
|
midbc.options.cluster_name = _name
|
|
midbc.is_address_in_replication_ip_allowlist = lambda x: True
|
|
midbc.generate_ip_allowlist_str = lambda: _allowlist
|
|
_script = (
|
|
"shell.connect('{}:{}@{}')\n"
|
|
"cluster = dba.get_cluster('{}')\n"
|
|
"cluster.add_instance("
|
|
"{{'user': '{}', 'host': '{}', 'password': '{}', 'port': '3306'}},"
|
|
"{{'recoveryMethod': 'clone', 'waitRecovery': '2', "
|
|
"'interactive': False, 'ipAllowlist': '{}'}})"
|
|
.format(
|
|
midbc.cluster_user, midbc.cluster_password,
|
|
midbc.cluster_address, midbc.cluster_name,
|
|
midbc.cluster_user, _remote_addr, midbc.cluster_password,
|
|
_allowlist))
|
|
with self.assertRaises(subprocess.CalledProcessError):
|
|
midbc.add_instance_to_cluster(_remote_addr)
|
|
midbc.run_mysqlsh_script.assert_called_once_with(_script)
|
|
|
|
def test_get_allowed_units(self):
|
|
_allowed = ["unit/2", "unit/1", "unit/0"]
|
|
_expected = "unit/0 unit/1 unit/2"
|
|
_helper = mock.MagicMock()
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.get_db_helper = mock.MagicMock()
|
|
midbc.get_db_helper.return_value = _helper
|
|
_helper.get_allowed_units.return_value = _allowed
|
|
self.assertEqual(
|
|
_expected,
|
|
midbc.get_allowed_units("db", "user", "rel:2"))
|
|
|
|
def test_create_databases_and_users_shared_db(self):
|
|
# The test setup is a bit convoluted and requires mimicking reactive,
|
|
# however, this is the heart of the charm and therefore deserves to
|
|
# be thoroughly tested. It is important to have multiple relations and
|
|
# multiple units per relation.
|
|
self.patch_object(
|
|
mysql_innodb_cluster.mysql, "get_db_data")
|
|
self.get_db_data.side_effect = self._fake_get_db_data
|
|
|
|
_addr = "10.99.99.99"
|
|
self.get_relation_ip.return_value = _addr
|
|
|
|
self.interface.relations = {
|
|
self.keystone_shared_db.relation_id: self.keystone_shared_db,
|
|
self.nova_shared_db.relation_id: self.nova_shared_db}
|
|
|
|
self.interface.all_joined_units = []
|
|
for rel in self.interface.relations.values():
|
|
self.interface.all_joined_units.extend(rel.joined_units)
|
|
|
|
self.patch_object(
|
|
mysql_innodb_cluster.reactive, "endpoint_from_flag",
|
|
return_value=self.certificates)
|
|
self.certificates.root_ca_cert = "Certificate Authority"
|
|
self.certificates.root_ca_chain = "Intermediate Chain Certificate"
|
|
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.get_allowed_units = mock.MagicMock()
|
|
midbc.get_allowed_units.side_effect = self._fake_get_allowed_units
|
|
_db_helper = mock.MagicMock()
|
|
midbc.get_db_helper = mock.MagicMock()
|
|
midbc.get_db_helper.return_value = _db_helper
|
|
_rw_db_helper = mock.MagicMock()
|
|
midbc.get_cluster_rw_db_helper = mock.MagicMock()
|
|
midbc.get_cluster_rw_db_helper.return_value = _rw_db_helper
|
|
|
|
_wait_timeout = 60
|
|
midbc.options.wait_timeout = _wait_timeout
|
|
midbc.options.ssl_ca = None
|
|
|
|
midbc.configure_db_for_hosts = mock.MagicMock()
|
|
midbc.configure_db_router = mock.MagicMock()
|
|
|
|
# Execute the function under test expect incomplete
|
|
midbc.configure_db_for_hosts.side_effect = [
|
|
x if x % 5 else None for x in range(1, 11)]
|
|
self.assertFalse(midbc.create_databases_and_users(self.interface))
|
|
|
|
# Execute the function under test expect complete
|
|
midbc.configure_db_for_hosts.reset_mock()
|
|
self.interface.set_db_connection_info.reset_mock()
|
|
midbc.configure_db_for_hosts.side_effect = self._fake_configure
|
|
self.assertTrue(midbc.create_databases_and_users(self.interface))
|
|
|
|
# Validate
|
|
midbc.configure_db_router.assert_not_called()
|
|
|
|
_configure_db_calls = [
|
|
mock.call(self.keystone_unit5_ip, "keystone", "keystone",
|
|
rw_helper=_rw_db_helper),
|
|
mock.call(self.keystone_unit7_ip, "keystone", "keystone",
|
|
rw_helper=_rw_db_helper),
|
|
mock.call(self.nova_unit5_ip, "nova", "nova",
|
|
rw_helper=_rw_db_helper),
|
|
mock.call(self.nova_unit5_ip, "nova_api", "nova",
|
|
rw_helper=_rw_db_helper),
|
|
mock.call(self.nova_unit5_ip, "nova_cell0", "nova",
|
|
rw_helper=_rw_db_helper),
|
|
mock.call(self.nova_unit7_ip, "nova", "nova",
|
|
rw_helper=_rw_db_helper),
|
|
mock.call(self.nova_unit7_ip, "nova_api", "nova",
|
|
rw_helper=_rw_db_helper),
|
|
mock.call(self.nova_unit7_ip, "nova_cell0", "nova",
|
|
rw_helper=_rw_db_helper)]
|
|
midbc.configure_db_for_hosts.assert_has_calls(
|
|
_configure_db_calls, any_order=True)
|
|
|
|
_set_calls = [
|
|
mock.call(
|
|
self.keystone_shared_db.relation_id, _addr, "keystone-pwd",
|
|
allowed_units=self._fake_get_allowed_units(
|
|
None, None, self.keystone_shared_db.relation_id),
|
|
prefix=None,
|
|
wait_timeout=_wait_timeout, ssl_ca=midbc.ssl_ca),
|
|
mock.call(
|
|
self.nova_shared_db.relation_id, _addr, "nova-pwd",
|
|
allowed_units=self._fake_get_allowed_units(
|
|
None, None, self.nova_shared_db.relation_id),
|
|
prefix="nova",
|
|
wait_timeout=_wait_timeout, ssl_ca=midbc.ssl_ca),
|
|
mock.call(
|
|
self.nova_shared_db.relation_id, _addr, "nova-pwd",
|
|
allowed_units=self._fake_get_allowed_units(
|
|
None, None, self.nova_shared_db.relation_id),
|
|
prefix="novaapi",
|
|
wait_timeout=_wait_timeout, ssl_ca=midbc.ssl_ca),
|
|
mock.call(
|
|
self.nova_shared_db.relation_id, _addr, "nova-pwd",
|
|
allowed_units=self._fake_get_allowed_units(
|
|
None, None, self.nova_shared_db.relation_id),
|
|
prefix="novacell0",
|
|
wait_timeout=_wait_timeout, ssl_ca=midbc.ssl_ca)]
|
|
self.interface.set_db_connection_info.assert_has_calls(
|
|
_set_calls, any_order=True)
|
|
|
|
# DB/User create is unsuccessful
|
|
midbc.configure_db_for_hosts.reset_mock()
|
|
midbc.configure_db_for_hosts.side_effect = None
|
|
midbc.configure_db_for_hosts.return_value = None
|
|
midbc.configure_db_router.side_effect = None
|
|
midbc.configure_db_router.return_value = None
|
|
|
|
# Execute the function under test expect incomplete
|
|
self.interface.set_db_connection_info.reset_mock()
|
|
self.assertFalse(midbc.create_databases_and_users(self.interface))
|
|
self.interface.set_db_connection_info.assert_not_called()
|
|
|
|
def test_create_databases_and_users_db_router(self):
|
|
# The test setup is a bit convoluted and requires mimicking reactive,
|
|
# however, this is the heart of the charm and therefore deserves to
|
|
# be thoroughly tested. It is important to have multiple relations and
|
|
# multiple units per relation.
|
|
self.patch_object(
|
|
mysql_innodb_cluster.mysql, "get_db_data")
|
|
self.get_db_data.side_effect = self._fake_get_db_data
|
|
|
|
_addr = "10.99.99.99"
|
|
self.get_relation_ip.return_value = _addr
|
|
|
|
self.interface.relations = {
|
|
self.kmr_db_router.relation_id: self.kmr_db_router,
|
|
self.nmr_db_router.relation_id: self.nmr_db_router}
|
|
|
|
self.interface.all_joined_units = []
|
|
for rel in self.interface.relations.values():
|
|
self.interface.all_joined_units.extend(rel.joined_units)
|
|
self.patch_object(
|
|
mysql_innodb_cluster.reactive, "endpoint_from_flag",
|
|
return_value=self.certificates)
|
|
self.certificates.root_ca_cert = "Certificate Authority"
|
|
self.certificates.root_ca_chain = "Intermediate Chain Certificate"
|
|
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
_db_helper = mock.MagicMock()
|
|
midbc.get_db_helper = mock.MagicMock()
|
|
midbc.get_db_helper.return_value = _db_helper
|
|
_rw_db_helper = mock.MagicMock()
|
|
midbc.get_cluster_rw_db_helper = mock.MagicMock()
|
|
midbc.get_cluster_rw_db_helper.return_value = _rw_db_helper
|
|
midbc.get_allowed_units = mock.MagicMock()
|
|
midbc.get_allowed_units.side_effect = self._fake_get_allowed_units
|
|
midbc.configure_db_for_hosts = mock.MagicMock()
|
|
midbc.configure_db_for_hosts.side_effect = self._fake_configure
|
|
midbc.configure_db_router = mock.MagicMock()
|
|
_wait_timeout = 60
|
|
midbc.options.wait_timeout = _wait_timeout
|
|
midbc.options.ssl_ca = None
|
|
|
|
# Execute the function under test expect incomplete
|
|
midbc.configure_db_router.side_effect = [
|
|
x if x % 3 else None for x in range(1, 11)]
|
|
self.assertFalse(midbc.create_databases_and_users(self.interface))
|
|
|
|
# Execute the function under test expect complete
|
|
midbc.configure_db_router.reset_mock()
|
|
self.interface.set_db_connection_info.reset_mock()
|
|
midbc.configure_db_router.side_effect = self._fake_configure
|
|
self.assertTrue(midbc.create_databases_and_users(self.interface))
|
|
|
|
# Validate
|
|
_conigure_db_router_calls = [
|
|
mock.call(self.kmr_unit5_ip, "mysqlrouteruser",
|
|
rw_helper=_rw_db_helper),
|
|
mock.call(self.kmr_unit7_ip, "mysqlrouteruser",
|
|
rw_helper=_rw_db_helper),
|
|
mock.call(self.nmr_unit5_ip, "mysqlrouteruser",
|
|
rw_helper=_rw_db_helper),
|
|
mock.call(self.nmr_unit7_ip, "mysqlrouteruser",
|
|
rw_helper=_rw_db_helper)]
|
|
midbc.configure_db_router.assert_has_calls(
|
|
_conigure_db_router_calls, any_order=True)
|
|
|
|
_configure_db_calls = [
|
|
mock.call(self.kmr_unit5_ip, "keystone", "keystone",
|
|
rw_helper=_rw_db_helper),
|
|
mock.call(self.kmr_unit7_ip, "keystone", "keystone",
|
|
rw_helper=_rw_db_helper),
|
|
mock.call(self.nmr_unit5_ip, "nova", "nova",
|
|
rw_helper=_rw_db_helper),
|
|
mock.call(self.nmr_unit5_ip, "nova_api", "nova",
|
|
rw_helper=_rw_db_helper),
|
|
mock.call(self.nmr_unit5_ip, "nova_cell0", "nova",
|
|
rw_helper=_rw_db_helper),
|
|
mock.call(self.nmr_unit7_ip, "nova", "nova",
|
|
rw_helper=_rw_db_helper),
|
|
mock.call(self.nmr_unit7_ip, "nova_api", "nova",
|
|
rw_helper=_rw_db_helper),
|
|
mock.call(self.nmr_unit7_ip, "nova_cell0", "nova",
|
|
rw_helper=_rw_db_helper)]
|
|
midbc.configure_db_for_hosts.assert_has_calls(
|
|
_configure_db_calls, any_order=True)
|
|
|
|
_set_calls = [
|
|
mock.call(
|
|
self.kmr_db_router.relation_id, _addr, "keystone-pwd",
|
|
allowed_units=self._fake_get_allowed_units(
|
|
None, None, self.kmr_db_router.relation_id),
|
|
prefix=self.mock_unprefixed,
|
|
wait_timeout=_wait_timeout, ssl_ca=midbc.ssl_ca),
|
|
mock.call(
|
|
self.kmr_db_router.relation_id, _addr, "mysqlrouteruser-pwd",
|
|
allowed_units=" ".join(
|
|
[x.unit_name for x in self.kmr_db_router.joined_units]),
|
|
prefix="mysqlrouter",
|
|
wait_timeout=_wait_timeout, ssl_ca=midbc.ssl_ca),
|
|
|
|
mock.call(
|
|
self.nmr_db_router.relation_id, _addr, "nova-pwd",
|
|
allowed_units=self._fake_get_allowed_units(
|
|
None, None, self.nmr_db_router.relation_id),
|
|
prefix="nova",
|
|
wait_timeout=_wait_timeout, ssl_ca=midbc.ssl_ca),
|
|
mock.call(
|
|
self.nmr_db_router.relation_id, _addr, "nova-pwd",
|
|
allowed_units=self._fake_get_allowed_units(
|
|
None, None, self.nmr_db_router.relation_id),
|
|
prefix="novaapi",
|
|
wait_timeout=_wait_timeout, ssl_ca=midbc.ssl_ca),
|
|
mock.call(
|
|
self.nmr_db_router.relation_id, _addr, "nova-pwd",
|
|
allowed_units=self._fake_get_allowed_units(
|
|
None, None, self.nmr_db_router.relation_id),
|
|
prefix="novacell0",
|
|
wait_timeout=_wait_timeout, ssl_ca=midbc.ssl_ca),
|
|
mock.call(
|
|
self.nmr_db_router.relation_id, _addr, "mysqlrouteruser-pwd",
|
|
allowed_units=" ".join(
|
|
[x.unit_name for x in self.nmr_db_router.joined_units]),
|
|
prefix="mysqlrouter",
|
|
wait_timeout=_wait_timeout, ssl_ca=midbc.ssl_ca)]
|
|
self.interface.set_db_connection_info.assert_has_calls(
|
|
_set_calls, any_order=True)
|
|
|
|
# DB/User create is unsuccessful
|
|
midbc.configure_db_router.reset_mock()
|
|
midbc.configure_db_for_hosts.side_effect = None
|
|
midbc.configure_db_for_hosts.return_value = None
|
|
midbc.configure_db_router.side_effect = None
|
|
midbc.configure_db_router.return_value = None
|
|
|
|
# Execute the function under test expect incomplete
|
|
self.interface.set_db_connection_info.reset_mock()
|
|
self.assertFalse(midbc.create_databases_and_users(self.interface))
|
|
self.interface.set_db_connection_info.assert_not_called()
|
|
|
|
def test_configure_db_for_hosts(self):
|
|
_db = "db"
|
|
_user = "user"
|
|
_addr = "10.10.80.80"
|
|
_pass = "newpass"
|
|
_json_addrs = '["10.20.10.10", "10.20.10.20", "10.20.10.30"]'
|
|
_helper = mock.MagicMock()
|
|
_helper.configure_db.return_value = _pass
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.get_cluster_rw_db_helper = mock.MagicMock()
|
|
midbc.get_cluster_rw_db_helper.return_value = None
|
|
|
|
# Early bailout
|
|
self.assertEqual(
|
|
None,
|
|
midbc.configure_db_for_hosts(_addr, _db, _user))
|
|
|
|
# One host
|
|
midbc.get_cluster_rw_db_helper.return_value = _helper
|
|
self.assertEqual(
|
|
_pass,
|
|
midbc.configure_db_for_hosts(_addr, _db, _user))
|
|
|
|
_helper.configure_db.assert_called_once_with(_addr, _db, _user)
|
|
|
|
# Json multiple hosts
|
|
_helper.reset_mock()
|
|
_calls = [
|
|
mock.call("10.20.10.10", _db, _user),
|
|
mock.call("10.20.10.20", _db, _user),
|
|
mock.call("10.20.10.30", _db, _user)]
|
|
self.assertEqual(
|
|
_pass,
|
|
midbc.configure_db_for_hosts(_json_addrs, _db, _user))
|
|
_helper.configure_db.assert_has_calls(
|
|
_calls, any_order=True)
|
|
|
|
# Exception handling
|
|
self.patch_object(
|
|
mysql_innodb_cluster.mysql.MySQLdb, "_exceptions")
|
|
self._exceptions.OperationalError = FakeException
|
|
|
|
# Super read only
|
|
_helper.reset_mock()
|
|
_helper.configure_db.side_effect = (
|
|
self._exceptions.OperationalError(1290, "Super REad only"))
|
|
self.assertEqual(
|
|
None,
|
|
midbc.configure_db_for_hosts(_json_addrs, _db, _user))
|
|
|
|
# Unhandled Exception
|
|
_helper.reset_mock()
|
|
_helper.configure_db.side_effect = (
|
|
self._exceptions.OperationalError(999, "BROKEN"))
|
|
with self.assertRaises(FakeException):
|
|
midbc.configure_db_for_hosts(_json_addrs, _db, _user)
|
|
|
|
def test_configure_db_router(self):
|
|
_user = "user"
|
|
_addr = "10.10.90.90"
|
|
_pass = "newpass"
|
|
_json_addrs = '["10.30.10.10", "10.30.10.20", "10.30.10.30"]'
|
|
_helper = mock.MagicMock()
|
|
_helper.configure_router.return_value = _pass
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.get_cluster_rw_db_helper = mock.MagicMock()
|
|
|
|
# Early bailout
|
|
midbc.get_cluster_rw_db_helper.return_value = None
|
|
self.assertEqual(
|
|
None,
|
|
midbc.configure_db_router(_addr, _user))
|
|
|
|
# One host
|
|
midbc.get_cluster_rw_db_helper.return_value = _helper
|
|
self.assertEqual(
|
|
_pass,
|
|
midbc.configure_db_router(_addr, _user))
|
|
|
|
_helper.configure_router.assert_called_once_with(_addr, _user)
|
|
|
|
# Json multiple hosts
|
|
_helper.reset_mock()
|
|
_calls = [
|
|
mock.call("10.30.10.10", _user),
|
|
mock.call("10.30.10.20", _user),
|
|
mock.call("10.30.10.30", _user)]
|
|
self.assertEqual(
|
|
_pass,
|
|
midbc.configure_db_router(_json_addrs, _user))
|
|
_helper.configure_router.assert_has_calls(
|
|
_calls, any_order=True)
|
|
|
|
# Exception handling
|
|
self.patch_object(
|
|
mysql_innodb_cluster.mysql.MySQLdb, "_exceptions")
|
|
self._exceptions.OperationalError = FakeException
|
|
|
|
# Super read only
|
|
_helper.reset_mock()
|
|
_helper.configure_router.side_effect = (
|
|
self._exceptions.OperationalError(1290, "Super REad only"))
|
|
self.assertEqual(
|
|
None,
|
|
midbc.configure_db_router(_json_addrs, _user))
|
|
|
|
# Unhandled Exception
|
|
_helper.reset_mock()
|
|
_helper.configure_router.side_effect = (
|
|
self._exceptions.OperationalError(999, "BROKEN"))
|
|
with self.assertRaises(FakeException):
|
|
midbc.configure_db_router(_json_addrs, _user)
|
|
|
|
def test_states_to_check(self):
|
|
self.patch_object(
|
|
mysql_innodb_cluster.charms_openstack.charm.OpenStackCharm,
|
|
"states_to_check", "super_states")
|
|
self.super_states.return_value = {}
|
|
_required_rels = ["all"]
|
|
_name = "jujuCluster"
|
|
_addr = "10.20.20.20"
|
|
self.get_relation_ip.return_value = _addr
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.options.cluster_name = _name
|
|
_results = midbc.states_to_check(_required_rels)
|
|
_states_to_check = [x[0] for x in _results["charm"]]
|
|
self.super_states.assert_called_once_with(_required_rels)
|
|
self.assertTrue("charm.installed" in _states_to_check)
|
|
self.assertTrue(
|
|
"leadership.set.cluster-instance-configured-{}"
|
|
.format(_addr.replace(".", "-")) in _states_to_check)
|
|
self.assertTrue("leadership.set.cluster-created" in _states_to_check)
|
|
self.assertTrue(
|
|
"leadership.set.cluster-instances-configured" in _states_to_check)
|
|
self.assertTrue(
|
|
"leadership.set.cluster-instance-clustered-{}"
|
|
.format(_addr.replace(".", "-")) in _states_to_check)
|
|
self.assertTrue(
|
|
"leadership.set.cluster-instances-clustered" in _states_to_check)
|
|
|
|
def test__assess_status(self):
|
|
_check = mock.MagicMock()
|
|
_check.return_value = None, None
|
|
_conn_check = mock.MagicMock()
|
|
_conn_check.return_value = True
|
|
_status = mock.MagicMock()
|
|
_status.return_value = "OK"
|
|
_status_mode = mock.MagicMock()
|
|
_status_mode.return_value = "RO"
|
|
_status_text = mock.MagicMock()
|
|
_status_text.return_value = (
|
|
"Cluster is ONLINE and can tolerate up to ONE failure.")
|
|
self.patch_object(
|
|
mysql_innodb_cluster.charms_openstack.charm.OpenStackCharm,
|
|
"application_version")
|
|
self.patch_object(
|
|
mysql_innodb_cluster.ch_core.hookenv,
|
|
"status_set")
|
|
|
|
# All is well
|
|
self.is_flag_set.return_value = False
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.check_if_paused = _check
|
|
midbc.check_interfaces = _check
|
|
midbc.check_mandatory_config = _check
|
|
midbc.check_services_running = _check
|
|
midbc.check_mysql_connection = _conn_check
|
|
midbc.get_cluster_status_summary = _status
|
|
midbc.get_cluster_status_text = _status_text
|
|
midbc.get_cluster_instance_mode = _status_mode
|
|
midbc.get_denied_peers = lambda: []
|
|
|
|
midbc._assess_status()
|
|
self.assertEqual(4, len(_check.mock_calls))
|
|
_conn_check.assert_called_once_with()
|
|
_status.assert_called_once_with(nocache=True)
|
|
self.status_set.assert_called_once_with(
|
|
"active",
|
|
"Unit is ready: Mode: RO, Cluster is ONLINE and can "
|
|
"tolerate up to ONE failure.")
|
|
|
|
# First checks fail
|
|
self.status_set.reset_mock()
|
|
_check.return_value = "blocked", "for some reason"
|
|
midbc._assess_status()
|
|
self.status_set.assert_called_once_with(
|
|
"blocked", "for some reason")
|
|
|
|
# MySQL connect fails
|
|
self.status_set.reset_mock()
|
|
_check.return_value = None, None
|
|
_conn_check.return_value = False
|
|
midbc._assess_status()
|
|
self.status_set.assert_called_once_with(
|
|
"blocked", "MySQL is down on this instance")
|
|
|
|
# Cluster inaccessible from this unit
|
|
self.status_set.reset_mock()
|
|
_status.return_value = None
|
|
_check.return_value = None, None
|
|
_conn_check.return_value = True
|
|
midbc._assess_status()
|
|
self.status_set.assert_called_once_with(
|
|
"blocked",
|
|
"Cluster is inaccessible from this instance. "
|
|
"Please check logs for details.")
|
|
|
|
# Cluster not healthy
|
|
self.status_set.reset_mock()
|
|
_status.return_value = "Not Okay"
|
|
_status_text.return_value = "Cluster not healthy"
|
|
_check.return_value = None, None
|
|
_conn_check.return_value = True
|
|
midbc._assess_status()
|
|
self.status_set.assert_called_once_with(
|
|
"blocked", "MySQL InnoDB Cluster not healthy: Cluster not healthy")
|
|
|
|
# Departing
|
|
self.is_flag_set.return_value = True
|
|
self.status_set.reset_mock()
|
|
_check.reset_mock()
|
|
midbc._assess_status()
|
|
_check.assert_not_called()
|
|
self.status_set.assert_called_once_with(
|
|
"waiting", "This unit is departing. Shutting down.")
|
|
|
|
def test_get_cluster_status_not_clustered(self):
|
|
self.leader_data = {
|
|
'cluster-instance-clustered-10-10-50-50': 'False',
|
|
}
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
self.get_relation_ip.return_value = "10.10.50.50"
|
|
self.assertIsNone(midbc.get_cluster_status())
|
|
midbc.wait_until_cluster_available = mock.MagicMock()
|
|
midbc.wait_until_cluster_available.assert_not_called()
|
|
|
|
def test_get_cluster_status(self):
|
|
self.leader_data = {
|
|
'cluster-instance-clustered-10-10-50-50': 'True',
|
|
}
|
|
_local_addr = "10.10.50.50"
|
|
_name = "theCluster"
|
|
_string = "status output"
|
|
_json_string = '"status output"'
|
|
self.get_relation_ip.return_value = _local_addr
|
|
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.options.cluster_name = _name
|
|
midbc.wait_until_cluster_available = mock.MagicMock()
|
|
midbc.run_mysqlsh_script = mock.MagicMock()
|
|
midbc.run_mysqlsh_script.return_value = _json_string.encode("UTF-8")
|
|
|
|
_script = (
|
|
"shell.connect('{}:{}@{}')\n"
|
|
"cluster = dba.get_cluster('{}')\n"
|
|
"print(cluster.status({{'extended': 0}}))"
|
|
.format(
|
|
midbc.cluster_user, midbc.cluster_password,
|
|
midbc.cluster_address, midbc.cluster_name))
|
|
|
|
self.assertEqual(_string, midbc.get_cluster_status())
|
|
midbc.wait_until_cluster_available.assert_called_once()
|
|
midbc.run_mysqlsh_script.assert_called_once_with(_script)
|
|
|
|
# Cached data
|
|
midbc.run_mysqlsh_script.reset_mock()
|
|
midbc._cached_cluster_status = _string
|
|
self.assertEqual(_string, midbc.get_cluster_status())
|
|
midbc.run_mysqlsh_script.assert_not_called()
|
|
|
|
# Nocache requested
|
|
midbc.run_mysqlsh_script.reset_mock()
|
|
midbc._cached_cluster_status = _string
|
|
self.assertEqual(_string, midbc.get_cluster_status(nocache=True))
|
|
midbc.run_mysqlsh_script.assert_called_once_with(_script)
|
|
|
|
def test_get_cluster_status_summary(self):
|
|
_status_dict = {"defaultReplicaSet": {"status": "OK"}}
|
|
_status_obj = mock.MagicMock()
|
|
_status_obj.return_value = _status_dict
|
|
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.get_cluster_status = _status_obj
|
|
|
|
self.assertEqual("OK", midbc.get_cluster_status_summary())
|
|
_status_obj.assert_called_once_with(nocache=False)
|
|
|
|
# Cached data
|
|
_status_obj.reset_mock()
|
|
midbc._cached_cluster_status = _status_dict
|
|
self.assertEqual("OK", midbc.get_cluster_status_summary())
|
|
_status_obj.assert_not_called()
|
|
|
|
# Nocache requested
|
|
_status_obj.reset_mock()
|
|
midbc._cached_cluster_status = _status_dict
|
|
self.assertEqual("OK", midbc.get_cluster_status_summary(nocache=True))
|
|
_status_obj.assert_called_once_with(nocache=True)
|
|
|
|
def test__error_str(self):
|
|
import subprocess
|
|
mysql_innodb_cluster.subprocess = subprocess
|
|
e1 = subprocess.CalledProcessError(
|
|
returncode=1,
|
|
cmd="the-command",
|
|
stderr=b"some-stderr",
|
|
output=b"the-output")
|
|
e2 = subprocess.CalledProcessError(
|
|
returncode=1,
|
|
cmd="the-command",
|
|
stderr=None,
|
|
output=b"the-output")
|
|
e3 = Exception("an-exception")
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
self.assertEqual(midbc._error_str(e1), "some-stderr")
|
|
self.assertEqual(
|
|
midbc._error_str(e2),
|
|
"Command 'the-command' returned non-zero exit status 1.")
|
|
self.assertEqual(midbc._error_str(e3), "an-exception")
|
|
|
|
def test_get_cluster_primary_address(self):
|
|
_addr = "10.5.50.76"
|
|
_status_dict = {
|
|
"groupInformationSourceMember": "{}:3360".format(_addr)}
|
|
_status_obj = mock.MagicMock()
|
|
_status_obj.return_value = _status_dict
|
|
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.get_cluster_status = _status_obj
|
|
|
|
self.assertEqual(_addr, midbc.get_cluster_primary_address())
|
|
_status_obj.assert_called_once_with(nocache=False)
|
|
|
|
# Cached data
|
|
_status_obj.reset_mock()
|
|
midbc._cached_cluster_status = _status_dict
|
|
self.assertEqual(_addr, midbc.get_cluster_primary_address())
|
|
_status_obj.assert_not_called()
|
|
|
|
# Nocache requested
|
|
_status_obj.reset_mock()
|
|
midbc._cached_cluster_status = _status_dict
|
|
self.assertEqual(
|
|
_addr, midbc.get_cluster_primary_address(nocache=True))
|
|
_status_obj.assert_called_once_with(nocache=True)
|
|
|
|
def test_get_cluster_status_text(self):
|
|
_status_dict = {"defaultReplicaSet": {"statusText": "Text"}}
|
|
_status_obj = mock.MagicMock()
|
|
_status_obj.return_value = _status_dict
|
|
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.get_cluster_status = _status_obj
|
|
|
|
self.assertEqual("Text", midbc.get_cluster_status_text())
|
|
_status_obj.assert_called_once_with(nocache=False)
|
|
|
|
# Cached data
|
|
_status_obj.reset_mock()
|
|
midbc._cached_cluster_status = _status_dict
|
|
self.assertEqual("Text", midbc.get_cluster_status_text())
|
|
_status_obj.assert_not_called()
|
|
|
|
# Nocache requested
|
|
_status_obj.reset_mock()
|
|
midbc._cached_cluster_status = _status_dict
|
|
self.assertEqual("Text", midbc.get_cluster_status_text(nocache=True))
|
|
_status_obj.assert_called_once_with(nocache=True)
|
|
|
|
# verify None returned if _status is empty or lacks key
|
|
_status_obj.return_value = None
|
|
self.assertIsNone(midbc.get_cluster_status_text(nocache=True))
|
|
_status_obj.return_value = {
|
|
"defaultReplicaSet": {}}
|
|
self.assertIsNone(midbc.get_cluster_status_text(nocache=True))
|
|
|
|
def test_get_cluster_instance_mode(self):
|
|
_local_addr = "10.10.50.50"
|
|
self.get_relation_ip.return_value = _local_addr
|
|
_mode = "R/O"
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
_status_dict = {
|
|
"defaultReplicaSet":
|
|
{"topology":
|
|
{"{}:{}".format(_local_addr, midbc.cluster_port):
|
|
{"mode": _mode}}}}
|
|
_status_obj = mock.MagicMock()
|
|
_status_obj.return_value = _status_dict
|
|
midbc.get_cluster_status = _status_obj
|
|
|
|
self.assertEqual(_mode, midbc.get_cluster_instance_mode())
|
|
_status_obj.assert_called_once_with(nocache=False)
|
|
|
|
# Cached data
|
|
_status_obj.reset_mock()
|
|
midbc._cached_cluster_status = _status_dict
|
|
self.assertEqual(_mode, midbc.get_cluster_instance_mode())
|
|
_status_obj.assert_not_called()
|
|
|
|
# Nocache requested
|
|
_status_obj.reset_mock()
|
|
midbc._cached_cluster_status = _status_dict
|
|
self.assertEqual(_mode, midbc.get_cluster_instance_mode(nocache=True))
|
|
_status_obj.assert_called_once_with(nocache=True)
|
|
|
|
# verify None returned if _status is empty or lacks key
|
|
_status_obj.return_value = None
|
|
self.assertIsNone(midbc.get_cluster_instance_mode(nocache=True))
|
|
_status_obj.return_value = {
|
|
"defaultReplicaSet":
|
|
{"topology": {}}}
|
|
self.assertIsNone(midbc.get_cluster_instance_mode(nocache=True))
|
|
|
|
def test_check_mysql_connection(self):
|
|
self.patch_object(
|
|
mysql_innodb_cluster.mysql.MySQLdb, "_exceptions")
|
|
self._exceptions.OperationalError = Exception
|
|
_helper = mock.MagicMock()
|
|
_pass = "pass"
|
|
_root_pass = "differentpass"
|
|
_user = "user"
|
|
_addr = "10.20.30.30"
|
|
self.data = {"mysql.passwd": _root_pass}
|
|
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.get_db_helper = mock.MagicMock()
|
|
midbc.get_db_helper.return_value = _helper
|
|
midbc._get_password = mock.MagicMock()
|
|
midbc._get_password.side_effect = self._fake_data
|
|
|
|
self.assertTrue(
|
|
midbc.check_mysql_connection(
|
|
username=_user, password=_pass, address=_addr))
|
|
_helper.connect.assert_called_once_with(
|
|
user=_user, password=_pass, host=_addr)
|
|
|
|
_helper.reset_mock()
|
|
_helper.connect.side_effect = self._exceptions.OperationalError
|
|
self.assertFalse(midbc.check_mysql_connection())
|
|
_helper.connect.assert_called_once_with(
|
|
user="root", password=_root_pass, host="localhost")
|
|
|
|
def test_wait_unit_connectable(self):
|
|
_pass = "pass"
|
|
_user = "user"
|
|
_addr = "10.20.40.40"
|
|
_conn_check = mock.MagicMock()
|
|
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.check_mysql_connection = _conn_check
|
|
|
|
# Successful connect
|
|
_conn_check.return_value = True
|
|
midbc.wait_until_connectable(
|
|
username=_user, password=_pass, address=_addr)
|
|
_conn_check.assert_called_once_with(
|
|
username=_user, password=_pass, address=_addr)
|
|
|
|
# Failed to connect
|
|
_conn_check.reset_mock()
|
|
_conn_check.return_value = False
|
|
with self.assertRaises(mysql_innodb_cluster.CannotConnectToMySQL):
|
|
midbc.wait_until_connectable()
|
|
_conn_check.assert_called_once_with(
|
|
username=None, password=None, address=None)
|
|
|
|
def test_wait_unit_cluster_available(self):
|
|
_name = "theCluster"
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.options.cluster_name = _name
|
|
midbc.run_mysqlsh_script = mock.MagicMock()
|
|
_script = (
|
|
"shell.connect('{}:{}@{}')\n"
|
|
"cluster = dba.get_cluster('{}')"
|
|
.format(
|
|
midbc.cluster_user, midbc.cluster_password,
|
|
midbc.cluster_address, midbc.cluster_name))
|
|
|
|
# Cluster available
|
|
midbc.wait_until_cluster_available()
|
|
midbc.run_mysqlsh_script.assert_called_once_with(_script)
|
|
|
|
# Cluster not available
|
|
midbc.run_mysqlsh_script.reset_mock()
|
|
midbc.run_mysqlsh_script.side_effect = (Exception)
|
|
with self.assertRaises(Exception):
|
|
midbc.wait_until_cluster_available()
|
|
midbc.run_mysqlsh_script.assert_called_once_with(_script)
|
|
|
|
def test_run_mysqlsh_script(self):
|
|
self.patch_object(mysql_innodb_cluster.os.path, "exists")
|
|
self.exists.return_value = True
|
|
_byte_string = "UTF-8 byte string".encode("UTF-8")
|
|
self.subprocess.check_output.return_value = _byte_string
|
|
_script = "print('Hello World!')"
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
mysql_innodb_cluster.subprocess = self.subprocess
|
|
self.assertEqual(
|
|
_byte_string,
|
|
midbc.run_mysqlsh_script(_script))
|
|
self.subprocess.check_output.assert_called_once_with(
|
|
[midbc.mysqlsh_bin, "--no-wizard", "--python", "-f",
|
|
self.filename], stderr=self.subprocess.PIPE)
|
|
self.file.write.assert_called_once_with(_script)
|
|
self.subprocess.check_call.assert_not_called()
|
|
|
|
# No self.mysqlsh_common_dir
|
|
self.exists.return_value = False
|
|
self.assertEqual(
|
|
_byte_string,
|
|
midbc.run_mysqlsh_script(_script))
|
|
self.subprocess.check_call.assert_called_once_with(
|
|
[midbc.mysqlsh_bin, "--help"], stderr=self.subprocess.PIPE)
|
|
|
|
def test_mysqldump(self):
|
|
self.patch_object(mysql_innodb_cluster.datetime, "datetime")
|
|
_now = mock.MagicMock()
|
|
self.datetime.now.return_value = _now
|
|
_time = "_now_"
|
|
_now.strftime.return_value = _time
|
|
_path = "/tmp/backup"
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.write_root_my_cnf = mock.MagicMock()
|
|
|
|
# All DBs
|
|
_filename = "{}/mysqldump-all-databases-{}".format(_path, _time)
|
|
_calls = [
|
|
mock.call(
|
|
["/usr/bin/mysqldump", "-u", "root", "--triggers",
|
|
"--routines", "--events", "--ignore-table=mysql.event",
|
|
"--set-gtid-purged=COMMENTED",
|
|
"--result-file", _filename, "--all-databases"]),
|
|
mock.call(["/usr/bin/gzip", _filename])]
|
|
mysql_innodb_cluster.subprocess = self.subprocess
|
|
self.assertEqual(midbc.mysqldump(_path), "{}.gz".format(_filename))
|
|
midbc.write_root_my_cnf.assert_called_once()
|
|
self.subprocess.check_call.assert_has_calls(_calls)
|
|
|
|
# One DB
|
|
self.subprocess.check_call.reset_mock()
|
|
_dbs = "mydb"
|
|
_filename = "{}/mysqldump-{}-{}".format(_path, _dbs, _time)
|
|
_calls = [
|
|
mock.call(
|
|
["/usr/bin/mysqldump", "-u", "root", "-ppass", "--triggers",
|
|
"--routines", "--events", "--ignore-table=mysql.event",
|
|
"--set-gtid-purged=COMMENTED",
|
|
"--result-file", _filename, "--databases", _dbs]),
|
|
mock.call(["/usr/bin/gzip", _filename])]
|
|
self.assertEqual(midbc.mysqldump(_path, databases=_dbs),
|
|
"{}.gz".format(_filename))
|
|
|
|
# Multiple DBs
|
|
self.subprocess.check_call.reset_mock()
|
|
_dbs = "mydb,anotherdb"
|
|
_filename = "{}/mysqldump-{}-{}".format(
|
|
_path, "-".join(_dbs.split(",")), _time)
|
|
_calls = [
|
|
mock.call(
|
|
["/usr/bin/mysqldump", "-u", "root", "-ppass", "--triggers",
|
|
"--routines", "--events", "--ignore-table=mysql.event",
|
|
"--set-gtid-purged=COMMENTED",
|
|
"--result-file", _filename, "--databases"].extend(
|
|
_dbs.split(","))),
|
|
mock.call(["/usr/bin/gzip", _filename])]
|
|
self.assertEqual(midbc.mysqldump(_path, databases=_dbs),
|
|
"{}.gz".format(_filename))
|
|
|
|
def test_restore_mysqldump(self):
|
|
self.patch("builtins.open",
|
|
new_callable=mock.mock_open(),
|
|
name="_open")
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.write_root_my_cnf = mock.MagicMock()
|
|
|
|
_dump_file = "/home/ubuntu/dump.sql.gz"
|
|
|
|
_restore = mock.MagicMock(name="RESTORE")
|
|
_sql = mock.MagicMock()
|
|
self._open.return_value = _sql
|
|
self.subprocess.Popen.return_value = _restore
|
|
mysql_innodb_cluster.subprocess = self.subprocess
|
|
midbc.restore_mysqldump(_dump_file)
|
|
midbc.write_root_my_cnf.assert_called_once()
|
|
self.subprocess.check_call.assert_called_once_with(
|
|
["gunzip", _dump_file])
|
|
self.subprocess.Popen.assert_called_once_with(
|
|
["mysql", "-u", "root"], stdin=self.subprocess.PIPE)
|
|
_restore.communicate.assert_called_once_with(
|
|
input=_sql.__enter__().read())
|
|
|
|
def test_set_cluster_option(self):
|
|
_name = "theCluster"
|
|
_string = "status output"
|
|
_key = "option_name"
|
|
_value = "option_value"
|
|
_local_addr = "10.10.50.50"
|
|
self.get_relation_ip.return_value = _local_addr
|
|
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.get_cluster_primary_address = mock.MagicMock(
|
|
return_value=_local_addr)
|
|
midbc.options.cluster_name = _name
|
|
midbc.run_mysqlsh_script = mock.MagicMock()
|
|
midbc.run_mysqlsh_script.return_value = _string.encode("UTF-8")
|
|
|
|
_script = (
|
|
"shell.connect('{}:{}@{}')\n"
|
|
"cluster = dba.get_cluster('{}')\n"
|
|
"cluster.set_option('{}', {})"
|
|
.format(
|
|
midbc.cluster_user, midbc.cluster_password,
|
|
midbc.cluster_address, midbc.options.cluster_name,
|
|
_key, _value))
|
|
self.assertEqual(_string, midbc.set_cluster_option(_key, _value))
|
|
midbc.run_mysqlsh_script.assert_called_once_with(_script)
|
|
|
|
def test_get_ip_allowlist_str_from_db(self):
|
|
mock_m_helper = mock.MagicMock()
|
|
mock_m_helper.select.return_value = [
|
|
['group_replication_ip_allowlist', '10.0.0.10/24']]
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
self.assertEqual(
|
|
midbc.get_ip_allowlist_str_from_db(mock_m_helper),
|
|
'10.0.0.10/24')
|
|
mock_m_helper.select.assert_called_once_with(
|
|
"SHOW GLOBAL VARIABLES LIKE 'group_replication_ip_allowlist'")
|
|
mock_m_helper.select.return_value = []
|
|
with self.assertRaises(AssertionError):
|
|
midbc.get_ip_allowlist_str_from_db(mock_m_helper)
|
|
|
|
def test_get_ip_allowlist_list_from_db(self):
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.get_cluster_rw_db_helper = mock.MagicMock(return_value=None)
|
|
midbc.get_ip_allowlist_str_from_db = \
|
|
lambda m_helper: '::1,10.0.0.20/24'
|
|
self.assertEqual(
|
|
midbc.get_ip_allowlist_list_from_db(),
|
|
['::1', '10.0.0.20/24'])
|
|
|
|
def test_is_address_in_replication_ip_allowlist(self):
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
self.assertTrue(midbc.is_address_in_replication_ip_allowlist(
|
|
'10.0.0.10',
|
|
['127.0.0.1', '10.0.0.10/24']))
|
|
|
|
@mock.patch(('charm.openstack.mysql_innodb_cluster.'
|
|
'MySQLInnoDBClusterCharm.cluster_peer_addresses'),
|
|
new_callable=mock.PropertyMock)
|
|
def test_get_denied_peers(self, cluster_peer_addresses):
|
|
def addr_in_list(addr, ip_allowlist):
|
|
return addr == '10.0.0.20'
|
|
cluster_peer_addresses.return_value = ['10.0.0.20', '10.20.0.20']
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.get_ip_allowlist_list_from_db = lambda: ['10.0.0.10/24']
|
|
midbc.is_address_in_replication_ip_allowlist = addr_in_list
|
|
self.assertEqual(
|
|
midbc.get_denied_peers(),
|
|
['10.20.0.20'])
|
|
|
|
def test_reboot_cluster_from_complete_outage(self):
|
|
_pass = "clusterpass"
|
|
_name = "theCluster"
|
|
_string = "status output"
|
|
_local_addr = "10.10.50.50"
|
|
self.get_relation_ip.return_value = _local_addr
|
|
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.options.cluster_name = _name
|
|
midbc.run_mysqlsh_script = mock.MagicMock()
|
|
midbc.run_mysqlsh_script.return_value = _string.encode("UTF-8")
|
|
midbc._get_password = mock.MagicMock()
|
|
midbc._get_password.return_value = _pass
|
|
|
|
_script = (
|
|
"shell.connect('{}:{}@{}')\n"
|
|
"dba.reboot_cluster_from_complete_outage()"
|
|
.format(
|
|
midbc.cluster_user, midbc.cluster_password,
|
|
midbc.cluster_address))
|
|
self.assertEqual(_string, midbc.reboot_cluster_from_complete_outage())
|
|
midbc.run_mysqlsh_script.assert_called_once_with(_script)
|
|
|
|
def test_rejoin_instance(self):
|
|
_pass = "clusterpass"
|
|
_name = "theCluster"
|
|
_string = "status output"
|
|
_local_addr = "10.10.50.50"
|
|
_remote_addr = "10.10.50.70"
|
|
self.get_relation_ip.return_value = _local_addr
|
|
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.get_cluster_primary_address = mock.MagicMock(
|
|
return_value=_local_addr)
|
|
midbc.options.cluster_name = _name
|
|
midbc.run_mysqlsh_script = mock.MagicMock()
|
|
midbc.run_mysqlsh_script.return_value = _string.encode("UTF-8")
|
|
midbc._get_password = mock.MagicMock()
|
|
midbc._get_password.return_value = _pass
|
|
|
|
_script = (
|
|
"shell.connect('{}:{}@{}')\n"
|
|
"cluster = dba.get_cluster('{}')\n"
|
|
"cluster.rejoin_instance('{}:{}@{}')"
|
|
.format(
|
|
midbc.cluster_user, midbc.cluster_password,
|
|
midbc.cluster_address, midbc.options.cluster_name,
|
|
midbc.cluster_user, midbc.cluster_password, _remote_addr))
|
|
self.assertEqual(_string, midbc.rejoin_instance(_remote_addr))
|
|
midbc.run_mysqlsh_script.assert_called_once_with(_script)
|
|
|
|
def test_remove_instance(self):
|
|
_pass = "clusterpass"
|
|
_name = "theCluster"
|
|
_string = "status output"
|
|
_local_addr = "10.10.50.50"
|
|
_remote_addr = "10.10.50.70"
|
|
self.get_relation_ip.return_value = _local_addr
|
|
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.get_cluster_primary_address = mock.MagicMock(
|
|
return_value=_local_addr)
|
|
midbc.options.cluster_name = _name
|
|
midbc.run_mysqlsh_script = mock.MagicMock()
|
|
midbc.run_mysqlsh_script.return_value = _string.encode("UTF-8")
|
|
midbc._get_password = mock.MagicMock()
|
|
midbc._get_password.return_value = _pass
|
|
|
|
_script = (
|
|
"shell.connect('{}:{}@{}')\n"
|
|
"cluster = dba.get_cluster('{}')\n"
|
|
"cluster.remove_instance('{}@{}', {{'force': False}})"
|
|
.format(
|
|
midbc.cluster_user, midbc.cluster_password,
|
|
midbc.cluster_address, midbc.options.cluster_name,
|
|
midbc.cluster_user, _remote_addr))
|
|
self.assertEqual(_string, midbc.remove_instance(_remote_addr))
|
|
midbc.run_mysqlsh_script.assert_called_once_with(_script)
|
|
|
|
def test_cluster_rescan(self):
|
|
_pass = "clusterpass"
|
|
_name = "theCluster"
|
|
_string = "status output"
|
|
_local_addr = "10.10.50.50"
|
|
self.get_relation_ip.return_value = _local_addr
|
|
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.get_cluster_primary_address = mock.MagicMock(
|
|
return_value=_local_addr)
|
|
midbc.options.cluster_name = _name
|
|
midbc.run_mysqlsh_script = mock.MagicMock()
|
|
midbc.run_mysqlsh_script.return_value = _string.encode("UTF-8")
|
|
midbc._get_password = mock.MagicMock()
|
|
midbc._get_password.return_value = _pass
|
|
|
|
_script = (
|
|
"shell.connect('{}:{}@{}')\n"
|
|
"cluster = dba.get_cluster('{}')\n"
|
|
"cluster.rescan()"
|
|
.format(
|
|
midbc.cluster_user, midbc.cluster_password,
|
|
midbc.cluster_address, midbc.options.cluster_name))
|
|
self.assertEqual(_string, midbc.cluster_rescan())
|
|
midbc.run_mysqlsh_script.assert_called_once_with(_script)
|
|
|
|
def test_configure_and_add_instance(self):
|
|
_pass = "clusterpass"
|
|
_name = "theCluster"
|
|
_string = "status output"
|
|
_local_addr = "10.10.50.50"
|
|
_remote_addr = "10.10.50.70"
|
|
_user = "user"
|
|
self.get_relation_ip.return_value = _local_addr
|
|
self.patch_object(
|
|
mysql_innodb_cluster.reactive, "endpoint_from_flag",
|
|
return_value=self.cluster)
|
|
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.get_cluster_primary_address = mock.MagicMock(
|
|
return_value=_local_addr)
|
|
midbc.options.cluster_name = _name
|
|
midbc.run_mysqlsh_script = mock.MagicMock()
|
|
midbc.run_mysqlsh_script.return_value = _string.encode("UTF-8")
|
|
midbc._get_password = mock.MagicMock()
|
|
midbc._get_password.return_value = _pass
|
|
self.data = {
|
|
"cluster-address": _remote_addr,
|
|
"cluster-user": _user,
|
|
"cluster-password": _pass,
|
|
}
|
|
_create_user = mock.MagicMock()
|
|
_create_user.return_value = True
|
|
midbc.create_user = _create_user
|
|
_configure_instance = mock.MagicMock()
|
|
midbc.configure_instance = _configure_instance
|
|
_add_instance_to_cluster = mock.MagicMock()
|
|
midbc.add_instance_to_cluster = _add_instance_to_cluster
|
|
|
|
midbc.configure_and_add_instance(address=_remote_addr)
|
|
_create_user.assert_called_once_with(
|
|
_remote_addr, _user, _pass, "all")
|
|
_configure_instance.assert_called_once_with(_remote_addr)
|
|
_add_instance_to_cluster.assert_called_once_with(_remote_addr)
|
|
|
|
# Not all users created
|
|
_create_user.return_value = False
|
|
with self.assertRaises(Exception):
|
|
midbc.configure_and_add_instance(address=_remote_addr)
|
|
|
|
def test_configure_and_add_instance_bug1983158_success(self):
|
|
_pass = "clusterpass"
|
|
_name = "theCluster"
|
|
_string = "status output"
|
|
_local_addr = "10.10.50.50"
|
|
_remote_addr = "10.10.50.70"
|
|
_user = "user"
|
|
self.get_relation_ip.return_value = _local_addr
|
|
self.patch_object(
|
|
mysql_innodb_cluster.reactive, "endpoint_from_flag",
|
|
return_value=self.cluster)
|
|
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.get_cluster_primary_address = mock.MagicMock(
|
|
return_value=_local_addr)
|
|
midbc.options.cluster_name = _name
|
|
midbc.run_mysqlsh_script = mock.MagicMock()
|
|
midbc.run_mysqlsh_script.return_value = _string.encode("UTF-8")
|
|
midbc._get_password = mock.MagicMock()
|
|
midbc._get_password.return_value = _pass
|
|
self.data = {
|
|
"cluster-address": _remote_addr,
|
|
"cluster-user": _user,
|
|
"cluster-password": _pass,
|
|
}
|
|
midbc.create_user = mock.MagicMock()
|
|
midbc.create_user.return_value = True
|
|
midbc.configure_instance = mock.MagicMock()
|
|
midbc.add_instance_to_cluster = mock.MagicMock()
|
|
midbc.configure_instance.side_effect = [
|
|
subprocess.CalledProcessError(
|
|
1, 'foo', b'output', b'Server in SUPER_READ_ONLY mode'),
|
|
None
|
|
]
|
|
|
|
midbc.configure_and_add_instance(address=_remote_addr)
|
|
midbc.create_user.assert_called_once_with(
|
|
_remote_addr, _user, _pass, "all")
|
|
midbc.configure_instance.assert_has_calls([
|
|
mock.call(_remote_addr), mock.call(_remote_addr)
|
|
])
|
|
midbc.add_instance_to_cluster.assert_called_once_with(_remote_addr)
|
|
|
|
def test_configure_and_add_instance_bug1983158_error(self):
|
|
_pass = "clusterpass"
|
|
_name = "theCluster"
|
|
_string = "status output"
|
|
_local_addr = "10.10.50.50"
|
|
_remote_addr = "10.10.50.70"
|
|
_user = "user"
|
|
self.get_relation_ip.return_value = _local_addr
|
|
self.patch_object(
|
|
mysql_innodb_cluster.reactive, "endpoint_from_flag",
|
|
return_value=self.cluster)
|
|
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.get_cluster_primary_address = mock.MagicMock(
|
|
return_value=_local_addr)
|
|
midbc.options.cluster_name = _name
|
|
midbc.run_mysqlsh_script = mock.MagicMock()
|
|
midbc.run_mysqlsh_script.return_value = _string.encode("UTF-8")
|
|
midbc._get_password = mock.MagicMock()
|
|
midbc._get_password.return_value = _pass
|
|
self.data = {
|
|
"cluster-address": _remote_addr,
|
|
"cluster-user": _user,
|
|
"cluster-password": _pass,
|
|
}
|
|
midbc.create_user = mock.MagicMock()
|
|
midbc.create_user.return_value = True
|
|
midbc.configure_instance = mock.MagicMock()
|
|
midbc.add_instance_to_cluster = mock.MagicMock()
|
|
midbc.configure_instance.side_effect = [
|
|
subprocess.CalledProcessError(1, 'foo', b'output', b'error'),
|
|
]
|
|
with self.assertRaises(subprocess.CalledProcessError):
|
|
midbc.configure_and_add_instance(address=_remote_addr)
|
|
midbc.create_user.assert_called_once_with(
|
|
_remote_addr, _user, _pass, "all")
|
|
midbc.configure_instance.assert_called_once_with(_remote_addr)
|
|
midbc.add_instance_to_cluster.assert_not_called()
|
|
|
|
def test_clear_flags_for_removed_instance(self):
|
|
_addr = "10.5.0.10"
|
|
_expected = {
|
|
"cluster-instance-configured-{}"
|
|
.format(_addr.replace(".", "-")): None,
|
|
"cluster-instance-clustered-{}"
|
|
.format(_addr.replace(".", "-")): None}
|
|
self.is_leader.return_value = True
|
|
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.clear_flags_for_removed_instance(_addr)
|
|
self.leader_set.assert_called_once_with(_expected)
|
|
|
|
def test_update_dotted_flags(self):
|
|
_existing = {
|
|
"cluster-instance-configured-10.5.0.10": True,
|
|
"cluster-instance-configured-10.5.0.20": True,
|
|
"cluster-instance-configured-10.5.0.30": True,
|
|
"key": "value",
|
|
"mysql.passwd": "must-not-change",
|
|
"cluster-instance-configured-10-5-0-40": True}
|
|
_expected = {
|
|
"cluster-instance-configured-10.5.0.10": None,
|
|
"cluster-instance-configured-10-5-0-10": True,
|
|
"cluster-instance-configured-10.5.0.20": None,
|
|
"cluster-instance-configured-10-5-0-20": True,
|
|
"cluster-instance-configured-10.5.0.30": None,
|
|
"cluster-instance-configured-10-5-0-30": True,
|
|
"key": "value",
|
|
"mysql.passwd": "must-not-change",
|
|
"cluster-instance-configured-10-5-0-40": True}
|
|
|
|
self.is_leader.return_value = True
|
|
self.leader_get.side_effect = None
|
|
self.leader_get.return_value = _existing
|
|
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.update_dotted_flags()
|
|
self.leader_set.assert_called_once_with(_expected)
|
|
|
|
@mock.patch(('charm.openstack.mysql_innodb_cluster.'
|
|
'MySQLInnoDBClusterCharm.cluster_peer_addresses'),
|
|
new_callable=mock.PropertyMock)
|
|
@mock.patch(('charm.openstack.mysql_innodb_cluster.'
|
|
'MySQLInnoDBClusterCharm.cluster_address'),
|
|
new_callable=mock.PropertyMock)
|
|
def test_get_clustered_addresses(self, cluster_address,
|
|
cluster_peer_addresses):
|
|
self.leader_data = {
|
|
"cluster-instance-clustered-10-5-0-10": "True",
|
|
"cluster-instance-clustered-10-5-0-20": "True",
|
|
"cluster-instance-clustered-10-5-0-30": "False",
|
|
"key": "value",
|
|
"mysql.passwd": "must-not-change",
|
|
"cluster-instance-configured-10-5-0-40": "True"}
|
|
cluster_address.return_value = '10.5.0.10'
|
|
cluster_peer_addresses.return_value = [
|
|
'10.5.0.20',
|
|
'10.5.0.30',
|
|
'10.5.0.40']
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
self.assertEqual(
|
|
midbc.get_clustered_addresses(),
|
|
['10.5.0.20', '10.5.0.10'])
|
|
|
|
def test_update_acls(self):
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc.generate_ip_allowlist_str = lambda: '10.0.0.0/24'
|
|
midbc.get_clustered_addresses = lambda: ['10.0.0.10']
|
|
midbc.get_ip_allowlist_str_from_db = lambda x: '10.0.0.0/24'
|
|
midbc.wait_for_cluster_state = lambda x, y, z: None
|
|
m_helper_mock = mock.MagicMock()
|
|
self.patch_object(
|
|
mysql_innodb_cluster.mysql, "MySQL8Helper",
|
|
return_value=m_helper_mock)
|
|
midbc.update_acls()
|
|
self.assertFalse(m_helper_mock.execute.called)
|
|
|
|
# Test update needed
|
|
m_helper_mock.reset_mock()
|
|
midbc.generate_ip_allowlist_str = lambda: '10.0.0.0/24,10.10.0.0/24'
|
|
midbc.update_acls()
|
|
m_helper_mock.execute.assert_has_calls([
|
|
mock.call('STOP GROUP_REPLICATION'),
|
|
mock.call(
|
|
("SET GLOBAL group_replication_ip_allowlist = "
|
|
"'10.0.0.0/24,10.10.0.0/24'")),
|
|
mock.call('START GROUP_REPLICATION')])
|
|
|
|
def test_prometheus_exporter_user(self):
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
self.assertEqual(
|
|
midbc.prometheus_exporter_user,
|
|
"prom_exporter")
|
|
|
|
def test_prometheus_exporter_password(self):
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
midbc._get_password = mock.MagicMock()
|
|
midbc._get_password.side_effect = self._fake_data
|
|
_pass = "pass123"
|
|
self.data = {"mysql.passwd": _pass}
|
|
self.assertEqual(
|
|
midbc.mysql_password,
|
|
_pass)
|
|
|
|
def test_prometheus_exporter_port(self):
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
self.assertEqual(
|
|
midbc.prometheus_exporter_port,
|
|
"9104")
|
|
|
|
def test_get_service_usernames(self):
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
self.leader_data = {
|
|
"mysql.passwd": "mysql-password",
|
|
"cinder.passwd": "cinder-password",
|
|
"mysql-keystone.passwd": "keystone-password",
|
|
"other-key": "other-key-value",
|
|
}
|
|
self.assertEqual(midbc.get_service_usernames(), ['cinder', 'keystone'])
|
|
|
|
def test_rotate_service_user_passwd(self):
|
|
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
|
|
self.leader_data = {
|
|
"mysql.passwd": "mysql-password",
|
|
"cinder.passwd": "cinder-password",
|
|
"mysql-keystone.passwd": "keystone-password",
|
|
"other-key": "other-key-value",
|
|
}
|
|
self.is_leader.return_value = False
|
|
with self.assertRaises(exceptions.NotLeaderError):
|
|
midbc.rotate_service_user_passwd("hello", self.kmr_db_router)
|
|
self.is_leader.return_value = True
|
|
with self.assertRaises(exceptions.InvalidServiceUserError):
|
|
midbc.rotate_service_user_passwd("hello", self.kmr_db_router)
|
|
|
|
mock_db_helper = mock.MagicMock()
|
|
with mock.patch.object(
|
|
midbc, 'get_cluster_rw_db_helper') as mock_get_cluster_db:
|
|
mock_get_cluster_db.return_value = None
|
|
with self.assertRaises(exceptions.NotInCluster):
|
|
midbc.rotate_service_user_passwd(
|
|
"keystone", self.kmr_db_router)
|
|
mock_get_cluster_db.return_value = mock_db_helper
|
|
mock_db_helper.user_host_list.return_value = [
|
|
('cinder', 'hosta'),
|
|
('keystone', 'hostb'),
|
|
]
|
|
self.pwgen.return_value = "super-password"
|
|
|
|
# call with db_router as None
|
|
midbc.rotate_service_user_passwd('keystone', None)
|
|
|
|
mock_db_helper.set_mysql_password_using_current_connection \
|
|
.assert_called_once_with(
|
|
'keystone', 'super-password', ['hostb'])
|
|
|
|
self._assert_regex_in_log(r"^No db_router relations made")
|
|
|
|
# now test with a db_router
|
|
unit1 = mock.MagicMock()
|
|
unit1.recieved = [('a', 'aa'), ('b', 'bb')]
|
|
unit1.relation.relation_id = 'db_router:1'
|
|
relation_mock = mock.MagicMock()
|
|
self.kmr_db_router.all_joined_units = [unit1]
|
|
self.kmr_db_router.relations = {
|
|
'db_router:1': relation_mock,
|
|
}
|
|
self.patch_object(mysql_innodb_cluster, 'mysql')
|
|
self.mysql.get_db_data.return_value = {
|
|
'MRUP': {
|
|
'some-key': 'some-value',
|
|
'username': 'keystone',
|
|
}
|
|
}
|
|
midbc.rotate_service_user_passwd('keystone', self.kmr_db_router)
|
|
|
|
self._assert_regex_in_log(
|
|
r"^Setting password .* db_router:1 on key MRUP_password")
|
|
relation_mock.to_publish_app.__setitem__.assert_called_once_with(
|
|
'MRUP_password', 'super-password')
|
|
self.mysql.get_db_data.assert_called_once_with(
|
|
{}, unprefixed='MICUP')
|
|
|
|
def _assert_regex_in_log(self, regex):
|
|
pattern = re.compile(regex)
|
|
calls = self.log.call_args_list
|
|
for call in calls:
|
|
args = call[0]
|
|
msg = args[0]
|
|
print("Log message: {}".format(msg))
|
|
if pattern.match(msg):
|
|
return
|
|
self.fail("regex {} not found in any log.".format(regex))
|