Init: mediaserver

This commit is contained in:
2023-02-08 12:13:28 +01:00
parent 848bc9739c
commit f7c23d4ba9
31914 changed files with 6175775 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
# Collections Plugins Directory
This directory can be used to ship various plugins inside an Ansible collection. Each plugin is placed in a folder that
is named after the type of plugin it is in. It can also include the `module_utils` and `modules` directory that
would contain module utils and modules respectively.
Here is an example directory of the majority of plugins currently supported by Ansible:
```
└── plugins
├── action
├── become
├── cache
├── callback
├── cliconf
├── connection
├── filter
├── httpapi
├── inventory
├── lookup
├── module_utils
├── modules
├── netconf
├── shell
├── strategy
├── terminal
├── test
└── vars
```
A full list of plugin types can be found at [Working With Plugins](https://docs.ansible.com/ansible/latest/plugins/plugins.html).

View File

@@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2015, Jonathan Mainguy <jon@soh.re>
# 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
class ModuleDocFragment(object):
# Standard mysql documentation fragment
DOCUMENTATION = r'''
options:
login_user:
description:
- The username used to authenticate with.
type: str
login_password:
description:
- The password used to authenticate with.
type: str
login_host:
description:
- Host running the database.
- In some cases for local connections the I(login_unix_socket=/path/to/mysqld/socket),
that is usually C(/var/run/mysqld/mysqld.sock), needs to be used instead of I(login_host=localhost).
type: str
default: localhost
login_port:
description:
- Port of the MySQL server. Requires I(login_host) be defined as other than localhost if login_port is used.
type: int
default: 3306
login_unix_socket:
description:
- The path to a Unix domain socket for local connections.
- Use this parameter to avoid the C(Please explicitly state intended protocol) error.
type: str
connect_timeout:
description:
- The connection timeout when connecting to the MySQL server.
type: int
default: 30
config_file:
description:
- Specify a config file from which user and password are to be read.
- The default config file, C(~/.my.cnf), if it exists, will be read, even if I(config_file) is not specified.
- The default config file, C(~/.my.cnf), if it exists, must contain a C([client]) section as a MySQL connector requirement.
- To prevent the default config file from being read, set I(config_file) to be an empty string.
type: path
default: '~/.my.cnf'
ca_cert:
description:
- The path to a Certificate Authority (CA) certificate. This option, if used, must specify the same certificate
as used by the server.
type: path
aliases: [ ssl_ca ]
client_cert:
description:
- The path to a client public key certificate.
type: path
aliases: [ ssl_cert ]
client_key:
description:
- The path to the client private key.
type: path
aliases: [ ssl_key ]
check_hostname:
description:
- Whether to validate the server host name when an SSL connection is required. Corresponds to MySQL CLIs C(--ssl) switch.
- Setting this to C(false) disables hostname verification. Use with caution.
- Requires pymysql >= 0.7.11.
- This option has no effect on MySQLdb.
type: bool
version_added: '1.1.0'
requirements:
- mysqlclient (Python 3.5+) or
- PyMySQL (Python 2.7 and Python 3.x) or
- MySQLdb (Python 2.x)
notes:
- "To avoid the C(Please explicitly state intended protocol) error, use the I(login_unix_socket) argument,
for example, C(login_unix_socket: /run/mysqld/mysqld.sock)."
- Requires the PyMySQL (Python 2.7 and Python 3.X) or MySQL-python (Python 2.X) package installed on the remote host.
The Python package may be installed with apt-get install python-pymysql (Ubuntu; see M(ansible.builtin.apt)) or
yum install python2-PyMySQL (RHEL/CentOS/Fedora; see M(ansible.builtin.yum)). You can also use dnf install python2-PyMySQL
for newer versions of Fedora; see M(ansible.builtin.dnf).
- Be sure you have mysqlclient, PyMySQL, or MySQLdb library installed on the target machine
for the Python interpreter Ansible discovers. For example if ansible discovers and uses Python 3, you need to install
the Python 3 version of PyMySQL or mysqlclient. If ansible discovers and uses Python 2, you need to install the Python 2
version of either PyMySQL or MySQL-python.
- If you have trouble, it may help to force Ansible to use the Python interpreter you need by specifying
C(ansible_python_interpreter). For more information, see
U(https://docs.ansible.com/ansible/latest/reference_appendices/interpreter_discovery.html).
- Both C(login_password) and C(login_user) are required when you are
passing credentials. If none are present, the module will attempt to read
the credentials from C(~/.my.cnf), and finally fall back to using the MySQL
default login of 'root' with no password.
- If there are problems with local connections, using I(login_unix_socket=/path/to/mysqld/socket)
instead of I(login_host=localhost) might help. As an example, the default MariaDB installation of version 10.4
and later uses the unix_socket authentication plugin by default that
without using I(login_unix_socket=/var/run/mysqld/mysqld.sock) (the default path)
causes the error ``Host '127.0.0.1' is not allowed to connect to this MariaDB server``.
- Alternatively, you can use the mysqlclient library instead of MySQL-python (MySQLdb)
which supports both Python 2.X and Python >=3.5.
See U(https://pypi.org/project/mysqlclient/) how to install it.
- "If credentials from the config file (for example, C(/root/.my.cnf)) are not needed to connect to a database server, but
the file exists and does not contain a C([client]) section, before any other valid directives, it will be read and this
will cause the connection to fail, to prevent this set it to an empty string, (for example C(config_file: ''))."
'''

View File

@@ -0,0 +1,343 @@
# Vendored copy of distutils/version.py from CPython 3.9.5
#
# Implements multiple version numbering conventions for the
# Python Module Distribution Utilities.
#
# PSF License (see PSF-license.txt or https://opensource.org/licenses/Python-2.0)
#
"""Provides classes to represent module version numbers (one class for
each style of version numbering). There are currently two such classes
implemented: StrictVersion and LooseVersion.
Every version number class implements the following interface:
* the 'parse' method takes a string and parses it to some internal
representation; if the string is an invalid version number,
'parse' raises a ValueError exception
* the class constructor takes an optional string argument which,
if supplied, is passed to 'parse'
* __str__ reconstructs the string that was passed to 'parse' (or
an equivalent string -- ie. one that will generate an equivalent
version number instance)
* __repr__ generates Python code to recreate the version number instance
* _cmp compares the current instance with either another instance
of the same class or a string (which will be parsed to an instance
of the same class, thus must follow the same rules)
"""
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import re
try:
RE_FLAGS = re.VERBOSE | re.ASCII
except AttributeError:
RE_FLAGS = re.VERBOSE
class Version:
"""Abstract base class for version numbering classes. Just provides
constructor (__init__) and reproducer (__repr__), because those
seem to be the same for all version numbering classes; and route
rich comparisons to _cmp.
"""
def __init__(self, vstring=None):
if vstring:
self.parse(vstring)
def __repr__(self):
return "%s ('%s')" % (self.__class__.__name__, str(self))
def __eq__(self, other):
c = self._cmp(other)
if c is NotImplemented:
return c
return c == 0
def __lt__(self, other):
c = self._cmp(other)
if c is NotImplemented:
return c
return c < 0
def __le__(self, other):
c = self._cmp(other)
if c is NotImplemented:
return c
return c <= 0
def __gt__(self, other):
c = self._cmp(other)
if c is NotImplemented:
return c
return c > 0
def __ge__(self, other):
c = self._cmp(other)
if c is NotImplemented:
return c
return c >= 0
# Interface for version-number classes -- must be implemented
# by the following classes (the concrete ones -- Version should
# be treated as an abstract class).
# __init__ (string) - create and take same action as 'parse'
# (string parameter is optional)
# parse (string) - convert a string representation to whatever
# internal representation is appropriate for
# this style of version numbering
# __str__ (self) - convert back to a string; should be very similar
# (if not identical to) the string supplied to parse
# __repr__ (self) - generate Python code to recreate
# the instance
# _cmp (self, other) - compare two version numbers ('other' may
# be an unparsed version string, or another
# instance of your version class)
class StrictVersion(Version):
"""Version numbering for anal retentives and software idealists.
Implements the standard interface for version number classes as
described above. A version number consists of two or three
dot-separated numeric components, with an optional "pre-release" tag
on the end. The pre-release tag consists of the letter 'a' or 'b'
followed by a number. If the numeric components of two version
numbers are equal, then one with a pre-release tag will always
be deemed earlier (lesser) than one without.
The following are valid version numbers (shown in the order that
would be obtained by sorting according to the supplied cmp function):
0.4 0.4.0 (these two are equivalent)
0.4.1
0.5a1
0.5b3
0.5
0.9.6
1.0
1.0.4a3
1.0.4b1
1.0.4
The following are examples of invalid version numbers:
1
2.7.2.2
1.3.a4
1.3pl1
1.3c4
The rationale for this version numbering system will be explained
in the distutils documentation.
"""
version_re = re.compile(r'^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$',
RE_FLAGS)
def parse(self, vstring):
match = self.version_re.match(vstring)
if not match:
raise ValueError("invalid version number '%s'" % vstring)
(major, minor, patch, prerelease, prerelease_num) = \
match.group(1, 2, 4, 5, 6)
if patch:
self.version = tuple(map(int, [major, minor, patch]))
else:
self.version = tuple(map(int, [major, minor])) + (0,)
if prerelease:
self.prerelease = (prerelease[0], int(prerelease_num))
else:
self.prerelease = None
def __str__(self):
if self.version[2] == 0:
vstring = '.'.join(map(str, self.version[0:2]))
else:
vstring = '.'.join(map(str, self.version))
if self.prerelease:
vstring = vstring + self.prerelease[0] + str(self.prerelease[1])
return vstring
def _cmp(self, other):
if isinstance(other, str):
other = StrictVersion(other)
elif not isinstance(other, StrictVersion):
return NotImplemented
if self.version != other.version:
# numeric versions don't match
# prerelease stuff doesn't matter
if self.version < other.version:
return -1
else:
return 1
# have to compare prerelease
# case 1: neither has prerelease; they're equal
# case 2: self has prerelease, other doesn't; other is greater
# case 3: self doesn't have prerelease, other does: self is greater
# case 4: both have prerelease: must compare them!
if (not self.prerelease and not other.prerelease):
return 0
elif (self.prerelease and not other.prerelease):
return -1
elif (not self.prerelease and other.prerelease):
return 1
elif (self.prerelease and other.prerelease):
if self.prerelease == other.prerelease:
return 0
elif self.prerelease < other.prerelease:
return -1
else:
return 1
else:
raise AssertionError("never get here")
# end class StrictVersion
# The rules according to Greg Stein:
# 1) a version number has 1 or more numbers separated by a period or by
# sequences of letters. If only periods, then these are compared
# left-to-right to determine an ordering.
# 2) sequences of letters are part of the tuple for comparison and are
# compared lexicographically
# 3) recognize the numeric components may have leading zeroes
#
# The LooseVersion class below implements these rules: a version number
# string is split up into a tuple of integer and string components, and
# comparison is a simple tuple comparison. This means that version
# numbers behave in a predictable and obvious way, but a way that might
# not necessarily be how people *want* version numbers to behave. There
# wouldn't be a problem if people could stick to purely numeric version
# numbers: just split on period and compare the numbers as tuples.
# However, people insist on putting letters into their version numbers;
# the most common purpose seems to be:
# - indicating a "pre-release" version
# ('alpha', 'beta', 'a', 'b', 'pre', 'p')
# - indicating a post-release patch ('p', 'pl', 'patch')
# but of course this can't cover all version number schemes, and there's
# no way to know what a programmer means without asking him.
#
# The problem is what to do with letters (and other non-numeric
# characters) in a version number. The current implementation does the
# obvious and predictable thing: keep them as strings and compare
# lexically within a tuple comparison. This has the desired effect if
# an appended letter sequence implies something "post-release":
# eg. "0.99" < "0.99pl14" < "1.0", and "5.001" < "5.001m" < "5.002".
#
# However, if letters in a version number imply a pre-release version,
# the "obvious" thing isn't correct. Eg. you would expect that
# "1.5.1" < "1.5.2a2" < "1.5.2", but under the tuple/lexical comparison
# implemented here, this just isn't so.
#
# Two possible solutions come to mind. The first is to tie the
# comparison algorithm to a particular set of semantic rules, as has
# been done in the StrictVersion class above. This works great as long
# as everyone can go along with bondage and discipline. Hopefully a
# (large) subset of Python module programmers will agree that the
# particular flavour of bondage and discipline provided by StrictVersion
# provides enough benefit to be worth using, and will submit their
# version numbering scheme to its domination. The free-thinking
# anarchists in the lot will never give in, though, and something needs
# to be done to accommodate them.
#
# Perhaps a "moderately strict" version class could be implemented that
# lets almost anything slide (syntactically), and makes some heuristic
# assumptions about non-digits in version number strings. This could
# sink into special-case-hell, though; if I was as talented and
# idiosyncratic as Larry Wall, I'd go ahead and implement a class that
# somehow knows that "1.2.1" < "1.2.2a2" < "1.2.2" < "1.2.2pl3", and is
# just as happy dealing with things like "2g6" and "1.13++". I don't
# think I'm smart enough to do it right though.
#
# In any case, I've coded the test suite for this module (see
# ../test/test_version.py) specifically to fail on things like comparing
# "1.2a2" and "1.2". That's not because the *code* is doing anything
# wrong, it's because the simple, obvious design doesn't match my
# complicated, hairy expectations for real-world version numbers. It
# would be a snap to fix the test suite to say, "Yep, LooseVersion does
# the Right Thing" (ie. the code matches the conception). But I'd rather
# have a conception that matches common notions about version numbers.
class LooseVersion(Version):
"""Version numbering for anarchists and software realists.
Implements the standard interface for version number classes as
described above. A version number consists of a series of numbers,
separated by either periods or strings of letters. When comparing
version numbers, the numeric components will be compared
numerically, and the alphabetic components lexically. The following
are all valid version numbers, in no particular order:
1.5.1
1.5.2b2
161
3.10a
8.02
3.4j
1996.07.12
3.2.pl0
3.1.1.6
2g6
11g
0.960923
2.2beta29
1.13++
5.5.kw
2.0b1pl0
In fact, there is no such thing as an invalid version number under
this scheme; the rules for comparison are simple and predictable,
but may not always give the results you want (for some definition
of "want").
"""
component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE)
def __init__(self, vstring=None):
if vstring:
self.parse(vstring)
def parse(self, vstring):
# I've given up on thinking I can reconstruct the version string
# from the parsed tuple -- so I just store the string here for
# use by __str__
self.vstring = vstring
components = [x for x in self.component_re.split(vstring) if x and x != '.']
for i, obj in enumerate(components):
try:
components[i] = int(obj)
except ValueError:
pass
self.version = components
def __str__(self):
return self.vstring
def __repr__(self):
return "LooseVersion ('%s')" % str(self)
def _cmp(self, other):
if isinstance(other, str):
other = LooseVersion(other)
elif not isinstance(other, LooseVersion):
return NotImplemented
if self.version == other.version:
return 0
if self.version < other.version:
return -1
if self.version > other.version:
return 1
# end class LooseVersion

View File

