Scale in and scale out

Handle adding and removing units of the mysql-innodb-cluster gracefully.

Review and land:
https://github.com/openstack-charmers/zaza/pull/353
https://github.com/openstack-charmers/zaza-openstack-tests/pull/275

Closes-Bug: #1874479
Closes-Bug: #1877546
Closes-Bug: #1877616
Depends-On: Ie658c42b095bcff822cdfb0b771d41704ddc85ea
Change-Id: I39210c760e7204f84365d5dcb4eeedbc0889041f
This commit is contained in:
David Ames 2020-05-13 11:37:31 -07:00
parent 1519f17aaa
commit 7cbf9a88b0
10 changed files with 398 additions and 28 deletions

View File

@ -29,6 +29,10 @@ reboot-cluster-from-complete-outage:
description: |
In the case of a complete outage, reboot the cluster from this instance's
GTID superset.
cluster-rescan:
description: |
Clean up cluster metadata by rescanning the cluster.
See https://dev.mysql.com/doc/refman/8.0/en/mysql-innodb-cluster-working-with-cluster.html#rescan-cluster
rejoin-instance:
params:
address:
@ -38,6 +42,25 @@ rejoin-instance:
Rejoin an instance to the cluster. *Note* This action must be run on an
instance that is a functioning member of the cluster. For example, after a
complete outage the unit which ran reboot-cluster-from-complete-outage.
remove-instance:
params:
address:
type: string
description: Address of the instance to be removed from the cluster
force:
type: boolean
description: Remove the instance even if it is unreachable.
description: |
Remove an instance from the cluster. *Note* This action must be run on an
instance that is a functioning member of the cluster.
add-instance:
params:
address:
type: string
description: Address of the instance to add to the cluster
description: |
Configure and add an instnace to the cluster. *Note* This action must be
run on an instance that is a functioning member of the cluster.
set-cluster-option:
params:
key:

View File

@ -171,6 +171,26 @@ def reboot_cluster_from_complete_outage(args):
)
def cluster_rescan(args):
"""Rescan the cluster
Execute cluster.rescan() to clean up metadata.
:param args: sys.argv
:type args: sys.argv
:side effect: Calls instance.cluster_rescan
:returns: This function is called for its side effect
:rtype: None
:action return: Dictionary with command output
"""
with charm.provide_charm_instance() as instance:
output = instance.cluster_rescan()
ch_core.hookenv.action_set({
"output": output,
"outcome": "Success"}
)
def rejoin_instance(args):
"""Rejoin a given instance to the cluster.
@ -199,6 +219,60 @@ def rejoin_instance(args):
)
def add_instance(args):
"""Add an instance to the cluster.
If a new instance is not able to be joined to the cluster, this action will
configure and add the unit to the cluster.
:param args: sys.argv
:type args: sys.argv
:side effect: Calls instance.configure_and_add_instance
:returns: This function is called for its side effect
:rtype: None
:action param address: String address of the instance to be joined
:action return: Dictionary with command output
"""
# Note: Due to issues/# reactive does not initiate Endpoints during an
# action execution. This is here to work around that until the issue is
# resolved.
reactive.Endpoint._startup()
address = ch_core.hookenv.action_get("address")
with charm.provide_charm_instance() as instance:
output = instance.configure_and_add_instance(address)
ch_core.hookenv.action_set({
"output": output,
"outcome": "Success"}
)
def remove_instance(args):
"""Remove an instance from the cluster.
This action cleanly removes an instance from the cluster. If an instance
has died and is unrecoverable it shows up in metadata as MISSING. This
action will remove an instance from the metadata using the force option
even if it is unreachable.
:param args: sys.argv
:type args: sys.argv
:side effect: Calls instance.remove_instance
:returns: This function is called for its side effect
:rtype: None
:action param address: String address of the instance to be removed
:action param force: Boolean force removal of missing instance
:action return: Dictionary with command output
"""
address = ch_core.hookenv.action_get("address")
force = ch_core.hookenv.action_get("force")
with charm.provide_charm_instance() as instance:
output = instance.remove_instance(address, force=force)
ch_core.hookenv.action_set({
"output": output,
"outcome": "Success"}
)
def set_cluster_option(args):
"""Set cluster option.
@ -231,7 +305,10 @@ ACTIONS = {"mysqldump": mysqldump, "cluster-status": cluster_status,
"set-cluster-option": set_cluster_option,
"reboot-cluster-from-complete-outage":
reboot_cluster_from_complete_outage,
"rejoin-instance": rejoin_instance}
"rejoin-instance": rejoin_instance,
"add-instance": add_instance,
"remove-instance": remove_instance,
"cluster-rescan": cluster_rescan}
def main(args):

