import json import logging import mock import os import shutil import sys import tempfile import yaml import charmhelpers.contrib.openstack.ha.utils as ch_ha_utils from charmhelpers.contrib.database.mysql import PerconaClusterHelper from test_utils import CharmTestCase, FakeKvStore sys.modules['MySQLdb'] = mock.Mock() # python-apt is not installed as part of test-requirements but is imported by # some charmhelpers modules so create a fake import. sys.modules['apt'] = mock.Mock() with mock.patch('charmhelpers.contrib.hardening.harden.harden') as mock_dec: mock_dec.side_effect = (lambda *dargs, **dkwargs: lambda f: lambda *args, **kwargs: f(*args, **kwargs)) import percona_hooks as hooks TO_PATCH = ['log', 'config', 'get_db_helper', 'relation_ids', 'relation_set', 'update_nrpe_config', 'is_bootstrapped', 'network_get_primary_address', 'resolve_network_cidr', 'unit_get', 'resolve_hostname_to_ip', 'is_clustered', 'get_ipv6_addr', 'update_hacluster_dns_ha', 'update_hacluster_vip', 'sst_password', 'seeded', 'is_leader', 'leader_node_is_ready', 'get_db_helper', 'peer_store_and_set', 'leader_get', 'relation_clear', 'is_relation_made', 'is_sufficient_peers', 'peer_retrieve_by_prefix', 'client_node_is_ready', 'relation_set', 'relation_get', 'install_mysql_ocf', 'kv'] class TestSharedDBRelation(CharmTestCase): def setUp(self): CharmTestCase.setUp(self, hooks, TO_PATCH) self.network_get_primary_address.side_effect = NotImplementedError self.sst_password.return_value = 'ubuntu' def test_allowed_units_non_leader(self): self.seeded.return_value = True self.is_leader.return_value = False self.client_node_is_ready.return_value = True self.is_relation_made.return_value = True self.relation_ids.return_value = ['shared-db:3'] self.peer_retrieve_by_prefix.return_value = { 'password': 'pass123', 'allowed_units': 'keystone/1 keystone/2'} hooks.shared_db_changed() self.relation_set.assert_called_once_with( allowed_units='keystone/1 keystone/2', password='pass123', relation_id='shared-db:3') @mock.patch.object(hooks, 'get_db_host') @mock.patch.object(hooks, 'configure_db_for_hosts') def test_allowed_units_leader(self, configure_db_for_hosts, get_db_host): self.config.return_value = None allowed_unit_mock = mock.MagicMock() allowed_unit_mock.get_allowed_units.return_value = [ 'keystone/1', 'keystone/2'] self.get_db_helper.return_value = allowed_unit_mock self.test_config.set('access-network', None) self.seeded.return_value = True self.is_leader.return_value = True self.resolve_hostname_to_ip.return_value = '10.0.0.10' self.relation_get.return_value = { 'hostname': 'keystone-0', 'database': 'keystone', 'username': 'keyuser', } get_db_host.return_value = 'dbhost1' configure_db_for_hosts.return_value = 'password' hooks.shared_db_changed() self.relation_set.assert_called_once_with( allowed_units='keystone/1 keystone/2', relation_id=None) calls = [ mock.call( relation_id=None, relation_settings={'access-network': None}), mock.call( relation_id=None, db_host='dbhost1', password='password', allowed_units='keystone/1 keystone/2') ] self.peer_store_and_set.assert_has_calls(calls) class TestHARelation(CharmTestCase): def setUp(self): CharmTestCase.setUp(self, hooks, TO_PATCH) self.network_get_primary_address.side_effect = NotImplementedError self.sst_password.return_value = 'ubuntu' def test_ha_relation_joined(self): # dns-ha: False self.config.return_value = False self.relation_ids.return_value = ['rid:23'] def _add_vip_info(svc, rel_info): rel_info['groups'] = { 'grp_mysql_vips': 'res_mysql_1e39e82_vip'} print(rel_info) self.update_hacluster_vip.side_effect = _add_vip_info hooks.ha_relation_joined() base_settings = { 'clones': { 'cl_mysql_monitor': ( 'res_mysql_monitor meta interleave=true')}, 'colocations': { 'colo_mysql': ( 'inf: grp_mysql_vips ' 'cl_mysql_monitor')}, 'resource_params': { 'res_mysql_monitor': ( 'params user="sstuser" ' 'password="ubuntu" ' 'pid="/var/run/mysqld/mysqld.pid" ' 'socket="/var/run/mysqld/mysqld.sock" ' 'max_slave_lag="5" ' 'cluster_type="pxc" ' 'op monitor interval="1s" ' 'timeout="30s" ' 'OCF_CHECK_LEVEL="1" ' 'meta migration-threshold=INFINITY failure-timeout=5s')}, 'locations': { 'loc_mysql': ( 'grp_mysql_vips ' 'rule inf: writable eq 1')}, 'resources': { 'res_mysql_monitor': 'ocf:percona:mysql_monitor'}, 'delete_resources': ['loc_percona_cluster', 'grp_percona_cluster', 'res_mysql_vip'], 'groups': { 'grp_mysql_vips': 'res_mysql_1e39e82_vip'}} self.update_hacluster_vip.assert_called_once_with( 'mysql', base_settings) settings = { 'json_{}'.format(k): json.dumps(v, **ch_ha_utils.JSON_ENCODE_OPTIONS) for k, v in base_settings.items() if v } self.relation_set.assert_called_once_with( relation_id='rid:23', **settings) def test_ha_relation_joined_dnsha(self): # dns-ha: False self.config.return_value = True self.relation_ids.return_value = ['rid:23'] hooks.ha_relation_joined() base_settings = { 'clones': { 'cl_mysql_monitor': ( 'res_mysql_monitor meta interleave=true')}, 'colocations': { 'colo_mysql': ( 'inf: grp_mysql_hostnames ' 'cl_mysql_monitor')}, 'resource_params': { 'res_mysql_monitor': ( 'params user="sstuser" ' 'password="ubuntu" ' 'pid="/var/run/mysqld/mysqld.pid" ' 'socket="/var/run/mysqld/mysqld.sock" ' 'max_slave_lag="5" ' 'cluster_type="pxc" ' 'op monitor interval="1s" ' 'timeout="30s" ' 'OCF_CHECK_LEVEL="1" ' 'meta migration-threshold=INFINITY failure-timeout=5s')}, 'locations': { 'loc_mysql': ( 'grp_mysql_hostnames ' 'rule inf: writable eq 1')}, 'delete_resources': ['loc_percona_cluster', 'grp_percona_cluster', 'res_mysql_vip'], 'resources': { 'res_mysql_monitor': 'ocf:percona:mysql_monitor'}} self.update_hacluster_dns_ha.assert_called_once_with( 'mysql', base_settings) settings = { 'json_{}'.format(k): json.dumps(v, **ch_ha_utils.JSON_ENCODE_OPTIONS) for k, v in base_settings.items() if v } self.relation_set.assert_called_once_with( relation_id='rid:23', **settings) class TestHostResolution(CharmTestCase): def setUp(self): CharmTestCase.setUp(self, hooks, TO_PATCH) self.network_get_primary_address.side_effect = NotImplementedError self.is_clustered.return_value = False self.config.side_effect = self.test_config.get self.test_config.set('prefer-ipv6', False) def test_get_db_host_defaults(self): ''' Ensure that with nothing other than defaults private-address is used ''' self.unit_get.return_value = 'mydbhost' self.resolve_hostname_to_ip.return_value = '10.0.0.2' self.assertEqual(hooks.get_db_host('myclient'), 'mydbhost') def test_get_db_host_network_spaces(self): ''' Ensure that if the shared-db relation is bound, its bound address is used ''' self.resolve_hostname_to_ip.return_value = '10.0.0.2' self.network_get_primary_address.side_effect = None self.network_get_primary_address.return_value = '192.168.20.2' self.assertEqual(hooks.get_db_host('myclient'), '192.168.20.2') self.network_get_primary_address.assert_called_with('shared-db') def test_get_db_host_network_spaces_clustered(self): ''' Ensure that if the shared-db relation is bound and the unit is clustered, that the correct VIP is chosen ''' self.resolve_hostname_to_ip.return_value = '10.0.0.2' self.is_clustered.return_value = True self.test_config.set('vip', '10.0.0.100 192.168.20.200') self.network_get_primary_address.side_effect = None self.network_get_primary_address.return_value = '192.168.20.2' self.resolve_network_cidr.return_value = '192.168.20.2/24' self.assertEqual(hooks.get_db_host('myclient'), '192.168.20.200') self.network_get_primary_address.assert_called_with('shared-db') class TestNRPERelation(CharmTestCase): def setUp(self): patch_targets_nrpe = TO_PATCH[:] patch_targets_nrpe.remove("update_nrpe_config") patch_targets_nrpe.extend(["nrpe", "apt_install", "config", "set_nagios_user"]) CharmTestCase.setUp(self, hooks, patch_targets_nrpe) @mock.patch("percona_utils.config") @mock.patch("percona_utils.is_leader") @mock.patch("percona_utils.leader_get") def test_mysql_monitored(self, mock_leader_get, mock_is_leader, mock_config): """The mysql service is monitored by Nagios.""" self.nrpe.get_nagios_unit_name.return_value = "nagios-0" self.test_config.set("nrpe-threads-connected", "80,90") mock_config.side_effect = self.test_config.get mock_is_leader.return_value = True mock_leader_get.return_value = "1234" nrpe_setup = mock.MagicMock() nrpe_setup.add_check = mock.MagicMock() self.nrpe.NRPE.return_value = nrpe_setup hooks.update_nrpe_config() self.nrpe.add_init_service_checks.assert_called_once_with( mock.ANY, ["mysql"], mock.ANY) nrpe_setup.add_check.assert_has_calls([ mock.call( shortname="mysql_proc", description="Check MySQL process nagios-0", check_cmd="check_procs -c 1:1 -C mysqld"), mock.call( shortname="mysql_threads", description="Check MySQL connected threads", check_cmd="pmp-check-mysql-status " "--defaults-file /etc/nagios/mysql-check.cnf " "-x Threads_connected -o / -y max_connections " "-T pct -w 80 -c 90") ]) class TestMasterRelation(CharmTestCase): def setUp(self): patch_targets_master = TO_PATCH[:] patch_targets_master.extend(['configure_master', 'get_cluster_id', 'get_master_status', 'leader_set']) CharmTestCase.setUp(self, hooks, patch_targets_master) def test_master_joined_is_leader_and_no_leader_change(self): self.relation_ids.return_value = ['master:1'] self.get_cluster_id.return_value = 1 self.is_clustered.return_value = True self.leader_get.return_value = {'async-rep-password': 'password', 'master-address': '10.0.0.1', 'master-file': 'file1', 'master-position': 'position1'} self.is_leader.return_value = True self.configure_master.return_value = True self.get_master_status.return_value = '10.0.0.1', 'file1', 'position1' hooks.master_joined() self.leader_set.assert_called_with( {'async-rep-password': 'password', 'master-address': '10.0.0.1', 'master-file': 'file1', 'master-position': 'position1'}) self.relation_set.assert_called_with( relation_id='master:1', relation_settings={ 'leader': True, 'cluster_id': 1, 'master_address': '10.0.0.1', 'master_file': 'file1', 'master_password': 'password', 'master_position': 'position1'}) def test_master_joined_is_leader_and_leader_change(self): self.relation_ids.return_value = ['master:1'] self.get_cluster_id.return_value = 1 self.is_clustered.return_value = True self.leader_get.return_value = {'async-rep-password': 'password', 'master-address': '10.0.0.1', 'master-file': 'file1', 'master-position': 'position1'} self.is_leader.return_value = True self.configure_master.return_value = True self.get_master_status.return_value = '10.0.0.2', 'file2', 'position2' hooks.master_joined() self.leader_set.assert_called_with( {'async-rep-password': 'password', 'master-address': '10.0.0.2', 'master-file': 'file2', 'master-position': 'position2'}) self.relation_set.assert_called_with( relation_id='master:1', relation_settings={ 'leader': True, 'cluster_id': 1, 'master_address': '10.0.0.2', 'master_file': 'file2', 'master_password': 'password', 'master_position': 'position2'}) def test_master_joined_is_not_leader(self): self.relation_ids.return_value = ['master:1'] self.get_cluster_id.return_value = 1 self.is_clustered.return_value = True self.leader_get.return_value = {'async-rep-password': 'password', 'master-address': '10.0.0.1', 'master-file': 'file', 'master-position': 'position'} self.is_leader.return_value = False hooks.master_joined() self.relation_set.assert_called_with( relation_id='master:1', relation_settings={ 'leader': False, 'cluster_id': 1, 'master_address': '10.0.0.1', 'master_file': 'file', 'master_password': 'password', 'master_position': 'position'}) class TestSlaveRelation(CharmTestCase): def setUp(self): patch_targets_slave = TO_PATCH[:] patch_targets_slave.extend(["configure_slave", "get_cluster_id"]) CharmTestCase.setUp(self, hooks, patch_targets_slave) def test_slave_joined(self): self.relation_ids.return_value = ['slave:1'] self.is_clustered.return_value = True self.get_cluster_id.return_value = 1 self.is_leader.return_value = True self.configure_slave.return_value = True self.network_get_primary_address.return_value = '172.16.0.1' hooks.slave_joined() self.relation_set.assert_called_with( relation_id='slave:1', relation_settings={ 'slave_address': '172.16.0.1', 'cluster_id': 1}) class TestConfigChanged(CharmTestCase): TO_PATCH = [ 'log', 'open_port', 'config', 'is_unit_paused_set', 'get_cluster_hosts', 'is_leader_bootstrapped', 'is_bootstrapped', 'clustered_once', 'is_leader', 'is_sufficient_peers', 'render_config_restart_on_changed', 'update_client_db_relations', 'install_mysql_ocf', 'relation_ids', 'is_relation_made', 'ha_relation_joined', 'update_nrpe_config', 'assert_charm_supports_ipv6', 'update_bootstrap_uuid', 'update_root_password', 'install_percona_xtradb_cluster', 'get_cluster_hosts', 'leader_get', 'set_ready_on_peers', 'is_unit_paused_set', 'is_unit_upgrading_set', 'get_cluster_host_ip', 'status_set', 'kv', ] def setUp(self): CharmTestCase.setUp(self, hooks, self.TO_PATCH) self.test_config.set_previous("source", self.test_config["source"]) self.test_config.set_previous("key", self.test_config["key"]) self.config.side_effect = self.test_config.get self.is_unit_paused_set.return_value = False self.is_unit_upgrading_set.return_value = False self.is_leader.return_value = False self.is_leader_bootstrapped.return_value = False self.is_bootstrapped.return_value = False self.clustered_once.return_value = False self.relation_ids.return_value = [] self.is_relation_made.return_value = False self.get_cluster_hosts.return_value = [] self.kvstore = mock.MagicMock() self.kvstore.get.return_value = False self.kv.return_value = self.kvstore def _leader_get(key): settings = {'leader-ip': '10.10.10.10', 'cluster_series_upgrading': False} return settings.get(key) self.leader_get.side_effect = _leader_get @mock.patch("percona_utils.add_source") @mock.patch("percona_utils.apt_update") def test_config_change_update_source( self, mock_apt_update, mock_add_source): """Ensure add_source and apt_update is called after changing source""" self.test_config.set("source", "test-source") hooks.config_changed() self.status_set.assert_any_call( "maintenance", "Upgrading Percona packages") mock_add_source.assert_called_once_with( source="test-source", key=None, fail_invalid=True) mock_apt_update.assert_called_once_with() def test_config_change_update_source_failed(self): """Ensure failure after configuring an invalid source""" self.test_config.set("source", "invalid-source") self.kvstore.get.return_value = True hooks.config_changed() self.kvstore.set.assert_called_once_with( hooks.ADD_APT_REPOSITORY_FAILED, True) self.status_set.assert_has_calls([ mock.call("maintenance", "Upgrading Percona packages"), ]) def test_config_changed_open_port(self): '''Ensure open_port is called with MySQL default port''' self.is_leader_bootstrapped.return_value = True hooks.config_changed() self.open_port.assert_called_with(3306) def test_config_changed_render_leader(self): '''Ensure configuration is only rendered when ready for the leader''' self.is_leader.return_value = True # Render without peers, leader not bootsrapped self.get_cluster_hosts.return_value = [] hooks.config_changed() self.install_percona_xtradb_cluster.assert_called_once() self.render_config_restart_on_changed.assert_called_once_with([]) # Render without peers, leader bootstrapped self.is_leader_bootstrapped.return_value = True self.get_cluster_hosts.return_value = [] self.render_config_restart_on_changed.reset_mock() hooks.config_changed() self.render_config_restart_on_changed.assert_called_once_with([]) # Render without hosts, leader bootstrapped, never clustered self.is_leader_bootstrapped.return_value = True self.get_cluster_hosts.return_value = ['10.10.10.20', '10.10.10.30'] self.render_config_restart_on_changed.reset_mock() hooks.config_changed() self.render_config_restart_on_changed.assert_called_once_with([]) # Clustered at least once self.clustered_once.return_value = True # Render with hosts, leader bootstrapped self.is_leader_bootstrapped.return_value = True self.get_cluster_hosts.return_value = ['10.10.10.20', '10.10.10.30'] self.render_config_restart_on_changed.reset_mock() hooks.config_changed() self.render_config_restart_on_changed.assert_called_once_with( ['10.10.10.20', '10.10.10.30']) # In none of the prior scenarios should update_root_password have been # called. self.update_root_password.assert_not_called() # Render with hosts, leader and cluster bootstrapped self.is_leader_bootstrapped.return_value = True self.is_bootstrapped.return_value = True self.get_cluster_hosts.return_value = ['10.10.10.20', '10.10.10.30'] self.render_config_restart_on_changed.reset_mock() hooks.config_changed() self.render_config_restart_on_changed.assert_called_once_with( ['10.10.10.20', '10.10.10.30']) self.update_root_password.assert_called_once() def test_config_changed_render_non_leader(self): '''Ensure configuration is only rendered when ready for non-leaders''' # Avoid rendering for non-leader. # Bug #1738896 # Leader not bootstrapped # Do not render self.get_cluster_hosts.return_value = ['10.10.10.20', '10.10.10.30', '10.10.10.10'] self.is_leader_bootstrapped.return_value = False hooks.config_changed() self.install_percona_xtradb_cluster.assert_called_once_with() self.render_config_restart_on_changed.assert_not_called() self.update_bootstrap_uuid.assert_not_called() # Leader is bootstrapped, insufficient peers # Do not render self.is_sufficient_peers.return_value = False self.is_leader_bootstrapped.return_value = True self.render_config_restart_on_changed.reset_mock() self.install_percona_xtradb_cluster.reset_mock() hooks.config_changed() self.install_percona_xtradb_cluster.assert_called_once_with() self.render_config_restart_on_changed.assert_not_called() self.update_bootstrap_uuid.assert_not_called() # Leader is bootstrapped, sufficient peers # Use the leader node and render. self.is_sufficient_peers.return_value = True self.is_leader_bootstrapped.return_value = True self.get_cluster_hosts.return_value = [] self.render_config_restart_on_changed.reset_mock() self.install_percona_xtradb_cluster.reset_mock() hooks.config_changed() self.render_config_restart_on_changed.assert_called_once_with( ['10.10.10.10']) # Missing leader, leader bootstrapped # Bug #1738896 # Leader bootstrapped # Add the leader node and render. self.render_config_restart_on_changed.reset_mock() self.update_bootstrap_uuid.reset_mock() self.get_cluster_hosts.return_value = ['10.10.10.20', '10.10.10.30'] hooks.config_changed() self.render_config_restart_on_changed.assert_called_once_with( ['10.10.10.10', '10.10.10.20', '10.10.10.30']) self.update_bootstrap_uuid.assert_called_once() # Leader present, leader bootstrapped self.render_config_restart_on_changed.reset_mock() self.update_bootstrap_uuid.reset_mock() self.get_cluster_hosts.return_value = ['10.10.10.20', '10.10.10.30', '10.10.10.10'] hooks.config_changed() self.render_config_restart_on_changed.assert_called_once_with( ['10.10.10.20', '10.10.10.30', '10.10.10.10']) self.update_bootstrap_uuid.assert_called_once() # Bug #1838648 # Do not add *this* host as a former leader self.get_cluster_host_ip.return_value = '10.10.10.30' self.render_config_restart_on_changed.reset_mock() self.update_bootstrap_uuid.reset_mock() self.get_cluster_hosts.return_value = ['10.10.10.10', '10.10.10.20'] def _leader_get(key): settings = {'leader-ip': '10.10.10.30', 'cluster_series_upgrading': False} return settings.get(key) self.leader_get.side_effect = _leader_get hooks.config_changed() self.render_config_restart_on_changed.assert_called_once_with( ['10.10.10.10', '10.10.10.20']) self.update_bootstrap_uuid.assert_called_once() # In none of the prior scenarios should update_root_password have been # called. is_bootstrapped was defaulted to False self.update_root_password.assert_not_called() self.set_ready_on_peers.assert_not_called() # Leader present, leader bootstrapped, cluster bootstrapped self.is_bootstrapped.return_value = True self.render_config_restart_on_changed.reset_mock() self.update_bootstrap_uuid.reset_mock() self.get_cluster_hosts.return_value = ['10.10.10.20', '10.10.10.30', '10.10.10.10'] hooks.config_changed() self.render_config_restart_on_changed.assert_called_once_with( ['10.10.10.20', '10.10.10.30', '10.10.10.10']) self.update_bootstrap_uuid.assert_called_once() self.assertFalse(self.update_root_password.called) self.set_ready_on_peers.called_once() class TestInstallPerconaXtraDB(CharmTestCase): TO_PATCH = [ 'log', 'pxc_installed', 'root_password', 'sst_password', 'configure_mysql_root_password', 'apt_install', 'determine_packages', 'configure_sstuser', 'config', 'run_mysql_checks', 'is_leader_bootstrapped', 'is_leader', ] def setUp(self): CharmTestCase.setUp(self, hooks, self.TO_PATCH) self.config.side_effect = self.test_config.get self.pxc_installed.return_value = False def test_installed(self): self.pxc_installed.return_value = True hooks.install_percona_xtradb_cluster() self.configure_mysql_root_password.assert_not_called() self.apt_install.assert_not_called() def test_passwords_not_initialized(self): self.root_password.return_value = None self.sst_password.return_value = None hooks.install_percona_xtradb_cluster() self.configure_mysql_root_password.assert_not_called() self.configure_sstuser.assert_not_called() self.apt_install.assert_not_called() self.is_leader_bootstrapped.return_value = True self.root_password.return_value = None self.sst_password.return_value = 'testpassword' hooks.install_percona_xtradb_cluster() self.configure_sstuser.assert_not_called() self.configure_mysql_root_password.assert_not_called() self.apt_install.assert_not_called() def test_passwords_initialized(self): self.root_password.return_value = 'rootpassword' self.sst_password.return_value = 'testpassword' self.determine_packages.return_value = ['pxc-5.6'] self.is_leader_bootstrapped.return_value = True hooks.install_percona_xtradb_cluster() self.configure_mysql_root_password.assert_called_with('rootpassword') self.configure_sstuser.assert_called_with('testpassword') self.apt_install.assert_called_with(['pxc-5.6'], fatal=True) self.run_mysql_checks.assert_not_called() class TestLeaderHooks(CharmTestCase): TO_PATCH = [ 'maybe_notify_bootstrapped', 'config_changed', 'relation_ids', 'leader_get', 'relation_set', 'master_joined', 'deconfigure_slave', ] def setUp(self): CharmTestCase.setUp(self, hooks, self.TO_PATCH) def relation_ids_full(self, rel_id): return ['{}:1'.format(rel_id)] def test_leader_settings_changed(self): self.relation_ids.side_effect = self.relation_ids_full self.leader_get.return_value = None hooks.leader_settings_changed() self.maybe_notify_bootstrapped.assert_called_once_with() self.config_changed.assert_called_once_with() self.master_joined.assert_called_once_with() self.deconfigure_slave.assert_called_once_with() self.relation_set.assert_called_once_with( relation_id='shared-db:1', relation_settings={'cluster-series-upgrading': None}) class TestSeriesUpgrade(CharmTestCase): TO_PATCH = [ 'register_configs', 'pause_unit_helper', 'set_unit_upgrading', 'leader_get', 'leader_set', 'relation_ids', 'relation_set', 'get_relation_ip', 'render_config', ] def setUp(self): CharmTestCase.setUp(self, hooks, self.TO_PATCH) def test_prepare_leader(self): self.register_configs.return_value = 'registered_configs' self.leader_get.return_value = None self.get_relation_ip.return_value = '10.0.0.10' self.relation_ids.return_value = ['relid:1'] hooks.prepare() self.pause_unit_helper.assert_called_once_with('registered_configs') self.set_unit_upgrading.assert_called_once_with() leader_set_calls = [ mock.call(cluster_series_upgrading=True), mock.call(cluster_series_upgrade_leader='10.0.0.10')] self.leader_set.assert_has_calls(leader_set_calls) self.relation_set.assert_called_once_with( relation_id='relid:1', relation_settings={'cluster-series-upgrading': True}) self.render_config.assert_called_once_with([]) def test_prepare_non_leader(self): self.register_configs.return_value = 'registered_configs' self.leader_get.return_value = '10.0.0.10' hooks.prepare() self.pause_unit_helper.assert_called_once_with('registered_configs') self.set_unit_upgrading.assert_called_once_with() self.render_config.assert_called_once_with(['10.0.0.10']) class TestUpgradeCharm(CharmTestCase): TO_PATCH = [ 'config', 'log', 'is_leader', 'is_unit_paused_set', 'get_wsrep_value', 'config_changed', 'get_relation_ip', 'leader_set', 'sst_password', 'configure_sstuser', 'leader_get', 'notify_bootstrapped', 'mark_seeded', 'kv', 'is_unit_upgrading_set', ] def print_log(self, msg, level=None): print("juju-log: {}: {}".format(level, msg)) def setUp(self): CharmTestCase.setUp(self, hooks, self.TO_PATCH) self.config.side_effect = self.test_config.get self.log.side_effect = self.print_log self.tmpdir = tempfile.mkdtemp() self.is_unit_upgrading_set.return_value = False def tearDown(self): CharmTestCase.tearDown(self) try: shutil.rmtree(self.tmpdir) except Exception: pass def test_upgrade_charm_leader(self): self.is_leader.return_value = True self.is_unit_paused_set.return_value = False self.get_relation_ip.return_value = '10.10.10.10' self.leader_get.side_effect = [None, 'mypasswd', 'mypasswd'] def c(k): values = {'wsrep_ready': 'on', 'wsrep_cluster_state_uuid': '1234-abcd'} return values[k] self.get_wsrep_value.side_effect = c hooks.upgrade() self.mark_seeded.assert_called_once() self.notify_bootstrapped.assert_called_with(cluster_uuid='1234-abcd') self.configure_sstuser.assert_called_once() self.leader_set.assert_has_calls( [mock.call(**{'leader-ip': '10.10.10.10'}), mock.call(**{'root-password': 'mypasswd'})]) class TestConfigs(CharmTestCase): TO_PATCH = [ 'config', 'is_leader', 'render_override', ] def setUp(self): CharmTestCase.setUp(self, hooks, self.TO_PATCH) self.config.side_effect = self.test_config.get self.default_config = self._get_default_config() for key, value in self.default_config.items(): self.test_config.set(key, value) self.is_leader.return_value = False def _load_config(self): '''Walk backwords from __file__ looking for config.yaml, load and return the 'options' section' ''' config = None f = __file__ while config is None: d = os.path.dirname(f) if os.path.isfile(os.path.join(d, 'config.yaml')): config = os.path.join(d, 'config.yaml') break f = d if not config: logging.error('Could not find config.yaml in any parent directory ' 'of %s. ' % f) raise Exception return yaml.safe_load(open(config, encoding="UTF-8").read())['options'] def _get_default_config(self): '''Load default charm config from config.yaml return as a dict. If no default is set in config.yaml, its value is None. ''' default_config = {} config = self._load_config() for k, v in config.items(): if 'default' in v: default_config[k] = v['default'] else: default_config[k] = None return default_config @mock.patch.object(hooks, 'is_unit_upgrading_set') @mock.patch.object(os, 'makedirs') @mock.patch.object(hooks, 'get_cluster_host_ip') @mock.patch.object(hooks, 'get_wsrep_provider_options') @mock.patch.object(PerconaClusterHelper, 'parse_config') @mock.patch.object(hooks, 'render') @mock.patch.object(hooks, 'sst_password') @mock.patch.object(hooks, 'lsb_release') def test_render_config_defaults_xenial(self, lsb_release, sst_password, render, parse_config, get_wsrep_provider_options, get_cluster_host_ip, makedirs, mock_is_unit_upgrading_set): mock_is_unit_upgrading_set.return_value = False parse_config.return_value = {'key_buffer': '32M'} get_cluster_host_ip.return_value = '10.1.1.1' get_wsrep_provider_options.return_value = None sst_password.return_value = 'sstpassword' lsb_release.return_value = {'DISTRIB_CODENAME': 'xenial'} context = { 'wsrep_slave_threads': 1, 'server-id': hooks.get_server_id(), 'is_leader': hooks.is_leader(), 'series_upgrade': hooks.is_unit_upgrading_set(), 'private_address': '10.1.1.1', 'cluster_hosts': '', 'enable_binlogs': self.default_config['enable-binlogs'], 'sst_password': 'sstpassword', 'myisam_recover': 'BACKUP', 'sst_method': self.default_config['sst-method'], 'server_id': hooks.get_server_id(), 'binlogs_max_size': self.default_config['binlogs-max-size'], 'key_buffer': '32M', 'performance_schema': self.default_config['performance-schema'], 'binlogs_path': self.default_config['binlogs-path'], 'cluster_name': 'juju_cluster', 'binlogs_expire_days': self.default_config['binlogs-expire-days'], 'ipv6': False, 'innodb_file_per_table': self.default_config['innodb-file-per-table'], 'table_open_cache': self.default_config['table-open-cache'], 'wsrep_provider': '/usr/lib/libgalera_smm.so', } hooks.render_config() hooks.render.assert_called_once_with( 'mysqld.cnf', '/etc/mysql/percona-xtradb-cluster.conf.d/mysqld.cnf', context, perms=0o444) @mock.patch.object(hooks, 'is_unit_upgrading_set') @mock.patch.object(os, 'makedirs') @mock.patch.object(hooks, 'get_cluster_host_ip') @mock.patch.object(hooks, 'get_wsrep_provider_options') @mock.patch.object(PerconaClusterHelper, 'parse_config') @mock.patch.object(hooks, 'render') @mock.patch.object(hooks, 'sst_password') @mock.patch.object(hooks, 'lsb_release') def test_render_config_defaults(self, lsb_release, sst_password, render, parse_config, get_wsrep_provider_options, get_cluster_host_ip, makedirs, mock_is_unit_upgrading_set): mock_is_unit_upgrading_set.return_value = False parse_config.return_value = {'key_buffer': '32M'} get_cluster_host_ip.return_value = '10.1.1.1' get_wsrep_provider_options.return_value = None sst_password.return_value = 'sstpassword' lsb_release.return_value = {'DISTRIB_CODENAME': 'bionic'} context = { 'wsrep_slave_threads': 48, 'server_id': hooks.get_server_id(), 'server-id': hooks.get_server_id(), 'is_leader': hooks.is_leader(), 'series_upgrade': hooks.is_unit_upgrading_set(), 'private_address': '10.1.1.1', 'innodb_autoinc_lock_mode': '2', 'cluster_hosts': '', 'enable_binlogs': self.default_config['enable-binlogs'], 'sst_password': 'sstpassword', 'sst_method': self.default_config['sst-method'], 'pxc_strict_mode': 'enforcing', 'binlogs_max_size': self.default_config['binlogs-max-size'], 'cluster_name': 'juju_cluster', 'innodb_file_per_table': self.default_config['innodb-file-per-table'], 'table_open_cache': self.default_config['table-open-cache'], 'binlogs_path': self.default_config['binlogs-path'], 'binlogs_expire_days': self.default_config['binlogs-expire-days'], 'performance_schema': self.default_config['performance-schema'], 'key_buffer': '32M', 'default_storage_engine': 'InnoDB', 'wsrep_log_conflicts': True, 'ipv6': False, 'wsrep_provider': '/usr/lib/galera3/libgalera_smm.so', } hooks.render_config() hooks.render.assert_called_once_with( 'mysqld.cnf', '/etc/mysql/percona-xtradb-cluster.conf.d/mysqld.cnf', context, perms=0o444) @mock.patch.object(hooks, 'is_unit_upgrading_set') @mock.patch.object(os, 'makedirs') @mock.patch.object(hooks, 'get_cluster_host_ip') @mock.patch.object(hooks, 'get_wsrep_provider_options') @mock.patch.object(PerconaClusterHelper, 'parse_config') @mock.patch.object(hooks, 'render') @mock.patch.object(hooks, 'sst_password') @mock.patch.object(hooks, 'lsb_release') def test_render_config_wsrep_slave_threads( self, lsb_release, sst_password, render, parse_config, get_wsrep_provider_options, get_cluster_host_ip, makedirs, mock_is_unit_upgrading_set): mock_is_unit_upgrading_set.return_value = False parse_config.return_value = {'key_buffer': '32M'} get_cluster_host_ip.return_value = '10.1.1.1' get_wsrep_provider_options.return_value = None sst_password.return_value = 'sstpassword' self.test_config.set('wsrep-slave-threads', 2) lsb_release.return_value = {'DISTRIB_CODENAME': 'bionic'} context = { 'server_id': hooks.get_server_id(), 'server-id': hooks.get_server_id(), 'is_leader': hooks.is_leader(), 'series_upgrade': hooks.is_unit_upgrading_set(), 'private_address': '10.1.1.1', 'innodb_autoinc_lock_mode': '2', 'cluster_hosts': '', 'enable_binlogs': self.default_config['enable-binlogs'], 'sst_password': 'sstpassword', 'sst_method': self.default_config['sst-method'], 'pxc_strict_mode': 'enforcing', 'binlogs_max_size': self.default_config['binlogs-max-size'], 'cluster_name': 'juju_cluster', 'innodb_file_per_table': self.default_config['innodb-file-per-table'], 'table_open_cache': self.default_config['table-open-cache'], 'binlogs_path': self.default_config['binlogs-path'], 'binlogs_expire_days': self.default_config['binlogs-expire-days'], 'performance_schema': self.default_config['performance-schema'], 'key_buffer': '32M', 'default_storage_engine': 'InnoDB', 'wsrep_log_conflicts': True, 'ipv6': False, 'wsrep_provider': '/usr/lib/galera3/libgalera_smm.so', 'wsrep_slave_threads': 2, } hooks.render_config() hooks.render.assert_called_once_with( 'mysqld.cnf', '/etc/mysql/percona-xtradb-cluster.conf.d/mysqld.cnf', context, perms=0o444) class TestRenderConfigRestartOnChanged(CharmTestCase): TO_PATCH = [ 'is_leader', 'is_leader_bootstrapped', 'bootstrap_pxc', 'resolve_cnf_file', 'file_hash', 'render_config', 'create_binlogs_directory', 'bootstrap_pxc', 'notify_bootstrapped', 'service_running', 'service_stop', 'service_restart', 'cluster_wait', 'mark_seeded', 'cluster_wait', 'update_client_db_relations', ] def setUp(self): CharmTestCase.setUp(self, hooks, self.TO_PATCH) self.is_leader.return_value = False self.is_leader_bootstrapped.return_value = False self.bootstrap_pxc.return_value = None self.service_running.return_value = True self.resolve_cnf_file.return_value = \ '/etc/mysql/percona-xtradb-cluster.conf.d/mysqld.cnf' self.file_hash.return_value = 'original' self.service_restart.return_value = True def _render_config_changed(self, hosts): self.file_hash.return_value = 'changed' def test_bootstrap_leader(self): self.is_leader.return_value = True self.is_leader_bootstrapped.return_value = False self.render_config.side_effect = self._render_config_changed hooks.render_config_restart_on_changed([]) self.bootstrap_pxc.assert_called_once() self.service_restart.assert_not_called() def test_bootstrapped_leader(self): self.is_leader.return_value = True self.is_leader_bootstrapped.return_value = True self.render_config.side_effect = self._render_config_changed hooks.render_config_restart_on_changed([]) self.bootstrap_pxc.assert_not_called() self.service_restart.assert_called_once() def test_noleader(self): self.is_leader.return_value = False self.is_leader_bootstrapped.return_value = False self.render_config.side_effect = self._render_config_changed hooks.render_config_restart_on_changed([]) self.bootstrap_pxc.assert_not_called() self.service_restart.assert_called_once() def test_nochange_bootstrap_leader(self): self.is_leader.return_value = True self.is_leader_bootstrapped.return_value = False hooks.render_config_restart_on_changed([]) self.bootstrap_pxc.assert_called_once() self.service_restart.assert_not_called() def test_nochange_bootstrapped_leader(self): self.is_leader.return_value = True self.is_leader_bootstrapped.return_value = True hooks.render_config_restart_on_changed([]) self.bootstrap_pxc.assert_not_called() self.service_restart.assert_not_called() def test_nochange_noleader(self): self.is_leader.return_value = False self.is_leader_bootstrapped.return_value = False hooks.render_config_restart_on_changed([]) self.bootstrap_pxc.assert_not_called() self.service_restart.assert_not_called() class TestClusterRelation(CharmTestCase): def setUp(self): CharmTestCase.setUp(self, hooks, TO_PATCH) self.config.side_effect = self.test_config.get self.is_leader.return_value = False self.kvstore = FakeKvStore() self.kvstore.set('initial_client_update_done', True) self.kv.return_value = self.kvstore @mock.patch('percona_utils.kv') @mock.patch.object(hooks, 'config_changed') @mock.patch.object(hooks, 'get_cluster_host_ip') @mock.patch('percona_utils.notify_bootstrapped') @mock.patch('percona_utils.get_wsrep_value') @mock.patch('percona_utils.leader_get') @mock.patch('percona_utils.check_mysql_connection') @mock.patch.object(hooks, 'peer_echo') @mock.patch.object(hooks, 'mark_seeded') @mock.patch.object(hooks, 'seeded') @mock.patch.object(hooks, 'is_bootstrapped') def test_needs_to_be_mark_as_seeded(self, mock_is_bootstrapped, mock_seeded, mock_mark_seeded, mock_peer_echo, mock_check_connection, mock_leader_get, mock_get_wsrep_value, mock_notify_bootstrapped, mock_cluster_host_ip, mock_config_changed, mock_kv): def fake_leader_get(k): return { 'bootstrap-uuid': '1-2-3-4', 'wsrep_cluster_state_uuid': '1-2-3-4', }[k] mock_kv.return_value = self.kvstore mock_leader_get.side_effect = fake_leader_get mock_get_wsrep_value.side_effect = fake_leader_get mock_is_bootstrapped.return_value = True mock_seeded.return_value = False mock_check_connection.return_value = True mock_cluster_host_ip.return_value = '127.0.1.1' hooks.cluster_changed() mock_mark_seeded.assert_called() mock_config_changed.assert_called() mock_get_wsrep_value.assert_called_with('wsrep_cluster_state_uuid') mock_notify_bootstrapped.assert_called_with(cluster_uuid='1-2-3-4') mock_mark_seeded.reset_mock() mock_seeded.return_value = True hooks.cluster_changed() mock_mark_seeded.assert_not_called() mock_seeded.assert_called()