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:
|
||||
- User's plugin auth_string (``CREATE USER user IDENTIFIED WITH plugin BY plugin_auth_string``).
|
||||
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:
|
||||
- "MySQL server installs with default login_user of 'root' and no password. To secure this user
|
||||
|
@ -232,6 +239,13 @@ EXAMPLES = r'''
|
|||
priv: '*.*:ALL'
|
||||
state: present
|
||||
|
||||
- name: Limit bob's resources to 10 queries per hour and 5 connections per hour
|
||||
mysql_user:
|
||||
name: bob
|
||||
resource_limits:
|
||||
MAX_QUERIES_PER_HOUR: 10
|
||||
MAX_CONNECTIONS_PER_HOUR: 5
|
||||
|
||||
# Example .my.cnf file for setting the root password
|
||||
# [client]
|
||||
# user=root
|
||||
|
@ -672,6 +686,138 @@ def convert_priv_dict_to_str(priv):
|
|||
|
||||
return '/'.join(priv_list)
|
||||
|
||||
|
||||
# Alter user is supported since MySQL 5.6 and MariaDB 10.2.0
|
||||
def server_supports_alter_user(cursor):
|
||||
"""Check if the server supports ALTER USER statement or doesn't.
|
||||
|
||||
Args:
|
||||
cursor (cursor): DB driver cursor object.
|
||||
|
||||
Returns: True if supports, False otherwise.
|
||||
"""
|
||||
cursor.execute("SELECT VERSION()")
|
||||
version_str = cursor.fetchone()[0]
|
||||
version = version_str.split('.')
|
||||
|
||||
if 'mariadb' in version_str.lower():
|
||||
# MariaDB 10.2 and later
|
||||
if int(version[0]) * 1000 + int(version[1]) >= 10002:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
# MySQL 5.6 and later
|
||||
if int(version[0]) * 1000 + int(version[1]) >= 5006:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def get_resource_limits(cursor, user, host):
|
||||
"""Get user resource limits.
|
||||
|
||||
Args:
|
||||
cursor (cursor): DB driver cursor object.
|
||||
user (str): User name.
|
||||
host (str): User host name.
|
||||
|
||||
Returns: Dictionary containing current resource limits.
|
||||
"""
|
||||
|
||||
query = ('SELECT max_questions AS MAX_QUERIES_PER_HOUR, '
|
||||
'max_updates AS MAX_UPDATES_PER_HOUR, '
|
||||
'max_connections AS MAX_CONNECTIONS_PER_HOUR, '
|
||||
'max_user_connections AS MAX_USER_CONNECTIONS '
|
||||
'FROM mysql.user WHERE User = %s AND Host = %s')
|
||||
cursor.execute(query, (user, host))
|
||||
res = cursor.fetchone()
|
||||
|
||||
if not res:
|
||||
return None
|
||||
|
||||
current_limits = {
|
||||
'MAX_QUERIES_PER_HOUR': res[0],
|
||||
'MAX_UPDATES_PER_HOUR': res[1],
|
||||
'MAX_CONNECTIONS_PER_HOUR': res[2],
|
||||
'MAX_USER_CONNECTIONS': res[3],
|
||||
}
|
||||
return current_limits
|
||||
|
||||
|
||||
def match_resource_limits(module, current, desired):
|
||||
"""Check and match limits.
|
||||
|
||||
Args:
|
||||
module (AnsibleModule): Ansible module object.
|
||||
current (dict): Dictionary with current limits.
|
||||
desired (dict): Dictionary with desired limits.
|
||||
|
||||
Returns: Dictionary containing parameters that need to change.
|
||||
"""
|
||||
|
||||
if not current:
|
||||
# It means the user does not exists, so we need
|
||||
# to set all limits after its creation
|
||||
return desired
|
||||
|
||||
needs_to_change = {}
|
||||
|
||||
for key, val in iteritems(desired):
|
||||
if key not in current:
|
||||
# Supported keys are listed in the documentation
|
||||
# and must be determined in the get_resource_limits function
|
||||
# (follow 'AS' keyword)
|
||||
module.fail_json(msg="resource_limits: key '%s' is unsupported." % key)
|
||||
|
||||
try:
|
||||
val = int(val)
|
||||
except Exception:
|
||||
module.fail_json(msg="Can't convert value '%s' to integer." % val)
|
||||
|
||||
if val != current.get(key):
|
||||
needs_to_change[key] = val
|
||||
|
||||
return needs_to_change
|
||||
|
||||
|
||||
def limit_resources(module, cursor, user, host, resource_limits, check_mode):
|
||||
"""Limit user resources.
|
||||
|
||||
Args:
|
||||
module (AnsibleModule): Ansible module object.
|
||||
cursor (cursor): DB driver cursor object.
|
||||
user (str): User name.
|
||||
host (str): User host name.
|
||||
resource_limit (dict): Dictionary with desired limits.
|
||||
check_mode (bool): Run the function in check mode or not.
|
||||
|
||||
Returns: True, if changed, False otherwise.
|
||||
"""
|
||||
if not server_supports_alter_user(cursor):
|
||||
module.fail_json(msg="The server version does not match the requirements "
|
||||
"for resource_limits parameter. See module's documentation.")
|
||||
|
||||
current_limits = get_resource_limits(cursor, user, host)
|
||||
|
||||
needs_to_change = match_resource_limits(module, current_limits, resource_limits)
|
||||
|
||||
if not needs_to_change:
|
||||
return False
|
||||
|
||||
if needs_to_change and check_mode:
|
||||
return True
|
||||
|
||||
# If not check_mode
|
||||
tmp = []
|
||||
for key, val in iteritems(needs_to_change):
|
||||
tmp.append('%s %s' % (key, val))
|
||||
|
||||
query = "ALTER USER %s@%s"
|
||||
query += ' WITH %s' % ' '.join(tmp)
|
||||
cursor.execute(query, (user, host))
|
||||
return True
|
||||
|
||||
# ===========================================
|
||||
# Module execution.
|
||||
#
|
||||
|
@ -704,6 +850,7 @@ def main():
|
|||
plugin=dict(default=None, type='str'),
|
||||
plugin_hash_string=dict(default=None, type='str'),
|
||||
plugin_auth_string=dict(default=None, type='str'),
|
||||
resource_limits=dict(type='dict'),
|
||||
),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
|
@ -729,6 +876,7 @@ def main():
|
|||
plugin = module.params["plugin"]
|
||||
plugin_hash_string = module.params["plugin_hash_string"]
|
||||
plugin_auth_string = module.params["plugin_auth_string"]
|
||||
resource_limits = module.params["resource_limits"]
|
||||
if priv and not (isinstance(priv, str) or isinstance(priv, dict)):
|
||||
module.fail_json(msg="priv parameter must be str or dict but %s was passed" % type(priv))
|
||||
|
||||
|
@ -793,6 +941,10 @@ def main():
|
|||
|
||||
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)
|
||||
|
|
|
@ -21,6 +21,8 @@
|
|||
#
|
||||
- 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: 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