Fix deprecated options from MySQL 8.2 (#662)

* Fix show master status for MySQL 8.2+

* Fix mysqldump option form --master-data to --source-data

* Fix incompatibility between mysqldump 8.0 and MySQL 8.4

Installing the same version between the client and the server makes
sense anyway. The incompatibility arise when you use mysqldump with
--source-data. The the tool tries to perform a SHOW MASTER STATUS which
is deprecated in MySQL 8.2+.

* Fix missing condition

* Fix unit tests

* Add a query resolver depending on implementation and version

* Sanity

* Fix SHOW REPLICA STATUS queries

* Fix mariadb's SHOW REPLICA HOSTS query

* Fix CHANGE MASTER for MySQL 8.0.23+

* Fix integration test for CHANGE MASTER

* Fix integration test for CHANGE MASTER

* Fix replication queries for MySQL 8.0.23+ and 8.4+

* Revert file edited by mistake

* Enhance tests format
This commit is contained in:
Laurent Indermühle 2024-08-05 08:55:18 +02:00 committed by GitHub
parent c503dc5b6b
commit cd9f4fcf57
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 503 additions and 94 deletions

View file

@ -0,0 +1,180 @@
# -*- coding: utf-8 -*-
from __future__ import (absolute_import, division, print_function)
from ._version import LooseVersion
__metaclass__ = type
class CommandResolver():
def __init__(self, server_implementation, server_version):
self.server_implementation = server_implementation
self.server_version = LooseVersion(server_version)
def resolve_command(self, command):
"""
Resolves the appropriate SQL command based on the server implementation and version.
Parameters:
command (str): The base SQL command to be resolved (e.g., "SHOW SLAVE HOSTS").
Returns:
str: The resolved SQL command suitable for the given server implementation and version.
Raises:
ValueError: If the command is not supported or recognized.
Example:
Given a server implementation `mysql` and server version `8.0.23`, and a command `SHOW SLAVE HOSTS`,
the method will resolve the command based on the following table of versions:
Table:
[
("mysql", "default", "SHOW SLAVES HOSTS default"),
("mysql", "5.7.0", "SHOW SLAVES HOSTS"),
("mysql", "8.0.22", "SHOW REPLICAS"),
("mysql", "8.4.0", "SHOW REPLICAS 8.4"),
("mariadb", "10.5.1", "SHOW REPLICAS HOSTS"),
]
Example usage:
>>> resolver = CommandResolver("mysql", "8.0.23")
>>> resolver.resolve_command("SHOW SLAVE HOSTS")
'SHOW REPLICAS'
In this example, the resolver will:
- Filter and sort applicable versions: [
("8.4.0", "SHOW REPLICAS 8.4"),
("8.0.22", "HOW REPLICAS"),
("5.7.0", "SHOW SLAVES HOSTS")
]
- Iterate through the sorted list and find the first version less than or equal to 8.0.23,
which is 8.0.22, and return the corresponding command.
"""
# Convert the command to uppercase to ensure case-insensitive lookup
command = command.upper()
commands = {
"SHOW MASTER STATUS": {
("mysql", "default"): "SHOW MASTER STATUS",
("mariadb", "default"): "SHOW MASTER STATUS",
("mysql", "8.2.0"): "SHOW BINARY LOG STATUS",
("mariadb", "10.5.2"): "SHOW BINLOG STATUS",
},
"SHOW SLAVE STATUS": {
("mysql", "default"): "SHOW SLAVE STATUS",
("mariadb", "default"): "SHOW SLAVE STATUS",
("mysql", "8.0.22"): "SHOW REPLICA STATUS",
("mariadb", "10.5.1"): "SHOW REPLICA STATUS",
},
"SHOW SLAVE HOSTS": {
("mysql", "default"): "SHOW SLAVE HOSTS",
("mariadb", "default"): "SHOW SLAVE HOSTS",
("mysql", "8.0.22"): "SHOW REPLICAS",
("mariadb", "10.5.1"): "SHOW REPLICA HOSTS",
},
"CHANGE MASTER": {
("mysql", "default"): "CHANGE MASTER",
("mariadb", "default"): "CHANGE MASTER",
("mysql", "8.0.23"): "CHANGE REPLICATION SOURCE",
},
"MASTER_HOST": {
("mysql", "default"): "MASTER_HOST",
("mariadb", "default"): "MASTER_HOST",
("mysql", "8.0.23"): "SOURCE_HOST",
},
"MASTER_USER": {
("mysql", "default"): "MASTER_USER",
("mariadb", "default"): "MASTER_USER",
("mysql", "8.0.23"): "SOURCE_USER",
},
"MASTER_PASSWORD": {
("mysql", "default"): "MASTER_PASSWORD",
("mariadb", "default"): "MASTER_PASSWORD",
("mysql", "8.0.23"): "SOURCE_PASSWORD",
},
"MASTER_PORT": {
("mysql", "default"): "MASTER_PORT",
("mariadb", "default"): "MASTER_PORT",
("mysql", "8.0.23"): "SOURCE_PORT",
},
"MASTER_CONNECT_RETRY": {
("mysql", "default"): "MASTER_CONNECT_RETRY",
("mariadb", "default"): "MASTER_CONNECT_RETRY",
("mysql", "8.0.23"): "SOURCE_CONNECT_RETRY",
},
"MASTER_LOG_FILE": {
("mysql", "default"): "MASTER_LOG_FILE",
("mariadb", "default"): "MASTER_LOG_FILE",
("mysql", "8.0.23"): "SOURCE_LOG_FILE",
},
"MASTER_LOG_POS": {
("mysql", "default"): "MASTER_LOG_POS",
("mariadb", "default"): "MASTER_LOG_POS",
("mysql", "8.0.23"): "SOURCE_LOG_POS",
},
"MASTER_DELAY": {
("mysql", "default"): "MASTER_DELAY",
("mariadb", "default"): "MASTER_DELAY",
("mysql", "8.0.23"): "SOURCE_DELAY",
},
"MASTER_SSL": {
("mysql", "default"): "MASTER_SSL",
("mariadb", "default"): "MASTER_SSL",
("mysql", "8.0.23"): "SOURCE_SSL",
},
"MASTER_SSL_CA": {
("mysql", "default"): "MASTER_SSL_CA",
("mariadb", "default"): "MASTER_SSL_CA",
("mysql", "8.0.23"): "SOURCE_SSL_CA",
},
"MASTER_SSL_CAPATH": {
("mysql", "default"): "MASTER_SSL_CAPATH",
("mariadb", "default"): "MASTER_SSL_CAPATH",
("mysql", "8.0.23"): "SOURCE_SSL_CAPATH",
},
"MASTER_SSL_CERT": {
("mysql", "default"): "MASTER_SSL_CERT",
("mariadb", "default"): "MASTER_SSL_CERT",
("mysql", "8.0.23"): "SOURCE_SSL_CERT",
},
"MASTER_SSL_KEY": {
("mysql", "default"): "MASTER_SSL_KEY",
("mariadb", "default"): "MASTER_SSL_KEY",
("mysql", "8.0.23"): "SOURCE_SSL_KEY",
},
"MASTER_SSL_CIPHER": {
("mysql", "default"): "MASTER_SSL_CIPHER",
("mariadb", "default"): "MASTER_SSL_CIPHER",
("mysql", "8.0.23"): "SOURCE_SSL_CIPHER",
},
"MASTER_SSL_VERIFY_SERVER_CERT": {
("mysql", "default"): "MASTER_SSL_VERIFY_SERVER_CERT",
("mariadb", "default"): "MASTER_SSL_VERIFY_SERVER_CERT",
("mysql", "8.0.23"): "SOURCE_SSL_VERIFY_SERVER_CERT",
},
"MASTER_AUTO_POSITION": {
("mysql", "default"): "MASTER_AUTO_POSITION",
("mariadb", "default"): "MASTER_AUTO_POSITION",
("mysql", "8.0.23"): "SOURCE_AUTO_POSITION",
},
"RESET MASTER": {
("mysql", "default"): "RESET MASTER",
("mariadb", "default"): "RESET MASTER",
("mysql", "8.4.0"): "RESET BINARY LOGS AND GTIDS",
},
# Add more command mappings here
}
if command in commands:
cmd_syntaxes = commands[command]
applicable_versions = [(v, cmd) for (impl, v), cmd in cmd_syntaxes.items() if impl == self.server_implementation and v != 'default']
applicable_versions.sort(reverse=True, key=lambda x: LooseVersion(x[0]))
for version, cmd in applicable_versions:
if self.server_version >= LooseVersion(version):
return cmd
return cmd_syntaxes[(self.server_implementation, "default")]
raise ValueError("Unsupported command: %s" % command)

View file

@ -343,7 +343,15 @@ import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.mysql.plugins.module_utils.database import mysql_quote_identifier
from ansible_collections.community.mysql.plugins.module_utils.mysql import mysql_connect, mysql_driver, mysql_driver_fail_msg, mysql_common_argument_spec
from ansible_collections.community.mysql.plugins.module_utils.mysql import (
mysql_connect,
mysql_driver,
mysql_driver_fail_msg,
mysql_common_argument_spec,
get_server_implementation,
get_server_version,
)
from ansible_collections.community.mysql.plugins.module_utils.version import LooseVersion
from ansible.module_utils.six.moves import shlex_quote
from ansible.module_utils._text import to_native
@ -372,7 +380,8 @@ def db_delete(cursor, db):
def db_dump(module, host, user, password, db_name, target, all_databases, port,
config_file, socket=None, ssl_cert=None, ssl_key=None, ssl_ca=None,
config_file, server_implementation, server_version, socket=None,
ssl_cert=None, ssl_key=None, ssl_ca=None,
single_transaction=None, quick=None, ignore_tables=None, hex_blob=None,
encoding=None, force=False, master_data=0, skip_lock_tables=False,
dump_extra_args=None, unsafe_password=False, restrict_config_file=False,
@ -431,7 +440,11 @@ def db_dump(module, host, user, password, db_name, target, all_databases, port,
if hex_blob:
cmd += " --hex-blob"
if master_data:
cmd += " --master-data=%s" % master_data
if (server_implementation == 'mysql' and
LooseVersion(server_version) >= LooseVersion("8.2.0")):
cmd += " --source-data=%s" % master_data
else:
cmd += " --master-data=%s" % master_data
if dump_extra_args is not None:
cmd += " " + dump_extra_args
@ -690,6 +703,9 @@ def main():
else:
module.fail_json(msg="unable to find %s. Exception message: %s" % (config_file, to_native(e)))
server_implementation = get_server_implementation(cursor)
server_version = get_server_version(cursor)
changed = False
if not os.path.exists(config_file):
config_file = None
@ -730,7 +746,8 @@ def main():
module.exit_json(changed=True, db=db_name, db_list=db)
rc, stdout, stderr = db_dump(module, login_host, login_user,
login_password, db, target, all_databases,
login_port, config_file, socket, ssl_cert, ssl_key,
login_port, config_file, server_implementation, server_version,
socket, ssl_cert, ssl_key,
ssl_ca, single_transaction, quick, ignore_tables,
hex_blob, encoding, force, master_data, skip_lock_tables,
dump_extra_args, unsafe_login_password, restrict_config_file,

View file

@ -293,6 +293,9 @@ connector_version:
from decimal import Decimal
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.mysql.plugins.module_utils.command_resolver import (
CommandResolver
)
from ansible_collections.community.mysql.plugins.module_utils.mysql import (
mysql_connect,
mysql_common_argument_spec,
@ -301,6 +304,7 @@ from ansible_collections.community.mysql.plugins.module_utils.mysql import (
get_connector_name,
get_connector_version,
get_server_implementation,
get_server_version,
)
from ansible_collections.community.mysql.plugins.module_utils.user import (
@ -335,11 +339,13 @@ class MySQL_Info(object):
5. add info about the new subset with an example to RETURN block
"""
def __init__(self, module, cursor, server_implementation, user_implementation):
def __init__(self, module, cursor, server_implementation, server_version, user_implementation):
self.module = module
self.cursor = cursor
self.server_implementation = server_implementation
self.server_version = server_version
self.user_implementation = user_implementation
self.command_resolver = CommandResolver(self.server_implementation, self.server_version)
self.info = {
'version': {},
'databases': {},
@ -501,7 +507,8 @@ class MySQL_Info(object):
def __get_master_status(self):
"""Get master status if the instance is a master."""
res = self.__exec_sql('SHOW MASTER STATUS')
query = self.command_resolver.resolve_command("SHOW MASTER STATUS")
res = self.__exec_sql(query)
if res:
for line in res:
for vname, val in iteritems(line):
@ -509,10 +516,8 @@ class MySQL_Info(object):
def __get_slave_status(self):
"""Get slave status if the instance is a slave."""
if self.server_implementation == "mariadb":
res = self.__exec_sql('SHOW ALL SLAVES STATUS')
else:
res = self.__exec_sql('SHOW SLAVE STATUS')
query = self.command_resolver.resolve_command("SHOW SLAVE STATUS")
res = self.__exec_sql(query)
if res:
for line in res:
host = line['Master_Host']
@ -533,7 +538,8 @@ class MySQL_Info(object):
def __get_slaves(self):
"""Get slave hosts info if the instance is a master."""
res = self.__exec_sql('SHOW SLAVE HOSTS')
query = self.command_resolver.resolve_command("SHOW SLAVE HOSTS")
res = self.__exec_sql(query)
if res:
for line in res:
srv_id = line['Server_id']
@ -762,12 +768,13 @@ def main():
module.fail_json(msg)
server_implementation = get_server_implementation(cursor)
server_version = get_server_version(cursor)
user_implementation = get_user_implementation(cursor)
###############################
# Create object and do main job
mysql = MySQL_Info(module, cursor, server_implementation, user_implementation)
mysql = MySQL_Info(module, cursor, server_implementation, server_version, user_implementation)
module.exit_json(changed=False,
server_engine='MariaDB' if server_implementation == 'mariadb' else 'MySQL',

View file

@ -20,11 +20,12 @@ author:
- Balazs Pocze (@banyek)
- Andrew Klychkov (@Andersson007)
- Dennis Urtubia (@dennisurtubia)
- Laurent Indermühle (@laurent-indermuehle)
options:
mode:
description:
- Module operating mode. Could be
C(changeprimary) (CHANGE MASTER TO),
C(changeprimary) (CHANGE MASTER TO) - also works for MySQL 8.0.23 and later since community.mysql 3.10.0,
C(changereplication) (CHANGE REPLICATION SOURCE TO) - only supported in MySQL 8.0.23 and later,
C(getprimary) (SHOW MASTER STATUS),
C(getreplica) (SHOW REPLICA STATUS),
@ -298,8 +299,10 @@ queries:
import os
import warnings
from ansible_collections.community.mysql.plugins.module_utils.version import LooseVersion
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.mysql.plugins.module_utils.command_resolver import (
CommandResolver
)
from ansible_collections.community.mysql.plugins.module_utils.mysql import (
get_server_version,
get_server_implementation,
@ -313,18 +316,9 @@ from ansible.module_utils._text import to_native
executed_queries = []
def get_primary_status(cursor):
term = "MASTER"
version = get_server_version(cursor)
server_implementation = get_server_implementation(cursor)
if server_implementation == "mysql" and LooseVersion(version) >= LooseVersion("8.2.0"):
term = "BINARY LOG"
if server_implementation == "mariadb" and LooseVersion(version) >= LooseVersion("10.5.2"):
term = "BINLOG"
cursor.execute("SHOW %s STATUS" % term)
def get_primary_status(cursor, command_resolver):
query = command_resolver.resolve_command("SHOW MASTER STATUS")
cursor.execute(query)
primarystatus = cursor.fetchone()
return primarystatus
@ -410,8 +404,8 @@ def reset_replica_all(module, cursor, connection_name='', channel='', fail_on_er
return reset
def reset_primary(module, cursor, fail_on_error=False):
query = 'RESET MASTER'
def reset_primary(module, cursor, command_resolver, fail_on_error=False):
query = command_resolver.resolve_command('RESET MASTER')
try:
executed_queries.append(query)
cursor.execute(query)
@ -420,7 +414,7 @@ def reset_primary(module, cursor, fail_on_error=False):
reset = False
except Exception as e:
if fail_on_error:
module.fail_json(msg="RESET MASTER failed: %s" % to_native(e))
module.fail_json(msg="%s failed: %s" % (command_resolver.resolve_command('RESET MASTER'), to_native(e)))
reset = False
return reset
@ -447,11 +441,12 @@ def start_replica(module, cursor, connection_name='', channel='', fail_on_error=
return started
def changeprimary(cursor, chm, connection_name='', channel=''):
def changeprimary(cursor, command_resolver, chm, connection_name='', channel=''):
query_head = command_resolver.resolve_command("CHANGE MASTER")
if connection_name:
query = "CHANGE MASTER '%s' TO %s" % (connection_name, ','.join(chm))
query = "%s '%s' TO %s" % (query_head, connection_name, ','.join(chm))
else:
query = 'CHANGE MASTER TO %s' % ','.join(chm)
query = '%s TO %s' % (query_head, ','.join(chm))
if channel:
query += " FOR CHANNEL '%s'" % channel
@ -566,8 +561,11 @@ def main():
else:
module.fail_json(msg="unable to find %s. Exception message: %s" % (config_file, to_native(e)))
server_version = get_server_version(cursor)
server_implementation = get_server_implementation(cursor)
command_resolver = CommandResolver(server_implementation, server_version)
cursor.execute("SELECT VERSION()")
if 'mariadb' in cursor.fetchone()["VERSION()"].lower():
if server_implementation == 'mariadb':
from ansible_collections.community.mysql.plugins.module_utils.implementations.mariadb import replication as impl
else:
from ansible_collections.community.mysql.plugins.module_utils.implementations.mysql import replication as impl
@ -582,7 +580,7 @@ def main():
primary_use_gtid = 'slave_pos'
if mode == 'getprimary':
status = get_primary_status(cursor)
status = get_primary_status(cursor, command_resolver)
if status and "File" in status and "Position" in status:
status['Is_Primary'] = True
else:
@ -610,52 +608,52 @@ def main():
chm = []
result = {}
if primary_host is not None:
chm.append("MASTER_HOST='%s'" % primary_host)
chm.append("%s='%s'" % (command_resolver.resolve_command('MASTER_HOST'), primary_host))
if primary_user is not None:
chm.append("MASTER_USER='%s'" % primary_user)
chm.append("%s='%s'" % (command_resolver.resolve_command('MASTER_USER'), primary_user))
if primary_password is not None:
chm.append("MASTER_PASSWORD='%s'" % primary_password)
chm.append("%s='%s'" % (command_resolver.resolve_command('MASTER_PASSWORD'), primary_password))
if primary_port is not None:
chm.append("MASTER_PORT=%s" % primary_port)
chm.append("%s=%s" % (command_resolver.resolve_command('MASTER_PORT'), primary_port))
if primary_connect_retry is not None:
chm.append("MASTER_CONNECT_RETRY=%s" % primary_connect_retry)
chm.append("%s=%s" % (command_resolver.resolve_command('MASTER_CONNECT_RETRY'), primary_connect_retry))
if primary_log_file is not None:
chm.append("MASTER_LOG_FILE='%s'" % primary_log_file)
chm.append("%s='%s'" % (command_resolver.resolve_command('MASTER_LOG_FILE'), primary_log_file))
if primary_log_pos is not None:
chm.append("MASTER_LOG_POS=%s" % primary_log_pos)
chm.append("%s=%s" % (command_resolver.resolve_command('MASTER_LOG_POS'), primary_log_pos))
if primary_delay is not None:
chm.append("MASTER_DELAY=%s" % primary_delay)
chm.append("%s=%s" % (command_resolver.resolve_command('MASTER_DELAY'), primary_delay))
if relay_log_file is not None:
chm.append("RELAY_LOG_FILE='%s'" % relay_log_file)
if relay_log_pos is not None:
chm.append("RELAY_LOG_POS=%s" % relay_log_pos)
if primary_ssl is not None:
if primary_ssl:
chm.append("MASTER_SSL=1")
chm.append("%s=1" % command_resolver.resolve_command('MASTER_SSL'))
else:
chm.append("MASTER_SSL=0")
chm.append("%s=0" % command_resolver.resolve_command('MASTER_SSL'))
if primary_ssl_ca is not None:
chm.append("MASTER_SSL_CA='%s'" % primary_ssl_ca)
chm.append("%s='%s'" % (command_resolver.resolve_command('MASTER_SSL_CA'), primary_ssl_ca))
if primary_ssl_capath is not None:
chm.append("MASTER_SSL_CAPATH='%s'" % primary_ssl_capath)
chm.append("%s='%s'" % (command_resolver.resolve_command('MASTER_SSL_CAPATH'), primary_ssl_capath))
if primary_ssl_cert is not None:
chm.append("MASTER_SSL_CERT='%s'" % primary_ssl_cert)
chm.append("%s='%s'" % (command_resolver.resolve_command('MASTER_SSL_CERT'), primary_ssl_cert))
if primary_ssl_key is not None:
chm.append("MASTER_SSL_KEY='%s'" % primary_ssl_key)
chm.append("%s='%s'" % (command_resolver.resolve_command('MASTER_SSL_KEY'), primary_ssl_key))
if primary_ssl_cipher is not None:
chm.append("MASTER_SSL_CIPHER='%s'" % primary_ssl_cipher)
chm.append("%s='%s'" % (command_resolver.resolve_command('MASTER_SSL_CIPHER'), primary_ssl_cipher))
if primary_ssl_verify_server_cert:
chm.append("SOURCE_SSL_VERIFY_SERVER_CERT=1")
chm.append("%s=1" % command_resolver.resolve_command('MASTER_SSL_VERIFY_SERVER_CERT'))
if primary_auto_position:
chm.append("MASTER_AUTO_POSITION=1")
chm.append("%s=1" % command_resolver.resolve_command('MASTER_AUTO_POSITION'))
if primary_use_gtid is not None:
chm.append("MASTER_USE_GTID=%s" % primary_use_gtid)
chm.append("MASTER_USE_GTID=%s" % primary_use_gtid) # MariaDB only
try:
changeprimary(cursor, chm, connection_name, channel)
changeprimary(cursor, command_resolver, chm, connection_name, channel)
except mysql_driver.Warning as e:
result['warning'] = to_native(e)
except Exception as e:
module.fail_json(msg='%s. Query == CHANGE MASTER TO %s' % (to_native(e), chm))
module.fail_json(msg='%s. Query == %s TO %s' % (to_native(e), command_resolver.resolve_command('CHANGE MASTER'), chm))
result['changed'] = True
module.exit_json(queries=executed_queries, **result)
elif mode == "startreplica":
@ -671,7 +669,7 @@ def main():
else:
module.exit_json(msg="Replica already stopped", changed=False, queries=executed_queries)
elif mode == 'resetprimary':
reset = reset_primary(module, cursor, fail_on_error)
reset = reset_primary(module, cursor, command_resolver, fail_on_error)
if reset is True:
module.exit_json(msg="Primary reset", changed=True, queries=executed_queries)
else:

View file

@ -0,0 +1,49 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: SKS 1.1.6
Comment: Hostname: pgp.mit.edu
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
cAZUlaj3id3TxquAlud4lWDz
=h5nH
-----END PGP PUBLIC KEY BLOCK-----

View file

@ -2,6 +2,38 @@
# We use the ubuntu2204 image provided by ansible-test.
# The GPG key is imported in the files folder from:
# https://dev.mysql.com/doc/refman/8.4/en/checking-gpg-signature.html
# Downloading the key on each iteration of the tests is too slow.
- name: Install MySQL PGP public key
ansible.builtin.copy:
src: files/mysql.gpg
dest: /usr/share/keyrings/mysql.gpg
owner: root
group: root
mode: '0644'
when:
- db_engine == 'mysql'
- db_version is version('8.4', '>=')
- name: Add Apt signing key to keyring
ansible.builtin.apt_key:
id: A8D3785C
file: /usr/share/keyrings/mysql.gpg
state: present
when:
- db_engine == 'mysql'
- db_version is version('8.4', '>=')
- name: Add MySQL 8.4 repository
ansible.builtin.apt_repository:
repo: deb http://repo.mysql.com/apt/ubuntu/ jammy mysql-8.4-lts mysql-tools
state: present
filename: mysql
when:
- db_engine == 'mysql'
- db_version is version('8.4', '>=')
- name: "{{ role_name }} | Requirements | Install Linux packages"
ansible.builtin.package:
name:

View file

@ -111,11 +111,24 @@
check_implicit_admin: no
register: result
- name: Dump and Import | Assert successful completion of dump operation
- name: Dump and Import | Assert successful completion of dump operation for MariaDB and MySQL < 8.2
assert:
that:
- result is changed
- result.executed_commands[0] is search(".department --master-data=1 --skip-triggers")
when:
- >
db_engine == 'mariadb' or
(db_engine == 'mysql' and db_version is version('8.2', '<'))
- name: Dump and Import | Assert successful completion of dump operation for MySQL >= 8.2
assert:
that:
- result is changed
- result.executed_commands[0] is search(".department --source-data=1 --skip-triggers")
when:
- db_engine == 'mysql'
- db_version is version('8.2', '>=')
- name: Dump and Import | State dump/import - file name should exist (db_file_name)
file:

View file

@ -1,3 +1,4 @@
---
####################################################################
# WARNING: These are designed specifically for Ansible tests #
# and should not be used as examples of how to write Ansible roles #
@ -18,8 +19,7 @@
# Tests of channel parameter:
- import_tasks: mysql_replication_channel.yml
when:
- db_engine == 'mysql' # FIXME: mariadb introduces FOR CHANNEL in 10.7
- mysql8022_and_higher == true # FIXME: mysql 5.7 should work, but our tets fails, why?
- db_engine == 'mysql' # FIXME: mariadb introduces FOR CHANNEL in 10.7
# Tests of resetprimary mode:
- import_tasks: mysql_replication_resetprimary_mode.yml
@ -30,3 +30,4 @@
- import_tasks: mysql_replication_changereplication_mode.yml
when:
- db_engine == 'mysql'
- db_version is version('8.0.23', '>=')

View file

@ -32,10 +32,15 @@
channel: '{{ test_channel }}'
register: result
- assert:
- name: Assert that run replication with channel is changed and query matches for MariaDB and MySQL < 8.0.23
ansible.builtin.assert:
that:
- result is changed
- result.queries == result_query
when:
- >
db_engine == 'mariadb' or
(db_engine == 'mysql' and db_version is version('8.0.23', '<'))
vars:
result_query: ["CHANGE MASTER TO MASTER_HOST='{{ mysql_host }}',\
MASTER_USER='{{ replication_user }}',MASTER_PASSWORD='********',\
@ -43,6 +48,21 @@
'{{ mysql_primary_status.File }}',MASTER_LOG_POS=\
{{ mysql_primary_status.Position }} FOR CHANNEL '{{ test_channel }}'"]
- name: Assert that run replication with channel is changed and query matches for MySQL >= 8.0.23
ansible.builtin.assert:
that:
- result is changed
- result.queries == result_query
when:
- db_engine == 'mysql'
- db_version is version('8.0.23', '>=')
vars:
result_query: ["CHANGE REPLICATION SOURCE TO SOURCE_HOST='{{ mysql_host }}',\
SOURCE_USER='{{ replication_user }}',SOURCE_PASSWORD='********',\
SOURCE_PORT={{ mysql_primary_port }},SOURCE_LOG_FILE=\
'{{ mysql_primary_status.File }}',SOURCE_LOG_POS=\
{{ mysql_primary_status.Position }} FOR CHANNEL '{{ test_channel }}'"]
# Test startreplica mode:
- name: Start replica with channel
mysql_replication:
@ -83,7 +103,10 @@
mysql_host_value: '{{ mysql_host }}'
mysql_primary_port_value: '{{ mysql_primary_port }}'
test_channel_value: '{{ test_channel }}'
when: mysql8022_and_higher == false
when:
- >
db_engine == 'mariadb' or
(db_engine == 'mysql' and db_version is version('8.0.22', '<'))
- assert:
that:
@ -99,7 +122,9 @@
mysql_host_value: '{{ mysql_host }}'
mysql_primary_port_value: '{{ mysql_primary_port }}'
test_channel_value: '{{ test_channel }}'
when: mysql8022_and_higher == true
when:
- db_engine == 'mysql'
- db_version is version('8.0.22', '>=')
# Test stopreplica mode:

View file

@ -9,16 +9,6 @@
login_host: '{{ mysql_host }}'
block:
- name: Set mysql8022_and_higher
set_fact:
mysql8022_and_higher: false
- name: Set mysql8022_and_higher
set_fact:
mysql8022_and_higher: true
when:
- db_engine == 'mysql'
- db_version is version('8.0.22', '>=')
# We use iF NOT EXISTS because the GITHUB Action:
# "ansible-community/ansible-test-gh-action" uses "--retry-on-error".
@ -136,11 +126,10 @@
that:
- result is not failed
# Test changeprimary mode:
# primary_ssl_ca will be set as '' to check the module's behaviour for #23976,
# must be converted to an empty string
- name: Run replication
mysql_replication:
- name: Test changeprimary mode with empty primary_ssl_ca
community.mysql.mysql_replication:
<<: *mysql_params
login_port: '{{ mysql_replica1_port }}'
mode: changeprimary
@ -151,14 +140,18 @@
primary_log_file: '{{ mysql_primary_status.File }}'
primary_log_pos: '{{ mysql_primary_status.Position }}'
primary_ssl_ca: ''
primary_ssl: no
primary_ssl: false
register: result
- name: Assert that changeprimmary is changed and return expected query
assert:
- name: Assert that changeprimmary is changed and return expected query for MariaDB and MySQL < 8.0.23
ansible.builtin.assert:
that:
- result is changed
- result.queries == expected_queries
when:
- >
db_engine == 'mariadb' or
(db_engine == 'mysql' and db_version is version('8.0.23', '<'))
vars:
expected_queries: ["CHANGE MASTER TO MASTER_HOST='{{ mysql_host }}',\
MASTER_USER='{{ replication_user }}',MASTER_PASSWORD='********',\
@ -166,6 +159,22 @@
'{{ mysql_primary_status.File }}',MASTER_LOG_POS=\
{{ mysql_primary_status.Position }},MASTER_SSL=0,MASTER_SSL_CA=''"]
- name: Assert that changeprimmary is changed and return expected query for MySQL > 8.0.23
ansible.builtin.assert:
that:
- result is changed
- result.queries == expected_queries
when:
- db_engine == 'mysql'
- db_version is version('8.0.23', '>=')
vars:
expected_queries: ["CHANGE REPLICATION SOURCE TO \
SOURCE_HOST='{{ mysql_host }}',\
SOURCE_USER='{{ replication_user }}',SOURCE_PASSWORD='********',\
SOURCE_PORT={{ mysql_primary_port }},SOURCE_LOG_FILE=\
'{{ mysql_primary_status.File }}',SOURCE_LOG_POS=\
{{ mysql_primary_status.Position }},SOURCE_SSL=0,SOURCE_SSL_CA=''"]
# Test startreplica mode:
- name: Start replica
mysql_replication:
@ -201,7 +210,10 @@
vars:
mysql_host_value: "{{ mysql_host }}"
mysql_primary_port_value: "{{ mysql_primary_port }}"
when: mysql8022_and_higher is falsy(convert_bool=True)
when:
- >
db_engine == 'mariadb' or
(db_engine == 'mysql' and db_version is version('8.0.22', '<'))
- name: Assert that getreplica returns expected values for MySQL newer than 8.0.22
assert:
@ -216,7 +228,9 @@
vars:
mysql_host_value: "{{ mysql_host }}"
mysql_primary_port_value: "{{ mysql_primary_port }}"
when: mysql8022_and_higher is truthy(convert_bool=True)
when:
- db_engine == 'mysql'
- db_version is version('8.0.22', '>=')
# Create test table and add data to it:
- name: Create test table
@ -243,13 +257,18 @@
assert:
that:
- replica_status.Exec_Master_Log_Pos != mysql_primary_status.Position
when: mysql8022_and_higher == false
when:
- >
db_engine == 'mariadb' or
(db_engine == 'mysql' and db_version is version('8.0.22', '<'))
- name: Assert that getreplica Log_Pos is different for MySQL newer than 8.0.22
assert:
that:
- replica_status.Exec_Source_Log_Pos != mysql_primary_status.Position
when: mysql8022_and_higher == true
when:
- db_engine == 'mysql'
- db_version is version('8.0.22', '>=')
- name: Start replica that is already running
mysql_replication:

View file

@ -18,10 +18,24 @@
primary_delay: '{{ test_primary_delay }}'
register: result
- assert:
- name: Assert that run replication is changed and query match expectation for MariaDB and MySQL < 8.0.23
ansible.builtin.assert:
that:
- result is changed
- result.queries == ["CHANGE MASTER TO MASTER_DELAY=60"]
- result is changed
- result.queries == ["CHANGE MASTER TO MASTER_DELAY=60"]
when:
- >
db_engine == 'mariadb' or
(db_engine == 'mysql' and db_version is version('8.0.23', '<'))
- name: Assert that run replication is changed and query match expectation for MySQL >= 8.0.23
ansible.builtin.assert:
that:
- result is changed
- result.queries == ["CHANGE REPLICATION SOURCE TO SOURCE_DELAY=60"]
when:
- db_engine == 'mysql'
- db_version is version('8.0.23', '>=')
# Auxiliary step:
- name: Start replica

View file

@ -1,3 +1,4 @@
---
# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
@ -38,10 +39,24 @@
mode: resetprimary
register: result
- assert:
- name: Assert that reset primary is changed and query matches for MariaDB and MySQL < 8.4
ansible.builtin.assert:
that:
- result is changed
- result.queries == ["RESET MASTER"]
- result is changed
- result.queries == ["RESET MASTER"]
when:
- >
db_engine == 'mariadb' or
(db_engine == 'mysql' and db_version is version('8.4.0', '<'))
- name: Assert that reset primary is changed and query matches for MySQL > 8.4
ansible.builtin.assert:
that:
- result is changed
- result.queries == ["RESET BINARY LOGS AND GTIDS"]
when:
- db_engine == 'mysql'
- db_version is version('8.4.0', '>=')
# Get primary final status:
- name: Get primary status

View file

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import pytest
from ansible_collections.community.mysql.plugins.module_utils.command_resolver import (
CommandResolver,
)
@pytest.mark.parametrize(
'server_implementation,server_version,command,expected_output,expected_exception,expected_message',
[
('mysql', '1.0.0', 'SHOW NOTHING', '', ValueError, 'Unsupported command: SHOW NOTHING'),
('mysql', '8.0.20', 'SHOW MASTER STATUS', 'SHOW MASTER STATUS', None, None), # Case insensitive
('mysql', '8.0.20', 'show master status', 'SHOW MASTER STATUS', None, None), # Case insensitive
('mysql', '8.0.20', 'SHOW master STATUS', 'SHOW MASTER STATUS', None, None), # Case insensitive
('mysql', '8.2.0', 'SHOW MASTER STATUS', 'SHOW BINARY LOG STATUS', None, None),
('mysql', '9.0.0', 'SHOW MASTER STATUS', 'SHOW BINARY LOG STATUS', None, None),
('mariadb', '10.4.23', 'SHOW MASTER STATUS', 'SHOW MASTER STATUS', None, None), # Default
('mariadb', '10.5.1', 'SHOW MASTER STATUS', 'SHOW MASTER STATUS', None, None), # Default
('mariadb', '10.5.2', 'SHOW MASTER STATUS', 'SHOW BINLOG STATUS', None, None),
('mariadb', '10.6.17', 'SHOW MASTER STATUS', 'SHOW BINLOG STATUS', None, None),
('mysql', '8.4.1', 'CHANGE MASTER', 'CHANGE REPLICATION SOURCE', None, None),
]
)
def test_resolve_command(server_implementation, server_version, command, expected_output, expected_exception, expected_message):
"""
Tests that the CommandResolver method resolve_command return the correct query.
"""
resolver = CommandResolver(server_implementation, server_version)
if expected_exception:
with pytest.raises(expected_exception) as excinfo:
resolver.resolve_command(command)
assert str(excinfo.value) == expected_message
else:
assert resolver.resolve_command(command) == expected_output

View file

@ -14,15 +14,15 @@ from ansible_collections.community.mysql.plugins.modules.mysql_info import MySQL
@pytest.mark.parametrize(
'suffix,cursor_output,server_implementation,user_implementation',
'suffix,cursor_output,server_implementation,server_version,user_implementation',
[
('mysql', '5.5.1-mysql', 'mysql', 'mysql'),
('log', '5.7.31-log', 'mysql', 'mysql'),
('mariadb', '10.5.0-mariadb', 'mariadb', 'mariadb'),
('', '8.0.22', 'mysql', 'mysql'),
('mysql', '5.5.1-mysql', 'mysql', '5.5.1', 'mysql'),
('log', '5.7.31-log', 'mysql', '5.7.31', 'mysql'),
('mariadb', '10.5.0-mariadb', 'mariadb', '10.5.0', 'mariadb'),
('', '8.0.22', 'mysql', '8.0.22', 'mysql'),
]
)
def test_get_info_suffix(suffix, cursor_output, server_implementation, user_implementation):
def test_get_info_suffix(suffix, cursor_output, server_implementation, server_version, user_implementation):
def __cursor_return_value(input_parameter):
if input_parameter == "SHOW GLOBAL VARIABLES":
cursor.fetchall.return_value = [{"Variable_name": "version", "Value": cursor_output}]
@ -32,6 +32,6 @@ def test_get_info_suffix(suffix, cursor_output, server_implementation, user_impl
cursor = MagicMock()
cursor.execute.side_effect = __cursor_return_value
info = MySQL_Info(MagicMock(), cursor, server_implementation, user_implementation)
info = MySQL_Info(MagicMock(), cursor, server_implementation, server_version, user_implementation)
assert info.get_info([], [], False)['version']['suffix'] == suffix