mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-04-25 03:41:25 -07:00
Migrating MySQL to community.mysql (#633)
* Migrating MySQL to community.mysql * Added PR to changelog * Removed missed tests * Removed missed changelog fragments * Update meta/runtime.yml Co-authored-by: Ben Mildren <bmildren@digitalocean.com> Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
parent
d0b07885f0
commit
f420e8f02e
109 changed files with 42 additions and 8303 deletions
|
@ -1,727 +0,0 @@
|
|||
#!/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: no
|
||||
quick:
|
||||
description:
|
||||
- Option used for dumping large tables.
|
||||
type: bool
|
||||
default: yes
|
||||
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
|
||||
required: no
|
||||
default: []
|
||||
hex_blob:
|
||||
description:
|
||||
- Dump binary columns using hexadecimal notation.
|
||||
required: no
|
||||
default: no
|
||||
type: bool
|
||||
version_added: '0.2.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).
|
||||
required: no
|
||||
type: bool
|
||||
default: no
|
||||
version_added: '0.2.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).
|
||||
required: no
|
||||
type: int
|
||||
choices: [0, 1, 2]
|
||||
default: 0
|
||||
version_added: '0.2.0'
|
||||
skip_lock_tables:
|
||||
description:
|
||||
- Skip locking tables for read. Used when I(state=dump), ignored otherwise.
|
||||
required: no
|
||||
type: bool
|
||||
default: no
|
||||
version_added: '0.2.0'
|
||||
dump_extra_args:
|
||||
description:
|
||||
- Provide additional arguments for mysqldump.
|
||||
Used when I(state=dump) only, ignored otherwise.
|
||||
required: no
|
||||
type: str
|
||||
version_added: '0.2.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.
|
||||
required: no
|
||||
type: bool
|
||||
default: no
|
||||
version_added: '0.2.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: no
|
||||
version_added: '0.2.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: no
|
||||
version_added: '0.2.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: no
|
||||
version_added: '0.2.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: no
|
||||
version_added: '0.2.0'
|
||||
|
||||
seealso:
|
||||
- module: community.general.mysql_info
|
||||
- module: community.general.mysql_variables
|
||||
- module: community.general.mysql_user
|
||||
- module: community.general.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:
|
||||
- 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.general.mysql
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Create a new database with name 'bobdata'
|
||||
mysql_db:
|
||||
name: bobdata
|
||||
state: present
|
||||
|
||||
- name: Create new databases with names 'foo' and 'bar'
|
||||
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
|
||||
mysql_db:
|
||||
name: my_db
|
||||
state: import
|
||||
target: /tmp/dump.sql.bz2
|
||||
|
||||
- name: Restore database ignoring errors
|
||||
mysql_db:
|
||||
name: my_db
|
||||
state: import
|
||||
target: /tmp/dump.sql.bz2
|
||||
force: yes
|
||||
|
||||
- name: Dump multiple databases
|
||||
mysql_db:
|
||||
state: dump
|
||||
name: db_1,db_2
|
||||
target: /tmp/dump.sql
|
||||
|
||||
- name: Dump multiple databases
|
||||
mysql_db:
|
||||
state: dump
|
||||
name:
|
||||
- db_1
|
||||
- db_2
|
||||
target: /tmp/dump.sql
|
||||
|
||||
- name: Dump all databases to hostname.sql
|
||||
mysql_db:
|
||||
state: dump
|
||||
name: all
|
||||
target: /tmp/dump.sql
|
||||
|
||||
- name: Dump all databases to hostname.sql including master data
|
||||
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
|
||||
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>
|
||||
mysql_db:
|
||||
state: dump
|
||||
name: db_1
|
||||
encoding: latin1
|
||||
target: /tmp/dump.sql
|
||||
|
||||
- name: Delete database with name 'bobdata'
|
||||
mysql_db:
|
||||
name: bobdata
|
||||
state: absent
|
||||
|
||||
- name: Make sure there is neither a database with name 'foo', nor one with name 'bar'
|
||||
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
|
||||
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
|
||||
mysql_db:
|
||||
check_implicit_admin: yes
|
||||
login_user: bob
|
||||
login_password: 123456
|
||||
name: bobdata
|
||||
state: present
|
||||
'''
|
||||
|
||||
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.2.0'
|
||||
'''
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.database import mysql_quote_identifier
|
||||
from ansible_collections.community.general.plugins.module_utils.mysql import mysql_connect, mysql_driver, mysql_driver_fail_msg
|
||||
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("SHOW DATABASES LIKE %s", (each_db.replace("_", r"\_"),))
|
||||
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):
|
||||
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))
|
||||
else:
|
||||
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_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 += " --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:
|
||||
query = ['CREATE DATABASE %s' % mysql_quote_identifier(each_db, 'database')]
|
||||
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():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
login_user=dict(type='str'),
|
||||
login_password=dict(type='str', no_log=True),
|
||||
login_host=dict(type='str', default='localhost'),
|
||||
login_port=dict(type='int', default=3306),
|
||||
login_unix_socket=dict(type='str'),
|
||||
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']),
|
||||
client_cert=dict(type='path', aliases=['ssl_cert']),
|
||||
client_key=dict(type='path', aliases=['ssl_key']),
|
||||
ca_cert=dict(type='path', aliases=['ssl_ca']),
|
||||
connect_timeout=dict(type='int', default=30),
|
||||
config_file=dict(type='path', default='~/.my.cnf'),
|
||||
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),
|
||||
restrict_config_file=dict(type='bool', default=False),
|
||||
check_implicit_admin=dict(type='bool', default=False),
|
||||
config_overrides_defaults=dict(type='bool', default=False),
|
||||
),
|
||||
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"]
|
||||
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']
|
||||
|
||||
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,
|
||||
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)
|
||||
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)
|
||||
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()
|
|
@ -1,543 +0,0 @@
|
|||
#!/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.2.0'
|
||||
return_empty_dbs:
|
||||
description:
|
||||
- Includes names of empty databases to returned dictionary.
|
||||
type: bool
|
||||
default: no
|
||||
|
||||
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).
|
||||
|
||||
seealso:
|
||||
- module: community.general.mysql_variables
|
||||
- module: community.general.mysql_db
|
||||
- module: community.general.mysql_user
|
||||
- module: community.general.mysql_replication
|
||||
|
||||
author:
|
||||
- Andrew Klychkov (@Andersson007)
|
||||
|
||||
extends_documentation_fragment:
|
||||
- community.general.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'
|
||||
|
||||
- name: Collect all possible information using passwordless root access
|
||||
mysql_info:
|
||||
login_user: root
|
||||
|
||||
- name: Get MySQL version with non-default credentials
|
||||
mysql_info:
|
||||
login_user: mysuperuser
|
||||
login_password: mysuperpass
|
||||
filter: version
|
||||
|
||||
- name: Collect all info except settings and users by root
|
||||
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: yes
|
||||
mysql_info:
|
||||
filter:
|
||||
- databases
|
||||
- version
|
||||
|
||||
- name: Collect info about databases and version using ~alice/.my.cnf as a credential file
|
||||
become: yes
|
||||
mysql_info:
|
||||
config_file: /home/alice/.my.cnf
|
||||
filter:
|
||||
- databases
|
||||
- version
|
||||
|
||||
- name: Collect info about databases including empty and excluding their sizes
|
||||
become: yes
|
||||
mysql_info:
|
||||
config_file: /home/alice/.my.cnf
|
||||
filter:
|
||||
- databases
|
||||
exclude_fields: db_size
|
||||
return_empty_dbs: yes
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
version:
|
||||
description: Database server version.
|
||||
returned: if not excluded by filter
|
||||
type: dict
|
||||
sample: { "version": { "major": 5, "minor": 5, "release": 60 } }
|
||||
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
|
||||
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 } }
|
||||
'''
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.mysql import (
|
||||
mysql_connect,
|
||||
mysql_common_argument_spec,
|
||||
mysql_driver,
|
||||
mysql_driver_fail_msg,
|
||||
)
|
||||
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'])
|
||||
|
||||
ver = self.info['settings']['version'].split('.')
|
||||
release = ver[2].split('-')[0]
|
||||
|
||||
self.info['version'] = dict(
|
||||
major=int(ver[0]),
|
||||
minor=int(ver[1]),
|
||||
release=int(release),
|
||||
)
|
||||
|
||||
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:
|
||||
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']
|
||||
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)
|
||||
|
||||
try:
|
||||
cursor, db_conn = mysql_connect(module, login_user, login_password,
|
||||
config_file, ssl_cert, ssl_key, ssl_ca, db,
|
||||
connect_timeout=connect_timeout, cursor_class='DictCursor')
|
||||
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)))
|
||||
|
||||
###############################
|
||||
# Create object and do main job
|
||||
|
||||
mysql = MySQL_Info(module, cursor)
|
||||
|
||||
module.exit_json(changed=False, **mysql.get_info(filter_, exclude_fields, return_empty_dbs))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,233 +0,0 @@
|
|||
#!/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.
|
||||
version_added: '0.2.0'
|
||||
options:
|
||||
query:
|
||||
description:
|
||||
- SQL query to run. Multiple queries can be passed using YAML list syntax.
|
||||
type: list
|
||||
elements: str
|
||||
required: yes
|
||||
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: no
|
||||
notes:
|
||||
- To pass a query containing commas, use YAML list notation with hyphen (see EXAMPLES block).
|
||||
author:
|
||||
- Andrew Klychkov (@Andersson007)
|
||||
extends_documentation_fragment:
|
||||
- community.general.mysql
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Simple select query to acme db
|
||||
mysql_query:
|
||||
login_db: acme
|
||||
query: SELECT * FROM orders
|
||||
|
||||
- name: Select query to db acme with positional arguments
|
||||
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
|
||||
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
|
||||
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: yes
|
||||
'''
|
||||
|
||||
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]
|
||||
'''
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.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')
|
||||
# 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='list', elements='str', 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']
|
||||
config_file = module.params['config_file']
|
||||
query = module.params["query"]
|
||||
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,
|
||||
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 = []
|
||||
for q in query:
|
||||
try:
|
||||
cursor.execute(q, arguments)
|
||||
|
||||
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:
|
||||
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:
|
||||
changed = True
|
||||
|
||||
executed_queries.append(cursor._last_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()
|
|
@ -1,573 +0,0 @@
|
|||
#!/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, slave, master status, get and change master host.
|
||||
author:
|
||||
- Balazs Pocze (@banyek)
|
||||
- Andrew Klychkov (@Andersson007)
|
||||
options:
|
||||
mode:
|
||||
description:
|
||||
- Module operating mode. Could be
|
||||
C(changemaster) (CHANGE MASTER TO),
|
||||
C(getmaster) (SHOW MASTER STATUS),
|
||||
C(getslave) (SHOW SLAVE STATUS),
|
||||
C(startslave) (START SLAVE),
|
||||
C(stopslave) (STOP SLAVE),
|
||||
C(resetmaster) (RESET MASTER) - supported since community.general 0.2.0,
|
||||
C(resetslave) (RESET SLAVE),
|
||||
C(resetslaveall) (RESET SLAVE ALL).
|
||||
type: str
|
||||
choices:
|
||||
- changemaster
|
||||
- getmaster
|
||||
- getslave
|
||||
- startslave
|
||||
- stopslave
|
||||
- resetmaster
|
||||
- resetslave
|
||||
- resetslaveall
|
||||
default: getslave
|
||||
master_host:
|
||||
description:
|
||||
- Same as mysql variable.
|
||||
type: str
|
||||
master_user:
|
||||
description:
|
||||
- Same as mysql variable.
|
||||
type: str
|
||||
master_password:
|
||||
description:
|
||||
- Same as mysql variable.
|
||||
type: str
|
||||
master_port:
|
||||
description:
|
||||
- Same as mysql variable.
|
||||
type: int
|
||||
master_connect_retry:
|
||||
description:
|
||||
- Same as mysql variable.
|
||||
type: int
|
||||
master_log_file:
|
||||
description:
|
||||
- Same as mysql variable.
|
||||
type: str
|
||||
master_log_pos:
|
||||
description:
|
||||
- Same as mysql variable.
|
||||
type: int
|
||||
relay_log_file:
|
||||
description:
|
||||
- Same as mysql variable.
|
||||
type: str
|
||||
relay_log_pos:
|
||||
description:
|
||||
- Same as mysql variable.
|
||||
type: int
|
||||
master_ssl:
|
||||
description:
|
||||
- Same as mysql variable.
|
||||
type: bool
|
||||
master_ssl_ca:
|
||||
description:
|
||||
- Same as mysql variable.
|
||||
type: str
|
||||
master_ssl_capath:
|
||||
description:
|
||||
- Same as mysql variable.
|
||||
type: str
|
||||
master_ssl_cert:
|
||||
description:
|
||||
- Same as mysql variable.
|
||||
type: str
|
||||
master_ssl_key:
|
||||
description:
|
||||
- Same as mysql variable.
|
||||
type: str
|
||||
master_ssl_cipher:
|
||||
description:
|
||||
- Same as mysql variable.
|
||||
type: str
|
||||
master_auto_position:
|
||||
description:
|
||||
- Whether the host uses GTID based replication or not.
|
||||
type: bool
|
||||
master_use_gtid:
|
||||
description:
|
||||
- Configures the slave 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, slave_pos, disabled]
|
||||
type: str
|
||||
version_added: '0.2.0'
|
||||
master_delay:
|
||||
description:
|
||||
- Time lag behind the master's state (in seconds).
|
||||
- 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.2.0'
|
||||
connection_name:
|
||||
description:
|
||||
- Name of the master 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.2.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.2.0'
|
||||
fail_on_error:
|
||||
description:
|
||||
- Fails on error when calling mysql.
|
||||
type: bool
|
||||
default: False
|
||||
version_added: '0.2.0'
|
||||
|
||||
notes:
|
||||
- If an empty value for the parameter of string type is needed, use an empty string.
|
||||
|
||||
extends_documentation_fragment:
|
||||
- community.general.mysql
|
||||
|
||||
|
||||
seealso:
|
||||
- module: community.general.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: MariaDB replication reference
|
||||
description: Complete reference of the MariaDB replication documentation.
|
||||
link: https://mariadb.com/kb/en/library/setting-up-replication/
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Stop mysql slave thread
|
||||
mysql_replication:
|
||||
mode: stopslave
|
||||
|
||||
- name: Get master binlog file name and binlog position
|
||||
mysql_replication:
|
||||
mode: getmaster
|
||||
|
||||
- name: Change master to master server 192.0.2.1 and use binary log 'mysql-bin.000009' with position 4578
|
||||
mysql_replication:
|
||||
mode: changemaster
|
||||
master_host: 192.0.2.1
|
||||
master_log_file: mysql-bin.000009
|
||||
master_log_pos: 4578
|
||||
|
||||
- name: Check slave status using port 3308
|
||||
mysql_replication:
|
||||
mode: getslave
|
||||
login_host: ansible.example.com
|
||||
login_port: 3308
|
||||
|
||||
- name: On MariaDB change master to use GTID current_pos
|
||||
mysql_replication:
|
||||
mode: changemaster
|
||||
master_use_gtid: current_pos
|
||||
|
||||
- name: Change master to use replication delay 3600 seconds
|
||||
mysql_replication:
|
||||
mode: changemaster
|
||||
master_host: 192.0.2.1
|
||||
master_delay: 3600
|
||||
|
||||
- name: Start MariaDB standby with connection name master-1
|
||||
mysql_replication:
|
||||
mode: startslave
|
||||
connection_name: master-1
|
||||
|
||||
- name: Stop replication in channel master-1
|
||||
mysql_replication:
|
||||
mode: stopslave
|
||||
channel: master-1
|
||||
|
||||
- name: >
|
||||
Run RESET MASTER command which will delete all existing binary log files
|
||||
and reset the binary log index file on the master
|
||||
mysql_replication:
|
||||
mode: resetmaster
|
||||
|
||||
- name: Run start slave and fail the task on errors
|
||||
mysql_replication:
|
||||
mode: startslave
|
||||
connection_name: master-1
|
||||
fail_on_error: yes
|
||||
|
||||
- name: Change master and fail on error (like when slave thread is running)
|
||||
mysql_replication:
|
||||
mode: changemaster
|
||||
fail_on_error: yes
|
||||
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
queries:
|
||||
description: List of executed queries which modified DB's state.
|
||||
returned: always
|
||||
type: list
|
||||
sample: ["CHANGE MASTER TO MASTER_HOST='master2.example.com',MASTER_PORT=3306"]
|
||||
version_added: '0.2.0'
|
||||
'''
|
||||
|
||||
import os
|
||||
import warnings
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.mysql import mysql_connect, mysql_driver, mysql_driver_fail_msg
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
executed_queries = []
|
||||
|
||||
|
||||
def get_master_status(cursor):
|
||||
cursor.execute("SHOW MASTER STATUS")
|
||||
masterstatus = cursor.fetchone()
|
||||
return masterstatus
|
||||
|
||||
|
||||
def get_slave_status(cursor, connection_name='', channel=''):
|
||||
if connection_name:
|
||||
query = "SHOW SLAVE '%s' STATUS" % connection_name
|
||||
else:
|
||||
query = "SHOW SLAVE STATUS"
|
||||
|
||||
if channel:
|
||||
query += " FOR CHANNEL '%s'" % channel
|
||||
|
||||
cursor.execute(query)
|
||||
slavestatus = cursor.fetchone()
|
||||
return slavestatus
|
||||
|
||||
|
||||
def stop_slave(module, cursor, connection_name='', channel='', fail_on_error=False):
|
||||
if connection_name:
|
||||
query = "STOP SLAVE '%s'" % connection_name
|
||||
else:
|
||||
query = 'STOP SLAVE'
|
||||
|
||||
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 SLAVE failed: %s" % to_native(e))
|
||||
stopped = False
|
||||
return stopped
|
||||
|
||||
|
||||
def reset_slave(module, cursor, connection_name='', channel='', fail_on_error=False):
|
||||
if connection_name:
|
||||
query = "RESET SLAVE '%s'" % connection_name
|
||||
else:
|
||||
query = 'RESET SLAVE'
|
||||
|
||||
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 SLAVE failed: %s" % to_native(e))
|
||||
reset = False
|
||||
return reset
|
||||
|
||||
|
||||
def reset_slave_all(module, cursor, connection_name='', channel='', fail_on_error=False):
|
||||
if connection_name:
|
||||
query = "RESET SLAVE '%s' ALL" % connection_name
|
||||
else:
|
||||
query = 'RESET SLAVE ALL'
|
||||
|
||||
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 SLAVE ALL failed: %s" % to_native(e))
|
||||
reset = False
|
||||
return reset
|
||||
|
||||
|
||||
def reset_master(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_slave(module, cursor, connection_name='', channel='', fail_on_error=False):
|
||||
if connection_name:
|
||||
query = "START SLAVE '%s'" % connection_name
|
||||
else:
|
||||
query = 'START SLAVE'
|
||||
|
||||
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 SLAVE failed: %s" % to_native(e))
|
||||
started = False
|
||||
return started
|
||||
|
||||
|
||||
def changemaster(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():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
login_user=dict(type='str'),
|
||||
login_password=dict(type='str', no_log=True),
|
||||
login_host=dict(type='str', default='localhost'),
|
||||
login_port=dict(type='int', default=3306),
|
||||
login_unix_socket=dict(type='str'),
|
||||
mode=dict(type='str', default='getslave', choices=[
|
||||
'getmaster', 'getslave', 'changemaster', 'stopslave',
|
||||
'startslave', 'resetmaster', 'resetslave', 'resetslaveall']),
|
||||
master_auto_position=dict(type='bool', default=False),
|
||||
master_host=dict(type='str'),
|
||||
master_user=dict(type='str'),
|
||||
master_password=dict(type='str', no_log=True),
|
||||
master_port=dict(type='int'),
|
||||
master_connect_retry=dict(type='int'),
|
||||
master_log_file=dict(type='str'),
|
||||
master_log_pos=dict(type='int'),
|
||||
relay_log_file=dict(type='str'),
|
||||
relay_log_pos=dict(type='int'),
|
||||
master_ssl=dict(type='bool', default=False),
|
||||
master_ssl_ca=dict(type='str'),
|
||||
master_ssl_capath=dict(type='str'),
|
||||
master_ssl_cert=dict(type='str'),
|
||||
master_ssl_key=dict(type='str'),
|
||||
master_ssl_cipher=dict(type='str'),
|
||||
connect_timeout=dict(type='int', default=30),
|
||||
config_file=dict(type='path', default='~/.my.cnf'),
|
||||
client_cert=dict(type='path', aliases=['ssl_cert']),
|
||||
client_key=dict(type='path', aliases=['ssl_key']),
|
||||
ca_cert=dict(type='path', aliases=['ssl_ca']),
|
||||
master_use_gtid=dict(type='str', choices=['current_pos', 'slave_pos', 'disabled']),
|
||||
master_delay=dict(type='int'),
|
||||
connection_name=dict(type='str'),
|
||||
channel=dict(type='str'),
|
||||
fail_on_error=dict(type='bool', default=False),
|
||||
),
|
||||
mutually_exclusive=[
|
||||
['connection_name', 'channel']
|
||||
],
|
||||
)
|
||||
mode = module.params["mode"]
|
||||
master_host = module.params["master_host"]
|
||||
master_user = module.params["master_user"]
|
||||
master_password = module.params["master_password"]
|
||||
master_port = module.params["master_port"]
|
||||
master_connect_retry = module.params["master_connect_retry"]
|
||||
master_log_file = module.params["master_log_file"]
|
||||
master_log_pos = module.params["master_log_pos"]
|
||||
relay_log_file = module.params["relay_log_file"]
|
||||
relay_log_pos = module.params["relay_log_pos"]
|
||||
master_ssl = module.params["master_ssl"]
|
||||
master_ssl_ca = module.params["master_ssl_ca"]
|
||||
master_ssl_capath = module.params["master_ssl_capath"]
|
||||
master_ssl_cert = module.params["master_ssl_cert"]
|
||||
master_ssl_key = module.params["master_ssl_key"]
|
||||
master_ssl_cipher = module.params["master_ssl_cipher"]
|
||||
master_auto_position = module.params["master_auto_position"]
|
||||
ssl_cert = module.params["client_cert"]
|
||||
ssl_key = module.params["client_key"]
|
||||
ssl_ca = module.params["ca_cert"]
|
||||
connect_timeout = module.params['connect_timeout']
|
||||
config_file = module.params['config_file']
|
||||
master_delay = module.params['master_delay']
|
||||
if module.params.get("master_use_gtid") == 'disabled':
|
||||
master_use_gtid = 'no'
|
||||
else:
|
||||
master_use_gtid = module.params["master_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)
|
||||
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)))
|
||||
|
||||
if mode in "getmaster":
|
||||
status = get_master_status(cursor)
|
||||
if not isinstance(status, dict):
|
||||
status = dict(Is_Master=False, msg="Server is not configured as mysql master")
|
||||
else:
|
||||
status['Is_Master'] = True
|
||||
module.exit_json(queries=executed_queries, **status)
|
||||
|
||||
elif mode in "getslave":
|
||||
status = get_slave_status(cursor, connection_name, channel)
|
||||
if not isinstance(status, dict):
|
||||
status = dict(Is_Slave=False, msg="Server is not configured as mysql slave")
|
||||
else:
|
||||
status['Is_Slave'] = True
|
||||
module.exit_json(queries=executed_queries, **status)
|
||||
|
||||
elif mode in "changemaster":
|
||||
chm = []
|
||||
result = {}
|
||||
if master_host is not None:
|
||||
chm.append("MASTER_HOST='%s'" % master_host)
|
||||
if master_user is not None:
|
||||
chm.append("MASTER_USER='%s'" % master_user)
|
||||
if master_password is not None:
|
||||
chm.append("MASTER_PASSWORD='%s'" % master_password)
|
||||
if master_port is not None:
|
||||
chm.append("MASTER_PORT=%s" % master_port)
|
||||
if master_connect_retry is not None:
|
||||
chm.append("MASTER_CONNECT_RETRY=%s" % master_connect_retry)
|
||||
if master_log_file is not None:
|
||||
chm.append("MASTER_LOG_FILE='%s'" % master_log_file)
|
||||
if master_log_pos is not None:
|
||||
chm.append("MASTER_LOG_POS=%s" % master_log_pos)
|
||||
if master_delay is not None:
|
||||
chm.append("MASTER_DELAY=%s" % master_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 master_ssl:
|
||||
chm.append("MASTER_SSL=1")
|
||||
if master_ssl_ca is not None:
|
||||
chm.append("MASTER_SSL_CA='%s'" % master_ssl_ca)
|
||||
if master_ssl_capath is not None:
|
||||
chm.append("MASTER_SSL_CAPATH='%s'" % master_ssl_capath)
|
||||
if master_ssl_cert is not None:
|
||||
chm.append("MASTER_SSL_CERT='%s'" % master_ssl_cert)
|
||||
if master_ssl_key is not None:
|
||||
chm.append("MASTER_SSL_KEY='%s'" % master_ssl_key)
|
||||
if master_ssl_cipher is not None:
|
||||
chm.append("MASTER_SSL_CIPHER='%s'" % master_ssl_cipher)
|
||||
if master_auto_position:
|
||||
chm.append("MASTER_AUTO_POSITION=1")
|
||||
if master_use_gtid is not None:
|
||||
chm.append("MASTER_USE_GTID=%s" % master_use_gtid)
|
||||
try:
|
||||
changemaster(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 in "startslave":
|
||||
started = start_slave(module, cursor, connection_name, channel, fail_on_error)
|
||||
if started is True:
|
||||
module.exit_json(msg="Slave started ", changed=True, queries=executed_queries)
|
||||
else:
|
||||
module.exit_json(msg="Slave already started (Or cannot be started)", changed=False, queries=executed_queries)
|
||||
elif mode in "stopslave":
|
||||
stopped = stop_slave(module, cursor, connection_name, channel, fail_on_error)
|
||||
if stopped is True:
|
||||
module.exit_json(msg="Slave stopped", changed=True, queries=executed_queries)
|
||||
else:
|
||||
module.exit_json(msg="Slave already stopped", changed=False, queries=executed_queries)
|
||||
elif mode in "resetmaster":
|
||||
reset = reset_master(module, cursor, fail_on_error)
|
||||
if reset is True:
|
||||
module.exit_json(msg="Master reset", changed=True, queries=executed_queries)
|
||||
else:
|
||||
module.exit_json(msg="Master already reset", changed=False, queries=executed_queries)
|
||||
elif mode in "resetslave":
|
||||
reset = reset_slave(module, cursor, connection_name, channel, fail_on_error)
|
||||
if reset is True:
|
||||
module.exit_json(msg="Slave reset", changed=True, queries=executed_queries)
|
||||
else:
|
||||
module.exit_json(msg="Slave already reset", changed=False, queries=executed_queries)
|
||||
elif mode in "resetslaveall":
|
||||
reset = reset_slave_all(module, cursor, connection_name, channel, fail_on_error)
|
||||
if reset is True:
|
||||
module.exit_json(msg="Slave reset", changed=True, queries=executed_queries)
|
||||
else:
|
||||
module.exit_json(msg="Slave already reset", changed=False, queries=executed_queries)
|
||||
|
||||
warnings.simplefilter("ignore")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,991 +0,0 @@
|
|||
#!/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..
|
||||
type: str
|
||||
encrypted:
|
||||
description:
|
||||
- Indicate that the 'password' field is a `mysql_native_password` hash.
|
||||
type: bool
|
||||
default: no
|
||||
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: no
|
||||
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.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).
|
||||
type: raw
|
||||
append_privs:
|
||||
description:
|
||||
- Append the privileges defined by priv to the existing ones for this
|
||||
user instead of overwriting existing ones.
|
||||
type: bool
|
||||
default: no
|
||||
sql_log_bin:
|
||||
description:
|
||||
- Whether binary logging should be enabled or disabled for the connection.
|
||||
type: bool
|
||||
default: yes
|
||||
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: no
|
||||
update_password:
|
||||
description:
|
||||
- C(always) will update passwords if they differ.
|
||||
- C(on_create) will only set the password for newly created users.
|
||||
type: str
|
||||
choices: [ always, on_create ]
|
||||
default: always
|
||||
plugin:
|
||||
description:
|
||||
- User's plugin to authenticate (``CREATE USER user IDENTIFIED WITH plugin``).
|
||||
type: str
|
||||
version_added: '0.2.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.2.0'
|
||||
plugin_auth_string:
|
||||
description:
|
||||
- User's plugin auth_string (``CREATE USER user IDENTIFIED WITH plugin BY plugin_auth_string``).
|
||||
type: str
|
||||
version_added: '0.2.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)."
|
||||
- Used when I(state=present), ignored otherwise.
|
||||
type: dict
|
||||
version_added: '0.2.0'
|
||||
|
||||
notes:
|
||||
- "MySQL server installs with default login_user of 'root' and no password. To secure this user
|
||||
as part of an idempotent playbook, you must create at least two tasks: the first must change the root user's password,
|
||||
without providing any login_user/login_password details. The second must drop a ~/.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 `mysql_native_password` encrypted password hash module.
|
||||
|
||||
seealso:
|
||||
- module: community.general.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.general.mysql
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Removes anonymous user account for localhost
|
||||
mysql_user:
|
||||
name: ''
|
||||
host: localhost
|
||||
state: absent
|
||||
|
||||
- name: Removes all anonymous user accounts
|
||||
mysql_user:
|
||||
name: ''
|
||||
host_all: yes
|
||||
state: absent
|
||||
|
||||
- name: Create database user with name 'bob' and password '12345' with all database privileges
|
||||
mysql_user:
|
||||
name: bob
|
||||
password: 12345
|
||||
priv: '*.*:ALL'
|
||||
state: present
|
||||
|
||||
- name: Create database user using hashed password with all database privileges
|
||||
mysql_user:
|
||||
name: bob
|
||||
password: '*EE0D72C1085C46C5278932678FBE2C6A782821B4'
|
||||
encrypted: yes
|
||||
priv: '*.*:ALL'
|
||||
state: present
|
||||
|
||||
- name: Create database user with password and all database privileges and 'WITH GRANT OPTION'
|
||||
mysql_user:
|
||||
name: bob
|
||||
password: 12345
|
||||
priv: '*.*:ALL,GRANT'
|
||||
state: present
|
||||
|
||||
- name: Create user with password, all database privileges and 'WITH GRANT OPTION' in db1 and db2
|
||||
mysql_user:
|
||||
state: present
|
||||
name: bob
|
||||
password: 12345dd
|
||||
priv:
|
||||
'db1.*': 'ALL,GRANT'
|
||||
'db2.*': 'ALL,GRANT'
|
||||
|
||||
# Note that REQUIRESSL is a special privilege that should only apply to *.* by itself.
|
||||
- name: Modify user to require SSL connections.
|
||||
mysql_user:
|
||||
name: bob
|
||||
append_privs: yes
|
||||
priv: '*.*:REQUIRESSL'
|
||||
state: present
|
||||
|
||||
- name: Ensure no user named 'sally'@'localhost' exists, also passing in the auth credentials.
|
||||
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.
|
||||
mysql_user:
|
||||
check_implicit_admin: yes
|
||||
login_user: root
|
||||
login_password: 123456
|
||||
name: sally
|
||||
state: absent
|
||||
|
||||
- name: Ensure no user named 'sally' exists at all
|
||||
mysql_user:
|
||||
name: sally
|
||||
host_all: yes
|
||||
state: absent
|
||||
|
||||
- name: Specify grants composed of more than one word
|
||||
mysql_user:
|
||||
name: replication
|
||||
password: 12345
|
||||
priv: "*.*:REPLICATION CLIENT"
|
||||
state: present
|
||||
|
||||
- name: Revoke all privileges for user 'bob' and password '12345'
|
||||
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
|
||||
mysql_user:
|
||||
name: root
|
||||
password: abc123
|
||||
login_unix_socket: /var/run/mysqld/mysqld.sock
|
||||
|
||||
- name: Example of skipping binary logging while adding user 'bob'
|
||||
mysql_user:
|
||||
name: bob
|
||||
password: 12345
|
||||
priv: "*.*:USAGE"
|
||||
state: present
|
||||
sql_log_bin: no
|
||||
|
||||
- name: Create user 'bob' authenticated with plugin 'AWSAuthenticationPlugin'
|
||||
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
|
||||
mysql_user:
|
||||
name: bob
|
||||
resource_limits:
|
||||
MAX_QUERIES_PER_HOUR: 10
|
||||
MAX_CONNECTIONS_PER_HOUR: 5
|
||||
|
||||
# Example .my.cnf file for setting the root password
|
||||
# [client]
|
||||
# user=root
|
||||
# password=n<_665{vS43y
|
||||
'''
|
||||
|
||||
import re
|
||||
import string
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.database import SQLParseError
|
||||
from ansible_collections.community.general.plugins.module_utils.mysql import mysql_connect, mysql_driver, mysql_driver_fail_msg
|
||||
from ansible.module_utils.six import iteritems
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
|
||||
VALID_PRIVS = frozenset(('CREATE', 'DROP', 'GRANT', 'GRANT OPTION',
|
||||
'LOCK TABLES', 'REFERENCES', 'EVENT', 'ALTER',
|
||||
'DELETE', 'INDEX', 'INSERT', 'SELECT', 'UPDATE',
|
||||
'CREATE TEMPORARY TABLES', 'TRIGGER', 'CREATE VIEW',
|
||||
'SHOW VIEW', 'ALTER ROUTINE', 'CREATE ROUTINE',
|
||||
'EXECUTE', 'FILE', 'CREATE TABLESPACE', 'CREATE USER',
|
||||
'PROCESS', 'PROXY', 'RELOAD', 'REPLICATION CLIENT',
|
||||
'REPLICATION SLAVE', 'SHOW DATABASES', 'SHUTDOWN',
|
||||
'SUPER', 'ALL', 'ALL PRIVILEGES', 'USAGE', 'REQUIRESSL',
|
||||
'CREATE ROLE', 'DROP ROLE', 'APPLICATION_PASSWORD_ADMIN',
|
||||
'AUDIT_ADMIN', 'BACKUP_ADMIN', 'BINLOG_ADMIN',
|
||||
'BINLOG_ENCRYPTION_ADMIN', 'CLONE_ADMIN', 'CONNECTION_ADMIN',
|
||||
'ENCRYPTION_KEY_ADMIN', 'FIREWALL_ADMIN', 'FIREWALL_USER',
|
||||
'GROUP_REPLICATION_ADMIN', 'INNODB_REDO_LOG_ARCHIVE',
|
||||
'NDB_STORED_USER', 'PERSIST_RO_VARIABLES_ADMIN',
|
||||
'REPLICATION_APPLIER', 'REPLICATION_SLAVE_ADMIN',
|
||||
'RESOURCE_GROUP_ADMIN', 'RESOURCE_GROUP_USER',
|
||||
'ROLE_ADMIN', 'SESSION_VARIABLES_ADMIN', 'SET_USER_ID',
|
||||
'SYSTEM_USER', 'SYSTEM_VARIABLES_ADMIN', 'SYSTEM_USER',
|
||||
'TABLE_ENCRYPTION_ADMIN', 'VERSION_TOKEN_ADMIN',
|
||||
'XA_RECOVER_ADMIN', 'LOAD FROM S3', 'SELECT INTO S3',
|
||||
'INVOKE LAMBDA',
|
||||
'ALTER ROUTINE',
|
||||
'BINLOG ADMIN',
|
||||
'BINLOG MONITOR',
|
||||
'BINLOG REPLAY',
|
||||
'CONNECTION ADMIN',
|
||||
'READ_ONLY ADMIN',
|
||||
'REPLICATION MASTER ADMIN',
|
||||
'REPLICATION SLAVE',
|
||||
'REPLICATION SLAVE ADMIN',
|
||||
'SET USER',))
|
||||
|
||||
|
||||
class InvalidPrivsError(Exception):
|
||||
pass
|
||||
|
||||
# ===========================================
|
||||
# MySQL module specific support methods.
|
||||
#
|
||||
|
||||
|
||||
# User Authentication Management changed in MySQL 5.7 and MariaDB 10.2.0
|
||||
def use_old_user_mgmt(cursor):
|
||||
cursor.execute("SELECT VERSION()")
|
||||
result = cursor.fetchone()
|
||||
version_str = result[0]
|
||||
version = version_str.split('.')
|
||||
|
||||
if 'mariadb' in version_str.lower():
|
||||
# Prior to MariaDB 10.2
|
||||
if int(version[0]) * 1000 + int(version[1]) < 10002:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
# Prior to MySQL 5.7
|
||||
if int(version[0]) * 1000 + int(version[1]) < 5007:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def get_mode(cursor):
|
||||
cursor.execute('SELECT @@GLOBAL.sql_mode')
|
||||
result = cursor.fetchone()
|
||||
mode_str = result[0]
|
||||
if 'ANSI' in mode_str:
|
||||
mode = 'ANSI'
|
||||
else:
|
||||
mode = 'NOTANSI'
|
||||
return mode
|
||||
|
||||
|
||||
def user_exists(cursor, user, host, host_all):
|
||||
if host_all:
|
||||
cursor.execute("SELECT count(*) FROM mysql.user WHERE user = %s", (user,))
|
||||
else:
|
||||
cursor.execute("SELECT count(*) FROM mysql.user WHERE user = %s AND host = %s", (user, host))
|
||||
|
||||
count = cursor.fetchone()
|
||||
return count[0] > 0
|
||||
|
||||
|
||||
def user_add(cursor, user, host, host_all, password, encrypted,
|
||||
plugin, plugin_hash_string, plugin_auth_string, new_priv, check_mode):
|
||||
# we cannot create users without a proper hostname
|
||||
if host_all:
|
||||
return False
|
||||
|
||||
if check_mode:
|
||||
return True
|
||||
|
||||
# Determine what user management method server uses
|
||||
old_user_mgmt = use_old_user_mgmt(cursor)
|
||||
|
||||
if password and encrypted:
|
||||
cursor.execute("CREATE USER %s@%s IDENTIFIED BY PASSWORD %s", (user, host, password))
|
||||
elif password and not encrypted:
|
||||
if old_user_mgmt:
|
||||
cursor.execute("CREATE USER %s@%s IDENTIFIED BY %s", (user, host, password))
|
||||
else:
|
||||
cursor.execute("SELECT CONCAT('*', UCASE(SHA1(UNHEX(SHA1(%s)))))", (password,))
|
||||
encrypted_password = cursor.fetchone()[0]
|
||||
cursor.execute("CREATE USER %s@%s IDENTIFIED WITH mysql_native_password AS %s", (user, host, encrypted_password))
|
||||
|
||||
elif plugin and plugin_hash_string:
|
||||
cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s AS %s", (user, host, plugin, plugin_hash_string))
|
||||
elif plugin and plugin_auth_string:
|
||||
cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, plugin_auth_string))
|
||||
elif plugin:
|
||||
cursor.execute("CREATE USER %s@%s IDENTIFIED WITH %s", (user, host, plugin))
|
||||
else:
|
||||
cursor.execute("CREATE USER %s@%s", (user, host))
|
||||
if new_priv is not None:
|
||||
for db_table, priv in iteritems(new_priv):
|
||||
privileges_grant(cursor, user, host, db_table, priv)
|
||||
return True
|
||||
|
||||
|
||||
def is_hash(password):
|
||||
ishash = False
|
||||
if len(password) == 41 and password[0] == '*':
|
||||
if frozenset(password[1:]).issubset(string.hexdigits):
|
||||
ishash = True
|
||||
return ishash
|
||||
|
||||
|
||||
def user_mod(cursor, user, host, host_all, password, encrypted,
|
||||
plugin, plugin_hash_string, plugin_auth_string, new_priv, append_privs, module):
|
||||
changed = False
|
||||
msg = "User unchanged"
|
||||
grant_option = False
|
||||
|
||||
if host_all:
|
||||
hostnames = user_get_hostnames(cursor, [user])
|
||||
else:
|
||||
hostnames = [host]
|
||||
|
||||
for host in hostnames:
|
||||
# Handle clear text and hashed passwords.
|
||||
if bool(password):
|
||||
# Determine what user management method server uses
|
||||
old_user_mgmt = use_old_user_mgmt(cursor)
|
||||
|
||||
# Get a list of valid columns in mysql.user table to check if Password and/or authentication_string exist
|
||||
cursor.execute("""
|
||||
SELECT COLUMN_NAME FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user' AND COLUMN_NAME IN ('Password', 'authentication_string')
|
||||
ORDER BY COLUMN_NAME DESC LIMIT 1
|
||||
""")
|
||||
colA = cursor.fetchone()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT COLUMN_NAME FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'mysql' AND TABLE_NAME = 'user' AND COLUMN_NAME IN ('Password', 'authentication_string')
|
||||
ORDER BY COLUMN_NAME ASC LIMIT 1
|
||||
""")
|
||||
colB = cursor.fetchone()
|
||||
|
||||
# Select hash from either Password or authentication_string, depending which one exists and/or is filled
|
||||
cursor.execute("""
|
||||
SELECT COALESCE(
|
||||
CASE WHEN %s = '' THEN NULL ELSE %s END,
|
||||
CASE WHEN %s = '' THEN NULL ELSE %s END
|
||||
)
|
||||
FROM mysql.user WHERE user = %%s AND host = %%s
|
||||
""" % (colA[0], colA[0], colB[0], colB[0]), (user, host))
|
||||
current_pass_hash = cursor.fetchone()[0]
|
||||
if isinstance(current_pass_hash, bytes):
|
||||
current_pass_hash = current_pass_hash.decode('ascii')
|
||||
|
||||
if encrypted:
|
||||
encrypted_password = password
|
||||
if not is_hash(encrypted_password):
|
||||
module.fail_json(msg="encrypted was specified however it does not appear to be a valid hash expecting: *SHA1(SHA1(your_password))")
|
||||
else:
|
||||
if old_user_mgmt:
|
||||
cursor.execute("SELECT PASSWORD(%s)", (password,))
|
||||
else:
|
||||
cursor.execute("SELECT CONCAT('*', UCASE(SHA1(UNHEX(SHA1(%s)))))", (password,))
|
||||
encrypted_password = cursor.fetchone()[0]
|
||||
|
||||
if current_pass_hash != encrypted_password:
|
||||
msg = "Password updated"
|
||||
if module.check_mode:
|
||||
return (True, msg)
|
||||
if old_user_mgmt:
|
||||
cursor.execute("SET PASSWORD FOR %s@%s = %s", (user, host, encrypted_password))
|
||||
msg = "Password updated (old style)"
|
||||
else:
|
||||
try:
|
||||
cursor.execute("ALTER USER %s@%s IDENTIFIED WITH mysql_native_password AS %s", (user, host, encrypted_password))
|
||||
msg = "Password updated (new style)"
|
||||
except (mysql_driver.Error) as e:
|
||||
# https://stackoverflow.com/questions/51600000/authentication-string-of-root-user-on-mysql
|
||||
# Replacing empty root password with new authentication mechanisms fails with error 1396
|
||||
if e.args[0] == 1396:
|
||||
cursor.execute(
|
||||
"UPDATE mysql.user SET plugin = %s, authentication_string = %s, Password = '' WHERE User = %s AND Host = %s",
|
||||
('mysql_native_password', encrypted_password, user, host)
|
||||
)
|
||||
cursor.execute("FLUSH PRIVILEGES")
|
||||
msg = "Password forced update"
|
||||
else:
|
||||
raise e
|
||||
changed = True
|
||||
|
||||
# Handle plugin authentication
|
||||
if plugin:
|
||||
cursor.execute("SELECT plugin, authentication_string FROM mysql.user "
|
||||
"WHERE user = %s AND host = %s", (user, host))
|
||||
current_plugin = cursor.fetchone()
|
||||
|
||||
update = False
|
||||
|
||||
if current_plugin[0] != plugin:
|
||||
update = True
|
||||
|
||||
if plugin_hash_string and current_plugin[1] != plugin_hash_string:
|
||||
update = True
|
||||
|
||||
if plugin_auth_string and current_plugin[1] != plugin_auth_string:
|
||||
# this case can cause more updates than expected,
|
||||
# as plugin can hash auth_string in any way it wants
|
||||
# and there's no way to figure it out for
|
||||
# a check, so I prefer to update more often than never
|
||||
update = True
|
||||
|
||||
if update:
|
||||
if plugin_hash_string:
|
||||
cursor.execute("ALTER USER %s@%s IDENTIFIED WITH %s AS %s", (user, host, plugin, plugin_hash_string))
|
||||
elif plugin_auth_string:
|
||||
cursor.execute("ALTER USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, plugin_auth_string))
|
||||
else:
|
||||
cursor.execute("ALTER USER %s@%s IDENTIFIED WITH %s", (user, host, plugin))
|
||||
changed = True
|
||||
|
||||
# Handle privileges
|
||||
if new_priv is not None:
|
||||
curr_priv = privileges_get(cursor, user, host)
|
||||
|
||||
# If the user has privileges on a db.table that doesn't appear at all in
|
||||
# the new specification, then revoke all privileges on it.
|
||||
for db_table, priv in iteritems(curr_priv):
|
||||
# If the user has the GRANT OPTION on a db.table, revoke it first.
|
||||
if "GRANT" in priv:
|
||||
grant_option = True
|
||||
if db_table not in new_priv:
|
||||
if user != "root" and "PROXY" not in priv and not append_privs:
|
||||
msg = "Privileges updated"
|
||||
if module.check_mode:
|
||||
return (True, msg)
|
||||
privileges_revoke(cursor, user, host, db_table, priv, grant_option)
|
||||
changed = True
|
||||
|
||||
# If the user doesn't currently have any privileges on a db.table, then
|
||||
# we can perform a straight grant operation.
|
||||
for db_table, priv in iteritems(new_priv):
|
||||
if db_table not in curr_priv:
|
||||
msg = "New privileges granted"
|
||||
if module.check_mode:
|
||||
return (True, msg)
|
||||
privileges_grant(cursor, user, host, db_table, priv)
|
||||
changed = True
|
||||
|
||||
# If the db.table specification exists in both the user's current privileges
|
||||
# and in the new privileges, then we need to see if there's a difference.
|
||||
db_table_intersect = set(new_priv.keys()) & set(curr_priv.keys())
|
||||
for db_table in db_table_intersect:
|
||||
priv_diff = set(new_priv[db_table]) ^ set(curr_priv[db_table])
|
||||
if len(priv_diff) > 0:
|
||||
msg = "Privileges updated"
|
||||
if module.check_mode:
|
||||
return (True, msg)
|
||||
if not append_privs:
|
||||
privileges_revoke(cursor, user, host, db_table, curr_priv[db_table], grant_option)
|
||||
privileges_grant(cursor, user, host, db_table, new_priv[db_table])
|
||||
changed = True
|
||||
|
||||
return (changed, msg)
|
||||
|
||||
|
||||
def user_delete(cursor, user, host, host_all, check_mode):
|
||||
if check_mode:
|
||||
return True
|
||||
|
||||
if host_all:
|
||||
hostnames = user_get_hostnames(cursor, user)
|
||||
|
||||
for hostname in hostnames:
|
||||
cursor.execute("DROP USER %s@%s", (user, hostname))
|
||||
else:
|
||||
cursor.execute("DROP USER %s@%s", (user, host))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def user_get_hostnames(cursor, user):
|
||||
cursor.execute("SELECT Host FROM mysql.user WHERE user = %s", (user,))
|
||||
hostnames_raw = cursor.fetchall()
|
||||
hostnames = []
|
||||
|
||||
for hostname_raw in hostnames_raw:
|
||||
hostnames.append(hostname_raw[0])
|
||||
|
||||
return hostnames
|
||||
|
||||
|
||||
def privileges_get(cursor, user, host):
|
||||
""" MySQL doesn't have a better method of getting privileges aside from the
|
||||
SHOW GRANTS query syntax, which requires us to then parse the returned string.
|
||||
Here's an example of the string that is returned from MySQL:
|
||||
|
||||
GRANT USAGE ON *.* TO 'user'@'localhost' IDENTIFIED BY 'pass';
|
||||
|
||||
This function makes the query and returns a dictionary containing the results.
|
||||
The dictionary format is the same as that returned by privileges_unpack() below.
|
||||
"""
|
||||
output = {}
|
||||
cursor.execute("SHOW GRANTS FOR %s@%s", (user, host))
|
||||
grants = cursor.fetchall()
|
||||
|
||||
def pick(x):
|
||||
if x == 'ALL PRIVILEGES':
|
||||
return 'ALL'
|
||||
else:
|
||||
return x
|
||||
|
||||
for grant in grants:
|
||||
res = re.match("""GRANT (.+) ON (.+) TO (['`"]).*\\3@(['`"]).*\\4( IDENTIFIED BY PASSWORD (['`"]).+\\6)? ?(.*)""", grant[0])
|
||||
if res is None:
|
||||
raise InvalidPrivsError('unable to parse the MySQL grant string: %s' % grant[0])
|
||||
privileges = res.group(1).split(",")
|
||||
privileges = [pick(x.strip()) for x in privileges]
|
||||
if "WITH GRANT OPTION" in res.group(7):
|
||||
privileges.append('GRANT')
|
||||
if "REQUIRE SSL" in res.group(7):
|
||||
privileges.append('REQUIRESSL')
|
||||
db = res.group(2)
|
||||
output.setdefault(db, []).extend(privileges)
|
||||
return output
|
||||
|
||||
|
||||
def privileges_unpack(priv, mode):
|
||||
""" Take a privileges string, typically passed as a parameter, and unserialize
|
||||
it into a dictionary, the same format as privileges_get() above. We have this
|
||||
custom format to avoid using YAML/JSON strings inside YAML playbooks. Example
|
||||
of a privileges string:
|
||||
|
||||
mydb.*:INSERT,UPDATE/anotherdb.*:SELECT/yetanother.*:ALL
|
||||
|
||||
The privilege USAGE stands for no privileges, so we add that in on *.* if it's
|
||||
not specified in the string, as MySQL will always provide this by default.
|
||||
"""
|
||||
if mode == 'ANSI':
|
||||
quote = '"'
|
||||
else:
|
||||
quote = '`'
|
||||
output = {}
|
||||
privs = []
|
||||
for item in priv.strip().split('/'):
|
||||
pieces = item.strip().rsplit(':', 1)
|
||||
dbpriv = pieces[0].rsplit(".", 1)
|
||||
|
||||
# Check for FUNCTION or PROCEDURE object types
|
||||
parts = dbpriv[0].split(" ", 1)
|
||||
object_type = ''
|
||||
if len(parts) > 1 and (parts[0] == 'FUNCTION' or parts[0] == 'PROCEDURE'):
|
||||
object_type = parts[0] + ' '
|
||||
dbpriv[0] = parts[1]
|
||||
|
||||
# Do not escape if privilege is for database or table, i.e.
|
||||
# neither quote *. nor .*
|
||||
for i, side in enumerate(dbpriv):
|
||||
if side.strip('`') != '*':
|
||||
dbpriv[i] = '%s%s%s' % (quote, side.strip('`'), quote)
|
||||
pieces[0] = object_type + '.'.join(dbpriv)
|
||||
|
||||
if '(' in pieces[1]:
|
||||
output[pieces[0]] = re.split(r',\s*(?=[^)]*(?:\(|$))', pieces[1].upper())
|
||||
for i in output[pieces[0]]:
|
||||
privs.append(re.sub(r'\s*\(.*\)', '', i))
|
||||
else:
|
||||
output[pieces[0]] = pieces[1].upper().split(',')
|
||||
privs = output[pieces[0]]
|
||||
new_privs = frozenset(privs)
|
||||
if not new_privs.issubset(VALID_PRIVS):
|
||||
raise InvalidPrivsError('Invalid privileges specified: %s' % new_privs.difference(VALID_PRIVS))
|
||||
|
||||
if '*.*' not in output:
|
||||
output['*.*'] = ['USAGE']
|
||||
|
||||
# if we are only specifying something like REQUIRESSL and/or GRANT (=WITH GRANT OPTION) in *.*
|
||||
# we still need to add USAGE as a privilege to avoid syntax errors
|
||||
if 'REQUIRESSL' in priv and not set(output['*.*']).difference(set(['GRANT', 'REQUIRESSL'])):
|
||||
output['*.*'].append('USAGE')
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def privileges_revoke(cursor, user, host, db_table, priv, grant_option):
|
||||
# Escape '%' since mysql db.execute() uses a format string
|
||||
db_table = db_table.replace('%', '%%')
|
||||
if grant_option:
|
||||
query = ["REVOKE GRANT OPTION ON %s" % db_table]
|
||||
query.append("FROM %s@%s")
|
||||
query = ' '.join(query)
|
||||
cursor.execute(query, (user, host))
|
||||
priv_string = ",".join([p for p in priv if p not in ('GRANT', 'REQUIRESSL')])
|
||||
query = ["REVOKE %s ON %s" % (priv_string, db_table)]
|
||||
query.append("FROM %s@%s")
|
||||
query = ' '.join(query)
|
||||
cursor.execute(query, (user, host))
|
||||
|
||||
|
||||
def privileges_grant(cursor, user, host, db_table, priv):
|
||||
# Escape '%' since mysql db.execute uses a format string and the
|
||||
# specification of db and table often use a % (SQL wildcard)
|
||||
db_table = db_table.replace('%', '%%')
|
||||
priv_string = ",".join([p for p in priv if p not in ('GRANT', 'REQUIRESSL')])
|
||||
query = ["GRANT %s ON %s" % (priv_string, db_table)]
|
||||
query.append("TO %s@%s")
|
||||
if 'REQUIRESSL' in priv:
|
||||
query.append("REQUIRE SSL")
|
||||
if 'GRANT' in priv:
|
||||
query.append("WITH GRANT OPTION")
|
||||
query = ' '.join(query)
|
||||
cursor.execute(query, (user, host))
|
||||
|
||||
|
||||
def convert_priv_dict_to_str(priv):
|
||||
"""Converts privs dictionary to string of certain format.
|
||||
|
||||
Args:
|
||||
priv (dict): Dict of privileges that needs to be converted to string.
|
||||
|
||||
Returns:
|
||||
priv (str): String representation of input argument.
|
||||
"""
|
||||
priv_list = ['%s:%s' % (key, val) for key, val in iteritems(priv)]
|
||||
|
||||
return '/'.join(priv_list)
|
||||
|
||||
|
||||
# Alter user is supported since MySQL 5.6 and MariaDB 10.2.0
|
||||
def server_supports_alter_user(cursor):
|
||||
"""Check if the server supports ALTER USER statement or doesn't.
|
||||
|
||||
Args:
|
||||
cursor (cursor): DB driver cursor object.
|
||||
|
||||
Returns: True if supports, False otherwise.
|
||||
"""
|
||||
cursor.execute("SELECT VERSION()")
|
||||
version_str = cursor.fetchone()[0]
|
||||
version = version_str.split('.')
|
||||
|
||||
if 'mariadb' in version_str.lower():
|
||||
# MariaDB 10.2 and later
|
||||
if int(version[0]) * 1000 + int(version[1]) >= 10002:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
# MySQL 5.6 and later
|
||||
if int(version[0]) * 1000 + int(version[1]) >= 5006:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def get_resource_limits(cursor, user, host):
|
||||
"""Get user resource limits.
|
||||
|
||||
Args:
|
||||
cursor (cursor): DB driver cursor object.
|
||||
user (str): User name.
|
||||
host (str): User host name.
|
||||
|
||||
Returns: Dictionary containing current resource limits.
|
||||
"""
|
||||
|
||||
query = ('SELECT max_questions AS MAX_QUERIES_PER_HOUR, '
|
||||
'max_updates AS MAX_UPDATES_PER_HOUR, '
|
||||
'max_connections AS MAX_CONNECTIONS_PER_HOUR, '
|
||||
'max_user_connections AS MAX_USER_CONNECTIONS '
|
||||
'FROM mysql.user WHERE User = %s AND Host = %s')
|
||||
cursor.execute(query, (user, host))
|
||||
res = cursor.fetchone()
|
||||
|
||||
if not res:
|
||||
return None
|
||||
|
||||
current_limits = {
|
||||
'MAX_QUERIES_PER_HOUR': res[0],
|
||||
'MAX_UPDATES_PER_HOUR': res[1],
|
||||
'MAX_CONNECTIONS_PER_HOUR': res[2],
|
||||
'MAX_USER_CONNECTIONS': res[3],
|
||||
}
|
||||
return current_limits
|
||||
|
||||
|
||||
def match_resource_limits(module, current, desired):
|
||||
"""Check and match limits.
|
||||
|
||||
Args:
|
||||
module (AnsibleModule): Ansible module object.
|
||||
current (dict): Dictionary with current limits.
|
||||
desired (dict): Dictionary with desired limits.
|
||||
|
||||
Returns: Dictionary containing parameters that need to change.
|
||||
"""
|
||||
|
||||
if not current:
|
||||
# It means the user does not exists, so we need
|
||||
# to set all limits after its creation
|
||||
return desired
|
||||
|
||||
needs_to_change = {}
|
||||
|
||||
for key, val in iteritems(desired):
|
||||
if key not in current:
|
||||
# Supported keys are listed in the documentation
|
||||
# and must be determined in the get_resource_limits function
|
||||
# (follow 'AS' keyword)
|
||||
module.fail_json(msg="resource_limits: key '%s' is unsupported." % key)
|
||||
|
||||
try:
|
||||
val = int(val)
|
||||
except Exception:
|
||||
module.fail_json(msg="Can't convert value '%s' to integer." % val)
|
||||
|
||||
if val != current.get(key):
|
||||
needs_to_change[key] = val
|
||||
|
||||
return needs_to_change
|
||||
|
||||
|
||||
def limit_resources(module, cursor, user, host, resource_limits, check_mode):
|
||||
"""Limit user resources.
|
||||
|
||||
Args:
|
||||
module (AnsibleModule): Ansible module object.
|
||||
cursor (cursor): DB driver cursor object.
|
||||
user (str): User name.
|
||||
host (str): User host name.
|
||||
resource_limit (dict): Dictionary with desired limits.
|
||||
check_mode (bool): Run the function in check mode or not.
|
||||
|
||||
Returns: True, if changed, False otherwise.
|
||||
"""
|
||||
if not server_supports_alter_user(cursor):
|
||||
module.fail_json(msg="The server version does not match the requirements "
|
||||
"for resource_limits parameter. See module's documentation.")
|
||||
|
||||
current_limits = get_resource_limits(cursor, user, host)
|
||||
|
||||
needs_to_change = match_resource_limits(module, current_limits, resource_limits)
|
||||
|
||||
if not needs_to_change:
|
||||
return False
|
||||
|
||||
if needs_to_change and check_mode:
|
||||
return True
|
||||
|
||||
# If not check_mode
|
||||
tmp = []
|
||||
for key, val in iteritems(needs_to_change):
|
||||
tmp.append('%s %s' % (key, val))
|
||||
|
||||
query = "ALTER USER %s@%s"
|
||||
query += ' WITH %s' % ' '.join(tmp)
|
||||
cursor.execute(query, (user, host))
|
||||
return True
|
||||
|
||||
# ===========================================
|
||||
# Module execution.
|
||||
#
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
login_user=dict(type='str'),
|
||||
login_password=dict(type='str', no_log=True),
|
||||
login_host=dict(type='str', default='localhost'),
|
||||
login_port=dict(type='int', default=3306),
|
||||
login_unix_socket=dict(type='str'),
|
||||
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'),
|
||||
append_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'], no_log=False),
|
||||
connect_timeout=dict(type='int', default=30),
|
||||
config_file=dict(type='path', default='~/.my.cnf'),
|
||||
sql_log_bin=dict(type='bool', default=True),
|
||||
client_cert=dict(type='path', aliases=['ssl_cert']),
|
||||
client_key=dict(type='path', aliases=['ssl_key']),
|
||||
ca_cert=dict(type='path', aliases=['ssl_ca']),
|
||||
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'),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
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"]
|
||||
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"])
|
||||
update_password = module.params['update_password']
|
||||
ssl_cert = module.params["client_cert"]
|
||||
ssl_key = module.params["client_key"]
|
||||
ssl_ca = module.params["ca_cert"]
|
||||
db = ''
|
||||
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"]
|
||||
if priv and not (isinstance(priv, str) or isinstance(priv, 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)
|
||||
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)
|
||||
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 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)
|
||||
except Exception as e:
|
||||
module.fail_json(msg="invalid privileges string: %s" % to_native(e))
|
||||
|
||||
if state == "present":
|
||||
if user_exists(cursor, user, host, host_all):
|
||||
try:
|
||||
if update_password == 'always':
|
||||
changed, msg = user_mod(cursor, user, host, host_all, password, encrypted,
|
||||
plugin, plugin_hash_string, plugin_auth_string,
|
||||
priv, append_privs, module)
|
||||
else:
|
||||
changed, msg = user_mod(cursor, user, host, host_all, None, encrypted,
|
||||
plugin, plugin_hash_string, plugin_auth_string,
|
||||
priv, append_privs, module)
|
||||
|
||||
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:
|
||||
changed = user_add(cursor, user, host, host_all, password, encrypted,
|
||||
plugin, plugin_hash_string, plugin_auth_string,
|
||||
priv, module.check_mode)
|
||||
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)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,272 +0,0 @@
|
|||
#!/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: yes
|
||||
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.2.0'
|
||||
|
||||
seealso:
|
||||
- module: community.general.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.general.mysql
|
||||
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Check for sync_binlog setting
|
||||
mysql_variables:
|
||||
variable: sync_binlog
|
||||
|
||||
- name: Set read_only variable to 1 persistently
|
||||
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.2.0'
|
||||
'''
|
||||
|
||||
import os
|
||||
import warnings
|
||||
from re import match
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible_collections.community.general.plugins.module_utils.database import SQLParseError, mysql_quote_identifier
|
||||
from ansible_collections.community.general.plugins.module_utils.mysql import mysql_connect, mysql_driver, mysql_driver_fail_msg
|
||||
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():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
login_user=dict(type='str'),
|
||||
login_password=dict(type='str', no_log=True),
|
||||
login_host=dict(type='str', default='localhost'),
|
||||
login_port=dict(type='int', default=3306),
|
||||
login_unix_socket=dict(type='str'),
|
||||
variable=dict(type='str'),
|
||||
value=dict(type='str'),
|
||||
client_cert=dict(type='path', aliases=['ssl_cert']),
|
||||
client_key=dict(type='path', aliases=['ssl_key']),
|
||||
ca_cert=dict(type='path', aliases=['ssl_ca']),
|
||||
connect_timeout=dict(type='int', default=30),
|
||||
config_file=dict(type='path', default='~/.my.cnf'),
|
||||
mode=dict(type='str', choices=['global', 'persist', 'persist_only'], default='global'),
|
||||
),
|
||||
)
|
||||
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"]
|
||||
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-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)
|
||||
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()
|
Loading…
Add table
Add a link
Reference in a new issue