@@ -0,0 +1,189 @@
# This code is part of Ansible, but is an independent component.
# This particular file snippet, and this file snippet only, is BSD licensed.
# Modules you write using this snippet, which is embedded dynamically by Ansible
# still belong to the author of the module, and may assign their own license
# to the complete work.
#
# Copyright (c) 2014, Toshio Kuratomi <tkuratomi@ansible.com>
#
# Simplified BSD License (see simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import re
# Input patterns for is_input_dangerous function:
#
# 1. '"' in string and '--' in string or
# "'" in string and '--' in string
PATTERN_1 = re.compile(r'(\'|\").*--')
# 2. union \ intersect \ except + select
PATTERN_2 = re.compile(r'(UNION|INTERSECT|EXCEPT).*SELECT', re.IGNORECASE)
# 3. ';' and any KEY_WORDS
PATTERN_3 = re.compile(r';.*(SELECT|UPDATE|INSERT|DELETE|DROP|TRUNCATE|ALTER)', re.IGNORECASE)
class SQLParseError(Exception):
pass
class UnclosedQuoteError(SQLParseError):
pass
# maps a type of identifier to the maximum number of dot levels that are
# allowed to specify that identifier. For example, a database column can be
# specified by up to 4 levels: database.schema.table.column
_PG_IDENTIFIER_TO_DOT_LEVEL = dict(
database=1,
schema=2,
table=3,
column=4,
role=1,
tablespace=1,
sequence=3,
publication=1,
)
_MYSQL_IDENTIFIER_TO_DOT_LEVEL = dict(database=1, table=2, column=3, role=1, vars=1)
def _find_end_quote(identifier, quote_char):
accumulate = 0
while True:
try:
quote = identifier.index(quote_char)
except ValueError:
raise UnclosedQuoteError
accumulate = accumulate + quote
try:
next_char = identifier[quote + 1]
except IndexError:
return accumulate
if next_char == quote_char:
try:
identifier = identifier[quote + 2:]
accumulate = accumulate + 2
except IndexError:
raise UnclosedQuoteError
else:
return accumulate
def _identifier_parse(identifier, quote_char):
if not identifier:
raise SQLParseError('Identifier name unspecified or unquoted trailing dot')
already_quoted = False
if identifier.startswith(quote_char):
already_quoted = True
try:
end_quote = _find_end_quote(identifier[1:], quote_char=quote_char) + 1
except UnclosedQuoteError:
already_quoted = False
else:
if end_quote < len(identifier) - 1:
if identifier[end_quote + 1] == '.':
dot = end_quote + 1
first_identifier = identifier[:dot]
next_identifier = identifier[dot + 1:]
further_identifiers = _identifier_parse(next_identifier, quote_char)
further_identifiers.insert(0, first_identifier)
else:
raise SQLParseError('User escaped identifiers must escape extra quotes')
else:
further_identifiers = [identifier]
if not already_quoted:
try:
dot = identifier.index('.')
except ValueError:
identifier = identifier.replace(quote_char, quote_char * 2)
identifier = ''.join((quote_char, identifier, quote_char))
further_identifiers = [identifier]
else:
if dot == 0 or dot >= len(identifier) - 1:
identifier = identifier.replace(quote_char, quote_char * 2)
identifier = ''.join((quote_char, identifier, quote_char))
further_identifiers = [identifier]
else:
first_identifier = identifier[:dot]
next_identifier = identifier[dot + 1:]
further_identifiers = _identifier_parse(next_identifier, quote_char)
first_identifier = first_identifier.replace(quote_char, quote_char * 2)
first_identifier = ''.join((quote_char, first_identifier, quote_char))
further_identifiers.insert(0, first_identifier)
return further_identifiers
def pg_quote_identifier(identifier, id_type):
identifier_fragments = _identifier_parse(identifier, quote_char='"')
if len(identifier_fragments) > _PG_IDENTIFIER_TO_DOT_LEVEL[id_type]:
raise SQLParseError('PostgreSQL does not support %s with more than %i dots' % (id_type, _PG_IDENTIFIER_TO_DOT_LEVEL[id_type]))
return '.'.join(identifier_fragments)
def mysql_quote_identifier(identifier, id_type):
identifier_fragments = _identifier_parse(identifier, quote_char='`')
if (len(identifier_fragments) - 1) > _MYSQL_IDENTIFIER_TO_DOT_LEVEL[id_type]:
raise SQLParseError('MySQL does not support %s with more than %i dots' % (id_type, _MYSQL_IDENTIFIER_TO_DOT_LEVEL[id_type]))
special_cased_fragments = []
for fragment in identifier_fragments:
if fragment == '`*`':
special_cased_fragments.append('*')
else:
special_cased_fragments.append(fragment)
return '.'.join(special_cased_fragments)
def is_input_dangerous(string):
"""Check if the passed string is potentially dangerous.
Can be used to prevent SQL injections.
Note: use this function only when you can't use
psycopg2's cursor.execute method parametrized
(typically with DDL queries).
"""
if not string:
return False
for pattern in (PATTERN_1, PATTERN_2, PATTERN_3):
if re.search(pattern, string):
return True
return False
def check_input(module, *args):
"""Wrapper for is_input_dangerous function."""
needs_to_check = args
dangerous_elements = []
for elem in needs_to_check:
if isinstance(elem, str):
if is_input_dangerous(elem):
dangerous_elements.append(elem)
elif isinstance(elem, list):
for e in elem:
if is_input_dangerous(e):
dangerous_elements.append(e)
elif elem is None or isinstance(elem, bool):
pass
else:
elem = str(elem)
if is_input_dangerous(elem):
dangerous_elements.append(elem)
if dangerous_elements:
module.fail_json(msg="Passed input '%s' is "
"potentially dangerous" % ', '.join(dangerous_elements))

View File

@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# 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
from ansible_collections.community.mysql.plugins.module_utils.mysql import get_server_version
from ansible_collections.community.mysql.plugins.module_utils.version import LooseVersion
def uses_replica_terminology(cursor):
"""Checks if REPLICA must be used instead of SLAVE"""
return LooseVersion(get_server_version(cursor)) >= LooseVersion('10.5.1')

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# 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
from ansible_collections.community.mysql.plugins.module_utils.version import LooseVersion
from ansible_collections.community.mysql.plugins.module_utils.mysql import get_server_version
def supports_roles(cursor):
version = get_server_version(cursor)
return LooseVersion(version) >= LooseVersion('10.0.5')
def is_mariadb():
return True

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# 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
from ansible_collections.community.mysql.plugins.module_utils.version import LooseVersion
from ansible_collections.community.mysql.plugins.module_utils.mysql import get_server_version
def use_old_user_mgmt(cursor):
version = get_server_version(cursor)
return LooseVersion(version) < LooseVersion("10.2")
def supports_identified_by_password(cursor):
return True

View File

@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# 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
from ansible_collections.community.mysql.plugins.module_utils.mysql import get_server_version
from ansible_collections.community.mysql.plugins.module_utils.version import LooseVersion
def uses_replica_terminology(cursor):
"""Checks if REPLICA must be used instead of SLAVE"""
return LooseVersion(get_server_version(cursor)) >= LooseVersion('8.0.22')

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# 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
from ansible_collections.community.mysql.plugins.module_utils.version import LooseVersion
from ansible_collections.community.mysql.plugins.module_utils.mysql import get_server_version
def supports_roles(cursor):
version = get_server_version(cursor)
return LooseVersion(version) >= LooseVersion('8')
def is_mariadb():
return False

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# 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
from ansible_collections.community.mysql.plugins.module_utils.version import LooseVersion
from ansible_collections.community.mysql.plugins.module_utils.mysql import get_server_version
def use_old_user_mgmt(cursor):
version = get_server_version(cursor)
return LooseVersion(version) < LooseVersion("5.7")
def supports_identified_by_password(cursor):
version = get_server_version(cursor)
return LooseVersion(version) < LooseVersion("8")

View File

@@ -0,0 +1,151 @@
# This code is part of Ansible, but is an independent component.
# This particular file snippet, and this file snippet only, is BSD licensed.
# Modules you write using this snippet, which is embedded dynamically by Ansible
# still belong to the author of the module, and may assign their own license
# to the complete work.
#
# Copyright (c), Jonathan Mainguy <jon@soh.re>, 2015
# Most of this was originally added by Sven Schliesing @muffl0n in the mysql_user.py module
#
# Simplified BSD License (see simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
from __future__ import (absolute_import, division, print_function)
from functools import reduce
__metaclass__ = type
import os
from ansible.module_utils.six.moves import configparser
from ansible.module_utils._text import to_native
try:
import pymysql as mysql_driver
_mysql_cursor_param = 'cursor'
except ImportError:
try:
import MySQLdb as mysql_driver
import MySQLdb.cursors
_mysql_cursor_param = 'cursorclass'
except ImportError:
mysql_driver = None
mysql_driver_fail_msg = ('A MySQL module is required: for Python 2.7 either PyMySQL, or '
'MySQL-python, or for Python 3.X mysqlclient or PyMySQL. '
'Consider setting ansible_python_interpreter to use '
'the intended Python version.')
def parse_from_mysql_config_file(cnf):
# Default values of comment_prefix is '#' and ';'.
# '!' added to prevent a parsing error
# when a config file contains !includedir parameter.
cp = configparser.ConfigParser(comment_prefixes=('#', ';', '!'))
cp.read(cnf)
return cp
def mysql_connect(module, login_user=None, login_password=None, config_file='', ssl_cert=None,
ssl_key=None, ssl_ca=None, db=None, cursor_class=None, connect_timeout=30,
autocommit=False, config_overrides_defaults=False, check_hostname=None):
config = {}
if config_file and os.path.exists(config_file):
config['read_default_file'] = config_file
if config_overrides_defaults:
try:
cp = parse_from_mysql_config_file(config_file)
except Exception as e:
module.fail_json(msg="Failed to parse %s: %s" % (config_file, to_native(e)))
# Override some commond defaults with values from config file if needed
if cp and cp.has_section('client'):
try:
module.params['login_host'] = cp.get('client', 'host', fallback=module.params['login_host'])
module.params['login_port'] = cp.getint('client', 'port', fallback=module.params['login_port'])
except Exception as e:
if "got an unexpected keyword argument 'fallback'" in e.message:
module.fail_json(msg='To use config_overrides_defaults, '
'it needs Python 3.5+ as the default interpreter on a target host')
if ssl_ca is not None or ssl_key is not None or ssl_cert is not None or check_hostname is not None:
config['ssl'] = {}
if module.params['login_unix_socket']:
config['unix_socket'] = module.params['login_unix_socket']
else:
config['host'] = module.params['login_host']
config['port'] = module.params['login_port']
# If login_user or login_password are given, they should override the
# config file
if login_user is not None:
config['user'] = login_user
if login_password is not None:
config['passwd'] = login_password
if ssl_cert is not None:
config['ssl']['cert'] = ssl_cert
if ssl_key is not None:
config['ssl']['key'] = ssl_key
if ssl_ca is not None:
config['ssl']['ca'] = ssl_ca
if db is not None:
config['db'] = db
if connect_timeout is not None:
config['connect_timeout'] = connect_timeout
if check_hostname is not None:
if mysql_driver.__name__ == "pymysql":
version_tuple = (n for n in mysql_driver.__version__.split('.') if n != 'None')
if reduce(lambda x, y: int(x) * 100 + int(y), version_tuple) >= 711:
config['ssl']['check_hostname'] = check_hostname
else:
module.fail_json(msg='To use check_hostname, pymysql >= 0.7.11 is required on the target host')
if _mysql_cursor_param == 'cursor':
# In case of PyMySQL driver:
db_connection = mysql_driver.connect(autocommit=autocommit, **config)
else:
# In case of MySQLdb driver
db_connection = mysql_driver.connect(**config)
if autocommit:
db_connection.autocommit(True)
# Monkey patch the Connection class to close the connection when garbage collected
def _conn_patch(conn_self):
conn_self.close()
db_connection.__class__.__del__ = _conn_patch
# Patched
if cursor_class == 'DictCursor':
return db_connection.cursor(**{_mysql_cursor_param: mysql_driver.cursors.DictCursor}), db_connection
else:
return db_connection.cursor(), db_connection
def mysql_common_argument_spec():
return dict(
login_user=dict(type='str', default=None),
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'),
config_file=dict(type='path', default='~/.my.cnf'),
connect_timeout=dict(type='int', default=30),
client_cert=dict(type='path', aliases=['ssl_cert']),
client_key=dict(type='path', aliases=['ssl_key']),
ca_cert=dict(type='path', aliases=['ssl_ca']),
check_hostname=dict(type='bool', default=None),
)
def get_server_version(cursor):
"""Returns a string representation of the server version."""
cursor.execute("SELECT VERSION() AS version")
result = cursor.fetchone()
if isinstance(result, dict):
version_str = result['version']
else:
version_str = result[0]
return version_str

View File

