import os
import mock
import json
import unittest
import sys
import shutil
import tempfile

from collections import OrderedDict

sys.modules['MySQLdb'] = mock.Mock()
from charmhelpers.contrib.database import mysql  # noqa


class MysqlTests(unittest.TestCase):
    def setUp(self):
        super(MysqlTests, self).setUp()

    def test_connect_host_defined(self):
        helper = mysql.MySQLHelper('foo', 'bar', host='hostA')
        with mock.patch.object(mysql, 'log'):
            helper.connect(user='user', password='password', host='1.1.1.1')
        mysql.MySQLdb.connect.assert_called_with(
            passwd='password', host='1.1.1.1', user='user')

    def test_connect_host_not_defined(self):
        helper = mysql.MySQLHelper('foo', 'bar')
        with mock.patch.object(mysql, 'log'):
            helper.connect(user='user', password='password')
        mysql.MySQLdb.connect.assert_called_with(
            passwd='password', host='localhost', user='user')

    def test_connect_port_defined(self):
        helper = mysql.MySQLHelper('foo', 'bar')
        with mock.patch.object(mysql, 'log'):
            helper.connect(user='user', password='password', port=3316)
        mysql.MySQLdb.connect.assert_called_with(
            passwd='password', host='localhost', user='user', port=3316)

    @mock.patch.object(mysql.MySQLHelper, 'normalize_address')
    @mock.patch.object(mysql.MySQLHelper, 'get_mysql_password')
    @mock.patch.object(mysql.MySQLHelper, 'grant_exists')
    @mock.patch.object(mysql, 'relation_get')
    @mock.patch.object(mysql, 'related_units')
    @mock.patch.object(mysql, 'log')
    def test_get_allowed_units(self, mock_log, mock_related_units,
                               mock_relation_get,
                               mock_grant_exists,
                               mock_get_password,
                               mock_normalize_address):

        # echo
        mock_normalize_address.side_effect = lambda addr: addr

        def mock_rel_get(unit, rid):
            if unit == 'unit/0':
                # Non-prefixed settings
                d = {'private-address': '10.0.0.1',
                     'hostname': 'hostA'}
            elif unit == 'unit/1':
                # Containing prefixed settings
                d = {'private-address': '10.0.0.2',
                     'dbA_hostname': json.dumps(['10.0.0.2', '2001:db8:1::2'])}
            elif unit == 'unit/2':
                # No hostname
                d = {'private-address': '10.0.0.3'}

            return d

        mock_relation_get.side_effect = mock_rel_get
        mock_related_units.return_value = ['unit/0', 'unit/1', 'unit/2']

        helper = mysql.MySQLHelper('foo', 'bar', host='hostA')
        units = helper.get_allowed_units('dbA', 'userA')

        calls = [mock.call('dbA', 'userA', 'hostA'),
                 mock.call('dbA', 'userA', '10.0.0.2'),
                 mock.call('dbA', 'userA', '2001:db8:1::2'),
                 mock.call('dbA', 'userA', '10.0.0.3')]

        helper.grant_exists.assert_has_calls(calls, any_order=True)
        self.assertEqual(units, set(['unit/0', 'unit/1', 'unit/2']))

    @mock.patch('charmhelpers.contrib.network.ip.log',
                lambda *args, **kwargs: None)
    @mock.patch('charmhelpers.contrib.network.ip.ns_query')
    @mock.patch('charmhelpers.contrib.network.ip.socket')
    @mock.patch.object(mysql, 'unit_get')
    @mock.patch.object(mysql, 'config_get')
    @mock.patch.object(mysql, 'log')
    def test_normalize_address(self, mock_log, mock_config_get, mock_unit_get,
                               mock_socket, mock_ns_query):
        helper = mysql.MySQLHelper('foo', 'bar', host='hostA')
        # prefer-ipv6
        mock_config_get.return_value = False
        # echo
        mock_socket.gethostbyname.side_effect = lambda addr: addr

        mock_unit_get.return_value = '10.0.0.1'
        out = helper.normalize_address('10.0.0.1')
        self.assertEqual('127.0.0.1', out)
        mock_config_get.assert_called_with('prefer-ipv6')

        mock_unit_get.return_value = '10.0.0.1'
        out = helper.normalize_address('10.0.0.2')
        self.assertEqual('10.0.0.2', out)
        mock_config_get.assert_called_with('prefer-ipv6')

        out = helper.normalize_address('2001:db8:1::1')
        self.assertEqual('2001:db8:1::1', out)
        mock_config_get.assert_called_with('prefer-ipv6')

        mock_socket.gethostbyname.side_effect = Exception
        mock_ns_query.return_value = None
        out = helper.normalize_address('unresolvable')
        self.assertEqual('unresolvable', out)
        mock_config_get.assert_called_with('prefer-ipv6')

        # prefer-ipv6
        mock_config_get.return_value = True
        mock_socket.gethostbyname.side_effect = 'other'
        out = helper.normalize_address('unresolvable')
        self.assertEqual('unresolvable', out)
        mock_config_get.assert_called_with('prefer-ipv6')

    def test_passwd_keys(self):
        helper = mysql.MySQLHelper('foo', 'bar', host='hostA')
        self.assertEqual(list(helper.passwd_keys(None)), ['mysql.passwd'])
        self.assertEqual(list(helper.passwd_keys('auser')),
                         ['mysql-auser.passwd', 'auser.passwd'])

    @mock.patch.object(mysql.MySQLHelper, 'migrate_passwords_to_leader_storage')
    @mock.patch.object(mysql.MySQLHelper, 'get_mysql_password_on_disk')
    @mock.patch.object(mysql, 'leader_get')
    def test_get_mysql_password_no_peer_passwd(self, mock_leader_get,
                                               mock_get_disk_pw,
                                               mock_migrate_pw):
        helper = mysql.MySQLHelper('foo', 'bar', host='hostA')
        store = {}
        mock_leader_get.side_effect = lambda key: store.get(key)
        mock_get_disk_pw.return_value = "disk-passwd"
        self.assertEqual(helper.get_mysql_password(), "disk-passwd")
        self.assertTrue(mock_migrate_pw.called)

    @mock.patch.object(mysql.MySQLHelper, 'migrate_passwords_to_leader_storage')
    @mock.patch.object(mysql.MySQLHelper, 'get_mysql_password_on_disk')
    @mock.patch.object(mysql, 'leader_get')
    def test_get_mysql_password_peer_passwd(self, mock_leader_get,
                                            mock_get_disk_pw, mock_migrate_pw):
        helper = mysql.MySQLHelper('foo', 'bar', host='hostA')
        store = {'mysql-userA.passwd': 'passwdA'}
        mock_leader_get.side_effect = lambda key: store.get(key)
        mock_get_disk_pw.return_value = "disk-passwd"
        self.assertEqual(helper.get_mysql_password(username='userA'),
                         "passwdA")
        self.assertTrue(mock_migrate_pw.called)

    @mock.patch.object(mysql.MySQLHelper, 'migrate_passwords_to_leader_storage')
    @mock.patch.object(mysql.MySQLHelper, 'get_mysql_password_on_disk')
    @mock.patch.object(mysql, 'leader_get')
    def test_get_mysql_password_peer_passwd_legacy(self, mock_leader_get,
                                                   mock_get_disk_pw,
                                                   mock_migrate_pw):
        helper = mysql.MySQLHelper('foo', 'bar', host='hostA')
        store = {'userA.passwd': 'passwdA'}
        mock_leader_get.side_effect = lambda key: store.get(key)
        mock_get_disk_pw.return_value = "disk-passwd"
        self.assertEqual(helper.get_mysql_password(username='userA'),
                         "passwdA")
        self.assertTrue(mock_migrate_pw.called)

    @mock.patch.object(mysql.MySQLHelper, 'migrate_passwords_to_leader_storage')
    @mock.patch.object(mysql.MySQLHelper, 'get_mysql_password_on_disk')
    @mock.patch.object(mysql, 'leader_get')
    def test_get_mysql_password_peer_passwd_all(self, mock_leader_get,
                                                mock_get_disk_pw,
                                                mock_migrate_pw):
        helper = mysql.MySQLHelper('foo', 'bar', host='hostA')
        # Add * so we can identify that the new format key takes precedence
        # if found.
        store = {'mysql-userA.passwd': 'passwdA',
                 'userA.passwd': 'passwdA*'}
        mock_leader_get.side_effect = lambda key: store.get(key)
        mock_get_disk_pw.return_value = "disk-passwd"
        self.assertEqual(helper.get_mysql_password(username='userA'),
                         "passwdA")
        self.assertTrue(mock_migrate_pw.called)

    @mock.patch.object(mysql.MySQLHelper, 'set_mysql_password')
    def test_set_mysql_root_password(self, mock_set_passwd):
        helper = mysql.MySQLHelper('foo', 'bar', host='hostA')
        helper.set_mysql_root_password(password='1234')
        mock_set_passwd.assert_called_with(
            'root',
            '1234',
            current_password=None)

    @mock.patch.object(mysql.MySQLHelper, 'set_mysql_password')
    def test_set_mysql_root_password_cur_passwd(self, mock_set_passwd):
        helper = mysql.MySQLHelper('foo', 'bar', host='hostA')
        helper.set_mysql_root_password(password='1234', current_password='abc')
        mock_set_passwd.assert_called_with(
            'root',
            '1234',
            current_password='abc')

    @mock.patch.object(mysql, 'log', lambda *args, **kwargs: None)
    @mock.patch.object(mysql, 'is_leader')
    @mock.patch.object(mysql, 'leader_get')
    @mock.patch.object(mysql, 'leader_set')
    @mock.patch.object(mysql, 'CompareHostReleases')
    @mock.patch.object(mysql.MySQLHelper, 'get_mysql_password')
    @mock.patch.object(mysql.MySQLHelper, 'connect')
    def test_set_mysql_password(self, mock_connect, mock_get_passwd,
                                mock_compare_releases, mock_leader_set,
                                mock_leader_get, mock_is_leader):
        mock_connection = mock.MagicMock()
        mock_cursor = mock.MagicMock()
        mock_connection.cursor.return_value = mock_cursor
        mock_get_passwd.return_value = 'asdf'
        mock_is_leader.return_value = True
        mock_leader_get.return_value = '1234'
        mock_compare_releases.return_value = 'artful'

        helper = mysql.MySQLHelper('foo', 'bar', host='hostA')
        helper.connection = mock_connection

        helper.set_mysql_password(username='root', password='1234')

        mock_connect.assert_has_calls(
            [mock.call(user='root', password='asdf'),  # original password
             mock.call(user='root', password='1234')])  # new password
        mock_leader_get.assert_has_calls([mock.call('mysql.passwd')])
        mock_leader_set.assert_has_calls(
            [mock.call(settings={'mysql.passwd': '1234'})]
        )
        SQL_UPDATE_PASSWD = ("UPDATE mysql.user SET password = "
                             "PASSWORD( %s ) WHERE user = %s;")
        mock_cursor.assert_has_calls(
            [mock.call.execute(SQL_UPDATE_PASSWD, ('1234', 'root')),
             mock.call.execute('FLUSH PRIVILEGES;'),
             mock.call.close(),
             mock.call.execute('select 1;'),
             mock.call.close()]
        )
        mock_get_passwd.assert_called_once_with(None)

        # make sure for the non-leader leader-set is not called
        mock_is_leader.return_value = False
        mock_leader_set.reset_mock()
        mock_get_passwd.reset_mock()
        helper.set_mysql_password(username='root', password='1234')
        mock_leader_set.assert_not_called()
        mock_get_passwd.assert_called_once_with(None)

        mock_get_passwd.reset_mock()
        mock_compare_releases.return_value = 'bionic'
        helper.set_mysql_password(username='root', password='1234')
        SQL_UPDATE_PASSWD = ("UPDATE mysql.user SET "
                             "authentication_string = "
                             "PASSWORD( %s ) WHERE user = %s;")
        mock_cursor.assert_has_calls(
            [mock.call.execute(SQL_UPDATE_PASSWD, ('1234', 'root')),
             mock.call.execute('FLUSH PRIVILEGES;'),
             mock.call.close(),
             mock.call.execute('select 1;'),
             mock.call.close()]
        )
        mock_get_passwd.assert_called_once_with(None)

        # Test supplying the current password
        mock_is_leader.return_value = False
        mock_connect.reset_mock()
        mock_get_passwd.reset_mock()
        helper.set_mysql_password(
            username='root',
            password='1234',
            current_password='currpass')
        self.assertFalse(mock_get_passwd.called)
        mock_connect.assert_has_calls(
            [mock.call(user='root', password='currpass'),  # original password
             mock.call(user='root', password='1234')])  # new password

    @mock.patch.object(mysql, 'leader_get')
    @mock.patch.object(mysql, 'leader_set')
    @mock.patch.object(mysql.MySQLHelper, 'get_mysql_password')
    @mock.patch.object(mysql.MySQLHelper, 'connect')
    def test_set_mysql_password_fail_to_connect(self, mock_connect,
                                                mock_get_passwd,
                                                mock_leader_set,
                                                mock_leader_get):

        class FakeOperationalError(Exception):
            pass

        def fake_connect(*args, **kwargs):
            raise FakeOperationalError('foobar')

        mysql.MySQLdb.OperationalError = FakeOperationalError
        helper = mysql.MySQLHelper('foo', 'bar', host='hostA')
        mock_connect.side_effect = fake_connect
        self.assertRaises(mysql.MySQLSetPasswordError,
                          helper.set_mysql_password,
                          username='root', password='1234')

    @mock.patch.object(mysql, 'leader_get')
    @mock.patch.object(mysql, 'leader_set')
    @mock.patch.object(mysql.MySQLHelper, 'get_mysql_password')
    @mock.patch.object(mysql.MySQLHelper, 'connect')
    def test_set_mysql_password_fail_to_connect2(self, mock_connect,
                                                 mock_get_passwd,
                                                 mock_leader_set,
                                                 mock_leader_get):

        class FakeOperationalError(Exception):
            def __str__(self):
                return 'some-error'

        operational_error = FakeOperationalError('foobar')

        def fake_connect(user, password):
            # fail for the new password
            if user == 'root' and password == '1234':
                raise operational_error
            else:
                return mock.MagicMock()

        mysql.MySQLdb.OperationalError = FakeOperationalError
        helper = mysql.MySQLHelper('foo', 'bar', host='hostA')
        helper.connection = mock.MagicMock()
        mock_connect.side_effect = fake_connect
        with self.assertRaises(mysql.MySQLSetPasswordError) as cm:
            helper.set_mysql_password(username='root', password='1234')

        ex = cm.exception
        self.assertEqual(ex.args[0], 'Cannot connect using new password: some-error')
        self.assertEqual(ex.args[1], operational_error)

    @mock.patch.object(mysql, 'is_leader')
    @mock.patch.object(mysql, 'leader_set')
    def test_migrate_passwords_to_leader_storage(self, mock_leader_set,
                                                 mock_is_leader):
        files = {'mysql.passwd': '1',
                 'userA.passwd': '2',
                 'mysql-userA.passwd': '3'}
        store = {}

        def _store(settings):
            store.update(settings)

        mock_is_leader.return_value = True

        tmpdir = tempfile.mkdtemp('charm-helpers-unit-tests')
        try:
            root_tmplt = "%s/mysql.passwd" % (tmpdir)
            helper = mysql.MySQLHelper(root_tmplt, None, host='hostA')
            for f in files:
                with open(os.path.join(tmpdir, f), 'w') as fd:
                    fd.write(files[f])

            mock_leader_set.side_effect = _store
            helper.migrate_passwords_to_leader_storage()

            calls = [mock.call(settings={'mysql.passwd': '1'}),
                     mock.call(settings={'userA.passwd': '2'}),
                     mock.call(settings={'mysql-userA.passwd': '3'})]

            mock_leader_set.assert_has_calls(calls,
                                             any_order=True)
        finally:
            shutil.rmtree(tmpdir)

        # Note that legacy key/val is NOT overwritten
        self.assertEqual(store, {'mysql.passwd': '1',
                                 'userA.passwd': '2',
                                 'mysql-userA.passwd': '3'})

    @mock.patch.object(mysql, 'log', lambda *args, **kwargs: None)
    @mock.patch.object(mysql, 'is_leader')
    @mock.patch.object(mysql, 'leader_set')
    def test_migrate_passwords_to_leader_storage_not_leader(self, mock_leader_set,
                                                            mock_is_leader):
        mock_is_leader.return_value = False
        tmpdir = tempfile.mkdtemp('charm-helpers-unit-tests')
        try:
            root_tmplt = "%s/mysql.passwd" % (tmpdir)
            helper = mysql.MySQLHelper(root_tmplt, None, host='hostA')
            helper.migrate_passwords_to_leader_storage()
        finally:
            shutil.rmtree(tmpdir)
        mock_leader_set.assert_not_called()


