diff --git a/hooks/percona_hooks.py b/hooks/percona_hooks.py index 0186233..c1280cf 100755 --- a/hooks/percona_hooks.py +++ b/hooks/percona_hooks.py @@ -105,6 +105,7 @@ from percona_utils import ( pxc_installed, update_bootstrap_uuid, LeaderNoBootstrapUUIDError, + update_root_password, ) @@ -338,6 +339,11 @@ def config_changed(): open_port(DEFAULT_MYSQL_PORT) + # the password needs to be updated only if the node was already + # bootstrapped + if bootstrapped: + update_root_password() + @hooks.hook('cluster-relation-joined') def cluster_joined(): diff --git a/hooks/percona_utils.py b/hooks/percona_utils.py index 949332a..00091fc 100644 --- a/hooks/percona_utils.py +++ b/hooks/percona_utils.py @@ -820,3 +820,30 @@ def pxc_installed(): @returns: boolean: indicating installation ''' return os.path.exists('/usr/sbin/mysqld') + + +def update_root_password(): + """Update root password if needed + + :returns: `False` when configured password has not changed + """ + + cfg = config() + if not cfg.changed('root-password'): + return False + + m_helper = get_db_helper() + + # password that needs to be set + new_root_passwd = cfg['root-password'] + m_helper.set_mysql_root_password(new_root_passwd) + + # check the password was changed + try: + m_helper.connect(user='root', password=new_root_passwd) + m_helper.execute('select 1;') + except OperationalError as ex: + log("Error connecting using new passowrd: %s" % str(ex), level=DEBUG) + log(('Cannot connect using new password, not updating password in ' + 'the relation'), level=WARNING) + return diff --git a/tests/basic_deployment.py b/tests/basic_deployment.py index e5108b2..8f41759 100644 --- a/tests/basic_deployment.py +++ b/tests/basic_deployment.py @@ -13,6 +13,9 @@ from charmhelpers.contrib.openstack.amulet.deployment import ( from charmhelpers.contrib.amulet.utils import AmuletUtils +PXC_ROOT_PASSWD = 'ubuntu' + + class BasicDeployment(OpenStackAmuletDeployment): utils = AmuletUtils() @@ -73,7 +76,8 @@ class BasicDeployment(OpenStackAmuletDeployment): def _get_configs(self): """Configure all of the services.""" cfg_percona = {'min-cluster-size': self.units, - 'vip': self.vip} + 'vip': self.vip, + 'root-password': PXC_ROOT_PASSWD} cfg_ha = {'debug': True, 'corosync_key': ('xZP7GDWV0e8Qs0GxWThXirNNYlScgi3sRTdZk/IXKD' @@ -240,6 +244,31 @@ class BasicDeployment(OpenStackAmuletDeployment): assert self.is_port_open(address=self.vip), 'cannot connect to vip' + def test_change_root_password(self): + """ + Change root password and verify the change was effectively applied. + """ + + new_root_passwd = 'openstack' + + u = self.master_unit + root_password, _ = PXC_ROOT_PASSWD + cmd = "mysql -uroot -p{} -e\"select 1;\" ".format(root_password) + output, code = u.run(cmd) + + assert code == 0, output + + self.d.configure('percona-cluster', {'root-password': new_root_passwd}) + + time.sleep(5) # give some time to the unit to start the hook + self.d.sentry.wait() # wait until the hook finishes + + # try to connect using the new root password + cmd = "mysql -uroot -p{} -e\"select 1;\" ".format(new_root_passwd) + output, code = u.run(cmd) + + assert code == 0, output + def find_master(self, ha=True): for unit in self.d.sentry['percona-cluster']: if not ha: diff --git a/unit_tests/test_percona_utils.py b/unit_tests/test_percona_utils.py index 0d720e5..1da2a16 100644 --- a/unit_tests/test_percona_utils.py +++ b/unit_tests/test_percona_utils.py @@ -523,6 +523,8 @@ class TestUpdateBootstrapUUID(CharmTestCase): 'relation_set', 'is_leader', 'leader_set', + 'config', + 'leader_get', ] def setUp(self): @@ -583,3 +585,24 @@ class TestUpdateBootstrapUUID(CharmTestCase): self.get_wsrep_value.side_effect = fake_wsrep self.assertRaises(percona_utils.InconsistentUUIDError, percona_utils.update_bootstrap_uuid) + + @mock.patch('charmhelpers.contrib.database.mysql.leader_set') + @mock.patch('charmhelpers.contrib.database.mysql.is_leader') + @mock.patch('charmhelpers.contrib.database.mysql.leader_get') + def test_update_root_password(self, mock_leader_get, mock_is_leader, + mock_leader_set): + cur_password = 'openstack' + new_password = 'ubuntu' + leader_config = {'mysql.passwd': cur_password} + mock_leader_get.side_effect = lambda k: leader_config[k] + mock_is_leader.return_value = True + + self.config.side_effect = self.test_config.get + self.assertFalse(percona_utils.update_root_password()) + + self.test_config.set_previous('root-password', cur_password) + self.test_config.set('root-password', new_password) + percona_utils.update_root_password() + + mock_leader_set.assert_called_with( + settings={'mysql.passwd': new_password}) diff --git a/unit_tests/test_utils.py b/unit_tests/test_utils.py index b8b9224..c32389b 100644 --- a/unit_tests/test_utils.py +++ b/unit_tests/test_utils.py @@ -70,10 +70,24 @@ class TestConfig(object): def __init__(self): self.config = get_default_config() + self.config_prev = {} + + def previous(self, k): + return self.config_prev[k] if k in self.config_prev else self.config[k] + + def set_previous(self, k, v): + self.config_prev[k] = v + + def unset_previous(self, k): + if k in self.config_prev: + self.config_prev.pop(k) + + def changed(self, k): + return self.get(k) != self.previous(k) def get(self, attr=None): if not attr: - return self.get_all() + return self try: return self.config[attr] except KeyError: @@ -87,6 +101,9 @@ class TestConfig(object): raise KeyError self.config[attr] = value + def __getitem__(self, k): + return self.get(k) + class TestRelation(object):