summaryrefslogtreecommitdiffstats
path: root/ansible_collections/community/mysql/plugins
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-18 05:52:27 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-18 05:52:27 +0000
commit3b0807ad7b283c46c21862eb826dcbb4ad04e5e2 (patch)
tree6461ea75f03eca87a5a90c86c3c9a787a6ad037e /ansible_collections/community/mysql/plugins
parentAdding debian version 7.7.0+dfsg-3. (diff)
downloadansible-3b0807ad7b283c46c21862eb826dcbb4ad04e5e2.tar.xz
ansible-3b0807ad7b283c46c21862eb826dcbb4ad04e5e2.zip
Merging upstream version 9.4.0+dfsg.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/community/mysql/plugins')
-rw-r--r--ansible_collections/community/mysql/plugins/doc_fragments/mysql.py3
-rw-r--r--ansible_collections/community/mysql/plugins/module_utils/implementations/mariadb/user.py6
-rw-r--r--ansible_collections/community/mysql/plugins/module_utils/implementations/mysql/user.py6
-rw-r--r--ansible_collections/community/mysql/plugins/module_utils/mysql.py7
-rw-r--r--ansible_collections/community/mysql/plugins/module_utils/user.py370
-rw-r--r--ansible_collections/community/mysql/plugins/modules/mysql_db.py9
-rw-r--r--ansible_collections/community/mysql/plugins/modules/mysql_info.py172
-rw-r--r--ansible_collections/community/mysql/plugins/modules/mysql_query.py6
-rw-r--r--ansible_collections/community/mysql/plugins/modules/mysql_replication.py13
-rw-r--r--ansible_collections/community/mysql/plugins/modules/mysql_role.py35
-rw-r--r--ansible_collections/community/mysql/plugins/modules/mysql_user.py83
-rw-r--r--ansible_collections/community/mysql/plugins/modules/mysql_variables.py7
12 files changed, 617 insertions, 100 deletions
diff --git a/ansible_collections/community/mysql/plugins/doc_fragments/mysql.py b/ansible_collections/community/mysql/plugins/doc_fragments/mysql.py
index 939126cba..27ec6509a 100644
--- a/ansible_collections/community/mysql/plugins/doc_fragments/mysql.py
+++ b/ansible_collections/community/mysql/plugins/doc_fragments/mysql.py
@@ -110,4 +110,7 @@ notes:
- Alternatively, to avoid using I(login_unix_socket) argument on each invocation you can specify the socket path
using the `socket` option in your MySQL config file (usually C(~/.my.cnf)) on the destination host, for
example C(socket=/var/lib/mysql/mysql.sock).
+attributes:
+ check_mode:
+ description: Can run in check_mode and return changed status prediction without modifying target.
'''
diff --git a/ansible_collections/community/mysql/plugins/module_utils/implementations/mariadb/user.py b/ansible_collections/community/mysql/plugins/module_utils/implementations/mariadb/user.py
index c1d2b6133..cdc14b217 100644
--- a/ansible_collections/community/mysql/plugins/module_utils/implementations/mariadb/user.py
+++ b/ansible_collections/community/mysql/plugins/module_utils/implementations/mariadb/user.py
@@ -23,3 +23,9 @@ def server_supports_alter_user(cursor):
version = get_server_version(cursor)
return LooseVersion(version) >= LooseVersion("10.2")
+
+
+def server_supports_password_expire(cursor):
+ version = get_server_version(cursor)
+
+ return LooseVersion(version) >= LooseVersion("10.4.3")
diff --git a/ansible_collections/community/mysql/plugins/module_utils/implementations/mysql/user.py b/ansible_collections/community/mysql/plugins/module_utils/implementations/mysql/user.py
index 1bdad5740..4e41c0542 100644
--- a/ansible_collections/community/mysql/plugins/module_utils/implementations/mysql/user.py
+++ b/ansible_collections/community/mysql/plugins/module_utils/implementations/mysql/user.py
@@ -24,3 +24,9 @@ def server_supports_alter_user(cursor):
version = get_server_version(cursor)
return LooseVersion(version) >= LooseVersion("5.6")
+
+
+def server_supports_password_expire(cursor):
+ version = get_server_version(cursor)
+
+ return LooseVersion(version) >= LooseVersion("5.7")
diff --git a/ansible_collections/community/mysql/plugins/module_utils/mysql.py b/ansible_collections/community/mysql/plugins/module_utils/mysql.py
index b95d20d0d..10ccfcf44 100644
--- a/ansible_collections/community/mysql/plugins/module_utils/mysql.py
+++ b/ansible_collections/community/mysql/plugins/module_utils/mysql.py
@@ -207,6 +207,13 @@ def get_server_version(cursor):
return version_str
+def get_server_implementation(cursor):
+ if 'mariadb' in get_server_version(cursor).lower():
+ return "mariadb"
+ else:
+ return "mysql"
+
+
def set_session_vars(module, cursor, session_vars):
"""Set session vars."""
for var, value in session_vars.items():
diff --git a/ansible_collections/community/mysql/plugins/module_utils/user.py b/ansible_collections/community/mysql/plugins/module_utils/user.py
index a63ad89b5..17ad4b0c2 100644
--- a/ansible_collections/community/mysql/plugins/module_utils/user.py
+++ b/ansible_collections/community/mysql/plugins/module_utils/user.py
@@ -10,6 +10,7 @@ __metaclass__ = type
# Simplified BSD License (see simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
import string
+import json
import re
from ansible.module_utils.six import iteritems
@@ -112,35 +113,57 @@ def get_grants(cursor, user, host):
return grants.split(", ")
-def get_existing_authentication(cursor, user):
+def get_existing_authentication(cursor, user, host):
# 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():
+ srv_type = cursor.fetchone()
+ # Mysql_info use a DictCursor so we must convert back to a list
+ # otherwise we get KeyError 0
+ if isinstance(srv_type, dict):
+ srv_type = list(srv_type.values())
+
+ if 'mariadb' in srv_type[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
+ and host=%(host)s
union select plugin, authentication_string as auth from mysql.user where user=%(user)s
- ) x group by plugin, auth limit 2
- """, {'user': user})
+ and host=%(host)s) x group by plugin, auth limit 2
+ """, {'user': user, 'host': host})
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})
+ cursor.execute("""select plugin, authentication_string as auth
+ from mysql.user where user=%(user)s and host=%(host)s
+ group by plugin, authentication_string limit 2""", {'user': user, 'host': host})
rows = cursor.fetchall()
- if len(rows) == 1:
- return {'plugin': rows[0][0], 'auth_string': rows[0][1]}
+
+ # Mysql_info use a DictCursor so we must convert back to a list
+ # otherwise we get KeyError 0
+ if isinstance(rows, dict):
+ rows = list(rows.values())
+
+ if isinstance(rows[0], tuple):
+ return {'plugin': rows[0][0], 'plugin_auth_string': rows[0][1]}
+
+ if isinstance(rows[0], dict):
+ return {'plugin': rows[0].get('plugin'), 'plugin_auth_string': rows[0].get('auth')}
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):
+ attributes, tls_requires, reuse_existing_password, module,
+ password_expire, password_expire_interval):
+ # If attributes are set, perform a sanity check to ensure server supports user attributes before creating user
+ if attributes and not get_attribute_support(cursor):
+ module.fail_json(msg="user attributes were specified but the server does not support user attributes")
+
# we cannot create users without a proper hostname
if host_all:
- return {'changed': False, 'password_changed': False}
+ return {'changed': False, 'password_changed': False, 'attributes': attributes}
- if check_mode:
- return {'changed': True, 'password_changed': None}
+ if module.check_mode:
+ return {'changed': True, 'password_changed': None, 'attributes': attributes}
# Determine what user management method server uses
old_user_mgmt = impl.use_old_user_mgmt(cursor)
@@ -149,7 +172,7 @@ def user_add(cursor, user, host, host_all, password, encrypted,
used_existing_password = False
if reuse_existing_password:
- existing_auth = get_existing_authentication(cursor, user)
+ existing_auth = get_existing_authentication(cursor, user, host)
if existing_auth:
plugin = existing_auth['plugin']
plugin_hash_string = existing_auth['auth_string']
@@ -183,12 +206,25 @@ def user_add(cursor, user, host, host_all, password, encrypted,
query_with_args_and_tls_requires = query_with_args + (tls_requires,)
cursor.execute(*mogrify(*query_with_args_and_tls_requires))
+ if password_expire:
+ if not impl.server_supports_password_expire(cursor):
+ module.fail_json(msg="The server version does not match the requirements "
+ "for password_expire parameter. See module's documentation.")
+ set_password_expire(cursor, user, host, password_expire, password_expire_interval)
+
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}
+
+ final_attributes = None
+
+ if attributes:
+ cursor.execute("ALTER USER %s@%s ATTRIBUTE %s", (user, host, json.dumps(attributes)))
+ final_attributes = attributes_get(cursor, user, host)
+
+ return {'changed': True, 'password_changed': not used_existing_password, 'attributes': final_attributes}
def is_hash(password):
@@ -201,7 +237,8 @@ def is_hash(password):
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):
+ append_privs, subtract_privs, attributes, tls_requires, module,
+ password_expire, password_expire_interval, role=False, maria_role=False):
changed = False
msg = "User unchanged"
grant_option = False
@@ -261,27 +298,48 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
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
+ if not module.check_mode:
+ 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 password expiration
+ if bool(password_expire):
+ if not impl.server_supports_password_expire(cursor):
+ module.fail_json(msg="The server version does not match the requirements "
+ "for password_expire parameter. See module's documentation.")
+ update = False
+ mariadb_role = True if "mariadb" in str(impl.__name__) else False
+ current_password_policy = get_password_expiration_policy(cursor, user, host, maria_role=mariadb_role)
+ password_expired = is_password_expired(cursor, user, host)
+ # Check if changes needed to be applied.
+ if not ((current_password_policy == -1 and password_expire == "default") or
+ (current_password_policy == 0 and password_expire == "never") or
+ (current_password_policy == password_expire_interval and password_expire == "interval") or
+ (password_expire == 'now' and password_expired)):
+
+ update = True
+
+ if not module.check_mode:
+ set_password_expire(cursor, user, host, password_expire, password_expire_interval)
+ password_changed = True
changed = True
# Handle plugin authentication
@@ -335,9 +393,8 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
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)
+ if not module.check_mode:
+ 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
@@ -346,9 +403,8 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
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)
+ if not module.check_mode:
+ 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
@@ -387,17 +443,58 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
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)
+ if not module.check_mode:
+ 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)
+ else:
+ changed = True
# 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)
+ # Handle attributes
+ attribute_support = get_attribute_support(cursor)
+ final_attributes = {}
+
+ if attributes:
+ if not attribute_support:
+ module.fail_json(msg="user attributes were specified but the server does not support user attributes")
+ else:
+ current_attributes = attributes_get(cursor, user, host)
+
+ if current_attributes is None:
+ current_attributes = {}
+
+ attributes_to_change = {}
+
+ for key, value in attributes.items():
+ if key not in current_attributes or current_attributes[key] != value:
+ attributes_to_change[key] = value
+
+ if attributes_to_change:
+ msg = "Attributes updated: %s" % (", ".join(["%s: %s" % (key, value) for key, value in attributes_to_change.items()]))
+
+ # Calculate final attributes by re-running attributes_get when not in check mode, and merge dictionaries when in check mode
+ if not module.check_mode:
+ cursor.execute("ALTER USER %s@%s ATTRIBUTE %s", (user, host, json.dumps(attributes_to_change)))
+ final_attributes = attributes_get(cursor, user, host)
+ else:
+ # Final if statements excludes items whose values are None in attributes_to_change, i.e. attributes that will be deleted
+ final_attributes = {k: v for d in (current_attributes, attributes_to_change) for k, v in d.items() if k not in attributes_to_change or
+ attributes_to_change[k] is not None}
+
+ # Convert empty dict to None per return value requirements
+ final_attributes = final_attributes if final_attributes else None
+ changed = True
+ else:
+ final_attributes = current_attributes
+ else:
+ if attribute_support:
+ final_attributes = attributes_get(cursor, user, host)
+
if role:
continue
@@ -405,24 +502,23 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
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 not module.check_mode:
+ 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)
+ 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)
+ cursor.execute(*query_with_args)
changed = True
- return {'changed': changed, 'msg': msg, 'password_changed': password_changed}
+ return {'changed': changed, 'msg': msg, 'password_changed': password_changed, 'attributes': final_attributes}
def user_delete(cursor, user, host, host_all, check_mode):
@@ -478,6 +574,12 @@ def privileges_get(cursor, user, host, maria_role=False):
return x
for grant in grants:
+
+ # Mysql_info use a DictCursor so we must convert back to a list
+ # otherwise we get KeyError 0
+ if isinstance(grant, dict):
+ grant = list(grant.values())
+
if not maria_role:
res = re.match("""GRANT (.+) ON (.+) TO (['`"]).*\\3@(['`"]).*\\4( IDENTIFIED BY PASSWORD (['`"]).+\\6)? ?(.*)""", grant[0])
else:
@@ -627,7 +729,7 @@ def sort_column_order(statement):
return '%s(%s)' % (priv_name, ', '.join(columns))
-def privileges_unpack(priv, mode, ensure_usage=True):
+def privileges_unpack(priv, mode, column_case_sensitive, 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
@@ -663,9 +765,14 @@ def privileges_unpack(priv, mode, ensure_usage=True):
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))
+ if column_case_sensitive is True:
+ output[pieces[0]] = re.split(r',\s*(?=[^)]*(?:\(|$))', pieces[1])
+ for i in output[pieces[0]]:
+ privs.append(re.sub(r'\s*\(.*\)', '', i))
+ else:
+ 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]]
@@ -715,6 +822,14 @@ def privileges_grant(cursor, user, host, db_table, priv, tls_requires, maria_rol
priv_string = ",".join([p for p in priv if p not in ('GRANT', )])
query = ["GRANT %s ON %s" % (priv_string, db_table)]
+ # MySQL and MariaDB don't store roles in the user table the same manner:
+ # select user, host from mysql.user;
+ # +------------------+-----------+
+ # | user | host |
+ # +------------------+-----------+
+ # | role_foo | % | <- MySQL
+ # | role_foo | | <- MariaDB
+ # +------------------+-----------+
if not maria_role:
query.append("TO %s@%s")
params = (user, host)
@@ -772,6 +887,11 @@ def get_resource_limits(cursor, user, host):
cursor.execute(query, (user, host))
res = cursor.fetchone()
+ # Mysql_info use a DictCursor so we must convert back to a list
+ # otherwise we get KeyError 0
+ if isinstance(res, dict):
+ res = list(res.values())
+
if not res:
return None
@@ -783,11 +903,22 @@ def get_resource_limits(cursor, user, host):
}
cursor.execute("SELECT VERSION()")
- if 'mariadb' in cursor.fetchone()[0].lower():
+ srv_type = cursor.fetchone()
+ # Mysql_info use a DictCursor so we must convert back to a list
+ # otherwise we get KeyError 0
+ if isinstance(srv_type, dict):
+ srv_type = list(srv_type.values())
+
+ if 'mariadb' in srv_type[0].lower():
query = ('SELECT max_statement_time AS MAX_STATEMENT_TIME '
'FROM mysql.user WHERE User = %s AND Host = %s')
cursor.execute(query, (user, host))
res_max_statement_time = cursor.fetchone()
+
+ # Mysql_info use a DictCursor so we must convert back to a list
+ # otherwise we get KeyError 0
+ if isinstance(res_max_statement_time, dict):
+ res_max_statement_time = list(res_max_statement_time.values())
current_limits['MAX_STATEMENT_TIME'] = res_max_statement_time[0]
return current_limits
@@ -872,6 +1003,111 @@ def limit_resources(module, cursor, user, host, resource_limits, check_mode):
return True
+def set_password_expire(cursor, user, host, password_expire, password_expire_interval):
+ """Fuction to set passowrd expiration for user.
+
+ Args:
+ cursor (cursor): DB driver cursor object.
+ user (str): User name.
+ host (str): User hostname.
+ password_expire (str): Password expiration mode.
+ password_expire_days (int): Invterval of days password expires.
+ """
+ if password_expire.lower() == "never":
+ statement = "PASSWORD EXPIRE NEVER"
+ elif password_expire.lower() == "default":
+ statement = "PASSWORD EXPIRE DEFAULT"
+ elif password_expire.lower() == "interval":
+ statement = "PASSWORD EXPIRE INTERVAL %d DAY" % (password_expire_interval)
+ elif password_expire.lower() == "now":
+ statement = "PASSWORD EXPIRE"
+
+ cursor.execute("ALTER USER %s@%s " + statement, (user, host))
+
+
+def get_password_expiration_policy(cursor, user, host, maria_role=False):
+ """Function to get password policy for user.
+
+ Args:
+ cursor (cursor): DB driver cursor object.
+ user (str): User name.
+ host (str): User hostname.
+ maria_role (bool, optional): mariadb or mysql. Defaults to False.
+
+ Returns:
+ policy (int): Current users password policy.
+ """
+ if not maria_role:
+ statement = "SELECT IFNULL(password_lifetime, -1) FROM mysql.user \
+ WHERE User = %s AND Host = %s", (user, host)
+ else:
+ statement = "SELECT JSON_EXTRACT(Priv, '$.password_lifetime') AS password_lifetime \
+ FROM mysql.global_priv \
+ WHERE User = %s AND Host = %s", (user, host)
+ cursor.execute(*statement)
+ policy = cursor.fetchone()[0]
+ return int(policy)
+
+
+def is_password_expired(cursor, user, host):
+ """Function to check if password is expired
+
+ Args:
+ cursor (cursor): DB driver cursor object.
+ user (str): User name.
+ host (str): User hostname.
+
+ Returns:
+ expired (bool): True if expired, else False.
+ """
+ statement = "SELECT password_expired FROM mysql.user \
+ WHERE User = %s AND Host = %s", (user, host)
+ cursor.execute(*statement)
+ expired = cursor.fetchone()[0]
+ if str(expired) == "Y":
+ return True
+ return False
+
+
+def get_attribute_support(cursor):
+ """Checks if the MySQL server supports user attributes.
+
+ Args:
+ cursor (cursor): DB driver cursor object.
+ Returns:
+ True if attributes are supported, False if they are not.
+ """
+ try:
+ # information_schema.tables does not hold the tables within information_schema itself
+ cursor.execute("SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES LIMIT 0")
+ cursor.fetchone()
+ except mysql_driver.Error:
+ return False
+
+ return True
+
+
+def attributes_get(cursor, user, host):
+ """Get attributes for a given user.
+
+ Args:
+ cursor (cursor): DB driver cursor object.
+ user (str): User name.
+ host (str): User host name.
+
+ Returns:
+ None if the user does not exist or the user has no attributes set, otherwise a dict of attributes set on the user
+ """
+ cursor.execute("SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = %s AND host = %s", (user, host))
+
+ r = cursor.fetchone()
+ # convert JSON string stored in row into a dict - mysql enforces that user_attributes entires are in JSON format
+ j = json.loads(r[0]) if r and r[0] else None
+
+ # if the attributes dict is empty, return None instead
+ return j if j else None
+
+
def get_impl(cursor):
global impl
cursor.execute("SELECT VERSION()")
diff --git a/ansible_collections/community/mysql/plugins/modules/mysql_db.py b/ansible_collections/community/mysql/plugins/modules/mysql_db.py
index 5a8fe3e3e..2cb67dce2 100644
--- a/ansible_collections/community/mysql/plugins/modules/mysql_db.py
+++ b/ansible_collections/community/mysql/plugins/modules/mysql_db.py
@@ -188,13 +188,14 @@ 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.
+attributes:
+ check_mode:
+ support: full
extends_documentation_fragment:
- community.mysql.mysql
-
'''
EXAMPLES = r'''
@@ -576,14 +577,14 @@ def db_create(cursor, db, encoding, collation):
def main():
argument_spec = mysql_common_argument_spec()
argument_spec.update(
- name=dict(type='list', required=True, aliases=['db']),
+ name=dict(type='list', elements='str', 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=[]),
+ ignore_tables=dict(type='list', elements='str', 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]),
diff --git a/ansible_collections/community/mysql/plugins/modules/mysql_info.py b/ansible_collections/community/mysql/plugins/modules/mysql_info.py
index 11b1a8003..0be25fa8d 100644
--- a/ansible_collections/community/mysql/plugins/modules/mysql_info.py
+++ b/ansible_collections/community/mysql/plugins/modules/mysql_info.py
@@ -5,6 +5,7 @@
# 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'''
@@ -19,7 +20,7 @@ options:
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).
+ C(users), C(users_info), 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),
@@ -47,7 +48,10 @@ options:
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).
+
+attributes:
+ check_mode:
+ support: full
seealso:
- module: community.mysql.mysql_variables
@@ -71,6 +75,9 @@ EXAMPLES = r'''
# Display only databases and users info:
# ansible mysql-hosts -m mysql_info -a 'filter=databases,users'
+# Display all users privileges:
+# ansible mysql-hosts -m mysql_info -a 'filter=users_info'
+
# Display only slave status:
# ansible standby -m mysql_info -a 'filter=slave_status'
@@ -119,6 +126,38 @@ EXAMPLES = r'''
- databases
exclude_fields: db_size
return_empty_dbs: true
+
+- name: Clone users from one server to another
+ block:
+ # Step 1
+ - name: Fetch information from a source server
+ delegate_to: server_source
+ community.mysql.mysql_info:
+ filter:
+ - users_info
+ register: result
+
+ # Step 2
+ # Don't work with sha256_password and cache_sha2_password
+ - name: Clone users fetched in a previous task to a target server
+ community.mysql.mysql_user:
+ name: "{{ item.name }}"
+ host: "{{ item.host }}"
+ plugin: "{{ item.plugin | default(omit) }}"
+ plugin_auth_string: "{{ item.plugin_auth_string | default(omit) }}"
+ plugin_hash_string: "{{ item.plugin_hash_string | default(omit) }}"
+ tls_require: "{{ item.tls_require | default(omit) }}"
+ priv: "{{ item.priv | default(omit) }}"
+ resource_limits: "{{ item.resource_limits | default(omit) }}"
+ column_case_sensitive: true
+ state: present
+ loop: "{{ result.users_info }}"
+ loop_control:
+ label: "{{ item.name }}@{{ item.host }}"
+ when:
+ - item.name != 'root' # In case you don't want to import admin accounts
+ - item.name != 'mariadb.sys'
+ - item.name != 'mysql'
'''
RETURN = r'''
@@ -178,11 +217,31 @@ global_status:
sample:
- { "Innodb_buffer_pool_read_requests": 123, "Innodb_buffer_pool_reads": 32 }
users:
- description: Users information.
+ description: Return a dictionnary of users grouped by host and with global privileges only.
returned: if not excluded by filter
type: dict
sample:
- { "localhost": { "root": { "Alter_priv": "Y", "Alter_routine_priv": "Y" } } }
+users_info:
+ description:
+ - Information about users accounts.
+ - The output can be used as an input of the M(community.mysql.mysql_user) plugin.
+ - Useful when migrating accounts to another server or to create an inventory.
+ - Does not support proxy privileges. If an account has proxy privileges, they won't appear in the output.
+ - Causes issues with authentications plugins C(sha256_password) and C(caching_sha2_password).
+ If the output is fed to M(community.mysql.mysql_user), the
+ ``plugin_auth_string`` will most likely be unreadable due to non-binary
+ characters.
+ returned: if not excluded by filter
+ type: dict
+ sample:
+ - { "plugin_auth_string": '*1234567',
+ "name": "user1",
+ "host": "host.com",
+ "plugin": "mysql_native_password",
+ "priv": "db1.*:SELECT/db2.*:SELECT",
+ "resource_limits": { "MAX_USER_CONNECTIONS": 100 } }
+ version_added: '3.8.0'
engines:
description: Information about the server's storage engines.
returned: if not excluded by filter
@@ -234,6 +293,13 @@ from ansible_collections.community.mysql.plugins.module_utils.mysql import (
mysql_driver_fail_msg,
get_connector_name,
get_connector_version,
+ get_server_implementation,
+)
+
+from ansible_collections.community.mysql.plugins.module_utils.user import (
+ privileges_get,
+ get_resource_limits,
+ get_existing_authentication,
)
from ansible.module_utils.six import iteritems
from ansible.module_utils._text import to_native
@@ -261,9 +327,10 @@ class MySQL_Info(object):
5. add info about the new subset with an example to RETURN block
"""
- def __init__(self, module, cursor):
+ def __init__(self, module, cursor, server_implementation):
self.module = module
self.cursor = cursor
+ self.server_implementation = server_implementation
self.info = {
'version': {},
'databases': {},
@@ -271,6 +338,7 @@ class MySQL_Info(object):
'global_status': {},
'engines': {},
'users': {},
+ 'users_info': {},
'master_status': {},
'slave_hosts': {},
'slave_status': {},
@@ -339,6 +407,9 @@ class MySQL_Info(object):
if 'users' in wanted:
self.__get_users()
+ if 'users_info' in wanted:
+ self.__get_users_info()
+
if 'master_status' in wanted:
self.__get_master_status()
@@ -429,7 +500,10 @@ class MySQL_Info(object):
def __get_slave_status(self):
"""Get slave status if the instance is a slave."""
- res = self.__exec_sql('SHOW SLAVE STATUS')
+ if self.server_implementation == "mariadb":
+ res = self.__exec_sql('SHOW ALL SLAVES STATUS')
+ else:
+ res = self.__exec_sql('SHOW SLAVE STATUS')
if res:
for line in res:
host = line['Master_Host']
@@ -477,6 +551,86 @@ class MySQL_Info(object):
if vname not in ('Host', 'User'):
self.info['users'][host][user][vname] = self.__convert(val)
+ def __get_users_info(self):
+ """Get user privileges, passwords, resources_limits, ...
+
+ Query the server to get all the users and return a string
+ of privileges that can be used by the mysql_user plugin.
+ For instance:
+
+ "users_info": [
+ {
+ "host": "users_info.com",
+ "priv": "*.*: ALL,GRANT",
+ "name": "users_info_adm"
+ },
+ {
+ "host": "users_info.com",
+ "priv": "`mysql`.*: SELECT/`users_info_db`.*: SELECT",
+ "name": "users_info_multi"
+ }
+ ]
+ """
+ res = self.__exec_sql('SELECT * FROM mysql.user')
+ if not res:
+ return None
+
+ output = list()
+ for line in res:
+ user = line['User']
+ host = line['Host']
+
+ user_priv = privileges_get(self.cursor, user, host)
+
+ if not user_priv:
+ self.module.warn("No privileges found for %s on host %s" % (user, host))
+ continue
+
+ priv_string = list()
+ for db_table, priv in user_priv.items():
+ # Proxy privileges are hard to work with because of different quotes or
+ # backticks like ''@'', ''@'%' or even ``@``. In addition, MySQL will
+ # forbid you to grant a proxy privileges through TCP.
+ if set(priv) == {'PROXY', 'GRANT'} or set(priv) == {'PROXY'}:
+ continue
+
+ unquote_db_table = db_table.replace('`', '').replace("'", '')
+ priv_string.append('%s:%s' % (unquote_db_table, ','.join(priv)))
+
+ # Only keep *.* USAGE if it's the only user privilege given
+ if len(priv_string) > 1 and '*.*:USAGE' in priv_string:
+ priv_string.remove('*.*:USAGE')
+
+ resource_limits = get_resource_limits(self.cursor, user, host)
+
+ copy_ressource_limits = dict.copy(resource_limits)
+ output_dict = {
+ 'name': user,
+ 'host': host,
+ 'priv': '/'.join(priv_string),
+ 'resource_limits': copy_ressource_limits,
+ }
+
+ # Prevent returning a resource limit if empty
+ if resource_limits:
+ for key, value in resource_limits.items():
+ if value == 0:
+ del output_dict['resource_limits'][key]
+ if len(output_dict['resource_limits']) == 0:
+ del output_dict['resource_limits']
+
+ authentications = get_existing_authentication(self.cursor, user, host)
+ if authentications:
+ output_dict.update(authentications)
+
+ # TODO password_option
+ # TODO lock_option
+ # but both are not supported by mysql_user atm. So no point yet.
+
+ output.append(output_dict)
+
+ self.info['users_info'] = output
+
def __get_databases(self, exclude_fields, return_empty_dbs):
"""Get info about databases."""
if not exclude_fields:
@@ -544,8 +698,8 @@ 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'),
+ filter=dict(type='list', elements='str'),
+ exclude_fields=dict(type='list', elements='str'),
return_empty_dbs=dict(type='bool', default=False),
)
@@ -590,10 +744,12 @@ def main():
'Exception message: %s' % (connector_name, connector_version, config_file, to_native(e)))
module.fail_json(msg)
+ server_implementation = get_server_implementation(cursor)
+
###############################
# Create object and do main job
- mysql = MySQL_Info(module, cursor)
+ mysql = MySQL_Info(module, cursor, server_implementation)
module.exit_json(changed=False,
connector_name=connector_name,
diff --git a/ansible_collections/community/mysql/plugins/modules/mysql_query.py b/ansible_collections/community/mysql/plugins/modules/mysql_query.py
index 12d5a5630..fd3a8e09e 100644
--- a/ansible_collections/community/mysql/plugins/modules/mysql_query.py
+++ b/ansible_collections/community/mysql/plugins/modules/mysql_query.py
@@ -36,6 +36,7 @@ options:
- List of values to be passed as positional arguments to the query.
- Mutually exclusive with I(named_args).
type: list
+ elements: raw
named_args:
description:
- Dictionary of key-value arguments to pass to the query.
@@ -50,6 +51,9 @@ options:
- Where passed queries run in a single transaction (C(yes)) or commit them one-by-one (C(no)).
type: bool
default: false
+attributes:
+ check_mode:
+ support: none
seealso:
- module: community.mysql.mysql_db
author:
@@ -138,7 +142,7 @@ def main():
argument_spec.update(
query=dict(type='raw', required=True),
login_db=dict(type='str'),
- positional_args=dict(type='list'),
+ positional_args=dict(type='list', elements='raw'),
named_args=dict(type='dict'),
single_transaction=dict(type='bool', default=False),
)
diff --git a/ansible_collections/community/mysql/plugins/modules/mysql_replication.py b/ansible_collections/community/mysql/plugins/modules/mysql_replication.py
index 33e14bc26..934b479b9 100644
--- a/ansible_collections/community/mysql/plugins/modules/mysql_replication.py
+++ b/ansible_collections/community/mysql/plugins/modules/mysql_replication.py
@@ -23,12 +23,12 @@ options:
mode:
description:
- Module operating mode. Could be
- C(changeprimary) (CHANGE PRIMARY TO),
- C(getprimary) (SHOW PRIMARY STATUS),
- C(getreplica) (SHOW REPLICA),
+ C(changeprimary) (CHANGE MASTER TO),
+ C(getprimary) (SHOW MASTER STATUS),
+ C(getreplica) (SHOW REPLICA STATUS),
C(startreplica) (START REPLICA),
C(stopreplica) (STOP REPLICA),
- C(resetprimary) (RESET PRIMARY) - supported since community.mysql 0.1.0,
+ C(resetprimary) (RESET MASTER) - supported since community.mysql 0.1.0,
C(resetreplica) (RESET REPLICA),
C(resetreplicaall) (RESET REPLICA ALL).
type: str
@@ -190,10 +190,13 @@ options:
notes:
- If an empty value for the parameter of string type is needed, use an empty string.
+attributes:
+ check_mode:
+ support: none
+
extends_documentation_fragment:
- community.mysql.mysql
-
seealso:
- module: community.mysql.mysql_info
- name: MySQL replication reference
diff --git a/ansible_collections/community/mysql/plugins/modules/mysql_role.py b/ansible_collections/community/mysql/plugins/modules/mysql_role.py
index 070d7939d..3e3462ab1 100644
--- a/ansible_collections/community/mysql/plugins/modules/mysql_role.py
+++ b/ansible_collections/community/mysql/plugins/modules/mysql_role.py
@@ -121,11 +121,24 @@ options:
type: bool
default: true
+ column_case_sensitive:
+ description:
+ - The default is C(false).
+ - When C(true), the module will not uppercase the field in the privileges.
+ - When C(false), the field names will be upper-cased. This was the default before this
+ feature was introduced but since MySQL/MariaDB is case sensitive you should set this
+ to C(true) in most cases.
+ type: bool
+ version_added: '3.8.0'
+
notes:
- Pay attention that the module runs C(SET DEFAULT ROLE ALL TO)
all the I(members) passed by default when the state has changed.
If you want to avoid this behavior, set I(set_default_role_all) to C(no).
- - Supports C(check_mode).
+
+attributes:
+ check_mode:
+ support: full
seealso:
- module: community.mysql.mysql_user
@@ -136,6 +149,8 @@ seealso:
author:
- Andrew Klychkov (@Andersson007)
- Felix Hamme (@betanummeric)
+ - kmarse (@kmarse)
+ - Laurent Indermühle (@laurent-indermuehle)
extends_documentation_fragment:
- community.mysql.mysql
@@ -916,8 +931,9 @@ class Role():
if privs:
result = user_mod(self.cursor, self.name, self.host,
None, None, None, None, None, None,
- privs, append_privs, subtract_privs, None,
- self.module, role=True, maria_role=self.is_mariadb)
+ privs, append_privs, subtract_privs, None, None,
+ self.module, None, None, role=True,
+ maria_role=self.is_mariadb)
changed = result['changed']
if admin:
@@ -954,7 +970,8 @@ def main():
detach_members=dict(type='bool', default=False),
check_implicit_admin=dict(type='bool', default=False),
set_default_role_all=dict(type='bool', default=True),
- members_must_exist=dict(type='bool', default=True)
+ members_must_exist=dict(type='bool', default=True),
+ column_case_sensitive=dict(type='bool', default=None), # TODO 4.0.0 add default=True
)
module = AnsibleModule(
argument_spec=argument_spec,
@@ -989,6 +1006,7 @@ def main():
db = ''
set_default_role_all = module.params['set_default_role_all']
members_must_exist = module.params['members_must_exist']
+ column_case_sensitive = module.params['column_case_sensitive']
if priv and not isinstance(priv, (str, dict)):
msg = ('The "priv" parameter must be str or dict '
@@ -1001,6 +1019,13 @@ def main():
if mysql_driver is None:
module.fail_json(msg=mysql_driver_fail_msg)
+ # TODO Release 4.0.0 : Remove this test and variable assignation
+ if column_case_sensitive is None:
+ column_case_sensitive = False
+ module.warn("Option column_case_sensitive is not provided. "
+ "The default is now false, so the column's name will be uppercased. "
+ "The default will be changed to true in community.mysql 4.0.0.")
+
cursor = None
try:
if check_implicit_admin:
@@ -1038,7 +1063,7 @@ def main():
module.fail_json(msg=to_native(e))
try:
- priv = privileges_unpack(priv, mode, ensure_usage=not subtract_privs)
+ priv = privileges_unpack(priv, mode, column_case_sensitive, ensure_usage=not subtract_privs)
except Exception as e:
module.fail_json(msg='Invalid privileges string: %s' % to_native(e))
diff --git a/ansible_collections/community/mysql/plugins/modules/mysql_user.py b/ansible_collections/community/mysql/plugins/modules/mysql_user.py
index e87fe12db..e02b15326 100644
--- a/ansible_collections/community/mysql/plugins/modules/mysql_user.py
+++ b/ansible_collections/community/mysql/plugins/modules/mysql_user.py
@@ -155,6 +155,37 @@ options:
- Cannot be used to set global variables, use the M(community.mysql.mysql_variables) module instead.
type: dict
version_added: '3.6.0'
+ password_expire:
+ description:
+ - C(never) - I(password) will never expire.
+ - C(default) - I(password) is defined using global system variable I(default_password_lifetime) setting.
+ - C(interval) - I(password) will expire in days which is defined in I(password_expire_interval).
+ - C(now) - I(password) will expire immediately.
+ type: str
+ choices: [ now, never, default, interval ]
+ version_added: '3.9.0'
+ password_expire_interval:
+ description:
+ - Number of days I(password) will expire. Requires I(password_expire=interval).
+ type: int
+ version_added: '3.9.0'
+
+ column_case_sensitive:
+ description:
+ - The default is C(false).
+ - When C(true), the module will not uppercase the field names in the privileges.
+ - When C(false), the field names will be upper-cased. This is the default
+ - This feature was introduced because MySQL 8 and above uses case sensitive
+ fields names in privileges.
+ type: bool
+ version_added: '3.8.0'
+ attributes:
+ description:
+ - "Create, update, or delete user attributes (arbitrary 'key: value' comments) for the user."
+ - MySQL server must support the INFORMATION_SCHEMA.USER_ATTRIBUTES table. Provided since MySQL 8.0.
+ - To delete an existing attribute, set its value to null.
+ type: dict
+ version_added: '3.9.0'
notes:
- "MySQL server installs with default I(login_user) of C(root) and no password.
@@ -163,7 +194,10 @@ notes:
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).
+
+attributes:
+ check_mode:
+ support: full
seealso:
- module: community.mysql.mysql_info
@@ -178,9 +212,11 @@ author:
- Jonathan Mainguy (@Jmainguy)
- Benjamin Malynovytch (@bmalynovytch)
- Lukasz Tomaszkiewicz (@tomaszkiewicz)
+- kmarse (@kmarse)
+- Laurent Indermühle (@laurent-indermuehle)
+
extends_documentation_fragment:
- community.mysql.mysql
-
'''
EXAMPLES = r'''
@@ -242,6 +278,13 @@ EXAMPLES = r'''
FUNCTION my_db.my_function: EXECUTE
state: present
+- name: Modify user attributes, creating the attribute 'foo' and removing the attribute 'bar'
+ community.mysql.mysql_user:
+ name: bob
+ attributes:
+ foo: "foo"
+ bar: null
+
- name: Modify user to require TLS connection with a valid client certificate
community.mysql.mysql_user:
name: bob
@@ -390,6 +433,7 @@ def main():
tls_requires=dict(type='dict'),
append_privs=dict(type='bool', default=False),
subtract_privs=dict(type='bool', default=False),
+ attributes=dict(type='dict'),
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),
@@ -399,6 +443,9 @@ def main():
resource_limits=dict(type='dict'),
force_context=dict(type='bool', default=False),
session_vars=dict(type='dict'),
+ column_case_sensitive=dict(type='bool', default=None), # TODO 4.0.0 add default=True
+ password_expire=dict(type='str', choices=['now', 'never', 'default', 'interval'], no_log=True),
+ password_expire_interval=dict(type='int', required_if=[('password_expire', 'interval', True)], no_log=True),
)
module = AnsibleModule(
argument_spec=argument_spec,
@@ -421,6 +468,7 @@ def main():
append_privs = module.boolean(module.params["append_privs"])
subtract_privs = module.boolean(module.params['subtract_privs'])
update_password = module.params['update_password']
+ attributes = module.params['attributes']
ssl_cert = module.params["client_cert"]
ssl_key = module.params["client_key"]
ssl_ca = module.params["ca_cert"]
@@ -434,6 +482,9 @@ def main():
plugin_auth_string = module.params["plugin_auth_string"]
resource_limits = module.params["resource_limits"]
session_vars = module.params["session_vars"]
+ column_case_sensitive = module.params["column_case_sensitive"]
+ password_expire = module.params["password_expire"]
+ password_expire_interval = module.params["password_expire_interval"]
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))
@@ -444,6 +495,10 @@ def main():
if mysql_driver is None:
module.fail_json(msg=mysql_driver_fail_msg)
+ if password_expire_interval and password_expire_interval < 1:
+ module.fail_json(msg="password_expire_interval value \
+ should be positive number")
+
cursor = None
try:
if check_implicit_admin:
@@ -460,6 +515,13 @@ def main():
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)))
+ # TODO Release 4.0.0 : Remove this test and variable assignation
+ if column_case_sensitive is None:
+ column_case_sensitive = False
+ module.warn("Option column_case_sensitive is not provided. "
+ "The default is now false, so the column's name will be uppercased. "
+ "The default will be changed to true in community.mysql 4.0.0.")
+
if not sql_log_bin:
cursor.execute("SET SQL_LOG_BIN=0;")
@@ -473,23 +535,28 @@ def main():
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)
+
+ priv = privileges_unpack(priv, mode, column_case_sensitive, ensure_usage=not subtract_privs)
password_changed = False
+ final_attributes = None
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)
+ priv, append_privs, subtract_privs, attributes, tls_requires, module,
+ password_expire, password_expire_interval)
else:
result = user_mod(cursor, user, host, host_all, None, encrypted,
None, None, None,
- priv, append_privs, subtract_privs, tls_requires, module)
+ priv, append_privs, subtract_privs, attributes, tls_requires, module,
+ password_expire, password_expire_interval)
changed = result['changed']
msg = result['msg']
password_changed = result['password_changed']
+ final_attributes = result['attributes']
except (SQLParseError, InvalidPrivsError, mysql_driver.Error) as e:
module.fail_json(msg=to_native(e))
@@ -502,9 +569,11 @@ def main():
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)
+ priv, attributes, tls_requires, reuse_existing_password, module,
+ password_expire, password_expire_interval)
changed = result['changed']
password_changed = result['password_changed']
+ final_attributes = result['attributes']
if changed:
msg = "User added"
@@ -521,7 +590,7 @@ def main():
else:
changed = False
msg = "User doesn't exist"
- module.exit_json(changed=changed, user=user, msg=msg, password_changed=password_changed)
+ module.exit_json(changed=changed, user=user, msg=msg, password_changed=password_changed, attributes=final_attributes)
if __name__ == '__main__':
diff --git a/ansible_collections/community/mysql/plugins/modules/mysql_variables.py b/ansible_collections/community/mysql/plugins/modules/mysql_variables.py
index f404d5aab..dfe8466f0 100644
--- a/ansible_collections/community/mysql/plugins/modules/mysql_variables.py
+++ b/ansible_collections/community/mysql/plugins/modules/mysql_variables.py
@@ -44,8 +44,9 @@ options:
default: global
version_added: '0.1.0'
-notes:
-- Does not support C(check_mode).
+attributes:
+ check_mode:
+ support: none
seealso:
- module: community.mysql.mysql_info
@@ -175,7 +176,7 @@ def setvariable(cursor, mysqlvar, value, mode='global'):
def main():
argument_spec = mysql_common_argument_spec()
argument_spec.update(
- variable=dict(type='str'),
+ variable=dict(type='str', required=True),
value=dict(type='str'),
mode=dict(type='str', choices=['global', 'persist', 'persist_only'], default='global'),
)