1
src/actions/add-instance Symbolic link
View File

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

1
src/actions/cluster-rescan Symbolic link
View File

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

1
src/actions/remove-instance Symbolic link
View File

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

View File

@ -470,8 +470,13 @@ class MySQLInnoDBClusterCharm(charms_openstack.charm.OpenStackCharm):
if cluster_address in self.cluster_address:
addresses.append("localhost")
m_helper = self.get_db_helper()
m_helper.connect(password=self.mysql_password)
# If this is scale out and the cluster already exists, use the cluster
# RW node for writes.
m_helper = self.get_cluster_rw_db_helper()
if not m_helper:
m_helper = self.get_db_helper()
m_helper.connect(password=self.mysql_password)
for address in addresses:
try:
m_helper.execute(SQL_CLUSTER_USER_CREATE.format(
@ -603,12 +608,14 @@ class MySQLInnoDBClusterCharm(charms_openstack.charm.OpenStackCharm):
:returns: This function is called for its side effect
:rtype: None
"""
_primary = self.get_cluster_primary_address(nocache=True)
_script = (
"shell.connect('{}:{}@{}')\n"
"cluster = dba.get_cluster('{}')\n"
"cluster.set_option('{}', {})"
.format(
self.cluster_user, self.cluster_password, self.cluster_address,
self.cluster_user, self.cluster_password,
_primary or self.cluster_address,
self.options.cluster_name, key, value))
try:
output = self.run_mysqlsh_script(_script).decode("UTF-8")
@ -626,7 +633,7 @@ class MySQLInnoDBClusterCharm(charms_openstack.charm.OpenStackCharm):
:param self: Self
:type self: MySQLInnoDBClusterCharm instance
:param address: Address of the MySQL instance to be configured
:param address: Address of the MySQL instance to be clustered
:type address: str
:side effect: Calls self.run_mysqlsh_script
:returns: This function is called for its side effect
@ -639,6 +646,7 @@ class MySQLInnoDBClusterCharm(charms_openstack.charm.OpenStackCharm):
.format(address), "WARNING")
return
_primary = self.get_cluster_primary_address(nocache=True)
ch_core.hookenv.log("Adding instance, {}, to the cluster."
.format(address), "INFO")
_script = (
@ -650,7 +658,7 @@ class MySQLInnoDBClusterCharm(charms_openstack.charm.OpenStackCharm):
"{{'recoveryMethod': 'clone'}})"
.format(
user=self.cluster_user, pw=self.cluster_password,
caddr=self.cluster_address,
caddr=_primary or self.cluster_address,
name=self.options.cluster_name, addr=address))
try:
output = self.run_mysqlsh_script(_script)
@ -711,6 +719,7 @@ class MySQLInnoDBClusterCharm(charms_openstack.charm.OpenStackCharm):
:returns: This function is called for its side effect
:rtype: None
"""
_primary = self.get_cluster_primary_address(nocache=True)
ch_core.hookenv.log("Rejoin instance: {}.".format(address))
_script = (
"shell.connect('{user}:{pw}@{caddr}')\n"
@ -718,12 +727,12 @@ class MySQLInnoDBClusterCharm(charms_openstack.charm.OpenStackCharm):
"cluster.rejoin_instance('{user}:{pw}@{addr}')"
.format(
user=self.cluster_user, pw=self.cluster_password,
caddr=self.cluster_address,
caddr=_primary or self.cluster_address,
name=self.cluster_name, addr=address))
try:
output = self.run_mysqlsh_script(_script).decode("UTF-8")
ch_core.hookenv.log(
"Rejoin isntance {} successful: "
"Rejoin instance {} successful: "
"{}".format(address, output),
level="DEBUG")
return output
@ -735,6 +744,107 @@ class MySQLInnoDBClusterCharm(charms_openstack.charm.OpenStackCharm):
# Reraise for action handling
raise e
def remove_instance(self, address, force=False):
"""Remove instance from the cluster
Execute the cluster.remove_instance(address) to remove an instance from
the cluster.
:param self: Self
:type self: MySQLInnoDBClusterCharm instance
:side effect: Calls self.run_mysqlsh_script
:returns: This function is called for its side effect
:rtype: None
"""
_primary = self.get_cluster_primary_address(nocache=True)
ch_core.hookenv.log("Rejoin instance: {}.".format(address))
_script = (
"shell.connect('{user}:{pw}@{caddr}')\n"
"cluster = dba.get_cluster('{name}')\n"
"cluster.remove_instance('{user}@{addr}', {{'force': {force}}})"
.format(
user=self.cluster_user, pw=self.cluster_password,
caddr=_primary or self.cluster_address,
name=self.cluster_name, addr=address, force=force))
try:
output = self.run_mysqlsh_script(_script).decode("UTF-8")
ch_core.hookenv.log(
"Remove instance {} successful: "
"{}".format(address, output),
level="DEBUG")
return output
except subprocess.CalledProcessError as e:
ch_core.hookenv.log(
"Failed removing instance {}: {}"
.format(address, e.stderr.decode("UTF-8")),
"ERROR")
# Reraise for action handling
raise e
def cluster_rescan(self):
"""Rescan the cluster
Execute the cluster.rescan() to cleanup metadata.
:param self: Self
:type self: MySQLInnoDBClusterCharm instance
:side effect: Calls self.run_mysqlsh_script
:returns: This function is called for its side effect
:rtype: None
"""
_primary = self.get_cluster_primary_address(nocache=True)
ch_core.hookenv.log("Rescanning the cluster.")
_script = (
"shell.connect('{user}:{pw}@{caddr}')\n"
"cluster = dba.get_cluster('{name}')\n"
"cluster.rescan()"
.format(
user=self.cluster_user, pw=self.cluster_password,
caddr=_primary or self.cluster_address,
name=self.cluster_name))
try:
output = self.run_mysqlsh_script(_script).decode("UTF-8")
ch_core.hookenv.log(
"Cluster rescan successful",
level="DEBUG")
return output
except subprocess.CalledProcessError as e:
ch_core.hookenv.log(
"Failed rescanning the cluster.",
"ERROR")
# Reraise for action handling
raise e
def configure_and_add_instance(self, address):
"""Configure and add an instance to the cluster.
If an instance was not able to be joined to the cluster this method
will make sure it is configured and add it to the cluster.
:param self: Self
:type self: MySQLInnoDBClusterCharm instance
:side effect: Calls self.create_user, self.configure_instance and
self.add_instance_to_cluster.
:returns: This function is called for its side effects
:rtype: None
"""
ch_core.hookenv.log(
"Configuring and adding instance to the cluster: {}."
.format(address))
cluster = reactive.endpoint_from_flag("cluster.available")
if not cluster:
raise Exception(
"Cluster relation is not available in order to "
"create cluster user for {}.".format(address))
# Make sure we have the user in the DB
for unit in cluster.all_joined_units:
self.create_cluster_user(
unit.received['cluster-address'],
unit.received['cluster-user'],
unit.received['cluster-password'])
self.configure_instance(address)
self.add_instance_to_cluster(address)
def get_cluster_status(self, nocache=False):
"""Get cluster status
@ -750,6 +860,15 @@ class MySQLInnoDBClusterCharm(charms_openstack.charm.OpenStackCharm):
:returns: Dictionary cluster status output
:rtype: Union[None, dict]
"""
# Speed up when we are not yet clustered
if not reactive.is_flag_set(
"leadership.set.cluster-instance-clustered-{}"
.format(self.cluster_address)):
ch_core.hookenv.log(
"This instance is not yet clustered: cannot determine the "
"cluster status.", "WARNING")
return
# Try the cached version first
if self._cached_cluster_status and not nocache:
return self._cached_cluster_status

View File

@ -66,7 +66,7 @@ def create_local_cluster_user():
@reactive.when('local.cluster.user-created')
@reactive.when('cluster.connected')
@reactive.when_not('cluster.available')
def send_cluster_connection_info(cluster):
def send_cluster_connection_info():
"""Send cluster connection information.
Send cluster user, password and address information over the cluster
@ -75,6 +75,7 @@ def send_cluster_connection_info(cluster):
:param cluster: Cluster interface
:type cluster: MySQLInnoDBClusterPeers object
"""
cluster = reactive.endpoint_from_flag("cluster.connected")
ch_core.hookenv.log("Send cluster connection information.", "DEBUG")
with charm.provide_charm_instance() as instance:
cluster.set_cluster_connection_info(
@ -86,7 +87,7 @@ def send_cluster_connection_info(cluster):
@reactive.when_not('local.cluster.all-users-created')
@reactive.when('cluster.available')
def create_remote_cluster_user(cluster):
def create_remote_cluster_user():
"""Create remote cluster user.
Create the remote cluster peer user and grant cluster permissions in the
@ -95,6 +96,7 @@ def create_remote_cluster_user(cluster):
:param cluster: Cluster interface
:type cluster: MySQLInnoDBClusterPeers object
"""
cluster = reactive.endpoint_from_flag("cluster.available")
ch_core.hookenv.log("Creating remote users.", "DEBUG")
with charm.provide_charm_instance() as instance:
for unit in cluster.all_joined_units:
@ -129,7 +131,7 @@ def initialize_cluster():
@reactive.when('local.cluster.all-users-created')
@reactive.when('cluster.available')
@reactive.when_not('leadership.set.cluster-instances-configured')
def configure_instances_for_clustering(cluster):
def configure_instances_for_clustering():
"""Configure cluster peers for clustering.
Prepare peers to be added to the cluster.
@ -137,6 +139,7 @@ def configure_instances_for_clustering(cluster):
:param cluster: Cluster interface
:type cluster: MySQLInnoDBClusterPeers object
"""
cluster = reactive.endpoint_from_flag("cluster.available")
ch_core.hookenv.log("Configuring instances for clustering.", "DEBUG")
with charm.provide_charm_instance() as instance:
for unit in cluster.all_joined_units:
@ -162,12 +165,13 @@ def configure_instances_for_clustering(cluster):
@reactive.when('leadership.set.cluster-instances-configured')
@reactive.when('cluster.available')
@reactive.when_not('leadership.set.cluster-instances-clustered')
def add_instances_to_cluster(cluster):
def add_instances_to_cluster():
"""Add cluster peers to the cluster.
:param cluster: Cluster interface
:type cluster: MySQLInnoDBClusterPeers object
"""
cluster = reactive.endpoint_from_flag("cluster.available")
ch_core.hookenv.log("Adding instances to cluster.", "DEBUG")
with charm.provide_charm_instance() as instance:
for unit in cluster.all_joined_units:
@ -189,7 +193,7 @@ def add_instances_to_cluster(cluster):
@reactive.when_not('leadership.is_leader')
@reactive.when('leadership.set.cluster-created')
@reactive.when('cluster.available')
def signal_clustered(cluster):
def signal_clustered():
"""Signal unit clustered to peers.
Set this unit clustered on the cluster peer relation.
@ -197,6 +201,7 @@ def signal_clustered(cluster):
:param cluster: Cluster interface
:type cluster: MySQLInnoDBClusterPeers object
"""
cluster = reactive.endpoint_from_flag("cluster.available")
# Optimize clustering by causing a cluster relation changed
with charm.provide_charm_instance() as instance:
if reactive.is_flag_set(
@ -271,3 +276,28 @@ def db_router_respond():
"DB Router relation created DBs and users.", "DEBUG")
reactive.clear_flag('endpoint.db-router.changed')
instance.assess_status()
@reactive.when('endpoint.cluster.changed.unit-configure-ready')
@reactive.when('leadership.set.cluster-instances-clustered')
@reactive.when('leadership.is_leader')
def scale_out():
"""Handle scale-out adding new nodes to an existing cluster."""
ch_core.hookenv.log("Scale out: add new nodes.", "DEBUG")
with charm.provide_charm_instance() as instance:
if not reactive.is_flag_set(
"leadership.set.cluster-instance-clustered-{}"
.format(instance.cluster_address)):
ch_core.hookenv.log(
"Unexpected edge case. This node is the leader but it is "
"not yet clustered. As a non-cluster member it will not be "
"able to join itself to the cluster. Run the 'add_instance' "
"action on a member node with this unit's IP address to join "
"this instance to the cluster.",
"WARNING")
return
create_remote_cluster_user()
configure_instances_for_clustering()
add_instances_to_cluster()
reactive.clear_flag('endpoint.cluster.changed.unit-configure-ready')

View File

@ -12,27 +12,31 @@ configure:
- zaza.openstack.charm_tests.nova.setup.create_flavors
- zaza.openstack.charm_tests.nova.setup.manage_ssh_key
- zaza.openstack.charm_tests.keystone.setup.add_demo_user
- scale_in_out:
- zaza.openstack.charm_tests.keystone.setup.add_demo_user
# The pause and resume test validates changing the R/W primary.
# Running the keystone tests after the MySQLInnoDBClusterTests
# validates DB functionality after a change in the R/W primary.
tests:
# Temporarily disabling the cold start tests due to
# https://bugs.mysql.com/bug.php?id=97279
#- zaza.openstack.charm_tests.mysql.tests.MySQLInnoDBClusterColdStartTest
- zaza.openstack.charm_tests.mysql.tests.MySQLInnoDBClusterTests
- zaza.openstack.charm_tests.keystone.tests.AuthenticationAuthorizationTest
- eoan_full_model:
#- zaza.openstack.charm_tests.mysql.tests.MySQLInnoDBClusterColdStartTest
- zaza.openstack.charm_tests.mysql.tests.MySQLInnoDBClusterTests
- zaza.openstack.charm_tests.keystone.tests.AuthenticationAuthorizationTest
- focal_full_model:
#- zaza.openstack.charm_tests.mysql.tests.MySQLInnoDBClusterColdStartTest
- zaza.openstack.charm_tests.mysql.tests.MySQLInnoDBClusterTests
- zaza.openstack.charm_tests.keystone.tests.AuthenticationAuthorizationTest
- scale_in_out:
# Temporarily disabling the cold start tests due to
# https://bugs.mysql.com/bug.php?id=97279
# - zaza.openstack.charm_tests.mysql.tests.MySQLInnoDBClusterColdStartTest
- zaza.openstack.charm_tests.mysql.tests.MySQLInnoDBClusterScaleTest
- zaza.openstack.charm_tests.keystone.tests.AuthenticationAuthorizationTest
dev_bundles:
gate_bundles:
- focal_full_model: focal-full-ha
- eoan_full_model: eoan-full-ha
- scale_in_out: focal
smoke_bundles:
- focal
tests_options:

View File

@ -202,6 +202,11 @@ class TestMySQLInnoDBClusterCharm(test_utils.PatchHelper):
"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.cluster.all_joined_units = [self.unit1]
# Generic interface
self.interface = mock.MagicMock()
@ -427,6 +432,7 @@ class TestMySQLInnoDBClusterCharm(test_utils.PatchHelper):
midbc = mysql_innodb_cluster.MySQLInnoDBClusterCharm()
midbc.get_db_helper = mock.MagicMock()
midbc.get_db_helper.return_value = _helper
midbc.get_cluster_rw_db_helper = mock.MagicMock(return_value=None)
# Non-local
midbc.create_cluster_user(_addr, _user, _pass)
_calls = [
@ -533,10 +539,13 @@ class TestMySQLInnoDBClusterCharm(test_utils.PatchHelper):
_remote_addr = "10.10.60.60"
_name = "theCluster"
self.get_relation_ip.return_value = _local_addr
self.get_relation_ip.return_value = _local_addr
self.data = {"cluster-password": _pass}
self.is_flag_set.return_value = 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()
@ -1235,6 +1244,8 @@ class TestMySQLInnoDBClusterCharm(test_utils.PatchHelper):
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")
@ -1269,7 +1280,7 @@ class TestMySQLInnoDBClusterCharm(test_utils.PatchHelper):
"dba.reboot_cluster_from_complete_outage()"
.format(
midbc.cluster_user, midbc.cluster_password,
midbc.cluster_address, midbc.options.cluster_name))
midbc.cluster_address))
self.assertEqual(_string, midbc.reboot_cluster_from_complete_outage())
midbc.run_mysqlsh_script.assert_called_once_with(_script)
@ -1282,6 +1293,8 @@ class TestMySQLInnoDBClusterCharm(test_utils.PatchHelper):
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")
@ -1298,3 +1311,95 @@ class TestMySQLInnoDBClusterCharm(test_utils.PatchHelper):
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_cluster_user = mock.MagicMock()
midbc.create_cluster_user = _create_cluster_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_cluster_user.assert_called_once_with(
_remote_addr, _user, _pass)
_configure_instance.assert_called_once_with(_remote_addr)
_add_instance_to_cluster.assert_called_once_with(_remote_addr)

View File

@ -63,6 +63,10 @@ class TestRegisteredHooks(test_utils.TestRegisteredHooks):
"leadership.set.cluster-instances-clustered",
"endpoint.db-router.changed",
"db-router.available",),
"scale_out": (
"endpoint.cluster.changed.unit-configure-ready",
"leadership.set.cluster-instances-clustered",
"leadership.is_leader",),
},
"when_not": {
"leader_install": ("charm.installed",),
@ -141,7 +145,8 @@ class TestMySQLInnoDBClusterHandlers(test_utils.PatchHelper):
self.set_flag.assert_called_once_with("local.cluster.user-created")
def test_send_cluster_connection_info(self):
handlers.send_cluster_connection_info(self.cluster)
self.endpoint_from_flag.return_value = self.cluster
handlers.send_cluster_connection_info()
self.cluster.set_cluster_connection_info.assert_called_once_with(
self.midbc.cluster_address,
self.midbc.cluster_user,
@ -154,7 +159,8 @@ class TestMySQLInnoDBClusterHandlers(test_utils.PatchHelper):
self.data = {"cluster-address": _addr,
"cluster-user": _user,
"cluster-password": _pass}
handlers.create_remote_cluster_user(self.cluster)
self.endpoint_from_flag.return_value = self.cluster
handlers.create_remote_cluster_user()
self.midbc.create_cluster_user.assert_called_once_with(
_addr, _user, _pass)
self.cluster.set_unit_configure_ready.assert_called_once()
@ -169,10 +175,11 @@ class TestMySQLInnoDBClusterHandlers(test_utils.PatchHelper):
def test_configure_instances_for_clustering(self):
_addr = "10.10.10.30"
self.endpoint_from_flag.return_value = self.cluster
# Not ready
self.is_flag_set.return_value = False
self.data = {"cluster-address": _addr}
handlers.configure_instances_for_clustering(self.cluster)
handlers.configure_instances_for_clustering()
self.midbc.configure_instance.assert_not_called()
self.midbc.add_instance_to_cluster.assert_not_called()
self.leader_set.assert_not_called()
@ -181,7 +188,7 @@ class TestMySQLInnoDBClusterHandlers(test_utils.PatchHelper):
self.midbc.reset_mock()
self.is_flag_set.return_value = False
self.data = {"cluster-address": _addr, "unit-configure-ready": True}
handlers.configure_instances_for_clustering(self.cluster)
handlers.configure_instances_for_clustering()
self.midbc.configure_instance.assert_called_once_with(_addr)
self.midbc.add_instance_to_cluster.assert_called_once_with(_addr)
self.leader_set.assert_not_called()
@ -189,7 +196,7 @@ class TestMySQLInnoDBClusterHandlers(test_utils.PatchHelper):
# All ready
self.midbc.reset_mock()
self.is_flag_set.return_value = True
handlers.configure_instances_for_clustering(self.cluster)
handlers.configure_instances_for_clustering()
self.midbc.configure_instance.assert_called_once_with(_addr)
self.midbc.add_instance_to_cluster.assert_called_once_with(_addr)
self.leader_set.assert_called_once_with(
@ -197,32 +204,34 @@ class TestMySQLInnoDBClusterHandlers(test_utils.PatchHelper):
def test_add_instances_to_cluster(self):
_addr = "10.10.10.30"
self.endpoint_from_flag.return_value = self.cluster
# Some but not all
self.is_flag_set.return_value = False
self.data = {"cluster-address": _addr}
handlers.add_instances_to_cluster(self.cluster)
handlers.add_instances_to_cluster()
self.midbc.add_instance_to_cluster.assert_called_once_with(_addr)
self.leader_set.assert_not_called()
# All ready
self.midbc.reset_mock()
self.is_flag_set.return_value = True
handlers.add_instances_to_cluster(self.cluster)
handlers.add_instances_to_cluster()
self.midbc.add_instance_to_cluster.assert_called_once_with(_addr)
self.leader_set.assert_called_once_with(
{"cluster-instances-clustered": True})
def test_signal_clustered(self):
# Unit not clustered
self.endpoint_from_flag.return_value = self.cluster
self.is_flag_set.return_value = False
handlers.signal_clustered(self.cluster)
handlers.signal_clustered()
self.cluster.set_unit_clustered.assert_not_called()
# Unit Clustered
self.midbc.reset_mock()
self.is_flag_set.return_value = True
handlers.signal_clustered(self.cluster)
handlers.signal_clustered()
self.cluster.set_unit_clustered.assert_called_once()
def test_config_changed(self):