@@ -0,0 +1,885 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
# This code is part of Ansible, but is an independent component.
# This particular file snippet, and this file snippet only, is BSD licensed.
# Modules you write using this snippet, which is embedded dynamically by Ansible
# still belong to the author of the module, and may assign their own license
# to the complete work.
#
# Simplified BSD License (see simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
import string
import re
from ansible.module_utils.six import iteritems
from ansible_collections.community.mysql.plugins.module_utils.mysql import (
mysql_driver,
)
class InvalidPrivsError(Exception):
pass
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 sanitize_requires(tls_requires):
sanitized_requires = {}
if tls_requires:
for key in tls_requires.keys():
sanitized_requires[key.upper()] = tls_requires[key]
if any(key in ["CIPHER", "ISSUER", "SUBJECT"] for key in sanitized_requires.keys()):
sanitized_requires.pop("SSL", None)
sanitized_requires.pop("X509", None)
return sanitized_requires
if "X509" in sanitized_requires.keys():
sanitized_requires = "X509"
else:
sanitized_requires = "SSL"
return sanitized_requires
return None
def mogrify_requires(query, params, tls_requires):
if tls_requires:
if isinstance(tls_requires, dict):
k, v = zip(*tls_requires.items())
requires_query = " AND ".join(("%s %%s" % key for key in k))
params += v
else:
requires_query = tls_requires
query = " REQUIRE ".join((query, requires_query))
return query, params
def do_not_mogrify_requires(query, params, tls_requires):
return query, params
def get_tls_requires(cursor, user, host):
if user:
if not impl.use_old_user_mgmt(cursor):
query = "SHOW CREATE USER '%s'@'%s'" % (user, host)
else:
query = "SHOW GRANTS for '%s'@'%s'" % (user, host)
cursor.execute(query)
require_list = [tuple[0] for tuple in filter(lambda x: "REQUIRE" in x[0], cursor.fetchall())]
require_line = require_list[0] if require_list else ""
pattern = r"(?<=\bREQUIRE\b)(.*?)(?=(?:\bPASSWORD\b|$))"
requires_match = re.search(pattern, require_line)
requires = requires_match.group().strip() if requires_match else ""
if any((requires.startswith(req) for req in ('SSL', 'X509', 'NONE'))):
requires = requires.split()[0]
if requires == 'NONE':
requires = None
else:
import shlex
items = iter(shlex.split(requires))
requires = dict(zip(items, items))
return requires or None
def get_grants(cursor, user, host):
cursor.execute("SHOW GRANTS FOR %s@%s", (user, host))
grants_line = list(filter(lambda x: "ON *.*" in x[0], cursor.fetchall()))[0]
pattern = r"(?<=\bGRANT\b)(.*?)(?=(?:\bON\b))"
grants = re.search(pattern, grants_line[0]).group().strip()
return grants.split(", ")
def get_existing_authentication(cursor, user):
# Return the plugin and auth_string if there is exactly one distinct existing plugin and auth_string.
cursor.execute("SELECT VERSION()")
if 'mariadb' in cursor.fetchone()[0].lower():
# before MariaDB 10.2.19 and 10.3.11, "password" and "authentication_string" can differ
# when using mysql_native_password
cursor.execute("""select plugin, auth from (
select plugin, password as auth from mysql.user where user=%(user)s
union select plugin, authentication_string as auth from mysql.user where user=%(user)s
) x group by plugin, auth limit 2
""", {'user': user})
else:
cursor.execute("""select plugin, authentication_string as auth from mysql.user where user=%(user)s
group by plugin, authentication_string limit 2""", {'user': user})
rows = cursor.fetchall()
if len(rows) == 1:
return {'plugin': rows[0][0], 'auth_string': rows[0][1]}
return None
def user_add(cursor, user, host, host_all, password, encrypted,
plugin, plugin_hash_string, plugin_auth_string, new_priv,
tls_requires, check_mode, reuse_existing_password):
# we cannot create users without a proper hostname
if host_all:
return {'changed': False, 'password_changed': False}
if check_mode:
return {'changed': True, 'password_changed': None}
# Determine what user management method server uses
old_user_mgmt = impl.use_old_user_mgmt(cursor)
mogrify = do_not_mogrify_requires if old_user_mgmt else mogrify_requires
used_existing_password = False
if reuse_existing_password:
existing_auth = get_existing_authentication(cursor, user)
if existing_auth:
plugin = existing_auth['plugin']
plugin_hash_string = existing_auth['auth_string']
password = None
used_existing_password = True
if password and encrypted:
if impl.supports_identified_by_password(cursor):
query_with_args = "CREATE USER %s@%s IDENTIFIED BY PASSWORD %s", (user, host, password)
else:
query_with_args = "CREATE USER %s@%s IDENTIFIED WITH mysql_native_password AS %s", (user, host, password)
elif password and not encrypted:
if old_user_mgmt:
query_with_args = "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]
query_with_args = "CREATE USER %s@%s IDENTIFIED WITH mysql_native_password AS %s", (user, host, encrypted_password)
elif plugin and plugin_hash_string:
query_with_args = "CREATE USER %s@%s IDENTIFIED WITH %s AS %s", (user, host, plugin, plugin_hash_string)
elif plugin and plugin_auth_string:
query_with_args = "CREATE USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, plugin_auth_string)
elif plugin:
query_with_args = "CREATE USER %s@%s IDENTIFIED WITH %s", (user, host, plugin)
else:
query_with_args = "CREATE USER %s@%s", (user, host)
query_with_args_and_tls_requires = query_with_args + (tls_requires,)
cursor.execute(*mogrify(*query_with_args_and_tls_requires))
if new_priv is not None:
for db_table, priv in iteritems(new_priv):
privileges_grant(cursor, user, host, db_table, priv, tls_requires)
if tls_requires is not None:
privileges_grant(cursor, user, host, "*.*", get_grants(cursor, user, host), tls_requires)
return {'changed': True, 'password_changed': not used_existing_password}
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, subtract_privs, tls_requires, module, role=False, maria_role=False):
changed = False
msg = "User unchanged"
grant_option = False
# Determine what user management method server uses
old_user_mgmt = impl.use_old_user_mgmt(cursor)
if host_all and not role:
hostnames = user_get_hostnames(cursor, user)
else:
hostnames = [host]
password_changed = False
for host in hostnames:
# Handle clear text and hashed passwords.
if not role:
if bool(password):
# 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:
password_changed = True
msg = "Password updated"
if module.check_mode:
return {'changed': True, 'msg': msg, 'password_changed': password_changed}
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 and not role:
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:
query_with_args = "ALTER USER %s@%s IDENTIFIED WITH %s AS %s", (user, host, plugin, plugin_hash_string)
elif plugin_auth_string:
query_with_args = "ALTER USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, plugin_auth_string)
else:
query_with_args = "ALTER USER %s@%s IDENTIFIED WITH %s", (user, host, plugin)
cursor.execute(*query_with_args)
password_changed = True
changed = True
# Handle privileges
if new_priv is not None:
curr_priv = privileges_get(cursor, user, host, maria_role)
# 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.
if not append_privs and not subtract_privs:
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:
msg = "Privileges updated"
if module.check_mode:
return {'changed': True, 'msg': msg, 'password_changed': password_changed}
privileges_revoke(cursor, user, host, db_table, priv, grant_option, maria_role)
changed = True
# If the user doesn't currently have any privileges on a db.table, then
# we can perform a straight grant operation.
if not subtract_privs:
for db_table, priv in iteritems(new_priv):
if db_table not in curr_priv:
msg = "New privileges granted"
if module.check_mode:
return {'changed': True, 'msg': msg, 'password_changed': password_changed}
privileges_grant(cursor, user, host, db_table, priv, tls_requires, maria_role)
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:
grant_privs = []
revoke_privs = []
if append_privs:
# When appending privileges, only missing privileges need to be granted. Nothing is revoked.
grant_privs = list(set(new_priv[db_table]) - set(curr_priv[db_table]))
elif subtract_privs:
# When subtracting privileges, revoke only the intersection of requested and current privileges.
# No privileges are granted.
revoke_privs = list(set(new_priv[db_table]) & set(curr_priv[db_table]))
else:
# When replacing (neither append_privs nor subtract_privs), grant all missing privileges
# and revoke existing privileges that were not requested...
grant_privs = list(set(new_priv[db_table]) - set(curr_priv[db_table]))
revoke_privs = list(set(curr_priv[db_table]) - set(new_priv[db_table]))
# ... avoiding pointless revocations when ALL are granted
if 'ALL' in grant_privs or 'ALL PRIVILEGES' in grant_privs:
revoke_privs = list(set(['GRANT', 'PROXY']).intersection(set(revoke_privs)))
# Only revoke grant option if it exists and absence is requested
#
# For more details
# https://github.com/ansible-collections/community.mysql/issues/77#issuecomment-1209693807
grant_option = 'GRANT' in revoke_privs and 'GRANT' not in grant_privs
if grant_privs == ['GRANT']:
# USAGE grants no privileges, it is only needed because 'WITH GRANT OPTION' cannot stand alone
grant_privs.append('USAGE')
if len(grant_privs) + len(revoke_privs) > 0:
msg = "Privileges updated: granted %s, revoked %s" % (grant_privs, revoke_privs)
if module.check_mode:
return {'changed': True, 'msg': msg, 'password_changed': password_changed}
if len(revoke_privs) > 0:
privileges_revoke(cursor, user, host, db_table, revoke_privs, grant_option, maria_role)
if len(grant_privs) > 0:
privileges_grant(cursor, user, host, db_table, grant_privs, tls_requires, maria_role)
# after privilege manipulation, compare privileges from before and now
after_priv = privileges_get(cursor, user, host, maria_role)
changed = changed or (curr_priv != after_priv)
if role:
continue
# Handle TLS requirements
current_requires = get_tls_requires(cursor, user, host)
if current_requires != tls_requires:
msg = "TLS requires updated"
if module.check_mode:
return {'changed': True, 'msg': msg, 'password_changed': password_changed}
if not old_user_mgmt:
pre_query = "ALTER USER"
else:
pre_query = "GRANT %s ON *.* TO" % ",".join(get_grants(cursor, user, host))
if tls_requires is not None:
query = " ".join((pre_query, "%s@%s"))
query_with_args = mogrify_requires(query, (user, host), tls_requires)
else:
query = " ".join((pre_query, "%s@%s REQUIRE NONE"))
query_with_args = query, (user, host)
cursor.execute(*query_with_args)
changed = True
return {'changed': changed, 'msg': msg, 'password_changed': password_changed}
def user_delete(cursor, user, host, host_all, check_mode):
if check_mode:
return True
if host_all:
hostnames = user_get_hostnames(cursor, user)
else:
hostnames = [host]
for hostname in hostnames:
try:
cursor.execute("DROP USER IF EXISTS %s@%s", (user, hostname))
except Exception:
cursor.execute("DROP USER %s@%s", (user, hostname))
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, maria_role=False):
""" 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 = {}
if not maria_role:
cursor.execute("SHOW GRANTS FOR %s@%s", (user, host))
else:
cursor.execute("SHOW GRANTS FOR %s", (user,))
grants = cursor.fetchall()
def pick(x):
if x == 'ALL PRIVILEGES':
return 'ALL'
else:
return x
for grant in grants:
if not maria_role:
res = re.match("""GRANT (.+) ON (.+) TO (['`"]).*\\3@(['`"]).*\\4( IDENTIFIED BY PASSWORD (['`"]).+\\6)? ?(.*)""", grant[0])
else:
res = re.match("""GRANT (.+) ON (.+) TO (['`"]).*\\3""", grant[0])
if res is None:
# If a user has roles assigned, we'll have one of priv tuples looking like
# GRANT `admin`@`%` TO `user1`@`localhost`
# which will result None as res value.
# As we use the mysql_role module to manipulate roles
# we just ignore such privs below:
res = re.match("""GRANT (.+) TO (['`"]).*""", grant[0])
if not maria_role and res:
continue
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]
# Handle cases when there's privs like GRANT SELECT (colA, ...) in privs.
# To this point, the privileges list can look like
# ['SELECT (`A`', '`B`)', 'INSERT'] that is incorrect (SELECT statement is splitted).
# Columns should also be sorted to compare it with desired privileges later.
# Determine if there's a case similar to the above:
privileges = normalize_col_grants(privileges)
if not maria_role:
if "WITH GRANT OPTION" in res.group(7):
privileges.append('GRANT')
db = res.group(2)
output.setdefault(db, []).extend(privileges)
return output
def normalize_col_grants(privileges):
"""Fix and sort grants on columns in privileges list
Make ['SELECT (A, B)', 'INSERT (A, B)', 'DETELE']
from ['SELECT (A', 'B)', 'INSERT (B', 'A)', 'DELETE'].
See unit tests in tests/unit/plugins/modules/test_mysql_user.py
"""
for grant in ('SELECT', 'UPDATE', 'INSERT', 'REFERENCES'):
start, end = has_grant_on_col(privileges, grant)
# If not, either start and end will be None
if start is not None:
privileges = handle_grant_on_col(privileges, start, end)
return privileges
def has_grant_on_col(privileges, grant):
"""Check if there is a statement like SELECT (colA, colB)
in the privilege list.
Return (start index, end index).
"""
# Determine elements of privileges where
# columns are listed
start = None
end = None
for n, priv in enumerate(privileges):
if '%s (' % grant in priv:
# We found the start element
start = n
if start is not None and ')' in priv:
# We found the end element
end = n
break
if start is not None and end is not None:
# if the privileges list consist of, for example,
# ['SELECT (A', 'B), 'INSERT'], return indexes of related elements
return start, end
else:
# If start and end position is the same element,
# it means there's expression like 'SELECT (A)',
# so no need to handle it
return None, None
def handle_grant_on_col(privileges, start, end):
"""Handle cases when the privs like SELECT (colA, ...) is in the privileges list."""
# When the privileges list look like ['SELECT (colA,', 'colB)']
# (Notice that the statement is splitted)
if start != end:
output = list(privileges[:start])
select_on_col = ', '.join(privileges[start:end + 1])
select_on_col = sort_column_order(select_on_col)
output.append(select_on_col)
output.extend(privileges[end + 1:])
# When it look like it should be, e.g. ['SELECT (colA, colB)'],
# we need to be sure, the columns is sorted
else:
output = list(privileges)
output[start] = sort_column_order(output[start])
return output
def sort_column_order(statement):
"""Sort column order in grants like SELECT (colA, colB, ...).
MySQL changes columns order like below:
---------------------------------------
mysql> GRANT SELECT (testColA, testColB), INSERT ON `testDb`.`testTable` TO 'testUser'@'localhost';
Query OK, 0 rows affected (0.04 sec)
mysql> flush privileges;
Query OK, 0 rows affected (0.00 sec)
mysql> SHOW GRANTS FOR testUser@localhost;
+---------------------------------------------------------------------------------------------+
| Grants for testUser@localhost |
+---------------------------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO 'testUser'@'localhost' |
| GRANT SELECT (testColB, testColA), INSERT ON `testDb`.`testTable` TO 'testUser'@'localhost' |
+---------------------------------------------------------------------------------------------+
We should sort columns in our statement, otherwise the module always will return
that the state has changed.
"""
# 1. Extract stuff inside ()
# 2. Split
# 3. Sort
# 4. Put between () and return
# "SELECT/UPDATE/.. (colA, colB) => "colA, colB"
tmp = statement.split('(')
priv_name = tmp[0]
columns = tmp[1].rstrip(')')
# "colA, colB" => ["colA", "colB"]
columns = columns.split(',')
for i, col in enumerate(columns):
col = col.strip()
columns[i] = col.strip('`')
columns.sort()
return '%s(%s)' % (priv_name, ', '.join(columns))
def privileges_unpack(priv, mode, ensure_usage=True):
""" 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]]
# Handle cases when there's privs like GRANT SELECT (colA, ...) in privs.
output[pieces[0]] = normalize_col_grants(output[pieces[0]])
if ensure_usage and '*.*' not in output:
output['*.*'] = ['USAGE']
return output
def privileges_revoke(cursor, user, host, db_table, priv, grant_option, maria_role=False):
# 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]
if not maria_role:
query.append("FROM %s@%s")
else:
query.append("FROM %s")
query = ' '.join(query)
cursor.execute(query, (user, host))
priv_string = ",".join([p for p in priv if p not in ('GRANT', )])
query = ["REVOKE %s ON %s" % (priv_string, db_table)]
if not maria_role:
query.append("FROM %s@%s")
params = (user, host)
else:
query.append("FROM %s")
params = (user,)
query = ' '.join(query)
cursor.execute(query, params)
cursor.execute("FLUSH PRIVILEGES")
def privileges_grant(cursor, user, host, db_table, priv, tls_requires, maria_role=False):
# 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', )])
query = ["GRANT %s ON %s" % (priv_string, db_table)]
if not maria_role:
query.append("TO %s@%s")
params = (user, host)
else:
query.append("TO %s")
params = (user)
if tls_requires and impl.use_old_user_mgmt(cursor):
query, params = mogrify_requires(" ".join(query), params, tls_requires)
query = [query]
if 'GRANT' in priv:
query.append("WITH GRANT OPTION")
query = ' '.join(query)
if isinstance(params, str):
params = (params,)
try:
cursor.execute(query, params)
except (mysql_driver.ProgrammingError, mysql_driver.OperationalError, mysql_driver.InternalError) as e:
raise InvalidPrivsError("Error granting privileges, invalid priv string: %s" % priv_string)
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
def get_impl(cursor):
global impl
cursor.execute("SELECT VERSION()")
if 'mariadb' in cursor.fetchone()[0].lower():
from ansible_collections.community.mysql.plugins.module_utils.implementations.mariadb import user as mariauser
impl = mariauser
else:
from ansible_collections.community.mysql.plugins.module_utils.implementations.mysql import user as mysqluser
impl = mysqluser

View File

@@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2021, Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""Provide version object to compare version numbers."""
from __future__ import absolute_import, division, print_function
__metaclass__ = type
# Once we drop support for ansible-core 2.11, we can
# remove the _version.py file, and replace the following import by
#
# from ansible.module_utils.compat.version import LooseVersion
from ._version import LooseVersion

View File