class MySQLConfigHelperTests(unittest.TestCase):

    def setUp(self):
        super(MySQLConfigHelperTests, self).setUp()
        self.config_data = {}
        self.config = mock.MagicMock()
        mysql.config_get = self.config
        self.config.side_effect = self._fake_config

    def _fake_config(self, key=None):
        if key:
            try:
                return self.config_data[key]
            except KeyError:
                return None
        else:
            return self.config_data

    @mock.patch.object(mysql.MySQLConfigHelper, 'get_mem_total')
    @mock.patch.object(mysql, 'log')
    def test_get_innodb_pool_fixed(self, log, mem):
        mem.return_value = "100G"
        self.config_data = {
            'innodb-buffer-pool-size': "50%",
        }

        helper = mysql.MySQLConfigHelper()

        self.assertEqual(
            helper.get_innodb_buffer_pool_size(),
            helper.human_to_bytes("50G"))

    @mock.patch.object(mysql.MySQLConfigHelper, 'get_mem_total')
    @mock.patch.object(mysql, 'log')
    def test_get_innodb_pool_not_set(self, mog, mem):
        mem.return_value = "100G"
        self.config_data = {
            'innodb-buffer-pool-size': '',
        }

        helper = mysql.MySQLConfigHelper()

        self.assertEqual(
            helper.get_innodb_buffer_pool_size(),
            helper.DEFAULT_INNODB_BUFFER_SIZE_MAX)

    @mock.patch.object(mysql.MySQLConfigHelper, 'get_mem_total')
    @mock.patch.object(mysql, 'log')
    def test_get_innodb_buffer_unset(self, mog, mem):
        mem.return_value = "100G"
        self.config_data = {
            'innodb-buffer-pool-size': None,
            'dataset-size': None,
        }

        helper = mysql.MySQLConfigHelper()

        self.assertEqual(
            helper.get_innodb_buffer_pool_size(),
            helper.DEFAULT_INNODB_BUFFER_SIZE_MAX)

    @mock.patch.object(mysql.MySQLConfigHelper, 'get_mem_total')
    @mock.patch.object(mysql, 'log')
    def test_get_innodb_buffer_unset_small(self, mog, mem):
        mem.return_value = "512M"
        self.config_data = {
            'innodb-buffer-pool-size': None,
            'dataset-size': None,
        }

        helper = mysql.MySQLConfigHelper()

        self.assertEqual(
            helper.get_innodb_buffer_pool_size(),
            int(helper.human_to_bytes(mem.return_value) *
                helper.DEFAULT_INNODB_BUFFER_FACTOR))

    @mock.patch.object(mysql.MySQLConfigHelper, 'get_mem_total')
    @mock.patch.object(mysql, 'log')
    def test_get_innodb_dataset_size(self, mog, mem):
        mem.return_value = "100G"
        self.config_data = {
            'dataset-size': "10G",
        }

        helper = mysql.MySQLConfigHelper()

        self.assertEqual(
            helper.get_innodb_buffer_pool_size(),
            int(helper.human_to_bytes("10G")))

    @mock.patch.object(mysql.MySQLConfigHelper, 'get_mem_total')
    @mock.patch.object(mysql, 'log')
    def test_get_tuning_level(self, mog, mem):
        mem.return_value = "512M"
        self.config_data = {
            'tuning-level': 'safest',
        }

        helper = mysql.MySQLConfigHelper()

        self.assertEqual(
            helper.get_innodb_flush_log_at_trx_commit(),
            1
        )

    @mock.patch.object(mysql.MySQLConfigHelper, 'get_mem_total')
    @mock.patch.object(mysql, 'log')
    def test_get_tuning_level_fast(self, mog, mem):
        mem.return_value = "512M"
        self.config_data = {
            'tuning-level': 'fast',
        }

        helper = mysql.MySQLConfigHelper()

        self.assertEqual(
            helper.get_innodb_flush_log_at_trx_commit(),
            2
        )

    @mock.patch.object(mysql.MySQLConfigHelper, 'get_mem_total')
    @mock.patch.object(mysql, 'log')
    def test_get_tuning_level_unsafe(self, mog, mem):
        mem.return_value = "512M"
        self.config_data = {
            'tuning-level': 'unsafe',
        }

        helper = mysql.MySQLConfigHelper()

        self.assertEqual(
            helper.get_innodb_flush_log_at_trx_commit(),
            0
        )

    @mock.patch.object(mysql.MySQLConfigHelper, 'get_mem_total')
    @mock.patch.object(mysql, 'log')
    def test_get_innodb_valid_values(self, mog, mem):
        mem.return_value = "512M"
        self.config_data = {
            'innodb-change-buffering': 'all',
        }

        helper = mysql.MySQLConfigHelper()

        self.assertEqual(
            helper.get_innodb_change_buffering(),
            'all'
        )

    @mock.patch.object(mysql.MySQLConfigHelper, 'get_mem_total')
    @mock.patch.object(mysql, 'log')
    def test_get_innodb_invalid_values(self, mog, mem):
        mem.return_value = "512M"
        self.config_data = {
            'innodb-change-buffering': 'invalid',
        }

        helper = mysql.MySQLConfigHelper()

        self.assertTrue(helper.get_innodb_change_buffering() is None)


