summaryrefslogtreecommitdiffstats
path: root/ansible_collections/community/mysql/plugins/modules
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 12:04:41 +0000
commit975f66f2eebe9dadba04f275774d4ab83f74cf25 (patch)
tree89bd26a93aaae6a25749145b7e4bca4a1e75b2be /ansible_collections/community/mysql/plugins/modules
parentInitial commit. (diff)
downloadansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.tar.xz
ansible-975f66f2eebe9dadba04f275774d4ab83f74cf25.zip
Adding upstream version 7.7.0+dfsg.upstream/7.7.0+dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/community/mysql/plugins/modules')
-rw-r--r--ansible_collections/community/mysql/plugins/modules/mysql_db.py763
-rw-r--r--ansible_collections/community/mysql/plugins/modules/mysql_info.py605
-rw-r--r--ansible_collections/community/mysql/plugins/modules/mysql_query.py284
-rw-r--r--ansible_collections/community/mysql/plugins/modules/mysql_replication.py654
-rw-r--r--ansible_collections/community/mysql/plugins/modules/mysql_role.py1095
-rw-r--r--ansible_collections/community/mysql/plugins/modules/mysql_user.py528
-rw-r--r--ansible_collections/community/mysql/plugins/modules/mysql_variables.py271
7 files changed, 4200 insertions, 0 deletions
diff --git a/ansible_collections/community/mysql/plugins/modules/mysql_db.py b/ansible_collections/community/mysql/plugins/modules/mysql_db.py
new file mode 100644
index 000000000..5a8fe3e3e
--- /dev/null
+++ b/ansible_collections/community/mysql/plugins/modules/mysql_db.py
@@ -0,0 +1,763 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2012, Mark Theunissen <mark.theunissen@gmail.com>
+# Sponsored by Four Kitchens http://fourkitchens.com.
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = r'''
+---
+module: mysql_db
+short_description: Add or remove MySQL databases from a remote host
+description:
+- Add or remove MySQL databases from a remote host.
+options:
+ name:
+ description:
+ - Name of the database to add or remove.
+ - I(name=all) may only be provided if I(state) is C(dump) or C(import).
+ - List of databases is provided with I(state=dump), I(state=present) and I(state=absent).
+ - If I(name=all) it works like --all-databases option for mysqldump (Added in 2.0).
+ required: true
+ type: list
+ elements: str
+ aliases: [db]
+ state:
+ description:
+ - The database state.
+ type: str
+ default: present
+ choices: ['absent', 'dump', 'import', 'present']
+ collation:
+ description:
+ - Collation mode (sorting). This only applies to new table/databases and
+ does not update existing ones, this is a limitation of MySQL.
+ type: str
+ default: ''
+ encoding:
+ description:
+ - Encoding mode to use, examples include C(utf8) or C(latin1_swedish_ci),
+ at creation of database, dump or importation of sql script.
+ type: str
+ default: ''
+ target:
+ description:
+ - Location, on the remote host, of the dump file to read from or write to.
+ - Uncompressed SQL files (C(.sql)) as well as bzip2 (C(.bz2)), gzip (C(.gz)) and
+ xz (Added in 2.0) compressed files are supported.
+ type: path
+ single_transaction:
+ description:
+ - Execute the dump in a single transaction.
+ type: bool
+ default: false
+ quick:
+ description:
+ - Option used for dumping large tables.
+ type: bool
+ default: true
+ ignore_tables:
+ description:
+ - A list of table names that will be ignored in the dump
+ of the form database_name.table_name.
+ type: list
+ elements: str
+ default: []
+ hex_blob:
+ description:
+ - Dump binary columns using hexadecimal notation.
+ type: bool
+ default: false
+ version_added: '0.1.0'
+ force:
+ description:
+ - Continue dump or import even if we get an SQL error.
+ - Used only when I(state) is C(dump) or C(import).
+ type: bool
+ default: false
+ version_added: '0.1.0'
+ master_data:
+ description:
+ - Option to dump a master replication server to produce a dump file
+ that can be used to set up another server as a slave of the master.
+ - C(0) to not include master data.
+ - C(1) to generate a 'CHANGE MASTER TO' statement
+ required on the slave to start the replication process.
+ - C(2) to generate a commented 'CHANGE MASTER TO'.
+ - Can be used when I(state=dump).
+ type: int
+ choices: [0, 1, 2]
+ default: 0
+ version_added: '0.1.0'
+ skip_lock_tables:
+ description:
+ - Skip locking tables for read. Used when I(state=dump), ignored otherwise.
+ type: bool
+ default: false
+ version_added: '0.1.0'
+ dump_extra_args:
+ description:
+ - Provide additional arguments for mysqldump.
+ Used when I(state=dump) only, ignored otherwise.
+ type: str
+ version_added: '0.1.0'
+ use_shell:
+ description:
+ - Used to prevent C(Broken pipe) errors when the imported I(target) file is compressed.
+ - If C(yes), the module will internally execute commands via a shell.
+ - Used when I(state=import), ignored otherwise.
+ type: bool
+ default: false
+ version_added: '0.1.0'
+ unsafe_login_password:
+ description:
+ - If C(no), the module will safely use a shell-escaped
+ version of the I(login_password) value.
+ - It makes sense to use C(yes) only if there are special
+ symbols in the value and errors C(Access denied) occur.
+ - Used only when I(state) is C(import) or C(dump) and
+ I(login_password) is passed, ignored otherwise.
+ type: bool
+ default: false
+ version_added: '0.1.0'
+ restrict_config_file:
+ description:
+ - Read only passed I(config_file).
+ - When I(state) is C(dump) or C(import),
+ by default the module passes I(config_file) parameter
+ using C(--defaults-extra-file) command-line argument to C(mysql/mysqldump) utilities
+ under the hood that read named option file in addition to usual option files.
+ - If this behavior is undesirable, use C(yes) to read only named option file.
+ type: bool
+ default: false
+ version_added: '0.1.0'
+ check_implicit_admin:
+ description:
+ - Check if mysql allows login as root/nopassword before trying supplied credentials.
+ - If success, passed I(login_user)/I(login_password) will be ignored.
+ type: bool
+ default: false
+ version_added: '0.1.0'
+ config_overrides_defaults:
+ description:
+ - If C(yes), connection parameters from I(config_file) will override the default
+ values of I(login_host) and I(login_port) parameters.
+ - Used when I(stat) is C(present) or C(absent), ignored otherwise.
+ - It needs Python 3.5+ as the default interpreter on a target host.
+ type: bool
+ default: false
+ version_added: '0.1.0'
+ chdir:
+ description:
+ - Changes the current working directory.
+ - Can be useful, for example, when I(state=import) and a dump file contains relative paths.
+ type: path
+ version_added: '3.4.0'
+ pipefail:
+ description:
+ - Use C(bash) instead of C(sh) and add C(-o pipefail) to catch errors from the
+ mysql_dump command when I(state=import) and compression is used.
+ - The default is C(no) to prevent issues on systems without bash as a default interpreter.
+ - The default will change to C(yes) in community.mysql 4.0.0.
+ type: bool
+ default: false
+ version_added: '3.4.0'
+
+seealso:
+- module: community.mysql.mysql_info
+- module: community.mysql.mysql_variables
+- module: community.mysql.mysql_user
+- module: community.mysql.mysql_replication
+- name: MySQL command-line client reference
+ description: Complete reference of the MySQL command-line client documentation.
+ link: https://dev.mysql.com/doc/refman/8.0/en/mysql.html
+- name: mysqldump reference
+ description: Complete reference of the ``mysqldump`` client utility documentation.
+ link: https://dev.mysql.com/doc/refman/8.0/en/mysqldump.html
+- name: CREATE DATABASE reference
+ description: Complete reference of the CREATE DATABASE command documentation.
+ link: https://dev.mysql.com/doc/refman/8.0/en/create-database.html
+- name: DROP DATABASE reference
+ description: Complete reference of the DROP DATABASE command documentation.
+ link: https://dev.mysql.com/doc/refman/8.0/en/drop-database.html
+author: "Ansible Core Team"
+requirements:
+ - mysql (command line binary)
+ - mysqldump (command line binary)
+notes:
+ - Supports C(check_mode).
+ - Requires the mysql and mysqldump binaries on the remote host.
+ - This module is B(not idempotent) when I(state) is C(import),
+ and will import the dump file each time if run more than once.
+extends_documentation_fragment:
+- community.mysql.mysql
+
+'''
+
+EXAMPLES = r'''
+# If you encounter the "Please explicitly state intended protocol" error,
+# use the login_unix_socket argument
+- name: Create a new database with name 'bobdata'
+ community.mysql.mysql_db:
+ name: bobdata
+ state: present
+ login_unix_socket: /run/mysqld/mysqld.sock
+
+- name: Create new databases with names 'foo' and 'bar'
+ community.mysql.mysql_db:
+ name:
+ - foo
+ - bar
+ state: present
+
+# Copy database dump file to remote host and restore it to database 'my_db'
+- name: Copy database dump file
+ copy:
+ src: dump.sql.bz2
+ dest: /tmp
+
+- name: Restore database
+ community.mysql.mysql_db:
+ name: my_db
+ state: import
+ target: /tmp/dump.sql.bz2
+
+- name: Restore database ignoring errors
+ community.mysql.mysql_db:
+ name: my_db
+ state: import
+ target: /tmp/dump.sql.bz2
+ force: true
+
+- name: Dump multiple databases
+ community.mysql.mysql_db:
+ state: dump
+ name: db_1,db_2
+ target: /tmp/dump.sql
+
+- name: Dump multiple databases
+ community.mysql.mysql_db:
+ state: dump
+ name:
+ - db_1
+ - db_2
+ target: /tmp/dump.sql
+
+- name: Dump all databases to hostname.sql
+ community.mysql.mysql_db:
+ state: dump
+ name: all
+ target: /tmp/dump.sql
+
+- name: Dump all databases to hostname.sql including master data
+ community.mysql.mysql_db:
+ state: dump
+ name: all
+ target: /tmp/dump.sql
+ master_data: 1
+
+# Import of sql script with encoding option
+- name: >
+ Import dump.sql with specific latin1 encoding,
+ similar to mysql -u <username> --default-character-set=latin1 -p <password> < dump.sql
+ community.mysql.mysql_db:
+ state: import
+ name: all
+ encoding: latin1
+ target: /tmp/dump.sql
+
+# Dump of database with encoding option
+- name: >
+ Dump of Databse with specific latin1 encoding,
+ similar to mysqldump -u <username> --default-character-set=latin1 -p <password> <database>
+ community.mysql.mysql_db:
+ state: dump
+ name: db_1
+ encoding: latin1
+ target: /tmp/dump.sql
+
+- name: Delete database with name 'bobdata'
+ community.mysql.mysql_db:
+ name: bobdata
+ state: absent
+
+- name: Make sure there is neither a database with name 'foo', nor one with name 'bar'
+ community.mysql.mysql_db:
+ name:
+ - foo
+ - bar
+ state: absent
+
+# Dump database with argument not directly supported by this module
+# using dump_extra_args parameter
+- name: Dump databases without including triggers
+ community.mysql.mysql_db:
+ state: dump
+ name: foo
+ target: /tmp/dump.sql
+ dump_extra_args: --skip-triggers
+
+- name: Try to create database as root/nopassword first. If not allowed, pass the credentials
+ community.mysql.mysql_db:
+ check_implicit_admin: true
+ login_user: bob
+ login_password: 123456
+ name: bobdata
+ state: present
+
+- name: Dump a database with compression and catch errors from mysqldump with bash pipefail
+ community.mysql.mysql_db:
+ state: dump
+ name: foo
+ target: /tmp/dump.sql.gz
+ pipefail: true
+'''
+
+RETURN = r'''
+db:
+ description: Database names in string format delimited by white space.
+ returned: always
+ type: str
+ sample: "foo bar"
+db_list:
+ description: List of database names.
+ returned: always
+ type: list
+ sample: ["foo", "bar"]
+executed_commands:
+ description: List of commands which tried to run.
+ returned: if executed
+ type: list
+ sample: ["CREATE DATABASE acme"]
+ version_added: '0.1.0'
+'''
+
+import os
+import subprocess
+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.module_utils.six.moves import shlex_quote
+from ansible.module_utils._text import to_native
+
+executed_commands = []
+
+# ===========================================
+# MySQL module specific support methods.
+#
+
+
+def db_exists(cursor, db):
+ res = 0
+ for each_db in db:
+ res += cursor.execute("SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = %s", (each_db,))
+ return res == len(db)
+
+
+def db_delete(cursor, db):
+ if not db:
+ return False
+ for each_db in db:
+ query = "DROP DATABASE %s" % mysql_quote_identifier(each_db, 'database')
+ executed_commands.append(query)
+ cursor.execute(query)
+ return True
+
+
+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,
+ 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,
+ check_implicit_admin=False, pipefail=False):
+ cmd = module.get_bin_path('mysqldump', True)
+ # If defined, mysqldump demands --defaults-extra-file be the first option
+ if config_file:
+ if restrict_config_file:
+ cmd += " --defaults-file=%s" % shlex_quote(config_file)
+ else:
+ cmd += " --defaults-extra-file=%s" % shlex_quote(config_file)
+
+ if check_implicit_admin:
+ cmd += " --user=root --password=''"
+ else:
+ if user is not None:
+ cmd += " --user=%s" % shlex_quote(user)
+
+ if password is not None:
+ if not unsafe_password:
+ cmd += " --password=%s" % shlex_quote(password)
+ else:
+ cmd += " --password=%s" % password
+
+ if ssl_cert is not None:
+ cmd += " --ssl-cert=%s" % shlex_quote(ssl_cert)
+ if ssl_key is not None:
+ cmd += " --ssl-key=%s" % shlex_quote(ssl_key)
+ if ssl_ca is not None:
+ cmd += " --ssl-ca=%s" % shlex_quote(ssl_ca)
+ if force:
+ cmd += " --force"
+ if socket is not None:
+ cmd += " --socket=%s" % shlex_quote(socket)
+ else:
+ cmd += " --host=%s --port=%i" % (shlex_quote(host), port)
+
+ if all_databases:
+ cmd += " --all-databases"
+ elif len(db_name) > 1:
+ cmd += " --databases {0}".format(' '.join(db_name))
+ else:
+ cmd += " %s" % shlex_quote(' '.join(db_name))
+
+ if skip_lock_tables:
+ cmd += " --skip-lock-tables"
+ if (encoding is not None) and (encoding != ""):
+ cmd += " --default-character-set=%s" % shlex_quote(encoding)
+ if single_transaction:
+ cmd += " --single-transaction=true"
+ if quick:
+ cmd += " --quick"
+ if ignore_tables:
+ for an_ignored_table in ignore_tables:
+ cmd += " --ignore-table={0}".format(an_ignored_table)
+ if hex_blob:
+ cmd += " --hex-blob"
+ if master_data:
+ cmd += " --master-data=%s" % master_data
+ if dump_extra_args is not None:
+ cmd += " " + dump_extra_args
+
+ path = None
+ if os.path.splitext(target)[-1] == '.gz':
+ path = module.get_bin_path('gzip', True)
+ elif os.path.splitext(target)[-1] == '.bz2':
+ path = module.get_bin_path('bzip2', True)
+ elif os.path.splitext(target)[-1] == '.xz':
+ path = module.get_bin_path('xz', True)
+
+ if path:
+ cmd = '%s | %s > %s' % (cmd, path, shlex_quote(target))
+ if pipefail:
+ cmd = 'set -o pipefail && ' + cmd
+ else:
+ cmd += " > %s" % shlex_quote(target)
+
+ executed_commands.append(cmd)
+
+ if pipefail:
+ rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True, executable='bash')
+ else:
+ rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True)
+
+ return rc, stdout, stderr
+
+
+def db_import(module, host, user, password, db_name, target, all_databases, port, config_file,
+ socket=None, ssl_cert=None, ssl_key=None, ssl_ca=None, encoding=None, force=False,
+ use_shell=False, unsafe_password=False, restrict_config_file=False,
+ check_implicit_admin=False):
+ if not os.path.exists(target):
+ return module.fail_json(msg="target %s does not exist on the host" % target)
+
+ cmd = [module.get_bin_path('mysql', True)]
+ # --defaults-file must go first, or errors out
+ if config_file:
+ if restrict_config_file:
+ cmd.append("--defaults-file=%s" % shlex_quote(config_file))
+ else:
+ cmd.append("--defaults-extra-file=%s" % shlex_quote(config_file))
+
+ if check_implicit_admin:
+ cmd.append("--user=root --password=''")
+ else:
+ if user:
+ cmd.append("--user=%s" % shlex_quote(user))
+
+ if password:
+ if not unsafe_password:
+ cmd.append("--password=%s" % shlex_quote(password))
+ else:
+ cmd.append("--password=%s" % password)
+
+ if ssl_cert is not None:
+ cmd.append("--ssl-cert=%s" % shlex_quote(ssl_cert))
+ if ssl_key is not None:
+ cmd.append("--ssl-key=%s" % shlex_quote(ssl_key))
+ if ssl_ca is not None:
+ cmd.append("--ssl-ca=%s" % shlex_quote(ssl_ca))
+ if force:
+ cmd.append("-f")
+ if socket is not None:
+ cmd.append("--socket=%s" % shlex_quote(socket))
+ else:
+ cmd.append("--host=%s" % shlex_quote(host))
+ cmd.append("--port=%i" % port)
+ if (encoding is not None) and (encoding != ""):
+ cmd.append("--default-character-set=%s" % shlex_quote(encoding))
+ if not all_databases:
+ cmd.append("--one-database")
+ cmd.append(shlex_quote(''.join(db_name)))
+
+ comp_prog_path = None
+ if os.path.splitext(target)[-1] == '.gz':
+ comp_prog_path = module.get_bin_path('gzip', required=True)
+ elif os.path.splitext(target)[-1] == '.bz2':
+ comp_prog_path = module.get_bin_path('bzip2', required=True)
+ elif os.path.splitext(target)[-1] == '.xz':
+ comp_prog_path = module.get_bin_path('xz', required=True)
+ if comp_prog_path:
+ # The line below is for returned data only:
+ executed_commands.append('%s -dc %s | %s' % (comp_prog_path, target, cmd))
+
+ if not use_shell:
+ p1 = subprocess.Popen([comp_prog_path, '-dc', target], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ p2 = subprocess.Popen(cmd, stdin=p1.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ (stdout2, stderr2) = p2.communicate()
+ p1.stdout.close()
+ p1.wait()
+
+ if p1.returncode != 0:
+ stderr1 = p1.stderr.read()
+ return p1.returncode, '', stderr1
+ else:
+ return p2.returncode, stdout2, stderr2
+ else:
+ # Used to prevent 'Broken pipe' errors that
+ # occasionaly occur when target files are compressed.
+ # FYI: passing the `shell=True` argument to p2 = subprocess.Popen()
+ # doesn't solve the problem.
+ cmd = " ".join(cmd)
+ cmd = "%s -dc %s | %s" % (comp_prog_path, shlex_quote(target), cmd)
+ rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True)
+ return rc, stdout, stderr
+
+ else:
+ cmd = ' '.join(cmd)
+ cmd += " < %s" % shlex_quote(target)
+ executed_commands.append(cmd)
+ rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True)
+ return rc, stdout, stderr
+
+
+def db_create(cursor, db, encoding, collation):
+ if not db:
+ return False
+ query_params = dict(enc=encoding, collate=collation)
+ res = 0
+ for each_db in db:
+ # Escape '%' since mysql cursor.execute() uses a format string
+ query = ['CREATE DATABASE %s' % mysql_quote_identifier(each_db, 'database').replace('%', '%%')]
+ if encoding:
+ query.append("CHARACTER SET %(enc)s")
+ if collation:
+ query.append("COLLATE %(collate)s")
+ query = ' '.join(query)
+ res += cursor.execute(query, query_params)
+ try:
+ executed_commands.append(cursor.mogrify(query, query_params))
+ except AttributeError:
+ executed_commands.append(cursor._executed)
+ except Exception:
+ executed_commands.append(query)
+ return res > 0
+
+
+# ===========================================
+# Module execution.
+#
+
+
+def main():
+ argument_spec = mysql_common_argument_spec()
+ argument_spec.update(
+ name=dict(type='list', required=True, aliases=['db']),
+ encoding=dict(type='str', default=''),
+ collation=dict(type='str', default=''),
+ target=dict(type='path'),
+ state=dict(type='str', default='present', choices=['absent', 'dump', 'import', 'present']),
+ single_transaction=dict(type='bool', default=False),
+ quick=dict(type='bool', default=True),
+ ignore_tables=dict(type='list', default=[]),
+ hex_blob=dict(default=False, type='bool'),
+ force=dict(type='bool', default=False),
+ master_data=dict(type='int', default=0, choices=[0, 1, 2]),
+ skip_lock_tables=dict(type='bool', default=False),
+ dump_extra_args=dict(type='str'),
+ use_shell=dict(type='bool', default=False),
+ unsafe_login_password=dict(type='bool', default=False, no_log=True),
+ restrict_config_file=dict(type='bool', default=False),
+ check_implicit_admin=dict(type='bool', default=False),
+ config_overrides_defaults=dict(type='bool', default=False),
+ chdir=dict(type='path'),
+ pipefail=dict(type='bool', default=False),
+ )
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ if mysql_driver is None:
+ module.fail_json(msg=mysql_driver_fail_msg)
+
+ db = module.params["name"]
+ if not db:
+ module.exit_json(changed=False, db=db, db_list=[])
+ db = [each_db.strip() for each_db in db]
+
+ encoding = module.params["encoding"]
+ collation = module.params["collation"]
+ state = module.params["state"]
+ target = module.params["target"]
+ socket = module.params["login_unix_socket"]
+ login_port = module.params["login_port"]
+ if login_port < 0 or login_port > 65535:
+ module.fail_json(msg="login_port must be a valid unix port number (0-65535)")
+ ssl_cert = module.params["client_cert"]
+ ssl_key = module.params["client_key"]
+ ssl_ca = module.params["ca_cert"]
+ check_hostname = module.params["check_hostname"]
+ connect_timeout = module.params['connect_timeout']
+ config_file = module.params['config_file']
+ login_password = module.params["login_password"]
+ unsafe_login_password = module.params["unsafe_login_password"]
+ login_user = module.params["login_user"]
+ login_host = module.params["login_host"]
+ ignore_tables = module.params["ignore_tables"]
+ for a_table in ignore_tables:
+ if a_table == "":
+ module.fail_json(msg="Name of ignored table cannot be empty")
+ single_transaction = module.params["single_transaction"]
+ quick = module.params["quick"]
+ hex_blob = module.params["hex_blob"]
+ force = module.params["force"]
+ master_data = module.params["master_data"]
+ skip_lock_tables = module.params["skip_lock_tables"]
+ dump_extra_args = module.params["dump_extra_args"]
+ use_shell = module.params["use_shell"]
+ restrict_config_file = module.params["restrict_config_file"]
+ check_implicit_admin = module.params['check_implicit_admin']
+ config_overrides_defaults = module.params['config_overrides_defaults']
+ chdir = module.params['chdir']
+ pipefail = module.params['pipefail']
+
+ if chdir:
+ try:
+ os.chdir(chdir)
+ except Exception as e:
+ module.fail_json("Cannot change the current directory to %s: %s" % (chdir, e))
+
+ if len(db) > 1 and state == 'import':
+ module.fail_json(msg="Multiple databases are not supported with state=import")
+ db_name = ' '.join(db)
+
+ all_databases = False
+ if state in ['dump', 'import']:
+ if target is None:
+ module.fail_json(msg="with state=%s target is required" % state)
+ if db == ['all']:
+ all_databases = True
+ else:
+ if db == ['all']:
+ module.fail_json(msg="name is not allowed to equal 'all' unless state equals import, or dump.")
+ try:
+ cursor = None
+ if check_implicit_admin:
+ try:
+ cursor, db_conn = mysql_connect(module, 'root', '', config_file, ssl_cert, ssl_key, ssl_ca,
+ connect_timeout=connect_timeout, check_hostname=check_hostname,
+ config_overrides_defaults=config_overrides_defaults)
+ except Exception as e:
+ check_implicit_admin = False
+ pass
+
+ if not cursor:
+ cursor, db_conn = mysql_connect(module, login_user, login_password, config_file, ssl_cert, ssl_key, ssl_ca,
+ connect_timeout=connect_timeout, config_overrides_defaults=config_overrides_defaults,
+ check_hostname=check_hostname)
+ except Exception as e:
+ if os.path.exists(config_file):
+ module.fail_json(msg="unable to connect to database, check login_user and login_password are correct or %s has the credentials. "
+ "Exception message: %s" % (config_file, to_native(e)))
+ else:
+ module.fail_json(msg="unable to find %s. Exception message: %s" % (config_file, to_native(e)))
+
+ changed = False
+ if not os.path.exists(config_file):
+ config_file = None
+
+ existence_list = []
+ non_existence_list = []
+
+ if not all_databases:
+ for each_database in db:
+ if db_exists(cursor, [each_database]):
+ existence_list.append(each_database)
+ else:
+ non_existence_list.append(each_database)
+
+ if state == "absent":
+ if module.check_mode:
+ module.exit_json(changed=bool(existence_list), db=db_name, db_list=db)
+ try:
+ changed = db_delete(cursor, existence_list)
+ except Exception as e:
+ module.fail_json(msg="error deleting database: %s" % to_native(e))
+ module.exit_json(changed=changed, db=db_name, db_list=db, executed_commands=executed_commands)
+ elif state == "present":
+ if module.check_mode:
+ module.exit_json(changed=bool(non_existence_list), db=db_name, db_list=db)
+ changed = False
+ if non_existence_list:
+ try:
+ changed = db_create(cursor, non_existence_list, encoding, collation)
+ except Exception as e:
+ module.fail_json(msg="error creating database: %s" % to_native(e),
+ exception=traceback.format_exc())
+ module.exit_json(changed=changed, db=db_name, db_list=db, executed_commands=executed_commands)
+ elif state == "dump":
+ if non_existence_list and not all_databases:
+ module.fail_json(msg="Cannot dump database(s) %r - not found" % (', '.join(non_existence_list)))
+ if module.check_mode:
+ 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,
+ 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,
+ check_implicit_admin, pipefail)
+ if rc != 0:
+ module.fail_json(msg="%s" % stderr)
+ module.exit_json(changed=True, db=db_name, db_list=db, msg=stdout,
+ executed_commands=executed_commands)
+ elif state == "import":
+ if module.check_mode:
+ module.exit_json(changed=True, db=db_name, db_list=db)
+ if non_existence_list and not all_databases:
+ try:
+ db_create(cursor, non_existence_list, encoding, collation)
+ except Exception as e:
+ module.fail_json(msg="error creating database: %s" % to_native(e),
+ exception=traceback.format_exc())
+ rc, stdout, stderr = db_import(module, login_host, login_user,
+ login_password, db, target,
+ all_databases,
+ login_port, config_file,
+ socket, ssl_cert, ssl_key, ssl_ca,
+ encoding, force, use_shell, unsafe_login_password,
+ restrict_config_file, check_implicit_admin)
+ if rc != 0:
+ module.fail_json(msg="%s" % stderr)
+ module.exit_json(changed=True, db=db_name, db_list=db, msg=stdout,
+ executed_commands=executed_commands)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/mysql/plugins/modules/mysql_info.py b/ansible_collections/community/mysql/plugins/modules/mysql_info.py
new file mode 100644
index 000000000..11b1a8003
--- /dev/null
+++ b/ansible_collections/community/mysql/plugins/modules/mysql_info.py
@@ -0,0 +1,605 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# 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)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = r'''
+---
+module: mysql_info
+short_description: Gather information about MySQL servers
+description:
+- Gathers information about MySQL servers.
+
+options:
+ filter:
+ description:
+ - Limit the collected information by comma separated string or YAML list.
+ - Allowable values are C(version), C(databases), C(settings), C(global_status),
+ C(users), C(engines), C(master_status), C(slave_status), C(slave_hosts).
+ - By default, collects all subsets.
+ - You can use '!' before value (for example, C(!settings)) to exclude it from the information.
+ - If you pass including and excluding values to the filter, for example, I(filter=!settings,version),
+ the excluding values, C(!settings) in this case, will be ignored.
+ type: list
+ elements: str
+ login_db:
+ description:
+ - Database name to connect to.
+ - It makes sense if I(login_user) is allowed to connect to a specific database only.
+ type: str
+ exclude_fields:
+ description:
+ - List of fields which are not needed to collect.
+ - "Supports elements: C(db_size). Unsupported elements will be ignored."
+ type: list
+ elements: str
+ version_added: '0.1.0'
+ return_empty_dbs:
+ description:
+ - Includes names of empty databases to returned dictionary.
+ type: bool
+ default: false
+
+notes:
+- Calculating the size of a database might be slow, depending on the number and size of tables in it.
+ To avoid this, use I(exclude_fields=db_size).
+- Supports C(check_mode).
+
+seealso:
+- module: community.mysql.mysql_variables
+- module: community.mysql.mysql_db
+- module: community.mysql.mysql_user
+- module: community.mysql.mysql_replication
+
+author:
+- Andrew Klychkov (@Andersson007)
+- Sebastian Gumprich (@rndmh3ro)
+- Laurent Indermühle (@laurent-indermuehle)
+
+extends_documentation_fragment:
+- community.mysql.mysql
+'''
+
+EXAMPLES = r'''
+# Display info from mysql-hosts group (using creds from ~/.my.cnf to connect):
+# ansible mysql-hosts -m mysql_info
+
+# Display only databases and users info:
+# ansible mysql-hosts -m mysql_info -a 'filter=databases,users'
+
+# Display only slave status:
+# ansible standby -m mysql_info -a 'filter=slave_status'
+
+# Display all info from databases group except settings:
+# ansible databases -m mysql_info -a 'filter=!settings'
+
+# If you encounter the "Please explicitly state intended protocol" error,
+# use the login_unix_socket argument
+- name: Collect all possible information using passwordless root access
+ community.mysql.mysql_info:
+ login_user: root
+ login_unix_socket: /run/mysqld/mysqld.sock
+
+- name: Get MySQL version with non-default credentials
+ community.mysql.mysql_info:
+ login_user: mysuperuser
+ login_password: mysuperpass
+ filter: version
+
+- name: Collect all info except settings and users by root
+ community.mysql.mysql_info:
+ login_user: root
+ login_password: rootpass
+ filter: "!settings,!users"
+
+- name: Collect info about databases and version using ~/.my.cnf as a credential file
+ become: true
+ community.mysql.mysql_info:
+ filter:
+ - databases
+ - version
+
+- name: Collect info about databases and version using ~alice/.my.cnf as a credential file
+ become: true
+ community.mysql.mysql_info:
+ config_file: /home/alice/.my.cnf
+ filter:
+ - databases
+ - version
+
+- name: Collect info about databases including empty and excluding their sizes
+ become: true
+ community.mysql.mysql_info:
+ config_file: /home/alice/.my.cnf
+ filter:
+ - databases
+ exclude_fields: db_size
+ return_empty_dbs: true
+'''
+
+RETURN = r'''
+version:
+ description: Database server version.
+ returned: if not excluded by filter
+ type: dict
+ sample: { "version": { "major": 5, "minor": 5, "release": 60, "suffix": "MariaDB", "full": "5.5.60-MariaDB" } }
+ contains:
+ major:
+ description: Major server version.
+ returned: if not excluded by filter
+ type: int
+ sample: 5
+ minor:
+ description: Minor server version.
+ returned: if not excluded by filter
+ type: int
+ sample: 5
+ release:
+ description: Release server version.
+ returned: if not excluded by filter
+ type: int
+ sample: 60
+ suffix:
+ description: Server suffix, for example MySQL, MariaDB, other or none.
+ returned: if not excluded by filter
+ type: str
+ sample: "MariaDB"
+ full:
+ description: Full server version.
+ returned: if not excluded by filter
+ type: str
+ sample: "5.5.60-MariaDB"
+databases:
+ description: Information about databases.
+ returned: if not excluded by filter
+ type: dict
+ sample:
+ - { "mysql": { "size": 656594 }, "information_schema": { "size": 73728 } }
+ contains:
+ size:
+ description: Database size in bytes.
+ returned: if not excluded by filter
+ type: dict
+ sample: { 'size': 656594 }
+settings:
+ description: Global settings (variables) information.
+ returned: if not excluded by filter
+ type: dict
+ sample:
+ - { "innodb_open_files": 300, innodb_page_size": 16384 }
+global_status:
+ description: Global status information.
+ returned: if not excluded by filter
+ type: dict
+ sample:
+ - { "Innodb_buffer_pool_read_requests": 123, "Innodb_buffer_pool_reads": 32 }
+users:
+ description: Users information.
+ returned: if not excluded by filter
+ type: dict
+ sample:
+ - { "localhost": { "root": { "Alter_priv": "Y", "Alter_routine_priv": "Y" } } }
+engines:
+ description: Information about the server's storage engines.
+ returned: if not excluded by filter
+ type: dict
+ sample:
+ - { "CSV": { "Comment": "CSV storage engine", "Savepoints": "NO", "Support": "YES", "Transactions": "NO", "XA": "NO" } }
+master_status:
+ description: Master status information.
+ returned: if master
+ type: dict
+ sample:
+ - { "Binlog_Do_DB": "", "Binlog_Ignore_DB": "mysql", "File": "mysql-bin.000001", "Position": 769 }
+slave_status:
+ description: Slave status information.
+ returned: if standby
+ type: dict
+ sample:
+ - { "192.168.1.101": { "3306": { "replication_user": { "Connect_Retry": 60, "Exec_Master_Log_Pos": 769, "Last_Errno": 0 } } } }
+slave_hosts:
+ description: Slave status information.
+ returned: if master
+ type: dict
+ sample:
+ - { "2": { "Host": "", "Master_id": 1, "Port": 3306 } }
+connector_name:
+ description: Name of the python connector used by the module. When the connector is not identified, returns C(Unknown).
+ returned: always
+ type: str
+ sample:
+ - "pymysql"
+ - "MySQLdb"
+ version_added: '3.6.0'
+connector_version:
+ description: Version of the python connector used by the module. When the connector is not identified, returns C(Unknown).
+ returned: always
+ type: str
+ sample:
+ - "1.0.2"
+ version_added: '3.6.0'
+'''
+
+from decimal import Decimal
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.mysql.plugins.module_utils.mysql import (
+ mysql_connect,
+ mysql_common_argument_spec,
+ mysql_driver,
+ mysql_driver_fail_msg,
+ get_connector_name,
+ get_connector_version,
+)
+from ansible.module_utils.six import iteritems
+from ansible.module_utils._text import to_native
+
+
+# ===========================================
+# MySQL module specific support methods.
+#
+
+class MySQL_Info(object):
+
+ """Class for collection MySQL instance information.
+
+ Arguments:
+ module (AnsibleModule): Object of AnsibleModule class.
+ cursor (pymysql/mysql-python): Cursor class for interaction with
+ the database.
+
+ Note:
+ If you need to add a new subset:
+ 1. add a new key with the same name to self.info attr in self.__init__()
+ 2. add a new private method to get the information
+ 3. add invocation of the new method to self.__collect()
+ 4. add info about the new subset to the DOCUMENTATION block
+ 5. add info about the new subset with an example to RETURN block
+ """
+
+ def __init__(self, module, cursor):
+ self.module = module
+ self.cursor = cursor
+ self.info = {
+ 'version': {},
+ 'databases': {},
+ 'settings': {},
+ 'global_status': {},
+ 'engines': {},
+ 'users': {},
+ 'master_status': {},
+ 'slave_hosts': {},
+ 'slave_status': {},
+ }
+
+ def get_info(self, filter_, exclude_fields, return_empty_dbs):
+ """Get MySQL instance information based on filter_.
+
+ Arguments:
+ filter_ (list): List of collected subsets (e.g., databases, users, etc.),
+ when it is empty, return all available information.
+ """
+
+ inc_list = []
+ exc_list = []
+
+ if filter_:
+ partial_info = {}
+
+ for fi in filter_:
+ if fi.lstrip('!') not in self.info:
+ self.module.warn('filter element: %s is not allowable, ignored' % fi)
+ continue
+
+ if fi[0] == '!':
+ exc_list.append(fi.lstrip('!'))
+
+ else:
+ inc_list.append(fi)
+
+ if inc_list:
+ self.__collect(exclude_fields, return_empty_dbs, set(inc_list))
+
+ for i in self.info:
+ if i in inc_list:
+ partial_info[i] = self.info[i]
+
+ else:
+ not_in_exc_list = list(set(self.info) - set(exc_list))
+ self.__collect(exclude_fields, return_empty_dbs, set(not_in_exc_list))
+
+ for i in self.info:
+ if i not in exc_list:
+ partial_info[i] = self.info[i]
+
+ return partial_info
+
+ else:
+ self.__collect(exclude_fields, return_empty_dbs, set(self.info))
+ return self.info
+
+ def __collect(self, exclude_fields, return_empty_dbs, wanted):
+ """Collect all possible subsets."""
+ if 'version' in wanted or 'settings' in wanted:
+ self.__get_global_variables()
+
+ if 'databases' in wanted:
+ self.__get_databases(exclude_fields, return_empty_dbs)
+
+ if 'global_status' in wanted:
+ self.__get_global_status()
+
+ if 'engines' in wanted:
+ self.__get_engines()
+
+ if 'users' in wanted:
+ self.__get_users()
+
+ if 'master_status' in wanted:
+ self.__get_master_status()
+
+ if 'slave_status' in wanted:
+ self.__get_slave_status()
+
+ if 'slave_hosts' in wanted:
+ self.__get_slaves()
+
+ def __get_engines(self):
+ """Get storage engines info."""
+ res = self.__exec_sql('SHOW ENGINES')
+
+ if res:
+ for line in res:
+ engine = line['Engine']
+ self.info['engines'][engine] = {}
+
+ for vname, val in iteritems(line):
+ if vname != 'Engine':
+ self.info['engines'][engine][vname] = val
+
+ def __convert(self, val):
+ """Convert unserializable data."""
+ try:
+ if isinstance(val, Decimal):
+ val = float(val)
+ else:
+ val = int(val)
+
+ except ValueError:
+ pass
+
+ except TypeError:
+ pass
+
+ return val
+
+ def __get_global_variables(self):
+ """Get global variables (instance settings)."""
+ res = self.__exec_sql('SHOW GLOBAL VARIABLES')
+
+ if res:
+ for var in res:
+ self.info['settings'][var['Variable_name']] = self.__convert(var['Value'])
+
+ # version = ["5", "5," "60-MariaDB]
+ version = self.info['settings']['version'].split('.')
+
+ # full_version = "5.5.60-MariaDB"
+ full = self.info['settings']['version']
+
+ # release = "60"
+ release = version[2].split('-')[0]
+
+ # check if a suffix exists by counting the length
+ if len(version[2].split('-')) > 1:
+ # suffix = "MariaDB"
+ suffix = version[2].split('-', 1)[1]
+ else:
+ suffix = ""
+
+ self.info['version'] = dict(
+ # major = "5"
+ major=int(version[0]),
+ # minor = "5"
+ minor=int(version[1]),
+ release=int(release),
+ suffix=str(suffix),
+ full=str(full),
+ )
+
+ def __get_global_status(self):
+ """Get global status."""
+ res = self.__exec_sql('SHOW GLOBAL STATUS')
+
+ if res:
+ for var in res:
+ self.info['global_status'][var['Variable_name']] = self.__convert(var['Value'])
+
+ def __get_master_status(self):
+ """Get master status if the instance is a master."""
+ res = self.__exec_sql('SHOW MASTER STATUS')
+ if res:
+ for line in res:
+ for vname, val in iteritems(line):
+ self.info['master_status'][vname] = self.__convert(val)
+
+ def __get_slave_status(self):
+ """Get slave status if the instance is a slave."""
+ res = self.__exec_sql('SHOW SLAVE STATUS')
+ if res:
+ for line in res:
+ host = line['Master_Host']
+ if host not in self.info['slave_status']:
+ self.info['slave_status'][host] = {}
+
+ port = line['Master_Port']
+ if port not in self.info['slave_status'][host]:
+ self.info['slave_status'][host][port] = {}
+
+ user = line['Master_User']
+ if user not in self.info['slave_status'][host][port]:
+ self.info['slave_status'][host][port][user] = {}
+
+ for vname, val in iteritems(line):
+ if vname not in ('Master_Host', 'Master_Port', 'Master_User'):
+ self.info['slave_status'][host][port][user][vname] = self.__convert(val)
+
+ def __get_slaves(self):
+ """Get slave hosts info if the instance is a master."""
+ res = self.__exec_sql('SHOW SLAVE HOSTS')
+ if res:
+ for line in res:
+ srv_id = line['Server_id']
+ if srv_id not in self.info['slave_hosts']:
+ self.info['slave_hosts'][srv_id] = {}
+
+ for vname, val in iteritems(line):
+ if vname != 'Server_id':
+ self.info['slave_hosts'][srv_id][vname] = self.__convert(val)
+
+ def __get_users(self):
+ """Get user info."""
+ res = self.__exec_sql('SELECT * FROM mysql.user')
+ if res:
+ for line in res:
+ host = line['Host']
+ if host not in self.info['users']:
+ self.info['users'][host] = {}
+
+ user = line['User']
+ self.info['users'][host][user] = {}
+
+ for vname, val in iteritems(line):
+ if vname not in ('Host', 'User'):
+ self.info['users'][host][user][vname] = self.__convert(val)
+
+ def __get_databases(self, exclude_fields, return_empty_dbs):
+ """Get info about databases."""
+ if not exclude_fields:
+ query = ('SELECT table_schema AS "name", '
+ 'SUM(data_length + index_length) AS "size" '
+ 'FROM information_schema.TABLES GROUP BY table_schema')
+ else:
+ if 'db_size' in exclude_fields:
+ query = ('SELECT table_schema AS "name" '
+ 'FROM information_schema.TABLES GROUP BY table_schema')
+
+ res = self.__exec_sql(query)
+
+ if res:
+ for db in res:
+ self.info['databases'][db['name']] = {}
+
+ if not exclude_fields or 'db_size' not in exclude_fields:
+ if db['size'] is None:
+ db['size'] = 0
+
+ self.info['databases'][db['name']]['size'] = int(db['size'])
+
+ # If empty dbs are not needed in the returned dict, exit from the method
+ if not return_empty_dbs:
+ return None
+
+ # Add info about empty databases (issue #65727):
+ res = self.__exec_sql('SHOW DATABASES')
+ if res:
+ for db in res:
+ if db['Database'] not in self.info['databases']:
+ self.info['databases'][db['Database']] = {}
+
+ if not exclude_fields or 'db_size' not in exclude_fields:
+ self.info['databases'][db['Database']]['size'] = 0
+
+ def __exec_sql(self, query, ddl=False):
+ """Execute SQL.
+
+ Arguments:
+ ddl (bool): If True, return True or False.
+ Used for queries that don't return any rows
+ (mainly for DDL queries) (default False).
+ """
+ try:
+ self.cursor.execute(query)
+
+ if not ddl:
+ res = self.cursor.fetchall()
+ return res
+ return True
+
+ except Exception as e:
+ self.module.fail_json(msg="Cannot execute SQL '%s': %s" % (query, to_native(e)))
+ return False
+
+
+# ===========================================
+# Module execution.
+#
+
+
+def main():
+ argument_spec = mysql_common_argument_spec()
+ argument_spec.update(
+ login_db=dict(type='str'),
+ filter=dict(type='list'),
+ exclude_fields=dict(type='list'),
+ return_empty_dbs=dict(type='bool', default=False),
+ )
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ )
+
+ db = module.params['login_db']
+ connect_timeout = module.params['connect_timeout']
+ login_user = module.params['login_user']
+ login_password = module.params['login_password']
+ ssl_cert = module.params['client_cert']
+ ssl_key = module.params['client_key']
+ ssl_ca = module.params['ca_cert']
+ check_hostname = module.params['check_hostname']
+ config_file = module.params['config_file']
+ filter_ = module.params['filter']
+ exclude_fields = module.params['exclude_fields']
+ return_empty_dbs = module.params['return_empty_dbs']
+
+ if filter_:
+ filter_ = [f.strip() for f in filter_]
+
+ if exclude_fields:
+ exclude_fields = set([f.strip() for f in exclude_fields])
+
+ if mysql_driver is None:
+ module.fail_json(msg=mysql_driver_fail_msg)
+
+ connector_name = get_connector_name(mysql_driver)
+ connector_version = get_connector_version(mysql_driver)
+
+ try:
+ cursor, db_conn = mysql_connect(module, login_user, login_password,
+ config_file, ssl_cert, ssl_key, ssl_ca, db,
+ check_hostname=check_hostname,
+ connect_timeout=connect_timeout, cursor_class='DictCursor')
+ except Exception as e:
+ msg = ('unable to connect to database using %s %s, check login_user '
+ 'and login_password are correct or %s has the credentials. '
+ 'Exception message: %s' % (connector_name, connector_version, config_file, to_native(e)))
+ module.fail_json(msg)
+
+ ###############################
+ # Create object and do main job
+
+ mysql = MySQL_Info(module, cursor)
+
+ module.exit_json(changed=False,
+ connector_name=connector_name,
+ connector_version=connector_version,
+ **mysql.get_info(filter_, exclude_fields, return_empty_dbs))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/mysql/plugins/modules/mysql_query.py b/ansible_collections/community/mysql/plugins/modules/mysql_query.py
new file mode 100644
index 000000000..12d5a5630
--- /dev/null
+++ b/ansible_collections/community/mysql/plugins/modules/mysql_query.py
@@ -0,0 +1,284 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2020, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+DOCUMENTATION = r'''
+---
+module: mysql_query
+short_description: Run MySQL queries
+description:
+- Runs arbitrary MySQL queries.
+- Pay attention, the module does not support check mode!
+ All queries will be executed in autocommit mode.
+- To run SQL queries from a file, use M(community.mysql.mysql_db) module.
+version_added: '0.1.0'
+options:
+ query:
+ description:
+ - SQL query to run. Multiple queries can be passed using YAML list syntax.
+ - Must be a string or YAML list containing strings.
+ - If you use I(named_args) or I(positional_args) any C(%) will be interpreted
+ as a formatting character. All literal C(%) characters in the query should be
+ escaped as C(%%).
+ - Note that if you use the C(IF EXISTS/IF NOT EXISTS) clauses in your query
+ and C(mysqlclient) or C(PyMySQL 0.10.0+) connectors, the module will report
+ that the state has been changed even if it has not. If it is important in your
+ workflow, use the C(PyMySQL 0.9.3) connector instead.
+ type: raw
+ required: true
+ positional_args:
+ description:
+ - List of values to be passed as positional arguments to the query.
+ - Mutually exclusive with I(named_args).
+ type: list
+ named_args:
+ description:
+ - Dictionary of key-value arguments to pass to the query.
+ - Mutually exclusive with I(positional_args).
+ type: dict
+ login_db:
+ description:
+ - Name of database to connect to and run queries against.
+ type: str
+ single_transaction:
+ description:
+ - Where passed queries run in a single transaction (C(yes)) or commit them one-by-one (C(no)).
+ type: bool
+ default: false
+seealso:
+- module: community.mysql.mysql_db
+author:
+- Andrew Klychkov (@Andersson007)
+extends_documentation_fragment:
+- community.mysql.mysql
+
+'''
+
+EXAMPLES = r'''
+# If you encounter the "Please explicitly state intended protocol" error,
+# use the login_unix_socket argument
+- name: Simple select query to acme db
+ community.mysql.mysql_query:
+ login_db: acme
+ query: SELECT * FROM orders
+ login_unix_socket: /run/mysqld/mysqld.sock
+
+- name: Select query to db acme with positional arguments
+ community.mysql.mysql_query:
+ login_db: acme
+ query: SELECT * FROM acme WHERE id = %s AND story = %s
+ positional_args:
+ - 1
+ - test
+
+- name: Select query to test_db with named_args
+ community.mysql.mysql_query:
+ login_db: test_db
+ query: SELECT * FROM test WHERE id = %(id_val)s AND story = %(story_val)s
+ named_args:
+ id_val: 1
+ story_val: test
+
+- name: Run several insert queries against db test_db in single transaction
+ community.mysql.mysql_query:
+ login_db: test_db
+ query:
+ - INSERT INTO articles (id, story) VALUES (2, 'my_long_story')
+ - INSERT INTO prices (id, price) VALUES (123, '100.00')
+ single_transaction: true
+'''
+
+RETURN = r'''
+executed_queries:
+ description: List of executed queries.
+ returned: always
+ type: list
+ sample: ['SELECT * FROM bar', 'UPDATE bar SET id = 1 WHERE id = 2']
+query_result:
+ description:
+ - List of lists (sublist for each query) containing dictionaries
+ in column:value form representing returned rows.
+ returned: changed
+ type: list
+ sample: [[{"Column": "Value1"},{"Column": "Value2"}], [{"ID": 1}, {"ID": 2}]]
+rowcount:
+ description: Number of affected rows for each subquery.
+ returned: changed
+ type: list
+ sample: [5, 1]
+'''
+
+import warnings
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.mysql.plugins.module_utils.mysql import (
+ mysql_connect,
+ mysql_common_argument_spec,
+ mysql_driver,
+ mysql_driver_fail_msg,
+)
+from ansible.module_utils._text import to_native
+
+DML_QUERY_KEYWORDS = ('INSERT', 'UPDATE', 'DELETE', 'REPLACE')
+# TRUNCATE is not DDL query but it also returns 0 rows affected:
+DDL_QUERY_KEYWORDS = ('CREATE', 'DROP', 'ALTER', 'RENAME', 'TRUNCATE')
+
+
+# ===========================================
+# Module execution.
+#
+
+def main():
+ argument_spec = mysql_common_argument_spec()
+ argument_spec.update(
+ query=dict(type='raw', required=True),
+ login_db=dict(type='str'),
+ positional_args=dict(type='list'),
+ named_args=dict(type='dict'),
+ single_transaction=dict(type='bool', default=False),
+ )
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ mutually_exclusive=(
+ ('positional_args', 'named_args'),
+ ),
+ )
+
+ db = module.params['login_db']
+ connect_timeout = module.params['connect_timeout']
+ login_user = module.params['login_user']
+ login_password = module.params['login_password']
+ ssl_cert = module.params['client_cert']
+ ssl_key = module.params['client_key']
+ ssl_ca = module.params['ca_cert']
+ check_hostname = module.params['check_hostname']
+ config_file = module.params['config_file']
+ query = module.params["query"]
+
+ if not isinstance(query, (str, list)):
+ module.fail_json(msg="the query option value must be a string or list, passed %s" % type(query))
+
+ if isinstance(query, str):
+ query = [query]
+
+ for elem in query:
+ if not isinstance(elem, str):
+ module.fail_json(msg="the elements in query list must be strings, passed '%s' %s" % (elem, type(elem)))
+
+ if module.params["single_transaction"]:
+ autocommit = False
+ else:
+ autocommit = True
+ # Prepare args:
+ if module.params.get("positional_args"):
+ arguments = module.params["positional_args"]
+ elif module.params.get("named_args"):
+ arguments = module.params["named_args"]
+ else:
+ arguments = None
+
+ if mysql_driver is None:
+ module.fail_json(msg=mysql_driver_fail_msg)
+
+ # Connect to DB:
+ try:
+ cursor, db_connection = mysql_connect(module, login_user, login_password,
+ config_file, ssl_cert, ssl_key, ssl_ca, db,
+ check_hostname=check_hostname,
+ connect_timeout=connect_timeout,
+ cursor_class='DictCursor', autocommit=autocommit)
+ except Exception as e:
+ module.fail_json(msg="unable to connect to database, check login_user and "
+ "login_password are correct or %s has the credentials. "
+ "Exception message: %s" % (config_file, to_native(e)))
+
+ # Set defaults:
+ changed = False
+
+ max_keyword_len = len(max(DML_QUERY_KEYWORDS + DDL_QUERY_KEYWORDS, key=len))
+
+ # Execute query:
+ query_result = []
+ executed_queries = []
+ rowcount = []
+
+ already_exists = False
+ for q in query:
+ try:
+ with warnings.catch_warnings():
+ warnings.filterwarnings(action='error',
+ message='.*already exists*',
+ category=mysql_driver.Warning)
+
+ try:
+ cursor.execute(q, arguments)
+ except mysql_driver.Warning:
+ # When something is run with IF NOT EXISTS
+ # and there's "already exists" MySQL warning,
+ # set the flag as True.
+ # PyMySQL < 0.10.0 throws the warning, mysqlclient
+ # and PyMySQL 0.10.0+ does NOT.
+ already_exists = True
+
+ except Exception as e:
+ if not autocommit:
+ db_connection.rollback()
+
+ cursor.close()
+ module.fail_json(msg="Cannot execute SQL '%s' args [%s]: %s" % (q, arguments, to_native(e)))
+
+ try:
+ if not already_exists:
+ query_result.append([dict(row) for row in cursor.fetchall()])
+
+ except Exception as e:
+ if not autocommit:
+ db_connection.rollback()
+
+ module.fail_json(msg="Cannot fetch rows from cursor: %s" % to_native(e))
+
+ # Check DML or DDL keywords in query and set changed accordingly:
+ q = q.lstrip()[0:max_keyword_len].upper()
+ for keyword in DML_QUERY_KEYWORDS:
+ if keyword in q and cursor.rowcount > 0:
+ changed = True
+
+ for keyword in DDL_QUERY_KEYWORDS:
+ if keyword in q:
+ if already_exists:
+ # Indicates the entity already exists
+ changed = False
+ already_exists = False # Reset flag
+ else:
+ changed = True
+ try:
+ executed_queries.append(cursor._last_executed)
+ except AttributeError:
+ # MySQLdb removed cursor._last_executed as a duplicate of cursor._executed
+ executed_queries.append(cursor._executed)
+ rowcount.append(cursor.rowcount)
+
+ # When the module run with the single_transaction == True:
+ if not autocommit:
+ db_connection.commit()
+
+ # Create dict with returned values:
+ kw = {
+ 'changed': changed,
+ 'executed_queries': executed_queries,
+ 'query_result': query_result,
+ 'rowcount': rowcount,
+ }
+
+ # Exit:
+ module.exit_json(**kw)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/mysql/plugins/modules/mysql_replication.py b/ansible_collections/community/mysql/plugins/modules/mysql_replication.py
new file mode 100644
index 000000000..33e14bc26
--- /dev/null
+++ b/ansible_collections/community/mysql/plugins/modules/mysql_replication.py
@@ -0,0 +1,654 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2013, Balazs Pocze <banyek@gawker.com>
+# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru>
+# Certain parts are taken from Mark Theunissen's mysqldb module
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: mysql_replication
+short_description: Manage MySQL replication
+description:
+- Manages MySQL server replication, replica, primary status, get and change primary host.
+author:
+- Balazs Pocze (@banyek)
+- Andrew Klychkov (@Andersson007)
+options:
+ mode:
+ description:
+ - Module operating mode. Could be
+ C(changeprimary) (CHANGE PRIMARY TO),
+ C(getprimary) (SHOW PRIMARY STATUS),
+ C(getreplica) (SHOW REPLICA),
+ C(startreplica) (START REPLICA),
+ C(stopreplica) (STOP REPLICA),
+ C(resetprimary) (RESET PRIMARY) - supported since community.mysql 0.1.0,
+ C(resetreplica) (RESET REPLICA),
+ C(resetreplicaall) (RESET REPLICA ALL).
+ type: str
+ choices:
+ - changeprimary
+ - getprimary
+ - getreplica
+ - startreplica
+ - stopreplica
+ - resetprimary
+ - resetreplica
+ - resetreplicaall
+ default: getreplica
+ primary_host:
+ description:
+ - Same as the C(MASTER_HOST) mysql variable.
+ type: str
+ aliases: [master_host]
+ primary_user:
+ description:
+ - Same as the C(MASTER_USER) mysql variable.
+ type: str
+ aliases: [master_user]
+ primary_password:
+ description:
+ - Same as the C(MASTER_PASSWORD) mysql variable.
+ type: str
+ aliases: [master_password]
+ primary_port:
+ description:
+ - Same as the C(MASTER_PORT) mysql variable.
+ type: int
+ aliases: [master_port]
+ primary_connect_retry:
+ description:
+ - Same as the C(MASTER_CONNECT_RETRY) mysql variable.
+ type: int
+ aliases: [master_connect_retry]
+ primary_log_file:
+ description:
+ - Same as the C(MASTER_LOG_FILE) mysql variable.
+ type: str
+ aliases: [master_log_file]
+ primary_log_pos:
+ description:
+ - Same as the C(MASTER_LOG_POS) mysql variable.
+ type: int
+ aliases: [master_log_pos]
+ relay_log_file:
+ description:
+ - Same as mysql variable.
+ type: str
+ relay_log_pos:
+ description:
+ - Same as mysql variable.
+ type: int
+ primary_ssl:
+ description:
+ - Same as the C(MASTER_SSL) mysql variable.
+ - When setting it to C(yes), the connection attempt only succeeds
+ if an encrypted connection can be established.
+ - For details, refer to
+ L(MySQL encrypted replication documentation,https://dev.mysql.com/doc/refman/8.0/en/replication-solutions-encrypted-connections.html).
+ - The default is C(false).
+ type: bool
+ aliases: [master_ssl]
+ primary_ssl_ca:
+ description:
+ - Same as the C(MASTER_SSL_CA) mysql variable.
+ - For details, refer to
+ L(MySQL encrypted replication documentation,https://dev.mysql.com/doc/refman/8.0/en/replication-solutions-encrypted-connections.html).
+ type: str
+ aliases: [master_ssl_ca]
+ primary_ssl_capath:
+ description:
+ - Same as the C(MASTER_SSL_CAPATH) mysql variable.
+ - For details, refer to
+ L(MySQL encrypted replication documentation,https://dev.mysql.com/doc/refman/8.0/en/replication-solutions-encrypted-connections.html).
+ type: str
+ aliases: [master_ssl_capath]
+ primary_ssl_cert:
+ description:
+ - Same as the C(MASTER_SSL_CERT) mysql variable.
+ - For details, refer to
+ L(MySQL encrypted replication documentation,https://dev.mysql.com/doc/refman/8.0/en/replication-solutions-encrypted-connections.html).
+ type: str
+ aliases: [master_ssl_cert]
+ primary_ssl_key:
+ description:
+ - Same as the C(MASTER_SSL_KEY) mysql variable.
+ - For details, refer to
+ L(MySQL encrypted replication documentation,https://dev.mysql.com/doc/refman/8.0/en/replication-solutions-encrypted-connections.html).
+ type: str
+ aliases: [master_ssl_key]
+ primary_ssl_cipher:
+ description:
+ - Same as the C(MASTER_SSL_CIPHER) mysql variable.
+ - Specifies a colon-separated list of one or more ciphers permitted by the replica for the replication connection.
+ - For details, refer to
+ L(MySQL encrypted replication documentation,https://dev.mysql.com/doc/refman/8.0/en/replication-solutions-encrypted-connections.html).
+ type: str
+ aliases: [master_ssl_cipher]
+ primary_ssl_verify_server_cert:
+ description:
+ - Same as mysql variable.
+ type: bool
+ default: false
+ version_added: '3.5.0'
+ primary_auto_position:
+ description:
+ - Whether the host uses GTID based replication or not.
+ - Same as the C(MASTER_AUTO_POSITION) mysql variable.
+ type: bool
+ default: false
+ aliases: [master_auto_position]
+ primary_use_gtid:
+ description:
+ - Configures the replica to use the MariaDB Global Transaction ID.
+ - C(disabled) equals MASTER_USE_GTID=no command.
+ - To find information about available values see
+ U(https://mariadb.com/kb/en/library/change-master-to/#master_use_gtid).
+ - Available since MariaDB 10.0.2.
+ choices: [current_pos, replica_pos, disabled]
+ type: str
+ version_added: '0.1.0'
+ aliases: [master_use_gtid]
+ primary_delay:
+ description:
+ - Time lag behind the primary's state (in seconds).
+ - Same as the C(MASTER_DELAY) mysql variable.
+ - Available from MySQL 5.6.
+ - For more information see U(https://dev.mysql.com/doc/refman/8.0/en/replication-delayed.html).
+ type: int
+ version_added: '0.1.0'
+ aliases: [master_delay]
+ connection_name:
+ description:
+ - Name of the primary connection.
+ - Supported from MariaDB 10.0.1.
+ - Mutually exclusive with I(channel).
+ - For more information see U(https://mariadb.com/kb/en/library/multi-source-replication/).
+ type: str
+ version_added: '0.1.0'
+ channel:
+ description:
+ - Name of replication channel.
+ - Multi-source replication is supported from MySQL 5.7.
+ - Mutually exclusive with I(connection_name).
+ - For more information see U(https://dev.mysql.com/doc/refman/8.0/en/replication-multi-source.html).
+ type: str
+ version_added: '0.1.0'
+ fail_on_error:
+ description:
+ - Fails on error when calling mysql.
+ type: bool
+ default: false
+ version_added: '0.1.0'
+
+notes:
+- If an empty value for the parameter of string type is needed, use an empty string.
+
+extends_documentation_fragment:
+- community.mysql.mysql
+
+
+seealso:
+- module: community.mysql.mysql_info
+- name: MySQL replication reference
+ description: Complete reference of the MySQL replication documentation.
+ link: https://dev.mysql.com/doc/refman/8.0/en/replication.html
+- name: MySQL encrypted replication reference.
+ description: Setting up MySQL replication to use encrypted connection.
+ link: https://dev.mysql.com/doc/refman/8.0/en/replication-solutions-encrypted-connections.html
+- name: MariaDB replication reference
+ description: Complete reference of the MariaDB replication documentation.
+ link: https://mariadb.com/kb/en/library/setting-up-replication/
+'''
+
+EXAMPLES = r'''
+# If you encounter the "Please explicitly state intended protocol" error,
+# use the login_unix_socket argument
+- name: Stop mysql replica thread
+ community.mysql.mysql_replication:
+ mode: stopreplica
+ login_unix_socket: /run/mysqld/mysqld.sock
+
+- name: Get primary binlog file name and binlog position
+ community.mysql.mysql_replication:
+ mode: getprimary
+
+- name: Change primary to primary server 192.0.2.1 and use binary log 'mysql-bin.000009' with position 4578
+ community.mysql.mysql_replication:
+ mode: changeprimary
+ primary_host: 192.0.2.1
+ primary_log_file: mysql-bin.000009
+ primary_log_pos: 4578
+
+- name: Check replica status using port 3308
+ community.mysql.mysql_replication:
+ mode: getreplica
+ login_host: ansible.example.com
+ login_port: 3308
+
+- name: On MariaDB change primary to use GTID current_pos
+ community.mysql.mysql_replication:
+ mode: changeprimary
+ primary_use_gtid: current_pos
+
+- name: Change primary to use replication delay 3600 seconds
+ community.mysql.mysql_replication:
+ mode: changeprimary
+ primary_host: 192.0.2.1
+ primary_delay: 3600
+
+- name: Start MariaDB replica with connection name primary-1
+ community.mysql.mysql_replication:
+ mode: startreplica
+ connection_name: primary-1
+
+- name: Stop replication in channel primary-1
+ community.mysql.mysql_replication:
+ mode: stopreplica
+ channel: primary-1
+
+- name: >
+ Run RESET MASTER command which will delete all existing binary log files
+ and reset the binary log index file on the primary
+ community.mysql.mysql_replication:
+ mode: resetprimary
+
+- name: Run start replica and fail the task on errors
+ community.mysql.mysql_replication:
+ mode: startreplica
+ connection_name: primary-1
+ fail_on_error: true
+
+- name: Change primary and fail on error (like when replica thread is running)
+ community.mysql.mysql_replication:
+ mode: changeprimary
+ fail_on_error: true
+
+'''
+
+RETURN = r'''
+queries:
+ description: List of executed queries which modified DB's state.
+ returned: always
+ type: list
+ sample: ["CHANGE MASTER TO MASTER_HOST='primary2.example.com',MASTER_PORT=3306"]
+ version_added: '0.1.0'
+'''
+
+import os
+import warnings
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.mysql.plugins.module_utils.mysql import (
+ mysql_connect,
+ mysql_driver,
+ mysql_driver_fail_msg,
+ mysql_common_argument_spec,
+)
+from ansible.module_utils._text import to_native
+
+executed_queries = []
+
+
+def get_primary_status(cursor):
+ # TODO: when it's available to change on MySQL's side,
+ # change MASTER to PRIMARY using the approach from
+ # get_replica_status() function. Same for other functions.
+ cursor.execute("SHOW MASTER STATUS")
+ primarystatus = cursor.fetchone()
+ return primarystatus
+
+
+def get_replica_status(cursor, connection_name='', channel='', term='REPLICA'):
+ if connection_name:
+ query = "SHOW %s '%s' STATUS" % (term, connection_name)
+ else:
+ query = "SHOW %s STATUS" % term
+
+ if channel:
+ query += " FOR CHANNEL '%s'" % channel
+
+ cursor.execute(query)
+ replica_status = cursor.fetchone()
+ return replica_status
+
+
+def stop_replica(module, cursor, connection_name='', channel='', fail_on_error=False, term='REPLICA'):
+ if connection_name:
+ query = "STOP %s '%s'" % (term, connection_name)
+ else:
+ query = 'STOP %s' % term
+
+ if channel:
+ query += " FOR CHANNEL '%s'" % channel
+
+ try:
+ executed_queries.append(query)
+ cursor.execute(query)
+ stopped = True
+ except mysql_driver.Warning as e:
+ stopped = False
+ except Exception as e:
+ if fail_on_error:
+ module.fail_json(msg="STOP REPLICA failed: %s" % to_native(e))
+ stopped = False
+ return stopped
+
+
+def reset_replica(module, cursor, connection_name='', channel='', fail_on_error=False, term='REPLICA'):
+ if connection_name:
+ query = "RESET %s '%s'" % (term, connection_name)
+ else:
+ query = 'RESET %s' % term
+
+ if channel:
+ query += " FOR CHANNEL '%s'" % channel
+
+ try:
+ executed_queries.append(query)
+ cursor.execute(query)
+ reset = True
+ except mysql_driver.Warning as e:
+ reset = False
+ except Exception as e:
+ if fail_on_error:
+ module.fail_json(msg="RESET REPLICA failed: %s" % to_native(e))
+ reset = False
+ return reset
+
+
+def reset_replica_all(module, cursor, connection_name='', channel='', fail_on_error=False, term='REPLICA'):
+ if connection_name:
+ query = "RESET %s '%s' ALL" % (term, connection_name)
+ else:
+ query = 'RESET %s ALL' % term
+
+ if channel:
+ query += " FOR CHANNEL '%s'" % channel
+
+ try:
+ executed_queries.append(query)
+ cursor.execute(query)
+ reset = True
+ except mysql_driver.Warning as e:
+ reset = False
+ except Exception as e:
+ if fail_on_error:
+ module.fail_json(msg="RESET REPLICA ALL failed: %s" % to_native(e))
+ reset = False
+ return reset
+
+
+def reset_primary(module, cursor, fail_on_error=False):
+ query = 'RESET MASTER'
+ try:
+ executed_queries.append(query)
+ cursor.execute(query)
+ reset = True
+ except mysql_driver.Warning as e:
+ reset = False
+ except Exception as e:
+ if fail_on_error:
+ module.fail_json(msg="RESET MASTER failed: %s" % to_native(e))
+ reset = False
+ return reset
+
+
+def start_replica(module, cursor, connection_name='', channel='', fail_on_error=False, term='REPLICA'):
+ if connection_name:
+ query = "START %s '%s'" % (term, connection_name)
+ else:
+ query = 'START %s' % term
+
+ if channel:
+ query += " FOR CHANNEL '%s'" % channel
+
+ try:
+ executed_queries.append(query)
+ cursor.execute(query)
+ started = True
+ except mysql_driver.Warning as e:
+ started = False
+ except Exception as e:
+ if fail_on_error:
+ module.fail_json(msg="START REPLICA failed: %s" % to_native(e))
+ started = False
+ return started
+
+
+def changeprimary(cursor, chm, connection_name='', channel=''):
+ if connection_name:
+ query = "CHANGE MASTER '%s' TO %s" % (connection_name, ','.join(chm))
+ else:
+ query = 'CHANGE MASTER TO %s' % ','.join(chm)
+
+ if channel:
+ query += " FOR CHANNEL '%s'" % channel
+
+ executed_queries.append(query)
+ cursor.execute(query)
+
+
+def main():
+ argument_spec = mysql_common_argument_spec()
+ argument_spec.update(
+ mode=dict(type='str', default='getreplica', choices=[
+ 'getprimary',
+ 'getreplica',
+ 'changeprimary',
+ 'stopreplica',
+ 'startreplica',
+ 'resetprimary',
+ 'resetreplica',
+ 'resetreplicaall']),
+ primary_auto_position=dict(type='bool', default=False, aliases=['master_auto_position']),
+ primary_host=dict(type='str', aliases=['master_host']),
+ primary_user=dict(type='str', aliases=['master_user']),
+ primary_password=dict(type='str', no_log=True, aliases=['master_password']),
+ primary_port=dict(type='int', aliases=['master_port']),
+ primary_connect_retry=dict(type='int', aliases=['master_connect_retry']),
+ primary_log_file=dict(type='str', aliases=['master_log_file']),
+ primary_log_pos=dict(type='int', aliases=['master_log_pos']),
+ relay_log_file=dict(type='str'),
+ relay_log_pos=dict(type='int'),
+ primary_ssl=dict(type='bool', aliases=['master_ssl']),
+ primary_ssl_ca=dict(type='str', aliases=['master_ssl_ca']),
+ primary_ssl_capath=dict(type='str', aliases=['master_ssl_capath']),
+ primary_ssl_cert=dict(type='str', aliases=['master_ssl_cert']),
+ primary_ssl_key=dict(type='str', no_log=False, aliases=['master_ssl_key']),
+ primary_ssl_cipher=dict(type='str', aliases=['master_ssl_cipher']),
+ primary_ssl_verify_server_cert=dict(type='bool', default=False),
+ primary_use_gtid=dict(type='str', choices=[
+ 'current_pos', 'replica_pos', 'disabled'], aliases=['master_use_gtid']),
+ primary_delay=dict(type='int', aliases=['master_delay']),
+ connection_name=dict(type='str'),
+ channel=dict(type='str'),
+ fail_on_error=dict(type='bool', default=False),
+ )
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ mutually_exclusive=[
+ ['connection_name', 'channel']
+ ],
+ )
+ mode = module.params["mode"]
+ primary_host = module.params["primary_host"]
+ primary_user = module.params["primary_user"]
+ primary_password = module.params["primary_password"]
+ primary_port = module.params["primary_port"]
+ primary_connect_retry = module.params["primary_connect_retry"]
+ primary_log_file = module.params["primary_log_file"]
+ primary_log_pos = module.params["primary_log_pos"]
+ relay_log_file = module.params["relay_log_file"]
+ relay_log_pos = module.params["relay_log_pos"]
+ primary_ssl = module.params["primary_ssl"]
+ primary_ssl_ca = module.params["primary_ssl_ca"]
+ primary_ssl_capath = module.params["primary_ssl_capath"]
+ primary_ssl_cert = module.params["primary_ssl_cert"]
+ primary_ssl_key = module.params["primary_ssl_key"]
+ primary_ssl_cipher = module.params["primary_ssl_cipher"]
+ primary_ssl_verify_server_cert = module.params["primary_ssl_verify_server_cert"]
+ primary_auto_position = module.params["primary_auto_position"]
+ ssl_cert = module.params["client_cert"]
+ ssl_key = module.params["client_key"]
+ ssl_ca = module.params["ca_cert"]
+ check_hostname = module.params["check_hostname"]
+ connect_timeout = module.params['connect_timeout']
+ config_file = module.params['config_file']
+ primary_delay = module.params['primary_delay']
+ if module.params.get("primary_use_gtid") == 'disabled':
+ primary_use_gtid = 'no'
+ else:
+ primary_use_gtid = module.params["primary_use_gtid"]
+ connection_name = module.params["connection_name"]
+ channel = module.params['channel']
+ fail_on_error = module.params['fail_on_error']
+
+ if mysql_driver is None:
+ module.fail_json(msg=mysql_driver_fail_msg)
+ else:
+ warnings.filterwarnings('error', category=mysql_driver.Warning)
+
+ login_password = module.params["login_password"]
+ login_user = module.params["login_user"]
+
+ try:
+ cursor, db_conn = mysql_connect(module, login_user, login_password, config_file,
+ ssl_cert, ssl_key, ssl_ca, None, cursor_class='DictCursor',
+ connect_timeout=connect_timeout, check_hostname=check_hostname)
+ except Exception as e:
+ if os.path.exists(config_file):
+ module.fail_json(msg="unable to connect to database, check login_user and "
+ "login_password are correct or %s has the credentials. "
+ "Exception message: %s" % (config_file, to_native(e)))
+ else:
+ module.fail_json(msg="unable to find %s. Exception message: %s" % (config_file, to_native(e)))
+
+ cursor.execute("SELECT VERSION()")
+ if 'mariadb' in cursor.fetchone()["VERSION()"].lower():
+ 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
+
+ # Since MySQL 8.0.22 and MariaDB 10.5.1,
+ # "REPLICA" must be used instead of "SLAVE"
+ if impl.uses_replica_terminology(cursor):
+ replica_term = 'REPLICA'
+ else:
+ replica_term = 'SLAVE'
+ if primary_use_gtid == 'replica_pos':
+ primary_use_gtid = 'slave_pos'
+
+ if mode == 'getprimary':
+ status = get_primary_status(cursor)
+ if not isinstance(status, dict):
+ status = dict(Is_Primary=False,
+ msg="Server is not configured as mysql primary")
+ else:
+ status['Is_Primary'] = True
+
+ module.exit_json(queries=executed_queries, **status)
+
+ elif mode == "getreplica":
+ status = get_replica_status(cursor, connection_name, channel, replica_term)
+ if not isinstance(status, dict):
+ status = dict(Is_Replica=False, msg="Server is not configured as mysql replica")
+ else:
+ status['Is_Replica'] = True
+
+ module.exit_json(queries=executed_queries, **status)
+
+ elif mode == 'changeprimary':
+ chm = []
+ result = {}
+ if primary_host is not None:
+ chm.append("MASTER_HOST='%s'" % primary_host)
+ if primary_user is not None:
+ chm.append("MASTER_USER='%s'" % primary_user)
+ if primary_password is not None:
+ chm.append("MASTER_PASSWORD='%s'" % primary_password)
+ if primary_port is not None:
+ chm.append("MASTER_PORT=%s" % primary_port)
+ if primary_connect_retry is not None:
+ chm.append("MASTER_CONNECT_RETRY=%s" % primary_connect_retry)
+ if primary_log_file is not None:
+ chm.append("MASTER_LOG_FILE='%s'" % primary_log_file)
+ if primary_log_pos is not None:
+ chm.append("MASTER_LOG_POS=%s" % primary_log_pos)
+ if primary_delay is not None:
+ chm.append("MASTER_DELAY=%s" % 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")
+ else:
+ chm.append("MASTER_SSL=0")
+ if primary_ssl_ca is not None:
+ chm.append("MASTER_SSL_CA='%s'" % primary_ssl_ca)
+ if primary_ssl_capath is not None:
+ chm.append("MASTER_SSL_CAPATH='%s'" % primary_ssl_capath)
+ if primary_ssl_cert is not None:
+ chm.append("MASTER_SSL_CERT='%s'" % primary_ssl_cert)
+ if primary_ssl_key is not None:
+ chm.append("MASTER_SSL_KEY='%s'" % primary_ssl_key)
+ if primary_ssl_cipher is not None:
+ chm.append("MASTER_SSL_CIPHER='%s'" % primary_ssl_cipher)
+ if primary_ssl_verify_server_cert:
+ chm.append("SOURCE_SSL_VERIFY_SERVER_CERT=1")
+ if primary_auto_position:
+ chm.append("MASTER_AUTO_POSITION=1")
+ if primary_use_gtid is not None:
+ chm.append("MASTER_USE_GTID=%s" % primary_use_gtid)
+ try:
+ changeprimary(cursor, 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))
+ result['changed'] = True
+ module.exit_json(queries=executed_queries, **result)
+ elif mode == "startreplica":
+ started = start_replica(module, cursor, connection_name, channel, fail_on_error, replica_term)
+ if started is True:
+ module.exit_json(msg="Replica started ", changed=True, queries=executed_queries)
+ else:
+ module.exit_json(msg="Replica already started (Or cannot be started)", changed=False, queries=executed_queries)
+ elif mode == "stopreplica":
+ stopped = stop_replica(module, cursor, connection_name, channel, fail_on_error, replica_term)
+ if stopped is True:
+ module.exit_json(msg="Replica stopped", changed=True, queries=executed_queries)
+ else:
+ module.exit_json(msg="Replica already stopped", changed=False, queries=executed_queries)
+ elif mode == 'resetprimary':
+ reset = reset_primary(module, cursor, fail_on_error)
+ if reset is True:
+ module.exit_json(msg="Primary reset", changed=True, queries=executed_queries)
+ else:
+ module.exit_json(msg="Primary already reset", changed=False, queries=executed_queries)
+ elif mode == "resetreplica":
+ reset = reset_replica(module, cursor, connection_name, channel, fail_on_error, replica_term)
+ if reset is True:
+ module.exit_json(msg="Replica reset", changed=True, queries=executed_queries)
+ else:
+ module.exit_json(msg="Replica already reset", changed=False, queries=executed_queries)
+ elif mode == "resetreplicaall":
+ reset = reset_replica_all(module, cursor, connection_name, channel, fail_on_error, replica_term)
+ if reset is True:
+ module.exit_json(msg="Replica reset", changed=True, queries=executed_queries)
+ else:
+ module.exit_json(msg="Replica already reset", changed=False, queries=executed_queries)
+
+ warnings.simplefilter("ignore")
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/mysql/plugins/modules/mysql_role.py b/ansible_collections/community/mysql/plugins/modules/mysql_role.py
new file mode 100644
index 000000000..070d7939d
--- /dev/null
+++ b/ansible_collections/community/mysql/plugins/modules/mysql_role.py
@@ -0,0 +1,1095 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2021, Andrew Klychkov <aaklychkov@mail.ru>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = r'''
+---
+module: mysql_role
+
+short_description: Adds, removes, or updates a MySQL role
+
+description:
+ - Adds, removes, or updates a MySQL role.
+ - Roles are supported since MySQL 8.0.0 and MariaDB 10.0.5.
+
+version_added: '2.2.0'
+
+options:
+ name:
+ description:
+ - Name of the role to add or remove.
+ type: str
+ required: true
+
+ admin:
+ description:
+ - Supported by B(MariaDB).
+ - Name of the admin user of the role (the I(login_user), by default).
+ type: str
+
+ priv:
+ description:
+ - "MySQL privileges string in the format: C(db.table:priv1,priv2)."
+ - "You can specify multiple privileges by separating each one using
+ a forward slash: C(db.table:priv/db.table:priv)."
+ - The format is based on MySQL C(GRANT) statement.
+ - Database and table names can be quoted, MySQL-style.
+ - If column privileges are used, the C(priv1,priv2) part must be
+ exactly as returned by a C(SHOW GRANT) statement. If not followed,
+ the module will always report changes. It includes grouping columns
+ by permission (C(SELECT(col1,col2)) instead of C(SELECT(col1),SELECT(col2))).
+ - Can be passed as a dictionary (see the examples).
+ - Supports GRANTs for procedures and functions
+ (see the examples for the M(community.mysql.mysql_user) module).
+ type: raw
+
+ append_privs:
+ description:
+ - Append the privileges defined by the I(priv) option to the existing ones
+ for this role instead of overwriting them. Mutually exclusive with I(subtract_privs).
+ type: bool
+ default: false
+
+ subtract_privs:
+ description:
+ - Revoke the privileges defined by the I(priv) option and keep other existing privileges.
+ If set, invalid privileges in I(priv) are ignored.
+ Mutually exclusive with I(append_privs).
+ version_added: '3.2.0'
+ type: bool
+ default: false
+
+ members:
+ description:
+ - List of members of the role.
+ - For users, use the format C(username@hostname).
+ Always specify the hostname part explicitly.
+ - For roles, use the format C(rolename).
+ - Mutually exclusive with I(admin).
+ type: list
+ elements: str
+
+ append_members:
+ description:
+ - Add members defined by the I(members) option to the existing ones
+ for this role instead of overwriting them.
+ - Mutually exclusive with the I(detach_members) and I(admin) option.
+ type: bool
+ default: false
+
+ detach_members:
+ description:
+ - Detaches members defined by the I(members) option from the role
+ instead of overwriting all the current members.
+ - Mutually exclusive with the I(append_members) and I(admin) option.
+ type: bool
+ default: false
+
+ set_default_role_all:
+ description:
+ - Is not supported by MariaDB and is silently ignored when working with MariaDB.
+ - If C(yes), runs B(SET DEFAULT ROLE ALL TO) each of the I(members) when changed.
+ - If you want to avoid this behavior, set this option to C(no) explicitly.
+ type: bool
+ default: true
+
+ state:
+ description:
+ - If C(present) and the role does not exist, creates the role.
+ - If C(present) and the role exists, does nothing or updates its attributes.
+ - If C(absent), removes the role.
+ type: str
+ choices: [ absent, present ]
+ default: present
+
+ check_implicit_admin:
+ description:
+ - Check if mysql allows login as root/nopassword before trying supplied credentials.
+ - If success, passed I(login_user)/I(login_password) will be ignored.
+ type: bool
+ default: false
+
+ members_must_exist:
+ description:
+ - When C(yes), the module fails if any user in I(members) does not exist.
+ - When C(no), users in I(members) which don't exist are simply skipped.
+ type: bool
+ default: true
+
+notes:
+ - Pay attention that the module runs C(SET DEFAULT ROLE ALL TO)
+ all the I(members) passed by default when the state has changed.
+ If you want to avoid this behavior, set I(set_default_role_all) to C(no).
+ - Supports C(check_mode).
+
+seealso:
+ - module: community.mysql.mysql_user
+ - name: MySQL role reference
+ description: Complete reference of the MySQL role documentation.
+ link: https://dev.mysql.com/doc/refman/8.0/en/create-role.html
+
+author:
+ - Andrew Klychkov (@Andersson007)
+ - Felix Hamme (@betanummeric)
+
+extends_documentation_fragment:
+ - community.mysql.mysql
+'''
+
+EXAMPLES = r'''
+# If you encounter the "Please explicitly state intended protocol" error,
+# use the login_unix_socket argument, for example, login_unix_socket: /run/mysqld/mysqld.sock
+
+# Example of a .my.cnf file content for setting a root password
+# [client]
+# user=root
+# password=n<_665{vS43y
+#
+# Example of a privileges dictionary passed through the priv option
+# priv:
+# 'mydb.*': 'INSERT,UPDATE'
+# 'anotherdb.*': 'SELECT'
+# 'yetanotherdb.*': 'ALL'
+#
+# You can also use the string format like in the community.mysql.mysql_user module, for example
+# mydb.*:INSERT,UPDATE/anotherdb.*:SELECT/yetanotherdb.*:ALL
+#
+# For more examples on how to specify privileges, refer to the community.mysql.mysql_user module
+
+# Create a role developers with all database privileges
+# and add alice and bob as members.
+# The statement 'SET DEFAULT ROLE ALL' to them will be run.
+- name: Create role developers, add members
+ community.mysql.mysql_role:
+ name: developers
+ state: present
+ priv: '*.*:ALL'
+ members:
+ - 'alice@%'
+ - 'bob@%'
+
+- name: Same as above but do not run SET DEFAULT ROLE ALL TO each member
+ community.mysql.mysql_role:
+ name: developers
+ state: present
+ priv: '*.*:ALL'
+ members:
+ - 'alice@%'
+ - 'bob@%'
+ set_default_role_all: false
+
+# Assuming that the role developers exists,
+# add john to the current members
+- name: Add members to an existing role
+ community.mysql.mysql_role:
+ name: developers
+ state: present
+ append_members: true
+ members:
+ - 'joe@localhost'
+
+# Create role readers with the SELECT privilege
+# on all tables in the fiction database
+- name: Create role developers, add members
+ community.mysql.mysql_role:
+ name: readers
+ state: present
+ priv: 'fiction.*:SELECT'
+
+# Assuming that the role readers exists,
+# add the UPDATE privilege to the role on all tables in the fiction database
+- name: Create role developers, add members
+ community.mysql.mysql_role:
+ name: readers
+ state: present
+ priv: 'fiction.*:UPDATE'
+ append_privs: true
+
+- name: Create role with the 'SELECT' and 'UPDATE' privileges in db1 and db2
+ community.mysql.mysql_role:
+ state: present
+ name: foo
+ priv:
+ 'db1.*': 'SELECT,UPDATE'
+ 'db2.*': 'SELECT,UPDATE'
+
+- name: Remove joe from readers
+ community.mysql.mysql_role:
+ state: present
+ name: readers
+ members:
+ - 'joe@localhost'
+ detach_members: true
+
+- name: Remove the role readers if exists
+ community.mysql.mysql_role:
+ state: absent
+ name: readers
+
+- name: Example of using login_unix_socket to connect to the server
+ community.mysql.mysql_role:
+ name: readers
+ state: present
+ login_unix_socket: /var/run/mysqld/mysqld.sock
+
+# Pay attention that the admin cannot be changed later
+# and will be ignored if a role currently exists.
+# To change members, you need to run a separate task using the admin
+# of the role as the login_user.
+- name: On MariaDB, create the role readers with alice as its admin
+ community.mysql.mysql_role:
+ state: present
+ name: readers
+ admin: 'alice@%'
+
+- name: Create the role business, add the role marketing to members
+ community.mysql.mysql_role:
+ state: present
+ name: business
+ members:
+ - marketing
+
+- name: Ensure the role foo does not have the DELETE privilege
+ community.mysql.mysql_role:
+ state: present
+ name: foo
+ subtract_privs: true
+ priv:
+ 'db1.*': DELETE
+
+- name: Add some members to a role and skip not-existent users
+ community.mysql.mysql_role:
+ state: present
+ name: foo
+ append_members: true
+ members_must_exist: false
+ members:
+ - 'existing_user@localhost'
+ - 'not_existing_user@localhost'
+
+- name: Detach some members from a role and ignore not-existent users
+ community.mysql.mysql_role:
+ state: present
+ name: foo
+ detach_members: true
+ members_must_exist: false
+ members:
+ - 'existing_user@localhost'
+ - 'not_existing_user@localhost'
+'''
+
+RETURN = '''#'''
+
+from ansible.module_utils.basic import AnsibleModule
+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.user import (
+ convert_priv_dict_to_str,
+ get_impl,
+ get_mode,
+ user_mod,
+ privileges_grant,
+ privileges_unpack,
+)
+from ansible.module_utils._text import to_native
+from ansible.module_utils.six import iteritems
+
+
+def normalize_users(module, users, is_mariadb=False):
+ """Normalize passed user names.
+
+ Example of transformation:
+ ['user0'] => [('user0', '')] / ['user0'] => [('user0', '%')]
+ ['user0@host0'] => [('user0', 'host0')]
+
+ Args:
+ module (AnsibleModule): Object of the AnsibleModule class.
+ users (list): List of user names.
+ is_mariadb (bool): Flag indicating we are working with MariaDB
+
+ Returns:
+ list: List of tuples like [('user0', ''), ('user0', 'host0')].
+ """
+ normalized_users = []
+
+ for user in users:
+ try:
+ tmp = user.split('@')
+
+ if tmp[0] == '':
+ module.fail_json(msg="Member's name cannot be empty.")
+
+ if len(tmp) == 1:
+ if not is_mariadb:
+ normalized_users.append((tmp[0], '%'))
+ else:
+ normalized_users.append((tmp[0], ''))
+
+ elif len(tmp) == 2:
+ normalized_users.append((tmp[0], tmp[1]))
+
+ except Exception as e:
+ msg = ('Error occured while parsing the name "%s": %s. '
+ 'It must be in the format "username" or '
+ '"username@hostname" ' % (user, to_native(e)))
+ module.fail_json(msg=msg)
+
+ return normalized_users
+
+
+class DbServer():
+ """Class to fetch information from a database.
+
+ Args:
+ module (AnsibleModule): Object of the AnsibleModule class.
+ cursor (cursor): Cursor object of a database Python connector.
+
+ Attributes:
+ module (AnsibleModule): Object of the AnsibleModule class.
+ cursor (cursor): Cursor object of a database Python connector.
+ role_impl (library): Corresponding library depending
+ on a server type (MariaDB or MySQL)
+ mariadb (bool): True if MariaDB, False otherwise.
+ roles_supported (bool): True if roles are supported, False otherwise.
+ users (set): Set of users existing in a DB in the form (username, hostname).
+ """
+ def __init__(self, module, cursor):
+ self.module = module
+ self.cursor = cursor
+ self.role_impl = self.get_implementation()
+ self.mariadb = self.role_impl.is_mariadb()
+ self.roles_supported = self.role_impl.supports_roles(self.cursor)
+ self.users = set(self.__get_users())
+
+ def is_mariadb(self):
+ """Get info whether a DB server is a MariaDB instance.
+
+ Returns:
+ self.mariadb: Attribute value.
+ """
+ return self.mariadb
+
+ def supports_roles(self):
+ """Get info whether a DB server supports roles.
+
+ Returns:
+ self.roles_supported: Attribute value.
+ """
+ return self.roles_supported
+
+ def get_implementation(self):
+ """Get a current server implementation depending on its type.
+
+ Returns:
+ library: Depending on a server type (MySQL or MariaDB).
+ """
+ self.cursor.execute("SELECT VERSION()")
+
+ if 'mariadb' in self.cursor.fetchone()[0].lower():
+ import ansible_collections.community.mysql.plugins.module_utils.implementations.mariadb.role as role_impl
+ else:
+ import ansible_collections.community.mysql.plugins.module_utils.implementations.mysql.role as role_impl
+
+ return role_impl
+
+ def check_users_in_db(self, users):
+ """Check if users exist in a database.
+
+ Args:
+ users (list): List of tuples (username, hostname) to check.
+ """
+ for user in users:
+ if user not in self.users:
+ msg = 'User / role `%s` with host `%s` does not exist' % (user[0], user[1])
+ self.module.fail_json(msg=msg)
+
+ def filter_existing_users(self, users):
+ for user in users:
+ if user in self.users:
+ yield user
+
+ def __get_users(self):
+ """Get users.
+
+ Returns:
+ list: List of tuples (username, hostname).
+ """
+ self.cursor.execute('SELECT User, Host FROM mysql.user')
+ return self.cursor.fetchall()
+
+ def get_users(self):
+ """Get set of tuples (username, hostname) existing in a DB.
+
+ Returns:
+ self.users: Attribute value.
+ """
+ return self.users
+
+ def get_grants(self, user, host):
+ """Get grants.
+
+ Args:
+ user (str): User name
+ host (str): Host name
+
+ Returns:
+ list: List of tuples like [(grant1,), (grant2,), ... ].
+ """
+ if host:
+ self.cursor.execute('SHOW GRANTS FOR %s@%s', (user, host))
+ else:
+ self.cursor.execute('SHOW GRANTS FOR %s', (user,))
+
+ return self.cursor.fetchall()
+
+
+class MySQLQueryBuilder():
+ """Class to build and return queries specific to MySQL.
+
+ Args:
+ name (str): Role name.
+ host (str): Role host.
+
+ Attributes:
+ name (str): Role name.
+ host (str): Role host.
+ """
+ def __init__(self, name, host):
+ self.name = name
+ self.host = host
+
+ def role_exists(self):
+ """Return a query to check if a role with self.name and self.host exists in a database.
+
+ Returns:
+ tuple: (query_string, tuple_containing_parameters).
+ """
+ return 'SELECT count(*) FROM mysql.user WHERE user = %s AND host = %s', (self.name, self.host)
+
+ def role_grant(self, user):
+ """Return a query to grant a role to a user or a role.
+
+ Args:
+ user (tuple): User / role to grant the role to in the form (username, hostname).
+
+ Returns:
+ tuple: (query_string, tuple_containing_parameters).
+ """
+ if user[1]:
+ return 'GRANT %s@%s TO %s@%s', (self.name, self.host, user[0], user[1])
+ else:
+ return 'GRANT %s@%s TO %s', (self.name, self.host, user[0])
+
+ def role_revoke(self, user):
+ """Return a query to revoke a role from a user or role.
+
+ Args:
+ user (tuple): User / role to revoke the role from in the form (username, hostname).
+
+ Returns:
+ tuple: (query_string, tuple_containing_parameters).
+ """
+ if user[1]:
+ return 'REVOKE %s@%s FROM %s@%s', (self.name, self.host, user[0], user[1])
+ else:
+ return 'REVOKE %s@%s FROM %s', (self.name, self.host, user[0])
+
+ def role_create(self, admin=None):
+ """Return a query to create a role.
+
+ Args:
+ admin (tuple): Admin user in the form (username, hostname).
+ Because it is not supported by MySQL, we ignore it.
+
+ Returns:
+ tuple: (query_string, tuple_containing_parameters).
+ """
+ return 'CREATE ROLE %s', (self.name,)
+
+
+class MariaDBQueryBuilder():
+ """Class to build and return queries specific to MariaDB.
+
+ Args:
+ name (str): Role name.
+
+ Attributes:
+ name (str): Role name.
+ """
+ def __init__(self, name):
+ self.name = name
+
+ def role_exists(self):
+ """Return a query to check if a role with self.name exists in a database.
+
+ Returns:
+ tuple: (query_string, tuple_containing_parameters).
+ """
+ return "SELECT count(*) FROM mysql.user WHERE user = %s AND is_role = 'Y'", (self.name,)
+
+ def role_grant(self, user):
+ """Return a query to grant a role to a user or role.
+
+ Args:
+ user (tuple): User / role to grant the role to in the form (username, hostname).
+
+ Returns:
+ tuple: (query_string, tuple_containing_parameters).
+ """
+ if user[1]:
+ return 'GRANT %s TO %s@%s', (self.name, user[0], user[1])
+ else:
+ return 'GRANT %s TO %s', (self.name, user[0])
+
+ def role_revoke(self, user):
+ """Return a query to revoke a role from a user or role.
+
+ Args:
+ user (tuple): User / role to revoke the role from in the form (username, hostname).
+
+ Returns:
+ tuple: (query_string, tuple_containing_parameters).
+ """
+ if user[1]:
+ return 'REVOKE %s FROM %s@%s', (self.name, user[0], user[1])
+ else:
+ return 'REVOKE %s FROM %s', (self.name, user[0])
+
+ def role_create(self, admin=None):
+ """Return a query to create a role.
+
+ Args:
+ admin (tuple): Admin user in the form (username, hostname).
+
+ Returns:
+ tuple: (query_string, tuple_containing_parameters).
+ """
+ if not admin:
+ return 'CREATE ROLE %s', (self.name,)
+
+ if admin[1]:
+ return 'CREATE ROLE %s WITH ADMIN %s@%s', (self.name, admin[0], admin[1])
+ else:
+ return 'CREATE ROLE %s WITH ADMIN %s', (self.name, admin[0])
+
+
+class MySQLRoleImpl():
+ """Class to work with MySQL role implementation.
+
+ Args:
+ module (AnsibleModule): Object of the AnsibleModule class.
+ cursor (cursor): Cursor object of a database Python connector.
+ name (str): Role name.
+ host (str): Role host.
+
+ Attributes:
+ module (AnsibleModule): Object of the AnsibleModule class.
+ cursor (cursor): Cursor object of a database Python connector.
+ name (str): Role name.
+ host (str): Role host.
+ """
+ def __init__(self, module, cursor, name, host):
+ self.module = module
+ self.cursor = cursor
+ self.name = name
+ self.host = host
+
+ def set_default_role_all(self, user):
+ """Run 'SET DEFAULT ROLE ALL TO' a user.
+
+ Args:
+ user (tuple): User / role to run the command against in the form (username, hostname).
+ """
+ if user[1]:
+ self.cursor.execute('SET DEFAULT ROLE ALL TO %s@%s', (user[0], user[1]))
+ else:
+ self.cursor.execute('SET DEFAULT ROLE ALL TO %s', (user[0],))
+
+ def get_admin(self):
+ """Get a current admin of a role.
+
+ Not supported by MySQL, so ignored here.
+ """
+ pass
+
+ def set_admin(self, admin):
+ """Set an admin of a role.
+
+ Not supported by MySQL, so ignored here.
+
+ TODO: Implement the feature if this gets supported.
+
+ Args:
+ admin (tuple): Admin user of the role in the form (username, hostname).
+ """
+ pass
+
+
+class MariaDBRoleImpl():
+ """Class to work with MariaDB role implementation.
+
+ Args:
+ module (AnsibleModule): Object of the AnsibleModule class.
+ cursor (cursor): Cursor object of a database Python connector.
+ name (str): Role name.
+
+ Attributes:
+ module (AnsibleModule): Object of the AnsibleModule class.
+ cursor (cursor): Cursor object of a database Python connector.
+ name (str): Role name.
+ """
+ def __init__(self, module, cursor, name):
+ self.module = module
+ self.cursor = cursor
+ self.name = name
+
+ def set_default_role_all(self, user):
+ """Run 'SET DEFAULT ROLE ALL TO' a user.
+
+ The command is not supported by MariaDB, ignored.
+
+ Args:
+ user (tuple): User / role to run the command against in the form (username, hostname).
+ """
+ pass
+
+ def get_admin(self):
+ """Get a current admin of a role.
+
+ Returns:
+ tuple: Of the form (username, hostname).
+ """
+ query = ("SELECT User, Host FROM mysql.roles_mapping "
+ "WHERE Role = %s and Admin_option = 'Y'")
+
+ self.cursor.execute(query, (self.name,))
+ return self.cursor.fetchone()
+
+ def set_admin(self, admin):
+ """Set an admin of a role.
+
+ TODO: Implement changing when ALTER ROLE statement to
+ change role's admin gets supported.
+
+ Args:
+ admin (tuple): Admin user of the role in the form (username, hostname).
+ """
+ admin_user = admin[0]
+ admin_host = admin[1]
+ current_admin = self.get_admin()
+
+ if (admin_user, admin_host) != current_admin:
+ msg = ('The "admin" option value and the current '
+ 'roles admin (%s@%s) don not match. Ignored. '
+ 'To change the admin, you need to drop and create the '
+ 'role again.' % (current_admin[0], current_admin[1]))
+ self.module.warn(msg)
+
+
+class Role():
+ """Class to work with MySQL role objects.
+
+ Args:
+ module (AnsibleModule): Object of the AnsibleModule class.
+ cursor (cursor): Cursor object of a database Python connector.
+ name (str): Role name.
+ server (DbServer): Object of the DbServer class.
+
+ Attributes:
+ module (AnsibleModule): Object of the AnsibleModule class.
+ cursor (cursor): Cursor object of a database Python connector.
+ name (str): Role name.
+ server (DbServer): Object of the DbServer class.
+ host (str): Role's host.
+ full_name (str): Role's full name.
+ exists (bool): Indicates if a role exists or not.
+ members (set): Set of current role's members.
+ """
+ def __init__(self, module, cursor, name, server):
+ self.module = module
+ self.cursor = cursor
+ self.name = name
+ self.server = server
+ self.is_mariadb = self.server.is_mariadb()
+
+ if self.is_mariadb:
+ self.q_builder = MariaDBQueryBuilder(self.name)
+ self.role_impl = MariaDBRoleImpl(self.module, self.cursor, self.name)
+ self.full_name = '`%s`' % self.name
+ self.host = ''
+ else:
+ self.host = '%'
+ self.q_builder = MySQLQueryBuilder(self.name, self.host)
+ self.role_impl = MySQLRoleImpl(self.module, self.cursor, self.name, self.host)
+ self.full_name = '`%s`@`%s`' % (self.name, self.host)
+
+ self.exists = self.__role_exists()
+ self.members = set()
+
+ if self.exists:
+ self.members = self.__get_members()
+
+ def __role_exists(self):
+ """Check if a role exists.
+
+ Returns:
+ bool: True if the role exists, False if it does not.
+ """
+ self.cursor.execute(*self.q_builder.role_exists())
+ return self.cursor.fetchone()[0] > 0
+
+ def add(self, users, privs, check_mode=False, admin=False,
+ set_default_role_all=True):
+ """Add a role.
+
+ Args:
+ users (list): Role members.
+ privs (str): String containing privileges.
+ check_mode (bool): If True, just checks and does nothing.
+ admin (tuple): Role's admin. Contains (username, hostname).
+ set_default_role_all (bool): If True, runs SET DEFAULT ROLE ALL TO each member.
+
+ Returns:
+ bool: True if the state has changed, False if has not.
+ """
+ if check_mode:
+ if not self.exists:
+ return True
+ return False
+
+ self.cursor.execute(*self.q_builder.role_create(admin))
+
+ if users:
+ self.update_members(users, set_default_role_all=set_default_role_all)
+
+ if privs:
+ for db_table, priv in iteritems(privs):
+ privileges_grant(self.cursor, self.name, self.host,
+ db_table, priv, tls_requires=None,
+ maria_role=self.is_mariadb)
+
+ return True
+
+ def drop(self, check_mode=False):
+ """Drop a role.
+
+ Args:
+ check_mode (bool): If True, just checks and does nothing.
+
+ Returns:
+ bool: True if the state has changed, False if has not.
+ """
+ if not self.exists:
+ return False
+
+ if check_mode and self.exists:
+ return True
+
+ self.cursor.execute('DROP ROLE %s', (self.name,))
+ return True
+
+ def update_members(self, users, check_mode=False, append_members=False,
+ set_default_role_all=True):
+ """Add users to a role.
+
+ Args:
+ users (list): Role members.
+ check_mode (bool): If True, just checks and does nothing.
+ append_members (bool): If True, adds new members passed through users
+ not touching current members.
+ set_default_role_all (bool): If True, runs SET DEFAULT ROLE ALL TO each member.
+
+ Returns:
+ bool: True if the state has changed, False if has not.
+ """
+ if not users:
+ return False
+
+ changed = False
+ for user in users:
+ if user not in self.members:
+ if check_mode:
+ return True
+
+ self.cursor.execute(*self.q_builder.role_grant(user))
+
+ if set_default_role_all:
+ self.role_impl.set_default_role_all(user)
+
+ changed = True
+
+ if append_members:
+ return changed
+
+ for user in self.members:
+ if user not in users and user != ('root', 'localhost'):
+ changed = self.__remove_member(user, check_mode)
+
+ return changed
+
+ def remove_members(self, users, check_mode=False):
+ """Remove members from a role.
+
+ Args:
+ users (list): Role members.
+ check_mode (bool): If True, just checks and does nothing.
+
+ Returns:
+ bool: True if the state has changed, False if has not.
+ """
+ if not users:
+ return False
+
+ changed = False
+ for user in users:
+ if user in self.members:
+ changed = self.__remove_member(user, check_mode)
+
+ return changed
+
+ def __remove_member(self, user, check_mode=False):
+ """Remove a member from a role.
+
+ Args:
+ user (str): Role member to remove.
+ check_mode (bool): If True, just returns True and does nothing.
+
+ Returns:
+ bool: True if the state has changed, False if has not.
+ """
+ if check_mode:
+ return True
+
+ self.cursor.execute(*self.q_builder.role_revoke(user))
+
+ return True
+
+ def update(self, users, privs, check_mode=False,
+ append_privs=False, subtract_privs=False,
+ append_members=False, detach_members=False,
+ admin=False, set_default_role_all=True):
+ """Update a role.
+
+ Update a role if needed.
+
+ Todo: Implement changing of role's admin when ALTER ROLE statement
+ to do that gets supported.
+
+ Args:
+ users (list): Role members.
+ privs (str): String containing privileges.
+ check_mode (bool): If True, just checks and does nothing.
+ append_privs (bool): If True, adds new privileges passed through privs
+ not touching current privileges.
+ subtract_privs (bool): If True, revoke the privileges passed through privs
+ not touching other existing privileges.
+ append_members (bool): If True, adds new members passed through users
+ not touching current members.
+ detach_members (bool): If True, removes members passed through users from a role.
+ admin (tuple): Role's admin. Contains (username, hostname).
+ set_default_role_all (bool): If True, runs SET DEFAULT ROLE ALL TO each member.
+
+ Returns:
+ bool: True if the state has changed, False if has not.
+ """
+ changed = False
+ members_changed = False
+
+ if users:
+ if detach_members:
+ members_changed = self.remove_members(users, check_mode=check_mode)
+
+ else:
+ members_changed = self.update_members(users, check_mode=check_mode,
+ append_members=append_members,
+ set_default_role_all=set_default_role_all)
+
+ if privs:
+ result = user_mod(self.cursor, self.name, self.host,
+ None, None, None, None, None, None,
+ privs, append_privs, subtract_privs, None,
+ self.module, role=True, maria_role=self.is_mariadb)
+ changed = result['changed']
+
+ if admin:
+ self.role_impl.set_admin(admin)
+
+ changed = changed or members_changed
+
+ return changed
+
+ def __get_members(self):
+ """Get current role's members.
+
+ Returns:
+ set: Members.
+ """
+ if self.is_mariadb:
+ self.cursor.execute('select user, host from mysql.roles_mapping where role = %s', (self.name,))
+ else:
+ self.cursor.execute('select TO_USER as user, TO_HOST as host from mysql.role_edges where FROM_USER = %s', (self.name,))
+ return set(self.cursor.fetchall())
+
+
+def main():
+ argument_spec = mysql_common_argument_spec()
+ argument_spec.update(
+ name=dict(type='str', required=True),
+ state=dict(type='str', default='present', choices=['absent', 'present']),
+ admin=dict(type='str'),
+ priv=dict(type='raw'),
+ append_privs=dict(type='bool', default=False),
+ subtract_privs=dict(type='bool', default=False),
+ members=dict(type='list', elements='str'),
+ append_members=dict(type='bool', default=False),
+ detach_members=dict(type='bool', default=False),
+ check_implicit_admin=dict(type='bool', default=False),
+ set_default_role_all=dict(type='bool', default=True),
+ members_must_exist=dict(type='bool', default=True)
+ )
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ mutually_exclusive=(
+ ('append_members', 'detach_members'),
+ ('admin', 'members'),
+ ('admin', 'append_members'),
+ ('admin', 'detach_members'),
+ ('append_privs', 'subtract_privs'),
+ ),
+ )
+
+ login_user = module.params['login_user']
+ login_password = module.params['login_password']
+ name = module.params['name']
+ state = module.params['state']
+ admin = module.params['admin']
+ priv = module.params['priv']
+ check_implicit_admin = module.params['check_implicit_admin']
+ connect_timeout = module.params['connect_timeout']
+ config_file = module.params['config_file']
+ append_privs = module.params['append_privs']
+ subtract_privs = module.boolean(module.params['subtract_privs'])
+ members = module.params['members']
+ append_members = module.params['append_members']
+ detach_members = module.params['detach_members']
+ ssl_cert = module.params['client_cert']
+ ssl_key = module.params['client_key']
+ ssl_ca = module.params['ca_cert']
+ check_hostname = module.params['check_hostname']
+ db = ''
+ set_default_role_all = module.params['set_default_role_all']
+ members_must_exist = module.params['members_must_exist']
+
+ if priv and not isinstance(priv, (str, dict)):
+ msg = ('The "priv" parameter must be str or dict '
+ 'but %s was passed' % type(priv))
+ module.fail_json(msg=msg)
+
+ if priv and isinstance(priv, dict):
+ priv = convert_priv_dict_to_str(priv)
+
+ if mysql_driver is None:
+ module.fail_json(msg=mysql_driver_fail_msg)
+
+ cursor = None
+ try:
+ if check_implicit_admin:
+ try:
+ cursor, db_conn = mysql_connect(module, 'root', '', config_file,
+ ssl_cert, ssl_key, ssl_ca, db,
+ connect_timeout=connect_timeout,
+ check_hostname=check_hostname,
+ autocommit=True)
+ except Exception:
+ pass
+
+ if not cursor:
+ cursor, db_conn = mysql_connect(module, login_user, login_password,
+ config_file, ssl_cert, ssl_key,
+ ssl_ca, db, connect_timeout=connect_timeout,
+ check_hostname=check_hostname,
+ autocommit=True)
+
+ except Exception as e:
+ module.fail_json(msg='unable to connect to database, '
+ 'check login_user and login_password '
+ 'are correct or %s has the credentials. '
+ 'Exception message: %s' % (config_file, to_native(e)))
+
+ # Set defaults
+ changed = False
+
+ get_impl(cursor)
+
+ if priv is not None:
+ try:
+ mode = get_mode(cursor)
+ except Exception as e:
+ module.fail_json(msg=to_native(e))
+
+ try:
+ priv = privileges_unpack(priv, mode, ensure_usage=not subtract_privs)
+ except Exception as e:
+ module.fail_json(msg='Invalid privileges string: %s' % to_native(e))
+
+ server = DbServer(module, cursor)
+
+ # Check if the server supports roles
+ if not server.supports_roles():
+ msg = ('Roles are not supported by the server. '
+ 'Minimal versions are MySQL 8.0.0 or MariaDB 10.0.5.')
+ module.fail_json(msg=msg)
+
+ if admin:
+ if not server.is_mariadb():
+ module.fail_json(msg='The "admin" option can be used only with MariaDB.')
+
+ admin = normalize_users(module, [admin])[0]
+ server.check_users_in_db([admin])
+
+ if members:
+ members = normalize_users(module, members, server.is_mariadb())
+ if members_must_exist:
+ server.check_users_in_db(members)
+ else:
+ members = list(server.filter_existing_users(members))
+
+ # Main job starts here
+ role = Role(module, cursor, name, server)
+
+ try:
+ if state == 'present':
+ if not role.exists:
+ if subtract_privs:
+ priv = None # avoid granting unwanted privileges
+ if detach_members:
+ members = None # avoid adding unwanted members
+ changed = role.add(members, priv, module.check_mode, admin,
+ set_default_role_all)
+
+ else:
+ changed = role.update(members, priv, module.check_mode, append_privs, subtract_privs,
+ append_members, detach_members, admin,
+ set_default_role_all)
+
+ elif state == 'absent':
+ changed = role.drop(module.check_mode)
+
+ except Exception as e:
+ module.fail_json(msg=to_native(e))
+
+ module.exit_json(changed=changed)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/mysql/plugins/modules/mysql_user.py b/ansible_collections/community/mysql/plugins/modules/mysql_user.py
new file mode 100644
index 000000000..e87fe12db
--- /dev/null
+++ b/ansible_collections/community/mysql/plugins/modules/mysql_user.py
@@ -0,0 +1,528 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2012, Mark Theunissen <mark.theunissen@gmail.com>
+# Sponsored by Four Kitchens http://fourkitchens.com.
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = r'''
+---
+module: mysql_user
+short_description: Adds or removes a user from a MySQL database
+description:
+ - Adds or removes a user from a MySQL database.
+options:
+ name:
+ description:
+ - Name of the user (role) to add or remove.
+ type: str
+ required: true
+ password:
+ description:
+ - Set the user's password. Only for C(mysql_native_password) authentication.
+ For other authentication plugins see the combination of I(plugin), I(plugin_hash_string), I(plugin_auth_string).
+ type: str
+ encrypted:
+ description:
+ - Indicate that the 'password' field is a `mysql_native_password` hash.
+ type: bool
+ default: false
+ host:
+ description:
+ - The 'host' part of the MySQL username.
+ type: str
+ default: localhost
+ host_all:
+ description:
+ - Override the host option, making ansible apply changes
+ to all hostnames for a given user.
+ - This option cannot be used when creating users.
+ type: bool
+ default: false
+ priv:
+ description:
+ - "MySQL privileges string in the format: C(db.table:priv1,priv2)."
+ - "Multiple privileges can be specified by separating each one using
+ a forward slash: C(db.table1:priv/db.table2:priv)."
+ - The format is based on MySQL C(GRANT) statement.
+ - Database and table names can be quoted, MySQL-style.
+ - If column privileges are used, the C(priv1,priv2) part must be
+ exactly as returned by a C(SHOW GRANT) statement. If not followed,
+ the module will always report changes. It includes grouping columns
+ by permission (C(SELECT(col1,col2)) instead of C(SELECT(col1),SELECT(col2))).
+ - Can be passed as a dictionary (see the examples).
+ - Supports GRANTs for procedures and functions (see the examples).
+ - "Note: If you pass the same C(db.table) combination to this parameter
+ two or more times with different privileges,
+ for example, C('*.*:SELECT/*.*:SHOW VIEW'), only the last one will be applied,
+ in this example, it will be C(SHOW VIEW) respectively.
+ Use C('*.*:SELECT,SHOW VIEW') instead to apply both."
+ type: raw
+ append_privs:
+ description:
+ - Append the privileges defined by priv to the existing ones for this
+ user instead of overwriting existing ones. Mutually exclusive with I(subtract_privs).
+ type: bool
+ default: false
+ subtract_privs:
+ description:
+ - Revoke the privileges defined by the I(priv) option and keep other existing privileges.
+ If set, invalid privileges in I(priv) are ignored.
+ Mutually exclusive with I(append_privs).
+ version_added: '3.2.0'
+ type: bool
+ default: false
+ tls_requires:
+ description:
+ - Set requirement for secure transport as a dictionary of requirements (see the examples).
+ - Valid requirements are SSL, X509, SUBJECT, ISSUER, CIPHER.
+ - SUBJECT, ISSUER and CIPHER are complementary, and mutually exclusive with SSL and X509.
+ - U(https://mariadb.com/kb/en/securing-connections-for-client-and-server/#requiring-tls).
+ type: dict
+ version_added: 1.0.0
+ sql_log_bin:
+ description:
+ - Whether binary logging should be enabled or disabled for the connection.
+ type: bool
+ default: true
+ force_context:
+ description:
+ - Sets the С(mysql) system database as context for the executed statements (it will be used
+ as a database to connect to). Useful if you use binlog / replication filters in MySQL as
+ per default the statements can not be caught by a binlog / replication filter, they require
+ a database to be set to work, otherwise the replication can break down.
+ - See U(https://dev.mysql.com/doc/refman/8.0/en/replication-options-binary-log.html#option_mysqld_binlog-ignore-db)
+ for a description on how binlog filters work (filtering on the primary).
+ - See U(https://dev.mysql.com/doc/refman/8.0/en/replication-options-replica.html#option_mysqld_replicate-ignore-db)
+ for a description on how replication filters work (filtering on the replica).
+ type: bool
+ default: false
+ version_added: '3.1.0'
+ state:
+ description:
+ - Whether the user should exist.
+ - When C(absent), removes the user.
+ type: str
+ choices: [ absent, present ]
+ default: present
+ check_implicit_admin:
+ description:
+ - Check if mysql allows login as root/nopassword before trying supplied credentials.
+ - If success, passed I(login_user)/I(login_password) will be ignored.
+ type: bool
+ default: false
+ update_password:
+ description:
+ - C(always) will update passwords if they differ. This affects I(password) and the combination of I(plugin), I(plugin_hash_string), I(plugin_auth_string).
+ - C(on_create) will only set the password or the combination of I(plugin), I(plugin_hash_string), I(plugin_auth_string) for newly created users.
+ - "C(on_new_username) works like C(on_create), but it tries to reuse an existing password: If one different user
+ with the same username exists, or multiple different users with the same username and equal C(plugin) and
+ C(authentication_string) attribute, the existing C(plugin) and C(authentication_string) are used for the
+ new user instead of the I(password), I(plugin), I(plugin_hash_string) or I(plugin_auth_string) argument."
+ type: str
+ choices: [ always, on_create, on_new_username ]
+ default: always
+ plugin:
+ description:
+ - User's plugin to authenticate (``CREATE USER user IDENTIFIED WITH plugin``).
+ type: str
+ version_added: '0.1.0'
+ plugin_hash_string:
+ description:
+ - User's plugin hash string (``CREATE USER user IDENTIFIED WITH plugin AS plugin_hash_string``).
+ type: str
+ version_added: '0.1.0'
+ plugin_auth_string:
+ description:
+ - User's plugin auth_string (``CREATE USER user IDENTIFIED WITH plugin BY plugin_auth_string``).
+ - If I(plugin) is ``pam`` (MariaDB) or ``auth_pam`` (MySQL) an optional I(plugin_auth_string) can be used to choose a specific PAM service.
+ type: str
+ version_added: '0.1.0'
+ resource_limits:
+ description:
+ - Limit the user for certain server resources. Provided since MySQL 5.6 / MariaDB 10.2.
+ - "Available options are C(MAX_QUERIES_PER_HOUR: num), C(MAX_UPDATES_PER_HOUR: num),
+ C(MAX_CONNECTIONS_PER_HOUR: num), C(MAX_USER_CONNECTIONS: num), C(MAX_STATEMENT_TIME: num) (supported only for MariaDB since collection version 3.7.0)."
+ - Used when I(state=present), ignored otherwise.
+ type: dict
+ version_added: '0.1.0'
+ session_vars:
+ description:
+ - "Dictionary of session variables in form of C(variable: value) to set at the beginning of module execution."
+ - Cannot be used to set global variables, use the M(community.mysql.mysql_variables) module instead.
+ type: dict
+ version_added: '3.6.0'
+
+notes:
+ - "MySQL server installs with default I(login_user) of C(root) and no password.
+ To secure this user as part of an idempotent playbook, you must create at least two tasks:
+ 1) change the root user's password, without providing any I(login_user)/I(login_password) details,
+ 2) drop a C(~/.my.cnf) file containing the new root credentials.
+ Subsequent runs of the playbook will then succeed by reading the new credentials from the file."
+ - Currently, there is only support for the C(mysql_native_password) encrypted password hash module.
+ - Supports (check_mode).
+
+seealso:
+- module: community.mysql.mysql_info
+- name: MySQL access control and account management reference
+ description: Complete reference of the MySQL access control and account management documentation.
+ link: https://dev.mysql.com/doc/refman/8.0/en/access-control.html
+- name: MySQL provided privileges reference
+ description: Complete reference of the MySQL provided privileges documentation.
+ link: https://dev.mysql.com/doc/refman/8.0/en/privileges-provided.html
+
+author:
+- Jonathan Mainguy (@Jmainguy)
+- Benjamin Malynovytch (@bmalynovytch)
+- Lukasz Tomaszkiewicz (@tomaszkiewicz)
+extends_documentation_fragment:
+- community.mysql.mysql
+
+'''
+
+EXAMPLES = r'''
+# If you encounter the "Please explicitly state intended protocol" error,
+# use the login_unix_socket argument
+- name: Removes anonymous user account for localhost
+ community.mysql.mysql_user:
+ name: ''
+ host: localhost
+ state: absent
+ login_unix_socket: /run/mysqld/mysqld.sock
+
+- name: Removes all anonymous user accounts
+ community.mysql.mysql_user:
+ name: ''
+ host_all: true
+ state: absent
+
+- name: Create database user with name 'bob' and password '12345' with all database privileges
+ community.mysql.mysql_user:
+ name: bob
+ password: 12345
+ priv: '*.*:ALL'
+ state: present
+
+- name: Create database user using hashed password with all database privileges
+ community.mysql.mysql_user:
+ name: bob
+ password: '*EE0D72C1085C46C5278932678FBE2C6A782821B4'
+ encrypted: true
+ priv: '*.*:ALL'
+ state: present
+
+# Set session var wsrep_on=off before creating the user
+- name: Create database user with password and all database privileges and 'WITH GRANT OPTION'
+ community.mysql.mysql_user:
+ name: bob
+ password: 12345
+ priv: '*.*:ALL,GRANT'
+ state: present
+ session_vars:
+ wsrep_on: off
+
+- name: Create user with password, all database privileges and 'WITH GRANT OPTION' in db1 and db2
+ community.mysql.mysql_user:
+ state: present
+ name: bob
+ password: 12345dd
+ priv:
+ 'db1.*': 'ALL,GRANT'
+ 'db2.*': 'ALL,GRANT'
+
+# Use 'PROCEDURE' instead of 'FUNCTION' to apply GRANTs for a MySQL procedure instead.
+- name: Grant a user the right to execute a function
+ community.mysql.mysql_user:
+ name: readonly
+ password: 12345
+ priv:
+ FUNCTION my_db.my_function: EXECUTE
+ state: present
+
+- name: Modify user to require TLS connection with a valid client certificate
+ community.mysql.mysql_user:
+ name: bob
+ tls_requires:
+ x509:
+ state: present
+
+- name: Modify user to require TLS connection with a specific client certificate and cipher
+ community.mysql.mysql_user:
+ name: bob
+ tls_requires:
+ subject: '/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland'
+ cipher: 'ECDHE-ECDSA-AES256-SHA384'
+
+- name: Modify user to no longer require SSL
+ community.mysql.mysql_user:
+ name: bob
+ tls_requires:
+
+- name: Ensure no user named 'sally'@'localhost' exists, also passing in the auth credentials
+ community.mysql.mysql_user:
+ login_user: root
+ login_password: 123456
+ name: sally
+ state: absent
+
+# check_implicit_admin example
+- name: >
+ Ensure no user named 'sally'@'localhost' exists, also passing in the auth credentials.
+ If mysql allows root/nopassword login, try it without the credentials first.
+ If it's not allowed, pass the credentials
+ community.mysql.mysql_user:
+ check_implicit_admin: true
+ login_user: root
+ login_password: 123456
+ name: sally
+ state: absent
+
+- name: Ensure no user named 'sally' exists at all
+ community.mysql.mysql_user:
+ name: sally
+ host_all: true
+ state: absent
+
+- name: Specify grants composed of more than one word
+ community.mysql.mysql_user:
+ name: replication
+ password: 12345
+ priv: "*.*:REPLICATION CLIENT"
+ state: present
+
+- name: Revoke all privileges for user 'bob' and password '12345'
+ community.mysql.mysql_user:
+ name: bob
+ password: 12345
+ priv: "*.*:USAGE"
+ state: present
+
+# Example privileges string format
+# mydb.*:INSERT,UPDATE/anotherdb.*:SELECT/yetanotherdb.*:ALL
+
+- name: Example using login_unix_socket to connect to server
+ community.mysql.mysql_user:
+ name: root
+ password: abc123
+ login_unix_socket: /var/run/mysqld/mysqld.sock
+
+- name: Example of skipping binary logging while adding user 'bob'
+ community.mysql.mysql_user:
+ name: bob
+ password: 12345
+ priv: "*.*:USAGE"
+ state: present
+ sql_log_bin: false
+
+- name: Create user 'bob' authenticated with plugin 'AWSAuthenticationPlugin'
+ community.mysql.mysql_user:
+ name: bob
+ plugin: AWSAuthenticationPlugin
+ plugin_hash_string: RDS
+ priv: '*.*:ALL'
+ state: present
+
+- name: Limit bob's resources to 10 queries per hour and 5 connections per hour
+ community.mysql.mysql_user:
+ name: bob
+ resource_limits:
+ MAX_QUERIES_PER_HOUR: 10
+ MAX_CONNECTIONS_PER_HOUR: 5
+
+- name: Ensure bob does not have the DELETE privilege
+ community.mysql.mysql_user:
+ name: bob
+ subtract_privs: true
+ priv:
+ 'db1.*': DELETE
+
+# Example .my.cnf file for setting the root password
+# [client]
+# user=root
+# password=n<_665{vS43y
+'''
+
+RETURN = '''#'''
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.mysql.plugins.module_utils.database import SQLParseError
+from ansible_collections.community.mysql.plugins.module_utils.mysql import (
+ mysql_connect,
+ mysql_driver,
+ mysql_driver_fail_msg,
+ mysql_common_argument_spec,
+ set_session_vars,
+)
+from ansible_collections.community.mysql.plugins.module_utils.user import (
+ convert_priv_dict_to_str,
+ get_impl,
+ get_mode,
+ InvalidPrivsError,
+ limit_resources,
+ privileges_unpack,
+ sanitize_requires,
+ user_add,
+ user_delete,
+ user_exists,
+ user_mod,
+)
+from ansible.module_utils._text import to_native
+
+
+# ===========================================
+# Module execution.
+#
+
+
+def main():
+ argument_spec = mysql_common_argument_spec()
+ argument_spec.update(
+ user=dict(type='str', required=True, aliases=['name']),
+ password=dict(type='str', no_log=True),
+ encrypted=dict(type='bool', default=False),
+ host=dict(type='str', default='localhost'),
+ host_all=dict(type="bool", default=False),
+ state=dict(type='str', default='present', choices=['absent', 'present']),
+ priv=dict(type='raw'),
+ tls_requires=dict(type='dict'),
+ append_privs=dict(type='bool', default=False),
+ subtract_privs=dict(type='bool', default=False),
+ check_implicit_admin=dict(type='bool', default=False),
+ update_password=dict(type='str', default='always', choices=['always', 'on_create', 'on_new_username'], no_log=False),
+ sql_log_bin=dict(type='bool', default=True),
+ plugin=dict(default=None, type='str'),
+ plugin_hash_string=dict(default=None, type='str'),
+ plugin_auth_string=dict(default=None, type='str'),
+ resource_limits=dict(type='dict'),
+ force_context=dict(type='bool', default=False),
+ session_vars=dict(type='dict'),
+ )
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ supports_check_mode=True,
+ mutually_exclusive=(('append_privs', 'subtract_privs'),)
+ )
+ login_user = module.params["login_user"]
+ login_password = module.params["login_password"]
+ user = module.params["user"]
+ password = module.params["password"]
+ encrypted = module.boolean(module.params["encrypted"])
+ host = module.params["host"].lower()
+ host_all = module.params["host_all"]
+ state = module.params["state"]
+ priv = module.params["priv"]
+ tls_requires = sanitize_requires(module.params["tls_requires"])
+ check_implicit_admin = module.params["check_implicit_admin"]
+ connect_timeout = module.params["connect_timeout"]
+ config_file = module.params["config_file"]
+ append_privs = module.boolean(module.params["append_privs"])
+ subtract_privs = module.boolean(module.params['subtract_privs'])
+ update_password = module.params['update_password']
+ ssl_cert = module.params["client_cert"]
+ ssl_key = module.params["client_key"]
+ ssl_ca = module.params["ca_cert"]
+ check_hostname = module.params["check_hostname"]
+ db = ''
+ if module.params["force_context"]:
+ db = 'mysql'
+ sql_log_bin = module.params["sql_log_bin"]
+ plugin = module.params["plugin"]
+ plugin_hash_string = module.params["plugin_hash_string"]
+ plugin_auth_string = module.params["plugin_auth_string"]
+ resource_limits = module.params["resource_limits"]
+ session_vars = module.params["session_vars"]
+
+ if priv and not isinstance(priv, (str, dict)):
+ module.fail_json(msg="priv parameter must be str or dict but %s was passed" % type(priv))
+
+ if priv and isinstance(priv, dict):
+ priv = convert_priv_dict_to_str(priv)
+
+ if mysql_driver is None:
+ module.fail_json(msg=mysql_driver_fail_msg)
+
+ cursor = None
+ try:
+ if check_implicit_admin:
+ try:
+ cursor, db_conn = mysql_connect(module, "root", "", config_file, ssl_cert, ssl_key, ssl_ca, db,
+ connect_timeout=connect_timeout, check_hostname=check_hostname, autocommit=True)
+ except Exception:
+ pass
+
+ if not cursor:
+ cursor, db_conn = mysql_connect(module, login_user, login_password, config_file, ssl_cert, ssl_key, ssl_ca, db,
+ connect_timeout=connect_timeout, check_hostname=check_hostname, autocommit=True)
+ except Exception as e:
+ module.fail_json(msg="unable to connect to database, check login_user and login_password are correct or %s has the credentials. "
+ "Exception message: %s" % (config_file, to_native(e)))
+
+ if not sql_log_bin:
+ cursor.execute("SET SQL_LOG_BIN=0;")
+
+ if session_vars:
+ set_session_vars(module, cursor, session_vars)
+
+ get_impl(cursor)
+
+ if priv is not None:
+ try:
+ mode = get_mode(cursor)
+ except Exception as e:
+ module.fail_json(msg=to_native(e))
+ priv = privileges_unpack(priv, mode, ensure_usage=not subtract_privs)
+ password_changed = False
+ if state == "present":
+ if user_exists(cursor, user, host, host_all):
+ try:
+ if update_password == "always":
+ result = user_mod(cursor, user, host, host_all, password, encrypted,
+ plugin, plugin_hash_string, plugin_auth_string,
+ priv, append_privs, subtract_privs, tls_requires, module)
+
+ else:
+ result = user_mod(cursor, user, host, host_all, None, encrypted,
+ None, None, None,
+ priv, append_privs, subtract_privs, tls_requires, module)
+ changed = result['changed']
+ msg = result['msg']
+ password_changed = result['password_changed']
+
+ except (SQLParseError, InvalidPrivsError, mysql_driver.Error) as e:
+ module.fail_json(msg=to_native(e))
+ else:
+ if host_all:
+ module.fail_json(msg="host_all parameter cannot be used when adding a user")
+ try:
+ if subtract_privs:
+ priv = None # avoid granting unwanted privileges
+ reuse_existing_password = update_password == 'on_new_username'
+ result = user_add(cursor, user, host, host_all, password, encrypted,
+ plugin, plugin_hash_string, plugin_auth_string,
+ priv, tls_requires, module.check_mode, reuse_existing_password)
+ changed = result['changed']
+ password_changed = result['password_changed']
+ if changed:
+ msg = "User added"
+
+ except (SQLParseError, InvalidPrivsError, mysql_driver.Error) as e:
+ module.fail_json(msg=to_native(e))
+
+ if resource_limits:
+ changed = limit_resources(module, cursor, user, host, resource_limits, module.check_mode) or changed
+
+ elif state == "absent":
+ if user_exists(cursor, user, host, host_all):
+ changed = user_delete(cursor, user, host, host_all, module.check_mode)
+ msg = "User deleted"
+ else:
+ changed = False
+ msg = "User doesn't exist"
+ module.exit_json(changed=changed, user=user, msg=msg, password_changed=password_changed)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/mysql/plugins/modules/mysql_variables.py b/ansible_collections/community/mysql/plugins/modules/mysql_variables.py
new file mode 100644
index 000000000..f404d5aab
--- /dev/null
+++ b/ansible_collections/community/mysql/plugins/modules/mysql_variables.py
@@ -0,0 +1,271 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright: (c) 2013, Balazs Pocze <banyek@gawker.com>
+# Certain parts are taken from Mark Theunissen's mysqldb module
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = r'''
+---
+module: mysql_variables
+
+short_description: Manage MySQL global variables
+description:
+- Query / Set MySQL variables.
+author:
+- Balazs Pocze (@banyek)
+options:
+ variable:
+ description:
+ - Variable name to operate.
+ type: str
+ required: true
+ value:
+ description:
+ - If set, then sets variable value to this.
+ type: str
+ mode:
+ description:
+ - C(global) assigns C(value) to a global system variable which will be changed at runtime
+ but won't persist across server restarts.
+ - C(persist) assigns C(value) to a global system variable and persists it to
+ the mysqld-auto.cnf option file in the data directory
+ (the variable will survive service restarts).
+ - C(persist_only) persists C(value) to the mysqld-auto.cnf option file in the data directory
+ but without setting the global variable runtime value
+ (the value will be changed after the next service restart).
+ - Supported by MySQL 8.0 or later.
+ - For more information see U(https://dev.mysql.com/doc/refman/8.0/en/set-variable.html).
+ type: str
+ choices: ['global', 'persist', 'persist_only']
+ default: global
+ version_added: '0.1.0'
+
+notes:
+- Does not support C(check_mode).
+
+seealso:
+- module: community.mysql.mysql_info
+- name: MySQL SET command reference
+ description: Complete reference of the MySQL SET command documentation.
+ link: https://dev.mysql.com/doc/refman/8.0/en/set-statement.html
+
+extends_documentation_fragment:
+- community.mysql.mysql
+'''
+
+EXAMPLES = r'''
+# If you encounter the "Please explicitly state intended protocol" error,
+# use the login_unix_socket argument
+- name: Check for sync_binlog setting
+ community.mysql.mysql_variables:
+ variable: sync_binlog
+ login_unix_socket: /run/mysqld/mysqld.sock
+
+- name: Set read_only variable to 1 persistently
+ community.mysql.mysql_variables:
+ variable: read_only
+ value: 1
+ mode: persist
+'''
+
+RETURN = r'''
+queries:
+ description: List of executed queries which modified DB's state.
+ returned: if executed
+ type: list
+ sample: ["SET GLOBAL `read_only` = 1"]
+ version_added: '0.1.0'
+'''
+
+import os
+import warnings
+from re import match
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible_collections.community.mysql.plugins.module_utils.database import SQLParseError, 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.module_utils._text import to_native
+
+executed_queries = []
+
+
+def check_mysqld_auto(module, cursor, mysqlvar):
+ """Check variable's value in mysqld-auto.cnf."""
+ query = ("SELECT VARIABLE_VALUE "
+ "FROM performance_schema.persisted_variables "
+ "WHERE VARIABLE_NAME = %s")
+ try:
+ cursor.execute(query, (mysqlvar,))
+ res = cursor.fetchone()
+ except Exception as e:
+ if "Table 'performance_schema.persisted_variables' doesn't exist" in str(e):
+ module.fail_json(msg='Server version must be 8.0 or greater.')
+
+ if res:
+ return res[0]
+ else:
+ return None
+
+
+def typedvalue(value):
+ """
+ Convert value to number whenever possible, return same value
+ otherwise.
+
+ >>> typedvalue('3')
+ 3
+ >>> typedvalue('3.0')
+ 3.0
+ >>> typedvalue('foobar')
+ 'foobar'
+
+ """
+ try:
+ return int(value)
+ except ValueError:
+ pass
+
+ try:
+ return float(value)
+ except ValueError:
+ pass
+
+ return value
+
+
+def getvariable(cursor, mysqlvar):
+ cursor.execute("SHOW VARIABLES WHERE Variable_name = %s", (mysqlvar,))
+ mysqlvar_val = cursor.fetchall()
+ if len(mysqlvar_val) == 1:
+ return mysqlvar_val[0][1]
+ else:
+ return None
+
+
+def setvariable(cursor, mysqlvar, value, mode='global'):
+ """ Set a global mysql variable to a given value
+
+ The DB driver will handle quoting of the given value based on its
+ type, thus numeric strings like '3.0' or '8' are illegal, they
+ should be passed as numeric literals.
+
+ """
+ if mode == 'persist':
+ query = "SET PERSIST %s = " % mysql_quote_identifier(mysqlvar, 'vars')
+ elif mode == 'global':
+ query = "SET GLOBAL %s = " % mysql_quote_identifier(mysqlvar, 'vars')
+ elif mode == 'persist_only':
+ query = "SET PERSIST_ONLY %s = " % mysql_quote_identifier(mysqlvar, 'vars')
+
+ try:
+ cursor.execute(query + "%s", (value,))
+ executed_queries.append(query + "%s" % value)
+ cursor.fetchall()
+ result = True
+ except Exception as e:
+ result = to_native(e)
+
+ return result
+
+
+def main():
+ argument_spec = mysql_common_argument_spec()
+ argument_spec.update(
+ variable=dict(type='str'),
+ value=dict(type='str'),
+ mode=dict(type='str', choices=['global', 'persist', 'persist_only'], default='global'),
+ )
+
+ module = AnsibleModule(
+ argument_spec=argument_spec
+ )
+ user = module.params["login_user"]
+ password = module.params["login_password"]
+ connect_timeout = module.params['connect_timeout']
+ ssl_cert = module.params["client_cert"]
+ ssl_key = module.params["client_key"]
+ ssl_ca = module.params["ca_cert"]
+ check_hostname = module.params["check_hostname"]
+ config_file = module.params['config_file']
+ db = 'mysql'
+
+ mysqlvar = module.params["variable"]
+ value = module.params["value"]
+ mode = module.params["mode"]
+
+ if mysqlvar is None:
+ module.fail_json(msg="Cannot run without variable to operate with")
+ if match('^[0-9A-Za-z_.]+$', mysqlvar) is None:
+ module.fail_json(msg="invalid variable name \"%s\"" % mysqlvar)
+ if mysql_driver is None:
+ module.fail_json(msg=mysql_driver_fail_msg)
+ else:
+ warnings.filterwarnings('error', category=mysql_driver.Warning)
+
+ try:
+ cursor, db_conn = mysql_connect(module, user, password, config_file, ssl_cert, ssl_key, ssl_ca, db,
+ connect_timeout=connect_timeout, check_hostname=check_hostname)
+ except Exception as e:
+ if os.path.exists(config_file):
+ module.fail_json(msg=("unable to connect to database, check login_user and "
+ "login_password are correct or %s has the credentials. "
+ "Exception message: %s" % (config_file, to_native(e))))
+ else:
+ module.fail_json(msg="unable to find %s. Exception message: %s" % (config_file, to_native(e)))
+
+ mysqlvar_val = None
+ var_in_mysqld_auto_cnf = None
+
+ mysqlvar_val = getvariable(cursor, mysqlvar)
+ if mysqlvar_val is None:
+ module.fail_json(msg="Variable not available \"%s\"" % mysqlvar, changed=False)
+
+ if value is None:
+ module.exit_json(msg=mysqlvar_val)
+
+ if mode in ('persist', 'persist_only'):
+ var_in_mysqld_auto_cnf = check_mysqld_auto(module, cursor, mysqlvar)
+
+ if mode == 'persist_only':
+ if var_in_mysqld_auto_cnf is None:
+ mysqlvar_val = False
+ else:
+ mysqlvar_val = var_in_mysqld_auto_cnf
+
+ # Type values before using them
+ value_wanted = typedvalue(value)
+ value_actual = typedvalue(mysqlvar_val)
+ value_in_auto_cnf = None
+ if var_in_mysqld_auto_cnf is not None:
+ value_in_auto_cnf = typedvalue(var_in_mysqld_auto_cnf)
+
+ if value_wanted == value_actual and mode in ('global', 'persist'):
+ if mode == 'persist' and value_wanted == value_in_auto_cnf:
+ module.exit_json(msg="Variable is already set to requested value globally"
+ "and stored into mysqld-auto.cnf file.", changed=False)
+
+ elif mode == 'global':
+ module.exit_json(msg="Variable is already set to requested value.", changed=False)
+
+ if mode == 'persist_only' and value_wanted == value_in_auto_cnf:
+ module.exit_json(msg="Variable is already stored into mysqld-auto.cnf "
+ "with requested value.", changed=False)
+
+ try:
+ result = setvariable(cursor, mysqlvar, value_wanted, mode)
+ except SQLParseError as e:
+ result = to_native(e)
+
+ if result is True:
+ module.exit_json(msg="Variable change succeeded prev_value=%s" % value_actual,
+ changed=True, queries=executed_queries)
+ else:
+ module.fail_json(msg=result, changed=False)
+
+
+if __name__ == '__main__':
+ main()