1
0
Fork 0
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:
Andrew Klychkov 2020-04-12 07:13:56 +03:00 committed by GitHub
parent 5fbe8a1f9d
commit dd1bb708d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 268 additions and 0 deletions

View file

@ -0,0 +1,2 @@
minor_changes:
- mysql_user - add the resource_limits parameter (https://github.com/ansible-collections/community.general/issues/133).

View file

@ -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)

View file

@ -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 }}

View 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')