class PerconaTests(unittest.TestCase):

    def setUp(self):
        super(PerconaTests, self).setUp()
        self.config_data = {}
        self.config = mock.MagicMock()
        mysql.config_get = self.config
        self.config.side_effect = self._fake_config

    def _fake_config(self, key=None):
        if key:
            try:
                return self.config_data[key]
            except KeyError:
                return None
        else:
            return self.config_data

    @mock.patch.object(mysql.MySQLConfigHelper, 'get_mem_total')
    @mock.patch.object(mysql, 'log')
    def test_parse_config_innodb_pool_fixed(self, log, mem):
        mem.return_value = "100G"
        self.config_data = {
            'innodb-buffer-pool-size': "50%",
        }

        helper = mysql.PerconaClusterHelper()
        mysql_config = helper.parse_config()

        self.assertEqual(mysql_config.get('innodb_buffer_pool_size'),
                         helper.human_to_bytes("50G"))

    @mock.patch.object(mysql.MySQLConfigHelper, 'get_mem_total')
    @mock.patch.object(mysql, 'log')
    def test_parse_config_innodb_pool_not_set(self, mog, mem):
        mem.return_value = "100G"
        self.config_data = {
            'innodb-buffer-pool-size': '',
        }

        helper = mysql.PerconaClusterHelper()
        mysql_config = helper.parse_config()

        self.assertEqual(
            mysql_config.get('innodb_buffer_pool_size'),
            helper.DEFAULT_INNODB_BUFFER_SIZE_MAX)

    @mock.patch.object(mysql.MySQLConfigHelper, 'get_mem_total')
    @mock.patch.object(mysql, 'log')
    def test_parse_config_innodb_buffer_unset(self, mog, mem):
        mem.return_value = "100G"
        self.config_data = {
            'innodb-buffer-pool-size': None,
            'dataset-size': None,
        }

        helper = mysql.PerconaClusterHelper()
        mysql_config = helper.parse_config()

        self.assertEqual(
            mysql_config.get('innodb_buffer_pool_size'),
            helper.DEFAULT_INNODB_BUFFER_SIZE_MAX)

    @mock.patch.object(mysql.MySQLConfigHelper, 'get_mem_total')
    @mock.patch.object(mysql, 'log')
    def test_parse_config_innodb_buffer_unset_small(self, mog, mem):
        mem.return_value = "512M"
        self.config_data = {
            'innodb-buffer-pool-size': None,
            'dataset-size': None,
        }

        helper = mysql.PerconaClusterHelper()
        mysql_config = helper.parse_config()

        self.assertEqual(
            mysql_config.get('innodb_buffer_pool_size'),
            int(helper.human_to_bytes(mem.return_value) *
                helper.DEFAULT_INNODB_BUFFER_FACTOR))

    @mock.patch.object(mysql.MySQLConfigHelper, 'get_mem_total')
    @mock.patch.object(mysql, 'log')
    def test_parse_config_innodb_dataset_size(self, mog, mem):
        mem.return_value = "100G"
        self.config_data = {
            'dataset-size': "10G",
        }

        helper = mysql.PerconaClusterHelper()
        mysql_config = helper.parse_config()

        self.assertEqual(
            mysql_config.get('innodb_buffer_pool_size'),
            int(helper.human_to_bytes("10G")))

    @mock.patch.object(mysql.MySQLConfigHelper, 'get_mem_total')
    @mock.patch.object(mysql, 'log')
    def test_parse_config_wait_timeout(self, mog, mem):
        mem.return_value = "100G"

        timeout = 314
        self.config_data = {
            'wait-timeout': timeout,
        }

        helper = mysql.PerconaClusterHelper()
        mysql_config = helper.parse_config()

        self.assertEqual(
            mysql_config.get('wait_timeout'),
            timeout)

    @mock.patch.object(mysql.MySQLConfigHelper, 'get_mem_total')
    @mock.patch.object(mysql, 'log')
    def test_parse_config_tuning_level(self, mog, mem):
        mem.return_value = "512M"
        self.config_data = {
            'tuning-level': 'safest',
        }

        helper = mysql.PerconaClusterHelper()
        mysql_config = helper.parse_config()

        self.assertEqual(
            mysql_config.get('innodb_flush_log_at_trx_commit'),
            1
        )

    @mock.patch.object(mysql.MySQLConfigHelper, 'get_mem_total')
    @mock.patch.object(mysql, 'log')
    def test_parse_config_tuning_level_fast(self, mog, mem):
        mem.return_value = "512M"
        self.config_data = {
            'tuning-level': 'fast',
        }

        helper = mysql.PerconaClusterHelper()
        mysql_config = helper.parse_config()

        self.assertEqual(
            mysql_config.get('innodb_flush_log_at_trx_commit'),
            2
        )

    @mock.patch.object(mysql.MySQLConfigHelper, 'get_mem_total')
    @mock.patch.object(mysql, 'log')
    def test_parse_config_tuning_level_unsafe(self, mog, mem):
        mem.return_value = "512M"
        self.config_data = {
            'tuning-level': 'unsafe',
        }

        helper = mysql.PerconaClusterHelper()
        mysql_config = helper.parse_config()

        self.assertEqual(
            mysql_config.get('innodb_flush_log_at_trx_commit'),
            0
        )

    @mock.patch.object(mysql.MySQLConfigHelper, 'get_mem_total')
    @mock.patch.object(mysql, 'log')
    def test_parse_config_innodb_valid_values(self, mog, mem):
        mem.return_value = "512M"
        self.config_data = {
            'innodb-change-buffering': 'all',
            'innodb-io-capacity': 100,
        }

        helper = mysql.PerconaClusterHelper()
        mysql_config = helper.parse_config()

        self.assertEqual(
            mysql_config.get('innodb_change_buffering'),
            'all'
        )

        self.assertEqual(
            mysql_config.get('innodb_io_capacity'),
            100
        )

    @mock.patch.object(mysql.MySQLConfigHelper, 'get_mem_total')
    @mock.patch.object(mysql, 'log')
    def test_parse_config_innodb_invalid_values(self, mog, mem):
        mem.return_value = "512M"
        self.config_data = {
            'innodb-change-buffering': 'invalid',
        }

        helper = mysql.PerconaClusterHelper()
        mysql_config = helper.parse_config()

        self.assertTrue('innodb_change_buffering' not in mysql_config)
        self.assertTrue('innodb_io_capacity' not in mysql_config)


