mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
mysql_user: add resource_limits parameter (#142)
* mysql_user: add resource_limits parameter * add CI test * added changelog fragment * skip opensuse * remove skip/suse
This commit is contained in:
parent
5fbe8a1f9d
commit
dd1bb708d3
4 changed files with 268 additions and 0 deletions
|
@ -0,0 +1,2 @@
|
||||||
|
minor_changes:
|
||||||
|
- mysql_user - add the resource_limits parameter (https://github.com/ansible-collections/community.general/issues/133).
|
|
@ -101,6 +101,13 @@ options:
|
||||||
description:
|
description:
|
||||||
- User's plugin auth_string (``CREATE USER user IDENTIFIED WITH plugin BY plugin_auth_string``).
|
- User's plugin auth_string (``CREATE USER user IDENTIFIED WITH plugin BY plugin_auth_string``).
|
||||||
type: str
|
type: str
|
||||||
|
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
|
||||||
|
|
||||||
notes:
|
notes:
|
||||||
- "MySQL server installs with default login_user of 'root' and no password. To secure this user
|
- "MySQL server installs with default login_user of 'root' and no password. To secure this user
|
||||||
|
@ -232,6 +239,13 @@ EXAMPLES = r'''
|
||||||
priv: '*.*:ALL'
|
priv: '*.*:ALL'
|
||||||
state: present
|
state: present
|
||||||
|
|
||||||
|
- name: Limit bob's resources to 10 queries per hour and 5 connections per hour
|
||||||
|
mysql_user:
|
||||||
|
name: bob
|
||||||
|
resource_limits:
|
||||||
|
MAX_QUERIES_PER_HOUR: 10
|
||||||
|
MAX_CONNECTIONS_PER_HOUR: 5
|
||||||
|
|
||||||
# Example .my.cnf file for setting the root password
|
# Example .my.cnf file for setting the root password
|
||||||
# [client]
|
# [client]
|
||||||
# user=root
|
# user=root
|
||||||
|
@ -672,6 +686,138 @@ def convert_priv_dict_to_str(priv):
|
||||||
|
|
||||||
return '/'.join(priv_list)
|
return '/'.join(priv_list)
|
||||||
|
|
||||||
|
|
||||||
|
# Alter user is supported since MySQL 5.6 and MariaDB 10.2.0
|
||||||
|
def server_supports_alter_user(cursor):
|
||||||
|
"""Check if the server supports ALTER USER statement or doesn't.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cursor (cursor): DB driver cursor object.
|
||||||
|
|
||||||
|
Returns: True if supports, False otherwise.
|
||||||
|
"""
|
||||||
|
cursor.execute("SELECT VERSION()")
|
||||||
|
version_str = cursor.fetchone()[0]
|
||||||
|
version = version_str.split('.')
|
||||||
|
|
||||||
|
if 'mariadb' in version_str.lower():
|
||||||
|
# MariaDB 10.2 and later
|
||||||
|
if int(version[0]) * 1000 + int(version[1]) >= 10002:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
# MySQL 5.6 and later
|
||||||
|
if int(version[0]) * 1000 + int(version[1]) >= 5006:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_resource_limits(cursor, user, host):
|
||||||
|
"""Get user resource limits.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cursor (cursor): DB driver cursor object.
|
||||||
|
user (str): User name.
|
||||||
|
host (str): User host name.
|
||||||
|
|
||||||
|
Returns: Dictionary containing current resource limits.
|
||||||
|
"""
|
||||||
|
|
||||||
|
query = ('SELECT max_questions AS MAX_QUERIES_PER_HOUR, '
|
||||||
|
'max_updates AS MAX_UPDATES_PER_HOUR, '
|
||||||
|
'max_connections AS MAX_CONNECTIONS_PER_HOUR, '
|
||||||
|
'max_user_connections AS MAX_USER_CONNECTIONS '
|
||||||
|
'FROM mysql.user WHERE User = %s AND Host = %s')
|
||||||
|
cursor.execute(query, (user, host))
|
||||||
|
res = cursor.fetchone()
|
||||||
|
|
||||||
|
if not res:
|
||||||
|
return None
|
||||||
|
|
||||||
|
current_limits = {
|
||||||
|
'MAX_QUERIES_PER_HOUR': res[0],
|
||||||
|
'MAX_UPDATES_PER_HOUR': res[1],
|
||||||
|
'MAX_CONNECTIONS_PER_HOUR': res[2],
|
||||||
|
'MAX_USER_CONNECTIONS': res[3],
|
||||||
|
}
|
||||||
|
return current_limits
|
||||||
|
|
||||||
|
|
||||||
|
def match_resource_limits(module, current, desired):
|
||||||
|
"""Check and match limits.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
module (AnsibleModule): Ansible module object.
|
||||||
|
current (dict): Dictionary with current limits.
|
||||||
|
desired (dict): Dictionary with desired limits.
|
||||||
|
|
||||||
|
Returns: Dictionary containing parameters that need to change.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not current:
|
||||||
|
# It means the user does not exists, so we need
|
||||||
|
# to set all limits after its creation
|
||||||
|
return desired
|
||||||
|
|
||||||
|
needs_to_change = {}
|
||||||
|
|
||||||
|
for key, val in iteritems(desired):
|
||||||
|
if key not in current:
|
||||||
|
# Supported keys are listed in the documentation
|
||||||
|
# and must be determined in the get_resource_limits function
|
||||||
|
# (follow 'AS' keyword)
|
||||||
|
module.fail_json(msg="resource_limits: key '%s' is unsupported." % key)
|
||||||
|
|
||||||
|
try:
|
||||||
|
val = int(val)
|
||||||
|
except Exception:
|
||||||
|
module.fail_json(msg="Can't convert value '%s' to integer." % val)
|
||||||
|
|
||||||
|
if val != current.get(key):
|
||||||
|
needs_to_change[key] = val
|
||||||
|
|
||||||
|
return needs_to_change
|
||||||
|
|
||||||
|
|
||||||
|
def limit_resources(module, cursor, user, host, resource_limits, check_mode):
|
||||||
|
"""Limit user resources.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
module (AnsibleModule): Ansible module object.
|
||||||
|
cursor (cursor): DB driver cursor object.
|
||||||
|
user (str): User name.
|
||||||
|
host (str): User host name.
|
||||||
|
resource_limit (dict): Dictionary with desired limits.
|
||||||
|
check_mode (bool): Run the function in check mode or not.
|
||||||
|
|
||||||
|
Returns: True, if changed, False otherwise.
|
||||||
|
"""
|
||||||
|
if not server_supports_alter_user(cursor):
|
||||||
|
module.fail_json(msg="The server version does not match the requirements "
|
||||||
|
"for resource_limits parameter. See module's documentation.")
|
||||||
|
|
||||||
|
current_limits = get_resource_limits(cursor, user, host)
|
||||||
|
|
||||||
|
needs_to_change = match_resource_limits(module, current_limits, resource_limits)
|
||||||
|
|
||||||
|
if not needs_to_change:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if needs_to_change and check_mode:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# If not check_mode
|
||||||
|
tmp = []
|
||||||
|
for key, val in iteritems(needs_to_change):
|
||||||
|
tmp.append('%s %s' % (key, val))
|
||||||
|
|
||||||
|
query = "ALTER USER %s@%s"
|
||||||
|
query += ' WITH %s' % ' '.join(tmp)
|
||||||
|
cursor.execute(query, (user, host))
|
||||||
|
return True
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# Module execution.
|
# Module execution.
|
||||||
#
|
#
|
||||||
|
@ -704,6 +850,7 @@ def main():
|
||||||
plugin=dict(default=None, type='str'),
|
plugin=dict(default=None, type='str'),
|
||||||
plugin_hash_string=dict(default=None, type='str'),
|
plugin_hash_string=dict(default=None, type='str'),
|
||||||
plugin_auth_string=dict(default=None, type='str'),
|
plugin_auth_string=dict(default=None, type='str'),
|
||||||
|
resource_limits=dict(type='dict'),
|
||||||
),
|
),
|
||||||
supports_check_mode=True,
|
supports_check_mode=True,
|
||||||
)
|
)
|
||||||
|
@ -729,6 +876,7 @@ def main():
|
||||||
plugin = module.params["plugin"]
|
plugin = module.params["plugin"]
|
||||||
plugin_hash_string = module.params["plugin_hash_string"]
|
plugin_hash_string = module.params["plugin_hash_string"]
|
||||||
plugin_auth_string = module.params["plugin_auth_string"]
|
plugin_auth_string = module.params["plugin_auth_string"]
|
||||||
|
resource_limits = module.params["resource_limits"]
|
||||||
if priv and not (isinstance(priv, str) or isinstance(priv, dict)):
|
if priv and not (isinstance(priv, str) or isinstance(priv, dict)):
|
||||||
module.fail_json(msg="priv parameter must be str or dict but %s was passed" % type(priv))
|
module.fail_json(msg="priv parameter must be str or dict but %s was passed" % type(priv))
|
||||||
|
|
||||||
|
@ -793,6 +941,10 @@ def main():
|
||||||
|
|
||||||
except (SQLParseError, InvalidPrivsError, mysql_driver.Error) as e:
|
except (SQLParseError, InvalidPrivsError, mysql_driver.Error) as e:
|
||||||
module.fail_json(msg=to_native(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":
|
elif state == "absent":
|
||||||
if user_exists(cursor, user, host, host_all):
|
if user_exists(cursor, user, host, host_all):
|
||||||
changed = user_delete(cursor, user, host, host_all, module.check_mode)
|
changed = user_delete(cursor, user, host, host_all, module.check_mode)
|
||||||
|
|
|
@ -21,6 +21,8 @@
|
||||||
#
|
#
|
||||||
- include: create_user.yml user_name={{user_name_1}} user_password={{ user_password_1 }}
|
- include: create_user.yml user_name={{user_name_1}} user_password={{ user_password_1 }}
|
||||||
|
|
||||||
|
- include: resource_limits.yml
|
||||||
|
|
||||||
- include: assert_user.yml user_name={{user_name_1}}
|
- include: assert_user.yml user_name={{user_name_1}}
|
||||||
|
|
||||||
- include: remove_user.yml user_name={{user_name_1}} user_password={{ user_password_1 }}
|
- include: remove_user.yml user_name={{user_name_1}} user_password={{ user_password_1 }}
|
||||||
|
|
112
tests/integration/targets/mysql_user/tasks/resource_limits.yml
Normal file
112
tests/integration/targets/mysql_user/tasks/resource_limits.yml
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
# test code for resource_limits parameter
|
||||||
|
|
||||||
|
- block:
|
||||||
|
|
||||||
|
- name: Drop mysql user {{ user_name_1 }} if exists
|
||||||
|
mysql_user:
|
||||||
|
name: '{{ user_name_1 }}'
|
||||||
|
state: absent
|
||||||
|
login_unix_socket: '{{ mysql_socket }}'
|
||||||
|
|
||||||
|
- name: Create mysql user {{ user_name_1 }} with resource limits in check_mode
|
||||||
|
mysql_user:
|
||||||
|
name: '{{ user_name_1 }}'
|
||||||
|
password: '{{ user_password_1 }}'
|
||||||
|
state: present
|
||||||
|
login_unix_socket: '{{ mysql_socket }}'
|
||||||
|
resource_limits:
|
||||||
|
MAX_QUERIES_PER_HOUR: 10
|
||||||
|
MAX_CONNECTIONS_PER_HOUR: 5
|
||||||
|
check_mode: yes
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
|
||||||
|
- name: Create mysql user {{ user_name_1 }} with resource limits in actual mode
|
||||||
|
mysql_user:
|
||||||
|
name: '{{ user_name_1 }}'
|
||||||
|
password: '{{ user_password_1 }}'
|
||||||
|
state: present
|
||||||
|
login_unix_socket: '{{ mysql_socket }}'
|
||||||
|
resource_limits:
|
||||||
|
MAX_QUERIES_PER_HOUR: 10
|
||||||
|
MAX_CONNECTIONS_PER_HOUR: 5
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
|
||||||
|
- name: Check
|
||||||
|
mysql_query:
|
||||||
|
query: >
|
||||||
|
SELECT User FROM mysql.user WHERE User = '{{ user_name_1 }}' AND Host = 'localhost'
|
||||||
|
AND max_questions = 10 AND max_connections = 5
|
||||||
|
login_unix_socket: '{{ mysql_socket }}'
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- result.rowcount[0] == 1
|
||||||
|
|
||||||
|
- name: Try to set the same limits again in check mode
|
||||||
|
mysql_user:
|
||||||
|
name: '{{ user_name_1 }}'
|
||||||
|
password: '{{ user_password_1 }}'
|
||||||
|
state: present
|
||||||
|
login_unix_socket: '{{ mysql_socket }}'
|
||||||
|
resource_limits:
|
||||||
|
MAX_QUERIES_PER_HOUR: 10
|
||||||
|
MAX_CONNECTIONS_PER_HOUR: 5
|
||||||
|
check_mode: yes
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- result is not changed
|
||||||
|
|
||||||
|
- name: Try to set the same limits again in actual mode
|
||||||
|
mysql_user:
|
||||||
|
name: '{{ user_name_1 }}'
|
||||||
|
password: '{{ user_password_1 }}'
|
||||||
|
state: present
|
||||||
|
login_unix_socket: '{{ mysql_socket }}'
|
||||||
|
resource_limits:
|
||||||
|
MAX_QUERIES_PER_HOUR: 10
|
||||||
|
MAX_CONNECTIONS_PER_HOUR: 5
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- result is not changed
|
||||||
|
|
||||||
|
- name: Change limits
|
||||||
|
mysql_user:
|
||||||
|
name: '{{ user_name_1 }}'
|
||||||
|
password: '{{ user_password_1 }}'
|
||||||
|
state: present
|
||||||
|
login_unix_socket: '{{ mysql_socket }}'
|
||||||
|
resource_limits:
|
||||||
|
MAX_QUERIES_PER_HOUR: 5
|
||||||
|
MAX_CONNECTIONS_PER_HOUR: 5
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- result is changed
|
||||||
|
|
||||||
|
- name: Check
|
||||||
|
mysql_query:
|
||||||
|
query: >
|
||||||
|
SELECT User FROM mysql.user WHERE User = '{{ user_name_1 }}' AND Host = 'localhost'
|
||||||
|
AND max_questions = 5 AND max_connections = 5
|
||||||
|
login_unix_socket: '{{ mysql_socket }}'
|
||||||
|
register: result
|
||||||
|
|
||||||
|
- assert:
|
||||||
|
that:
|
||||||
|
- result.rowcount[0] == 1
|
||||||
|
|
||||||
|
when: (ansible_distribution == 'Ubuntu' and ansible_distribution_major_version >= '18') or (ansible_distribution == 'CentOS' and ansible_distribution_major_version >= '8')
|
Loading…
Reference in a new issue