@@ -0,0 +1,763 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2012, Mark Theunissen <mark.theunissen@gmail.com>
# Sponsored by Four Kitchens http://fourkitchens.com.
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: mysql_db
short_description: Add or remove MySQL databases from a remote host
description:
- Add or remove MySQL databases from a remote host.
options:
name:
description:
- Name of the database to add or remove.
- I(name=all) may only be provided if I(state) is C(dump) or C(import).
- List of databases is provided with I(state=dump), I(state=present) and I(state=absent).
- If I(name=all) it works like --all-databases option for mysqldump (Added in 2.0).
required: true
type: list
elements: str
aliases: [db]
state:
description:
- The database state.
type: str
default: present
choices: ['absent', 'dump', 'import', 'present']
collation:
description:
- Collation mode (sorting). This only applies to new table/databases and
does not update existing ones, this is a limitation of MySQL.
type: str
default: ''
encoding:
description:
- Encoding mode to use, examples include C(utf8) or C(latin1_swedish_ci),
at creation of database, dump or importation of sql script.
type: str
default: ''
target:
description:
- Location, on the remote host, of the dump file to read from or write to.
- Uncompressed SQL files (C(.sql)) as well as bzip2 (C(.bz2)), gzip (C(.gz)) and
xz (Added in 2.0) compressed files are supported.
type: path
single_transaction:
description:
- Execute the dump in a single transaction.
type: bool
default: 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
default: []
hex_blob:
description:
- Dump binary columns using hexadecimal notation.
type: bool
default: no
version_added: '0.1.0'
force:
description:
- Continue dump or import even if we get an SQL error.
- Used only when I(state) is C(dump) or C(import).
type: bool
default: no
version_added: '0.1.0'
master_data:
description:
- Option to dump a master replication server to produce a dump file
that can be used to set up another server as a slave of the master.
- C(0) to not include master data.
- C(1) to generate a 'CHANGE MASTER TO' statement
required on the slave to start the replication process.
- C(2) to generate a commented 'CHANGE MASTER TO'.
- Can be used when I(state=dump).
type: int
choices: [0, 1, 2]
default: 0
version_added: '0.1.0'
skip_lock_tables:
description:
- Skip locking tables for read. Used when I(state=dump), ignored otherwise.
type: bool
default: no
version_added: '0.1.0'
dump_extra_args:
description:
- Provide additional arguments for mysqldump.
Used when I(state=dump) only, ignored otherwise.
type: str
version_added: '0.1.0'
use_shell:
description:
- Used to prevent C(Broken pipe) errors when the imported I(target) file is compressed.
- If C(yes), the module will internally execute commands via a shell.
- Used when I(state=import), ignored otherwise.
type: bool
default: no
version_added: '0.1.0'
unsafe_login_password:
description:
- If C(no), the module will safely use a shell-escaped
version of the I(login_password) value.
- It makes sense to use C(yes) only if there are special
symbols in the value and errors C(Access denied) occur.
- Used only when I(state) is C(import) or C(dump) and
I(login_password) is passed, ignored otherwise.
type: bool
default: no
version_added: '0.1.0'
restrict_config_file:
description:
- Read only passed I(config_file).
- When I(state) is C(dump) or C(import),
by default the module passes I(config_file) parameter
using C(--defaults-extra-file) command-line argument to C(mysql/mysqldump) utilities
under the hood that read named option file in addition to usual option files.
- If this behavior is undesirable, use C(yes) to read only named option file.
type: bool
default: no
version_added: '0.1.0'
check_implicit_admin:
description:
- Check if mysql allows login as root/nopassword before trying supplied credentials.
- If success, passed I(login_user)/I(login_password) will be ignored.
type: bool
default: no
version_added: '0.1.0'
config_overrides_defaults:
description:
- If C(yes), connection parameters from I(config_file) will override the default
values of I(login_host) and I(login_port) parameters.
- Used when I(stat) is C(present) or C(absent), ignored otherwise.
- It needs Python 3.5+ as the default interpreter on a target host.
type: bool
default: no
version_added: '0.1.0'
chdir:
description:
- Changes the current working directory.
- Can be useful, for example, when I(state=import) and a dump file contains relative paths.
type: path
version_added: '3.4.0'
pipefail:
description:
- Use C(bash) instead of C(sh) and add C(-o pipefail) to catch errors from the
mysql_dump command when I(state=import) and compression is used.
- The default is C(no) to prevent issues on systems without bash as a default interpreter.
- The default will change to C(yes) in community.mysql 4.0.0.
type: bool
default: no
version_added: '3.4.0'
seealso:
- module: community.mysql.mysql_info
- module: community.mysql.mysql_variables
- module: community.mysql.mysql_user
- module: community.mysql.mysql_replication
- name: MySQL command-line client reference
description: Complete reference of the MySQL command-line client documentation.
link: https://dev.mysql.com/doc/refman/8.0/en/mysql.html
- name: mysqldump reference
description: Complete reference of the ``mysqldump`` client utility documentation.
link: https://dev.mysql.com/doc/refman/8.0/en/mysqldump.html
- name: CREATE DATABASE reference
description: Complete reference of the CREATE DATABASE command documentation.
link: https://dev.mysql.com/doc/refman/8.0/en/create-database.html
- name: DROP DATABASE reference
description: Complete reference of the DROP DATABASE command documentation.
link: https://dev.mysql.com/doc/refman/8.0/en/drop-database.html
author: "Ansible Core Team"
requirements:
- mysql (command line binary)
- mysqldump (command line binary)
notes:
- Supports C(check_mode).
- Requires the mysql and mysqldump binaries on the remote host.
- This module is B(not idempotent) when I(state) is C(import),
and will import the dump file each time if run more than once.
extends_documentation_fragment:
- community.mysql.mysql
'''
EXAMPLES = r'''
# If you encounter the "Please explicitly state intended protocol" error,
# use the login_unix_socket argument
- name: Create a new database with name 'bobdata'
community.mysql.mysql_db:
name: bobdata
state: present
login_unix_socket: /run/mysqld/mysqld.sock
- name: Create new databases with names 'foo' and 'bar'
community.mysql.mysql_db:
name:
- foo
- bar
state: present
# Copy database dump file to remote host and restore it to database 'my_db'
- name: Copy database dump file
copy:
src: dump.sql.bz2
dest: /tmp
- name: Restore database
community.mysql.mysql_db:
name: my_db
state: import
target: /tmp/dump.sql.bz2
- name: Restore database ignoring errors
community.mysql.mysql_db:
name: my_db
state: import
target: /tmp/dump.sql.bz2
force: yes
- name: Dump multiple databases
community.mysql.mysql_db:
state: dump
name: db_1,db_2
target: /tmp/dump.sql
- name: Dump multiple databases
community.mysql.mysql_db:
state: dump
name:
- db_1
- db_2
target: /tmp/dump.sql
- name: Dump all databases to hostname.sql
community.mysql.mysql_db:
state: dump
name: all
target: /tmp/dump.sql
- name: Dump all databases to hostname.sql including master data
community.mysql.mysql_db:
state: dump
name: all
target: /tmp/dump.sql
master_data: 1
# Import of sql script with encoding option
- name: >
Import dump.sql with specific latin1 encoding,
similar to mysql -u <username> --default-character-set=latin1 -p <password> < dump.sql
community.mysql.mysql_db:
state: import
name: all
encoding: latin1
target: /tmp/dump.sql
# Dump of database with encoding option
- name: >
Dump of Databse with specific latin1 encoding,
similar to mysqldump -u <username> --default-character-set=latin1 -p <password> <database>
community.mysql.mysql_db:
state: dump
name: db_1
encoding: latin1
target: /tmp/dump.sql
- name: Delete database with name 'bobdata'
community.mysql.mysql_db:
name: bobdata
state: absent
- name: Make sure there is neither a database with name 'foo', nor one with name 'bar'
community.mysql.mysql_db:
name:
- foo
- bar
state: absent
# Dump database with argument not directly supported by this module
# using dump_extra_args parameter
- name: Dump databases without including triggers
community.mysql.mysql_db:
state: dump
name: foo
target: /tmp/dump.sql
dump_extra_args: --skip-triggers
- name: Try to create database as root/nopassword first. If not allowed, pass the credentials
community.mysql.mysql_db:
check_implicit_admin: yes
login_user: bob
login_password: 123456
name: bobdata
state: present
- name: Dump a database with compression and catch errors from mysqldump with bash pipefail
community.mysql.mysql_db:
state: dump
name: foo
target: /tmp/dump.sql.gz
pipefail: true
'''
RETURN = r'''
db:
description: Database names in string format delimited by white space.
returned: always
type: str
sample: "foo bar"
db_list:
description: List of database names.
returned: always
type: list
sample: ["foo", "bar"]
executed_commands:
description: List of commands which tried to run.
returned: if executed
type: list
sample: ["CREATE DATABASE acme"]
version_added: '0.1.0'
'''
import os
import subprocess
import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.mysql.plugins.module_utils.database import mysql_quote_identifier
from ansible_collections.community.mysql.plugins.module_utils.mysql import mysql_connect, mysql_driver, mysql_driver_fail_msg, mysql_common_argument_spec
from ansible.module_utils.six.moves import shlex_quote
from ansible.module_utils._text import to_native
executed_commands = []
# ===========================================
# MySQL module specific support methods.
#
def db_exists(cursor, db):
res = 0
for each_db in db:
res += cursor.execute("SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = %s", (each_db,))
return res == len(db)
def db_delete(cursor, db):
if not db:
return False
for each_db in db:
query = "DROP DATABASE %s" % mysql_quote_identifier(each_db, 'database')
executed_commands.append(query)
cursor.execute(query)
return True
def db_dump(module, host, user, password, db_name, target, all_databases, port,
config_file, socket=None, ssl_cert=None, ssl_key=None, ssl_ca=None,
single_transaction=None, quick=None, ignore_tables=None, hex_blob=None,
encoding=None, force=False, master_data=0, skip_lock_tables=False,
dump_extra_args=None, unsafe_password=False, restrict_config_file=False,
check_implicit_admin=False, pipefail=False):
cmd = module.get_bin_path('mysqldump', True)
# If defined, mysqldump demands --defaults-extra-file be the first option
if config_file:
if restrict_config_file:
cmd += " --defaults-file=%s" % shlex_quote(config_file)
else:
cmd += " --defaults-extra-file=%s" % shlex_quote(config_file)
if check_implicit_admin:
cmd += " --user=root --password=''"
else:
if user is not None:
cmd += " --user=%s" % shlex_quote(user)
if password is not None:
if not unsafe_password:
cmd += " --password=%s" % shlex_quote(password)
else:
cmd += " --password=%s" % password
if ssl_cert is not None:
cmd += " --ssl-cert=%s" % shlex_quote(ssl_cert)
if ssl_key is not None:
cmd += " --ssl-key=%s" % shlex_quote(ssl_key)
if ssl_ca is not None:
cmd += " --ssl-ca=%s" % shlex_quote(ssl_ca)
if force:
cmd += " --force"
if socket is not None:
cmd += " --socket=%s" % shlex_quote(socket)
else:
cmd += " --host=%s --port=%i" % (shlex_quote(host), port)
if all_databases:
cmd += " --all-databases"
elif len(db_name) > 1:
cmd += " --databases {0}".format(' '.join(db_name))
else:
cmd += " %s" % shlex_quote(' '.join(db_name))
if skip_lock_tables:
cmd += " --skip-lock-tables"
if (encoding is not None) and (encoding != ""):
cmd += " --default-character-set=%s" % shlex_quote(encoding)
if single_transaction:
cmd += " --single-transaction=true"
if quick:
cmd += " --quick"
if ignore_tables:
for an_ignored_table in ignore_tables:
cmd += " --ignore-table={0}".format(an_ignored_table)
if hex_blob:
cmd += " --hex-blob"
if master_data:
cmd += " --master-data=%s" % master_data
if dump_extra_args is not None:
cmd += " " + dump_extra_args
path = None
if os.path.splitext(target)[-1] == '.gz':
path = module.get_bin_path('gzip', True)
elif os.path.splitext(target)[-1] == '.bz2':
path = module.get_bin_path('bzip2', True)
elif os.path.splitext(target)[-1] == '.xz':
path = module.get_bin_path('xz', True)
if path:
cmd = '%s | %s > %s' % (cmd, path, shlex_quote(target))
if pipefail:
cmd = 'set -o pipefail && ' + cmd
else:
cmd += " > %s" % shlex_quote(target)
executed_commands.append(cmd)
if pipefail:
rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True, executable='bash')
else:
rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True)
return rc, stdout, stderr
def db_import(module, host, user, password, db_name, target, all_databases, port, config_file,
socket=None, ssl_cert=None, ssl_key=None, ssl_ca=None, encoding=None, force=False,
use_shell=False, unsafe_password=False, restrict_config_file=False,
check_implicit_admin=False):
if not os.path.exists(target):
return module.fail_json(msg="target %s does not exist on the host" % target)
cmd = [module.get_bin_path('mysql', True)]
# --defaults-file must go first, or errors out
if config_file:
if restrict_config_file:
cmd.append("--defaults-file=%s" % shlex_quote(config_file))
else:
cmd.append("--defaults-extra-file=%s" % shlex_quote(config_file))
if check_implicit_admin:
cmd.append("--user=root --password=''")
else:
if user:
cmd.append("--user=%s" % shlex_quote(user))
if password:
if not unsafe_password:
cmd.append("--password=%s" % shlex_quote(password))
else:
cmd.append("--password=%s" % password)
if ssl_cert is not None:
cmd.append("--ssl-cert=%s" % shlex_quote(ssl_cert))
if ssl_key is not None:
cmd.append("--ssl-key=%s" % shlex_quote(ssl_key))
if ssl_ca is not None:
cmd.append("--ssl-ca=%s" % shlex_quote(ssl_ca))
if force:
cmd.append("-f")
if socket is not None:
cmd.append("--socket=%s" % shlex_quote(socket))
else:
cmd.append("--host=%s" % shlex_quote(host))
cmd.append("--port=%i" % port)
if (encoding is not None) and (encoding != ""):
cmd.append("--default-character-set=%s" % shlex_quote(encoding))
if not all_databases:
cmd.append("--one-database")
cmd.append(shlex_quote(''.join(db_name)))
comp_prog_path = None
if os.path.splitext(target)[-1] == '.gz':
comp_prog_path = module.get_bin_path('gzip', required=True)
elif os.path.splitext(target)[-1] == '.bz2':
comp_prog_path = module.get_bin_path('bzip2', required=True)
elif os.path.splitext(target)[-1] == '.xz':
comp_prog_path = module.get_bin_path('xz', required=True)
if comp_prog_path:
# The line below is for returned data only:
executed_commands.append('%s -dc %s | %s' % (comp_prog_path, target, cmd))
if not use_shell:
p1 = subprocess.Popen([comp_prog_path, '-dc', target], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p2 = subprocess.Popen(cmd, stdin=p1.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(stdout2, stderr2) = p2.communicate()
p1.stdout.close()
p1.wait()
if p1.returncode != 0:
stderr1 = p1.stderr.read()
return p1.returncode, '', stderr1
else:
return p2.returncode, stdout2, stderr2
else:
# Used to prevent 'Broken pipe' errors that
# occasionaly occur when target files are compressed.
# FYI: passing the `shell=True` argument to p2 = subprocess.Popen()
# doesn't solve the problem.
cmd = " ".join(cmd)
cmd = "%s -dc %s | %s" % (comp_prog_path, shlex_quote(target), cmd)
rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True)
return rc, stdout, stderr
else:
cmd = ' '.join(cmd)
cmd += " < %s" % shlex_quote(target)
executed_commands.append(cmd)
rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True)
return rc, stdout, stderr
def db_create(cursor, db, encoding, collation):
if not db:
return False
query_params = dict(enc=encoding, collate=collation)
res = 0
for each_db in db:
# Escape '%' since mysql cursor.execute() uses a format string
query = ['CREATE DATABASE %s' % mysql_quote_identifier(each_db, 'database').replace('%', '%%')]
if encoding:
query.append("CHARACTER SET %(enc)s")
if collation:
query.append("COLLATE %(collate)s")
query = ' '.join(query)
res += cursor.execute(query, query_params)
try:
executed_commands.append(cursor.mogrify(query, query_params))
except AttributeError:
executed_commands.append(cursor._executed)
except Exception:
executed_commands.append(query)
return res > 0
# ===========================================
# Module execution.
#
def main():
argument_spec = mysql_common_argument_spec()
argument_spec.update(
name=dict(type='list', required=True, aliases=['db']),
encoding=dict(type='str', default=''),
collation=dict(type='str', default=''),
target=dict(type='path'),
state=dict(type='str', default='present', choices=['absent', 'dump', 'import', 'present']),
single_transaction=dict(type='bool', default=False),
quick=dict(type='bool', default=True),
ignore_tables=dict(type='list', default=[]),
hex_blob=dict(default=False, type='bool'),
force=dict(type='bool', default=False),
master_data=dict(type='int', default=0, choices=[0, 1, 2]),
skip_lock_tables=dict(type='bool', default=False),
dump_extra_args=dict(type='str'),
use_shell=dict(type='bool', default=False),
unsafe_login_password=dict(type='bool', default=False, no_log=True),
restrict_config_file=dict(type='bool', default=False),
check_implicit_admin=dict(type='bool', default=False),
config_overrides_defaults=dict(type='bool', default=False),
chdir=dict(type='path'),
pipefail=dict(type='bool', default=False),
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
)
if mysql_driver is None:
module.fail_json(msg=mysql_driver_fail_msg)
db = module.params["name"]
if not db:
module.exit_json(changed=False, db=db, db_list=[])
db = [each_db.strip() for each_db in db]
encoding = module.params["encoding"]
collation = module.params["collation"]
state = module.params["state"]
target = module.params["target"]
socket = module.params["login_unix_socket"]
login_port = module.params["login_port"]
if login_port < 0 or login_port > 65535:
module.fail_json(msg="login_port must be a valid unix port number (0-65535)")
ssl_cert = module.params["client_cert"]
ssl_key = module.params["client_key"]
ssl_ca = module.params["ca_cert"]
check_hostname = module.params["check_hostname"]
connect_timeout = module.params['connect_timeout']
config_file = module.params['config_file']
login_password = module.params["login_password"]
unsafe_login_password = module.params["unsafe_login_password"]
login_user = module.params["login_user"]
login_host = module.params["login_host"]
ignore_tables = module.params["ignore_tables"]
for a_table in ignore_tables:
if a_table == "":
module.fail_json(msg="Name of ignored table cannot be empty")
single_transaction = module.params["single_transaction"]
quick = module.params["quick"]
hex_blob = module.params["hex_blob"]
force = module.params["force"]
master_data = module.params["master_data"]
skip_lock_tables = module.params["skip_lock_tables"]
dump_extra_args = module.params["dump_extra_args"]
use_shell = module.params["use_shell"]
restrict_config_file = module.params["restrict_config_file"]
check_implicit_admin = module.params['check_implicit_admin']
config_overrides_defaults = module.params['config_overrides_defaults']
chdir = module.params['chdir']
pipefail = module.params['pipefail']
if chdir:
try:
os.chdir(chdir)
except Exception as e:
module.fail_json("Cannot change the current directory to %s: %s" % (chdir, e))
if len(db) > 1 and state == 'import':
module.fail_json(msg="Multiple databases are not supported with state=import")
db_name = ' '.join(db)
all_databases = False
if state in ['dump', 'import']:
if target is None:
module.fail_json(msg="with state=%s target is required" % state)
if db == ['all']:
all_databases = True
else:
if db == ['all']:
module.fail_json(msg="name is not allowed to equal 'all' unless state equals import, or dump.")
try:
cursor = None
if check_implicit_admin:
try:
cursor, db_conn = mysql_connect(module, 'root', '', config_file, ssl_cert, ssl_key, ssl_ca,
connect_timeout=connect_timeout, check_hostname=check_hostname,
config_overrides_defaults=config_overrides_defaults)
except Exception as e:
check_implicit_admin = False
pass
if not cursor:
cursor, db_conn = mysql_connect(module, login_user, login_password, config_file, ssl_cert, ssl_key, ssl_ca,
connect_timeout=connect_timeout, config_overrides_defaults=config_overrides_defaults,
check_hostname=check_hostname)
except Exception as e:
if os.path.exists(config_file):
module.fail_json(msg="unable to connect to database, check login_user and login_password are correct or %s has the credentials. "
"Exception message: %s" % (config_file, to_native(e)))
else:
module.fail_json(msg="unable to find %s. Exception message: %s" % (config_file, to_native(e)))
changed = False
if not os.path.exists(config_file):
config_file = None
existence_list = []
non_existence_list = []
if not all_databases:
for each_database in db:
if db_exists(cursor, [each_database]):
existence_list.append(each_database)
else:
non_existence_list.append(each_database)
if state == "absent":
if module.check_mode:
module.exit_json(changed=bool(existence_list), db=db_name, db_list=db)
try:
changed = db_delete(cursor, existence_list)
except Exception as e:
module.fail_json(msg="error deleting database: %s" % to_native(e))
module.exit_json(changed=changed, db=db_name, db_list=db, executed_commands=executed_commands)
elif state == "present":
if module.check_mode:
module.exit_json(changed=bool(non_existence_list), db=db_name, db_list=db)
changed = False
if non_existence_list:
try:
changed = db_create(cursor, non_existence_list, encoding, collation)
except Exception as e:
module.fail_json(msg="error creating database: %s" % to_native(e),
exception=traceback.format_exc())
module.exit_json(changed=changed, db=db_name, db_list=db, executed_commands=executed_commands)
elif state == "dump":
if non_existence_list and not all_databases:
module.fail_json(msg="Cannot dump database(s) %r - not found" % (', '.join(non_existence_list)))
if module.check_mode:
module.exit_json(changed=True, db=db_name, db_list=db)
rc, stdout, stderr = db_dump(module, login_host, login_user,
login_password, db, target, all_databases,
login_port, config_file, socket, ssl_cert, ssl_key,
ssl_ca, single_transaction, quick, ignore_tables,
hex_blob, encoding, force, master_data, skip_lock_tables,
dump_extra_args, unsafe_login_password, restrict_config_file,
check_implicit_admin, pipefail)
if rc != 0:
module.fail_json(msg="%s" % stderr)
module.exit_json(changed=True, db=db_name, db_list=db, msg=stdout,
executed_commands=executed_commands)
elif state == "import":
if module.check_mode:
module.exit_json(changed=True, db=db_name, db_list=db)
if non_existence_list and not all_databases:
try:
db_create(cursor, non_existence_list, encoding, collation)
except Exception as e:
module.fail_json(msg="error creating database: %s" % to_native(e),
exception=traceback.format_exc())
rc, stdout, stderr = db_import(module, login_host, login_user,
login_password, db, target,
all_databases,
login_port, config_file,
socket, ssl_cert, ssl_key, ssl_ca,
encoding, force, use_shell, unsafe_login_password,
restrict_config_file, check_implicit_admin)
if rc != 0:
module.fail_json(msg="%s" % stderr)
module.exit_json(changed=True, db=db_name, db_list=db, msg=stdout,
executed_commands=executed_commands)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,579 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: mysql_info
short_description: Gather information about MySQL servers
description:
- Gathers information about MySQL servers.
options:
filter:
description:
- Limit the collected information by comma separated string or YAML list.
- Allowable values are C(version), C(databases), C(settings), C(global_status),
C(users), C(engines), C(master_status), C(slave_status), C(slave_hosts).
- By default, collects all subsets.
- You can use '!' before value (for example, C(!settings)) to exclude it from the information.
- If you pass including and excluding values to the filter, for example, I(filter=!settings,version),
the excluding values, C(!settings) in this case, will be ignored.
type: list
elements: str
login_db:
description:
- Database name to connect to.
- It makes sense if I(login_user) is allowed to connect to a specific database only.
type: str
exclude_fields:
description:
- List of fields which are not needed to collect.
- "Supports elements: C(db_size). Unsupported elements will be ignored."
type: list
elements: str
version_added: '0.1.0'
return_empty_dbs:
description:
- Includes names of empty databases to returned dictionary.
type: bool
default: 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).
- Supports C(check_mode).
seealso:
- module: community.mysql.mysql_variables
- module: community.mysql.mysql_db
- module: community.mysql.mysql_user
- module: community.mysql.mysql_replication
author:
- Andrew Klychkov (@Andersson007)
- Sebastian Gumprich (@rndmh3ro)
extends_documentation_fragment:
- community.mysql.mysql
'''
EXAMPLES = r'''
# Display info from mysql-hosts group (using creds from ~/.my.cnf to connect):
# ansible mysql-hosts -m mysql_info
# Display only databases and users info:
# ansible mysql-hosts -m mysql_info -a 'filter=databases,users'
# Display only slave status:
# ansible standby -m mysql_info -a 'filter=slave_status'
# Display all info from databases group except settings:
# ansible databases -m mysql_info -a 'filter=!settings'
# If you encounter the "Please explicitly state intended protocol" error,
# use the login_unix_socket argument
- name: Collect all possible information using passwordless root access
community.mysql.mysql_info:
login_user: root
login_unix_socket: /run/mysqld/mysqld.sock
- name: Get MySQL version with non-default credentials
community.mysql.mysql_info:
login_user: mysuperuser
login_password: mysuperpass
filter: version
- name: Collect all info except settings and users by root
community.mysql.mysql_info:
login_user: root
login_password: rootpass
filter: "!settings,!users"
- name: Collect info about databases and version using ~/.my.cnf as a credential file
become: yes
community.mysql.mysql_info:
filter:
- databases
- version
- name: Collect info about databases and version using ~alice/.my.cnf as a credential file
become: yes
community.mysql.mysql_info:
config_file: /home/alice/.my.cnf
filter:
- databases
- version
- name: Collect info about databases including empty and excluding their sizes
become: yes
community.mysql.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, "suffix": "MariaDB", "full": "5.5.60-MariaDB" } }
contains:
major:
description: Major server version.
returned: if not excluded by filter
type: int
sample: 5
minor:
description: Minor server version.
returned: if not excluded by filter
type: int
sample: 5
release:
description: Release server version.
returned: if not excluded by filter
type: int
sample: 60
suffix:
description: Server suffix, for example MySQL, MariaDB, other or none.
returned: if not excluded by filter
type: str
sample: "MariaDB"
full:
description: Full server version.
returned: if not excluded by filter
type: str
sample: "5.5.60-MariaDB"
databases:
description: Information about databases.
returned: if not excluded by filter
type: dict
sample:
- { "mysql": { "size": 656594 }, "information_schema": { "size": 73728 } }
contains:
size:
description: Database size in bytes.
returned: if not excluded by filter
type: dict
sample: { 'size': 656594 }
settings:
description: Global settings (variables) information.
returned: if not excluded by filter
type: dict
sample:
- { "innodb_open_files": 300, innodb_page_size": 16384 }
global_status:
description: Global status information.
returned: if not excluded by filter
type: dict
sample:
- { "Innodb_buffer_pool_read_requests": 123, "Innodb_buffer_pool_reads": 32 }
users:
description: Users information.
returned: if not excluded by filter
type: dict
sample:
- { "localhost": { "root": { "Alter_priv": "Y", "Alter_routine_priv": "Y" } } }
engines:
description: Information about the server's storage engines.
returned: if not excluded by filter
type: dict
sample:
- { "CSV": { "Comment": "CSV storage engine", "Savepoints": "NO", "Support": "YES", "Transactions": "NO", "XA": "NO" } }
master_status:
description: Master status information.
returned: if master
type: dict
sample:
- { "Binlog_Do_DB": "", "Binlog_Ignore_DB": "mysql", "File": "mysql-bin.000001", "Position": 769 }
slave_status:
description: Slave status information.
returned: if standby
type: dict
sample:
- { "192.168.1.101": { "3306": { "replication_user": { "Connect_Retry": 60, "Exec_Master_Log_Pos": 769, "Last_Errno": 0 } } } }
slave_hosts:
description: Slave status information.
returned: if master
type: dict
sample:
- { "2": { "Host": "", "Master_id": 1, "Port": 3306 } }
'''
from decimal import Decimal
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.mysql.plugins.module_utils.mysql import (
mysql_connect,
mysql_common_argument_spec,
mysql_driver,
mysql_driver_fail_msg,
)
from ansible.module_utils.six import iteritems
from ansible.module_utils._text import to_native
# ===========================================
# MySQL module specific support methods.
#
class MySQL_Info(object):
"""Class for collection MySQL instance information.
Arguments:
module (AnsibleModule): Object of AnsibleModule class.
cursor (pymysql/mysql-python): Cursor class for interaction with
the database.
Note:
If you need to add a new subset:
1. add a new key with the same name to self.info attr in self.__init__()
2. add a new private method to get the information
3. add invocation of the new method to self.__collect()
4. add info about the new subset to the DOCUMENTATION block
5. add info about the new subset with an example to RETURN block
"""
def __init__(self, module, cursor):
self.module = module
self.cursor = cursor
self.info = {
'version': {},
'databases': {},
'settings': {},
'global_status': {},
'engines': {},
'users': {},
'master_status': {},
'slave_hosts': {},
'slave_status': {},
}
def get_info(self, filter_, exclude_fields, return_empty_dbs):
"""Get MySQL instance information based on filter_.
Arguments:
filter_ (list): List of collected subsets (e.g., databases, users, etc.),
when it is empty, return all available information.
"""
inc_list = []
exc_list = []
if filter_:
partial_info = {}
for fi in filter_:
if fi.lstrip('!') not in self.info:
self.module.warn('filter element: %s is not allowable, ignored' % fi)
continue
if fi[0] == '!':
exc_list.append(fi.lstrip('!'))
else:
inc_list.append(fi)
if inc_list:
self.__collect(exclude_fields, return_empty_dbs, set(inc_list))
for i in self.info:
if i in inc_list:
partial_info[i] = self.info[i]
else:
not_in_exc_list = list(set(self.info) - set(exc_list))
self.__collect(exclude_fields, return_empty_dbs, set(not_in_exc_list))
for i in self.info:
if i not in exc_list:
partial_info[i] = self.info[i]
return partial_info
else:
self.__collect(exclude_fields, return_empty_dbs, set(self.info))
return self.info
def __collect(self, exclude_fields, return_empty_dbs, wanted):
"""Collect all possible subsets."""
if 'version' in wanted or 'settings' in wanted:
self.__get_global_variables()
if 'databases' in wanted:
self.__get_databases(exclude_fields, return_empty_dbs)
if 'global_status' in wanted:
self.__get_global_status()
if 'engines' in wanted:
self.__get_engines()
if 'users' in wanted:
self.__get_users()
if 'master_status' in wanted:
self.__get_master_status()
if 'slave_status' in wanted:
self.__get_slave_status()
if 'slave_hosts' in wanted:
self.__get_slaves()
def __get_engines(self):
"""Get storage engines info."""
res = self.__exec_sql('SHOW ENGINES')
if res:
for line in res:
engine = line['Engine']
self.info['engines'][engine] = {}
for vname, val in iteritems(line):
if vname != 'Engine':
self.info['engines'][engine][vname] = val
def __convert(self, val):
"""Convert unserializable data."""
try:
if isinstance(val, Decimal):
val = float(val)
else:
val = int(val)
except ValueError:
pass
except TypeError:
pass
return val
def __get_global_variables(self):
"""Get global variables (instance settings)."""
res = self.__exec_sql('SHOW GLOBAL VARIABLES')
if res:
for var in res:
self.info['settings'][var['Variable_name']] = self.__convert(var['Value'])
# version = ["5", "5," "60-MariaDB]
version = self.info['settings']['version'].split('.')
# full_version = "5.5.60-MariaDB"
full = self.info['settings']['version']
# release = "60"
release = version[2].split('-')[0]
# check if a suffix exists by counting the length
if len(version[2].split('-')) > 1:
# suffix = "MariaDB"
suffix = version[2].split('-', 1)[1]
else:
suffix = ""
self.info['version'] = dict(
# major = "5"
major=int(version[0]),
# minor = "5"
minor=int(version[1]),
release=int(release),
suffix=str(suffix),
full=str(full),
)
def __get_global_status(self):
"""Get global status."""
res = self.__exec_sql('SHOW GLOBAL STATUS')
if res:
for var in res:
self.info['global_status'][var['Variable_name']] = self.__convert(var['Value'])
def __get_master_status(self):
"""Get master status if the instance is a master."""
res = self.__exec_sql('SHOW MASTER STATUS')
if res:
for line in res:
for vname, val in iteritems(line):
self.info['master_status'][vname] = self.__convert(val)
def __get_slave_status(self):
"""Get slave status if the instance is a slave."""
res = self.__exec_sql('SHOW SLAVE STATUS')
if res:
for line in res:
host = line['Master_Host']
if host not in self.info['slave_status']:
self.info['slave_status'][host] = {}
port = line['Master_Port']
if port not in self.info['slave_status'][host]:
self.info['slave_status'][host][port] = {}
user = line['Master_User']
if user not in self.info['slave_status'][host][port]:
self.info['slave_status'][host][port][user] = {}
for vname, val in iteritems(line):
if vname not in ('Master_Host', 'Master_Port', 'Master_User'):
self.info['slave_status'][host][port][user][vname] = self.__convert(val)
def __get_slaves(self):
"""Get slave hosts info if the instance is a master."""
res = self.__exec_sql('SHOW SLAVE HOSTS')
if res:
for line in res:
srv_id = line['Server_id']
if srv_id not in self.info['slave_hosts']:
self.info['slave_hosts'][srv_id] = {}
for vname, val in iteritems(line):
if vname != 'Server_id':
self.info['slave_hosts'][srv_id][vname] = self.__convert(val)
def __get_users(self):
"""Get user info."""
res = self.__exec_sql('SELECT * FROM mysql.user')
if res:
for line in res:
host = line['Host']
if host not in self.info['users']:
self.info['users'][host] = {}
user = line['User']
self.info['users'][host][user] = {}
for vname, val in iteritems(line):
if vname not in ('Host', 'User'):
self.info['users'][host][user][vname] = self.__convert(val)
def __get_databases(self, exclude_fields, return_empty_dbs):
"""Get info about databases."""
if not exclude_fields:
query = ('SELECT table_schema AS "name", '
'SUM(data_length + index_length) AS "size" '
'FROM information_schema.TABLES GROUP BY table_schema')
else:
if 'db_size' in exclude_fields:
query = ('SELECT table_schema AS "name" '
'FROM information_schema.TABLES GROUP BY table_schema')
res = self.__exec_sql(query)
if res:
for db in res:
self.info['databases'][db['name']] = {}
if not exclude_fields or 'db_size' not in exclude_fields:
if db['size'] is None:
db['size'] = 0
self.info['databases'][db['name']]['size'] = int(db['size'])
# If empty dbs are not needed in the returned dict, exit from the method
if not return_empty_dbs:
return None
# Add info about empty databases (issue #65727):
res = self.__exec_sql('SHOW DATABASES')
if res:
for db in res:
if db['Database'] not in self.info['databases']:
self.info['databases'][db['Database']] = {}
if not exclude_fields or 'db_size' not in exclude_fields:
self.info['databases'][db['Database']]['size'] = 0
def __exec_sql(self, query, ddl=False):
"""Execute SQL.
Arguments:
ddl (bool): If True, return True or False.
Used for queries that don't return any rows
(mainly for DDL queries) (default False).
"""
try:
self.cursor.execute(query)
if not ddl:
res = self.cursor.fetchall()
return res
return True
except Exception as e:
self.module.fail_json(msg="Cannot execute SQL '%s': %s" % (query, to_native(e)))
return False
# ===========================================
# Module execution.
#
def main():
argument_spec = mysql_common_argument_spec()
argument_spec.update(
login_db=dict(type='str'),
filter=dict(type='list'),
exclude_fields=dict(type='list'),
return_empty_dbs=dict(type='bool', default=False),
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
)
db = module.params['login_db']
connect_timeout = module.params['connect_timeout']
login_user = module.params['login_user']
login_password = module.params['login_password']
ssl_cert = module.params['client_cert']
ssl_key = module.params['client_key']
ssl_ca = module.params['ca_cert']
check_hostname = module.params['check_hostname']
config_file = module.params['config_file']
filter_ = module.params['filter']
exclude_fields = module.params['exclude_fields']
return_empty_dbs = module.params['return_empty_dbs']
if filter_:
filter_ = [f.strip() for f in filter_]
if exclude_fields:
exclude_fields = set([f.strip() for f in exclude_fields])
if mysql_driver is None:
module.fail_json(msg=mysql_driver_fail_msg)
try:
cursor, db_conn = mysql_connect(module, login_user, login_password,
config_file, ssl_cert, ssl_key, ssl_ca, db,
check_hostname=check_hostname,
connect_timeout=connect_timeout, cursor_class='DictCursor')
except Exception as e:
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()