class Mysql8Tests(unittest.TestCase):

    def setUp(self):
        super(Mysql8Tests, self).setUp()
        self.template = "/tmp/mysql-passwd.txt"
        self.connection = mock.MagicMock()
        self.cursor = mock.MagicMock()
        self.connection.cursor.return_value = self.cursor
        self.helper = mysql.MySQL8Helper(
            rpasswdf_template=self.template,
            upasswdf_template=self.template)
        self.helper.connection = self.connection
        self.user = "user"
        self.host = "10.5.0.21"
        self.password = "passwd"
        self.db = "mydb"

    def test_grant_exists(self):
        # With backticks
        self.cursor.fetchall.return_value = (
            ("GRANT USAGE ON *.* TO `{}`@`{}`".format(self.user, self.host),),
            ("GRANT ALL PRIVILEGES ON `{}`.* TO `{}`@`{}`"
             .format(self.db, self.user, self.host),))
        self.assertTrue(self.helper.grant_exists(self.db, self.user, self.host))

        self.cursor.execute.assert_called_with(
            "SHOW GRANTS FOR '{}'@'{}'".format(self.user, self.host))

        # With single quotes
        self.cursor.fetchall.return_value = (
            ("GRANT USAGE ON *.* TO '{}'@'{}'".format(self.user, self.host),),
            ("GRANT ALL PRIVILEGES ON '{}'.* TO '{}'@'{}'"
             .format(self.db, self.user, self.host),))
        self.assertTrue(self.helper.grant_exists(self.db, self.user, self.host))

        # Grant not there
        self.cursor.fetchall.return_value = (
            ("GRANT USAGE ON *.* TO '{}'@'{}'".format("someuser", "notmyhost"),),
            ("GRANT ALL PRIVILEGES ON '{}'.* TO '{}'@'{}'"
             .format("somedb", "someuser", "notmyhost"),))
        self.assertFalse(self.helper.grant_exists(self.db, self.user, self.host))

    def test_create_grant(self):
        self.helper.grant_exists = mock.MagicMock(return_value=False)
        self.helper.create_user = mock.MagicMock()

        self.helper.create_grant(self.db, self.user, self.host, self.password)
        self.cursor.execute.assert_called_with(
            "GRANT ALL PRIVILEGES ON `{}`.* TO '{}'@'{}'"
            .format(self.db, self.user, self.host))
        self.helper.create_user.assert_called_with(self.user, self.host, self.password)

    def test_create_user(self):
        self.helper.create_user(self.user, self.host, self.password)
        self.cursor.execute.assert_called_with(
            "CREATE USER '{}'@'{}' IDENTIFIED BY '{}'".
            format(self.user, self.host, self.password))

    def test_create_router_grant(self):
        self.helper.create_user = mock.MagicMock()

        self.helper.create_router_grant(self.user, self.host, self.password)
        _calls = [
            mock.call("GRANT CREATE USER ON *.* TO '{}'@'{}' WITH GRANT OPTION"
                      .format(self.user, self.host)),
            mock.call("GRANT SELECT, INSERT, UPDATE, DELETE, EXECUTE ON "
                      "mysql_innodb_cluster_metadata.* TO '{}'@'{}'"
                      .format(self.user, self.host)),
            mock.call("GRANT SELECT ON mysql.user TO '{}'@'{}'"
                      .format(self.user, self.host)),
            mock.call("GRANT SELECT ON "
                      "performance_schema.replication_group_members TO "
                      "'{}'@'{}'".format(self.user, self.host)),
            mock.call("GRANT SELECT ON "
                      "performance_schema.replication_group_member_stats TO "
                      "'{}'@'{}'".format(self.user, self.host)),
            mock.call("GRANT SELECT ON "
                      "performance_schema.global_variables TO "
                      "'{}'@'{}'".format(self.user, self.host))]

        self.cursor.execute.assert_has_calls(_calls)
        self.helper.create_user.assert_called_with(self.user, self.host, self.password)

    def test_configure_router(self):
        self.helper.create_user = mock.MagicMock()
        self.helper.create_router_grant = mock.MagicMock()
        self.helper.normalize_address = mock.MagicMock(return_value=self.host)
        self.helper.get_mysql_password = mock.MagicMock(return_value=self.password)

        self.assertEqual(self.password, self.helper.configure_router(self.host, self.user))
        self.helper.create_user.assert_called_with(self.user, self.host, self.password)
        self.helper.create_router_grant.assert_called_with(self.user, self.host, self.password)


class MysqlHelperTests(unittest.TestCase):

    def setUp(self):
        super(MysqlHelperTests, self).setUp()

    def test_get_prefix(self):
        _tests = {
            "prefix1": "prefix1_username",
            "prefix2": "prefix2_database",
            "prefix3": "prefix3_hostname"}

        for key in _tests.keys():
            self.assertEqual(
                key,
                mysql.get_prefix(_tests[key]))

    def test_get_db_data(self):
        _unprefixed = "myprefix"
        # Test relation data has every variation of shared-db/db-router data
        _relation_data = {
            "egress-subnets": "10.5.0.43/32",
            "ingress-address": "10.5.0.43",
            "nova_database": "nova",
            "nova_hostname": "10.5.0.43",
            "nova_username": "nova",
            "novaapi_database": "nova_api",
            "novaapi_hostname": "10.5.0.43",
            "novaapi_username": "nova",
            "novacell0_database": "nova_cell0",
            "novacell0_hostname": "10.5.0.43",
            "novacell0_username": "nova",
            "private-address": "10.5.0.43",
            "database": "keystone",
            "username": "keystone",
            "hostname": "10.5.0.43",
            "mysqlrouter_username":
            "mysqlrouteruser",
            "mysqlrouter_hostname": "10.5.0.43"}

        _expected_data = OrderedDict([
            ('nova', OrderedDict([('database', 'nova'),
                                  ('hostname', '10.5.0.43'),
                                  ('username', 'nova')])),
            ('novaapi', OrderedDict([('database', 'nova_api'),
                                     ('hostname', '10.5.0.43'),
                                     ('username', 'nova')])),
            ('novacell0', OrderedDict([('database', 'nova_cell0'),
                                       ('hostname', '10.5.0.43'),
                                       ('username', 'nova')])),
            ('mysqlrouter', OrderedDict([('username', 'mysqlrouteruser'),
                                         ('hostname', '10.5.0.43')])),
            ('myprefix', OrderedDict([('hostname', '10.5.0.43'),
                                      ('database', 'keystone'),
                                      ('username', 'keystone')]))])

        _results = mysql.get_db_data(_relation_data, unprefixed=_unprefixed)

        for prefix in _expected_data.keys():
            for key in _expected_data[prefix].keys():
                self.assertEqual(
                    _results[prefix][key], _expected_data[prefix][key])