View File

@@ -0,0 +1,280 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2020, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = r'''
---
module: mysql_query
short_description: Run MySQL queries
description:
- Runs arbitrary MySQL queries.
- Pay attention, the module does not support check mode!
All queries will be executed in autocommit mode.
- To run SQL queries from a file, use M(community.mysql.mysql_db) module.
version_added: '0.1.0'
options:
query:
description:
- SQL query to run. Multiple queries can be passed using YAML list syntax.
- Must be a string or YAML list containing strings.
- Note that if you use the C(IF EXISTS/IF NOT EXISTS) clauses in your query
and C(mysqlclient) connector, the module will report that
the state has been changed even if it has not. If it is important in your
workflow, use the C(PyMySQL) connector instead.
type: raw
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
seealso:
- module: community.mysql.mysql_db
author:
- Andrew Klychkov (@Andersson007)
extends_documentation_fragment:
- community.mysql.mysql
'''
EXAMPLES = r'''
# If you encounter the "Please explicitly state intended protocol" error,
# use the login_unix_socket argument
- name: Simple select query to acme db
community.mysql.mysql_query:
login_db: acme
query: SELECT * FROM orders
login_unix_socket: /run/mysqld/mysqld.sock
- name: Select query to db acme with positional arguments
community.mysql.mysql_query:
login_db: acme
query: SELECT * FROM acme WHERE id = %s AND story = %s
positional_args:
- 1
- test
- name: Select query to test_db with named_args
community.mysql.mysql_query:
login_db: test_db
query: SELECT * FROM test WHERE id = %(id_val)s AND story = %(story_val)s
named_args:
id_val: 1
story_val: test
- name: Run several insert queries against db test_db in single transaction
community.mysql.mysql_query:
login_db: test_db
query:
- INSERT INTO articles (id, story) VALUES (2, 'my_long_story')
- INSERT INTO prices (id, price) VALUES (123, '100.00')
single_transaction: 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]
'''
import warnings
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.mysql.plugins.module_utils.mysql import (
mysql_connect,
mysql_common_argument_spec,
mysql_driver,
mysql_driver_fail_msg,
)
from ansible.module_utils._text import to_native
DML_QUERY_KEYWORDS = ('INSERT', 'UPDATE', 'DELETE', 'REPLACE')
# TRUNCATE is not DDL query but it also returns 0 rows affected:
DDL_QUERY_KEYWORDS = ('CREATE', 'DROP', 'ALTER', 'RENAME', 'TRUNCATE')
# ===========================================
# Module execution.
#
def main():
argument_spec = mysql_common_argument_spec()
argument_spec.update(
query=dict(type='raw', required=True),
login_db=dict(type='str'),
positional_args=dict(type='list'),
named_args=dict(type='dict'),
single_transaction=dict(type='bool', default=False),
)
module = AnsibleModule(
argument_spec=argument_spec,
mutually_exclusive=(
('positional_args', 'named_args'),
),
)
db = module.params['login_db']
connect_timeout = module.params['connect_timeout']
login_user = module.params['login_user']
login_password = module.params['login_password']
ssl_cert = module.params['client_cert']
ssl_key = module.params['client_key']
ssl_ca = module.params['ca_cert']
check_hostname = module.params['check_hostname']
config_file = module.params['config_file']
query = module.params["query"]
if not isinstance(query, (str, list)):
module.fail_json(msg="the query option value must be a string or list, passed %s" % type(query))
if isinstance(query, str):
query = [query]
for elem in query:
if not isinstance(elem, str):
module.fail_json(msg="the elements in query list must be strings, passed '%s' %s" % (elem, type(elem)))
if module.params["single_transaction"]:
autocommit = False
else:
autocommit = True
# Prepare args:
if module.params.get("positional_args"):
arguments = module.params["positional_args"]
elif module.params.get("named_args"):
arguments = module.params["named_args"]
else:
arguments = None
if mysql_driver is None:
module.fail_json(msg=mysql_driver_fail_msg)
# Connect to DB:
try:
cursor, db_connection = mysql_connect(module, login_user, login_password,
config_file, ssl_cert, ssl_key, ssl_ca, db,
check_hostname=check_hostname,
connect_timeout=connect_timeout,
cursor_class='DictCursor', autocommit=autocommit)
except Exception as e:
module.fail_json(msg="unable to connect to database, check login_user and "
"login_password are correct or %s has the credentials. "
"Exception message: %s" % (config_file, to_native(e)))
# Set defaults:
changed = False
max_keyword_len = len(max(DML_QUERY_KEYWORDS + DDL_QUERY_KEYWORDS, key=len))
# Execute query:
query_result = []
executed_queries = []
rowcount = []
already_exists = False
for q in query:
try:
with warnings.catch_warnings():
warnings.filterwarnings(action='error',
message='.*already exists*',
category=mysql_driver.Warning)
try:
cursor.execute(q, arguments)
except mysql_driver.Warning:
# When something is run with IF NOT EXISTS
# and there's "already exists" MySQL warning,
# set the flag as True.
# PyMySQL throws the warning, mysqlclinet does NOT.
already_exists = True
except Exception as e:
if not autocommit:
db_connection.rollback()
cursor.close()
module.fail_json(msg="Cannot execute SQL '%s' args [%s]: %s" % (q, arguments, to_native(e)))
try:
if not already_exists:
query_result.append([dict(row) for row in cursor.fetchall()])
except Exception as e:
if not autocommit:
db_connection.rollback()
module.fail_json(msg="Cannot fetch rows from cursor: %s" % to_native(e))
# Check DML or DDL keywords in query and set changed accordingly:
q = q.lstrip()[0:max_keyword_len].upper()
for keyword in DML_QUERY_KEYWORDS:
if keyword in q and cursor.rowcount > 0:
changed = True
for keyword in DDL_QUERY_KEYWORDS:
if keyword in q:
if already_exists:
# Indicates the entity already exists
changed = False
already_exists = False # Reset flag
else:
changed = True
try:
executed_queries.append(cursor._last_executed)
except AttributeError:
# MySQLdb removed cursor._last_executed as a duplicate of cursor._executed
executed_queries.append(cursor._executed)
rowcount.append(cursor.rowcount)
# When the module run with the single_transaction == True:
if not autocommit:
db_connection.commit()
# Create dict with returned values:
kw = {
'changed': changed,
'executed_queries': executed_queries,
'query_result': query_result,
'rowcount': rowcount,
}
# Exit:
module.exit_json(**kw)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,655 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2013, Balazs Pocze <banyek@gawker.com>
# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru>
# Certain parts are taken from Mark Theunissen's mysqldb module
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: mysql_replication
short_description: Manage MySQL replication
description:
- Manages MySQL server replication, replica, primary status, get and change primary host.
author:
- Balazs Pocze (@banyek)
- Andrew Klychkov (@Andersson007)
options:
mode:
description:
- Module operating mode. Could be
C(changeprimary) (CHANGE PRIMARY TO),
C(getprimary) (SHOW PRIMARY STATUS),
C(getreplica) (SHOW REPLICA),
C(startreplica) (START REPLICA),
C(stopreplica) (STOP REPLICA),
C(resetprimary) (RESET PRIMARY) - supported since community.mysql 0.1.0,
C(resetreplica) (RESET REPLICA),
C(resetreplicaall) (RESET REPLICA ALL).
type: str
choices:
- changeprimary
- getprimary
- getreplica
- startreplica
- stopreplica
- resetprimary
- resetreplica
- resetreplicaall
default: getreplica
primary_host:
description:
- Same as the C(MASTER_HOST) mysql variable.
type: str
aliases: [master_host]
primary_user:
description:
- Same as the C(MASTER_USER) mysql variable.
type: str
aliases: [master_user]
primary_password:
description:
- Same as the C(MASTER_PASSWORD) mysql variable.
type: str
aliases: [master_password]
primary_port:
description:
- Same as the C(MASTER_PORT) mysql variable.
type: int
aliases: [master_port]
primary_connect_retry:
description:
- Same as the C(MASTER_CONNECT_RETRY) mysql variable.
type: int
aliases: [master_connect_retry]
primary_log_file:
description:
- Same as the C(MASTER_LOG_FILE) mysql variable.
type: str
aliases: [master_log_file]
primary_log_pos:
description:
- Same as the C(MASTER_LOG_POS) mysql variable.
type: int
aliases: [master_log_pos]
relay_log_file:
description:
- Same as mysql variable.
type: str
relay_log_pos:
description:
- Same as mysql variable.
type: int
primary_ssl:
description:
- Same as the C(MASTER_SSL) mysql variable.
- When setting it to C(yes), the connection attempt only succeeds
if an encrypted connection can be established.
- For details, refer to
L(MySQL encrypted replication documentation,https://dev.mysql.com/doc/refman/8.0/en/replication-solutions-encrypted-connections.html).
- The default is C(false).
type: bool
aliases: [master_ssl]
primary_ssl_ca:
description:
- Same as the C(MASTER_SSL_CA) mysql variable.
- For details, refer to
L(MySQL encrypted replication documentation,https://dev.mysql.com/doc/refman/8.0/en/replication-solutions-encrypted-connections.html).
type: str
aliases: [master_ssl_ca]
primary_ssl_capath:
description:
- Same as the C(MASTER_SSL_CAPATH) mysql variable.
- For details, refer to
L(MySQL encrypted replication documentation,https://dev.mysql.com/doc/refman/8.0/en/replication-solutions-encrypted-connections.html).
type: str
aliases: [master_ssl_capath]
primary_ssl_cert:
description:
- Same as the C(MASTER_SSL_CERT) mysql variable.
- For details, refer to
L(MySQL encrypted replication documentation,https://dev.mysql.com/doc/refman/8.0/en/replication-solutions-encrypted-connections.html).
type: str
aliases: [master_ssl_cert]
primary_ssl_key:
description:
- Same as the C(MASTER_SSL_KEY) mysql variable.
- For details, refer to
L(MySQL encrypted replication documentation,https://dev.mysql.com/doc/refman/8.0/en/replication-solutions-encrypted-connections.html).
type: str
aliases: [master_ssl_key]
primary_ssl_cipher:
description:
- Same as the C(MASTER_SSL_CIPHER) mysql variable.
- Specifies a colon-separated list of one or more ciphers permitted by the replica for the replication connection.
- For details, refer to
L(MySQL encrypted replication documentation,https://dev.mysql.com/doc/refman/8.0/en/replication-solutions-encrypted-connections.html).
type: str
aliases: [master_ssl_cipher]
primary_ssl_verify_server_cert:
description:
- Same as mysql variable.
type: bool
default: false
version_added: '3.5.0'
primary_auto_position:
description:
- Whether the host uses GTID based replication or not.
- Same as the C(MASTER_AUTO_POSITION) mysql variable.
type: bool
default: false
aliases: [master_auto_position]
primary_use_gtid:
description:
- Configures the replica to use the MariaDB Global Transaction ID.
- C(disabled) equals MASTER_USE_GTID=no command.
- To find information about available values see
U(https://mariadb.com/kb/en/library/change-master-to/#master_use_gtid).
- Available since MariaDB 10.0.2.
choices: [current_pos, replica_pos, disabled]
type: str
version_added: '0.1.0'
aliases: [master_use_gtid]
primary_delay:
description:
- Time lag behind the primary's state (in seconds).
- Same as the C(MASTER_DELAY) mysql variable.
- Available from MySQL 5.6.
- For more information see U(https://dev.mysql.com/doc/refman/8.0/en/replication-delayed.html).
type: int
version_added: '0.1.0'
aliases: [master_delay]
connection_name:
description:
- Name of the primary connection.
- Supported from MariaDB 10.0.1.
- Mutually exclusive with I(channel).
- For more information see U(https://mariadb.com/kb/en/library/multi-source-replication/).
type: str
version_added: '0.1.0'
channel:
description:
- Name of replication channel.
- Multi-source replication is supported from MySQL 5.7.
- Mutually exclusive with I(connection_name).
- For more information see U(https://dev.mysql.com/doc/refman/8.0/en/replication-multi-source.html).
type: str
version_added: '0.1.0'
fail_on_error:
description:
- Fails on error when calling mysql.
type: bool
default: False
version_added: '0.1.0'
notes:
- If an empty value for the parameter of string type is needed, use an empty string.
extends_documentation_fragment:
- community.mysql.mysql
seealso:
- module: community.mysql.mysql_info
- name: MySQL replication reference
description: Complete reference of the MySQL replication documentation.
link: https://dev.mysql.com/doc/refman/8.0/en/replication.html
- name: MySQL encrypted replication reference.
description: Setting up MySQL replication to use encrypted connection.
link: https://dev.mysql.com/doc/refman/8.0/en/replication-solutions-encrypted-connections.html
- name: MariaDB replication reference
description: Complete reference of the MariaDB replication documentation.
link: https://mariadb.com/kb/en/library/setting-up-replication/
'''
EXAMPLES = r'''
# If you encounter the "Please explicitly state intended protocol" error,
# use the login_unix_socket argument
- name: Stop mysql replica thread
community.mysql.mysql_replication:
mode: stopreplica
login_unix_socket: /run/mysqld/mysqld.sock
- name: Get primary binlog file name and binlog position
community.mysql.mysql_replication:
mode: getprimary
- name: Change primary to primary server 192.0.2.1 and use binary log 'mysql-bin.000009' with position 4578
community.mysql.mysql_replication:
mode: changeprimary
primary_host: 192.0.2.1
primary_log_file: mysql-bin.000009
primary_log_pos: 4578
- name: Check replica status using port 3308
community.mysql.mysql_replication:
mode: getreplica
login_host: ansible.example.com
login_port: 3308
- name: On MariaDB change primary to use GTID current_pos
community.mysql.mysql_replication:
mode: changeprimary
primary_use_gtid: current_pos
- name: Change primary to use replication delay 3600 seconds
community.mysql.mysql_replication:
mode: changeprimary
primary_host: 192.0.2.1
primary_delay: 3600
- name: Start MariaDB replica with connection name primary-1
community.mysql.mysql_replication:
mode: startreplica
connection_name: primary-1
- name: Stop replication in channel primary-1
community.mysql.mysql_replication:
mode: stopreplica
channel: primary-1
- name: >
Run RESET MASTER command which will delete all existing binary log files
and reset the binary log index file on the primary
community.mysql.mysql_replication:
mode: resetprimary
- name: Run start replica and fail the task on errors
community.mysql.mysql_replication:
mode: startreplica
connection_name: primary-1
fail_on_error: yes
- name: Change primary and fail on error (like when replica thread is running)
community.mysql.mysql_replication:
mode: changeprimary
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='primary2.example.com',MASTER_PORT=3306"]
version_added: '0.1.0'
'''
import os
import warnings
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.mysql.plugins.module_utils.mysql import (
mysql_connect,
mysql_driver,
mysql_driver_fail_msg,
mysql_common_argument_spec,
)
from ansible.module_utils._text import to_native
from ansible_collections.community.mysql.plugins.module_utils.version import LooseVersion
executed_queries = []
def get_primary_status(cursor):
# TODO: when it's available to change on MySQL's side,
# change MASTER to PRIMARY using the approach from
# get_replica_status() function. Same for other functions.
cursor.execute("SHOW MASTER STATUS")
primarystatus = cursor.fetchone()
return primarystatus
def get_replica_status(cursor, connection_name='', channel='', term='REPLICA'):
if connection_name:
query = "SHOW %s '%s' STATUS" % (term, connection_name)
else:
query = "SHOW %s STATUS" % term
if channel:
query += " FOR CHANNEL '%s'" % channel
cursor.execute(query)
replica_status = cursor.fetchone()
return replica_status
def stop_replica(module, cursor, connection_name='', channel='', fail_on_error=False, term='REPLICA'):
if connection_name:
query = "STOP %s '%s'" % (term, connection_name)
else:
query = 'STOP %s' % term
if channel:
query += " FOR CHANNEL '%s'" % channel
try:
executed_queries.append(query)
cursor.execute(query)
stopped = True
except mysql_driver.Warning as e:
stopped = False
except Exception as e:
if fail_on_error:
module.fail_json(msg="STOP REPLICA failed: %s" % to_native(e))
stopped = False
return stopped
def reset_replica(module, cursor, connection_name='', channel='', fail_on_error=False, term='REPLICA'):
if connection_name:
query = "RESET %s '%s'" % (term, connection_name)
else:
query = 'RESET %s' % term
if channel:
query += " FOR CHANNEL '%s'" % channel
try:
executed_queries.append(query)
cursor.execute(query)
reset = True
except mysql_driver.Warning as e:
reset = False
except Exception as e:
if fail_on_error:
module.fail_json(msg="RESET REPLICA failed: %s" % to_native(e))
reset = False
return reset
def reset_replica_all(module, cursor, connection_name='', channel='', fail_on_error=False, term='REPLICA'):
if connection_name:
query = "RESET %s '%s' ALL" % (term, connection_name)
else:
query = 'RESET %s ALL' % term
if channel:
query += " FOR CHANNEL '%s'" % channel
try:
executed_queries.append(query)
cursor.execute(query)
reset = True
except mysql_driver.Warning as e:
reset = False
except Exception as e:
if fail_on_error:
module.fail_json(msg="RESET REPLICA ALL failed: %s" % to_native(e))
reset = False
return reset
def reset_primary(module, cursor, fail_on_error=False):
query = 'RESET MASTER'
try:
executed_queries.append(query)
cursor.execute(query)
reset = True
except mysql_driver.Warning as e:
reset = False
except Exception as e:
if fail_on_error:
module.fail_json(msg="RESET MASTER failed: %s" % to_native(e))
reset = False
return reset
def start_replica(module, cursor, connection_name='', channel='', fail_on_error=False, term='REPLICA'):
if connection_name:
query = "START %s '%s'" % (term, connection_name)
else:
query = 'START %s' % term
if channel:
query += " FOR CHANNEL '%s'" % channel
try:
executed_queries.append(query)
cursor.execute(query)
started = True
except mysql_driver.Warning as e:
started = False
except Exception as e:
if fail_on_error:
module.fail_json(msg="START REPLICA failed: %s" % to_native(e))
started = False
return started
def changeprimary(cursor, chm, connection_name='', channel=''):
if connection_name:
query = "CHANGE MASTER '%s' TO %s" % (connection_name, ','.join(chm))
else:
query = 'CHANGE MASTER TO %s' % ','.join(chm)
if channel:
query += " FOR CHANNEL '%s'" % channel
executed_queries.append(query)
cursor.execute(query)
def main():
argument_spec = mysql_common_argument_spec()
argument_spec.update(
mode=dict(type='str', default='getreplica', choices=[
'getprimary',
'getreplica',
'changeprimary',
'stopreplica',
'startreplica',
'resetprimary',
'resetreplica',
'resetreplicaall']),
primary_auto_position=dict(type='bool', default=False, aliases=['master_auto_position']),
primary_host=dict(type='str', aliases=['master_host']),
primary_user=dict(type='str', aliases=['master_user']),
primary_password=dict(type='str', no_log=True, aliases=['master_password']),
primary_port=dict(type='int', aliases=['master_port']),
primary_connect_retry=dict(type='int', aliases=['master_connect_retry']),
primary_log_file=dict(type='str', aliases=['master_log_file']),
primary_log_pos=dict(type='int', aliases=['master_log_pos']),
relay_log_file=dict(type='str'),
relay_log_pos=dict(type='int'),
primary_ssl=dict(type='bool', aliases=['master_ssl']),
primary_ssl_ca=dict(type='str', aliases=['master_ssl_ca']),
primary_ssl_capath=dict(type='str', aliases=['master_ssl_capath']),
primary_ssl_cert=dict(type='str', aliases=['master_ssl_cert']),
primary_ssl_key=dict(type='str', no_log=False, aliases=['master_ssl_key']),
primary_ssl_cipher=dict(type='str', aliases=['master_ssl_cipher']),
primary_ssl_verify_server_cert=dict(type='bool', default=False),
primary_use_gtid=dict(type='str', choices=[
'current_pos', 'replica_pos', 'disabled'], aliases=['master_use_gtid']),
primary_delay=dict(type='int', aliases=['master_delay']),
connection_name=dict(type='str'),
channel=dict(type='str'),
fail_on_error=dict(type='bool', default=False),
)
module = AnsibleModule(
argument_spec=argument_spec,
mutually_exclusive=[
['connection_name', 'channel']
],
)
mode = module.params["mode"]
primary_host = module.params["primary_host"]
primary_user = module.params["primary_user"]
primary_password = module.params["primary_password"]
primary_port = module.params["primary_port"]
primary_connect_retry = module.params["primary_connect_retry"]
primary_log_file = module.params["primary_log_file"]
primary_log_pos = module.params["primary_log_pos"]
relay_log_file = module.params["relay_log_file"]
relay_log_pos = module.params["relay_log_pos"]
primary_ssl = module.params["primary_ssl"]
primary_ssl_ca = module.params["primary_ssl_ca"]
primary_ssl_capath = module.params["primary_ssl_capath"]
primary_ssl_cert = module.params["primary_ssl_cert"]
primary_ssl_key = module.params["primary_ssl_key"]
primary_ssl_cipher = module.params["primary_ssl_cipher"]
primary_ssl_verify_server_cert = module.params["primary_ssl_verify_server_cert"]
primary_auto_position = module.params["primary_auto_position"]
ssl_cert = module.params["client_cert"]
ssl_key = module.params["client_key"]
ssl_ca = module.params["ca_cert"]
check_hostname = module.params["check_hostname"]
connect_timeout = module.params['connect_timeout']
config_file = module.params['config_file']
primary_delay = module.params['primary_delay']
if module.params.get("primary_use_gtid") == 'disabled':
primary_use_gtid = 'no'
else:
primary_use_gtid = module.params["primary_use_gtid"]
connection_name = module.params["connection_name"]
channel = module.params['channel']
fail_on_error = module.params['fail_on_error']
if mysql_driver is None:
module.fail_json(msg=mysql_driver_fail_msg)
else:
warnings.filterwarnings('error', category=mysql_driver.Warning)
login_password = module.params["login_password"]
login_user = module.params["login_user"]
try:
cursor, db_conn = mysql_connect(module, login_user, login_password, config_file,
ssl_cert, ssl_key, ssl_ca, None, cursor_class='DictCursor',
connect_timeout=connect_timeout, check_hostname=check_hostname)
except Exception as e:
if os.path.exists(config_file):
module.fail_json(msg="unable to connect to database, check login_user and "
"login_password are correct or %s has the credentials. "
"Exception message: %s" % (config_file, to_native(e)))
else:
module.fail_json(msg="unable to find %s. Exception message: %s" % (config_file, to_native(e)))
cursor.execute("SELECT VERSION()")
if 'mariadb' in cursor.fetchone()["VERSION()"].lower():
from ansible_collections.community.mysql.plugins.module_utils.implementations.mariadb import replication as impl
else:
from ansible_collections.community.mysql.plugins.module_utils.implementations.mysql import replication as impl
# Since MySQL 8.0.22 and MariaDB 10.5.1,
# "REPLICA" must be used instead of "SLAVE"
if impl.uses_replica_terminology(cursor):
replica_term = 'REPLICA'
else:
replica_term = 'SLAVE'
if primary_use_gtid == 'replica_pos':
primary_use_gtid = 'slave_pos'
if mode == 'getprimary':
status = get_primary_status(cursor)
if not isinstance(status, dict):
status = dict(Is_Primary=False,
msg="Server is not configured as mysql primary")
else:
status['Is_Primary'] = True
module.exit_json(queries=executed_queries, **status)
elif mode == "getreplica":
status = get_replica_status(cursor, connection_name, channel, replica_term)
if not isinstance(status, dict):
status = dict(Is_Replica=False, msg="Server is not configured as mysql replica")
else:
status['Is_Replica'] = True
module.exit_json(queries=executed_queries, **status)
elif mode == 'changeprimary':
chm = []
result = {}
if primary_host is not None:
chm.append("MASTER_HOST='%s'" % primary_host)
if primary_user is not None:
chm.append("MASTER_USER='%s'" % primary_user)
if primary_password is not None:
chm.append("MASTER_PASSWORD='%s'" % primary_password)
if primary_port is not None:
chm.append("MASTER_PORT=%s" % primary_port)
if primary_connect_retry is not None:
chm.append("MASTER_CONNECT_RETRY=%s" % primary_connect_retry)
if primary_log_file is not None:
chm.append("MASTER_LOG_FILE='%s'" % primary_log_file)
if primary_log_pos is not None:
chm.append("MASTER_LOG_POS=%s" % primary_log_pos)
if primary_delay is not None:
chm.append("MASTER_DELAY=%s" % primary_delay)
if relay_log_file is not None:
chm.append("RELAY_LOG_FILE='%s'" % relay_log_file)
if relay_log_pos is not None:
chm.append("RELAY_LOG_POS=%s" % relay_log_pos)
if primary_ssl is not None:
if primary_ssl:
chm.append("MASTER_SSL=1")
else:
chm.append("MASTER_SSL=0")
if primary_ssl_ca is not None:
chm.append("MASTER_SSL_CA='%s'" % primary_ssl_ca)
if primary_ssl_capath is not None:
chm.append("MASTER_SSL_CAPATH='%s'" % primary_ssl_capath)
if primary_ssl_cert is not None:
chm.append("MASTER_SSL_CERT='%s'" % primary_ssl_cert)
if primary_ssl_key is not None:
chm.append("MASTER_SSL_KEY='%s'" % primary_ssl_key)
if primary_ssl_cipher is not None:
chm.append("MASTER_SSL_CIPHER='%s'" % primary_ssl_cipher)
if primary_ssl_verify_server_cert:
chm.append("SOURCE_SSL_VERIFY_SERVER_CERT=1")
if primary_auto_position:
chm.append("MASTER_AUTO_POSITION=1")
if primary_use_gtid is not None:
chm.append("MASTER_USE_GTID=%s" % primary_use_gtid)
try:
changeprimary(cursor, chm, connection_name, channel)
except mysql_driver.Warning as e:
result['warning'] = to_native(e)
except Exception as e:
module.fail_json(msg='%s. Query == CHANGE MASTER TO %s' % (to_native(e), chm))
result['changed'] = True
module.exit_json(queries=executed_queries, **result)
elif mode == "startreplica":
started = start_replica(module, cursor, connection_name, channel, fail_on_error, replica_term)
if started is True:
module.exit_json(msg="Replica started ", changed=True, queries=executed_queries)
else:
module.exit_json(msg="Replica already started (Or cannot be started)", changed=False, queries=executed_queries)
elif mode == "stopreplica":
stopped = stop_replica(module, cursor, connection_name, channel, fail_on_error, replica_term)
if stopped is True:
module.exit_json(msg="Replica stopped", changed=True, queries=executed_queries)
else:
module.exit_json(msg="Replica already stopped", changed=False, queries=executed_queries)
elif mode == 'resetprimary':
reset = reset_primary(module, cursor, fail_on_error)
if reset is True:
module.exit_json(msg="Primary reset", changed=True, queries=executed_queries)
else:
module.exit_json(msg="Primary already reset", changed=False, queries=executed_queries)
elif mode == "resetreplica":
reset = reset_replica(module, cursor, connection_name, channel, fail_on_error, replica_term)
if reset is True:
module.exit_json(msg="Replica reset", changed=True, queries=executed_queries)
else:
module.exit_json(msg="Replica already reset", changed=False, queries=executed_queries)
elif mode == "resetreplicaall":
reset = reset_replica_all(module, cursor, connection_name, channel, fail_on_error, replica_term)
if reset is True:
module.exit_json(msg="Replica reset", changed=True, queries=executed_queries)
else:
module.exit_json(msg="Replica already reset", changed=False, queries=executed_queries)
warnings.simplefilter("ignore")
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,508 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2012, Mark Theunissen <mark.theunissen@gmail.com>
# Sponsored by Four Kitchens http://fourkitchens.com.
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: mysql_user
short_description: Adds or removes a user from a MySQL database
description:
- Adds or removes a user from a MySQL database.
options:
name:
description:
- Name of the user (role) to add or remove.
type: str
required: true
password:
description:
- Set the user's password. Only for C(mysql_native_password) authentication.
For other authentication plugins see the combination of I(plugin), I(plugin_hash_string), I(plugin_auth_string).
type: str
encrypted:
description:
- Indicate that the 'password' field is a `mysql_native_password` hash.
type: bool
default: 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.table1:priv/db.table2:priv)."
- The format is based on MySQL C(GRANT) statement.
- Database and table names can be quoted, MySQL-style.
- If column privileges are used, the C(priv1,priv2) part must be
exactly as returned by a C(SHOW GRANT) statement. If not followed,
the module will always report changes. It includes grouping columns
by permission (C(SELECT(col1,col2)) instead of C(SELECT(col1),SELECT(col2))).
- Can be passed as a dictionary (see the examples).
- Supports GRANTs for procedures and functions (see the examples).
- "Note: If you pass the same C(db.table) combination to this parameter
two or more times with different privileges,
for example, C('*.*:SELECT/*.*:SHOW VIEW'), only the last one will be applied,
in this example, it will be C(SHOW VIEW) respectively.
Use C('*.*:SELECT,SHOW VIEW') instead to apply both."
type: raw
append_privs:
description:
- Append the privileges defined by priv to the existing ones for this
user instead of overwriting existing ones. Mutually exclusive with I(subtract_privs).
type: bool
default: no
subtract_privs:
description:
- Revoke the privileges defined by the I(priv) option and keep other existing privileges.
If set, invalid privileges in I(priv) are ignored.
Mutually exclusive with I(append_privs).
version_added: '3.2.0'
type: bool
default: no
tls_requires:
description:
- Set requirement for secure transport as a dictionary of requirements (see the examples).
- Valid requirements are SSL, X509, SUBJECT, ISSUER, CIPHER.
- SUBJECT, ISSUER and CIPHER are complementary, and mutually exclusive with SSL and X509.
- U(https://mariadb.com/kb/en/securing-connections-for-client-and-server/#requiring-tls).
type: dict
version_added: 1.0.0
sql_log_bin:
description:
- Whether binary logging should be enabled or disabled for the connection.
type: bool
default: yes
force_context:
description:
- Sets the С(mysql) system database as context for the executed statements (it will be used
as a database to connect to). Useful if you use binlog / replication filters in MySQL as
per default the statements can not be caught by a binlog / replication filter, they require
a database to be set to work, otherwise the replication can break down.
- See U(https://dev.mysql.com/doc/refman/8.0/en/replication-options-binary-log.html#option_mysqld_binlog-ignore-db)
for a description on how binlog filters work (filtering on the primary).
- See U(https://dev.mysql.com/doc/refman/8.0/en/replication-options-replica.html#option_mysqld_replicate-ignore-db)
for a description on how replication filters work (filtering on the replica).
type: bool
default: no
version_added: '3.1.0'
state:
description:
- Whether the user should exist.
- When C(absent), removes the user.
type: str
choices: [ absent, present ]
default: present
check_implicit_admin:
description:
- Check if mysql allows login as root/nopassword before trying supplied credentials.
- If success, passed I(login_user)/I(login_password) will be ignored.
type: bool
default: no
update_password:
description:
- C(always) will update passwords if they differ. This affects I(password) and the combination of I(plugin), I(plugin_hash_string), I(plugin_auth_string).
- C(on_create) will only set the password or the combination of plugin, plugin_hash_string, plugin_auth_string for newly created users.
- "C(on_new_username) works like C(on_create), but it tries to reuse an existing password: If one different user
with the same username exists, or multiple different users with the same username and equal C(plugin) and
C(authentication_string) attribute, the existing C(plugin) and C(authentication_string) are used for the
new user instead of the I(password), I(plugin), I(plugin_hash_string) or I(plugin_auth_string) argument."
type: str
choices: [ always, on_create, on_new_username ]
default: always
plugin:
description:
- User's plugin to authenticate (``CREATE USER user IDENTIFIED WITH plugin``).
type: str
version_added: '0.1.0'
plugin_hash_string:
description:
- User's plugin hash string (``CREATE USER user IDENTIFIED WITH plugin AS plugin_hash_string``).
type: str
version_added: '0.1.0'
plugin_auth_string:
description:
- User's plugin auth_string (``CREATE USER user IDENTIFIED WITH plugin BY plugin_auth_string``).
type: str
version_added: '0.1.0'
resource_limits:
description:
- Limit the user for certain server resources. Provided since MySQL 5.6 / MariaDB 10.2.
- "Available options are C(MAX_QUERIES_PER_HOUR: num), C(MAX_UPDATES_PER_HOUR: num),
C(MAX_CONNECTIONS_PER_HOUR: num), C(MAX_USER_CONNECTIONS: num)."
- Used when I(state=present), ignored otherwise.
type: dict
version_added: '0.1.0'
notes:
- "MySQL server installs with default I(login_user) of C(root) and no password.
To secure this user as part of an idempotent playbook, you must create at least two tasks:
1) change the root user's password, without providing any I(login_user)/I(login_password) details,
2) drop a C(~/.my.cnf) file containing the new root credentials.
Subsequent runs of the playbook will then succeed by reading the new credentials from the file."
- Currently, there is only support for the C(mysql_native_password) encrypted password hash module.
- Supports (check_mode).
seealso:
- module: community.mysql.mysql_info
- name: MySQL access control and account management reference
description: Complete reference of the MySQL access control and account management documentation.
link: https://dev.mysql.com/doc/refman/8.0/en/access-control.html
- name: MySQL provided privileges reference
description: Complete reference of the MySQL provided privileges documentation.
link: https://dev.mysql.com/doc/refman/8.0/en/privileges-provided.html
author:
- Jonathan Mainguy (@Jmainguy)
- Benjamin Malynovytch (@bmalynovytch)
- Lukasz Tomaszkiewicz (@tomaszkiewicz)
extends_documentation_fragment:
- community.mysql.mysql
'''
EXAMPLES = r'''
# If you encounter the "Please explicitly state intended protocol" error,
# use the login_unix_socket argument
- name: Removes anonymous user account for localhost
community.mysql.mysql_user:
name: ''
host: localhost
state: absent
login_unix_socket: /run/mysqld/mysqld.sock
- name: Removes all anonymous user accounts
community.mysql.mysql_user:
name: ''
host_all: yes
state: absent
- name: Create database user with name 'bob' and password '12345' with all database privileges
community.mysql.mysql_user:
name: bob
password: 12345
priv: '*.*:ALL'
state: present
- name: Create database user using hashed password with all database privileges
community.mysql.mysql_user:
name: bob
password: '*EE0D72C1085C46C5278932678FBE2C6A782821B4'
encrypted: yes
priv: '*.*:ALL'
state: present
- name: Create database user with password and all database privileges and 'WITH GRANT OPTION'
community.mysql.mysql_user:
name: bob
password: 12345
priv: '*.*:ALL,GRANT'
state: present
- name: Create user with password, all database privileges and 'WITH GRANT OPTION' in db1 and db2
community.mysql.mysql_user:
state: present
name: bob
password: 12345dd
priv:
'db1.*': 'ALL,GRANT'
'db2.*': 'ALL,GRANT'
# Use 'PROCEDURE' instead of 'FUNCTION' to apply GRANTs for a MySQL procedure instead.
- name: Grant a user the right to execute a function
community.mysql.mysql_user:
name: readonly
password: 12345
priv:
FUNCTION my_db.my_function: EXECUTE
state: present
- name: Modify user to require TLS connection with a valid client certificate
community.mysql.mysql_user:
name: bob
tls_requires:
x509:
state: present
- name: Modify user to require TLS connection with a specific client certificate and cipher
community.mysql.mysql_user:
name: bob
tls_requires:
subject: '/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland'
cipher: 'ECDHE-ECDSA-AES256-SHA384'
- name: Modify user to no longer require SSL
community.mysql.mysql_user:
name: bob
tls_requires:
- name: Ensure no user named 'sally'@'localhost' exists, also passing in the auth credentials
community.mysql.mysql_user:
login_user: root
login_password: 123456
name: sally
state: absent
# check_implicit_admin example
- name: >
Ensure no user named 'sally'@'localhost' exists, also passing in the auth credentials.
If mysql allows root/nopassword login, try it without the credentials first.
If it's not allowed, pass the credentials
community.mysql.mysql_user:
check_implicit_admin: yes
login_user: root
login_password: 123456
name: sally
state: absent
- name: Ensure no user named 'sally' exists at all
community.mysql.mysql_user:
name: sally
host_all: yes
state: absent
- name: Specify grants composed of more than one word
community.mysql.mysql_user:
name: replication
password: 12345
priv: "*.*:REPLICATION CLIENT"
state: present
- name: Revoke all privileges for user 'bob' and password '12345'
community.mysql.mysql_user:
name: bob
password: 12345
priv: "*.*:USAGE"
state: present
# Example privileges string format
# mydb.*:INSERT,UPDATE/anotherdb.*:SELECT/yetanotherdb.*:ALL
- name: Example using login_unix_socket to connect to server
community.mysql.mysql_user:
name: root
password: abc123
login_unix_socket: /var/run/mysqld/mysqld.sock
- name: Example of skipping binary logging while adding user 'bob'
community.mysql.mysql_user:
name: bob
password: 12345
priv: "*.*:USAGE"
state: present
sql_log_bin: no
- name: Create user 'bob' authenticated with plugin 'AWSAuthenticationPlugin'
community.mysql.mysql_user:
name: bob
plugin: AWSAuthenticationPlugin
plugin_hash_string: RDS
priv: '*.*:ALL'
state: present
- name: Limit bob's resources to 10 queries per hour and 5 connections per hour
community.mysql.mysql_user:
name: bob
resource_limits:
MAX_QUERIES_PER_HOUR: 10
MAX_CONNECTIONS_PER_HOUR: 5
- name: Ensure bob does not have the DELETE privilege
community.mysql.mysql_user:
name: bob
subtract_privs: yes
priv:
'db1.*': DELETE
# Example .my.cnf file for setting the root password
# [client]
# user=root
# password=n<_665{vS43y
'''
RETURN = '''#'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.mysql.plugins.module_utils.database import SQLParseError
from ansible_collections.community.mysql.plugins.module_utils.mysql import (
mysql_connect, mysql_driver, mysql_driver_fail_msg, mysql_common_argument_spec
)
from ansible_collections.community.mysql.plugins.module_utils.user import (
convert_priv_dict_to_str,
get_impl,
get_mode,
InvalidPrivsError,
limit_resources,
privileges_unpack,
sanitize_requires,
user_add,
user_delete,
user_exists,
user_mod,
)
from ansible.module_utils._text import to_native
# ===========================================
# Module execution.
#
def main():
argument_spec = mysql_common_argument_spec()
argument_spec.update(
user=dict(type='str', required=True, aliases=['name']),
password=dict(type='str', no_log=True),
encrypted=dict(type='bool', default=False),
host=dict(type='str', default='localhost'),
host_all=dict(type="bool", default=False),
state=dict(type='str', default='present', choices=['absent', 'present']),
priv=dict(type='raw'),
tls_requires=dict(type='dict'),
append_privs=dict(type='bool', default=False),
subtract_privs=dict(type='bool', default=False),
check_implicit_admin=dict(type='bool', default=False),
update_password=dict(type='str', default='always', choices=['always', 'on_create', 'on_new_username'], no_log=False),
sql_log_bin=dict(type='bool', default=True),
plugin=dict(default=None, type='str'),
plugin_hash_string=dict(default=None, type='str'),
plugin_auth_string=dict(default=None, type='str'),
resource_limits=dict(type='dict'),
force_context=dict(type='bool', default=False),
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True,
mutually_exclusive=(('append_privs', 'subtract_privs'),)
)
login_user = module.params["login_user"]
login_password = module.params["login_password"]
user = module.params["user"]
password = module.params["password"]
encrypted = module.boolean(module.params["encrypted"])
host = module.params["host"].lower()
host_all = module.params["host_all"]
state = module.params["state"]
priv = module.params["priv"]
tls_requires = sanitize_requires(module.params["tls_requires"])
check_implicit_admin = module.params["check_implicit_admin"]
connect_timeout = module.params["connect_timeout"]
config_file = module.params["config_file"]
append_privs = module.boolean(module.params["append_privs"])
subtract_privs = module.boolean(module.params['subtract_privs'])
update_password = module.params['update_password']
ssl_cert = module.params["client_cert"]
ssl_key = module.params["client_key"]
ssl_ca = module.params["ca_cert"]
check_hostname = module.params["check_hostname"]
db = ''
if module.params["force_context"]:
db = 'mysql'
sql_log_bin = module.params["sql_log_bin"]
plugin = module.params["plugin"]
plugin_hash_string = module.params["plugin_hash_string"]
plugin_auth_string = module.params["plugin_auth_string"]
resource_limits = module.params["resource_limits"]
if priv and not isinstance(priv, (str, dict)):
module.fail_json(msg="priv parameter must be str or dict but %s was passed" % type(priv))
if priv and isinstance(priv, dict):
priv = convert_priv_dict_to_str(priv)
if mysql_driver is None:
module.fail_json(msg=mysql_driver_fail_msg)
cursor = None
try:
if check_implicit_admin:
try:
cursor, db_conn = mysql_connect(module, "root", "", config_file, ssl_cert, ssl_key, ssl_ca, db,
connect_timeout=connect_timeout, check_hostname=check_hostname)
except Exception:
pass
if not cursor:
cursor, db_conn = mysql_connect(module, login_user, login_password, config_file, ssl_cert, ssl_key, ssl_ca, db,
connect_timeout=connect_timeout, check_hostname=check_hostname)
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;")
get_impl(cursor)
if priv is not None:
try:
mode = get_mode(cursor)
except Exception as e:
module.fail_json(msg=to_native(e))
priv = privileges_unpack(priv, mode, ensure_usage=not subtract_privs)
password_changed = False
if state == "present":
if user_exists(cursor, user, host, host_all):
try:
if update_password == "always":
result = user_mod(cursor, user, host, host_all, password, encrypted,
plugin, plugin_hash_string, plugin_auth_string,
priv, append_privs, subtract_privs, tls_requires, module)
else:
result = user_mod(cursor, user, host, host_all, None, encrypted,
None, None, None,
priv, append_privs, subtract_privs, tls_requires, module)
changed = result['changed']
msg = result['msg']
password_changed = result['password_changed']
except (SQLParseError, InvalidPrivsError, mysql_driver.Error) as e:
module.fail_json(msg=to_native(e))
else:
if host_all:
module.fail_json(msg="host_all parameter cannot be used when adding a user")
try:
if subtract_privs:
priv = None # avoid granting unwanted privileges
reuse_existing_password = update_password == 'on_new_username'
result = user_add(cursor, user, host, host_all, password, encrypted,
plugin, plugin_hash_string, plugin_auth_string,
priv, tls_requires, module.check_mode, reuse_existing_password)
changed = result['changed']
password_changed = result['password_changed']
if changed:
msg = "User added"
except (SQLParseError, InvalidPrivsError, mysql_driver.Error) as e:
module.fail_json(msg=to_native(e))
if resource_limits:
changed = limit_resources(module, cursor, user, host, resource_limits, module.check_mode) or changed
elif state == "absent":
if user_exists(cursor, user, host, host_all):
changed = user_delete(cursor, user, host, host_all, module.check_mode)
msg = "User deleted"
else:
changed = False
msg = "User doesn't exist"
module.exit_json(changed=changed, user=user, msg=msg, password_changed=password_changed)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,271 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2013, Balazs Pocze <banyek@gawker.com>
# Certain parts are taken from Mark Theunissen's mysqldb module
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: mysql_variables
short_description: Manage MySQL global variables
description:
- Query / Set MySQL variables.
author:
- Balazs Pocze (@banyek)
options:
variable:
description:
- Variable name to operate.
type: str
required: 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.1.0'
notes:
- Does not support C(check_mode).
seealso:
- module: community.mysql.mysql_info
- name: MySQL SET command reference
description: Complete reference of the MySQL SET command documentation.
link: https://dev.mysql.com/doc/refman/8.0/en/set-statement.html
extends_documentation_fragment:
- community.mysql.mysql
'''
EXAMPLES = r'''
# If you encounter the "Please explicitly state intended protocol" error,
# use the login_unix_socket argument
- name: Check for sync_binlog setting
community.mysql.mysql_variables:
variable: sync_binlog
login_unix_socket: /run/mysqld/mysqld.sock
- name: Set read_only variable to 1 persistently
community.mysql.mysql_variables:
variable: read_only
value: 1
mode: persist
'''
RETURN = r'''
queries:
description: List of executed queries which modified DB's state.
returned: if executed
type: list
sample: ["SET GLOBAL `read_only` = 1"]
version_added: '0.1.0'
'''
import os
import warnings
from re import match
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.mysql.plugins.module_utils.database import SQLParseError, mysql_quote_identifier
from ansible_collections.community.mysql.plugins.module_utils.mysql import mysql_connect, mysql_driver, mysql_driver_fail_msg, mysql_common_argument_spec
from ansible.module_utils._text import to_native
executed_queries = []
def check_mysqld_auto(module, cursor, mysqlvar):
"""Check variable's value in mysqld-auto.cnf."""
query = ("SELECT VARIABLE_VALUE "
"FROM performance_schema.persisted_variables "
"WHERE VARIABLE_NAME = %s")
try:
cursor.execute(query, (mysqlvar,))
res = cursor.fetchone()
except Exception as e:
if "Table 'performance_schema.persisted_variables' doesn't exist" in str(e):
module.fail_json(msg='Server version must be 8.0 or greater.')
if res:
return res[0]
else:
return None
def typedvalue(value):
"""
Convert value to number whenever possible, return same value
otherwise.
>>> typedvalue('3')
3
>>> typedvalue('3.0')
3.0
>>> typedvalue('foobar')
'foobar'
"""
try:
return int(value)
except ValueError:
pass
try:
return float(value)
except ValueError:
pass
return value
def getvariable(cursor, mysqlvar):
cursor.execute("SHOW VARIABLES WHERE Variable_name = %s", (mysqlvar,))
mysqlvar_val = cursor.fetchall()
if len(mysqlvar_val) == 1:
return mysqlvar_val[0][1]
else:
return None
def setvariable(cursor, mysqlvar, value, mode='global'):
""" Set a global mysql variable to a given value
The DB driver will handle quoting of the given value based on its
type, thus numeric strings like '3.0' or '8' are illegal, they
should be passed as numeric literals.
"""
if mode == 'persist':
query = "SET PERSIST %s = " % mysql_quote_identifier(mysqlvar, 'vars')
elif mode == 'global':
query = "SET GLOBAL %s = " % mysql_quote_identifier(mysqlvar, 'vars')
elif mode == 'persist_only':
query = "SET PERSIST_ONLY %s = " % mysql_quote_identifier(mysqlvar, 'vars')
try:
cursor.execute(query + "%s", (value,))
executed_queries.append(query + "%s" % value)
cursor.fetchall()
result = True
except Exception as e:
result = to_native(e)
return result
def main():
argument_spec = mysql_common_argument_spec()
argument_spec.update(
variable=dict(type='str'),
value=dict(type='str'),
mode=dict(type='str', choices=['global', 'persist', 'persist_only'], default='global'),
)
module = AnsibleModule(
argument_spec=argument_spec
)
user = module.params["login_user"]
password = module.params["login_password"]
connect_timeout = module.params['connect_timeout']
ssl_cert = module.params["client_cert"]
ssl_key = module.params["client_key"]
ssl_ca = module.params["ca_cert"]
check_hostname = module.params["check_hostname"]
config_file = module.params['config_file']
db = 'mysql'
mysqlvar = module.params["variable"]
value = module.params["value"]
mode = module.params["mode"]
if mysqlvar is None:
module.fail_json(msg="Cannot run without variable to operate with")
if match('^[0-9a-z_.]+$', mysqlvar) is None:
module.fail_json(msg="invalid variable name \"%s\"" % mysqlvar)
if mysql_driver is None:
module.fail_json(msg=mysql_driver_fail_msg)
else:
warnings.filterwarnings('error', category=mysql_driver.Warning)
try:
cursor, db_conn = mysql_connect(module, user, password, config_file, ssl_cert, ssl_key, ssl_ca, db,
connect_timeout=connect_timeout, check_hostname=check_hostname)
except Exception as e:
if os.path.exists(config_file):
module.fail_json(msg=("unable to connect to database, check login_user and "
"login_password are correct or %s has the credentials. "
"Exception message: %s" % (config_file, to_native(e))))
else:
module.fail_json(msg="unable to find %s. Exception message: %s" % (config_file, to_native(e)))
mysqlvar_val = None
var_in_mysqld_auto_cnf = None
mysqlvar_val = getvariable(cursor, mysqlvar)
if mysqlvar_val is None:
module.fail_json(msg="Variable not available \"%s\"" % mysqlvar, changed=False)
if value is None:
module.exit_json(msg=mysqlvar_val)
if mode in ('persist', 'persist_only'):
var_in_mysqld_auto_cnf = check_mysqld_auto(module, cursor, mysqlvar)
if mode == 'persist_only':
if var_in_mysqld_auto_cnf is None:
mysqlvar_val = False
else:
mysqlvar_val = var_in_mysqld_auto_cnf
# Type values before using them
value_wanted = typedvalue(value)
value_actual = typedvalue(mysqlvar_val)
value_in_auto_cnf = None
if var_in_mysqld_auto_cnf is not None:
value_in_auto_cnf = typedvalue(var_in_mysqld_auto_cnf)
if value_wanted == value_actual and mode in ('global', 'persist'):
if mode == 'persist' and value_wanted == value_in_auto_cnf:
module.exit_json(msg="Variable is already set to requested value globally"
"and stored into mysqld-auto.cnf file.", changed=False)
elif mode == 'global':
module.exit_json(msg="Variable is already set to requested value.", changed=False)
if mode == 'persist_only' and value_wanted == value_in_auto_cnf:
module.exit_json(msg="Variable is already stored into mysqld-auto.cnf "
"with requested value.", changed=False)
try:
result = setvariable(cursor, mysqlvar, value_wanted, mode)
except SQLParseError as e:
result = to_native(e)
if result is True:
module.exit_json(msg="Variable change succeeded prev_value=%s" % value_actual,
changed=True, queries=executed_queries)
else:
module.fail_json(msg=result, changed=False)
if __name__ == '__main__':
main()