From 900899740bac7fcb03fb1186f56679b1921f19df Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Thu, 15 Apr 2021 10:21:17 +0200 Subject: [PATCH 01/43] Release 2.0.0 commit --- changelogs/CHANGELOG.rst | 29 ++++++++++++++++ changelogs/changelog.yaml | 33 +++++++++++++++++++ .../fragments/101-drop-requiressl-support.yml | 4 --- .../103-mysql_and_mariadb_divergence.yml | 2 -- .../fragments/108-mysql_priv_add_grant.yml | 2 -- ...d_mysql_full_version_suffix_return_var.yml | 2 -- ...ange_deprecated_connection_ parameters.yml | 2 -- ...replication_remove_slave_from_messages.yml | 4 --- galaxy.yml | 2 +- 9 files changed, 63 insertions(+), 17 deletions(-) delete mode 100644 changelogs/fragments/101-drop-requiressl-support.yml delete mode 100644 changelogs/fragments/103-mysql_and_mariadb_divergence.yml delete mode 100644 changelogs/fragments/108-mysql_priv_add_grant.yml delete mode 100644 changelogs/fragments/115-add_mysql_full_version_suffix_return_var.yml delete mode 100644 changelogs/fragments/116-change_deprecated_connection_ parameters.yml delete mode 100644 changelogs/fragments/144-mysql_replication_remove_slave_from_messages.yml diff --git a/changelogs/CHANGELOG.rst b/changelogs/CHANGELOG.rst index a80c771..c186696 100644 --- a/changelogs/CHANGELOG.rst +++ b/changelogs/CHANGELOG.rst @@ -5,6 +5,35 @@ Community MySQL Collection Release Notes .. contents:: Topics +v2.0.0 +====== + +Release Summary +--------------- + +This is release 2.0.0 of the ``community.mysql`` collection, released on 2021-04-15. + +Major Changes +------------- + +- mysql_replication - the return value ``Is_Slave`` and ``Is_Master`` will be replaced with ``Is_Replica`` and ``Is_Primary`` in ``community.mysql`` 3.0.0 (https://github.com/ansible-collections/community.mysql/issues/145). +- mysql_replication - the word ``master`` in messages returned by the module will be replaced with ``primary`` in ``community.mysql`` 3.0.0 (https://github.com/ansible-collections/community.mysql/issues/145). +- mysql_replication - the word ``slave`` in messages returned by the module replaced with ``replica`` (https://github.com/ansible-collections/community.mysql/issues/98). +- mysql_user - the ``REQUIRESSL`` is an alias for the ``ssl`` key in the ``tls_requires`` option in ``community.mysql`` 2.0.0 and support will be dropped altogether in ``community.mysql`` 3.0.0 (https://github.com/ansible-collections/community.mysql/issues/121). + +Minor Changes +------------- + +- mysql module utils - change deprecated connection parameters ``passwd`` and ``db`` to ``password`` and ``database`` (https://github.com/ansible-collections/community.mysql/pull/116). +- mysql_collection - introduce codebabse split to handle divergences between MySQL and MariaDB (https://github.com/ansible-collections/community.mysql/pull/103). +- mysql_info - add `version.full` and `version.suffix` return values (https://github.com/ansible-collections/community.mysql/issues/114). +- mysql_user - deprecate the ``REQUIRESSL`` privilege (https://github.com/ansible-collections/community.mysql/issues/101). + +Bugfixes +-------- + +- mysql_user - add support for ``REPLICA MONITOR`` privilege (https://github.com/ansible-collections/community.mysql/issues/105). + v1.3.0 ====== diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index c163341..fe92a0f 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -235,3 +235,36 @@ releases: - 107-mysql_user_fix_grant_on_col_handling.yml - 97-mysql_replication_deprecate_offending_terminology.yml release_date: '2021-03-08' + 2.0.0: + changes: + bugfixes: + - mysql_user - add support for ``REPLICA MONITOR`` privilege (https://github.com/ansible-collections/community.mysql/issues/105). + major_changes: + - mysql_replication - the return value ``Is_Slave`` and ``Is_Master`` will be + replaced with ``Is_Replica`` and ``Is_Primary`` in ``community.mysql`` 3.0.0 + (https://github.com/ansible-collections/community.mysql/issues/145). + - mysql_replication - the word ``master`` in messages returned by the module + will be replaced with ``primary`` in ``community.mysql`` 3.0.0 (https://github.com/ansible-collections/community.mysql/issues/145). + - mysql_replication - the word ``slave`` in messages returned by the module + replaced with ``replica`` (https://github.com/ansible-collections/community.mysql/issues/98). + - mysql_user - the ``REQUIRESSL`` is an alias for the ``ssl`` key in the ``tls_requires`` + option in ``community.mysql`` 2.0.0 and support will be dropped altogether + in ``community.mysql`` 3.0.0 (https://github.com/ansible-collections/community.mysql/issues/121). + minor_changes: + - mysql module utils - change deprecated connection parameters ``passwd`` and + ``db`` to ``password`` and ``database`` (https://github.com/ansible-collections/community.mysql/pull/116). + - mysql_collection - introduce codebabse split to handle divergences between + MySQL and MariaDB (https://github.com/ansible-collections/community.mysql/pull/103). + - mysql_info - add `version.full` and `version.suffix` return values (https://github.com/ansible-collections/community.mysql/issues/114). + - mysql_user - deprecate the ``REQUIRESSL`` privilege (https://github.com/ansible-collections/community.mysql/issues/101). + release_summary: This is release 2.0.0 of the ``community.mysql`` collection, + released on 2021-04-15. + fragments: + - 101-drop-requiressl-support.yml + - 103-mysql_and_mariadb_divergence.yml + - 108-mysql_priv_add_grant.yml + - 115-add_mysql_full_version_suffix_return_var.yml + - 116-change_deprecated_connection_ parameters.yml + - 144-mysql_replication_remove_slave_from_messages.yml + - 2.0.0.yml + release_date: '2021-04-15' diff --git a/changelogs/fragments/101-drop-requiressl-support.yml b/changelogs/fragments/101-drop-requiressl-support.yml deleted file mode 100644 index fded3e3..0000000 --- a/changelogs/fragments/101-drop-requiressl-support.yml +++ /dev/null @@ -1,4 +0,0 @@ -minor_changes: - - mysql_user - deprecate the ``REQUIRESSL`` privilege (https://github.com/ansible-collections/community.mysql/issues/101). -major_changes: - - mysql_user - the ``REQUIRESSL`` is an alias for the ``ssl`` key in the ``tls_requires`` option in ``community.mysql`` 2.0.0 and support will be dropped altogether in ``community.mysql`` 3.0.0 (https://github.com/ansible-collections/community.mysql/issues/121). diff --git a/changelogs/fragments/103-mysql_and_mariadb_divergence.yml b/changelogs/fragments/103-mysql_and_mariadb_divergence.yml deleted file mode 100644 index 039c654..0000000 --- a/changelogs/fragments/103-mysql_and_mariadb_divergence.yml +++ /dev/null @@ -1,2 +0,0 @@ -minor_changes: -- mysql_collection - introduce codebabse split to handle divergences between MySQL and MariaDB (https://github.com/ansible-collections/community.mysql/pull/103). diff --git a/changelogs/fragments/108-mysql_priv_add_grant.yml b/changelogs/fragments/108-mysql_priv_add_grant.yml deleted file mode 100644 index 4cfd00e..0000000 --- a/changelogs/fragments/108-mysql_priv_add_grant.yml +++ /dev/null @@ -1,2 +0,0 @@ -bugfixes: -- mysql_user - add support for ``REPLICA MONITOR`` privilege (https://github.com/ansible-collections/community.mysql/issues/105). diff --git a/changelogs/fragments/115-add_mysql_full_version_suffix_return_var.yml b/changelogs/fragments/115-add_mysql_full_version_suffix_return_var.yml deleted file mode 100644 index 3eadb46..0000000 --- a/changelogs/fragments/115-add_mysql_full_version_suffix_return_var.yml +++ /dev/null @@ -1,2 +0,0 @@ -minor_changes: -- mysql_info - add `version.full` and `version.suffix` return values (https://github.com/ansible-collections/community.mysql/issues/114). diff --git a/changelogs/fragments/116-change_deprecated_connection_ parameters.yml b/changelogs/fragments/116-change_deprecated_connection_ parameters.yml deleted file mode 100644 index d6b0d7e..0000000 --- a/changelogs/fragments/116-change_deprecated_connection_ parameters.yml +++ /dev/null @@ -1,2 +0,0 @@ -minor_changes: -- mysql module utils - change deprecated connection parameters ``passwd`` and ``db`` to ``password`` and ``database`` (https://github.com/ansible-collections/community.mysql/pull/116). \ No newline at end of file diff --git a/changelogs/fragments/144-mysql_replication_remove_slave_from_messages.yml b/changelogs/fragments/144-mysql_replication_remove_slave_from_messages.yml deleted file mode 100644 index e88691e..0000000 --- a/changelogs/fragments/144-mysql_replication_remove_slave_from_messages.yml +++ /dev/null @@ -1,4 +0,0 @@ -major_changes: -- mysql_replication - the word ``slave`` in messages returned by the module replaced with ``replica`` (https://github.com/ansible-collections/community.mysql/issues/98). -- mysql_replication - the return value ``Is_Slave`` and ``Is_Master`` will be replaced with ``Is_Replica`` and ``Is_Primary`` in ``community.mysql`` 3.0.0 (https://github.com/ansible-collections/community.mysql/issues/145). -- mysql_replication - the word ``master`` in messages returned by the module will be replaced with ``primary`` in ``community.mysql`` 3.0.0 (https://github.com/ansible-collections/community.mysql/issues/145). diff --git a/galaxy.yml b/galaxy.yml index dad76b9..f625ed9 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,6 +1,6 @@ namespace: community name: mysql -version: 1.3.0 +version: 2.0.0 readme: README.md authors: - Ansible community From 5fbac2248663dedd0d938c471394c5f5fdeb73c1 Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Thu, 15 Apr 2021 10:48:56 +0200 Subject: [PATCH 02/43] Update collection version to the next expected in galaxy.yml --- galaxy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/galaxy.yml b/galaxy.yml index f625ed9..5d44fe8 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,6 +1,6 @@ namespace: community name: mysql -version: 2.0.0 +version: 2.1.0 readme: README.md authors: - Ansible community From da7e73ef6e60881448423ba628e2e930ee3372b0 Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Fri, 16 Apr 2021 07:23:53 +0200 Subject: [PATCH 03/43] mysql_replication: add deprecation warning about future replacement of Is_Slave and Is_Master return values, add alternatives (#147) (#149) * mysql_replication: add deprecation warning about future replacement of Is_Slave and Is_Master return values, add alternatives * Add changelog fragment (cherry picked from commit 853db5a2a415cb2b9dbcea0c0c65aec64e9a2f82) --- ...7-mysql_replication_deprecate_ret_vals.yml | 5 ++++ plugins/modules/mysql_replication.py | 23 ++++++++++++++----- .../tasks/mysql_replication_channel.yml | 4 ++-- .../tasks/mysql_replication_initial.yml | 4 ++-- 4 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 changelogs/fragments/147-mysql_replication_deprecate_ret_vals.yml diff --git a/changelogs/fragments/147-mysql_replication_deprecate_ret_vals.yml b/changelogs/fragments/147-mysql_replication_deprecate_ret_vals.yml new file mode 100644 index 0000000..9261453 --- /dev/null +++ b/changelogs/fragments/147-mysql_replication_deprecate_ret_vals.yml @@ -0,0 +1,5 @@ +major_changes: +- mysql_replication - add deprecation warning that the ``Is_Slave`` and ``Is_Master`` return values will be replaced with ``Is_Primary`` and ``Is_Replica`` in ``community.mysql`` 3.0.0 (https://github.com/ansible-collections/community.mysql/pull/147). + +minor_changes: +- mysql_replication - add the ``Is_Primary`` and ``Is_Replica`` alternatives to the ``Is_Slave`` and ``Is_Master`` return values as a preparation for replacement in ``community.mysql`` 3.0.0 (https://github.com/ansible-collections/community.mysql/pull/147). diff --git a/plugins/modules/mysql_replication.py b/plugins/modules/mysql_replication.py index e81ef17..8f1d269 100644 --- a/plugins/modules/mysql_replication.py +++ b/plugins/modules/mysql_replication.py @@ -524,9 +524,17 @@ def main(): if mode in "getmaster": status = get_master_status(cursor) if not isinstance(status, dict): - status = dict(Is_Master=False, msg="Server is not configured as mysql master") + status = dict(Is_Master=False, Is_Primary=False, + msg="Server is not configured as mysql master") else: status['Is_Master'] = True + status['Is_Primary'] = True + + module.deprecate('"Is_Master" and "Is_Slave" return values are deprecated ' + 'and will be replaced with "Is_Primary" and "Is_Replica" ' + 'in the next major release. Use "Is_Primary" and "Is_Replica" instead.', + version='3.0.0', collection_name='community.mysql') + module.exit_json(queries=executed_queries, **status) elif mode in ("getreplica", "getslave"): @@ -536,13 +544,16 @@ def main(): status = get_replica_status(cursor, connection_name, channel, replica_term) if not isinstance(status, dict): - # TODO: announce it and replace with Replica - # in the next major release. Maybe a warning? - status = dict(Is_Slave=False, msg="Server is not configured as mysql replica") + status = dict(Is_Slave=False, Is_Replica=False, msg="Server is not configured as mysql replica") else: - # TODO: announce it and replace with Replica - # in the next major release. Maybe a warning? status['Is_Slave'] = True + status['Is_Replica'] = True + + module.deprecate('"Is_Master" and "Is_Slave" return values are deprecated ' + 'and will be replaced with "Is_Primary" and "Is_Replica" ' + 'in the next major release. Use "Is_Primary" and "Is_Replica" instead.', + version='3.0.0', collection_name='community.mysql') + module.exit_json(queries=executed_queries, **status) elif mode in "changemaster": diff --git a/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_channel.yml b/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_channel.yml index 1bbc1bc..c95f792 100644 --- a/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_channel.yml +++ b/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_channel.yml @@ -61,7 +61,7 @@ - assert: that: - - replica_status.Is_Slave == true + - replica_status.Is_Replica == true - replica_status.Master_Host == '{{ mysql_host }}' - replica_status.Exec_Master_Log_Pos == mysql_primary_status.Position - replica_status.Master_Port == {{ mysql_primary_port }} @@ -73,7 +73,7 @@ - assert: that: - - replica_status.Is_Slave == true + - replica_status.Is_Replica == true - replica_status.Source_Host == '{{ mysql_host }}' - replica_status.Exec_Source_Log_Pos == mysql_primary_status.Position - replica_status.Source_Port == {{ mysql_primary_port }} diff --git a/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_initial.yml b/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_initial.yml index 4e90707..27743fb 100644 --- a/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_initial.yml +++ b/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_initial.yml @@ -59,7 +59,7 @@ - assert: that: - - mysql_primary_status.Is_Master == true + - mysql_primary_status.Is_Primary == true - mysql_primary_status.Position != 0 - mysql_primary_status is not changed @@ -148,7 +148,7 @@ - assert: that: - - replica_status.Is_Slave == true + - replica_status.Is_Replica == true - replica_status.Master_Host == '{{ mysql_host }}' - replica_status.Exec_Master_Log_Pos == mysql_primary_status.Position - replica_status.Master_Port == {{ mysql_primary_port }} From d9651f37d30aca9d4bf9dc7bef7b30261e533661 Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Fri, 23 Apr 2021 08:31:02 +0300 Subject: [PATCH 04/43] mysql_replication: Add aliases to "master" options, add alternatives to "master" state options, add announcement (#150) (#152) * mysql_replication: Add aliases, add alternatives for the state option, announce major changes * Change tests * Add changelog fragment * Fix changelog * Update plugins/modules/mysql_replication.py Co-authored-by: Jorge Rodriguez (A.K.A. Tiriel) * Update plugins/modules/mysql_replication.py Co-authored-by: Jorge Rodriguez (A.K.A. Tiriel) Co-authored-by: Jorge Rodriguez (A.K.A. Tiriel) (cherry picked from commit c8f9b1cd3f668305d7fd5ff169db64e9225bcb4b) --- .../150-mysql_replication_master_related.yml | 6 + plugins/modules/mysql_replication.py | 317 ++++++++++-------- .../test_mysql_replication/defaults/main.yml | 2 +- .../test_mysql_replication/tasks/issue-28.yml | 4 +- .../test_mysql_replication/tasks/main.yml | 8 +- .../tasks/mysql_replication_channel.yml | 22 +- .../tasks/mysql_replication_initial.yml | 32 +- ...ml => mysql_replication_primary_delay.yml} | 10 +- ...> mysql_replication_resetprimary_mode.yml} | 18 +- 9 files changed, 234 insertions(+), 185 deletions(-) create mode 100644 changelogs/fragments/150-mysql_replication_master_related.yml rename tests/integration/targets/test_mysql_replication/tasks/{mysql_replication_master_delay.yml => mysql_replication_primary_delay.yml} (84%) rename tests/integration/targets/test_mysql_replication/tasks/{mysql_replication_resetmaster_mode.yml => mysql_replication_resetprimary_mode.yml} (83%) diff --git a/changelogs/fragments/150-mysql_replication_master_related.yml b/changelogs/fragments/150-mysql_replication_master_related.yml new file mode 100644 index 0000000..83b3538 --- /dev/null +++ b/changelogs/fragments/150-mysql_replication_master_related.yml @@ -0,0 +1,6 @@ +minor_changes: +- mysql_replication - change ``master_`` options to ``primary_`` options, add aliases to keep compatibility (https://github.com/ansible-collections/community.mysql/pull/150). +- mysql_replication - add alternative (``primary``) choices to the ``state`` option choices containing ``master`` (https://github.com/ansible-collections/community.mysql/pull/150). + +major_changes: +- mysql_replication - the choices of the ``state`` option containing ``master`` will be finally replaced with the alternative ``primary`` choices in ``community.mysql`` 3.0.0, add deprecation warnings (https://github.com/ansible-collections/community.mysql/pull/150). diff --git a/plugins/modules/mysql_replication.py b/plugins/modules/mysql_replication.py index 8f1d269..12dffc4 100644 --- a/plugins/modules/mysql_replication.py +++ b/plugins/modules/mysql_replication.py @@ -15,7 +15,7 @@ DOCUMENTATION = r''' module: mysql_replication short_description: Manage MySQL replication description: -- Manages MySQL server replication, replica, master status, get and change master host. +- Manages MySQL server replication, replica, primary status, get and change primary host. author: - Balazs Pocze (@banyek) - Andrew Klychkov (@Andersson007) @@ -23,17 +23,19 @@ options: mode: description: - Module operating mode. Could be - C(changemaster) (CHANGE MASTER TO), - C(getmaster) (SHOW MASTER STATUS), + C(changeprimary | changemaster) (CHANGE PRIMARY | MASTER TO), + C(getprimary | getmaster) (SHOW PRIMARY | MASTER STATUS), C(getreplica | getslave) (SHOW REPLICA | SLAVE STATUS), C(startreplica | startslave) (START REPLICA | SLAVE), C(stopreplica | stopslave) (STOP REPLICA | SLAVE), - C(resetmaster) (RESET MASTER) - supported since community.mysql 0.1.0, + C(resetprimary | resetmaster) (RESET PRIMARY | MASTER) - supported since community.mysql 0.1.0, C(resetreplica, resetslave) (RESET REPLICA | SLAVE), C(resetreplicaall, resetslave) (RESET REPLICA | SLAVE ALL). type: str choices: + - changeprimary - changemaster + - getprimary - getmaster - getreplica - getslave @@ -41,40 +43,48 @@ options: - startslave - stopreplica - stopslave + - resetprimary - resetmaster - resetreplica - resetslave - resetreplicaall - resetslaveall default: getreplica - master_host: + primary_host: description: - - Same as mysql variable. + - Same as the C(MASTER_HOST) mysql variable. type: str - master_user: + aliases: [master_host] + primary_user: description: - - Same as mysql variable. + - Same as the C(MASTER_USER) mysql variable. type: str - master_password: + aliases: [master_user] + primary_password: description: - - Same as mysql variable. + - Same as the C(MASTER_PASSWORD) mysql variable. type: str - master_port: + aliases: [master_password] + primary_port: description: - - Same as mysql variable. + - Same as the C(MASTER_PORT) mysql variable. type: int - master_connect_retry: + aliases: [master_port] + primary_connect_retry: description: - - Same as mysql variable. + - Same as the C(MASTER_CONNECT_RETRY) mysql variable. type: int - master_log_file: + aliases: [master_connect_retry] + primary_log_file: description: - - Same as mysql variable. + - Same as the C(MASTER_LOG_FILE) mysql variable. type: str - master_log_pos: + aliases: [master_log_file] + primary_log_pos: description: - - Same as mysql variable. + - Same as the C(MASTER_LOG_POS) mysql variable. type: int + aliases: [master_log_pos] relay_log_file: description: - Same as mysql variable. @@ -83,7 +93,7 @@ options: description: - Same as mysql variable. type: int - master_ssl: + primary_ssl: description: - Same as the C(MASTER_SSL) mysql variable. - When setting it to C(yes), the connection attempt only succeeds @@ -92,43 +102,51 @@ options: L(MySQL encrypted replication documentation,https://dev.mysql.com/doc/refman/8.0/en/replication-solutions-encrypted-connections.html). type: bool default: false - master_ssl_ca: + 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 - master_ssl_capath: + 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 - master_ssl_cert: + 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 - master_ssl_key: + 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 - master_ssl_cipher: + 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 - master_auto_position: + aliases: [master_ssl_cipher] + 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 - master_use_gtid: + 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. @@ -140,16 +158,19 @@ options: choices: [current_pos, replica_pos, slave_pos, disabled] type: str version_added: '0.1.0' - master_delay: + aliases: [master_use_gtid] + primary_delay: description: - - Time lag behind the master's state (in seconds). + - 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 master connection. + - 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/). @@ -195,16 +216,16 @@ EXAMPLES = r''' community.mysql.mysql_replication: mode: stopreplica -- name: Get master binlog file name and binlog position +- name: Get primary binlog file name and binlog position community.mysql.mysql_replication: - mode: getmaster + mode: getprimary -- name: Change master to master server 192.0.2.1 and use binary log 'mysql-bin.000009' with position 4578 +- 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: changemaster - master_host: 192.0.2.1 - master_log_file: mysql-bin.000009 - master_log_pos: 4578 + 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: @@ -212,42 +233,42 @@ EXAMPLES = r''' login_host: ansible.example.com login_port: 3308 -- name: On MariaDB change master to use GTID current_pos +- name: On MariaDB change primary to use GTID current_pos community.mysql.mysql_replication: - mode: changemaster - master_use_gtid: current_pos + mode: changeprimary + primary_use_gtid: current_pos -- name: Change master to use replication delay 3600 seconds +- name: Change primary to use replication delay 3600 seconds community.mysql.mysql_replication: - mode: changemaster - master_host: 192.0.2.1 - master_delay: 3600 + mode: changeprimary + primary_host: 192.0.2.1 + primary_delay: 3600 -- name: Start MariaDB replica with connection name master-1 +- name: Start MariaDB replica with connection name primary-1 community.mysql.mysql_replication: mode: startreplica - connection_name: master-1 + connection_name: primary-1 -- name: Stop replication in channel master-1 +- name: Stop replication in channel primary-1 community.mysql.mysql_replication: mode: stopreplica - channel: master-1 + 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 master + and reset the binary log index file on the primary community.mysql.mysql_replication: - mode: resetmaster + mode: resetprimary - name: Run start replica and fail the task on errors community.mysql.mysql_replication: mode: startreplica - connection_name: master-1 + connection_name: primary-1 fail_on_error: yes -- name: Change master and fail on error (like when replica thread is running) +- name: Change primary and fail on error (like when replica thread is running) community.mysql.mysql_replication: - mode: changemaster + mode: changeprimary fail_on_error: yes ''' @@ -257,7 +278,7 @@ queries: description: List of executed queries which modified DB's state. returned: always type: list - sample: ["CHANGE MASTER TO MASTER_HOST='master2.example.com',MASTER_PORT=3306"] + sample: ["CHANGE MASTER TO MASTER_HOST='primary2.example.com',MASTER_PORT=3306"] version_added: '0.1.0' ''' @@ -277,10 +298,13 @@ from distutils.version import LooseVersion executed_queries = [] -def get_master_status(cursor): +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") - masterstatus = cursor.fetchone() - return masterstatus + primarystatus = cursor.fetchone() + return primarystatus def get_replica_status(cursor, connection_name='', channel='', term='REPLICA'): @@ -363,7 +387,7 @@ def reset_replica_all(module, cursor, connection_name='', channel='', fail_on_er return reset -def reset_master(module, cursor, fail_on_error=False): +def reset_primary(module, cursor, fail_on_error=False): query = 'RESET MASTER' try: executed_queries.append(query) @@ -400,7 +424,7 @@ def start_replica(module, cursor, connection_name='', channel='', fail_on_error= return started -def changemaster(cursor, chm, connection_name='', channel=''): +def changeprimary(cursor, chm, connection_name='', channel=''): if connection_name: query = "CHANGE MASTER '%s' TO %s" % (connection_name, ','.join(chm)) else: @@ -417,29 +441,33 @@ def main(): argument_spec = mysql_common_argument_spec() argument_spec.update( mode=dict(type='str', default='getreplica', choices=[ - 'getmaster', 'getreplica', 'getslave', 'changemaster', - 'stopreplica', 'stopslave', 'startreplica', 'startslave', - 'resetmaster', 'resetreplica', 'resetslave', + 'getprimary', 'getmaster', + 'getreplica', 'getslave', + 'changeprimary', 'changemaster', + 'stopreplica', 'stopslave', + 'startreplica', 'startslave', + 'resetprimary', 'resetmaster', + 'resetreplica', 'resetslave', 'resetreplicaall', 'resetslaveall']), - master_auto_position=dict(type='bool', default=False), - master_host=dict(type='str'), - master_user=dict(type='str'), - master_password=dict(type='str', no_log=True), - master_port=dict(type='int'), - master_connect_retry=dict(type='int'), - master_log_file=dict(type='str'), - master_log_pos=dict(type='int'), + 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'), - master_ssl=dict(type='bool', default=False), - master_ssl_ca=dict(type='str'), - master_ssl_capath=dict(type='str'), - master_ssl_cert=dict(type='str'), - master_ssl_key=dict(type='str', no_log=False), - master_ssl_cipher=dict(type='str'), - master_use_gtid=dict(type='str', choices=[ - 'current_pos', 'replica_pos', 'slave_pos', 'disabled']), - master_delay=dict(type='int'), + primary_ssl=dict(type='bool', default=False, 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_use_gtid=dict(type='str', choices=[ + 'current_pos', 'replica_pos', 'slave_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), @@ -451,33 +479,33 @@ def main(): ], ) mode = module.params["mode"] - master_host = module.params["master_host"] - master_user = module.params["master_user"] - master_password = module.params["master_password"] - master_port = module.params["master_port"] - master_connect_retry = module.params["master_connect_retry"] - master_log_file = module.params["master_log_file"] - master_log_pos = module.params["master_log_pos"] + 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"] - master_ssl = module.params["master_ssl"] - master_ssl_ca = module.params["master_ssl_ca"] - master_ssl_capath = module.params["master_ssl_capath"] - master_ssl_cert = module.params["master_ssl_cert"] - master_ssl_key = module.params["master_ssl_key"] - master_ssl_cipher = module.params["master_ssl_cipher"] - master_auto_position = module.params["master_auto_position"] + 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_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'] - master_delay = module.params['master_delay'] - if module.params.get("master_use_gtid") == 'disabled': - master_use_gtid = 'no' + primary_delay = module.params['primary_delay'] + if module.params.get("primary_use_gtid") == 'disabled': + primary_use_gtid = 'no' else: - master_use_gtid = module.params["master_use_gtid"] + 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'] @@ -512,18 +540,24 @@ def main(): # "REPLICA" must be used instead of "SLAVE" if impl.uses_replica_terminology(cursor): replica_term = 'REPLICA' - if master_use_gtid == 'slave_pos': - module.deprecate('master_use_gtid "slave_pos" value is deprecated, use "replica_pos" instead.', + if primary_use_gtid == 'slave_pos': + module.deprecate('primary_use_gtid | master_use_gtid "slave_pos" value is ' + 'deprecated, use "replica_pos" instead.', version='3.0.0', collection_name='community.mysql') - master_use_gtid = 'replica_pos' + primary_use_gtid = 'replica_pos' else: replica_term = 'SLAVE' - if master_use_gtid == 'replica_pos': - master_use_gtid = 'slave_pos' + if primary_use_gtid == 'replica_pos': + primary_use_gtid = 'slave_pos' - if mode in "getmaster": - status = get_master_status(cursor) + if mode in ('getprimary', 'getmaster'): + if mode == 'getmaster': + module.deprecate('"getmaster" option is deprecated, use "getprimary" instead.', + version='3.0.0', collection_name='community.mysql') + + status = get_primary_status(cursor) if not isinstance(status, dict): + # TODO: change the word master to primary in 3.0.0 status = dict(Is_Master=False, Is_Primary=False, msg="Server is not configured as mysql master") else: @@ -556,47 +590,50 @@ def main(): module.exit_json(queries=executed_queries, **status) - elif mode in "changemaster": + elif mode in ('changeprimary', 'changemaster'): + if mode == 'changemaster': + module.deprecate('"changemaster" option is deprecated, use "changeprimary" instead.', + version='3.0.0', collection_name='community.mysql') chm = [] result = {} - if master_host is not None: - chm.append("MASTER_HOST='%s'" % master_host) - if master_user is not None: - chm.append("MASTER_USER='%s'" % master_user) - if master_password is not None: - chm.append("MASTER_PASSWORD='%s'" % master_password) - if master_port is not None: - chm.append("MASTER_PORT=%s" % master_port) - if master_connect_retry is not None: - chm.append("MASTER_CONNECT_RETRY=%s" % master_connect_retry) - if master_log_file is not None: - chm.append("MASTER_LOG_FILE='%s'" % master_log_file) - if master_log_pos is not None: - chm.append("MASTER_LOG_POS=%s" % master_log_pos) - if master_delay is not None: - chm.append("MASTER_DELAY=%s" % master_delay) + 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 master_ssl: + if primary_ssl: chm.append("MASTER_SSL=1") - if master_ssl_ca is not None: - chm.append("MASTER_SSL_CA='%s'" % master_ssl_ca) - if master_ssl_capath is not None: - chm.append("MASTER_SSL_CAPATH='%s'" % master_ssl_capath) - if master_ssl_cert is not None: - chm.append("MASTER_SSL_CERT='%s'" % master_ssl_cert) - if master_ssl_key is not None: - chm.append("MASTER_SSL_KEY='%s'" % master_ssl_key) - if master_ssl_cipher is not None: - chm.append("MASTER_SSL_CIPHER='%s'" % master_ssl_cipher) - if master_auto_position: + 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_auto_position: chm.append("MASTER_AUTO_POSITION=1") - if master_use_gtid is not None: - chm.append("MASTER_USE_GTID=%s" % master_use_gtid) + if primary_use_gtid is not None: + chm.append("MASTER_USE_GTID=%s" % primary_use_gtid) try: - changemaster(cursor, chm, connection_name, channel) + changeprimary(cursor, chm, connection_name, channel) except mysql_driver.Warning as e: result['warning'] = to_native(e) except Exception as e: @@ -623,11 +660,17 @@ def main(): 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 in "resetmaster": - reset = reset_master(module, cursor, fail_on_error) + elif mode in ('resetprimary', 'resetmaster'): + if mode == 'resetmaster': + module.deprecate('"resetmaster" option is deprecated, use "resetprimary" instead.', + version='3.0.0', collection_name='community.mysql') + + reset = reset_primary(module, cursor, fail_on_error) if reset is True: + # TODO: Change "Master" to "Primary" in release 3.0.0 module.exit_json(msg="Master reset", changed=True, queries=executed_queries) else: + # TODO: Change "Master" to "Primary" in release 3.0.0 module.exit_json(msg="Master already reset", changed=False, queries=executed_queries) elif mode in ("resetreplica", "resetslave"): if mode == "resetslave": diff --git a/tests/integration/targets/test_mysql_replication/defaults/main.yml b/tests/integration/targets/test_mysql_replication/defaults/main.yml index fefcf29..d2d2080 100644 --- a/tests/integration/targets/test_mysql_replication/defaults/main.yml +++ b/tests/integration/targets/test_mysql_replication/defaults/main.yml @@ -7,7 +7,7 @@ mysql_replica2_port: 3309 test_db: test_db test_table: test_table -test_master_delay: 60 +test_primary_delay: 60 replication_user: replication_user replication_pass: replication_pass dump_path: /tmp/dump.sql diff --git a/tests/integration/targets/test_mysql_replication/tasks/issue-28.yml b/tests/integration/targets/test_mysql_replication/tasks/issue-28.yml index c666820..7943e35 100644 --- a/tests/integration/targets/test_mysql_replication/tasks/issue-28.yml +++ b/tests/integration/targets/test_mysql_replication/tasks/issue-28.yml @@ -36,7 +36,7 @@ - name: attempt connection with newly created user (expect failure) mysql_replication: - mode: getmaster + mode: getprimary login_user: '{{ user_name_1 }}' login_password: '{{ user_password_1 }}' login_host: 127.0.0.1 @@ -57,7 +57,7 @@ - name: attempt connection with newly created user ignoring hostname mysql_replication: - mode: getmaster + mode: getprimary login_user: '{{ user_name_1 }}' login_password: '{{ user_password_1 }}' login_host: 127.0.0.1 diff --git a/tests/integration/targets/test_mysql_replication/tasks/main.yml b/tests/integration/targets/test_mysql_replication/tasks/main.yml index 239598a..d4c7150 100644 --- a/tests/integration/targets/test_mysql_replication/tasks/main.yml +++ b/tests/integration/targets/test_mysql_replication/tasks/main.yml @@ -9,13 +9,13 @@ # Initial CI tests of mysql_replication module: - import_tasks: mysql_replication_initial.yml -# Tests of master_delay parameter: -- import_tasks: mysql_replication_master_delay.yml +# Tests of primary_delay parameter: +- import_tasks: mysql_replication_primary_delay.yml # Tests of channel parameter: - import_tasks: mysql_replication_channel.yml -# Tests of resetmaster mode: -- import_tasks: mysql_replication_resetmaster_mode.yml +# Tests of resetprimary mode: +- import_tasks: mysql_replication_resetprimary_mode.yml - include: issue-28.yml diff --git a/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_channel.yml b/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_channel.yml index c95f792..e314aae 100644 --- a/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_channel.yml +++ b/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_channel.yml @@ -8,26 +8,26 @@ login_host: 127.0.0.1 block: - # Get master log file and log pos: - - name: Get master status + # Get primary log file and log pos: + - name: Get primary status mysql_replication: <<: *mysql_params login_port: '{{ mysql_primary_port }}' - mode: getmaster + mode: getprimary register: mysql_primary_status - # Test changemaster mode: + # Test changeprimary mode: - name: Run replication with channel mysql_replication: <<: *mysql_params login_port: '{{ mysql_replica2_port }}' - mode: changemaster - master_host: '{{ mysql_host }}' - master_port: '{{ mysql_primary_port }}' - master_user: '{{ replication_user }}' - master_password: '{{ replication_pass }}' - master_log_file: '{{ mysql_primary_status.File }}' - master_log_pos: '{{ mysql_primary_status.Position }}' + mode: changeprimary + primary_host: '{{ mysql_host }}' + primary_port: '{{ mysql_primary_port }}' + primary_user: '{{ replication_user }}' + primary_password: '{{ replication_pass }}' + primary_log_file: '{{ mysql_primary_status.File }}' + primary_log_pos: '{{ mysql_primary_status.Position }}' channel: '{{ test_channel }}' register: result diff --git a/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_initial.yml b/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_initial.yml index 27743fb..c80bae4 100644 --- a/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_initial.yml +++ b/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_initial.yml @@ -49,12 +49,12 @@ - name: Restore the dump to replica2 shell: '{{ mysql_command }} -P{{ mysql_replica2_port }} < {{ dump_path }}' - # Test getmaster mode: - - name: Get master status + # Test getprimary mode: + - name: Get primary status mysql_replication: <<: *mysql_params login_port: '{{ mysql_primary_port }}' - mode: getmaster + mode: getprimary register: mysql_primary_status - assert: @@ -63,8 +63,8 @@ - mysql_primary_status.Position != 0 - mysql_primary_status is not changed - # Test startreplica fails without changemaster first. This needs fail_on_error - - name: Start replica (using deprecated startslave choice) and fail because master is not specified; failing on error as requested + # Test startreplica fails without changeprimary first. This needs fail_on_error + - name: Start replica (using deprecated startslave choice) and fail because primary is not specified; failing on error as requested mysql_replication: <<: *mysql_params login_port: '{{ mysql_replica1_port }}' @@ -103,21 +103,21 @@ that: - result is not failed - # Test changemaster mode: - # master_ssl_ca will be set as '' to check the module's behaviour for #23976, + # Test changeprimary mode: + # primary_ssl_ca will be set as '' to check the module's behaviour for #23976, # must be converted to an empty string - name: Run replication mysql_replication: <<: *mysql_params login_port: '{{ mysql_replica1_port }}' - mode: changemaster - master_host: '{{ mysql_host }}' - master_port: '{{ mysql_primary_port }}' - master_user: '{{ replication_user }}' - master_password: '{{ replication_pass }}' - master_log_file: '{{ mysql_primary_status.File }}' - master_log_pos: '{{ mysql_primary_status.Position }}' - master_ssl_ca: '' + mode: changeprimary + primary_host: '{{ mysql_host }}' + primary_port: '{{ mysql_primary_port }}' + primary_user: '{{ replication_user }}' + primary_password: '{{ replication_pass }}' + primary_log_file: '{{ mysql_primary_status.File }}' + primary_log_pos: '{{ mysql_primary_status.Position }}' + primary_ssl_ca: '' register: result - assert: @@ -179,7 +179,7 @@ pause: seconds: 2 - # Test master log pos has been changed: + # Test primary log pos has been changed: - name: Get replica status mysql_replication: <<: *mysql_params diff --git a/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_master_delay.yml b/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_primary_delay.yml similarity index 84% rename from tests/integration/targets/test_mysql_replication/tasks/mysql_replication_master_delay.yml rename to tests/integration/targets/test_mysql_replication/tasks/mysql_replication_primary_delay.yml index 94a10b2..ecdcc81 100644 --- a/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_master_delay.yml +++ b/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_primary_delay.yml @@ -9,13 +9,13 @@ block: - # Test master_delay mode: + # Test primary_delay mode: - name: Run replication mysql_replication: <<: *mysql_params login_port: '{{ mysql_replica1_port }}' - mode: changemaster - master_delay: '{{ test_master_delay }}' + mode: changeprimary + primary_delay: '{{ test_primary_delay }}' register: result - assert: @@ -31,7 +31,7 @@ mode: startreplica register: result - # Check master_delay: + # Check primary_delay: - name: Get standby status mysql_replication: <<: *mysql_params @@ -41,5 +41,5 @@ - assert: that: - - replica_status.SQL_Delay == {{ test_master_delay }} + - replica_status.SQL_Delay == {{ test_primary_delay }} - replica_status is not changed diff --git a/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_resetmaster_mode.yml b/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_resetprimary_mode.yml similarity index 83% rename from tests/integration/targets/test_mysql_replication/tasks/mysql_replication_resetmaster_mode.yml rename to tests/integration/targets/test_mysql_replication/tasks/mysql_replication_resetprimary_mode.yml index 223e325..a4ed75e 100644 --- a/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_resetmaster_mode.yml +++ b/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_resetprimary_mode.yml @@ -22,20 +22,20 @@ login_port: '{{ mysql_replica1_port }}' mode: resetreplicaall - # Get master initial status: - - name: Get master status + # Get primary initial status: + - name: Get primary status mysql_replication: <<: *mysql_params login_port: '{{ mysql_primary_port }}' - mode: getmaster + mode: getprimary register: mysql_primary_initial_status - # Test resetmaster mode: - - name: Reset master + # Test resetprimary mode: + - name: Reset primary mysql_replication: <<: *mysql_params login_port: '{{ mysql_primary_port }}' - mode: resetmaster + mode: resetprimary register: result - assert: @@ -43,12 +43,12 @@ - result is changed - result.queries == ["RESET MASTER"] - # Get master final status: - - name: Get master status + # Get primary final status: + - name: Get primary status mysql_replication: <<: *mysql_params login_port: '{{ mysql_primary_port }}' - mode: getmaster + mode: getprimary register: mysql_primary_final_status - assert: From 34a300d5f0a6ccd2fbec2f4752b8583ebcad7da2 Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Fri, 23 Apr 2021 15:08:36 +0300 Subject: [PATCH 05/43] mysql: revert changes made in PR 116 (#153) (#155) * mysql: revert changes made in PR 116 * Add changelog fragment * Fix CI * Fix CI * Fix CI * Update CI * Fix CI (cherry picked from commit 738343d64c058207f2e14aa4a03ac0cb1713304a) --- .github/workflows/ansible-test-plugins.yml | 2 +- changelogs/fragments/153-mysql_revert_connector_changes.yml | 2 ++ plugins/module_utils/mysql.py | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 changelogs/fragments/153-mysql_revert_connector_changes.yml diff --git a/.github/workflows/ansible-test-plugins.yml b/.github/workflows/ansible-test-plugins.yml index 7e5c33d..da79c04 100644 --- a/.github/workflows/ansible-test-plugins.yml +++ b/.github/workflows/ansible-test-plugins.yml @@ -61,7 +61,7 @@ jobs: - stable-2.9 - stable-2.10 - stable-2.11 - - devel + #- devel python: - 3.6 connector: diff --git a/changelogs/fragments/153-mysql_revert_connector_changes.yml b/changelogs/fragments/153-mysql_revert_connector_changes.yml new file mode 100644 index 0000000..48c8b36 --- /dev/null +++ b/changelogs/fragments/153-mysql_revert_connector_changes.yml @@ -0,0 +1,2 @@ +bugfixes: +- mysql - revert changes of connector arguments made in pull request 116 causing the invalid keyword argument error (https://github.com/ansible-collections/community.mysql/pull/116). diff --git a/plugins/module_utils/mysql.py b/plugins/module_utils/mysql.py index 67e0033..5af9c20 100644 --- a/plugins/module_utils/mysql.py +++ b/plugins/module_utils/mysql.py @@ -79,7 +79,7 @@ def mysql_connect(module, login_user=None, login_password=None, config_file='', if login_user is not None: config['user'] = login_user if login_password is not None: - config['password'] = login_password + config['passwd'] = login_password if ssl_cert is not None: config['ssl']['cert'] = ssl_cert if ssl_key is not None: @@ -87,7 +87,7 @@ def mysql_connect(module, login_user=None, login_password=None, config_file='', if ssl_ca is not None: config['ssl']['ca'] = ssl_ca if db is not None: - config['database'] = db + config['db'] = db if connect_timeout is not None: config['connect_timeout'] = connect_timeout if check_hostname is not None: From b32380a9e1fd173adf4c251b2f69cafb9553279b Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Fri, 23 Apr 2021 16:11:57 +0300 Subject: [PATCH 06/43] Release 2.1.0 commit (#157) --- changelogs/CHANGELOG.rst | 28 +++++++++++++++++ changelogs/changelog.yaml | 30 +++++++++++++++++++ ...7-mysql_replication_deprecate_ret_vals.yml | 5 ---- .../150-mysql_replication_master_related.yml | 6 ---- .../153-mysql_revert_connector_changes.yml | 2 -- 5 files changed, 58 insertions(+), 13 deletions(-) delete mode 100644 changelogs/fragments/147-mysql_replication_deprecate_ret_vals.yml delete mode 100644 changelogs/fragments/150-mysql_replication_master_related.yml delete mode 100644 changelogs/fragments/153-mysql_revert_connector_changes.yml diff --git a/changelogs/CHANGELOG.rst b/changelogs/CHANGELOG.rst index c186696..8c6c034 100644 --- a/changelogs/CHANGELOG.rst +++ b/changelogs/CHANGELOG.rst @@ -5,6 +5,34 @@ Community MySQL Collection Release Notes .. contents:: Topics +v2.1.0 +====== + +Release Summary +--------------- + +This is the minor release of the ``community.mysql`` collection. +This changelog contains all changes to the modules in this collection +that have been added after the release of ``community.mysql`` 2.0.0. + +Major Changes +------------- + +- mysql_replication - add deprecation warning that the ``Is_Slave`` and ``Is_Master`` return values will be replaced with ``Is_Primary`` and ``Is_Replica`` in ``community.mysql`` 3.0.0 (https://github.com/ansible-collections/community.mysql/pull/147). +- mysql_replication - the choices of the ``state`` option containing ``master`` will be finally replaced with the alternative ``primary`` choices in ``community.mysql`` 3.0.0, add deprecation warnings (https://github.com/ansible-collections/community.mysql/pull/150). + +Minor Changes +------------- + +- mysql_replication - add alternative (``primary``) choices to the ``state`` option choices containing ``master`` (https://github.com/ansible-collections/community.mysql/pull/150). +- mysql_replication - add the ``Is_Primary`` and ``Is_Replica`` alternatives to the ``Is_Slave`` and ``Is_Master`` return values as a preparation for replacement in ``community.mysql`` 3.0.0 (https://github.com/ansible-collections/community.mysql/pull/147). +- mysql_replication - change ``master_`` options to ``primary_`` options, add aliases to keep compatibility (https://github.com/ansible-collections/community.mysql/pull/150). + +Bugfixes +-------- + +- mysql - revert changes of connector arguments made in pull request 116 causing the invalid keyword argument error (https://github.com/ansible-collections/community.mysql/pull/116). + v2.0.0 ====== diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index fe92a0f..337f532 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -268,3 +268,33 @@ releases: - 144-mysql_replication_remove_slave_from_messages.yml - 2.0.0.yml release_date: '2021-04-15' + 2.1.0: + changes: + bugfixes: + - mysql - revert changes of connector arguments made in pull request 116 causing + the invalid keyword argument error (https://github.com/ansible-collections/community.mysql/pull/116). + major_changes: + - mysql_replication - add deprecation warning that the ``Is_Slave`` and ``Is_Master`` + return values will be replaced with ``Is_Primary`` and ``Is_Replica`` in ``community.mysql`` + 3.0.0 (https://github.com/ansible-collections/community.mysql/pull/147). + - mysql_replication - the choices of the ``state`` option containing ``master`` + will be finally replaced with the alternative ``primary`` choices in ``community.mysql`` + 3.0.0, add deprecation warnings (https://github.com/ansible-collections/community.mysql/pull/150). + minor_changes: + - mysql_replication - add alternative (``primary``) choices to the ``state`` + option choices containing ``master`` (https://github.com/ansible-collections/community.mysql/pull/150). + - mysql_replication - add the ``Is_Primary`` and ``Is_Replica`` alternatives + to the ``Is_Slave`` and ``Is_Master`` return values as a preparation for replacement + in ``community.mysql`` 3.0.0 (https://github.com/ansible-collections/community.mysql/pull/147). + - mysql_replication - change ``master_`` options to ``primary_`` options, add + aliases to keep compatibility (https://github.com/ansible-collections/community.mysql/pull/150). + release_summary: 'This is the minor release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules in this collection + + that have been added after the release of ``community.mysql`` 2.0.0.' + fragments: + - 147-mysql_replication_deprecate_ret_vals.yml + - 150-mysql_replication_master_related.yml + - 153-mysql_revert_connector_changes.yml + release_date: '2021-04-23' diff --git a/changelogs/fragments/147-mysql_replication_deprecate_ret_vals.yml b/changelogs/fragments/147-mysql_replication_deprecate_ret_vals.yml deleted file mode 100644 index 9261453..0000000 --- a/changelogs/fragments/147-mysql_replication_deprecate_ret_vals.yml +++ /dev/null @@ -1,5 +0,0 @@ -major_changes: -- mysql_replication - add deprecation warning that the ``Is_Slave`` and ``Is_Master`` return values will be replaced with ``Is_Primary`` and ``Is_Replica`` in ``community.mysql`` 3.0.0 (https://github.com/ansible-collections/community.mysql/pull/147). - -minor_changes: -- mysql_replication - add the ``Is_Primary`` and ``Is_Replica`` alternatives to the ``Is_Slave`` and ``Is_Master`` return values as a preparation for replacement in ``community.mysql`` 3.0.0 (https://github.com/ansible-collections/community.mysql/pull/147). diff --git a/changelogs/fragments/150-mysql_replication_master_related.yml b/changelogs/fragments/150-mysql_replication_master_related.yml deleted file mode 100644 index 83b3538..0000000 --- a/changelogs/fragments/150-mysql_replication_master_related.yml +++ /dev/null @@ -1,6 +0,0 @@ -minor_changes: -- mysql_replication - change ``master_`` options to ``primary_`` options, add aliases to keep compatibility (https://github.com/ansible-collections/community.mysql/pull/150). -- mysql_replication - add alternative (``primary``) choices to the ``state`` option choices containing ``master`` (https://github.com/ansible-collections/community.mysql/pull/150). - -major_changes: -- mysql_replication - the choices of the ``state`` option containing ``master`` will be finally replaced with the alternative ``primary`` choices in ``community.mysql`` 3.0.0, add deprecation warnings (https://github.com/ansible-collections/community.mysql/pull/150). diff --git a/changelogs/fragments/153-mysql_revert_connector_changes.yml b/changelogs/fragments/153-mysql_revert_connector_changes.yml deleted file mode 100644 index 48c8b36..0000000 --- a/changelogs/fragments/153-mysql_revert_connector_changes.yml +++ /dev/null @@ -1,2 +0,0 @@ -bugfixes: -- mysql - revert changes of connector arguments made in pull request 116 causing the invalid keyword argument error (https://github.com/ansible-collections/community.mysql/pull/116). From a68c83f7b5cda7ec9913ab1220d22423c062d0b5 Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Fri, 23 Apr 2021 17:07:29 +0300 Subject: [PATCH 07/43] Update next expected release version in galaxy.yml (#158) --- galaxy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/galaxy.yml b/galaxy.yml index 5d44fe8..bc6ce0e 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,6 +1,6 @@ namespace: community name: mysql -version: 2.1.0 +version: 2.2.0 readme: README.md authors: - Ansible community From 2e748efb02b6efd6c83a0b948162696058ea59b1 Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Wed, 9 Jun 2021 11:38:17 +0300 Subject: [PATCH 08/43] [stable-2] Backport of all the doc PRs merged to main since the last backporting (#182) * Add CONTRIBUTORS file (#166) (cherry picked from commit ac927fdb085c1068d12bf4567ae6163684344fdd) * Add documentation for privs with functions and procedures (#169) (cherry picked from commit 6bce48e77120614a87c96eb78a91e401d99f4a04) * Update README.md (#168) * Update README.md * Fix * Add MAINTAINERS file (cherry picked from commit 479edd81d1daf40b1ffbf4131fa77e05f35ae86c) * Improve wording in README (#170) * Improve wording in README * Update README.md Co-authored-by: Jorge Rodriguez (A.K.A. Tiriel) Co-authored-by: Jorge Rodriguez (A.K.A. Tiriel) (cherry picked from commit c909aa2182421e3f2ff25cf97029470d4b3119f6) * Update REVIEW_CHECKLIST.md (#171) (cherry picked from commit 2236110bae9bf022b13477ec486b06d29fe4bd6c) * README: add a note how people can complain (#172) * README: add a note how people can complain * Change * Improve * Update README.md Co-authored-by: Jorge Rodriguez (A.K.A. Tiriel) Co-authored-by: Jorge Rodriguez (A.K.A. Tiriel) (cherry picked from commit be4e84a92a39f999c65977a918f8dfd636e0a956) * README: fix typos (#174) (cherry picked from commit 2a80c301a64bd9e13d8e43b84f1945dac6039ae2) * README.md: Add link to IRC (#175) (cherry picked from commit 3335a95ba5f6aab6876bc83f7168ea2383933938) * README.md, CONTRIBUTING.md: add links to the Maintainer guidelines (#179) (cherry picked from commit 8fad3f85b8e9a2d62ede48535d5aeea6bfdf4d52) * Update README (#181) (cherry picked from commit 6d9288d19bc21ca11e9c19fd3e8cc91d947c5cd4) Co-authored-by: Alexander Skiba --- CONTRIBUTING.md | 2 + CONTRIBUTORS | 273 ++++++++++++++++++++++++++++++++++ MAINTAINERS | 3 + README.md | 33 +++- REVIEW_CHECKLIST.md | 37 +---- plugins/modules/mysql_user.py | 10 ++ 6 files changed, 317 insertions(+), 41 deletions(-) create mode 100644 CONTRIBUTORS create mode 100644 MAINTAINERS diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 88207e9..70a18b3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,8 @@ We follow [Ansible Code of Conduct](https://docs.ansible.com/ansible/latest/comm If you are a committer, also refer to the [Ansible committer guidelines](https://docs.ansible.com/ansible/devel/community/committer_guidelines.html). +If you are a maintainer / interested in becoming a maintainer of this collection, refer to the [Maintainer guidelines](https://github.com/ansible/community-docs/blob/main/maintaining.rst). + ## Issue tracker Whether you are looking for an opportunity to contribute or you found a bug and already know how to solve it, please go to the [issue tracker](https://github.com/ansible-collections/community.mysql/issues). diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 0000000..7678773 --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1,273 @@ +116davinder +20 +28 +29 +4 +4n70w4 +abadger +abondis +acozine +adamchainz +adq +Akasurde +Alexander198961 +alustenberg +aminvakil +amitk79 +amree +Andersson007 +andrewhowdencom +ansibot +anthonyxpalermo +antonioribeiro +apollo13 +aquach +arcmop +asad-at-srt +AshDevilRed +aurimasl +axelll +axisK +azielke +baldpale +banyek +BarbzYHOOL +Berbe +bjne +bmalynovytch +bmildren +boreal321 +brutus +burner1024 +calfonso +candeira +caphrim007 +cdalbergue +checkphi +chrismeyersfsu +ChristopherGAndrews +cmodijk +codeaken +codebymikey +coreylane +CormacBracken +cosmix +cptMikky +crashes +dagwieers +damianmoore +Davidffry +denisemauldin +diclophis +d-lee +dmp1ce +dnelson +dramaley +drybjed +drzraf +DSpeichert +dungdm93 +dwagelaar +dylanjbarth +einarc +E-M +eowin +Ernest0x +esamattis +Everspace +F21 +faitno +felixfontein +flatrocks +fourjay +fraff +g00fy- +geerlingguy +georgeOsdDev +ghjm +ghost +giacmir +giorgio-v +gkoller +gottwald +gstorme +gundalow +hansbaer +hchargois +hluaces +hwali +hyperfocus1338 +igormukhingmailcom +imjoseangel +infigoKriti +ipergenitsa +iredmail +ivandigiusto +jadbaz +jaikdean +jamescassell +janosmiko +jarnold-timeout +JaSafieddine +jborean93 +jctanner +jean-christophe-manciot +Jean-Daniel +jgornick +jhagg +jhoekx +jirib +jkleckner +jkordish +jlaska +Jmainguy +jochu +JoelFeiner +johnavp1989 +jonatasbaldin +Jorge-Rodriguez +jpjaatin +jpmens +JSafieddine +jsmartin +juergenhoetzel +jw34 +kalaisubbiah +kenichi-ogawa-1988 +kkeane +klingac +kotso +kuntalFreshBooks +kurtdavis +larsks +ldesgrange +leeadh +LeonB +leucos +loomsen +lorin +lowwalker +lperezs +makmanalp +manuelmorena +MarcinOrlowski +markdorison +markotitel +marktheunissen +markuman +mattclay +matt-horwood-mayden +mavimo +maxamillion +maxbube +mcgoldrickm +meanstrong +meersjo +megamisan +michaeldg +michalmedvecky +MikeiLL +milky-milk +milosz +mistaka0s +mklassen +mkrizek +mmoya +mohag +mohsenSy +mpdehaan +MRwangyd +mverwijs +mvgrimes +mysqlbox +netmonk +nhojpatrick +nicolas-g +NielsH +nitinkansal1984 +nitzmahone +Ompragash +on +order +organman91 +p53 +pakal +paulbadcock +pennycoders +petoju +petracvv +pgrenaud +philfry +pileofrogs +pkaramol +platypus-geek +plumbeo +pratikgadiya12 +pshanbhag +r0bj +rajsshah86 +reduzent +relrod +resmo +ricco24 +richlv +riupie +rndmh3ro +robertdebock +robpblake +rokka-n +Roxyrob +roysmith +rthouvenin +ruudk +samccann +samdoran +sayap +scottbrown +seanorama +sedrubal +sergey-trukhin +Shaps +shrikeh +sivel +skalfyfan +skoriy88 +sperantus +spoyd +steverweber +steveteahan +stijnopheide +stintel +stoned +strixBE +SWADESNA +tapologo +tejatsk14 +tersmitten +the +the02 +thomasliddledba +time-palominodb +timorunge +Tomasthanes +tomdymond +Tronde +tvlooy +tyll +UncertaintyP +vamshi8 +vanne +vdboor +vmahadev +v-zhuravlev +webmat +wedi +whysthatso +willthames +windowsansiblernew +wrosario +xiata +Xyon +yangchao0512 +ziegenberg +Zverik diff --git a/MAINTAINERS b/MAINTAINERS new file mode 100644 index 0000000..c520538 --- /dev/null +++ b/MAINTAINERS @@ -0,0 +1,3 @@ +bmalynovytch +Jorge-Rodriguez +Andersson007 (andersson007_ in #ansible-community IRC) diff --git a/README.md b/README.md index b1bd9d2..cf44f31 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,33 @@ # MySQL collection for Ansible [![Plugins CI](https://github.com/ansible-collections/community.mysql/workflows/Plugins%20CI/badge.svg?event=push)](https://github.com/ansible-collections/community.mysql/actions?query=workflow%3A"Plugins+CI") [![Roles CI](https://github.com/ansible-collections/community.mysql/workflows/Roles%20CI/badge.svg?event=push)](https://github.com/ansible-collections/community.mysql/actions?query=workflow%3A"Roles+CI") [![Codecov](https://img.shields.io/codecov/c/github/ansible-collections/community.mysql)](https://codecov.io/gh/ansible-collections/community.mysql) +This collection is a part of Ansible package. + +## Code of Conduct + +We follow the [Ansible Code of Conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html) in all our interactions within this project. + +If you encounter abusive behavior violating the [Ansible Code of Conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html), please refer to the [policy violations](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html#policy-violations) section of the Code of Conduct for information on how to raise a complaint. + +## Contributing + +The content of this collection is made by [people](CONTRIBUTORS) like you. + +Any kind of contribution is very welcome. + +You don't know how to start? Refer to our [contribution guide](CONTRIBUTING.md)! + +The current maintainers are listed in the [MAINTAINERS](MAINTAINERS) file. Don't hesitate to reach them out mentioning in the proposals. To learn how to maintain / become a maintainer of this collection, refer to the [Maintainer guidelines](https://github.com/ansible/community-docs/blob/main/maintaining.rst). + +Join us on IRC in the ``ansible-community`` [irc.libera.chat](https://libera.chat/) channel. +See the [Registration guide](https://libera.chat/guides/registration) to learn how to register. + +## Governance + +The process of decision making in this collection is based on discussing and finding consensus among participants. + +Every voice is important and every idea is valuable. If you have something on your mind, create an issue or dedicated discussion and let's discuss it! + ## Included content - **Modules**: @@ -20,7 +47,7 @@ ## External requirements -The MySQL modules rely on a MySQL connector. The list of supported drivers is below: +The MySQL modules rely on a MySQL connector. The list of supported drivers is below: - [PyMySQL](https://github.com/PyMySQL/PyMySQL) - [MySQLdb](https://github.com/PyMySQL/mysqlclient-python) @@ -46,10 +73,6 @@ collections: See [Ansible Using collections](https://docs.ansible.com/ansible/latest/user_guide/collections_using.html) for more details. -## Contributing - -See the [contribution guide](CONTRIBUTING.md). - ## Licensing diff --git a/REVIEW_CHECKLIST.md b/REVIEW_CHECKLIST.md index 3ea3440..9dccf7e 100644 --- a/REVIEW_CHECKLIST.md +++ b/REVIEW_CHECKLIST.md @@ -1,38 +1,3 @@ # Review Checklist -When reviewing, keep in mind that we follow [Ansible Code of Conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html) in all our contributions and interactions within this repository. - -If you are a committer, also refer to the [Ansible committer guidelines](https://docs.ansible.com/ansible/devel/community/committer_guidelines.html). - -**General tips** -- Try to create a culture of collaboration when reviewing -- Welcome the author and thank them for the pull request -- When suggesting changes, try to use questions, not statements -- When suggesting mandatory changes, do it as politely as possible providing documentation references -- If your suggestion is optional or a matter of personal preferences, please say it explicitly -- When asking for adding tests or for complex code refactoring, say that the author is welcome to ask for clarifications and help if they need -- If somebody suggests a good idea, mention it or put a thumbs up -- After merging, thank the author and reviewers for their time and effort - -**Standards and documentation** -- [ ] if the pull request is not a documentation fix, it must include a [changelog fragment](https://docs.ansible.com/ansible/devel/community/development_process.html#creating-a-changelog-fragment) - please check the format carefully -- [ ] if new files are added with the pull request, they follow the [licensing rules](https://github.com/ansible-collections/overview/blob/main/collection_requirements.rst#licensing) -- [ ] the changes follow the [Ansible documentation standards](https://docs.ansible.com/ansible/devel/dev_guide/developing_modules_documenting.html) and the [style guide](https://docs.ansible.com/ansible/devel/dev_guide/style_guide/index.html#style-guide) -- [ ] the changes follow the [development conventions](https://docs.ansible.com/ansible/devel/dev_guide/developing_modules_best_practices.html) -- [ ] if a new plugin is added, it is one of the [allowed plugin types](https://github.com/ansible-collections/overview/blob/main/collection_requirements.rst#modules-plugins) -- [ ] documentation, examples, and return sections use FQCNs for the `M(..)` [format macros](https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_documenting.html#linking-and-other-format-macros-within-module-documentation) when referring to modules -- [ ] modules and plugins from ansible-core use `ansible.builtin.` as a FQCN prefix when mentioned -- [ ] when a new option, module, plugin, or return value is added, the corresponding documentation or return sections use `version_added:` containing the *collection* version which they will be first released in - * this usually is the next minor release, sometimes the next major release (example: if 2.7.5 is the current release, the next minor release will be 2.8.0, and the next major release will be 3.0.0) -- [ ] FQCNs are used for `extends_documentation_fragment:`, unless the author is referring to doc_fragments from ansible-core - -**Tests (if applicable and technically possible to implement)** -- [ ] the pull request has [integration tests](https://docs.ansible.com/ansible/devel/dev_guide/testing_integration.html) -- [ ] the pull request has [unit tests](https://docs.ansible.com/ansible/devel/dev_guide/testing_units.html) -- [ ] all changes are covered -- [ ] integration tests also cover `check_mode` (if it is supported) -- [ ] integration tests check an actual state of the system, not only what the module reports (for example, if the module changes a file, check that the file was actually changed by using the `ansible.builtin.stat` module) - -**Other** -- [ ] the pull request does not contain merge commits (see GitHub warnings at the bottom of the pull request) - in this case, ask the author to rebase the pull request branch -- [ ] if the pull request contains breaking changes, ask the author and the collection maintainers if it is really needed and there is no way not to introduce them +Refer to the [Collection review checklist](https://github.com/ansible/community-docs/blob/main/review_checklist.rst). diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index b39b11a..38c1a55 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -53,6 +53,7 @@ options: 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). type: raw append_privs: description: @@ -188,6 +189,15 @@ EXAMPLES = r''' '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 + # Note that REQUIRESSL is a special privilege that should only apply to *.* by itself. # Setting this privilege in this manner is deprecated. # Use 'tls_requires' instead. From 5d7451aed6ba68b4dca3af3515f3a5b5dab285bf Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Tue, 10 Aug 2021 15:15:49 +0300 Subject: [PATCH 09/43] [stable-2] Doc PRs backport (#194) * Update README.md (#183) * Update README.md * Change IRC ref (cherry picked from commit 69012a2eb9be7f64f41d790356e4839de33824d9) * README: fix link (#184) (cherry picked from commit 8ab6ea771423936e4d992c90ae9c9200ad883da1) * README: fix the channel name (#185) (cherry picked from commit cd759924fd21f9eb48cb6d390a6a8f695eb54706) * CONTRIBUTING.md: replace the content with a link to Ansible contributing guidelines (#187) (cherry picked from commit 56a214885a9e6445a0ba864ff900245248371ea8) * Update README (#186) * Update README * Fix * Fix * Fix * Fix * Add Libera.Chat link (cherry picked from commit adb201a7950a43a38c8708d2d00853001ee11ae8) * fix typo (#190) "optoin" -> "option" (cherry picked from commit 596ba0cedbd14211c2fc1646633b7aa360546c89) * Update README.md (#191) * Update README.md * Update README.md (cherry picked from commit 6f02cb266a6768e82739ae71a80841142940624f) * Add MAINTAINING.md, update README.md (#192) (cherry picked from commit 0fabb2b77a9255bc3d15d5b37da8849098ba9f63) Co-authored-by: Baptiste Mille-Mathias --- CONTRIBUTING.md | 201 +-------------------------------- MAINTAINING.md | 3 + README.md | 37 +++++- plugins/doc_fragments/mysql.py | 2 +- 4 files changed, 36 insertions(+), 207 deletions(-) create mode 100644 MAINTAINING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 70a18b3..edcfe55 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,202 +1,3 @@ # Contributing -We follow [Ansible Code of Conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html) in all our contributions and interactions within this repository. - -If you are a committer, also refer to the [Ansible committer guidelines](https://docs.ansible.com/ansible/devel/community/committer_guidelines.html). - -If you are a maintainer / interested in becoming a maintainer of this collection, refer to the [Maintainer guidelines](https://github.com/ansible/community-docs/blob/main/maintaining.rst). - -## Issue tracker - -Whether you are looking for an opportunity to contribute or you found a bug and already know how to solve it, please go to the [issue tracker](https://github.com/ansible-collections/community.mysql/issues). -There you can find feature ideas to implement, reports about bugs to solve, or submit an issue to discuss your idea before implementing it which can help choose a right direction at the beginning of your work and potentially save a lot of time and effort. -Also somebody may already have started discussing or working on implementing the same or a similar idea, -so you can cooperate to create a better solution together. - -## Open pull requests - -Look through currently [open pull requests](https://github.com/ansible-collections/community.mysql/pulls). -You can help by reviewing them. Reviews help move pull requests to merge state. Some good pull requests cannot be merged only due to a lack of reviews. And it is always worth saying that good reviews are often more valuable than pull requests themselves. For more information how to provide a good review, refer to the [review checklist](REVIEW_CHECKLIST.md). - -Also, consider taking up a valuable, reviewed, but abandoned pull request which you could politely ask the original authors to complete yourself. - -## Discussions - -For open questions, broad suggestions, and other comments that will not typically fit in the scope of an issue or pull request, [discussions](https://github.com/ansible-collections/community.mysql/discussions) are available. That section provides a place to have a more open and informal conversation about any and all things related to this collection, included but not limited to future development plans, functionality explanations and feature proposals that are not yet fleshed out enough for an issue of their own. - -## Looking for an idea to implement - -First, see the paragraphs above. - -If you came up with a new feature, it is always worth creating an issue -before starting to write code to discuss the idea with the community first. -If you are going to implement the feature yourself, say it in the issue explicitly to avoid working in parallel with somebody else. - -## Step-by-step guide how to get into development quickly - -We assume that you use Linux as a work environment (you can use a virtual machine as well) and have `git` installed. - -1. If possible, make sure that you have installed and started `docker`. While you can also run tests without docker, this makes it a lot easier since you do not have to install the precise requirements, and tests are running properly isolated and in the exact same environments as in CI. You often can also use `podman` with the `docker` executable shim, so if you have that you probably do not need to install `docker`. - -2. Clone the [ansible-core](https://github.com/ansible/ansible) repository: -```bash -git clone https://github.com/ansible/ansible.git -``` - -Instead of installing ansible-core from source, you can also work with an already existing installation of Ansible, ansible-base or ansible-core. Simply skip steps 2 and 3 in that case. - -3. Go to the cloned repository and prepare the environment: -```bash -cd ansible && source hacking/env-setup -pip install -r requirements.txt -cd ~ -``` -4. Create the following directories in your home directory: -```bash -mkdir -p ~/ansible_collections/community/mysql -``` - -5. Fork the `community.mysql` repository through the GitHub web interface. - -6. Clone the forked repository from your profile to the created path: -```bash -git clone https://github.com/YOURACC/community.mysql.git ~/ansible_collections/community/mysql -``` - -If you prefer to use the SSH protocol: -```bash -git clone git@github.com:YOURACC/community.mysql.git ~/ansible_collections/community/mysql -``` - - -7. Go to your new cloned repository. -```bash -cd ~/ansible_collections/community/mysql -``` - -8. Be sure you are in the main branch: -```bash -git status -``` - -9. Show remotes. There should be the `origin` repository only: -```bash -git remote -v -``` - -10. Add the `upstream` repository: -```bash -git remote add upstream https://github.com/ansible-collections/community.mysql.git -``` - -11. Update your local `main` branch: -```bash -git fetch upstream -git rebase upstream/main -``` - -12. Create a branch for your changes: -```bash -git checkout -b name_of_my_branch -``` - -13. We recommend you start with writing integration tests if applicable. - -Note: If there are any difficulties with writing the tests or you are not sure if the case can be covered, feel free to skip this step. -If needed, other contributors can help you with it later. - -All integration tests are stored in `tests/integration/targets` subdirectories. -Go to the subdirectory containing the name of module you are going to change. -For example, if you are fixing the `mysql_user` module, its tests are in `tests/integration/targets/test_mysql_user/tasks`. - -The `main.yml` file holds test tasks and includes other test files. -Look for a suitable test file to integrate your tests or create and include a dedicated test file. -You can use one of the existing test files as a draft. - -When fixing a bug, write a task which reproduces the bug from the issue. - -Put the reported case in the tests, then run integration tests with the following command: -```bash -ansible-test integration test_mysql_user --docker -vvv -``` -If the tests do not want to run, first, check you complete step 3 of this guide. - -If the tests ran successfully, there are usually two possible outcomes: -a) If the bug has not appeared and the tests have passed successfully, ask the reporter to provide more details. The bug can be not a bug actually or can relate to a particular software version used or specifics of local environment configuration. - -b) The bug has appeared and the tests has failed as expected showing the reported symptoms. - -14. Fix the bug. - -15. Run `flake8` against a changed file. If it is `plugins/modules/mysql_user.py`: -```bash -flake8 plugins/modules/mysql_user.py -``` -It is worth installing and running `flake8` against the changed file(s) first. -It shows unused imports, which is not shown by sanity tests (see the next step), as well as other common issues. -Optionally, you can use the `--max-line-length=160` command-line argument. - -16. Run sanity tests: -```bash -ansible-test sanity plugins/modules/mysql_user.py --docker -``` -If they failed, look at the output carefully - it is usually very informative and helps to identify a problem line quickly. -Sanity failings usually relate to wrong code and documentation formatting. - -17. Run integration tests: -```bash -ansible-test integration test_mysql_user --docker -vvv -``` - -There are two possible outcomes: -a) They have failed. Look at the output of the command. -Fix the problem place in the code and run again. -Repeat the cycle until the tests pass. - -b) They have passed. Remember they failed originally? Our congratulations! You have fixed the bug. - -18. Commit your changes with an informative but short commit message: -```bash -git add /path/to/changed/file -git commit -m "mysql_user: fix crash when ..." -``` - -19. Push the branch to the `origin` (your fork): -```bash -git push origin name_of_my_branch -``` - -20. Go to the `upstream` (http://github.com/ansible-collections/community.mysql). - -21. Go to `Pull requests` tab and create a pull request. - -GitHub is tracking your fork, so it should see the new branch in it and automatically offer -to create a pull request. Sometimes GitHub does not do it and you should click the `New pull request` button yourself. -Then choose `compare across forks` under the `Compare changes` title. -Choose your repository and the new branch you pushed in the right drop-down list. -Confirm. Fill out the pull request template with all information you want to mention. -Put "Fixes + link to the issue" in the pull request's description. -Put "[WIP] + short description" in the pull request's title. It's often a good idea to mention the name of the module/plugin you are modifying at the beginning of the description. -Click `Create pull request`. - -22. Add a [changelog fragment](https://docs.ansible.com/ansible/devel/community/development_process.html#changelogs) to the `changelog/fragments` directory. It will be published in release notes, so users will know about the fix. - -Commit and push it: -```bash -git add changelog/fragments/myfragment.yml -git commit -m "Add changelog fragment" -git push origin name_of_my_branch -``` - -23. The CI tests will run automatically on Red Hat infrastructure after every commit. - -You will see the CI status in the bottom of your pull request. -If they are green, remove "[WIP]" from the title. Mention the issue reporter in a comment and let contributors know that the pull request is "Ready for review". - -24. Wait for reviews. You can also ask for review on IRC in the #ansible-community channel. - -25. If the pull request looks good to the community, committers will merge it. - -For details, refer to the [Ansible developer guide](https://docs.ansible.com/ansible/latest/dev_guide/index.html). - -If you find any inconsistencies or places in this document which can be improved, feel free to raise an issue or pull request to fix it. +Refer to the [Ansible Contributing guidelines](https://github.com/ansible/community-docs/blob/main/contributing.rst) to learn how to contribute to this collection. diff --git a/MAINTAINING.md b/MAINTAINING.md new file mode 100644 index 0000000..9fad0d3 --- /dev/null +++ b/MAINTAINING.md @@ -0,0 +1,3 @@ +# Maintaining this collection + +Refer to the [Maintainer guidelines](https://github.com/ansible/community-docs/blob/main/maintaining.rst). diff --git a/README.md b/README.md index cf44f31..8d1349e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # MySQL collection for Ansible [![Plugins CI](https://github.com/ansible-collections/community.mysql/workflows/Plugins%20CI/badge.svg?event=push)](https://github.com/ansible-collections/community.mysql/actions?query=workflow%3A"Plugins+CI") [![Roles CI](https://github.com/ansible-collections/community.mysql/workflows/Roles%20CI/badge.svg?event=push)](https://github.com/ansible-collections/community.mysql/actions?query=workflow%3A"Roles+CI") [![Codecov](https://img.shields.io/codecov/c/github/ansible-collections/community.mysql)](https://codecov.io/gh/ansible-collections/community.mysql) -This collection is a part of Ansible package. +This collection is a part of the Ansible package. ## Code of Conduct @@ -11,16 +11,29 @@ If you encounter abusive behavior violating the [Ansible Code of Conduct](https: ## Contributing -The content of this collection is made by [people](CONTRIBUTORS) like you. +The content of this collection is made by [people](https://github.com/ansible-collections/community.mysql/blob/main/CONTRIBUTORS) just like you, a community of individuals collaborating on making the world better through developing automation software. + +We are actively accepting new contributors. Any kind of contribution is very welcome. -You don't know how to start? Refer to our [contribution guide](CONTRIBUTING.md)! +You don't know how to start? Refer to our [contribution guide](https://github.com/ansible-collections/community.mysql/blob/main/CONTRIBUTING.md)! -The current maintainers are listed in the [MAINTAINERS](MAINTAINERS) file. Don't hesitate to reach them out mentioning in the proposals. To learn how to maintain / become a maintainer of this collection, refer to the [Maintainer guidelines](https://github.com/ansible/community-docs/blob/main/maintaining.rst). +## Collection maintenance -Join us on IRC in the ``ansible-community`` [irc.libera.chat](https://libera.chat/) channel. -See the [Registration guide](https://libera.chat/guides/registration) to learn how to register. +The current maintainers (contributors with `write` or higher access) are listed in the [MAINTAINERS](https://github.com/ansible-collections/community.mysql/blob/main/MAINTAINERS) file. If you have questions or need help, feel free to mention them in the proposals. + +To learn how to maintain / become a maintainer of this collection, refer to the [Maintainer guidelines](https://github.com/ansible-collections/community.mysql/blob/main/MAINTAINING.md). + +## Communication + +We announce releases and important changes through Ansible's [The Bullhorn newsletter](https://eepurl.com/gZmiEP). Be sure you are subscribed. + +Join us in the `#ansible` (general use questions and support), `#ansible-community` (community and collection development questions), and other [IRC channels](https://docs.ansible.com/ansible/devel/community/communication.html#irc-channels) on [Libera.Chat](https://libera.chat). + +We take part in the global quarterly [Ansible Contributor Summit](https://github.com/ansible/community/wiki/Contributor-Summit) virtually or in-person. Track [The Bullhorn newsletter](https://eepurl.com/gZmiEP) and join us. + +For more information about communication, refer to the [Ansible Communication guide](https://docs.ansible.com/ansible/devel/community/communication.html). ## Governance @@ -71,6 +84,18 @@ collections: - name: community.mysql ``` +Note that if you install the collection from Ansible Galaxy, it will not be upgraded automatically if you upgrade the Ansible package. To upgrade the collection to the latest available version, run the following command: + +```bash +ansible-galaxy collection install community.mysql --upgrade +``` + +You can also install a specific version of the collection, for example, if you need to downgrade when something is broken in the latest version (please report an issue in this repository). Use the following syntax: + +```bash +ansible-galaxy collection install community.mysql:==2.0.0 +``` + See [Ansible Using collections](https://docs.ansible.com/ansible/latest/user_guide/collections_using.html) for more details. ## Licensing diff --git a/plugins/doc_fragments/mysql.py b/plugins/doc_fragments/mysql.py index 6d7a546..b7a9152 100644 --- a/plugins/doc_fragments/mysql.py +++ b/plugins/doc_fragments/mysql.py @@ -67,7 +67,7 @@ options: - 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 optoin has no effect on MySQLdb. + - This option has no effect on MySQLdb. type: bool version_added: '1.1.0' requirements: From 0c261b76d691eb71fd0d94bbad24ba3ba6e7fa6e Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Tue, 10 Aug 2021 15:25:29 +0300 Subject: [PATCH 10/43] mysql_query: correctly reflect changed status in replace statements (#193) (#195) * mysql_query: correctly reflect changed status in replace statements. * Fix the wrong indent. (cherry picked from commit 9055bb4c8c95aede61e7ebe4aa21d7c6217b4a80) Co-authored-by: Tong He <68936428+unnecessary-username@users.noreply.github.com> --- CONTRIBUTORS | 1 + ...t_changed_status_in_replace_statements.yml | 2 ++ plugins/modules/mysql_query.py | 2 +- .../test_mysql_query/defaults/main.yml | 1 + .../tasks/mysql_query_initial.yml | 32 +++++++++++++++++++ 5 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 changelogs/fragments/193-reflect_changed_status_in_replace_statements.yml diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 7678773..718caa2 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -255,6 +255,7 @@ Tronde tvlooy tyll UncertaintyP +unnecessary-username vamshi8 vanne vdboor diff --git a/changelogs/fragments/193-reflect_changed_status_in_replace_statements.yml b/changelogs/fragments/193-reflect_changed_status_in_replace_statements.yml new file mode 100644 index 0000000..8ce0461 --- /dev/null +++ b/changelogs/fragments/193-reflect_changed_status_in_replace_statements.yml @@ -0,0 +1,2 @@ +minor_changes: +- mysql_query - correctly reflect changed status in replace statements (https://github.com/ansible-collections/community.mysql/pull/193). \ No newline at end of file diff --git a/plugins/modules/mysql_query.py b/plugins/modules/mysql_query.py index ed3ace4..fc789c5 100644 --- a/plugins/modules/mysql_query.py +++ b/plugins/modules/mysql_query.py @@ -112,7 +112,7 @@ from ansible_collections.community.mysql.plugins.module_utils.mysql import ( ) from ansible.module_utils._text import to_native -DML_QUERY_KEYWORDS = ('INSERT', 'UPDATE', 'DELETE') +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') diff --git a/tests/integration/targets/test_mysql_query/defaults/main.yml b/tests/integration/targets/test_mysql_query/defaults/main.yml index 51a3bd7..4ee25ff 100644 --- a/tests/integration/targets/test_mysql_query/defaults/main.yml +++ b/tests/integration/targets/test_mysql_query/defaults/main.yml @@ -7,6 +7,7 @@ test_db: testdb test_table1: test1 test_table2: test2 test_table3: test3 +test_table4: test4 test_script_path: /tmp/test.sql user_name_1: 'db_user1' diff --git a/tests/integration/targets/test_mysql_query/tasks/mysql_query_initial.yml b/tests/integration/targets/test_mysql_query/tasks/mysql_query_initial.yml index b01de55..30182fe 100644 --- a/tests/integration/targets/test_mysql_query/tasks/mysql_query_initial.yml +++ b/tests/integration/targets/test_mysql_query/tasks/mysql_query_initial.yml @@ -289,6 +289,38 @@ - result is failed - result.msg is search('the elements in query list must be strings') + - name: Create {{ test_table4 }} + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: 'CREATE TABLE {{ test_table4 }} (id int primary key, story text)' + + - name: Insert test data using replace statement + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: "REPLACE INTO {{ test_table4 }} VALUES (1, 'first')" + single_transaction: yes + register: result + + - assert: + that: + - result is changed + - result.rowcount == [1] + + - name: Replace test data + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: "REPLACE INTO {{ test_table4 }} VALUES (1, 'one')" + single_transaction: yes + register: result + + - assert: + that: + - result is changed + - result.rowcount == [2] + - name: Drop db {{ test_db }} mysql_query: <<: *mysql_params From fa62fd30d801bcf58713e2c78c516071694e60b3 Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Wed, 11 Aug 2021 09:47:16 +0300 Subject: [PATCH 11/43] Release 2.1.1 commit (#199) --- changelogs/CHANGELOG.rst | 15 +++++++++++++++ changelogs/changelog.yaml | 13 +++++++++++++ ...flect_changed_status_in_replace_statements.yml | 2 -- galaxy.yml | 2 +- 4 files changed, 29 insertions(+), 3 deletions(-) delete mode 100644 changelogs/fragments/193-reflect_changed_status_in_replace_statements.yml diff --git a/changelogs/CHANGELOG.rst b/changelogs/CHANGELOG.rst index 8c6c034..e2ed3e6 100644 --- a/changelogs/CHANGELOG.rst +++ b/changelogs/CHANGELOG.rst @@ -5,6 +5,21 @@ Community MySQL Collection Release Notes .. contents:: Topics +v2.1.1 +====== + +Release Summary +--------------- + +This is the patch release of the ``community.mysql`` collection. +This changelog contains all changes to the modules in this collection that +have been added after the release of ``community.mysql`` 2.1.0. + +Minor Changes +------------- + +- mysql_query - correctly reflect changed status in replace statements (https://github.com/ansible-collections/community.mysql/pull/193). + v2.1.0 ====== diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 337f532..497e740 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -298,3 +298,16 @@ releases: - 150-mysql_replication_master_related.yml - 153-mysql_revert_connector_changes.yml release_date: '2021-04-23' + 2.1.1: + changes: + minor_changes: + - mysql_query - correctly reflect changed status in replace statements (https://github.com/ansible-collections/community.mysql/pull/193). + release_summary: 'This is the patch release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules in this collection that + + have been added after the release of ``community.mysql`` 2.1.0.' + fragments: + - 193-reflect_changed_status_in_replace_statements.yml + - 2.1.1.yml + release_date: '2021-08-11' diff --git a/changelogs/fragments/193-reflect_changed_status_in_replace_statements.yml b/changelogs/fragments/193-reflect_changed_status_in_replace_statements.yml deleted file mode 100644 index 8ce0461..0000000 --- a/changelogs/fragments/193-reflect_changed_status_in_replace_statements.yml +++ /dev/null @@ -1,2 +0,0 @@ -minor_changes: -- mysql_query - correctly reflect changed status in replace statements (https://github.com/ansible-collections/community.mysql/pull/193). \ No newline at end of file diff --git a/galaxy.yml b/galaxy.yml index bc6ce0e..3e6d8ff 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,6 +1,6 @@ namespace: community name: mysql -version: 2.2.0 +version: 2.1.1 readme: README.md authors: - Ansible community From a8e2c5290b878e4fa4e1d99c425b8a1e7e0b946a Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Wed, 11 Aug 2021 10:46:50 +0300 Subject: [PATCH 12/43] mysql_role: new module (#189) (#200) * mysql_role: new module * fixes * fixes * Add the role class * Check if role exists * role.add() * role.__get_members * tmp * tmp * Change tests * Fix * Fix * add_members() * get_privs() * tmp * __extract_grants() filler version * Before big work * tmp * drop() * tmp * tmp * Big changes * Fix * append_members, detach_members, append_privs * tmp * admin option * Add tests * Add tests * Fix tests * Remove debug warning * Fix tests * Add documentation * Fix MariaDB case * Fix MariaDB * Fix MariaDB * Fix MariaDB * Fix MariaDB * Fix MariaDB * Fix * Fix * Remove debug warning * Add try-except block * tmp * tmp * tmp * Fix * Add err handling * Add user check * Check admin in db * Fix CI * Fix CI * Fix CI * Fix CI * Fix * Add mutually exclusive options * Small refactoring, documenting * Documenting, refactoring * Change docs * Refactoring * Refactoring * Refactoring * Add unit tests * Update README.md (cherry picked from commit ce2b269f8427dfd62eecad71469e45479acb94ef) --- README.md | 1 + .../implementations/mariadb/role.py | 15 + .../implementations/mysql/role.py | 15 + plugins/module_utils/user.py | 866 +++++++++++++ plugins/modules/mysql_role.py | 1065 +++++++++++++++ plugins/modules/mysql_user.py | 839 +----------- .../targets/test_mysql_role/defaults/main.yml | 16 + .../targets/test_mysql_role/meta/main.yml | 2 + .../targets/test_mysql_role/tasks/main.yml | 7 + .../tasks/mysql_role_initial.yml | 1151 +++++++++++++++++ .../test_mysql_user.py | 2 +- tests/unit/plugins/modules/test_mysql_role.py | 119 ++ 12 files changed, 3273 insertions(+), 825 deletions(-) create mode 100644 plugins/module_utils/implementations/mariadb/role.py create mode 100644 plugins/module_utils/implementations/mysql/role.py create mode 100644 plugins/module_utils/user.py create mode 100644 plugins/modules/mysql_role.py create mode 100644 tests/integration/targets/test_mysql_role/defaults/main.yml create mode 100644 tests/integration/targets/test_mysql_role/meta/main.yml create mode 100644 tests/integration/targets/test_mysql_role/tasks/main.yml create mode 100644 tests/integration/targets/test_mysql_role/tasks/mysql_role_initial.yml rename tests/unit/plugins/{modules => module_utils}/test_mysql_user.py (98%) create mode 100644 tests/unit/plugins/modules/test_mysql_role.py diff --git a/README.md b/README.md index 8d1349e..f3768f0 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ Every voice is important and every idea is valuable. If you have something on yo - [mysql_info](https://docs.ansible.com/ansible/latest/collections/community/mysql/mysql_info_module.html) - [mysql_query](https://docs.ansible.com/ansible/latest/collections/community/mysql/mysql_query_module.html) - [mysql_replication](https://docs.ansible.com/ansible/latest/collections/community/mysql/mysql_replication_module.html) + - [mysql_role](https://docs.ansible.com/ansible/latest/collections/community/mysql/mysql_role_module.html) - [mysql_user](https://docs.ansible.com/ansible/latest/collections/community/mysql/mysql_user_module.html) - [mysql_variables](https://docs.ansible.com/ansible/latest/collections/community/mysql/mysql_variables_module.html) diff --git a/plugins/module_utils/implementations/mariadb/role.py b/plugins/module_utils/implementations/mariadb/role.py new file mode 100644 index 0000000..3b4c777 --- /dev/null +++ b/plugins/module_utils/implementations/mariadb/role.py @@ -0,0 +1,15 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from distutils.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 diff --git a/plugins/module_utils/implementations/mysql/role.py b/plugins/module_utils/implementations/mysql/role.py new file mode 100644 index 0000000..42276ff --- /dev/null +++ b/plugins/module_utils/implementations/mysql/role.py @@ -0,0 +1,15 @@ +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from distutils.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 diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py new file mode 100644 index 0000000..c59f72a --- /dev/null +++ b/plugins/module_utils/user.py @@ -0,0 +1,866 @@ +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 licenses/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, +) + + +VALID_PRIVS = frozenset(('CREATE', 'DROP', 'GRANT', 'GRANT OPTION', + 'LOCK TABLES', 'REFERENCES', 'EVENT', 'ALTER', + 'DELETE', 'INDEX', 'INSERT', 'SELECT', 'UPDATE', + 'CREATE TEMPORARY TABLES', 'TRIGGER', 'CREATE VIEW', + 'SHOW VIEW', 'ALTER ROUTINE', 'CREATE ROUTINE', + 'EXECUTE', 'FILE', 'CREATE TABLESPACE', 'CREATE USER', + 'PROCESS', 'PROXY', 'RELOAD', 'REPLICATION CLIENT', + 'REPLICATION SLAVE', 'SHOW DATABASES', 'SHUTDOWN', + 'SUPER', 'ALL', 'ALL PRIVILEGES', 'USAGE', + 'REQUIRESSL', # Deprecated, to be removed in version 3.0.0 + 'CREATE ROLE', 'DROP ROLE', 'APPLICATION_PASSWORD_ADMIN', + 'AUDIT_ADMIN', 'BACKUP_ADMIN', 'BINLOG_ADMIN', + 'BINLOG_ENCRYPTION_ADMIN', 'CLONE_ADMIN', 'CONNECTION_ADMIN', + 'ENCRYPTION_KEY_ADMIN', 'FIREWALL_ADMIN', 'FIREWALL_USER', + 'GROUP_REPLICATION_ADMIN', 'INNODB_REDO_LOG_ARCHIVE', + 'NDB_STORED_USER', 'PERSIST_RO_VARIABLES_ADMIN', + 'REPLICATION_APPLIER', 'REPLICATION_SLAVE_ADMIN', + 'RESOURCE_GROUP_ADMIN', 'RESOURCE_GROUP_USER', + 'ROLE_ADMIN', 'SESSION_VARIABLES_ADMIN', 'SET_USER_ID', + 'SYSTEM_USER', 'SYSTEM_VARIABLES_ADMIN', 'SYSTEM_USER', + 'TABLE_ENCRYPTION_ADMIN', 'VERSION_TOKEN_ADMIN', + 'XA_RECOVER_ADMIN', 'LOAD FROM S3', 'SELECT INTO S3', + 'INVOKE LAMBDA', + 'ALTER ROUTINE', + 'BINLOG ADMIN', + 'BINLOG MONITOR', + 'BINLOG REPLAY', + 'CONNECTION ADMIN', + 'READ_ONLY ADMIN', + 'REPLICATION MASTER ADMIN', + 'REPLICATION SLAVE ADMIN', + 'SET USER', + 'SHOW_ROUTINE', + 'SLAVE MONITOR', + 'REPLICA MONITOR',)) + + +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 user_add(cursor, user, host, host_all, password, encrypted, + plugin, plugin_hash_string, plugin_auth_string, new_priv, + tls_requires, check_mode): + # we cannot create users without a proper hostname + if host_all: + return False + + if check_mode: + return True + + # 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 + + 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 True + + +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, 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] + + 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: + msg = "Password updated" + if module.check_mode: + return (True, msg) + 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) + 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. + 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 and not append_privs: + msg = "Privileges updated" + if module.check_mode: + return (True, msg) + 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. + for db_table, priv in iteritems(new_priv): + if db_table not in curr_priv: + msg = "New privileges granted" + if module.check_mode: + return (True, msg) + 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: + + # If appending privileges, only the set difference between new privileges and current privileges matter. + # The symmetric difference isn't relevant for append because existing privileges will not be revoked. + if append_privs: + priv_diff = set(new_priv[db_table]) - set(curr_priv[db_table]) + else: + priv_diff = set(new_priv[db_table]) ^ set(curr_priv[db_table]) + + if len(priv_diff) > 0: + msg = "Privileges updated" + if module.check_mode: + return (True, msg) + if not append_privs: + privileges_revoke(cursor, user, host, db_table, curr_priv[db_table], grant_option, maria_role) + privileges_grant(cursor, user, host, db_table, new_priv[db_table], tls_requires, maria_role) + changed = True + + 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 (True, msg) + 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, msg) + + +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: + 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: + 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): + """ 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]]) + + new_privs = frozenset(privs) + if not new_privs.issubset(VALID_PRIVS): + raise InvalidPrivsError('Invalid privileges specified: %s' % new_privs.difference(VALID_PRIVS)) + + if '*.*' 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) + + +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) + cursor.execute(query, params) + + +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) + + +def handle_requiressl_in_priv_string(module, priv, tls_requires): + module.deprecate('The "REQUIRESSL" privilege is deprecated, use the "tls_requires" option instead.', + version='3.0.0', collection_name='community.mysql') + priv_groups = re.search(r"(.*?)(\*\.\*:)([^/]*)(.*)", priv) + if priv_groups.group(3) == "REQUIRESSL": + priv = priv_groups.group(1) + priv_groups.group(4) or None + else: + inner_priv_groups = re.search(r"(.*?),?REQUIRESSL,?(.*)", priv_groups.group(3)) + priv = '{0}{1}{2}{3}'.format( + priv_groups.group(1), + priv_groups.group(2), + ','.join(filter(None, (inner_priv_groups.group(1), inner_priv_groups.group(2)))), + priv_groups.group(4) + ) + if not tls_requires: + tls_requires = "SSL" + else: + module.warn('Ignoring "REQUIRESSL" privilege as "tls_requires" is defined and it takes precedence.') + return priv, tls_requires + + +# 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 mysqluser + impl = mysqluser + else: + from ansible_collections.community.mysql.plugins.module_utils.implementations.mysql import user as mariauser + impl = mariauser diff --git a/plugins/modules/mysql_role.py b/plugins/modules/mysql_role.py new file mode 100644 index 0000000..80d0144 --- /dev/null +++ b/plugins/modules/mysql_role.py @@ -0,0 +1,1065 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Andrew Klychkov +# 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_role + +short_description: Adds, removes, or updates a MySQL role + +description: + - Adds, removes, or updates a MySQL role. + - Roles are supported since MySQL 8.0.0 and MariaDB 10.0.5. + +version_added: '2.2.0' + +options: + name: + description: + - Name of the role to add or remove. + type: str + required: true + + admin: + description: + - Supported by B(MariaDB). + - Name of the admin user of the role (the I(login_user), by default). + type: str + + priv: + description: + - "MySQL privileges string in the format: C(db.table:priv1,priv2)." + - "You can specify multiple privileges by separating each one using + a forward slash: C(db.table:priv/db.table: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 for the M(community.mysql.mysql_user) module). + type: raw + + append_privs: + description: + - Append the privileges defined by the I(priv) option to the existing ones + for this role instead of overwriting them. + type: bool + default: no + + members: + description: + - List of members of the role. + - For users, use the format C(username@hostname). + Always specify the hostname part explicitly. + - For roles, use the format C(rolename). + - Mutually exclusive with I(admin). + type: list + elements: str + + append_members: + description: + - Add members defined by the I(members) option to the existing ones + for this role instead of overwriting them. + - Mutually exclusive with the I(detach_members) and I(admin) option. + type: bool + default: no + + detach_members: + description: + - Detaches members defined by the I(members) option from the role + instead of overwriting all the current members. + - Mutually exclusive with the I(append_members) and I(admin) option. + type: bool + default: no + + set_default_role_all: + description: + - Is not supported by MariaDB and is silently ignored when working with MariaDB. + - If C(yes), runs B(SET DEFAULT ROLE ALL TO) each of the I(members) when changed. + - If you want to avoid this behavior, set this option to C(no) explicitly. + type: bool + default: yes + + state: + description: + - If C(present) and the role does not exist, creates the role. + - If C(present) and the role exists, does nothing or updates its attributes. + - If C(absent), removes the role. + 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 + +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). + +seealso: + - module: community.mysql.mysql_user + - name: MySQL role reference + description: Complete reference of the MySQL role documentation. + link: https://dev.mysql.com/doc/refman/8.0/en/create-role.html + +author: + - Andrew Klychkov (@Andersson007) + +extends_documentation_fragment: + - community.mysql.mysql +''' + +EXAMPLES = r''' +# Example of a .my.cnf file content for setting a root password +# [client] +# user=root +# password=n<_665{vS43y +# +# Example of a privileges dictionary passed through the priv option +# priv: +# 'mydb.*': 'INSERT,UPDATE' +# 'anotherdb.*': 'SELECT' +# 'yetanotherdb.*': 'ALL' +# +# You can also use the string format like in the community.mysql.mysql_user module, for example +# mydb.*:INSERT,UPDATE/anotherdb.*:SELECT/yetanotherdb.*:ALL +# +# For more examples on how to specify privileges, refer to the community.mysql.mysql_user module + +# Create a role developers with all database privileges +# and add alice and bob as members. +# The statement 'SET DEFAULT ROLE ALL' to them will be run. +- name: Create role developers, add members + community.mysql.mysql_role: + name: developers + state: present + priv: '*.*:ALL' + members: + - 'alice@%' + - 'bob@%' + +- name: Same as above but do not run SET DEFAULT ROLE ALL TO each member + community.mysql.mysql_role: + name: developers + state: present + priv: '*.*:ALL' + members: + - 'alice@%' + - 'bob@%' + set_default_role_all: no + +# Assuming that the role developers exists, +# add john to the current members +- name: Add members to an existing role + community.mysql.mysql_role: + name: developers + state: present + append_members: yes + members: + - 'joe@localhost' + +# Create role readers with the SELECT privilege +# on all tables in the fiction database +- name: Create role developers, add members + community.mysql.mysql_role: + name: readers + state: present + priv: 'fiction.*:SELECT' + +# Assuming that the role readers exists, +# add the UPDATE privilege to the role on all tables in the fiction database +- name: Create role developers, add members + community.mysql.mysql_role: + name: readers + state: present + priv: 'fiction.*:UPDATE' + append_privs: yes + +- name: Create role with the 'SELECT' and 'UPDATE' privileges in db1 and db2 + community.mysql.mysql_role: + state: present + name: foo + priv: + 'db1.*': 'SELECT,UPDATE' + 'db2.*': 'SELECT,UPDATE' + +- name: Remove joe from readers + community.mysql.mysql_role: + state: present + name: readers + members: + - 'joe@localhost' + detach_members: yes + +- name: Remove the role readers if exists + community.mysql.mysql_role: + state: absent + name: readers + +- name: Example of using login_unix_socket to connect to the server + community.mysql.mysql_role: + name: readers + state: present + login_unix_socket: /var/run/mysqld/mysqld.sock + +# Pay attention that the admin cannot be changed later +# and will be ignored if a role currently exists. +# To change members, you need to run a separate task using the admin +# of the role as the login_user. +- name: On MariaDB, create the role readers with alice as its admin + community.mysql.mysql_role: + state: present + name: readers + admin: 'alice@%' + +- name: Create the role business, add the role marketing to members + community.mysql.mysql_role: + state: present + name: business + members: + - marketing +''' + +RETURN = '''#''' + +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_collections.community.mysql.plugins.module_utils.user import ( + convert_priv_dict_to_str, + get_impl, + get_mode, + user_mod, + privileges_grant, + privileges_unpack, +) +from ansible.module_utils._text import to_native +from ansible.module_utils.six import iteritems + + +def normalize_users(module, users, is_mariadb=False): + """Normalize passed user names. + + Example of transformation: + ['user0'] => [('user0', '')] / ['user0'] => [('user0', '%')] + ['user0@host0'] => [('user0', 'host0')] + + Args: + module (AnsibleModule): Object of the AnsibleModule class. + users (list): List of user names. + is_mariadb (bool): Flag indicating we are working with MariaDB + + Returns: + list: List of tuples like [('user0', ''), ('user0', 'host0')]. + """ + normalized_users = [] + + for user in users: + try: + tmp = user.split('@') + + if tmp[0] == '': + module.fail_json(msg="Member's name cannot be empty.") + + if len(tmp) == 1: + if not is_mariadb: + normalized_users.append((tmp[0], '%')) + else: + normalized_users.append((tmp[0], '')) + + elif len(tmp) == 2: + normalized_users.append((tmp[0], tmp[1])) + + except Exception as e: + msg = ('Error occured while parsing the name "%s": %s. ' + 'It must be in the format "username" or ' + '"username@hostname" ' % (user, to_native(e))) + module.fail_json(msg=msg) + + return normalized_users + + +class DbServer(): + """Class to fetch information from a database. + + Args: + module (AnsibleModule): Object of the AnsibleModule class. + cursor (cursor): Cursor object of a database Python connector. + + Attributes: + module (AnsibleModule): Object of the AnsibleModule class. + cursor (cursor): Cursor object of a database Python connector. + role_impl (library): Corresponding library depending + on a server type (MariaDB or MySQL) + mariadb (bool): True if MariaDB, False otherwise. + roles_supported (bool): True if roles are supported, False otherwise. + users (set): Set of users existing in a DB in the form (username, hostname). + """ + def __init__(self, module, cursor): + self.module = module + self.cursor = cursor + self.role_impl = self.get_implementation() + self.mariadb = self.role_impl.is_mariadb() + self.roles_supported = self.role_impl.supports_roles(self.cursor) + self.users = set(self.__get_users()) + + def is_mariadb(self): + """Get info whether a DB server is a MariaDB instance. + + Returns: + self.mariadb: Attribute value. + """ + return self.mariadb + + def supports_roles(self): + """Get info whether a DB server supports roles. + + Returns: + self.roles_supported: Attribute value. + """ + return self.roles_supported + + def get_implementation(self): + """Get a current server implementation depending on its type. + + Returns: + library: Depending on a server type (MySQL or MariaDB). + """ + self.cursor.execute("SELECT VERSION()") + + if 'mariadb' in self.cursor.fetchone()[0].lower(): + import ansible_collections.community.mysql.plugins.module_utils.implementations.mariadb.role as role_impl + else: + import ansible_collections.community.mysql.plugins.module_utils.implementations.mysql.role as role_impl + + return role_impl + + def check_users_in_db(self, users): + """Check if users exist in a database. + + Args: + users (list): List of tuples (username, hostname) to check. + """ + for user in users: + if user not in self.users: + msg = 'User / role `%s` with host `%s` does not exist' % (user[0], user[1]) + self.module.fail_json(msg=msg) + + def __get_users(self): + """Get users. + + Returns: + list: List of tuples (username, hostname). + """ + self.cursor.execute('SELECT User, Host FROM mysql.user') + return self.cursor.fetchall() + + def get_users(self): + """Get set of tuples (username, hostname) existing in a DB. + + Returns: + self.users: Attribute value. + """ + return self.users + + def get_grants(self, user, host): + """Get grants. + + Args: + user (str): User name + host (str): Host name + + Returns: + list: List of tuples like [(grant1,), (grant2,), ... ]. + """ + if host: + self.cursor.execute('SHOW GRANTS FOR %s@%s', (user, host)) + else: + self.cursor.execute('SHOW GRANTS FOR %s', (user,)) + + return self.cursor.fetchall() + + +class MySQLQueryBuilder(): + """Class to build and return queries specific to MySQL. + + Args: + name (str): Role name. + host (str): Role host. + + Attributes: + name (str): Role name. + host (str): Role host. + """ + def __init__(self, name, host): + self.name = name + self.host = host + + def role_exists(self): + """Return a query to check if a role with self.name and self.host exists in a database. + + Returns: + tuple: (query_string, tuple_containing_parameters). + """ + return 'SELECT count(*) FROM mysql.user WHERE user = %s AND host = %s', (self.name, self.host) + + def role_grant(self, user): + """Return a query to grant a role to a user or a role. + + Args: + user (tuple): User / role to grant the role to in the form (username, hostname). + + Returns: + tuple: (query_string, tuple_containing_parameters). + """ + if user[1]: + return 'GRANT %s@%s TO %s@%s', (self.name, self.host, user[0], user[1]) + else: + return 'GRANT %s@%s TO %s', (self.name, self.host, user[0]) + + def role_revoke(self, user): + """Return a query to revoke a role from a user or role. + + Args: + user (tuple): User / role to revoke the role from in the form (username, hostname). + + Returns: + tuple: (query_string, tuple_containing_parameters). + """ + if user[1]: + return 'REVOKE %s@%s FROM %s@%s', (self.name, self.host, user[0], user[1]) + else: + return 'REVOKE %s@%s FROM %s', (self.name, self.host, user[0]) + + def role_create(self, admin=None): + """Return a query to create a role. + + Args: + admin (tuple): Admin user in the form (username, hostname). + Because it is not supported by MySQL, we ignore it. + + Returns: + tuple: (query_string, tuple_containing_parameters). + """ + return 'CREATE ROLE %s', (self.name,) + + +class MariaDBQueryBuilder(): + """Class to build and return queries specific to MariaDB. + + Args: + name (str): Role name. + + Attributes: + name (str): Role name. + """ + def __init__(self, name): + self.name = name + + def role_exists(self): + """Return a query to check if a role with self.name exists in a database. + + Returns: + tuple: (query_string, tuple_containing_parameters). + """ + return "SELECT count(*) FROM mysql.user WHERE user = %s AND is_role = 'Y'", (self.name) + + def role_grant(self, user): + """Return a query to grant a role to a user or role. + + Args: + user (tuple): User / role to grant the role to in the form (username, hostname). + + Returns: + tuple: (query_string, tuple_containing_parameters). + """ + if user[1]: + return 'GRANT %s TO %s@%s', (self.name, user[0], user[1]) + else: + return 'GRANT %s TO %s', (self.name, user[0]) + + def role_revoke(self, user): + """Return a query to revoke a role from a user or role. + + Args: + user (tuple): User / role to revoke the role from in the form (username, hostname). + + Returns: + tuple: (query_string, tuple_containing_parameters). + """ + if user[1]: + return 'REVOKE %s FROM %s@%s', (self.name, user[0], user[1]) + else: + return 'REVOKE %s FROM %s', (self.name, user[0]) + + def role_create(self, admin=None): + """Return a query to create a role. + + Args: + admin (tuple): Admin user in the form (username, hostname). + + Returns: + tuple: (query_string, tuple_containing_parameters). + """ + if not admin: + return 'CREATE ROLE %s', (self.name,) + + if admin[1]: + return 'CREATE ROLE %s WITH ADMIN %s@%s', (self.name, admin[0], admin[1]) + else: + return 'CREATE ROLE %s WITH ADMIN %s', (self.name, admin[0]) + + +class MySQLRoleImpl(): + """Class to work with MySQL role implementation. + + Args: + module (AnsibleModule): Object of the AnsibleModule class. + cursor (cursor): Cursor object of a database Python connector. + name (str): Role name. + host (str): Role host. + + Attributes: + module (AnsibleModule): Object of the AnsibleModule class. + cursor (cursor): Cursor object of a database Python connector. + name (str): Role name. + host (str): Role host. + """ + def __init__(self, module, cursor, name, host): + self.module = module + self.cursor = cursor + self.name = name + self.host = host + + def set_default_role_all(self, user): + """Run 'SET DEFAULT ROLE ALL TO' a user. + + Args: + user (tuple): User / role to run the command against in the form (username, hostname). + """ + if user[1]: + self.cursor.execute('SET DEFAULT ROLE ALL TO %s@%s', (user[0], user[1])) + else: + self.cursor.execute('SET DEFAULT ROLE ALL TO %s', (user[0],)) + + def get_admin(self): + """Get a current admin of a role. + + Not supported by MySQL, so ignored here. + """ + pass + + def set_admin(self, admin): + """Set an admin of a role. + + Not supported by MySQL, so ignored here. + + TODO: Implement the feature if this gets supported. + + Args: + admin (tuple): Admin user of the role in the form (username, hostname). + """ + pass + + +class MariaDBRoleImpl(): + """Class to work with MariaDB role implementation. + + Args: + module (AnsibleModule): Object of the AnsibleModule class. + cursor (cursor): Cursor object of a database Python connector. + name (str): Role name. + + Attributes: + module (AnsibleModule): Object of the AnsibleModule class. + cursor (cursor): Cursor object of a database Python connector. + name (str): Role name. + """ + def __init__(self, module, cursor, name): + self.module = module + self.cursor = cursor + self.name = name + + def set_default_role_all(self, user): + """Run 'SET DEFAULT ROLE ALL TO' a user. + + The command is not supported by MariaDB, ignored. + + Args: + user (tuple): User / role to run the command against in the form (username, hostname). + """ + pass + + def get_admin(self): + """Get a current admin of a role. + + Returns: + tuple: Of the form (username, hostname). + """ + query = ("SELECT User, Host FROM mysql.roles_mapping " + "WHERE Role = %s and Admin_option = 'Y'") + + self.cursor.execute(query, (self.name,)) + return self.cursor.fetchone() + + def set_admin(self, admin): + """Set an admin of a role. + + TODO: Implement changing when ALTER ROLE statement to + change role's admin gets supported. + + Args: + admin (tuple): Admin user of the role in the form (username, hostname). + """ + admin_user = admin[0] + admin_host = admin[1] + current_admin = self.get_admin() + + if (admin_user, admin_host) != current_admin: + msg = ('The "admin" option value and the current ' + 'roles admin (%s@%s) don not match. Ignored. ' + 'To change the admin, you need to drop and create the ' + 'role again.' % (current_admin[0], current_admin[1])) + self.module.warn(msg) + + +class Role(): + """Class to work with MySQL role objects. + + Args: + module (AnsibleModule): Object of the AnsibleModule class. + cursor (cursor): Cursor object of a database Python connector. + name (str): Role name. + server (DbServer): Object of the DbServer class. + + Attributes: + module (AnsibleModule): Object of the AnsibleModule class. + cursor (cursor): Cursor object of a database Python connector. + name (str): Role name. + server (DbServer): Object of the DbServer class. + host (str): Role's host. + full_name (str): Role's full name. + exists (bool): Indicates if a role exists or not. + members (set): Set of current role's members. + """ + def __init__(self, module, cursor, name, server): + self.module = module + self.cursor = cursor + self.name = name + self.server = server + self.is_mariadb = self.server.is_mariadb() + + if self.is_mariadb: + self.q_builder = MariaDBQueryBuilder(self.name) + self.role_impl = MariaDBRoleImpl(self.module, self.cursor, self.name) + self.full_name = '`%s`' % self.name + self.host = '' + else: + self.host = '%' + self.q_builder = MySQLQueryBuilder(self.name, self.host) + self.role_impl = MySQLRoleImpl(self.module, self.cursor, self.name, self.host) + self.full_name = '`%s`@`%s`' % (self.name, self.host) + + self.exists = self.__role_exists() + self.members = set() + + if self.exists: + self.members = self.__get_members() + + def __role_exists(self): + """Check if a role exists. + + Returns: + bool: True if the role exists, False if it does not. + """ + self.cursor.execute(*self.q_builder.role_exists()) + return self.cursor.fetchone()[0] > 0 + + def add(self, users, privs, check_mode=False, admin=False, + set_default_role_all=True): + """Add a role. + + Args: + users (list): Role members. + privs (str): String containing privileges. + check_mode (bool): If True, just checks and does nothing. + admin (tuple): Role's admin. Contains (username, hostname). + set_default_role_all (bool): If True, runs SET DEFAULT ROLE ALL TO each member. + + Returns: + bool: True if the state has changed, False if has not. + """ + if check_mode: + if not self.exists: + return True + return False + + self.cursor.execute(*self.q_builder.role_create(admin)) + + if users: + self.update_members(users, set_default_role_all=set_default_role_all) + + if privs: + for db_table, priv in iteritems(privs): + privileges_grant(self.cursor, self.name, self.host, + db_table, priv, tls_requires=None, + maria_role=self.is_mariadb) + + return True + + def drop(self, check_mode=False): + """Drop a role. + + Args: + check_mode (bool): If True, just checks and does nothing. + + Returns: + bool: True if the state has changed, False if has not. + """ + if not self.exists: + return False + + if check_mode and self.exists: + return True + + self.cursor.execute('DROP ROLE %s', (self.name,)) + return True + + def update_members(self, users, check_mode=False, append_members=False, + set_default_role_all=True): + """Add users to a role. + + Args: + users (list): Role members. + check_mode (bool): If True, just checks and does nothing. + append_members (bool): If True, adds new members passed through users + not touching current members. + set_default_role_all (bool): If True, runs SET DEFAULT ROLE ALL TO each member. + + Returns: + bool: True if the state has changed, False if has not. + """ + if not users: + return False + + changed = False + for user in users: + if user not in self.members: + if check_mode: + return True + + self.cursor.execute(*self.q_builder.role_grant(user)) + + self.role_impl.set_default_role_all(user) + + changed = True + + if append_members: + return changed + + for user in self.members: + if user not in users and user != ('root', 'localhost'): + changed = self.__remove_member(user, check_mode) + + return changed + + def remove_members(self, users, check_mode=False): + """Remove members from a role. + + Args: + users (list): Role members. + check_mode (bool): If True, just checks and does nothing. + + Returns: + bool: True if the state has changed, False if has not. + """ + if not users: + return False + + changed = False + for user in users: + if user in self.members: + changed = self.__remove_member(user, check_mode) + + return changed + + def __remove_member(self, user, check_mode=False): + """Remove a member from a role. + + Args: + user (str): Role member to remove. + check_mode (bool): If True, just returns True and does nothing. + + Returns: + bool: True if the state has changed, False if has not. + """ + if check_mode: + return True + + self.cursor.execute(*self.q_builder.role_revoke(user)) + + return True + + def update(self, users, privs, check_mode=False, + append_privs=False, append_members=False, + detach_members=False, admin=False, + set_default_role_all=True): + """Update a role. + + Update a role if needed. + + Todo: Implement changing of role's admin when ALTER ROLE statement + to do that gets supported. + + Args: + users (list): Role members. + privs (str): String containing privileges. + check_mode (bool): If True, just checks and does nothing. + append_privs (bool): If True, adds new privileges passed through privs + not touching current privileges. + append_members (bool): If True, adds new members passed through users + not touching current members. + detach_members (bool): If True, removes members passed through users from a role. + admin (tuple): Role's admin. Contains (username, hostname). + set_default_role_all (bool): If True, runs SET DEFAULT ROLE ALL TO each member. + + Returns: + bool: True if the state has changed, False if has not. + """ + changed = False + members_changed = False + + if users: + if detach_members: + members_changed = self.remove_members(users, check_mode=check_mode) + + else: + members_changed = self.update_members(users, check_mode=check_mode, + append_members=append_members, + set_default_role_all=set_default_role_all) + + if privs: + changed, msg = user_mod(self.cursor, self.name, self.host, + None, None, None, None, None, None, + privs, append_privs, None, + self.module, role=True, maria_role=self.is_mariadb) + + if admin: + self.role_impl.set_admin(admin) + + changed = changed or members_changed + + return changed + + def __get_members(self): + """Get current role's members. + + Returns: + set: Members. + """ + members = set() + + for user, host in self.server.get_users(): + # Don't handle itself + if user == self.name and host == self.host: + continue + + grants = self.server.get_grants(user, host) + + if self.__is_member(grants): + members.add((user, host)) + + return members + + def __is_member(self, grants): + """Check if a user / role is a member of a role. + + To check if a user is a member of a role, + we parse their grants looking for the role name in them. + In the following grants, we can see that test@% is a member of readers. + +---------------------------------------------------+ + | Grants for test@% | + +---------------------------------------------------+ + | GRANT SELECT, INSERT, UPDATE ON *.* TO `test`@`%` | + | GRANT ALL PRIVILEGES ON `mysql`.* TO `test`@`%` | + | GRANT INSERT ON `mysql`.`user` TO `test`@`%` | + | GRANT `readers`@`%` TO `test`@`%` | + +---------------------------------------------------+ + + Args: + grants (list): Grants of a user to parse. + + Returns: + bool: True if the self.full_name has been found in grants, + otherwise returns False. + """ + if not grants: + return False + + for grant in grants: + if self.full_name in grant[0]: + return True + + return False + + +def main(): + argument_spec = mysql_common_argument_spec() + argument_spec.update( + name=dict(type='str', required=True), + state=dict(type='str', default='present', choices=['absent', 'present']), + admin=dict(type='str'), + priv=dict(type='raw'), + append_privs=dict(type='bool', default=False), + members=dict(type='list', elements='str'), + append_members=dict(type='bool', default=False), + detach_members=dict(type='bool', default=False), + check_implicit_admin=dict(type='bool', default=False), + set_default_role_all=dict(type='bool', default=True), + ) + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=True, + mutually_exclusive=( + ('append_members', 'detach_members'), + ('admin', 'members'), + ('admin', 'append_members'), + ('admin', 'detach_members'), + ), + ) + + login_user = module.params['login_user'] + login_password = module.params['login_password'] + name = module.params['name'] + state = module.params['state'] + admin = module.params['admin'] + priv = module.params['priv'] + check_implicit_admin = module.params['check_implicit_admin'] + connect_timeout = module.params['connect_timeout'] + config_file = module.params['config_file'] + append_privs = module.params['append_privs'] + members = module.params['members'] + append_members = module.params['append_members'] + detach_members = module.params['detach_members'] + 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 = '' + set_default_role_all = module.params['set_default_role_all'] + + if priv and not isinstance(priv, (str, dict)): + msg = ('The "priv" parameter must be str or dict ' + 'but %s was passed' % type(priv)) + module.fail_json(msg=msg) + + 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))) + + # Set defaults + changed = False + + get_impl(cursor) + + if priv is not None: + try: + mode = get_mode(cursor) + except Exception as e: + module.fail_json(msg=to_native(e)) + + try: + priv = privileges_unpack(priv, mode) + except Exception as e: + module.fail_json(msg='Invalid privileges string: %s' % to_native(e)) + + server = DbServer(module, cursor) + + # Check if the server supports roles + if not server.supports_roles(): + msg = ('Roles are not supported by the server. ' + 'Minimal versions are MySQL 8.0.0 or MariaDB 10.0.5.') + module.fail_json(msg=msg) + + if admin: + if not server.is_mariadb(): + module.fail_json(msg='The "admin" option can be used only with MariaDB.') + + admin = normalize_users(module, [admin])[0] + server.check_users_in_db([admin]) + + if members: + members = normalize_users(module, members, server.is_mariadb()) + server.check_users_in_db(members) + + # Main job starts here + role = Role(module, cursor, name, server) + + try: + if state == 'present': + if not role.exists: + changed = role.add(members, priv, module.check_mode, admin, + set_default_role_all) + + else: + changed = role.update(members, priv, module.check_mode, append_privs, + append_members, detach_members, admin, + set_default_role_all) + + elif state == 'absent': + changed = role.drop(module.check_mode) + + except Exception as e: + module.fail_json(msg=to_native(e)) + + # Exit + db_conn.close() + module.exit_json(changed=changed) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index 38c1a55..3172553 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -306,830 +306,28 @@ EXAMPLES = r''' RETURN = '''#''' -import re -import string - 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.module_utils.six import iteritems +from ansible_collections.community.mysql.plugins.module_utils.user import ( + convert_priv_dict_to_str, + get_impl, + get_mode, + handle_requiressl_in_priv_string, + InvalidPrivsError, + limit_resources, + privileges_unpack, + sanitize_requires, + user_add, + user_delete, + user_exists, + user_mod, +) from ansible.module_utils._text import to_native -VALID_PRIVS = frozenset(('CREATE', 'DROP', 'GRANT', 'GRANT OPTION', - 'LOCK TABLES', 'REFERENCES', 'EVENT', 'ALTER', - 'DELETE', 'INDEX', 'INSERT', 'SELECT', 'UPDATE', - 'CREATE TEMPORARY TABLES', 'TRIGGER', 'CREATE VIEW', - 'SHOW VIEW', 'ALTER ROUTINE', 'CREATE ROUTINE', - 'EXECUTE', 'FILE', 'CREATE TABLESPACE', 'CREATE USER', - 'PROCESS', 'PROXY', 'RELOAD', 'REPLICATION CLIENT', - 'REPLICATION SLAVE', 'SHOW DATABASES', 'SHUTDOWN', - 'SUPER', 'ALL', 'ALL PRIVILEGES', 'USAGE', - 'REQUIRESSL', # Deprecated, to be removed in version 3.0.0 - 'CREATE ROLE', 'DROP ROLE', 'APPLICATION_PASSWORD_ADMIN', - 'AUDIT_ADMIN', 'BACKUP_ADMIN', 'BINLOG_ADMIN', - 'BINLOG_ENCRYPTION_ADMIN', 'CLONE_ADMIN', 'CONNECTION_ADMIN', - 'ENCRYPTION_KEY_ADMIN', 'FIREWALL_ADMIN', 'FIREWALL_USER', - 'GROUP_REPLICATION_ADMIN', 'INNODB_REDO_LOG_ARCHIVE', - 'NDB_STORED_USER', 'PERSIST_RO_VARIABLES_ADMIN', - 'REPLICATION_APPLIER', 'REPLICATION_SLAVE_ADMIN', - 'RESOURCE_GROUP_ADMIN', 'RESOURCE_GROUP_USER', - 'ROLE_ADMIN', 'SESSION_VARIABLES_ADMIN', 'SET_USER_ID', - 'SYSTEM_USER', 'SYSTEM_VARIABLES_ADMIN', 'SYSTEM_USER', - 'TABLE_ENCRYPTION_ADMIN', 'VERSION_TOKEN_ADMIN', - 'XA_RECOVER_ADMIN', 'LOAD FROM S3', 'SELECT INTO S3', - 'INVOKE LAMBDA', - 'ALTER ROUTINE', - 'BINLOG ADMIN', - 'BINLOG MONITOR', - 'BINLOG REPLAY', - 'CONNECTION ADMIN', - 'READ_ONLY ADMIN', - 'REPLICATION MASTER ADMIN', - 'REPLICATION SLAVE ADMIN', - 'SET USER', - 'SHOW_ROUTINE', - 'SLAVE MONITOR', - 'REPLICA MONITOR',)) - - -class InvalidPrivsError(Exception): - pass - -# =========================================== -# MySQL module specific support methods. -# - - -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 user_add(cursor, user, host, host_all, password, encrypted, - plugin, plugin_hash_string, plugin_auth_string, new_priv, - tls_requires, check_mode): - # we cannot create users without a proper hostname - if host_all: - return False - - if check_mode: - return True - - # 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 - - 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 True - - -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, tls_requires, module): - 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: - hostnames = user_get_hostnames(cursor, user) - else: - hostnames = [host] - - for host in hostnames: - # Handle clear text and hashed passwords. - 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: - msg = "Password updated" - if module.check_mode: - return (True, msg) - 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: - 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) - changed = True - - # Handle privileges - if new_priv is not None: - curr_priv = privileges_get(cursor, user, host) - - # 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. - 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 and not append_privs: - msg = "Privileges updated" - if module.check_mode: - return (True, msg) - privileges_revoke(cursor, user, host, db_table, priv, grant_option) - changed = True - - # If the user doesn't currently have any privileges on a db.table, then - # we can perform a straight grant operation. - for db_table, priv in iteritems(new_priv): - if db_table not in curr_priv: - msg = "New privileges granted" - if module.check_mode: - return (True, msg) - privileges_grant(cursor, user, host, db_table, priv, tls_requires) - 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: - - # If appending privileges, only the set difference between new privileges and current privileges matter. - # The symmetric difference isn't relevant for append because existing privileges will not be revoked. - if append_privs: - priv_diff = set(new_priv[db_table]) - set(curr_priv[db_table]) - else: - priv_diff = set(new_priv[db_table]) ^ set(curr_priv[db_table]) - - if len(priv_diff) > 0: - msg = "Privileges updated" - if module.check_mode: - return (True, msg) - if not append_privs: - privileges_revoke(cursor, user, host, db_table, curr_priv[db_table], grant_option) - privileges_grant(cursor, user, host, db_table, new_priv[db_table], tls_requires) - changed = True - - # 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 (True, msg) - 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, msg) - - -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: - 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): - """ 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 = {} - cursor.execute("SHOW GRANTS FOR %s@%s", (user, host)) - grants = cursor.fetchall() - - def pick(x): - if x == 'ALL PRIVILEGES': - return 'ALL' - else: - return x - - for grant in grants: - res = re.match("""GRANT (.+) ON (.+) TO (['`"]).*\\3@(['`"]).*\\4( IDENTIFIED BY PASSWORD (['`"]).+\\6)? ?(.*)""", grant[0]) - if res is None: - 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 "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): - """ 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]]) - - new_privs = frozenset(privs) - if not new_privs.issubset(VALID_PRIVS): - raise InvalidPrivsError('Invalid privileges specified: %s' % new_privs.difference(VALID_PRIVS)) - - if '*.*' not in output: - output['*.*'] = ['USAGE'] - - return output - - -def privileges_revoke(cursor, user, host, db_table, priv, grant_option): - # 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] - query.append("FROM %s@%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)] - query.append("FROM %s@%s") - query = ' '.join(query) - cursor.execute(query, (user, host)) - - -def privileges_grant(cursor, user, host, db_table, priv, tls_requires): - # 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)] - query.append("TO %s@%s") - params = (user, host) - 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) - cursor.execute(query, params) - - -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) - - -def handle_requiressl_in_priv_string(module, priv, tls_requires): - module.deprecate('The "REQUIRESSL" privilege is deprecated, use the "tls_requires" option instead.', - version='3.0.0', collection_name='community.mysql') - priv_groups = re.search(r"(.*?)(\*\.\*:)([^/]*)(.*)", priv) - if priv_groups.group(3) == "REQUIRESSL": - priv = priv_groups.group(1) + priv_groups.group(4) or None - else: - inner_priv_groups = re.search(r"(.*?),?REQUIRESSL,?(.*)", priv_groups.group(3)) - priv = '{0}{1}{2}{3}'.format( - priv_groups.group(1), - priv_groups.group(2), - ','.join(filter(None, (inner_priv_groups.group(1), inner_priv_groups.group(2)))), - priv_groups.group(4) - ) - if not tls_requires: - tls_requires = "SSL" - else: - module.warn('Ignoring "REQUIRESSL" privilege as "tls_requires" is defined and it takes precedence.') - return priv, tls_requires - - -# 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. # @@ -1215,14 +413,7 @@ def main(): if not sql_log_bin: cursor.execute("SET SQL_LOG_BIN=0;") - 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 mysqluser - impl = mysqluser - else: - from ansible_collections.community.mysql.plugins.module_utils.implementations.mysql import user as mariauser - impl = mariauser + get_impl(cursor) if priv is not None: try: diff --git a/tests/integration/targets/test_mysql_role/defaults/main.yml b/tests/integration/targets/test_mysql_role/defaults/main.yml new file mode 100644 index 0000000..744ba34 --- /dev/null +++ b/tests/integration/targets/test_mysql_role/defaults/main.yml @@ -0,0 +1,16 @@ +mysql_user: root +mysql_password: msandbox +mysql_primary_port: 3307 + +test_db: test_db +test_table: test_table +test_db1: test_db1 +test_db2: test_db2 + +user0: user0 +user1: user1 +user2: user2 +nonexistent: user3 + +role0: role0 +role1: role1 diff --git a/tests/integration/targets/test_mysql_role/meta/main.yml b/tests/integration/targets/test_mysql_role/meta/main.yml new file mode 100644 index 0000000..ce08dc4 --- /dev/null +++ b/tests/integration/targets/test_mysql_role/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: +- setup_mysql diff --git a/tests/integration/targets/test_mysql_role/tasks/main.yml b/tests/integration/targets/test_mysql_role/tasks/main.yml new file mode 100644 index 0000000..5bcd5ec --- /dev/null +++ b/tests/integration/targets/test_mysql_role/tasks/main.yml @@ -0,0 +1,7 @@ +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +# mysql_role module initial CI tests +- import_tasks: mysql_role_initial.yml diff --git a/tests/integration/targets/test_mysql_role/tasks/mysql_role_initial.yml b/tests/integration/targets/test_mysql_role/tasks/mysql_role_initial.yml new file mode 100644 index 0000000..ba3125c --- /dev/null +++ b/tests/integration/targets/test_mysql_role/tasks/mysql_role_initial.yml @@ -0,0 +1,1151 @@ +# Test code for mysql_role module + +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: 127.0.0.1 + login_port: '{{ mysql_primary_port }}' + + task_parameters: &task_params + register: result + + block: + + - name: Get server version + mysql_info: + <<: *mysql_params + register: srv + + - name: When run with unsupported server versions, must fail + <<: *task_params + mysql_role: + <<: *mysql_params + name: test + ignore_errors: yes + + - name: Must fail when meet unsupported version + assert: + that: + - result is failed + - result is search('Roles are not supported by the server') + when: + - srv['version']['major'] < 8 + + # Skip unsupported versions + - meta: end_play + when: + - srv['version']['major'] < 8 + + ######### + # Prepare + - name: Create db {{ test_db }} + <<: *task_params + mysql_db: + <<: *mysql_params + name: '{{ test_db }}' + + - name: Create table {{ test_table }} + <<: *task_params + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: 'DROP TABLE IF EXISTS {{ test_table }}' + + - name: Create table {{ test_table }} + <<: *task_params + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: 'CREATE TABLE IF NOT EXISTS {{ test_table }} (id int)' + + - name: Create users + <<: *task_params + mysql_user: + <<: *mysql_params + name: '{{ item }}' + password: '{{ mysql_password }}' + loop: + - '{{ user0 }}' + - '{{ user1 }}' + - '{{ user2 }}' + + ########### + # Run tests + + - name: Create role {{ role0 }} in check_mode + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: present + members: + - '{{ user0 }}@localhost' + check_mode: yes + + - name: Check + assert: + that: + - result is changed + + - name: Check in DB + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}' AND Host = '%'" + + - name: Check + assert: + that: + - result.rowcount.0 == 0 + + # It must fail because of check_mode + - name: Check in DB, if not granted, the query will fail + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" + ignore_errors: yes + + - name: Check + assert: + that: + - result is failed + + #===================== + + - name: Check that the user have no active roles + <<: *task_params + mysql_query: + login_user: '{{ user0 }}' + login_password: '{{ mysql_password }}' + login_host: 127.0.0.1 + login_port: '{{ mysql_primary_port }}' + query: 'SELECT current_role()' + + - name: Check + assert: + that: + - result.query_result.0.0["current_role()"] == "NONE" + + - name: Create role {{ role0 }} + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: present + members: + - '{{ user0 }}@localhost' + + - name: Check + assert: + that: + - result is changed + + - name: Check in DB + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}' AND Host = '%'" + + - name: Check + assert: + that: + - result.rowcount.0 == 1 + + - name: Check in DB, if not granted, the query will fail + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" + + - name: Check + assert: + that: + - result is succeeded + + - name: Check that the role is active + <<: *task_params + mysql_query: + login_user: '{{ user0 }}' + login_password: '{{ mysql_password }}' + login_host: 127.0.0.1 + login_port: '{{ mysql_primary_port }}' + query: 'SELECT current_role()' + + - name: Check + assert: + that: + - result.query_result.0.0["current_role()"] == "`{{ role0 }}`@`%`" + + #======================== + + - name: Create role {{ role0 }} again in check_mode + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: present + check_mode: yes + + - name: Check + assert: + that: + - result is not changed + + - name: Check in DB + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}' AND Host = '%'" + + - name: Check + assert: + that: + - result.rowcount.0 == 1 + + - name: Check in DB, if not granted, the query will fail + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" + + - name: Check + assert: + that: + - result is succeeded + + #======================== + + - name: Create role {{ role0 }} again + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: present + + - name: Check + assert: + that: + - result is not changed + + - name: Check in DB + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}' AND Host = '%'" + + - name: Check + assert: + that: + - result.rowcount.0 == 1 + + #======================== + + - name: Drop role {{ role0 }} in check_mode + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: absent + check_mode: yes + + - name: Check + assert: + that: + - result is changed + + - name: Check in DB + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}' AND Host = '%'" + + - name: Check + assert: + that: + - result.rowcount.0 == 1 + + # Must pass because of check_mode + - name: Check in DB, if not granted, the query will fail + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" + + - name: Check + assert: + that: + - result is succeeded + + #======================== + + - name: Drop role {{ role0 }} + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: absent + + - name: Check + assert: + that: + - result is changed + + - name: Check in DB + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}' AND Host = '%'" + + - name: Check + assert: + that: + - result.rowcount.0 == 0 + + - name: Check in DB, if not granted, the query will fail + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" + ignore_errors: yes + + - name: Check + assert: + that: + - result is failed + + #======================== + + - name: Drop role {{ role0 }} again in check_mode + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: absent + check_mode: yes + + - name: Check + assert: + that: + - result is not changed + + - name: Drop role {{ role0 }} again + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: absent + + - name: Check + assert: + that: + - result is not changed + + # ================== + + - name: Create role {{ role0 }} in check_mode + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: present + members: + - '{{ user0 }}@localhost' + priv: + '*.*': 'SELECT,INSERT' + 'mysql.*': 'UPDATE' + check_mode: yes + + - name: Check + assert: + that: + - result is changed + + - name: Check in DB + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}' AND Host = '%'" + + - name: Check + assert: + that: + - result.rowcount.0 == 0 + + #======================== + + - name: Create role {{ role0 }} + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: present + members: + - '{{ user0 }}@localhost' + priv: + '*.*': 'SELECT,INSERT' + 'mysql.*': 'UPDATE' + + - name: Check + assert: + that: + - result is changed + + - name: Check in DB + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}' AND Host = '%'" + + - name: Check + assert: + that: + - result.rowcount.0 == 1 + + #======================== + + - name: Create role {{ role0 }} in check_mode again + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: present + members: + - '{{ user0 }}@localhost' + priv: + '*.*': 'SELECT,INSERT' + 'mysql.*': 'UPDATE' + check_mode: yes + + - name: Check + assert: + that: + - result is not changed + + #======================== + + - name: Create role {{ role0 }} again + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: present + members: + - '{{ user0 }}@localhost' + priv: + '*.*': 'SELECT,INSERT' + 'mysql.*': 'UPDATE' + + - name: Check + assert: + that: + - result is not changed + + # ############################################## + # Test rewriting / appending / detaching members + # ############################################## + + - name: Create role {{ role1 }} + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role1 }}' + state: present + + # Rewriting members + - name: Rewrite members in check_mode + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: present + members: + - '{{ user1 }}@localhost' + - '{{ user2 }}@localhost' + - '{{ role1 }}' + check_mode: yes + + - name: Check + assert: + that: + - result is changed + + # user0 is still a member because of check_mode + - name: Check in DB, if not granted, the query will fail + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" + + - name: Check + assert: + that: + - result is succeeded + + # user1, user2, and role1 are not members because of check_mode + - name: Check in DB, if not granted, the query will fail + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR {{ user1 }}@localhost USING '{{ role0 }}'" + ignore_errors: yes + + - name: Check + assert: + that: + - result is failed + + - name: Check in DB, if not granted, the query will fail + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR {{ user2 }}@localhost USING '{{ role0 }}'" + ignore_errors: yes + + - name: Check + assert: + that: + - result is failed + + - name: Check in DB, if not granted, the query will fail + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR {{ role1 }} USING '{{ role0 }}'" + ignore_errors: yes + + - name: Check + assert: + that: + - result is failed + #======================== + + - name: Rewrite members + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: present + members: + - '{{ user1 }}@localhost' + - '{{ user2 }}@localhost' + - '{{ role1 }}' + + - name: Check + assert: + that: + - result is changed + + # user0 is not a member any more + - name: Check in DB, if not granted, the query will fail + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" + ignore_errors: yes + + - name: Check + assert: + that: + - result is failed + + - name: Check in DB, if not granted, the query will fail + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR {{ user1 }}@localhost USING '{{ role0 }}'" + + - name: Check + assert: + that: + - result is succeeded + + - name: Check in DB, if not granted, the query will fail + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR {{ user2 }}@localhost USING '{{ role0 }}'" + + - name: Check + assert: + that: + - result is succeeded + + - name: Check in DB, if not granted, the query will fail + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR {{ role1 }} USING '{{ role0 }}'" + ignore_errors: yes + + - name: Check + assert: + that: + - result is succeeded + + #========================== + + - name: Rewrite members again in check_mode + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: present + members: + - '{{ user1 }}@localhost' + - '{{ user2 }}@localhost' + - '{{ role1 }}' + check_mode: yes + + - name: Check + assert: + that: + - result is not changed + + #========================== + + - name: Rewrite members again + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: present + members: + - '{{ user1 }}@localhost' + - '{{ user2 }}@localhost' + - '{{ role1 }}' + + - name: Check + assert: + that: + - result is not changed + + #========================== + + # Append members + - name: Append a member in check_mode + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: present + append_members: yes + members: + - '{{ user0 }}@localhost' + check_mode: yes + + - name: Check + assert: + that: + - result is changed + + - name: Check in DB, if not granted, the query will fail + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" + ignore_errors: yes + + - name: Check + assert: + that: + - result is failed + + #===================== + + - name: Append a member + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: present + append_members: yes + members: + - '{{ user0 }}@localhost' + + - name: Check + assert: + that: + - result is changed + + - name: Check in DB, if not granted, the query will fail + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" + + - name: Check + assert: + that: + - result is succeeded + + # user1 and user2 must still be in DB because we are appending + - name: Check in DB, if not granted, the query will fail + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR {{ user1 }}@localhost USING '{{ role0 }}'" + + - name: Check + assert: + that: + - result is succeeded + + - name: Check in DB, if not granted, the query will fail + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR {{ user2 }}@localhost USING '{{ role0 }}'" + + - name: Check + assert: + that: + - result is succeeded + + #======================== + + - name: Append a member again in check_mode + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: present + append_members: yes + members: + - '{{ user0 }}@localhost' + check_mode: yes + + - name: Check + assert: + that: + - result is not changed + + #======================== + + - name: Append a member again + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: present + append_members: yes + members: + - '{{ user0 }}@localhost' + + - name: Check + assert: + that: + - result is not changed + + ############## + # Detach users + - name: Detach users in check_mode + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: present + detach_members: yes + members: + - '{{ user1 }}@localhost' + - '{{ user2 }}@localhost' + check_mode: yes + + - name: Check + assert: + that: + - result is changed + + # They must be there because of check_mode + - name: Check in DB, if not granted, the query will fail + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" + + - name: Check + assert: + that: + - result is succeeded + + - name: Check in DB, if not granted, the query will fail + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR {{ user1 }}@localhost USING '{{ role0 }}'" + + - name: Check + assert: + that: + - result is succeeded + + - name: Check in DB, if not granted, the query will fail + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR {{ user2 }}@localhost USING '{{ role0 }}'" + + - name: Check + assert: + that: + - result is succeeded + + #======================== + + - name: Detach users + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: present + detach_members: yes + members: + - '{{ user1 }}@localhost' + - '{{ user2 }}@localhost' + + - name: Check + assert: + that: + - result is changed + + - name: Check in DB, if not granted, the query will fail + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" + + - name: Check + assert: + that: + - result is succeeded + + - name: Check in DB, if not granted, the query will fail + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR {{ user1 }}@localhost USING '{{ role0 }}'" + ignore_errors: yes + + - name: Check + assert: + that: + - result is failed + + - name: Check in DB, if not granted, the query will fail + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR {{ user2 }}@localhost USING '{{ role0 }}'" + ignore_errors: yes + + - name: Check + assert: + that: + - result is failed + + #===================== + + - name: Detach users in check_mode again + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: present + detach_members: yes + members: + - '{{ user1 }}@localhost' + - '{{ user2 }}@localhost' + check_mode: yes + + - name: Check + assert: + that: + - result is not changed + + - name: Detach users again + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: present + detach_members: yes + members: + - '{{ user1 }}@localhost' + - '{{ user2 }}@localhost' + + - name: Check + assert: + that: + - result is not changed + + # ########## + # Test privs + # ########## + + - name: Create test DBs + <<: *task_params + mysql_query: + <<: *mysql_params + query: 'CREATE DATABASE {{ item }}' + loop: + - '{{ test_db1 }}' + - '{{ test_db2 }}' + + - name: Create table {{ test_table }} + <<: *task_params + mysql_query: + <<: *mysql_params + login_db: '{{ item }}' + query: 'CREATE TABLE {{ test_table }} (id int)' + loop: + - '{{ test_db1 }}' + - '{{ test_db2 }}' + + - name: Check grants + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR {{ role0 }}" + + - name: Check + assert: + that: + - result.query_result.0.0["Grants for role0@%"] == "GRANT SELECT, INSERT ON *.* TO `role0`@`%`" + - result.query_result.0.1["Grants for role0@%"] == "GRANT UPDATE ON `mysql`.* TO `role0`@`%`" + - result.rowcount.0 == 2 + + - name: Append privs in check_mode + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: present + priv: '{{ test_db1 }}.{{ test_table }}:SELECT,INSERT/{{ test_db2 }}.{{ test_table }}:DELETE' + append_privs: yes + check_mode: yes + + - name: Check + assert: + that: + - result is changed + + - name: Check grants + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR {{ role0 }}" + + - name: Check + assert: + that: + - result.query_result.0.0["Grants for role0@%"] == "GRANT SELECT, INSERT ON *.* TO `role0`@`%`" + - result.query_result.0.1["Grants for role0@%"] == "GRANT UPDATE ON `mysql`.* TO `role0`@`%`" + - result.rowcount.0 == 2 + + - name: Append privs + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: present + priv: '{{ test_db1 }}.{{ test_table }}:SELECT,INSERT/{{ test_db2 }}.{{ test_table }}:DELETE' + append_privs: yes + + - name: Check + assert: + that: + - result is changed + + - name: Check grants + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR {{ role0 }}" + + - name: Check + assert: + that: + - result.query_result.0.0["Grants for role0@%"] == "GRANT SELECT, INSERT ON *.* TO `role0`@`%`" + - result.query_result.0.1["Grants for role0@%"] == "GRANT UPDATE ON `mysql`.* TO `role0`@`%`" + - result.query_result.0.2["Grants for role0@%"] == "GRANT SELECT, INSERT ON `test_db1`.`test_table` TO `role0`@`%`" + - result.query_result.0.3["Grants for role0@%"] == "GRANT DELETE ON `test_db2`.`test_table` TO `role0`@`%`" + - result.rowcount.0 == 4 + + - name: Append privs again in check_mode + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: present + priv: '{{ test_db1 }}.{{ test_table }}:SELECT,INSERT/{{ test_db2 }}.{{ test_table }}:DELETE' + append_privs: yes + check_mode: yes + + # TODO it must be changed. The module uses user_mod function + # taken from mysql_user module. It's a bug / expected behavior + # because I added a similar tasks to mysql_user tests + # https://github.com/ansible-collections/community.mysql/issues/50#issuecomment-871216825 + # and it's also failed. Create an issue after the module is merged to avoid conflicts. + # TODO Fix this after user_mod is fixed. + - name: Check + assert: + that: + - result is changed + + - name: Append privs again + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: present + priv: '{{ test_db1 }}.{{ test_table }}:SELECT,INSERT/{{ test_db2 }}.{{ test_table }}:DELETE' + append_privs: yes + + # TODO it must be changed. The module uses user_mod function + # taken from mysql_user module. It's a bug / expected behavior + # because I added a similar tasks to mysql_user tests + # https://github.com/ansible-collections/community.mysql/issues/50#issuecomment-871216825 + # and it's also failed. Create an issue after the module is merged to avoid conflicts. + # TODO Fix this after user_mod is fixed. + - name: Check + assert: + that: + - result is changed + + - name: Rewrite privs + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: present + priv: + '*.*': 'SELECT' + + - name: Check + assert: + that: + - result is changed + + - name: Check grants + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR {{ role0 }}" + + - name: Check + assert: + that: + - result.query_result.0.0["Grants for role0@%"] == "GRANT SELECT ON *.* TO `role0`@`%`" + - result.rowcount.0 == 1 + + # ################# + # Test admin option + # ################# + + - name: Drop role + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: absent + + - name: Create role with admin + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: present + admin: '{{ user0 }}' + ignore_errors: yes + + - name: Check with MySQL + assert: + that: + - result is failed + - result.msg is search('option can be used only with MariaDB') + when: + # Semantically, when there's MySQL + - srv['version']['major'] < 10 + + - name: Check with MariaDB + assert: + that: + - result is changed + when: + # Semantically, when there's MariaDB + - srv['version']['major'] >= 10 + + - name: Check in DB + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}' AND Host = '%'" + when: + # Semantically, when there's MariaDB + - srv['version']['major'] >= 10 + + - name: Check + assert: + that: + - result.rowcount.0 == 1 + when: + # Semantically, when there's MariaDB + - srv['version']['major'] >= 10 + + - name: Create role with admin again + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: present + admin: '{{ user0 }}' + ignore_errors: yes + + - name: Check with MySQL + assert: + that: + - result is failed + - result.msg is search('option can be used only with MariaDB') + when: + # Semantically, when there's MySQL + - srv['version']['major'] < 10 + + - name: Check with MariaDB + assert: + that: + - result is not changed + # Semantically, when there's MariaDB + when: + - srv['version']['major'] >= 10 + + # Try to grant a role to a user who does not exist + - name: Create role with admin again + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ role0 }}' + state: present + members: + - '{{ nonexistent }}@localhost' + ignore_errors: yes + + - name: Check with MySQL + assert: + that: + - result is failed + - result.msg is search('does not exist') + when: + # Semantically, when there's MySQL + - srv['version']['major'] < 10 + + always: + # Clean up + - name: Drop DBs + mysql_query: + <<: *mysql_params + query: 'DROP DATABASE {{ item }}' + loop: + - '{{ test_db }}' + - '{{ test_db1 }}' + - '{{ test_db2 }}' diff --git a/tests/unit/plugins/modules/test_mysql_user.py b/tests/unit/plugins/module_utils/test_mysql_user.py similarity index 98% rename from tests/unit/plugins/modules/test_mysql_user.py rename to tests/unit/plugins/module_utils/test_mysql_user.py index 3b88e24..0edf922 100644 --- a/tests/unit/plugins/modules/test_mysql_user.py +++ b/tests/unit/plugins/module_utils/test_mysql_user.py @@ -9,7 +9,7 @@ try: except ImportError: from mock import MagicMock -from ansible_collections.community.mysql.plugins.modules.mysql_user import ( +from ansible_collections.community.mysql.plugins.module_utils.user import ( handle_grant_on_col, has_grant_on_col, normalize_col_grants, diff --git a/tests/unit/plugins/modules/test_mysql_role.py b/tests/unit/plugins/modules/test_mysql_role.py new file mode 100644 index 0000000..2ed5a2c --- /dev/null +++ b/tests/unit/plugins/modules/test_mysql_role.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from ansible_collections.community.mysql.plugins.modules.mysql_role import ( + MariaDBQueryBuilder, + MySQLQueryBuilder, + normalize_users, +) + +# TODO: Also cover DbServer, Role, MySQLRoleImpl, MariaDBRoleImpl classes + + +class Module(): + def __init__(self): + self.msg = None + + def fail_json(self, msg=None): + self.msg = msg + + +module = Module() + + +@pytest.mark.parametrize( + 'builder,output', + [ + (MariaDBQueryBuilder('role0'), ("SELECT count(*) FROM mysql.user WHERE user = %s AND is_role = 'Y'", ('role0'))), + (MySQLQueryBuilder('role0', '%'), ('SELECT count(*) FROM mysql.user WHERE user = %s AND host = %s', ('role0', '%'))), + (MariaDBQueryBuilder('role1'), ("SELECT count(*) FROM mysql.user WHERE user = %s AND is_role = 'Y'", ('role1'))), + (MySQLQueryBuilder('role1', 'fake'), ('SELECT count(*) FROM mysql.user WHERE user = %s AND host = %s', ('role1', 'fake'))), + ] +) +def test_query_builder_role_exists(builder, output): + """Test role_exists method of the builder classes.""" + assert builder.role_exists() == output + + +@pytest.mark.parametrize( + 'builder,admin,output', + [ + (MariaDBQueryBuilder('role0'), None, ('CREATE ROLE %s', ('role0',))), + (MySQLQueryBuilder('role0', '%'), None, ('CREATE ROLE %s', ('role0',))), + (MariaDBQueryBuilder('role1'), None, ('CREATE ROLE %s', ('role1',))), + (MySQLQueryBuilder('role1', 'fake'), None, ('CREATE ROLE %s', ('role1',))), + (MariaDBQueryBuilder('role0'), ('user0', ''), ('CREATE ROLE %s WITH ADMIN %s', ('role0', 'user0'))), + (MySQLQueryBuilder('role0', '%'), ('user0', ''), ('CREATE ROLE %s', ('role0',))), + (MariaDBQueryBuilder('role1'), ('user0', 'localhost'), ('CREATE ROLE %s WITH ADMIN %s@%s', ('role1', 'user0', 'localhost'))), + (MySQLQueryBuilder('role1', 'fake'), ('user0', 'localhost'), ('CREATE ROLE %s', ('role1',))), + ] +) +def test_query_builder_role_create(builder, admin, output): + """Test role_create method of the builder classes.""" + assert builder.role_create(admin) == output + + +@pytest.mark.parametrize( + 'builder,user,output', + [ + (MariaDBQueryBuilder('role0'), ('user0', ''), ('GRANT %s TO %s', ('role0', 'user0'))), + (MySQLQueryBuilder('role0', '%'), ('user0', ''), ('GRANT %s@%s TO %s', ('role0', '%', 'user0'))), + (MariaDBQueryBuilder('role1'), ('user0', 'localhost'), ('GRANT %s TO %s@%s', ('role1', 'user0', 'localhost'))), + (MySQLQueryBuilder('role1', 'fake'), ('user0', 'localhost'), ('GRANT %s@%s TO %s@%s', ('role1', 'fake', 'user0', 'localhost'))), + ] +) +def test_query_builder_role_grant(builder, user, output): + """Test role_grant method of the builder classes.""" + assert builder.role_grant(user) == output + + +@pytest.mark.parametrize( + 'builder,user,output', + [ + (MariaDBQueryBuilder('role0'), ('user0', ''), ('REVOKE %s FROM %s', ('role0', 'user0'))), + (MySQLQueryBuilder('role0', '%'), ('user0', ''), ('REVOKE %s@%s FROM %s', ('role0', '%', 'user0'))), + (MariaDBQueryBuilder('role1'), ('user0', 'localhost'), ('REVOKE %s FROM %s@%s', ('role1', 'user0', 'localhost'))), + (MySQLQueryBuilder('role1', 'fake'), ('user0', 'localhost'), ('REVOKE %s@%s FROM %s@%s', ('role1', 'fake', 'user0', 'localhost'))), + ] +) +def test_query_builder_role_revoke(builder, user, output): + """Test role_revoke method of the builder classes.""" + assert builder.role_revoke(user) == output + + +@pytest.mark.parametrize( + 'input_,output,is_mariadb', + [ + (['user'], [('user', '')], True), + (['user'], [('user', '%')], False), + (['user@%'], [('user', '%')], True), + (['user@%'], [('user', '%')], False), + (['user@localhost'], [('user', 'localhost')], True), + (['user@localhost'], [('user', 'localhost')], False), + (['user', 'user@%'], [('user', ''), ('user', '%')], True), + (['user', 'user@%'], [('user', '%'), ('user', '%')], False), + ] +) +def test_normalize_users(input_, output, is_mariadb): + """Test normalize_users function with expected input.""" + assert normalize_users(None, input_, is_mariadb) == output + + +@pytest.mark.parametrize( + 'input_,is_mariadb,err_msg', + [ + ([''], True, "Member's name cannot be empty."), + ([''], False, "Member's name cannot be empty."), + ([None], True, "Error occured while parsing"), + ([None], False, "Error occured while parsing"), + ] +) +def test_normalize_users_failing(input_, is_mariadb, err_msg): + """Test normalize_users function with wrong input.""" + + normalize_users(module, input_, is_mariadb) + assert err_msg in module.msg From 04c0f9f049b93e98ba48cbbfb28e12f5b836e824 Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Wed, 11 Aug 2021 11:20:05 +0300 Subject: [PATCH 13/43] Release 2.2.0-a1 commit (#201) --- changelogs/CHANGELOG.rst | 15 +++++++++++++++ changelogs/changelog.yaml | 14 ++++++++++++++ galaxy.yml | 2 +- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/changelogs/CHANGELOG.rst b/changelogs/CHANGELOG.rst index e2ed3e6..08b6a18 100644 --- a/changelogs/CHANGELOG.rst +++ b/changelogs/CHANGELOG.rst @@ -5,6 +5,21 @@ Community MySQL Collection Release Notes .. contents:: Topics +v2.2.0-a1 +========= + +Release Summary +--------------- + +This is the minor pre-release of the ``community.mysql`` collection. +This changelog contains all changes to the modules in this collection that +have been added after the release of ``community.mysql`` 2.1.1 + +New Modules +----------- + +- mysql_role - Adds, removes, or updates a MySQL role + v2.1.1 ====== diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 497e740..fc37422 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -311,3 +311,17 @@ releases: - 193-reflect_changed_status_in_replace_statements.yml - 2.1.1.yml release_date: '2021-08-11' + 2.2.0-a1: + changes: + release_summary: 'This is the minor pre-release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules in this collection that + + have been added after the release of ``community.mysql`` 2.1.1' + fragments: + - 2.2.0-a1.yml + modules: + - description: Adds, removes, or updates a MySQL role + name: mysql_role + namespace: '' + release_date: '2021-08-11' diff --git a/galaxy.yml b/galaxy.yml index 3e6d8ff..b09fee0 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,6 +1,6 @@ namespace: community name: mysql -version: 2.1.1 +version: 2.2.0-a1 readme: README.md authors: - Ansible community From 2508c420ce2051c4c818a3933d300a03fb6f2537 Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Wed, 11 Aug 2021 12:12:34 +0300 Subject: [PATCH 14/43] Update galaxy.yml after release (#202) --- galaxy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/galaxy.yml b/galaxy.yml index b09fee0..cfe62f1 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,6 +1,6 @@ namespace: community name: mysql -version: 2.2.0-a1 +version: 2.3.0 readme: README.md authors: - Ansible community From 77bd3bfa2eab75ad48056106e3fd194318446b63 Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Fri, 20 Aug 2021 10:50:41 +0300 Subject: [PATCH 15/43] [stable-2] mysql_info: fix TypeError failure when there are databases that do not contain tables (#205) (#208) * mysql_info: fix TypeError failure when there are databases that do not contain tables (#205) * mysql_info: fix TypeError failure when there are databases that do not contain tables * Add changelog fragment (cherry picked from commit a1f419d5413f198ace0e837d01e68ee1abb142fc) * Fix sanity errors (#206) (cherry picked from commit 8a17e43eae5c93266a9d8cbdbc563c5550d00e1f) --- ..._info_fix_failure_when_no_tables_in_db.yml | 2 ++ plugins/module_utils/user.py | 2 +- plugins/modules/mysql_info.py | 3 +++ .../targets/test_mysql_info/tasks/main.yml | 22 +++++++++++++++++++ 4 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 changelogs/fragments/205-mysql_info_fix_failure_when_no_tables_in_db.yml diff --git a/changelogs/fragments/205-mysql_info_fix_failure_when_no_tables_in_db.yml b/changelogs/fragments/205-mysql_info_fix_failure_when_no_tables_in_db.yml new file mode 100644 index 0000000..fd9260e --- /dev/null +++ b/changelogs/fragments/205-mysql_info_fix_failure_when_no_tables_in_db.yml @@ -0,0 +1,2 @@ +bugfixes: +- mysql_info - fix TypeError failure when there are databases that do not contain tables (https://github.com/ansible-collections/community.mysql/issues/204). diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index c59f72a..a8da7f1 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -86,7 +86,7 @@ def sanitize_requires(tls_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()]): + 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 diff --git a/plugins/modules/mysql_info.py b/plugins/modules/mysql_info.py index 7380b6b..6f57403 100644 --- a/plugins/modules/mysql_info.py +++ b/plugins/modules/mysql_info.py @@ -474,6 +474,9 @@ class MySQL_Info(object): 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 diff --git a/tests/integration/targets/test_mysql_info/tasks/main.yml b/tests/integration/targets/test_mysql_info/tasks/main.yml index 785e814..0ed2af9 100644 --- a/tests/integration/targets/test_mysql_info/tasks/main.yml +++ b/tests/integration/targets/test_mysql_info/tasks/main.yml @@ -191,3 +191,25 @@ state: absent - include: issue-28.yml + + # https://github.com/ansible-collections/community.mysql/issues/204 + - name: Create database containing only views + mysql_db: + <<: *mysql_params + name: allviews + + - name: Create view + mysql_query: + <<: *mysql_params + login_db: allviews + query: 'CREATE VIEW v_today (today) AS SELECT CURRENT_DATE' + + - name: Fetch info + mysql_info: + <<: *mysql_params + register: result + + - name: Check + assert: + that: + result.databases.allviews.size == 0 From 0c462c84ff3a0040648816d00dd8a4a8fa0469d4 Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Thu, 23 Sep 2021 12:32:06 +0300 Subject: [PATCH 16/43] Release 2.2.0 commit (#218) --- changelogs/CHANGELOG.rst | 6 +++--- changelogs/changelog.yaml | 10 ++++++++++ galaxy.yml | 2 +- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/changelogs/CHANGELOG.rst b/changelogs/CHANGELOG.rst index 08b6a18..aacd828 100644 --- a/changelogs/CHANGELOG.rst +++ b/changelogs/CHANGELOG.rst @@ -5,13 +5,13 @@ Community MySQL Collection Release Notes .. contents:: Topics -v2.2.0-a1 -========= +v2.2.0 +====== Release Summary --------------- -This is the minor pre-release of the ``community.mysql`` collection. +This is the minor release of the ``community.mysql`` collection. This changelog contains all changes to the modules in this collection that have been added after the release of ``community.mysql`` 2.1.1 diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index fc37422..63d8d87 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -311,6 +311,16 @@ releases: - 193-reflect_changed_status_in_replace_statements.yml - 2.1.1.yml release_date: '2021-08-11' + 2.2.0: + changes: + release_summary: 'This is the minor release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules in this collection that + + have been added after the release of ``community.mysql`` 2.1.1' + fragments: + - 2.2.0.yml + release_date: '2021-09-23' 2.2.0-a1: changes: release_summary: 'This is the minor pre-release of the ``community.mysql`` collection. diff --git a/galaxy.yml b/galaxy.yml index cfe62f1..bc6ce0e 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,6 +1,6 @@ namespace: community name: mysql -version: 2.3.0 +version: 2.2.0 readme: README.md authors: - Ansible community From c5676ff0c9207e644193af378fb3a23d19829636 Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Thu, 23 Sep 2021 13:51:32 +0300 Subject: [PATCH 17/43] [stable-2] Backport stable 2 4 (#219) * Fix wrong impl for mysql (#210) If 'mariadb' in version info, the db instance should be mariadb(reverse in code) rather than mysql. (cherry picked from commit 663590689f3fe9aa94d223ccec527399c383480a) * Update README.md (#216) (cherry picked from commit 4de0e25ea0eed0264763c43a73a10244dc7c1889) * mysql_user: replace VALID_PRIVS by get_valid_privs() function (#217) * mysql_user: replace VALID_PRIVS by get_valid_privs() function * Add EXTRA_PRIVS in case we need to add more privs in the future * Add changelog fragment (cherry picked from commit 0ce1fa1634eab1152c7bb442ca7ee82935081e05) Co-authored-by: int32bit Co-authored-by: R.Sicart --- README.md | 7 +++ ...-get-valid-privs-from-show-privilegees.yml | 2 + plugins/module_utils/user.py | 57 +++++-------------- plugins/modules/mysql_role.py | 4 +- plugins/modules/mysql_user.py | 4 +- 5 files changed, 30 insertions(+), 44 deletions(-) create mode 100644 changelogs/fragments/217-mysql-user-replace-get-valid-privs-from-show-privilegees.yml diff --git a/README.md b/README.md index f3768f0..11727ee 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,13 @@ The current maintainers (contributors with `write` or higher access) are listed To learn how to maintain / become a maintainer of this collection, refer to the [Maintainer guidelines](https://github.com/ansible-collections/community.mysql/blob/main/MAINTAINING.md). +It is necessary for maintainers of this collection to be subscribed to: + +* The collection itself (the `Watch` button -> `All Activity` in the upper right corner of the repository's homepage). +* The "Changes Impacting Collection Contributors and Maintainers" [issue](https://github.com/ansible-collections/overview/issues/45). + +They also should be subscribed to Ansible's [The Bullhorn newsletter](https://docs.ansible.com/ansible/devel/community/communication.html#the-bullhorn). + ## Communication We announce releases and important changes through Ansible's [The Bullhorn newsletter](https://eepurl.com/gZmiEP). Be sure you are subscribed. diff --git a/changelogs/fragments/217-mysql-user-replace-get-valid-privs-from-show-privilegees.yml b/changelogs/fragments/217-mysql-user-replace-get-valid-privs-from-show-privilegees.yml new file mode 100644 index 0000000..60cd451 --- /dev/null +++ b/changelogs/fragments/217-mysql-user-replace-get-valid-privs-from-show-privilegees.yml @@ -0,0 +1,2 @@ +minor_changes: + - mysql_user - replace VALID_PRIVS constant by get_valid_privs() function (https://github.com/ansible-collections/community.mysql/pull/217). diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index a8da7f1..2ba60f2 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -19,41 +19,7 @@ from ansible_collections.community.mysql.plugins.module_utils.mysql import ( ) -VALID_PRIVS = frozenset(('CREATE', 'DROP', 'GRANT', 'GRANT OPTION', - 'LOCK TABLES', 'REFERENCES', 'EVENT', 'ALTER', - 'DELETE', 'INDEX', 'INSERT', 'SELECT', 'UPDATE', - 'CREATE TEMPORARY TABLES', 'TRIGGER', 'CREATE VIEW', - 'SHOW VIEW', 'ALTER ROUTINE', 'CREATE ROUTINE', - 'EXECUTE', 'FILE', 'CREATE TABLESPACE', 'CREATE USER', - 'PROCESS', 'PROXY', 'RELOAD', 'REPLICATION CLIENT', - 'REPLICATION SLAVE', 'SHOW DATABASES', 'SHUTDOWN', - 'SUPER', 'ALL', 'ALL PRIVILEGES', 'USAGE', - 'REQUIRESSL', # Deprecated, to be removed in version 3.0.0 - 'CREATE ROLE', 'DROP ROLE', 'APPLICATION_PASSWORD_ADMIN', - 'AUDIT_ADMIN', 'BACKUP_ADMIN', 'BINLOG_ADMIN', - 'BINLOG_ENCRYPTION_ADMIN', 'CLONE_ADMIN', 'CONNECTION_ADMIN', - 'ENCRYPTION_KEY_ADMIN', 'FIREWALL_ADMIN', 'FIREWALL_USER', - 'GROUP_REPLICATION_ADMIN', 'INNODB_REDO_LOG_ARCHIVE', - 'NDB_STORED_USER', 'PERSIST_RO_VARIABLES_ADMIN', - 'REPLICATION_APPLIER', 'REPLICATION_SLAVE_ADMIN', - 'RESOURCE_GROUP_ADMIN', 'RESOURCE_GROUP_USER', - 'ROLE_ADMIN', 'SESSION_VARIABLES_ADMIN', 'SET_USER_ID', - 'SYSTEM_USER', 'SYSTEM_VARIABLES_ADMIN', 'SYSTEM_USER', - 'TABLE_ENCRYPTION_ADMIN', 'VERSION_TOKEN_ADMIN', - 'XA_RECOVER_ADMIN', 'LOAD FROM S3', 'SELECT INTO S3', - 'INVOKE LAMBDA', - 'ALTER ROUTINE', - 'BINLOG ADMIN', - 'BINLOG MONITOR', - 'BINLOG REPLAY', - 'CONNECTION ADMIN', - 'READ_ONLY ADMIN', - 'REPLICATION MASTER ADMIN', - 'REPLICATION SLAVE ADMIN', - 'SET USER', - 'SHOW_ROUTINE', - 'SLAVE MONITOR', - 'REPLICA MONITOR',)) +EXTRA_PRIVS = ['ALL', 'ALL PRIVILEGES', 'GRANT', 'REQUIRESSL'] class InvalidPrivsError(Exception): @@ -141,6 +107,13 @@ def get_tls_requires(cursor, user, host): return requires or None +def get_valid_privs(cursor): + cursor.execute("SHOW PRIVILEGES") + show_privs = [priv[0].upper() for priv in cursor.fetchall()] + all_privs = show_privs + EXTRA_PRIVS + return frozenset(all_privs) + + 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] @@ -583,7 +556,7 @@ def sort_column_order(statement): return '%s(%s)' % (priv_name, ', '.join(columns)) -def privileges_unpack(priv, mode): +def privileges_unpack(priv, mode, valid_privs): """ 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 @@ -630,8 +603,8 @@ def privileges_unpack(priv, mode): output[pieces[0]] = normalize_col_grants(output[pieces[0]]) new_privs = frozenset(privs) - if not new_privs.issubset(VALID_PRIVS): - raise InvalidPrivsError('Invalid privileges specified: %s' % new_privs.difference(VALID_PRIVS)) + if not new_privs.issubset(valid_privs): + raise InvalidPrivsError('Invalid privileges specified: %s' % new_privs.difference(valid_privs)) if '*.*' not in output: output['*.*'] = ['USAGE'] @@ -859,8 +832,8 @@ 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 mysqluser - impl = mysqluser - else: - from ansible_collections.community.mysql.plugins.module_utils.implementations.mysql import user as mariauser + 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 diff --git a/plugins/modules/mysql_role.py b/plugins/modules/mysql_role.py index 80d0144..47b0b58 100644 --- a/plugins/modules/mysql_role.py +++ b/plugins/modules/mysql_role.py @@ -250,6 +250,7 @@ from ansible_collections.community.mysql.plugins.module_utils.user import ( get_mode, user_mod, privileges_grant, + get_valid_privs, privileges_unpack, ) from ansible.module_utils._text import to_native @@ -1013,7 +1014,8 @@ def main(): module.fail_json(msg=to_native(e)) try: - priv = privileges_unpack(priv, mode) + valid_privs = get_valid_privs(cursor) + priv = privileges_unpack(priv, mode, valid_privs) except Exception as e: module.fail_json(msg='Invalid privileges string: %s' % to_native(e)) diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index 3172553..9c043cc 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -318,6 +318,7 @@ from ansible_collections.community.mysql.plugins.module_utils.user import ( handle_requiressl_in_priv_string, InvalidPrivsError, limit_resources, + get_valid_privs, privileges_unpack, sanitize_requires, user_add, @@ -421,7 +422,8 @@ def main(): except Exception as e: module.fail_json(msg=to_native(e)) try: - priv = privileges_unpack(priv, mode) + valid_privs = get_valid_privs(cursor) + priv = privileges_unpack(priv, mode, valid_privs) except Exception as e: module.fail_json(msg="invalid privileges string: %s" % to_native(e)) From a6bacefc4127d3dcd2962c15a3574a4c7d0eb4b9 Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Thu, 23 Sep 2021 14:39:01 +0300 Subject: [PATCH 18/43] Release 2.3.0 commit (#220) --- changelogs/CHANGELOG.rst | 20 +++++++++++++++++++ changelogs/changelog.yaml | 17 ++++++++++++++++ ..._info_fix_failure_when_no_tables_in_db.yml | 2 -- ...-get-valid-privs-from-show-privilegees.yml | 2 -- galaxy.yml | 2 +- 5 files changed, 38 insertions(+), 5 deletions(-) delete mode 100644 changelogs/fragments/205-mysql_info_fix_failure_when_no_tables_in_db.yml delete mode 100644 changelogs/fragments/217-mysql-user-replace-get-valid-privs-from-show-privilegees.yml diff --git a/changelogs/CHANGELOG.rst b/changelogs/CHANGELOG.rst index aacd828..71ce87d 100644 --- a/changelogs/CHANGELOG.rst +++ b/changelogs/CHANGELOG.rst @@ -5,6 +5,26 @@ Community MySQL Collection Release Notes .. contents:: Topics +v2.3.0 +====== + +Release Summary +--------------- + +This is the minor release of the ``community.mysql`` collection. +This changelog contains all changes to the modules in this collection that +have been added after the release of ``community.mysql`` 2.2.0. + +Minor Changes +------------- + +- mysql_user - replace VALID_PRIVS constant by get_valid_privs() function (https://github.com/ansible-collections/community.mysql/pull/217). + +Bugfixes +-------- + +- mysql_info - fix TypeError failure when there are databases that do not contain tables (https://github.com/ansible-collections/community.mysql/issues/204). + v2.2.0 ====== diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 63d8d87..efa0c2d 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -335,3 +335,20 @@ releases: name: mysql_role namespace: '' release_date: '2021-08-11' + 2.3.0: + changes: + bugfixes: + - mysql_info - fix TypeError failure when there are databases that do not contain + tables (https://github.com/ansible-collections/community.mysql/issues/204). + minor_changes: + - mysql_user - replace VALID_PRIVS constant by get_valid_privs() function (https://github.com/ansible-collections/community.mysql/pull/217). + release_summary: 'This is the minor release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules in this collection that + + have been added after the release of ``community.mysql`` 2.2.0.' + fragments: + - 2.3.0.yml + - 205-mysql_info_fix_failure_when_no_tables_in_db.yml + - 217-mysql-user-replace-get-valid-privs-from-show-privilegees.yml + release_date: '2021-09-23' diff --git a/changelogs/fragments/205-mysql_info_fix_failure_when_no_tables_in_db.yml b/changelogs/fragments/205-mysql_info_fix_failure_when_no_tables_in_db.yml deleted file mode 100644 index fd9260e..0000000 --- a/changelogs/fragments/205-mysql_info_fix_failure_when_no_tables_in_db.yml +++ /dev/null @@ -1,2 +0,0 @@ -bugfixes: -- mysql_info - fix TypeError failure when there are databases that do not contain tables (https://github.com/ansible-collections/community.mysql/issues/204). diff --git a/changelogs/fragments/217-mysql-user-replace-get-valid-privs-from-show-privilegees.yml b/changelogs/fragments/217-mysql-user-replace-get-valid-privs-from-show-privilegees.yml deleted file mode 100644 index 60cd451..0000000 --- a/changelogs/fragments/217-mysql-user-replace-get-valid-privs-from-show-privilegees.yml +++ /dev/null @@ -1,2 +0,0 @@ -minor_changes: - - mysql_user - replace VALID_PRIVS constant by get_valid_privs() function (https://github.com/ansible-collections/community.mysql/pull/217). diff --git a/galaxy.yml b/galaxy.yml index bc6ce0e..cfe62f1 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,6 +1,6 @@ namespace: community name: mysql -version: 2.2.0 +version: 2.3.0 readme: README.md authors: - Ansible community From 55a8ecd64efc0f18efff1943964fb9453f6250e7 Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Tue, 19 Oct 2021 13:20:30 +0300 Subject: [PATCH 19/43] [stable-2] Backport stable 2 5 (#235) * Copy ignore-2.12.txt to ignore-2.13.txt (#225) (cherry picked from commit 4f205ef540baef1ea059f0591570eaf1524890be) * CI matrix update (#226) * CI matrix update * Fix test_mysql_user * Fix CI * Fix CI * Fix CI * Fix CI * Fix CI (cherry picked from commit fc984b28aa06f916e380381a51fea3b736d54d37) * integration tests: remove superfluous debug task (#228) * integration tests: remove superfluous debug task * Turn off integration tests against devel (cherry picked from commit f47d4635f13ac2d642c678187b8321b613a18c15) * mysql_user: fix broken compatibility for priviledge aliases (#233) * mysql_user: fix broken compatibility for priviledge aliases * add changelog fragment * fix changelog fragment * Improve formatting (cherry picked from commit bb3e9fd3fa5d6fe62241b87bf38960330856e0ea) --- .github/workflows/ansible-test-plugins.yml | 15 +++++++ .../233-mysql_user_return_valid_privs.yml | 2 + plugins/module_utils/user.py | 43 ++++++++++++++++++- .../targets/setup_mysql/tasks/install.yml | 17 ++++++++ .../tasks/config_overrides_defaults.yml | 13 +++--- .../targets/test_mysql_db/tasks/issue-28.yml | 7 +-- .../test_mysql_info/tasks/issue-28.yml | 7 +-- .../test_mysql_query/tasks/issue-28.yml | 7 +-- .../test_mysql_replication/tasks/issue-28.yml | 7 +-- .../test_mysql_user/tasks/issue-28.yml | 7 +-- .../test_mysql_variables/tasks/issue-28.yml | 7 +-- .../tasks/mysql_variables.yml | 7 +-- tests/sanity/ignore-2.13.txt | 8 ++++ 13 files changed, 104 insertions(+), 43 deletions(-) create mode 100644 changelogs/fragments/233-mysql_user_return_valid_privs.yml create mode 100644 tests/sanity/ignore-2.13.txt diff --git a/.github/workflows/ansible-test-plugins.yml b/.github/workflows/ansible-test-plugins.yml index da79c04..64435cd 100644 --- a/.github/workflows/ansible-test-plugins.yml +++ b/.github/workflows/ansible-test-plugins.yml @@ -28,6 +28,7 @@ jobs: - stable-2.9 - stable-2.10 - stable-2.11 + - stable-2.12 - devel steps: @@ -61,9 +62,11 @@ jobs: - stable-2.9 - stable-2.10 - stable-2.11 + - stable-2.12 #- devel python: - 3.6 + - 3.8 connector: - pymysql==0.7.10 - pymysql==0.9.3 @@ -71,6 +74,17 @@ jobs: exclude: - mysql: 8.0.22 connector: pymysql==0.7.10 + - python: 3.8 + ansible: stable-2.9 + - python: 3.8 + ansible: stable-2.10 + - python: 3.8 + ansible: stable-2.11 + - python: 3.6 + ansible: stable-2.12 + - python: 3.6 + ansible: devel + steps: - name: Check out code @@ -116,6 +130,7 @@ jobs: - stable-2.9 - stable-2.10 - stable-2.11 + - stable-2.12 - devel steps: diff --git a/changelogs/fragments/233-mysql_user_return_valid_privs.yml b/changelogs/fragments/233-mysql_user_return_valid_privs.yml new file mode 100644 index 0000000..4f4b23e --- /dev/null +++ b/changelogs/fragments/233-mysql_user_return_valid_privs.yml @@ -0,0 +1,2 @@ +bugfixes: + - mysql_user - Fix crash reporting ``Invalid privileges specified`` when passing privileges that became aliases (https://github.com/ansible-collections/community.mysql/issues/232). diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index 2ba60f2..0532de9 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -21,6 +21,46 @@ from ansible_collections.community.mysql.plugins.module_utils.mysql import ( EXTRA_PRIVS = ['ALL', 'ALL PRIVILEGES', 'GRANT', 'REQUIRESSL'] +# This list is kept for backwards compatibility after release 2.3.0, +# see https://github.com/ansible-collections/community.mysql/issues/232 for details +VALID_PRIVS = [ + 'CREATE', 'DROP', 'GRANT', 'GRANT OPTION', + 'LOCK TABLES', 'REFERENCES', 'EVENT', 'ALTER', + 'DELETE', 'INDEX', 'INSERT', 'SELECT', 'UPDATE', + 'CREATE TEMPORARY TABLES', 'TRIGGER', 'CREATE VIEW', + 'SHOW VIEW', 'ALTER ROUTINE', 'CREATE ROUTINE', + 'EXECUTE', 'FILE', 'CREATE TABLESPACE', 'CREATE USER', + 'PROCESS', 'PROXY', 'RELOAD', 'REPLICATION CLIENT', + 'REPLICATION SLAVE', 'SHOW DATABASES', 'SHUTDOWN', + 'SUPER', 'ALL', 'ALL PRIVILEGES', 'USAGE', + 'REQUIRESSL', # Deprecated, to be removed in version 3.0.0 + 'CREATE ROLE', 'DROP ROLE', 'APPLICATION_PASSWORD_ADMIN', + 'AUDIT_ADMIN', 'BACKUP_ADMIN', 'BINLOG_ADMIN', + 'BINLOG_ENCRYPTION_ADMIN', 'CLONE_ADMIN', 'CONNECTION_ADMIN', + 'ENCRYPTION_KEY_ADMIN', 'FIREWALL_ADMIN', 'FIREWALL_USER', + 'GROUP_REPLICATION_ADMIN', 'INNODB_REDO_LOG_ARCHIVE', + 'NDB_STORED_USER', 'PERSIST_RO_VARIABLES_ADMIN', + 'REPLICATION_APPLIER', 'REPLICATION_SLAVE_ADMIN', + 'RESOURCE_GROUP_ADMIN', 'RESOURCE_GROUP_USER', + 'ROLE_ADMIN', 'SESSION_VARIABLES_ADMIN', 'SET_USER_ID', + 'SYSTEM_USER', 'SYSTEM_VARIABLES_ADMIN', 'SYSTEM_USER', + 'TABLE_ENCRYPTION_ADMIN', 'VERSION_TOKEN_ADMIN', + 'XA_RECOVER_ADMIN', 'LOAD FROM S3', 'SELECT INTO S3', + 'INVOKE LAMBDA', + 'ALTER ROUTINE', + 'BINLOG ADMIN', + 'BINLOG MONITOR', + 'BINLOG REPLAY', + 'CONNECTION ADMIN', + 'READ_ONLY ADMIN', + 'REPLICATION MASTER ADMIN', + 'REPLICATION SLAVE ADMIN', + 'SET USER', + 'SHOW_ROUTINE', + 'SLAVE MONITOR', + 'REPLICA MONITOR', +] + class InvalidPrivsError(Exception): pass @@ -110,7 +150,8 @@ def get_tls_requires(cursor, user, host): def get_valid_privs(cursor): cursor.execute("SHOW PRIVILEGES") show_privs = [priv[0].upper() for priv in cursor.fetchall()] - all_privs = show_privs + EXTRA_PRIVS + # See the comment above VALID_PRIVS declaration + all_privs = show_privs + EXTRA_PRIVS + VALID_PRIVS return frozenset(all_privs) diff --git a/tests/integration/targets/setup_mysql/tasks/install.yml b/tests/integration/targets/setup_mysql/tasks/install.yml index aacdddc..57e4b31 100644 --- a/tests/integration/targets/setup_mysql/tasks/install.yml +++ b/tests/integration/targets/setup_mysql/tasks/install.yml @@ -28,6 +28,23 @@ - name: "{{ role_name }} | install | install python packages" pip: name: "{{ python_packages }}" + register: connector + +- name: Extract connector.name.0 content + set_fact: + connector_name: "{{ connector.name.0 }}" + +- name: Debug connector_name content + debug: + msg: '{{ connector_name }}' + +- name: Extract connector version + set_fact: + connector_ver: "{{ connector_name.split('=')[2].strip() }}" + +- name: Debug connector_ver var content + debug: + msg: '{{ connector_ver }}' - name: "{{ role_name }} | install | install packages required by mysql" apt: diff --git a/tests/integration/targets/test_mysql_db/tasks/config_overrides_defaults.yml b/tests/integration/targets/test_mysql_db/tasks/config_overrides_defaults.yml index 42d8fd7..90c72b5 100644 --- a/tests/integration/targets/test_mysql_db/tasks/config_overrides_defaults.yml +++ b/tests/integration/targets/test_mysql_db/tasks/config_overrides_defaults.yml @@ -11,27 +11,26 @@ - name: Add fake port to config file shell: 'echo "port = {{ fake_port }}" >> {{ config_file }}' -- name: Get pymysql version - shell: pip show pymysql | awk '/Version/ {print $2}' - register: pymysql_version - - name: Add blank line shell: 'echo "" >> {{ config_file }}' - when: (pymysql_version.stdout | default('1000', true)) is version('0.9.3', '>=') + when: + - (connector.name.0 is search('pymysql') and connector_ver is version('0.9.3', '>=')) or connector.name.0 is not search('pymysql') - name: Create include_dir file: path: '{{ include_dir }}' state: directory mode: '0777' - when: (pymysql_version.stdout | default('1000', true)) is version('0.9.3', '>=') + when: + - (connector.name.0 is search('pymysql') and connector_ver is version('0.9.3', '>=')) or connector.name.0 is not search('pymysql') - name: Add include_dir lineinfile: path: '{{ config_file }}' line: '!includedir {{ include_dir }}' insertafter: EOF - when: (pymysql_version.stdout | default('1000', true)) is version('0.9.3', '>=') + when: + - (connector.name.0 is search('pymysql') and connector_ver is version('0.9.3', '>=')) or connector.name.0 is not search('pymysql') - name: Create database using fake port to connect to, must fail mysql_db: diff --git a/tests/integration/targets/test_mysql_db/tasks/issue-28.yml b/tests/integration/targets/test_mysql_db/tasks/issue-28.yml index 871e92d..a0b037f 100644 --- a/tests/integration/targets/test_mysql_db/tasks/issue-28.yml +++ b/tests/integration/targets/test_mysql_db/tasks/issue-28.yml @@ -9,9 +9,6 @@ block: # ============================================================ - - shell: pip show pymysql | awk '/Version/ {print $2}' - register: pymysql_version - - name: get server certificate copy: content: "{{ lookup('pipe', \"openssl s_client -starttls mysql -connect localhost:3307 -showcerts 2>/dev/null /dev/null /dev/null /dev/null /dev/null /dev/null Date: Tue, 19 Oct 2021 13:41:22 +0300 Subject: [PATCH 20/43] Release 2.3.1 commit (#236) --- changelogs/CHANGELOG.rst | 15 +++++++++++++++ changelogs/changelog.yaml | 14 ++++++++++++++ .../233-mysql_user_return_valid_privs.yml | 2 -- galaxy.yml | 2 +- 4 files changed, 30 insertions(+), 3 deletions(-) delete mode 100644 changelogs/fragments/233-mysql_user_return_valid_privs.yml diff --git a/changelogs/CHANGELOG.rst b/changelogs/CHANGELOG.rst index 71ce87d..e1d5af3 100644 --- a/changelogs/CHANGELOG.rst +++ b/changelogs/CHANGELOG.rst @@ -5,6 +5,21 @@ Community MySQL Collection Release Notes .. contents:: Topics +v2.3.1 +====== + +Release Summary +--------------- + +This is the patch release of the ``community.mysql`` collection. +This changelog contains all changes to the modules in this collection that +have been added after the release of ``community.mysql`` 2.3.0. + +Bugfixes +-------- + +- mysql_user - Fix crash reporting ``Invalid privileges specified`` when passing privileges that became aliases (https://github.com/ansible-collections/community.mysql/issues/232). + v2.3.0 ====== diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index efa0c2d..085081b 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -352,3 +352,17 @@ releases: - 205-mysql_info_fix_failure_when_no_tables_in_db.yml - 217-mysql-user-replace-get-valid-privs-from-show-privilegees.yml release_date: '2021-09-23' + 2.3.1: + changes: + bugfixes: + - mysql_user - Fix crash reporting ``Invalid privileges specified`` when passing + privileges that became aliases (https://github.com/ansible-collections/community.mysql/issues/232). + release_summary: 'This is the patch release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules in this collection that + + have been added after the release of ``community.mysql`` 2.3.0.' + fragments: + - 2.3.1.yml + - 233-mysql_user_return_valid_privs.yml + release_date: '2021-10-19' diff --git a/changelogs/fragments/233-mysql_user_return_valid_privs.yml b/changelogs/fragments/233-mysql_user_return_valid_privs.yml deleted file mode 100644 index 4f4b23e..0000000 --- a/changelogs/fragments/233-mysql_user_return_valid_privs.yml +++ /dev/null @@ -1,2 +0,0 @@ -bugfixes: - - mysql_user - Fix crash reporting ``Invalid privileges specified`` when passing privileges that became aliases (https://github.com/ansible-collections/community.mysql/issues/232). diff --git a/galaxy.yml b/galaxy.yml index cfe62f1..50c44a1 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,6 +1,6 @@ namespace: community name: mysql -version: 2.3.0 +version: 2.3.1 readme: README.md authors: - Ansible community From 7aab5cc04f896a8654fd232847b9f71e64605f38 Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Mon, 29 Nov 2021 13:50:08 +0300 Subject: [PATCH 21/43] Backport of PRs to stable-2 (#251) * Allow the "%" character in database name (#227) The naming rules for MySQL/MariaDB identifiers, when quoted, allow the `%` character. However, currently, the use of the `%` character in database names results in mismatch or missing databases. - Rewrite query to identify the databases in the catalog using `information_schema` instead of `SHOW DATABASES LIKE` - Escape the `%` character in `CREATE DATABASE` query. Signed-off-by: Nicolas Payart (cherry picked from commit 6b12435b2b4f21e724cb24f6ca0d988681824fb6) * mysql_db: Fix assert in tests suite (#239) Signed-off-by: Nicolas Payart (cherry picked from commit 5522e45284a72cf4b306ba74ed22d02e25f00390) * mysql_db: Improve tests (#240) - Define variables "db_names" and "db_formats" in defaults - Use of the "vars" option in includes instead of default parameters that might be overridden by a previous task - Use of the "loop" option in includes instead of duplicating include tasks - Use a nested loop on db_names and db_formats in state_dump_import test Signed-off-by: Nicolas Payart (cherry picked from commit e4de13aabee3990216e844181c46afbd49d6de08) * MAINTAINERS file: Add new maintainer (#248) (cherry picked from commit d411a8e2167cd808fa6c755420b1cddac56e501d) Co-authored-by: Nicolas PAYART --- MAINTAINERS | 3 +- .../fragments/227-db-create-special-name.yaml | 2 + plugins/modules/mysql_db.py | 5 +- .../targets/test_mysql_db/defaults/main.yml | 12 +- .../tasks/encoding_dump_import.yml | 3 +- .../targets/test_mysql_db/tasks/main.yml | 325 ++---------------- .../tasks/state_present_absent.yml | 300 ++++++++++++++++ 7 files changed, 352 insertions(+), 298 deletions(-) create mode 100644 changelogs/fragments/227-db-create-special-name.yaml create mode 100644 tests/integration/targets/test_mysql_db/tasks/state_present_absent.yml diff --git a/MAINTAINERS b/MAINTAINERS index c520538..97d0030 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -1,3 +1,4 @@ bmalynovytch Jorge-Rodriguez -Andersson007 (andersson007_ in #ansible-community IRC) +rsicart +Andersson007 (andersson007_ in #ansible-community IRC/Matrix) diff --git a/changelogs/fragments/227-db-create-special-name.yaml b/changelogs/fragments/227-db-create-special-name.yaml new file mode 100644 index 0000000..99f7e96 --- /dev/null +++ b/changelogs/fragments/227-db-create-special-name.yaml @@ -0,0 +1,2 @@ +bugfixes: + - mysql_db - Fix mismatch when database name contains a ``%`` character (https://github.com/ansible-collections/community.mysql/pull/227). diff --git a/plugins/modules/mysql_db.py b/plugins/modules/mysql_db.py index aa9ade0..c2a6fd8 100644 --- a/plugins/modules/mysql_db.py +++ b/plugins/modules/mysql_db.py @@ -330,7 +330,7 @@ executed_commands = [] def db_exists(cursor, db): res = 0 for each_db in db: - res += cursor.execute("SHOW DATABASES LIKE %s", (each_db.replace("_", r"\_"),)) + res += cursor.execute("SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = %s", (each_db,)) return res == len(db) @@ -519,7 +519,8 @@ def db_create(cursor, db, encoding, collation): query_params = dict(enc=encoding, collate=collation) res = 0 for each_db in db: - query = ['CREATE DATABASE %s' % mysql_quote_identifier(each_db, 'database')] + # 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: diff --git a/tests/integration/targets/test_mysql_db/defaults/main.yml b/tests/integration/targets/test_mysql_db/defaults/main.yml index b6ae780..6448e15 100644 --- a/tests/integration/targets/test_mysql_db/defaults/main.yml +++ b/tests/integration/targets/test_mysql_db/defaults/main.yml @@ -4,7 +4,17 @@ mysql_user: root mysql_password: msandbox mysql_primary_port: 3307 -db_name: 'data' +# Database names +db_names: + - "data" + - "db%" + +# Database formats +db_formats: + - { format_type: "sql", file: "dbdata.sql", format_msg_type: "ASCII", file2: "dump2.sql", file3: "dump3.sql", file4: "dump4.sql" } + - { format_type: "gz", file: "dbdata.gz", format_msg_type: "gzip", file2: "dump2.gz", file3: "dump3.gz", file4: "dump4.gz" } + - { format_type: "bz2", file: "dbdata.bz2", format_msg_type: "bzip2", file2: "dump2.bz2", file3: "dump3.bz2", file4: "dump4.bz2" } + db_name2: 'data2' db_user1: 'datauser1' db_user2: 'datauser2' diff --git a/tests/integration/targets/test_mysql_db/tasks/encoding_dump_import.yml b/tests/integration/targets/test_mysql_db/tasks/encoding_dump_import.yml index 173386c..9ef3af5 100644 --- a/tests/integration/targets/test_mysql_db/tasks/encoding_dump_import.yml +++ b/tests/integration/targets/test_mysql_db/tasks/encoding_dump_import.yml @@ -45,7 +45,7 @@ encoding: latin1 target: "{{ latin1_file1 }}" state: dump - register: dump_result + register: result - assert: that: @@ -78,6 +78,7 @@ encoding: latin1 name: '{{ db_latin1_name }}' target: "{{ latin1_file1 }}" + register: result - assert: that: diff --git a/tests/integration/targets/test_mysql_db/tasks/main.yml b/tests/integration/targets/test_mysql_db/tasks/main.yml index 139d5bb..958e341 100644 --- a/tests/integration/targets/test_mysql_db/tasks/main.yml +++ b/tests/integration/targets/test_mysql_db/tasks/main.yml @@ -21,306 +21,45 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -# ============================================================ - name: alias mysql command to include default options set_fact: mysql_command: "mysql -u{{ mysql_user }} -p{{ mysql_password }} -P{{ mysql_primary_port }} --protocol=tcp" -- name: remove database if it exists - command: > - "{{ mysql_command }} -sse 'drop database {{ db_name }}'" - ignore_errors: True +- name: Check state present/absent + include_tasks: state_present_absent.yml + vars: + db_name: "{{ item }}" + loop: "{{ db_names }}" -- name: make sure the test database is not there - command: "{{ mysql_command }} {{ db_name }}" - register: mysql_db_check - failed_when: "'1049' not in mysql_db_check.stderr" +- name: Check state dump/import + include_tasks: state_dump_import.yml + vars: + db_name: "{{ item.0 }}" + file: "{{ item.1.file }}" + file2: "{{ item.1.file2 }}" + file3: "{{ item.1.file3 }}" + file4: "{{ item.1.file4 }}" + format_msg_type: "{{ item.1.format_msg_type }}" + format_type: "{{ item.1.format_type }}" + with_nested: + - "{{ db_names }}" + - "{{ db_formats }}" -- name: test state=present for a database name (expect changed=true) - mysql_db: - login_user: '{{ mysql_user }}' - login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 - login_port: '{{ mysql_primary_port }}' - name: '{{ db_name }}' - state: present - register: result +- name: Check state present/absent with multiple databases + include_tasks: multi_db_create_delete.yml -- name: assert output message that database exist - assert: - that: - - result is changed - - result.db == '{{ db_name }}' - - result.executed_commands == ["CREATE DATABASE `{{ db_name }}`"] +- name: Check state dump/import with encoding + include_tasks: encoding_dump_import.yml + vars: + file: "latin1.sql" + format_msg_type: "ASCII" -- name: run command to test state=present for a database name (expect db_name in stdout) - command: "{{ mysql_command }} -e \"show databases like '{{ db_name }}'\"" - register: result - -- name: assert database exist - assert: - that: - - "'{{ db_name }}' in result.stdout" - -# ============================================================ -- name: test state=absent for a database name (expect changed=true) - mysql_db: - login_user: '{{ mysql_user }}' - login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 - login_port: '{{ mysql_primary_port }}' - name: '{{ db_name }}' - state: absent - register: result - -- name: assert output message that database does not exist - assert: - that: - - result is changed - - result.db == '{{ db_name }}' - - result.executed_commands == ["DROP DATABASE `{{ db_name }}`"] - -- name: run command to test state=absent for a database name (expect db_name not in stdout) - command: "{{ mysql_command }} -e \"show databases like '{{ db_name }}'\"" - register: result - -- name: assert database does not exist - assert: - that: - - "'{{ db_name }}' not in result.stdout" - -# ============================================================ -- name: test mysql_db encoding param not valid - issue 8075 - mysql_db: - login_user: '{{ mysql_user }}' - login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 - login_port: '{{ mysql_primary_port }}' - name: datanotvalid - state: present - encoding: notvalid - register: result - ignore_errors: true - -- name: assert test mysql_db encoding param not valid - issue 8075 (failed=true) - assert: - that: - - "result.failed == true" - - "'Traceback' not in result.msg" - - "'Unknown character set' in result.msg" - -# ============================================================ -- name: test mysql_db using a valid encoding utf8 (expect changed=true) - mysql_db: - login_user: '{{ mysql_user }}' - login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 - login_port: '{{ mysql_primary_port }}' - name: 'en{{ db_name }}' - state: present - encoding: utf8 - register: result - -- name: assert output message created a database - assert: - that: - - result is changed - - result.executed_commands == ["CREATE DATABASE `en{{ db_name }}` CHARACTER SET 'utf8'"] - -- name: test database was created - command: "{{ mysql_command }} -e \"SHOW CREATE DATABASE en{{ db_name }}\"" - register: result - -- name: assert created database is of encoding utf8 - assert: - that: - - "'utf8' in result.stdout" - -- name: remove database - mysql_db: - login_user: '{{ mysql_user }}' - login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 - login_port: '{{ mysql_primary_port }}' - name: 'en{{ db_name }}' - state: absent - -# ============================================================ -- name: test mysql_db using valid encoding binary (expect changed=true) - mysql_db: - login_user: '{{ mysql_user }}' - login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 - login_port: '{{ mysql_primary_port }}' - name: 'en{{ db_name }}' - state: present - encoding: binary - register: result - -- name: assert output message that database was created - assert: - that: - - result is changed - - result.executed_commands == ["CREATE DATABASE `en{{ db_name }}` CHARACTER SET 'binary'"] - -- name: run command to test database was created - command: "{{ mysql_command }} -e \"SHOW CREATE DATABASE en{{ db_name }}\"" - register: result - -- name: assert created database is of encoding binary - assert: - that: - - "'binary' in result.stdout" - -- name: remove database - mysql_db: - login_user: '{{ mysql_user }}' - login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 - login_port: '{{ mysql_primary_port }}' - name: 'en{{ db_name }}' - state: absent - -# ============================================================ -- name: create user1 to access database dbuser1 - mysql_user: - login_user: '{{ mysql_user }}' - login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 - login_port: '{{ mysql_primary_port }}' - name: user1 - password: 'Hfd6fds^dfA8Ga' - priv: '*.*:ALL' - state: present - -- name: create database dbuser1 using user1 - mysql_db: - login_user: user1 - login_password: 'Hfd6fds^dfA8Ga' - login_host: 127.0.0.1 - login_port: '{{ mysql_primary_port }}' - name: '{{ db_user1 }}' - state: present - register: result - -- name: assert output message that database was created - assert: - that: - - "result.changed == true" - -- name: run command to test database was created using user1 - command: "{{ mysql_command }} -e \"show databases like '{{ db_user1 }}'\"" - register: result - -- name: assert database exist - assert: - that: - - "'{{ db_user1 }}' in result.stdout" - -# ============================================================ -- name: create user2 to access database with privilege select only - mysql_user: - login_user: '{{ mysql_user }}' - login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 - login_port: '{{ mysql_primary_port }}' - name: user2 - password: 'kjsfd&F7safjad' - priv: '*.*:SELECT' - state: present - -- name: create database dbuser2 using user2 with no privilege to create (expect failed=true) - mysql_db: - login_user: user2 - login_password: 'kjsfd&F7safjad' - login_host: 127.0.0.1 - login_port: '{{ mysql_primary_port }}' - name: '{{ db_user2 }}' - state: present - register: result - ignore_errors: true - -- name: assert output message that database was not created using dbuser2 - assert: - that: - - "result.failed == true" - - "'Access denied' in result.msg" - -- name: run command to test that database was not created - command: "{{ mysql_command }} -e \"show databases like '{{ db_user2 }}'\"" - register: result - -- name: assert database does not exist - assert: - that: - - "'{{ db_user2 }}' not in result.stdout" - -# ============================================================ -- name: delete database using user2 with no privilege to delete (expect failed=true) - mysql_db: - login_user: user2 - login_password: 'kjsfd&F7safjad' - login_host: 127.0.0.1 - login_port: '{{ mysql_primary_port }}' - name: '{{ db_user1 }}' - state: absent - register: result - ignore_errors: true - -- name: assert output message that database was not deleted using dbuser2 - assert: - that: - - "result.failed == true" - - "'Access denied' in result.msg" - -- name: run command to test database was not deleted - command: "{{ mysql_command }} -e \"show databases like '{{ db_user1 }}'\"" - register: result - -- name: assert database still exist - assert: - that: - - "'{{ db_user1 }}' in result.stdout" - -# ============================================================ -- name: delete database using user1 with all privilege to delete a database (expect changed=true) - mysql_db: - login_user: user1 - login_password: 'Hfd6fds^dfA8Ga' - login_host: 127.0.0.1 - login_port: '{{ mysql_primary_port }}' - name: '{{ db_user1 }}' - state: absent - register: result - ignore_errors: true - -- name: assert output message that database was deleted using user1 - assert: - that: - - result is changed - - result.executed_commands == ["DROP DATABASE `{{ db_user1 }}`"] - -- name: run command to test database was deleted using user1 - command: "{{ mysql_command }} -e \"show databases like '{{ db_name }}'\"" - register: result - -- name: assert database does not exist - assert: - that: - - "'{{ db_user1 }}' not in result.stdout" - -# ============================================================ -- include: state_dump_import.yml format_type=sql file=dbdata.sql format_msg_type=ASCII file2=dump2.sql file3=dump3.sql file4=dump4.sql - -- include: state_dump_import.yml format_type=gz file=dbdata.gz format_msg_type=gzip file2=dump2.gz file3=dump3.gz file4=dump4.gz - -- include: state_dump_import.yml format_type=bz2 file=dbdata.bz2 format_msg_type=bzip2 file2=dump2.bz2 file3=dump3.bz2 file4=dump4.bz2 - -- include: multi_db_create_delete.yml - -- include: encoding_dump_import.yml file=latin1.sql format_msg_type=ASCII - -- include: config_overrides_defaults.yml +- name: Check MySQL config file + include_tasks: config_overrides_defaults.yml when: ansible_python.version_info[0] >= 3 -- include: issue-28.yml +- name: Check issue 28 + include_tasks: issue-28.yml + vars: + db_name: "{{ item }}" + loop: "{{ db_names }}" diff --git a/tests/integration/targets/test_mysql_db/tasks/state_present_absent.yml b/tests/integration/targets/test_mysql_db/tasks/state_present_absent.yml new file mode 100644 index 0000000..02411f0 --- /dev/null +++ b/tests/integration/targets/test_mysql_db/tasks/state_present_absent.yml @@ -0,0 +1,300 @@ +# test code for mysql_db module with database name containing special chars + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# ============================================================ +- name: remove database if it exists + command: > + "{{ mysql_command }} -sse 'drop database {{ db_name }}'" + ignore_errors: True + +- name: make sure the test database is not there + command: "{{ mysql_command }} {{ db_name }}" + register: mysql_db_check + failed_when: "'1049' not in mysql_db_check.stderr" + +- name: test state=present for a database name (expect changed=true) + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: 127.0.0.1 + login_port: '{{ mysql_primary_port }}' + name: '{{ db_name }}' + state: present + register: result + +- name: assert output message that database exist + assert: + that: + - result is changed + - result.db == '{{ db_name }}' + - result.executed_commands == ["CREATE DATABASE `{{ db_name }}`"] + +- name: run command to test state=present for a database name (expect db_name in stdout) + command: "{{ mysql_command }} -e \"show databases like '{{ db_name | regex_replace(\"([%_\\\\])\", \"\\\\\\1\") }}'\"" + register: result + +- name: assert database exist + assert: + that: + - "'{{ db_name }}' in result.stdout" + +# ============================================================ +- name: test state=absent for a database name (expect changed=true) + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: 127.0.0.1 + login_port: '{{ mysql_primary_port }}' + name: '{{ db_name }}' + state: absent + register: result + +- name: assert output message that database does not exist + assert: + that: + - result is changed + - result.db == '{{ db_name }}' + - result.executed_commands == ["DROP DATABASE `{{ db_name }}`"] + +- name: run command to test state=absent for a database name (expect db_name not in stdout) + command: "{{ mysql_command }} -e \"show databases like '{{ db_name | regex_replace(\"([%_\\\\])\", \"\\\\\\1\") }}'\"" + register: result + +- name: assert database does not exist + assert: + that: + - "'{{ db_name }}' not in result.stdout" + +# ============================================================ +- name: test mysql_db encoding param not valid - issue 8075 + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: 127.0.0.1 + login_port: '{{ mysql_primary_port }}' + name: datanotvalid + state: present + encoding: notvalid + register: result + ignore_errors: true + +- name: assert test mysql_db encoding param not valid - issue 8075 (failed=true) + assert: + that: + - "result.failed == true" + - "'Traceback' not in result.msg" + - "'Unknown character set' in result.msg" + +# ============================================================ +- name: test mysql_db using a valid encoding utf8 (expect changed=true) + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: 127.0.0.1 + login_port: '{{ mysql_primary_port }}' + name: 'en{{ db_name }}' + state: present + encoding: utf8 + register: result + +- name: assert output message created a database + assert: + that: + - result is changed + - result.executed_commands == ["CREATE DATABASE `en{{ db_name }}` CHARACTER SET 'utf8'"] + +- name: test database was created + command: "{{ mysql_command }} -e \"SHOW CREATE DATABASE `en{{ db_name }}`\"" + register: result + +- name: assert created database is of encoding utf8 + assert: + that: + - "'utf8' in result.stdout" + +- name: remove database + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: 127.0.0.1 + login_port: '{{ mysql_primary_port }}' + name: 'en{{ db_name }}' + state: absent + +# ============================================================ +- name: test mysql_db using valid encoding binary (expect changed=true) + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: 127.0.0.1 + login_port: '{{ mysql_primary_port }}' + name: 'en{{ db_name }}' + state: present + encoding: binary + register: result + +- name: assert output message that database was created + assert: + that: + - result is changed + - result.executed_commands == ["CREATE DATABASE `en{{ db_name }}` CHARACTER SET 'binary'"] + +- name: run command to test database was created + command: "{{ mysql_command }} -e \"SHOW CREATE DATABASE `en{{ db_name }}`\"" + register: result + +- name: assert created database is of encoding binary + assert: + that: + - "'binary' in result.stdout" + +- name: remove database + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: 127.0.0.1 + login_port: '{{ mysql_primary_port }}' + name: 'en{{ db_name }}' + state: absent + +# ============================================================ +- name: create user1 to access database dbuser1 + mysql_user: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: 127.0.0.1 + login_port: '{{ mysql_primary_port }}' + name: user1 + password: 'Hfd6fds^dfA8Ga' + priv: '*.*:ALL' + state: present + +- name: create database dbuser1 using user1 + mysql_db: + login_user: user1 + login_password: 'Hfd6fds^dfA8Ga' + login_host: 127.0.0.1 + login_port: '{{ mysql_primary_port }}' + name: '{{ db_user1 }}' + state: present + register: result + +- name: assert output message that database was created + assert: + that: + - "result.changed == true" + +- name: run command to test database was created using user1 + command: "{{ mysql_command }} -e \"show databases like '{{ db_user1 | regex_replace(\"([%_\\\\])\", \"\\\\\\1\") }}'\"" + register: result + +- name: assert database exist + assert: + that: + - "'{{ db_user1 }}' in result.stdout" + +# ============================================================ +- name: create user2 to access database with privilege select only + mysql_user: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: 127.0.0.1 + login_port: '{{ mysql_primary_port }}' + name: user2 + password: 'kjsfd&F7safjad' + priv: '*.*:SELECT' + state: present + +- name: create database dbuser2 using user2 with no privilege to create (expect failed=true) + mysql_db: + login_user: user2 + login_password: 'kjsfd&F7safjad' + login_host: 127.0.0.1 + login_port: '{{ mysql_primary_port }}' + name: '{{ db_user2 }}' + state: present + register: result + ignore_errors: true + +- name: assert output message that database was not created using dbuser2 + assert: + that: + - "result.failed == true" + - "'Access denied' in result.msg" + +- name: run command to test that database was not created + command: "{{ mysql_command }} -e \"show databases like '{{ db_user2 | regex_replace(\"([%_\\\\])\", \"\\\\\\1\") }}'\"" + register: result + +- name: assert database does not exist + assert: + that: + - "'{{ db_user2 }}' not in result.stdout" + +# ============================================================ +- name: delete database using user2 with no privilege to delete (expect failed=true) + mysql_db: + login_user: user2 + login_password: 'kjsfd&F7safjad' + login_host: 127.0.0.1 + login_port: '{{ mysql_primary_port }}' + name: '{{ db_user1 }}' + state: absent + register: result + ignore_errors: true + +- name: assert output message that database was not deleted using dbuser2 + assert: + that: + - "result.failed == true" + - "'Access denied' in result.msg" + +- name: run command to test database was not deleted + command: "{{ mysql_command }} -e \"show databases like '{{ db_user1 | regex_replace(\"([%_\\\\])\", \"\\\\\\1\") }}'\"" + register: result + +- name: assert database still exist + assert: + that: + - "'{{ db_user1 }}' in result.stdout" + +# ============================================================ +- name: delete database using user1 with all privilege to delete a database (expect changed=true) + mysql_db: + login_user: user1 + login_password: 'Hfd6fds^dfA8Ga' + login_host: 127.0.0.1 + login_port: '{{ mysql_primary_port }}' + name: '{{ db_user1 }}' + state: absent + register: result + ignore_errors: true + +- name: assert output message that database was deleted using user1 + assert: + that: + - result is changed + - result.executed_commands == ["DROP DATABASE `{{ db_user1 }}`"] + +- name: run command to test database was deleted using user1 + command: "{{ mysql_command }} -e \"show databases like '{{ db_name | regex_replace(\"([%_\\\\])\", \"\\\\\\1\") }}'\"" + register: result + +- name: assert database does not exist + assert: + that: + - "'{{ db_user1 }}' not in result.stdout" From a2c0bf4f0ac107e8f5c567ce4a63016f3f9665d2 Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Mon, 29 Nov 2021 17:52:22 +0300 Subject: [PATCH 22/43] Release 2.3.2 commit (#253) --- changelogs/CHANGELOG.rst | 15 +++++++++++++++ changelogs/changelog.yaml | 13 +++++++++++++ .../fragments/227-db-create-special-name.yaml | 2 -- galaxy.yml | 2 +- 4 files changed, 29 insertions(+), 3 deletions(-) delete mode 100644 changelogs/fragments/227-db-create-special-name.yaml diff --git a/changelogs/CHANGELOG.rst b/changelogs/CHANGELOG.rst index e1d5af3..f8fcb6d 100644 --- a/changelogs/CHANGELOG.rst +++ b/changelogs/CHANGELOG.rst @@ -5,6 +5,21 @@ Community MySQL Collection Release Notes .. contents:: Topics +v2.3.2 +====== + +Release Summary +--------------- + +This is the patch release of the ``community.mysql`` collection. +This changelog contains all changes to the modules in this collection that +have been added after the release of ``community.mysql`` 2.3.1. + +Bugfixes +-------- + +- mysql_db - Fix mismatch when database name contains a ``%`` character (https://github.com/ansible-collections/community.mysql/pull/227). + v2.3.1 ====== diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 085081b..5edc3aa 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -366,3 +366,16 @@ releases: - 2.3.1.yml - 233-mysql_user_return_valid_privs.yml release_date: '2021-10-19' + 2.3.2: + changes: + bugfixes: + - mysql_db - Fix mismatch when database name contains a ``%`` character (https://github.com/ansible-collections/community.mysql/pull/227). + release_summary: 'This is the patch release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules in this collection that + + have been added after the release of ``community.mysql`` 2.3.1.' + fragments: + - 2.3.2.yml + - 227-db-create-special-name.yaml + release_date: '2021-11-29' diff --git a/changelogs/fragments/227-db-create-special-name.yaml b/changelogs/fragments/227-db-create-special-name.yaml deleted file mode 100644 index 99f7e96..0000000 --- a/changelogs/fragments/227-db-create-special-name.yaml +++ /dev/null @@ -1,2 +0,0 @@ -bugfixes: - - mysql_db - Fix mismatch when database name contains a ``%`` character (https://github.com/ansible-collections/community.mysql/pull/227). diff --git a/galaxy.yml b/galaxy.yml index 50c44a1..68e336d 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,6 +1,6 @@ namespace: community name: mysql -version: 2.3.1 +version: 2.3.2 readme: README.md authors: - Ansible community From 9c76f1a566c1080b15af4616f2df75f076764327 Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Tue, 18 Jan 2022 12:39:22 +0300 Subject: [PATCH 23/43] [stable-2] Use vendored version of distutils.version (#269) (#273) * Prepare for distutils.version being removed in Python 3.12 (#267) * Prepare for distutils.version being removed in Python 3.12 * Update plugins/module_utils/version.py Co-authored-by: Felix Fontein Co-authored-by: Felix Fontein (cherry picked from commit d9e12b85ad8f73b83de6a1a039ae2fe37e021225) * Use vendored version of distutils.version (#269) * Use vendored version of distutils.version * Correct fragment * Update plugins/module_utils/version.py Co-authored-by: Felix Fontein Co-authored-by: Felix Fontein (cherry picked from commit 9c575b4762d7906c0d6d1611c3d0d3681807cdd4) --- .../267-prepare_for_distutils_be_removed.yml | 2 + plugins/module_utils/_version.py | 343 ++++++++++++++++++ .../implementations/mariadb/replication.py | 2 +- .../implementations/mariadb/role.py | 2 +- .../implementations/mariadb/user.py | 2 +- .../implementations/mysql/replication.py | 2 +- .../implementations/mysql/role.py | 2 +- .../implementations/mysql/user.py | 2 +- plugins/module_utils/version.py | 16 + plugins/modules/mysql_replication.py | 2 +- 10 files changed, 368 insertions(+), 7 deletions(-) create mode 100644 changelogs/fragments/267-prepare_for_distutils_be_removed.yml create mode 100644 plugins/module_utils/_version.py create mode 100644 plugins/module_utils/version.py diff --git a/changelogs/fragments/267-prepare_for_distutils_be_removed.yml b/changelogs/fragments/267-prepare_for_distutils_be_removed.yml new file mode 100644 index 0000000..190b178 --- /dev/null +++ b/changelogs/fragments/267-prepare_for_distutils_be_removed.yml @@ -0,0 +1,2 @@ +bugfixes: +- Collection core functions - use vendored version of ``distutils.version`` instead of the deprecated Python standard library ``distutils`` (https://github.com/ansible-collections/community.mysql/pull/269). diff --git a/plugins/module_utils/_version.py b/plugins/module_utils/_version.py new file mode 100644 index 0000000..59ee9db --- /dev/null +++ b/plugins/module_utils/_version.py @@ -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 licenses/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 diff --git a/plugins/module_utils/implementations/mariadb/replication.py b/plugins/module_utils/implementations/mariadb/replication.py index 7968e92..cee4967 100644 --- a/plugins/module_utils/implementations/mariadb/replication.py +++ b/plugins/module_utils/implementations/mariadb/replication.py @@ -2,7 +2,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type from ansible_collections.community.mysql.plugins.module_utils.mysql import get_server_version -from distutils.version import LooseVersion +from ansible_collections.community.mysql.plugins.module_utils.version import LooseVersion def uses_replica_terminology(cursor): diff --git a/plugins/module_utils/implementations/mariadb/role.py b/plugins/module_utils/implementations/mariadb/role.py index 3b4c777..a3c9ea5 100644 --- a/plugins/module_utils/implementations/mariadb/role.py +++ b/plugins/module_utils/implementations/mariadb/role.py @@ -1,7 +1,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -from distutils.version import LooseVersion +from ansible_collections.community.mysql.plugins.module_utils.version import LooseVersion from ansible_collections.community.mysql.plugins.module_utils.mysql import get_server_version diff --git a/plugins/module_utils/implementations/mariadb/user.py b/plugins/module_utils/implementations/mariadb/user.py index fa2cac6..7579157 100644 --- a/plugins/module_utils/implementations/mariadb/user.py +++ b/plugins/module_utils/implementations/mariadb/user.py @@ -1,7 +1,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -from distutils.version import LooseVersion +from ansible_collections.community.mysql.plugins.module_utils.version import LooseVersion from ansible_collections.community.mysql.plugins.module_utils.mysql import get_server_version diff --git a/plugins/module_utils/implementations/mysql/replication.py b/plugins/module_utils/implementations/mysql/replication.py index 2fd3d6c..c5324da 100644 --- a/plugins/module_utils/implementations/mysql/replication.py +++ b/plugins/module_utils/implementations/mysql/replication.py @@ -2,7 +2,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type from ansible_collections.community.mysql.plugins.module_utils.mysql import get_server_version -from distutils.version import LooseVersion +from ansible_collections.community.mysql.plugins.module_utils.version import LooseVersion def uses_replica_terminology(cursor): diff --git a/plugins/module_utils/implementations/mysql/role.py b/plugins/module_utils/implementations/mysql/role.py index 42276ff..f9686c5 100644 --- a/plugins/module_utils/implementations/mysql/role.py +++ b/plugins/module_utils/implementations/mysql/role.py @@ -1,7 +1,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -from distutils.version import LooseVersion +from ansible_collections.community.mysql.plugins.module_utils.version import LooseVersion from ansible_collections.community.mysql.plugins.module_utils.mysql import get_server_version diff --git a/plugins/module_utils/implementations/mysql/user.py b/plugins/module_utils/implementations/mysql/user.py index ce7f5b8..43e400b 100644 --- a/plugins/module_utils/implementations/mysql/user.py +++ b/plugins/module_utils/implementations/mysql/user.py @@ -1,7 +1,7 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type -from distutils.version import LooseVersion +from ansible_collections.community.mysql.plugins.module_utils.version import LooseVersion from ansible_collections.community.mysql.plugins.module_utils.mysql import get_server_version diff --git a/plugins/module_utils/version.py b/plugins/module_utils/version.py new file mode 100644 index 0000000..359cceb --- /dev/null +++ b/plugins/module_utils/version.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Felix Fontein +# 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 2.9, ansible-base 2.10, and 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 diff --git a/plugins/modules/mysql_replication.py b/plugins/modules/mysql_replication.py index 12dffc4..09ae76e 100644 --- a/plugins/modules/mysql_replication.py +++ b/plugins/modules/mysql_replication.py @@ -293,7 +293,7 @@ from ansible_collections.community.mysql.plugins.module_utils.mysql import ( mysql_common_argument_spec, ) from ansible.module_utils._text import to_native -from distutils.version import LooseVersion +from ansible_collections.community.mysql.plugins.module_utils.version import LooseVersion executed_queries = [] From 9d9ea46603f786fb1f4dd9f0bd9f95f62334233e Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Tue, 18 Jan 2022 13:04:04 +0300 Subject: [PATCH 24/43] Release 2.3.3 commit (#276) --- changelogs/CHANGELOG.rst | 13 +++++++++++++ changelogs/changelog.yaml | 12 ++++++++++++ .../267-prepare_for_distutils_be_removed.yml | 2 -- galaxy.yml | 2 +- 4 files changed, 26 insertions(+), 3 deletions(-) delete mode 100644 changelogs/fragments/267-prepare_for_distutils_be_removed.yml diff --git a/changelogs/CHANGELOG.rst b/changelogs/CHANGELOG.rst index f8fcb6d..b28f49b 100644 --- a/changelogs/CHANGELOG.rst +++ b/changelogs/CHANGELOG.rst @@ -5,6 +5,19 @@ Community MySQL Collection Release Notes .. contents:: Topics +v2.3.3 +====== + +Release Summary +--------------- + +This is the patch release of the ``community.mysql`` collection. This changelog contains all changes to the modules in this collection that have been added after the release of ``community.mysql`` 2.3.2. + +Bugfixes +-------- + +- Collection core functions - use vendored version of ``distutils.version`` instead of the deprecated Python standard library ``distutils`` (https://github.com/ansible-collections/community.mysql/pull/269). + v2.3.2 ====== diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 5edc3aa..94b5d86 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -379,3 +379,15 @@ releases: - 2.3.2.yml - 227-db-create-special-name.yaml release_date: '2021-11-29' + 2.3.3: + changes: + bugfixes: + - Collection core functions - use vendored version of ``distutils.version`` + instead of the deprecated Python standard library ``distutils`` (https://github.com/ansible-collections/community.mysql/pull/269). + release_summary: This is the patch release of the ``community.mysql`` collection. + This changelog contains all changes to the modules in this collection that + have been added after the release of ``community.mysql`` 2.3.2. + fragments: + - 2.3.3.yml + - 267-prepare_for_distutils_be_removed.yml + release_date: '2022-01-18' diff --git a/changelogs/fragments/267-prepare_for_distutils_be_removed.yml b/changelogs/fragments/267-prepare_for_distutils_be_removed.yml deleted file mode 100644 index 190b178..0000000 --- a/changelogs/fragments/267-prepare_for_distutils_be_removed.yml +++ /dev/null @@ -1,2 +0,0 @@ -bugfixes: -- Collection core functions - use vendored version of ``distutils.version`` instead of the deprecated Python standard library ``distutils`` (https://github.com/ansible-collections/community.mysql/pull/269). diff --git a/galaxy.yml b/galaxy.yml index 68e336d..6dacee0 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,6 +1,6 @@ namespace: community name: mysql -version: 2.3.2 +version: 2.3.3 readme: README.md authors: - Ansible community From 8b2c418538ca03c955d51cb0846b197cee0747b9 Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Wed, 16 Feb 2022 13:11:55 +0300 Subject: [PATCH 25/43] [stable-2] Backport from main to stable-2 (#287) * Update CONTRIBUTORS file (#278) (cherry picked from commit cbdf51234a33f616292b0dd40622897e5bc4443c) * Honor the set_default_role_all parameter (#282) The set_default_role_all parameter is documented, but does nothing. This PR fixes this. (cherry picked from commit f547b66d353eeefe35650e392a68cab4eba79e20) * Add a changelog fragment to PR 282 (#283) (cherry picked from commit 952e1666d85d94d60847cb223de4f9e413c384eb) Co-authored-by: Benoit Garret --- CONTRIBUTORS | 9 +++++++++ .../282-mysql_role_fix_set_default_role_all_argument.yml | 2 ++ plugins/modules/mysql_role.py | 3 ++- 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 changelogs/fragments/282-mysql_role_fix_set_default_role_all_argument.yml diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 718caa2..cacb4ff 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -36,6 +36,7 @@ Berbe bjne bmalynovytch bmildren +bmillemathias boreal321 brutus burner1024 @@ -60,6 +61,7 @@ Davidffry denisemauldin diclophis d-lee +d-rupp dmp1ce dnelson dramaley @@ -100,6 +102,7 @@ hyperfocus1338 igormukhingmailcom imjoseangel infigoKriti +int32bit ipergenitsa iredmail ivandigiusto @@ -109,6 +112,7 @@ jamescassell janosmiko jarnold-timeout JaSafieddine +jb-2197 jborean93 jctanner jean-christophe-manciot @@ -136,10 +140,12 @@ kalaisubbiah kenichi-ogawa-1988 kkeane klingac +koleo kotso kuntalFreshBooks kurtdavis larsks +laurent-indermuehle ldesgrange leeadh LeonB @@ -218,6 +224,7 @@ robpblake rokka-n Roxyrob roysmith +rsicart rthouvenin ruudk samccann @@ -242,6 +249,7 @@ stoned strixBE SWADESNA tapologo +tarunm97 tejatsk14 tersmitten the @@ -252,6 +260,7 @@ timorunge Tomasthanes tomdymond Tronde +tuhoanganh tvlooy tyll UncertaintyP diff --git a/changelogs/fragments/282-mysql_role_fix_set_default_role_all_argument.yml b/changelogs/fragments/282-mysql_role_fix_set_default_role_all_argument.yml new file mode 100644 index 0000000..06fe4b6 --- /dev/null +++ b/changelogs/fragments/282-mysql_role_fix_set_default_role_all_argument.yml @@ -0,0 +1,2 @@ +bugfixes: +- mysql_role - make the ``set_default_role_all`` parameter actually working (https://github.com/ansible-collections/community.mysql/pull/282). diff --git a/plugins/modules/mysql_role.py b/plugins/modules/mysql_role.py index 47b0b58..cd24c27 100644 --- a/plugins/modules/mysql_role.py +++ b/plugins/modules/mysql_role.py @@ -770,7 +770,8 @@ class Role(): self.cursor.execute(*self.q_builder.role_grant(user)) - self.role_impl.set_default_role_all(user) + if set_default_role_all: + self.role_impl.set_default_role_all(user) changed = True From d7c6bddefa082e81456d24599033c121f08f31fa Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Wed, 16 Feb 2022 13:24:17 +0300 Subject: [PATCH 26/43] Release 2.3.4 commit (#288) --- changelogs/CHANGELOG.rst | 13 +++++++++++++ changelogs/changelog.yaml | 12 ++++++++++++ ...mysql_role_fix_set_default_role_all_argument.yml | 2 -- galaxy.yml | 2 +- 4 files changed, 26 insertions(+), 3 deletions(-) delete mode 100644 changelogs/fragments/282-mysql_role_fix_set_default_role_all_argument.yml diff --git a/changelogs/CHANGELOG.rst b/changelogs/CHANGELOG.rst index b28f49b..212af17 100644 --- a/changelogs/CHANGELOG.rst +++ b/changelogs/CHANGELOG.rst @@ -5,6 +5,19 @@ Community MySQL Collection Release Notes .. contents:: Topics +v2.3.4 +====== + +Release Summary +--------------- + +This is the patch release of the ``community.mysql`` collection. This changelog contains all changes to the modules in this collection that have been added after the release of ``community.mysql`` 2.3.3. + +Bugfixes +-------- + +- mysql_role - make the ``set_default_role_all`` parameter actually working (https://github.com/ansible-collections/community.mysql/pull/282). + v2.3.3 ====== diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 94b5d86..3fb24b7 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -391,3 +391,15 @@ releases: - 2.3.3.yml - 267-prepare_for_distutils_be_removed.yml release_date: '2022-01-18' + 2.3.4: + changes: + bugfixes: + - mysql_role - make the ``set_default_role_all`` parameter actually working + (https://github.com/ansible-collections/community.mysql/pull/282). + release_summary: This is the patch release of the ``community.mysql`` collection. + This changelog contains all changes to the modules in this collection that + have been added after the release of ``community.mysql`` 2.3.3. + fragments: + - 2.3.4.yml + - 282-mysql_role_fix_set_default_role_all_argument.yml + release_date: '2022-02-16' diff --git a/changelogs/fragments/282-mysql_role_fix_set_default_role_all_argument.yml b/changelogs/fragments/282-mysql_role_fix_set_default_role_all_argument.yml deleted file mode 100644 index 06fe4b6..0000000 --- a/changelogs/fragments/282-mysql_role_fix_set_default_role_all_argument.yml +++ /dev/null @@ -1,2 +0,0 @@ -bugfixes: -- mysql_role - make the ``set_default_role_all`` parameter actually working (https://github.com/ansible-collections/community.mysql/pull/282). diff --git a/galaxy.yml b/galaxy.yml index 6dacee0..692a899 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,6 +1,6 @@ namespace: community name: mysql -version: 2.3.3 +version: 2.3.4 readme: README.md authors: - Ansible community From cb960ef18263ac8ad5ababbf9b44ed53ce7eef89 Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Mon, 14 Mar 2022 13:13:17 +0300 Subject: [PATCH 27/43] Enable and fix integration tests for devel (#297) --- .github/workflows/ansible-test-plugins.yml | 2 +- tests/integration/targets/setup_mysql/defaults/main.yml | 2 +- tests/integration/targets/setup_mysql/vars/main.yml | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ansible-test-plugins.yml b/.github/workflows/ansible-test-plugins.yml index 64435cd..c492261 100644 --- a/.github/workflows/ansible-test-plugins.yml +++ b/.github/workflows/ansible-test-plugins.yml @@ -63,7 +63,7 @@ jobs: - stable-2.10 - stable-2.11 - stable-2.12 - #- devel + - devel python: - 3.6 - 3.8 diff --git a/tests/integration/targets/setup_mysql/defaults/main.yml b/tests/integration/targets/setup_mysql/defaults/main.yml index 7bcb2d2..2908c17 100644 --- a/tests/integration/targets/setup_mysql/defaults/main.yml +++ b/tests/integration/targets/setup_mysql/defaults/main.yml @@ -1,4 +1,4 @@ -dbdeployer_version: 1.52.0 +dbdeployer_version: 1.64.0 dbdeployer_home_dir: /opt/dbdeployer home_dir: /root diff --git a/tests/integration/targets/setup_mysql/vars/main.yml b/tests/integration/targets/setup_mysql/vars/main.yml index e267307..4448ac0 100644 --- a/tests/integration/targets/setup_mysql/vars/main.yml +++ b/tests/integration/targets/setup_mysql/vars/main.yml @@ -19,6 +19,7 @@ python_packages: [pymysql == 0.9.3] install_prereqs: - libaio1 - libnuma1 + - libncurses5 install_python_prereqs: - python3-dev From fc16243349e0dd2a3a29623f12be9a8511df095a Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Mon, 14 Mar 2022 13:47:28 +0300 Subject: [PATCH 28/43] Fix role integration tests for mariadb (#299) Co-authored-by: R.Sicart --- .github/workflows/ansible-test-plugins.yml | 4 + .../tasks/mysql_role_initial.yml | 465 ++++++++++++++++-- 2 files changed, 432 insertions(+), 37 deletions(-) diff --git a/.github/workflows/ansible-test-plugins.yml b/.github/workflows/ansible-test-plugins.yml index c492261..a82a624 100644 --- a/.github/workflows/ansible-test-plugins.yml +++ b/.github/workflows/ansible-test-plugins.yml @@ -74,6 +74,10 @@ jobs: exclude: - mysql: 8.0.22 connector: pymysql==0.7.10 + - db_engine_version: mariadb_10.3.34 + connector: mysqlclient==2.0.1 + - db_engine_version: mariadb_10.5.9 + connector: mysqlclient==2.0.1 - python: 3.8 ansible: stable-2.9 - python: 3.8 diff --git a/tests/integration/targets/test_mysql_role/tasks/mysql_role_initial.yml b/tests/integration/targets/test_mysql_role/tasks/mysql_role_initial.yml index ba3125c..1bca3ae 100644 --- a/tests/integration/targets/test_mysql_role/tasks/mysql_role_initial.yml +++ b/tests/integration/targets/test_mysql_role/tasks/mysql_role_initial.yml @@ -34,8 +34,7 @@ # Skip unsupported versions - meta: end_play - when: - - srv['version']['major'] < 8 + when: srv['version']['major'] < 8 ######### # Prepare @@ -92,7 +91,7 @@ <<: *task_params mysql_query: <<: *mysql_params - query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}' AND Host = '%'" + query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}'" - name: Check assert: @@ -106,11 +105,26 @@ <<: *mysql_params query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" ignore_errors: yes + when: install_type == 'mysql' - name: Check assert: that: - result is failed + when: install_type == 'mysql' + + - name: Check in DB (mariadb) + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = '{{ user0 }}' AND Host = 'localhost' AND Role = '{{ role0 }}'" + when: install_type == 'mariadb' + + - name: Check (mariadb) + assert: + that: + - result.query_result.0.0['user_roles'] == 0 + when: install_type == 'mariadb' #===================== @@ -121,7 +135,7 @@ login_password: '{{ mysql_password }}' login_host: 127.0.0.1 login_port: '{{ mysql_primary_port }}' - query: 'SELECT current_role()' + query: 'SELECT COALESCE(current_role(), "NONE") as "current_role()"' - name: Check assert: @@ -146,7 +160,7 @@ <<: *task_params mysql_query: <<: *mysql_params - query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}' AND Host = '%'" + query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}'" - name: Check assert: @@ -158,11 +172,26 @@ mysql_query: <<: *mysql_params query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" + when: install_type == 'mysql' - name: Check assert: that: - result is succeeded + when: install_type == 'mysql' + + - name: Check in DB (mariadb) + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = '{{ user0 }}' AND Host = 'localhost' AND Role = '{{ role0 }}'" + when: install_type == 'mariadb' + + - name: Check (mariadb) + assert: + that: + - result.query_result.0.0['user_roles'] == 1 + when: install_type == 'mariadb' - name: Check that the role is active <<: *task_params @@ -172,11 +201,31 @@ login_host: 127.0.0.1 login_port: '{{ mysql_primary_port }}' query: 'SELECT current_role()' + when: install_type == 'mysql' - name: Check assert: that: - - result.query_result.0.0["current_role()"] == "`{{ role0 }}`@`%`" + - "'{{ role0 }}' in result.query_result.0.0['current_role()']" + when: install_type == 'mysql' + + - name: Check that the role is active (mariadb) + <<: *task_params + mysql_query: + login_user: '{{ user0 }}' + login_password: '{{ mysql_password }}' + login_host: 127.0.0.1 + login_port: '{{ mysql_primary_port }}' + query: + - 'SET ROLE {{ role0 }}' + - 'SELECT current_role()' + when: install_type == 'mariadb' + + - name: Check (mariadb) + assert: + that: + - "'{{ role0 }}' in result.query_result.1.0['current_role()']" + when: install_type == 'mariadb' #======================== @@ -197,7 +246,7 @@ <<: *task_params mysql_query: <<: *mysql_params - query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}' AND Host = '%'" + query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}'" - name: Check assert: @@ -209,11 +258,26 @@ mysql_query: <<: *mysql_params query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" + when: install_type == 'mysql' - name: Check assert: that: - result is succeeded + when: install_type == 'mysql' + + - name: Check in DB (mariadb) + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = '{{ user0 }}' AND Host = 'localhost' AND Role = '{{ role0 }}'" + when: install_type == 'mariadb' + + - name: Check (mariadb) + assert: + that: + - result.query_result.0.0['user_roles'] == 1 + when: install_type == 'mariadb' #======================== @@ -233,7 +297,7 @@ <<: *task_params mysql_query: <<: *mysql_params - query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}' AND Host = '%'" + query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}'" - name: Check assert: @@ -259,7 +323,7 @@ <<: *task_params mysql_query: <<: *mysql_params - query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}' AND Host = '%'" + query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}'" - name: Check assert: @@ -272,11 +336,27 @@ mysql_query: <<: *mysql_params query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" + when: install_type == 'mysql' - name: Check assert: that: - result is succeeded + when: install_type == 'mysql' + + # Must pass because of check_mode + - name: Check in DB (mariadb) + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = '{{ user0 }}' AND Host = 'localhost' AND Role = '{{ role0 }}'" + when: install_type == 'mariadb' + + - name: Check (mariadb) + assert: + that: + - result.query_result.0.0['user_roles'] == 1 + when: install_type == 'mariadb' #======================== @@ -296,7 +376,7 @@ <<: *task_params mysql_query: <<: *mysql_params - query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}' AND Host = '%'" + query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}'" - name: Check assert: @@ -309,11 +389,27 @@ <<: *mysql_params query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" ignore_errors: yes + when: install_type == 'mysql' - name: Check assert: that: - result is failed + when: install_type == 'mysql' + + - name: Check in DB (mariadb) + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = '{{ user0 }}' AND Host = 'localhost' AND Role = '{{ role0 }}'" + ignore_errors: yes + when: install_type == 'mariadb' + + - name: Check (mariadb) + assert: + that: + - result.query_result.0.0['user_roles'] == 0 + when: install_type == 'mariadb' #======================== @@ -366,7 +462,7 @@ <<: *task_params mysql_query: <<: *mysql_params - query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}' AND Host = '%'" + query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}'" - name: Check assert: @@ -396,7 +492,7 @@ <<: *task_params mysql_query: <<: *mysql_params - query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}' AND Host = '%'" + query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}'" - name: Check assert: @@ -477,11 +573,27 @@ mysql_query: <<: *mysql_params query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" + when: install_type == 'mysql' - name: Check assert: that: - result is succeeded + when: install_type == 'mysql' + + # user0 is still a member because of check_mode + - name: Check in DB (mariadb) + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = '{{ user0 }}' AND Host = 'localhost' AND Role = '{{ role0 }}'" + when: install_type == 'mariadb' + + - name: Check (mariadb) + assert: + that: + - result.query_result.0.0['user_roles'] == 1 + when: install_type == 'mariadb' # user1, user2, and role1 are not members because of check_mode - name: Check in DB, if not granted, the query will fail @@ -490,11 +602,26 @@ <<: *mysql_params query: "SHOW GRANTS FOR {{ user1 }}@localhost USING '{{ role0 }}'" ignore_errors: yes + when: install_type == 'mysql' - name: Check assert: that: - result is failed + when: install_type == 'mysql' + + - name: Check in DB (mariadb) + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = '{{ user1 }}' AND Host = 'localhost' AND Role = '{{ role0 }}'" + when: install_type == 'mariadb' + + - name: Check (mariadb) + assert: + that: + - result.query_result.0.0['user_roles'] == 0 + when: install_type == 'mariadb' - name: Check in DB, if not granted, the query will fail <<: *task_params @@ -502,11 +629,26 @@ <<: *mysql_params query: "SHOW GRANTS FOR {{ user2 }}@localhost USING '{{ role0 }}'" ignore_errors: yes + when: install_type == 'mysql' - name: Check assert: that: - result is failed + when: install_type == 'mysql' + + - name: Check in DB (mariadb) + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = '{{ user2 }}' AND Host = 'localhost' AND Role = '{{ role0 }}'" + when: install_type == 'mariadb' + + - name: Check (mariadb) + assert: + that: + - result.query_result.0.0['user_roles'] == 0 + when: install_type == 'mariadb' - name: Check in DB, if not granted, the query will fail <<: *task_params @@ -514,11 +656,27 @@ <<: *mysql_params query: "SHOW GRANTS FOR {{ role1 }} USING '{{ role0 }}'" ignore_errors: yes + when: install_type == 'mysql' - name: Check assert: that: - result is failed + when: install_type == 'mysql' + + - name: Check in DB (mariadb) + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = '{{ role1 }}' AND Role = '{{ role0 }}'" + when: install_type == 'mariadb' + + - name: Check (mariadb) + assert: + that: + - result.query_result.0.0['user_roles'] == 0 + when: install_type == 'mariadb' + #======================== - name: Rewrite members @@ -544,33 +702,79 @@ <<: *mysql_params query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" ignore_errors: yes + when: install_type == 'mysql' - name: Check assert: that: - result is failed + when: install_type == 'mysql' + + # user0 is not a member any more + - name: Check in DB (mariadb) + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR {{ user0 }}@localhost" + when: install_type == 'mariadb' + + - name: Check (mariadb) + assert: + that: + - "'{{ role0 }}' not in result.query_result.0.0['Grants for user0@localhost']" + when: install_type == 'mariadb' - name: Check in DB, if not granted, the query will fail <<: *task_params mysql_query: <<: *mysql_params query: "SHOW GRANTS FOR {{ user1 }}@localhost USING '{{ role0 }}'" + when: install_type == 'mysql' - name: Check assert: that: - result is succeeded + when: install_type == 'mysql' + + - name: Check in DB (mariadb) + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = '{{ user1 }}' AND Host = 'localhost' AND Role = '{{ role0 }}'" + when: install_type == 'mariadb' + + - name: Check (mariadb) + assert: + that: + - result.query_result.0.0['user_roles'] == 1 + when: install_type == 'mariadb' - name: Check in DB, if not granted, the query will fail <<: *task_params mysql_query: <<: *mysql_params query: "SHOW GRANTS FOR {{ user2 }}@localhost USING '{{ role0 }}'" + when: install_type == 'mysql' - name: Check assert: that: - result is succeeded + when: install_type == 'mysql' + + - name: Check in DB (mariadb) + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = '{{ user2 }}' AND Host = 'localhost' AND Role = '{{ role0 }}'" + when: install_type == 'mariadb' + + - name: Check (mariadb) + assert: + that: + - result.query_result.0.0['user_roles'] == 1 + when: install_type == 'mariadb' - name: Check in DB, if not granted, the query will fail <<: *task_params @@ -578,11 +782,27 @@ <<: *mysql_params query: "SHOW GRANTS FOR {{ role1 }} USING '{{ role0 }}'" ignore_errors: yes + when: install_type == 'mysql' - name: Check assert: that: - result is succeeded + when: install_type == 'mysql' + + - name: Check in DB (mariadb) + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = '{{ role1 }}' AND Role = '{{ role0 }}'" + when: install_type == 'mariadb' + + - name: Check (mariadb) + assert: + that: + - result.query_result.0.0['user_roles'] == 1 + when: install_type == 'mariadb' + #========================== @@ -646,12 +866,26 @@ <<: *mysql_params query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" ignore_errors: yes + when: install_type == 'mysql' - name: Check assert: that: - result is failed + when: install_type == 'mysql' + - name: Check in DB (mariadb) + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = '{{ user0 }}' AND Host = 'localhost' AND Role = '{{ role0 }}'" + when: install_type == 'mariadb' + + - name: Check (mariadb) + assert: + that: + - result.query_result.0.0['user_roles'] == 0 + when: install_type == 'mariadb' #===================== - name: Append a member @@ -674,11 +908,26 @@ mysql_query: <<: *mysql_params query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" + when: install_type == 'mysql' - name: Check assert: that: - result is succeeded + when: install_type == 'mysql' + + - name: Check in DB (mariadb) + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = '{{ user0 }}' AND Host = 'localhost' AND Role = '{{ role0 }}'" + when: install_type == 'mariadb' + + - name: Check (mariadb) + assert: + that: + - result.query_result.0.0['user_roles'] == 1 + when: install_type == 'mariadb' # user1 and user2 must still be in DB because we are appending - name: Check in DB, if not granted, the query will fail @@ -686,22 +935,52 @@ mysql_query: <<: *mysql_params query: "SHOW GRANTS FOR {{ user1 }}@localhost USING '{{ role0 }}'" + when: install_type == 'mysql' - name: Check assert: that: - result is succeeded + when: install_type == 'mysql' + + - name: Check in DB (mariadb) + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = '{{ user1 }}' AND Host = 'localhost' AND Role = '{{ role0 }}'" + when: install_type == 'mariadb' + + - name: Check (mariadb) + assert: + that: + - result.query_result.0.0['user_roles'] == 1 + when: install_type == 'mariadb' - name: Check in DB, if not granted, the query will fail <<: *task_params mysql_query: <<: *mysql_params query: "SHOW GRANTS FOR {{ user2 }}@localhost USING '{{ role0 }}'" + when: install_type == 'mysql' - name: Check assert: that: - result is succeeded + when: install_type == 'mysql' + + - name: Check in DB (mariadb) + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = '{{ user2 }}' AND Host = 'localhost' AND Role = '{{ role0 }}'" + when: install_type == 'mariadb' + + - name: Check (mariadb) + assert: + that: + - result.query_result.0.0['user_roles'] == 1 + when: install_type == 'mariadb' #======================== @@ -763,33 +1042,78 @@ mysql_query: <<: *mysql_params query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" + when: install_type == 'mysql' - name: Check assert: that: - result is succeeded + when: install_type == 'mysql' + + - name: Check in DB (mariadb) + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = '{{ user0 }}' AND Host = 'localhost' AND Role = '{{ role0 }}'" + when: install_type == 'mariadb' + + - name: Check (mariadb) + assert: + that: + - result.query_result.0.0['user_roles'] == 1 + when: install_type == 'mariadb' - name: Check in DB, if not granted, the query will fail <<: *task_params mysql_query: <<: *mysql_params query: "SHOW GRANTS FOR {{ user1 }}@localhost USING '{{ role0 }}'" + when: install_type == 'mysql' - name: Check assert: that: - result is succeeded + when: install_type == 'mysql' + + - name: Check in DB (mariadb) + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = '{{ user1 }}' AND Host = 'localhost' AND Role = '{{ role0 }}'" + when: install_type == 'mariadb' + + - name: Check (mariadb) + assert: + that: + - result.query_result.0.0['user_roles'] == 1 + when: install_type == 'mariadb' - name: Check in DB, if not granted, the query will fail <<: *task_params mysql_query: <<: *mysql_params query: "SHOW GRANTS FOR {{ user2 }}@localhost USING '{{ role0 }}'" + when: install_type == 'mysql' - name: Check assert: that: - result is succeeded + when: install_type == 'mysql' + + - name: Check in DB (mariadb) + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = '{{ user2 }}' AND Host = 'localhost' AND Role = '{{ role0 }}'" + when: install_type == 'mariadb' + + - name: Check (mariadb) + assert: + that: + - result.query_result.0.0['user_roles'] == 1 + when: install_type == 'mariadb' #======================== @@ -814,11 +1138,26 @@ mysql_query: <<: *mysql_params query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" + when: install_type == 'mysql' - name: Check assert: that: - result is succeeded + when: install_type == 'mysql' + + - name: Check in DB (mariadb) + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = '{{ user0 }}' AND Host = 'localhost' AND Role = '{{ role0 }}'" + when: install_type == 'mariadb' + + - name: Check (mariadb) + assert: + that: + - result.query_result.0.0['user_roles'] == 1 + when: install_type == 'mariadb' - name: Check in DB, if not granted, the query will fail <<: *task_params @@ -826,11 +1165,26 @@ <<: *mysql_params query: "SHOW GRANTS FOR {{ user1 }}@localhost USING '{{ role0 }}'" ignore_errors: yes + when: install_type == 'mysql' - name: Check assert: that: - result is failed + when: install_type == 'mysql' + + - name: Check in DB (mariadb) + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = '{{ user1 }}' AND Host = 'localhost' AND Role = '{{ role0 }}'" + when: install_type == 'mariadb' + + - name: Check (mariadb) + assert: + that: + - result.query_result.0.0['user_roles'] == 0 + when: install_type == 'mariadb' - name: Check in DB, if not granted, the query will fail <<: *task_params @@ -838,11 +1192,26 @@ <<: *mysql_params query: "SHOW GRANTS FOR {{ user2 }}@localhost USING '{{ role0 }}'" ignore_errors: yes + when: install_type == 'mysql' - name: Check assert: that: - result is failed + when: install_type == 'mysql' + + - name: Check in DB (mariadb) + <<: *task_params + mysql_query: + <<: *mysql_params + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = '{{ user2 }}' AND Host = 'localhost' AND Role = '{{ role0 }}'" + when: install_type == 'mariadb' + + - name: Check (mariadb) + assert: + that: + - result.query_result.0.0['user_roles'] == 0 + when: install_type == 'mariadb' #===================== @@ -914,6 +1283,15 @@ - result.query_result.0.0["Grants for role0@%"] == "GRANT SELECT, INSERT ON *.* TO `role0`@`%`" - result.query_result.0.1["Grants for role0@%"] == "GRANT UPDATE ON `mysql`.* TO `role0`@`%`" - result.rowcount.0 == 2 + when: install_type == 'mysql' + + - name: Check (mariadb) + assert: + that: + - result.query_result.0.0["Grants for role0"] == "GRANT SELECT, INSERT ON *.* TO `role0`" + - result.query_result.0.1["Grants for role0"] == "GRANT UPDATE ON `mysql`.* TO `role0`" + - result.rowcount.0 == 2 + when: install_type == 'mariadb' - name: Append privs in check_mode <<: *task_params @@ -942,6 +1320,15 @@ - result.query_result.0.0["Grants for role0@%"] == "GRANT SELECT, INSERT ON *.* TO `role0`@`%`" - result.query_result.0.1["Grants for role0@%"] == "GRANT UPDATE ON `mysql`.* TO `role0`@`%`" - result.rowcount.0 == 2 + when: install_type == 'mysql' + + - name: Check (mariadb) + assert: + that: + - result.query_result.0.0["Grants for role0"] == "GRANT SELECT, INSERT ON *.* TO `role0`" + - result.query_result.0.1["Grants for role0"] == "GRANT UPDATE ON `mysql`.* TO `role0`" + - result.rowcount.0 == 2 + when: install_type == 'mariadb' - name: Append privs <<: *task_params @@ -971,6 +1358,17 @@ - result.query_result.0.2["Grants for role0@%"] == "GRANT SELECT, INSERT ON `test_db1`.`test_table` TO `role0`@`%`" - result.query_result.0.3["Grants for role0@%"] == "GRANT DELETE ON `test_db2`.`test_table` TO `role0`@`%`" - result.rowcount.0 == 4 + when: install_type == 'mysql' + + - name: Check (mariadb) + assert: + that: + - result.query_result.0.0["Grants for role0"] == "GRANT SELECT, INSERT ON *.* TO `role0`" + - result.query_result.0.1["Grants for role0"] == "GRANT UPDATE ON `mysql`.* TO `role0`" + - result.query_result.0.2["Grants for role0"] == "GRANT SELECT, INSERT ON `test_db1`.`test_table` TO `role0`" + - result.query_result.0.3["Grants for role0"] == "GRANT DELETE ON `test_db2`.`test_table` TO `role0`" + - result.rowcount.0 == 4 + when: install_type == 'mariadb' - name: Append privs again in check_mode <<: *task_params @@ -1038,6 +1436,14 @@ that: - result.query_result.0.0["Grants for role0@%"] == "GRANT SELECT ON *.* TO `role0`@`%`" - result.rowcount.0 == 1 + when: install_type == 'mysql' + + - name: Check (mariadb) + assert: + that: + - result.query_result.0.0["Grants for role0"] == "GRANT SELECT ON *.* TO `role0`" + - result.rowcount.0 == 1 + when: install_type == 'mariadb' # ################# # Test admin option @@ -1056,7 +1462,7 @@ <<: *mysql_params name: '{{ role0 }}' state: present - admin: '{{ user0 }}' + admin: '{{ user0 }}@localhost' ignore_errors: yes - name: Check with MySQL @@ -1064,34 +1470,26 @@ that: - result is failed - result.msg is search('option can be used only with MariaDB') - when: - # Semantically, when there's MySQL - - srv['version']['major'] < 10 + when: install_type == 'mysql' - name: Check with MariaDB assert: that: - result is changed - when: - # Semantically, when there's MariaDB - - srv['version']['major'] >= 10 + when: install_type == 'mariadb' - name: Check in DB <<: *task_params mysql_query: <<: *mysql_params - query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}' AND Host = '%'" - when: - # Semantically, when there's MariaDB - - srv['version']['major'] >= 10 + query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}' AND Host = ''" + when: install_type == 'mariadb' - name: Check assert: that: - result.rowcount.0 == 1 - when: - # Semantically, when there's MariaDB - - srv['version']['major'] >= 10 + when: install_type == 'mariadb' - name: Create role with admin again <<: *task_params @@ -1099,7 +1497,7 @@ <<: *mysql_params name: '{{ role0 }}' state: present - admin: '{{ user0 }}' + admin: '{{ user0 }}@localhost' ignore_errors: yes - name: Check with MySQL @@ -1107,17 +1505,13 @@ that: - result is failed - result.msg is search('option can be used only with MariaDB') - when: - # Semantically, when there's MySQL - - srv['version']['major'] < 10 + when: install_type == 'mysql' - name: Check with MariaDB assert: that: - result is not changed - # Semantically, when there's MariaDB - when: - - srv['version']['major'] >= 10 + when: install_type == 'mariadb' # Try to grant a role to a user who does not exist - name: Create role with admin again @@ -1130,14 +1524,11 @@ - '{{ nonexistent }}@localhost' ignore_errors: yes - - name: Check with MySQL + - name: Check assert: that: - result is failed - result.msg is search('does not exist') - when: - # Semantically, when there's MySQL - - srv['version']['major'] < 10 always: # Clean up From a516c1a6ad1b4c07e2c78d53e47736a93fd15983 Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Mon, 14 Mar 2022 14:01:18 +0300 Subject: [PATCH 29/43] Update requirements (#300) --- README.md | 13 +++++++++++++ plugins/doc_fragments/mysql.py | 3 ++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 11727ee..7db1f01 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,19 @@ Every voice is important and every idea is valuable. If you have something on yo - 2.11 - devel +### Databases + +- mysql 5.7.31 +- mysql 8.0.22 +- mariadb 10.3.34 +- mariadb 10.5.9 + +### Database connectors + +- pymysql 0.7.10 +- pymysql 0.9.3 +- mysqlclient 2.0.1 + ## External requirements The MySQL modules rely on a MySQL connector. The list of supported drivers is below: diff --git a/plugins/doc_fragments/mysql.py b/plugins/doc_fragments/mysql.py index b7a9152..9cc5bce 100644 --- a/plugins/doc_fragments/mysql.py +++ b/plugins/doc_fragments/mysql.py @@ -71,7 +71,8 @@ options: type: bool version_added: '1.1.0' requirements: - - PyMySQL (Python 2.7 and Python 3.X), or + - mysqlclient (Python 3.5+) or + - PyMySQL (Python 2.7 and Python 3.x) or - MySQLdb (Python 2.x) notes: - Requires the PyMySQL (Python 2.7 and Python 3.X) or MySQL-python (Python 2.X) package installed on the remote host. From 95c649cce2398f351060b673470c04ea18b28af1 Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Mon, 14 Mar 2022 16:41:38 +0300 Subject: [PATCH 30/43] Fix the collection to work with mysqlclient connector (#301) --- .github/workflows/ansible-test-plugins.yml | 4 ---- changelogs/fragments/0-mysqlclient.yml | 2 ++ plugins/module_utils/user.py | 8 ++++++-- plugins/modules/mysql_role.py | 2 +- tests/unit/plugins/modules/test_mysql_role.py | 4 ++-- 5 files changed, 11 insertions(+), 9 deletions(-) create mode 100644 changelogs/fragments/0-mysqlclient.yml diff --git a/.github/workflows/ansible-test-plugins.yml b/.github/workflows/ansible-test-plugins.yml index a82a624..c492261 100644 --- a/.github/workflows/ansible-test-plugins.yml +++ b/.github/workflows/ansible-test-plugins.yml @@ -74,10 +74,6 @@ jobs: exclude: - mysql: 8.0.22 connector: pymysql==0.7.10 - - db_engine_version: mariadb_10.3.34 - connector: mysqlclient==2.0.1 - - db_engine_version: mariadb_10.5.9 - connector: mysqlclient==2.0.1 - python: 3.8 ansible: stable-2.9 - python: 3.8 diff --git a/changelogs/fragments/0-mysqlclient.yml b/changelogs/fragments/0-mysqlclient.yml new file mode 100644 index 0000000..8c058d1 --- /dev/null +++ b/changelogs/fragments/0-mysqlclient.yml @@ -0,0 +1,2 @@ +bugfixes: +- Collection core functions - fixes related to the mysqlclient Python connector (https://github.com/ansible-collections/community.mysql/issues/292). diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index 0532de9..6dac57f 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -449,7 +449,7 @@ def privileges_get(cursor, user, host, maria_role=False): if not maria_role: cursor.execute("SHOW GRANTS FOR %s@%s", (user, host)) else: - cursor.execute("SHOW GRANTS FOR %s", (user)) + cursor.execute("SHOW GRANTS FOR %s", (user,)) grants = cursor.fetchall() def pick(x): @@ -673,7 +673,7 @@ def privileges_revoke(cursor, user, host, db_table, priv, grant_option, maria_ro params = (user, host) else: query.append("FROM %s") - params = (user) + params = (user,) query = ' '.join(query) cursor.execute(query, params) @@ -699,6 +699,10 @@ def privileges_grant(cursor, user, host, db_table, priv, tls_requires, maria_rol if 'GRANT' in priv: query.append("WITH GRANT OPTION") query = ' '.join(query) + + if isinstance(params, str): + params = (params,) + cursor.execute(query, params) diff --git a/plugins/modules/mysql_role.py b/plugins/modules/mysql_role.py index cd24c27..367692f 100644 --- a/plugins/modules/mysql_role.py +++ b/plugins/modules/mysql_role.py @@ -482,7 +482,7 @@ class MariaDBQueryBuilder(): Returns: tuple: (query_string, tuple_containing_parameters). """ - return "SELECT count(*) FROM mysql.user WHERE user = %s AND is_role = 'Y'", (self.name) + return "SELECT count(*) FROM mysql.user WHERE user = %s AND is_role = 'Y'", (self.name,) def role_grant(self, user): """Return a query to grant a role to a user or role. diff --git a/tests/unit/plugins/modules/test_mysql_role.py b/tests/unit/plugins/modules/test_mysql_role.py index 2ed5a2c..3c24719 100644 --- a/tests/unit/plugins/modules/test_mysql_role.py +++ b/tests/unit/plugins/modules/test_mysql_role.py @@ -28,9 +28,9 @@ module = Module() @pytest.mark.parametrize( 'builder,output', [ - (MariaDBQueryBuilder('role0'), ("SELECT count(*) FROM mysql.user WHERE user = %s AND is_role = 'Y'", ('role0'))), + (MariaDBQueryBuilder('role0'), ("SELECT count(*) FROM mysql.user WHERE user = %s AND is_role = 'Y'", ('role0',))), (MySQLQueryBuilder('role0', '%'), ('SELECT count(*) FROM mysql.user WHERE user = %s AND host = %s', ('role0', '%'))), - (MariaDBQueryBuilder('role1'), ("SELECT count(*) FROM mysql.user WHERE user = %s AND is_role = 'Y'", ('role1'))), + (MariaDBQueryBuilder('role1'), ("SELECT count(*) FROM mysql.user WHERE user = %s AND is_role = 'Y'", ('role1',))), (MySQLQueryBuilder('role1', 'fake'), ('SELECT count(*) FROM mysql.user WHERE user = %s AND host = %s', ('role1', 'fake'))), ] ) From fecc9866e3f03d73343b34650d8dcf4db5b32c9a Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Mon, 14 Mar 2022 17:17:21 +0300 Subject: [PATCH 31/43] Release 2.3.5 commit (#303) --- changelogs/CHANGELOG.rst | 13 +++++++++++++ changelogs/changelog.yaml | 12 ++++++++++++ changelogs/fragments/0-mysqlclient.yml | 2 -- galaxy.yml | 2 +- 4 files changed, 26 insertions(+), 3 deletions(-) delete mode 100644 changelogs/fragments/0-mysqlclient.yml diff --git a/changelogs/CHANGELOG.rst b/changelogs/CHANGELOG.rst index 212af17..3a87065 100644 --- a/changelogs/CHANGELOG.rst +++ b/changelogs/CHANGELOG.rst @@ -5,6 +5,19 @@ Community MySQL Collection Release Notes .. contents:: Topics +v2.3.5 +====== + +Release Summary +--------------- + +This is the patch release of the ``community.mysql`` collection. This changelog contains all changes to the modules in this collection that have been added after the release of ``community.mysql`` 2.3.4. + +Bugfixes +-------- + +- Collection core functions - fixes related to the mysqlclient Python connector (https://github.com/ansible-collections/community.mysql/issues/292). + v2.3.4 ====== diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 3fb24b7..4178449 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -403,3 +403,15 @@ releases: - 2.3.4.yml - 282-mysql_role_fix_set_default_role_all_argument.yml release_date: '2022-02-16' + 2.3.5: + changes: + bugfixes: + - Collection core functions - fixes related to the mysqlclient Python connector + (https://github.com/ansible-collections/community.mysql/issues/292). + release_summary: This is the patch release of the ``community.mysql`` collection. + This changelog contains all changes to the modules in this collection that + have been added after the release of ``community.mysql`` 2.3.4. + fragments: + - 0-mysqlclient.yml + - 2.3.5.yml + release_date: '2022-03-14' diff --git a/changelogs/fragments/0-mysqlclient.yml b/changelogs/fragments/0-mysqlclient.yml deleted file mode 100644 index 8c058d1..0000000 --- a/changelogs/fragments/0-mysqlclient.yml +++ /dev/null @@ -1,2 +0,0 @@ -bugfixes: -- Collection core functions - fixes related to the mysqlclient Python connector (https://github.com/ansible-collections/community.mysql/issues/292). diff --git a/galaxy.yml b/galaxy.yml index 692a899..20fd1c2 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,6 +1,6 @@ namespace: community name: mysql -version: 2.3.4 +version: 2.3.5 readme: README.md authors: - Ansible community From 66c340e9511fac5e62e6018b7b580207db5cda2f Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Tue, 15 Mar 2022 17:30:23 +0300 Subject: [PATCH 32/43] Add IF EXISTS clause to DROP USER statement (#307) (#308) * Add IF EXISTS clause to DROP USER statement * Add a changelog fragment * Fix exception (cherry picked from commit 3a452faeb07884a6547b62f76f4c0743de907115) --- .../fragments/307-mysql_user_add_if_exists_to_drop.yml | 2 ++ plugins/module_utils/user.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelogs/fragments/307-mysql_user_add_if_exists_to_drop.yml diff --git a/changelogs/fragments/307-mysql_user_add_if_exists_to_drop.yml b/changelogs/fragments/307-mysql_user_add_if_exists_to_drop.yml new file mode 100644 index 0000000..8de1b17 --- /dev/null +++ b/changelogs/fragments/307-mysql_user_add_if_exists_to_drop.yml @@ -0,0 +1,2 @@ +bugfixes: +- "mysql_user - fix the possibility for a race condition that breaks certain (circular) replication configurations when ``DROP USER`` is executed on multiple nodes in the replica set. Adding ``IF EXISTS`` avoids the need to use ``sql_log_bin: no`` making the statement always replication safe (https://github.com/ansible-collections/community.mysql/pull/287)." diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index 6dac57f..e305966 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -419,7 +419,10 @@ def user_delete(cursor, user, host, host_all, check_mode): hostnames = [host] for hostname in hostnames: - cursor.execute("DROP USER %s@%s", (user, hostname)) + try: + cursor.execute("DROP USER IF EXISTS %s@%s", (user, hostname)) + except Exception: + cursor.execute("DROP USER %s@%s", (user, hostname)) return True From 82cde26b11c13f166f2ab94c523d73e6307a7c26 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Thu, 17 Mar 2022 09:51:14 +0100 Subject: [PATCH 33/43] Setup patchback bot config file (resolves #310) (#311) (#312) (cherry picked from commit 55458f5b0b40f7e1810b62f24ba27b972cf2ff1a) Co-authored-by: R.Sicart --- .github/patchback.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/patchback.yml diff --git a/.github/patchback.yml b/.github/patchback.yml new file mode 100644 index 0000000..33ad6e8 --- /dev/null +++ b/.github/patchback.yml @@ -0,0 +1,5 @@ +--- +backport_branch_prefix: patchback/backports/ +backport_label_prefix: backport- +target_branch_prefix: stable- +... From 203338024fad64d12f50db213cdac7738b3c771d Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Thu, 24 Mar 2022 06:54:56 +0100 Subject: [PATCH 34/43] Fix roles CI (#316) (#317) (cherry picked from commit e6e661b87f182f4a9486a19594b640388b0d6d8f) Co-authored-by: Andrew Klychkov --- .github/workflows/ansible-test-roles.yml | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ansible-test-roles.yml b/.github/workflows/ansible-test-roles.yml index 0bc32f6..4f85d26 100644 --- a/.github/workflows/ansible-test-roles.yml +++ b/.github/workflows/ansible-test-roles.yml @@ -24,13 +24,24 @@ jobs: - 2.0.12 ansible: - stable-2.9 - ### it looks like there's errors for 2.10+ with ansible-lint (https://github.com/ansible/ansible-lint/pull/878) - ### and molecule (_maybe_ relating to https://github.com/ansible-community/molecule/pull/2547) - # - stable-2.10 - # - devel + - stable-2.10 + - stable-2.11 + - stable-2.12 + - devel python: - - 2.7 + - 3.6 - 3.8 + exclude: + - python: 3.8 + ansible: stable-2.9 + - python: 3.8 + ansible: stable-2.10 + - python: 3.8 + ansible: stable-2.11 + - python: 3.6 + ansible: stable-2.12 + - python: 3.6 + ansible: devel steps: @@ -44,7 +55,7 @@ jobs: with: python-version: ${{ matrix.python }} - - name: Install ansible-base (${{ matrix.ansible }}) + - name: Install ansible-core (${{ matrix.ansible }}) run: pip install https://github.com/ansible/ansible/archive/${{ matrix.ansible }}.tar.gz --disable-pip-version-check - name: Install molecule and related dependencies From 74ab876064e1aeb4b93d5c6b1a9685ee8abf36d8 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Fri, 25 Mar 2022 14:14:44 +0100 Subject: [PATCH 35/43] mysql_user: clarify behavior of priv parameter (#319) (#321) (cherry picked from commit 5afae459dc35b4507d25a34d7cc901231a3d07f4) Co-authored-by: Andrew Klychkov --- plugins/modules/mysql_user.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index 9c043cc..601e50a 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -45,7 +45,7 @@ options: 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.table:priv/db.table:priv)." + 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 @@ -54,6 +54,11 @@ options: 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: From 2de30ed7cb547efbcb3b67f6826af6900cdef459 Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Sat, 2 Apr 2022 18:33:46 +0300 Subject: [PATCH 36/43] [stable-2] Backports (#324) * Clarified error message about missing python modules (#279) * Clarified error message about missing python modules, and tweak documentation to suggest overriding interpreter. * Mention mysqlclient as another option * Correct mysqlclient suggestions from python2 to python3 Co-authored-by: Matthew Exon (cherry picked from commit 82baf7508ce757b102e229b51f902c46cf18bebc) * Copy ignore-2.13.txt to ignore-2.14.txt (#323) * Copy ignore-2.13.txt to ignore-2.14.txt * Fix sanity (cherry picked from commit c16b2428e80f8f837469d41fca48f6b0015fab8f) Co-authored-by: Matthew Exon --- plugins/doc_fragments/mysql.py | 11 +++++++---- plugins/module_utils/mysql.py | 5 ++++- tests/sanity/ignore-2.14.txt | 8 ++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 tests/sanity/ignore-2.14.txt diff --git a/plugins/doc_fragments/mysql.py b/plugins/doc_fragments/mysql.py index 9cc5bce..4b531d4 100644 --- a/plugins/doc_fragments/mysql.py +++ b/plugins/doc_fragments/mysql.py @@ -79,10 +79,13 @@ notes: 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 PyMySQL or MySQLdb library installed on the target machine - for the Python interpreter Ansible uses, for example, if it is Python 3, - you must install the library for Python 3. You can also change the interpreter. - For more information, see U(https://docs.ansible.com/ansible/latest/reference_appendices/interpreter_discovery.html). + - 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 diff --git a/plugins/module_utils/mysql.py b/plugins/module_utils/mysql.py index 5af9c20..9492ea8 100644 --- a/plugins/module_utils/mysql.py +++ b/plugins/module_utils/mysql.py @@ -29,7 +29,10 @@ except ImportError: except ImportError: mysql_driver = None -mysql_driver_fail_msg = 'The PyMySQL (Python 2.7 and Python 3.X) or MySQL-python (Python 2.X) module is required.' +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): diff --git a/tests/sanity/ignore-2.14.txt b/tests/sanity/ignore-2.14.txt new file mode 100644 index 0000000..c0323af --- /dev/null +++ b/tests/sanity/ignore-2.14.txt @@ -0,0 +1,8 @@ +plugins/modules/mysql_db.py validate-modules:doc-elements-mismatch +plugins/modules/mysql_db.py validate-modules:parameter-list-no-elements +plugins/modules/mysql_db.py validate-modules:use-run-command-not-popen +plugins/modules/mysql_info.py validate-modules:doc-elements-mismatch +plugins/modules/mysql_info.py validate-modules:parameter-list-no-elements +plugins/modules/mysql_query.py validate-modules:parameter-list-no-elements +plugins/modules/mysql_user.py validate-modules:undocumented-parameter +plugins/modules/mysql_variables.py validate-modules:doc-required-mismatch From 9857a16cb5e70a0bce07a7ecb6ec721ee9cd5573 Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Sun, 3 Apr 2022 09:10:35 +0300 Subject: [PATCH 37/43] CI: add testing against ansible-core 2.13 (#327) Co-authored-by: R.Sicart --- .github/workflows/ansible-test-plugins.yml | 5 +++++ .github/workflows/ansible-test-roles.yml | 3 +++ README.md | 2 ++ 3 files changed, 10 insertions(+) diff --git a/.github/workflows/ansible-test-plugins.yml b/.github/workflows/ansible-test-plugins.yml index c492261..6f857d2 100644 --- a/.github/workflows/ansible-test-plugins.yml +++ b/.github/workflows/ansible-test-plugins.yml @@ -29,6 +29,7 @@ jobs: - stable-2.10 - stable-2.11 - stable-2.12 + - stable-2.13 - devel steps: @@ -63,6 +64,7 @@ jobs: - stable-2.10 - stable-2.11 - stable-2.12 + - stable-2.13 - devel python: - 3.6 @@ -82,6 +84,8 @@ jobs: ansible: stable-2.11 - python: 3.6 ansible: stable-2.12 + - python: 3.6 + ansible: stable-2.13 - python: 3.6 ansible: devel @@ -131,6 +135,7 @@ jobs: - stable-2.10 - stable-2.11 - stable-2.12 + - stable-2.13 - devel steps: diff --git a/.github/workflows/ansible-test-roles.yml b/.github/workflows/ansible-test-roles.yml index 4f85d26..5c27416 100644 --- a/.github/workflows/ansible-test-roles.yml +++ b/.github/workflows/ansible-test-roles.yml @@ -27,6 +27,7 @@ jobs: - stable-2.10 - stable-2.11 - stable-2.12 + - stable-2.13 - devel python: - 3.6 @@ -40,6 +41,8 @@ jobs: ansible: stable-2.11 - python: 3.6 ansible: stable-2.12 + - python: 3.6 + ansible: stable-2.13 - python: 3.6 ansible: devel diff --git a/README.md b/README.md index 7db1f01..99674ca 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,8 @@ Every voice is important and every idea is valuable. If you have something on yo - 2.9 - 2.10 - 2.11 +- 2.12 +- 2.13 - devel ### Databases From d38813d4182d969ba8a49f9bbe4a52d0e3322fb9 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 5 Apr 2022 11:31:38 +0200 Subject: [PATCH 38/43] mysql_role: remove redundant connection closing (fixes #329) (#330) (#332) * mysql_role: remove redundant connection closing (fixes #329) * add changelog fragment for pull request #330 Co-authored-by: Felix Hamme (cherry picked from commit 641894e6e8fa0c02384e64e7eaaaf5e5e45b82a0) Co-authored-by: betanummeric <40263343+betanummeric@users.noreply.github.com> --- .../329-mysql_role-remove-redudant-connection-closing.yml | 2 ++ plugins/modules/mysql_role.py | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 changelogs/fragments/329-mysql_role-remove-redudant-connection-closing.yml diff --git a/changelogs/fragments/329-mysql_role-remove-redudant-connection-closing.yml b/changelogs/fragments/329-mysql_role-remove-redudant-connection-closing.yml new file mode 100644 index 0000000..8035b32 --- /dev/null +++ b/changelogs/fragments/329-mysql_role-remove-redudant-connection-closing.yml @@ -0,0 +1,2 @@ +bugfixes: + - "mysql_role - remove redundant connection closing (https://github.com/ansible-collections/community.mysql/pull/330)." diff --git a/plugins/modules/mysql_role.py b/plugins/modules/mysql_role.py index 367692f..557ceac 100644 --- a/plugins/modules/mysql_role.py +++ b/plugins/modules/mysql_role.py @@ -1059,8 +1059,6 @@ def main(): except Exception as e: module.fail_json(msg=to_native(e)) - # Exit - db_conn.close() module.exit_json(changed=changed) From d61d442861b5b25b2432f5e548a833c0ffed1acd Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Fri, 29 Apr 2022 16:29:33 +0200 Subject: [PATCH 39/43] mysql_user: added flush privileges to write dynamic privs into db (#338) (#339) * added flush privileges to write dynamic privs into db Fixes https://github.com/ansible-collections/community.mysql/issues/120 * added changelog fragment * Update changelogs/fragments/338-mysql_user_fix_missing_dynamic_privileges.yml Co-authored-by: Andrew Klychkov Co-authored-by: Andrew Klychkov (cherry picked from commit 1dcc5ec086434e707d0ad122ffd9b612187b1132) Co-authored-by: bigo8525 <53953606+bigo8525@users.noreply.github.com> --- .../fragments/338-mysql_user_fix_missing_dynamic_privileges.yml | 2 ++ plugins/module_utils/user.py | 1 + 2 files changed, 3 insertions(+) create mode 100644 changelogs/fragments/338-mysql_user_fix_missing_dynamic_privileges.yml diff --git a/changelogs/fragments/338-mysql_user_fix_missing_dynamic_privileges.yml b/changelogs/fragments/338-mysql_user_fix_missing_dynamic_privileges.yml new file mode 100644 index 0000000..1054ea6 --- /dev/null +++ b/changelogs/fragments/338-mysql_user_fix_missing_dynamic_privileges.yml @@ -0,0 +1,2 @@ +bugfixes: + - "mysql_user - fix missing dynamic privileges after revoke and grant privileges to user (https://github.com/ansible-collections/community.mysql/issues/120)." \ No newline at end of file diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index e305966..41c89b8 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -680,6 +680,7 @@ def privileges_revoke(cursor, user, host, db_table, priv, grant_option, maria_ro 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): From 65eb142960b33b151837834e7259701d4538f350 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Thu, 12 May 2022 11:55:02 +0200 Subject: [PATCH 40/43] Move CHANGELOG.rst at top level (#349) (#350) (cherry picked from commit f57ed38beb46f25aeb37eb33cdce262f8f5ed5b1) Co-authored-by: Andrew Klychkov --- changelogs/CHANGELOG.rst => CHANGELOG.rst | 0 changelogs/config.yaml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename changelogs/CHANGELOG.rst => CHANGELOG.rst (100%) diff --git a/changelogs/CHANGELOG.rst b/CHANGELOG.rst similarity index 100% rename from changelogs/CHANGELOG.rst rename to CHANGELOG.rst diff --git a/changelogs/config.yaml b/changelogs/config.yaml index 559e6c4..70ab036 100644 --- a/changelogs/config.yaml +++ b/changelogs/config.yaml @@ -1,4 +1,4 @@ -changelog_filename_template: CHANGELOG.rst +changelog_filename_template: ../CHANGELOG.rst changelog_filename_version_depth: 0 changes_file: changelog.yaml changes_format: combined From cc78846f2272bcf00c4b88bdf9688eb43d69bd01 Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Thu, 12 May 2022 15:20:44 +0300 Subject: [PATCH 41/43] mysql_user: fix parsing privs when a user has roles assigned to it (#345) --- .gitignore | 1 + ...ser_fix_pars_users_with_roles_assigned.yml | 2 + plugins/module_utils/user.py | 11 +++ .../tasks/mysql_role_initial.yml | 21 ++++ .../targets/test_mysql_user/tasks/main.yml | 3 + .../test_user_grants_with_roles_applied.yml | 95 +++++++++++++++++++ 6 files changed, 133 insertions(+) create mode 100644 changelogs/fragments/001-mysql_user_fix_pars_users_with_roles_assigned.yml create mode 100644 tests/integration/targets/test_mysql_user/tasks/test_user_grants_with_roles_applied.yml diff --git a/.gitignore b/.gitignore index f440722..6bbe85a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /tests/output/ /changelogs/.plugin-cache.yaml +*.swp # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/changelogs/fragments/001-mysql_user_fix_pars_users_with_roles_assigned.yml b/changelogs/fragments/001-mysql_user_fix_pars_users_with_roles_assigned.yml new file mode 100644 index 0000000..121bc46 --- /dev/null +++ b/changelogs/fragments/001-mysql_user_fix_pars_users_with_roles_assigned.yml @@ -0,0 +1,2 @@ +bugfixes: +- mysql_user - fix parsing privs when a user has roles assigned (https://github.com/ansible-collections/community.mysql/issues/231). diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index 41c89b8..ed79da1 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -466,8 +466,19 @@ def privileges_get(cursor, user, host, maria_role=False): 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] diff --git a/tests/integration/targets/test_mysql_role/tasks/mysql_role_initial.yml b/tests/integration/targets/test_mysql_role/tasks/mysql_role_initial.yml index 1bca3ae..a2167c6 100644 --- a/tests/integration/targets/test_mysql_role/tasks/mysql_role_initial.yml +++ b/tests/integration/targets/test_mysql_role/tasks/mysql_role_initial.yml @@ -1540,3 +1540,24 @@ - '{{ test_db }}' - '{{ test_db1 }}' - '{{ test_db2 }}' + + - name: Drop users + <<: *task_params + mysql_user: + <<: *mysql_params + name: '{{ item }}' + state: absent + loop: + - '{{ user0 }}' + - '{{ user1 }}' + - '{{ user2 }}' + + - name: Drop roles + <<: *task_params + mysql_role: + <<: *mysql_params + name: '{{ item }}' + state: absent + loop: + - '{{ role0 }}' + - test diff --git a/tests/integration/targets/test_mysql_user/tasks/main.yml b/tests/integration/targets/test_mysql_user/tasks/main.yml index 93cb12d..9b6caf3 100644 --- a/tests/integration/targets/test_mysql_user/tasks/main.yml +++ b/tests/integration/targets/test_mysql_user/tasks/main.yml @@ -282,3 +282,6 @@ - import_tasks: issue-64560.yaml tags: - issue-64560 + + # https://github.com/ansible-collections/community.mysql/issues/231 + - include: test_user_grants_with_roles_applied.yml diff --git a/tests/integration/targets/test_mysql_user/tasks/test_user_grants_with_roles_applied.yml b/tests/integration/targets/test_mysql_user/tasks/test_user_grants_with_roles_applied.yml new file mode 100644 index 0000000..8ee738e --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/test_user_grants_with_roles_applied.yml @@ -0,0 +1,95 @@ +# https://github.com/ansible-collections/community.mysql/issues/231 +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: 127.0.0.1 + login_port: '{{ mysql_primary_port }}' + + block: + - name: Get server version + mysql_info: + <<: *mysql_params + register: srv + + # Skip unsupported versions + - meta: end_play + when: srv['version']['major'] < 8 + + - name: Create test databases + mysql_db: + <<: *mysql_params + name: '{{ item }}' + state: present + loop: + - data1 + - data2 + + - name: Create user with privileges + mysql_user: + <<: *mysql_params + name: '{{ user_name_3 }}' + password: '{{ user_password_3 }}' + priv: + "data1.*": "SELECT" + "data2.*": "SELECT" + state: present + + - name: Run command to show privileges for user (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_3 }}'@'localhost'\"" + register: result + + - name: Assert user has giving privileges + assert: + that: + - "'GRANT SELECT ON `data1`.*' in result.stdout" + - "'GRANT SELECT ON `data2`.*' in result.stdout" + + - name: Create role + mysql_role: + <<: *mysql_params + name: test231 + members: + - '{{ user_name_3 }}@localhost' + + - name: Try to change privs + mysql_user: + <<: *mysql_params + name: '{{ user_name_3 }}' + priv: + "data1.*": "INSERT" + "data2.*": "INSERT" + state: present + + - name: Run command to show privileges for user (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_3 }}'@'localhost'\"" + register: result + + - name: Assert user has giving privileges + assert: + that: + - "'GRANT INSERT ON `data1`.*' in result.stdout" + - "'GRANT INSERT ON `data2`.*' in result.stdout" + + ########## + # Clean up + - name: Drop test databases + mysql_db: + <<: *mysql_params + name: '{{ item }}' + state: present + loop: + - data1 + - data2 + + - name: Drop test user + mysql_user: + <<: *mysql_params + name: '{{ user_name_3 }}' + state: absent + + - name: Drop test role + mysql_role: + <<: *mysql_params + name: test231 + state: absent From f854fc96db88744a634d6c84f85548ae80b31a0c Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Thu, 12 May 2022 15:31:07 +0300 Subject: [PATCH 42/43] Drop support for Ansible 2.9 and ansible-base 2.10 (#343) (#348) * Drop support for Ansible 2.9 and ansible-base 2.10 * Improve README (cherry picked from commit eff87f952bf8adb543bd77eedf7ecd518054f2ca) --- .github/workflows/ansible-test-plugins.yml | 10 ---------- .github/workflows/ansible-test-roles.yml | 6 ------ README.md | 8 ++++---- changelogs/fragments/drop_support_of_2.9-2.10.yml | 2 ++ plugins/module_utils/version.py | 2 +- 5 files changed, 7 insertions(+), 21 deletions(-) create mode 100644 changelogs/fragments/drop_support_of_2.9-2.10.yml diff --git a/.github/workflows/ansible-test-plugins.yml b/.github/workflows/ansible-test-plugins.yml index 6f857d2..c1cecd9 100644 --- a/.github/workflows/ansible-test-plugins.yml +++ b/.github/workflows/ansible-test-plugins.yml @@ -25,8 +25,6 @@ jobs: strategy: matrix: ansible: - - stable-2.9 - - stable-2.10 - stable-2.11 - stable-2.12 - stable-2.13 @@ -60,8 +58,6 @@ jobs: - 5.7.31 - 8.0.22 ansible: - - stable-2.9 - - stable-2.10 - stable-2.11 - stable-2.12 - stable-2.13 @@ -76,10 +72,6 @@ jobs: exclude: - mysql: 8.0.22 connector: pymysql==0.7.10 - - python: 3.8 - ansible: stable-2.9 - - python: 3.8 - ansible: stable-2.10 - python: 3.8 ansible: stable-2.11 - python: 3.6 @@ -131,8 +123,6 @@ jobs: fail-fast: true matrix: ansible: - - stable-2.9 - - stable-2.10 - stable-2.11 - stable-2.12 - stable-2.13 diff --git a/.github/workflows/ansible-test-roles.yml b/.github/workflows/ansible-test-roles.yml index 5c27416..bda6986 100644 --- a/.github/workflows/ansible-test-roles.yml +++ b/.github/workflows/ansible-test-roles.yml @@ -23,8 +23,6 @@ jobs: mysql: - 2.0.12 ansible: - - stable-2.9 - - stable-2.10 - stable-2.11 - stable-2.12 - stable-2.13 @@ -33,10 +31,6 @@ jobs: - 3.6 - 3.8 exclude: - - python: 3.8 - ansible: stable-2.9 - - python: 3.8 - ansible: stable-2.10 - python: 3.8 ansible: stable-2.11 - python: 3.6 diff --git a/README.md b/README.md index 99674ca..b71c6c4 100644 --- a/README.md +++ b/README.md @@ -59,14 +59,14 @@ Every voice is important and every idea is valuable. If you have something on yo - [mysql_user](https://docs.ansible.com/ansible/latest/collections/community/mysql/mysql_user_module.html) - [mysql_variables](https://docs.ansible.com/ansible/latest/collections/community/mysql/mysql_variables_module.html) -## Tested with Ansible +## Tested with + +### ansible-core -- 2.9 -- 2.10 - 2.11 - 2.12 - 2.13 -- devel +- current development version ### Databases diff --git a/changelogs/fragments/drop_support_of_2.9-2.10.yml b/changelogs/fragments/drop_support_of_2.9-2.10.yml new file mode 100644 index 0000000..8570210 --- /dev/null +++ b/changelogs/fragments/drop_support_of_2.9-2.10.yml @@ -0,0 +1,2 @@ +major_changes: +- The community.mysql collection no longer supports ``Ansible 2.9`` and ``ansible-base 2.10``. While we take no active measures to prevent usage and there are no plans to introduce incompatible code to the modules, we will stop testing against ``Ansible 2.9`` and ``ansible-base 2.10``. Both will very soon be End of Life and if you are still using them, you should consider upgrading to the ``latest Ansible / ansible-core 2.11 or later`` as soon as possible (https://github.com/ansible-collections/community.mysql/pull/343). diff --git a/plugins/module_utils/version.py b/plugins/module_utils/version.py index 359cceb..9473134 100644 --- a/plugins/module_utils/version.py +++ b/plugins/module_utils/version.py @@ -8,7 +8,7 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type -# Once we drop support for Ansible 2.9, ansible-base 2.10, and ansible-core 2.11, we can +# 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 65b1773ed0ac6309ffaa9ada57b7b8021ffd75cf Mon Sep 17 00:00:00 2001 From: Andrew Klychkov Date: Fri, 13 May 2022 10:16:09 +0300 Subject: [PATCH 43/43] Release 2.3.6 commit (#354) --- CHANGELOG.rst | 23 +++++++++++++ changelogs/changelog.yaml | 32 +++++++++++++++++++ ...ser_fix_pars_users_with_roles_assigned.yml | 2 -- .../307-mysql_user_add_if_exists_to_drop.yml | 2 -- ...ole-remove-redudant-connection-closing.yml | 2 -- ...ql_user_fix_missing_dynamic_privileges.yml | 2 -- .../fragments/drop_support_of_2.9-2.10.yml | 2 -- galaxy.yml | 2 +- 8 files changed, 56 insertions(+), 11 deletions(-) delete mode 100644 changelogs/fragments/001-mysql_user_fix_pars_users_with_roles_assigned.yml delete mode 100644 changelogs/fragments/307-mysql_user_add_if_exists_to_drop.yml delete mode 100644 changelogs/fragments/329-mysql_role-remove-redudant-connection-closing.yml delete mode 100644 changelogs/fragments/338-mysql_user_fix_missing_dynamic_privileges.yml delete mode 100644 changelogs/fragments/drop_support_of_2.9-2.10.yml diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3a87065..aedde21 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,29 @@ Community MySQL Collection Release Notes .. contents:: Topics +v2.3.6 +====== + +Release Summary +--------------- + +This is the patch release of the ``community.mysql`` collection. +This changelog contains all changes to the modules in this collection +that have been added after the release of ``community.mysql`` 2.3.5. + +Major Changes +------------- + +- The community.mysql collection no longer supports ``Ansible 2.9`` and ``ansible-base 2.10``. While we take no active measures to prevent usage and there are no plans to introduce incompatible code to the modules, we will stop testing against ``Ansible 2.9`` and ``ansible-base 2.10``. Both will very soon be End of Life and if you are still using them, you should consider upgrading to the ``latest Ansible / ansible-core 2.11 or later`` as soon as possible (https://github.com/ansible-collections/community.mysql/pull/343). + +Bugfixes +-------- + +- mysql_role - remove redundant connection closing (https://github.com/ansible-collections/community.mysql/pull/330). +- mysql_user - fix missing dynamic privileges after revoke and grant privileges to user (https://github.com/ansible-collections/community.mysql/issues/120). +- mysql_user - fix parsing privs when a user has roles assigned (https://github.com/ansible-collections/community.mysql/issues/231). +- mysql_user - fix the possibility for a race condition that breaks certain (circular) replication configurations when ``DROP USER`` is executed on multiple nodes in the replica set. Adding ``IF EXISTS`` avoids the need to use ``sql_log_bin: no`` making the statement always replication safe (https://github.com/ansible-collections/community.mysql/pull/287). + v2.3.5 ====== diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index 4178449..9d548dc 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -415,3 +415,35 @@ releases: - 0-mysqlclient.yml - 2.3.5.yml release_date: '2022-03-14' + 2.3.6: + changes: + bugfixes: + - mysql_role - remove redundant connection closing (https://github.com/ansible-collections/community.mysql/pull/330). + - mysql_user - fix missing dynamic privileges after revoke and grant privileges + to user (https://github.com/ansible-collections/community.mysql/issues/120). + - mysql_user - fix parsing privs when a user has roles assigned (https://github.com/ansible-collections/community.mysql/issues/231). + - 'mysql_user - fix the possibility for a race condition that breaks certain + (circular) replication configurations when ``DROP USER`` is executed on multiple + nodes in the replica set. Adding ``IF EXISTS`` avoids the need to use ``sql_log_bin: + no`` making the statement always replication safe (https://github.com/ansible-collections/community.mysql/pull/287).' + major_changes: + - The community.mysql collection no longer supports ``Ansible 2.9`` and ``ansible-base + 2.10``. While we take no active measures to prevent usage and there are no + plans to introduce incompatible code to the modules, we will stop testing + against ``Ansible 2.9`` and ``ansible-base 2.10``. Both will very soon be + End of Life and if you are still using them, you should consider upgrading + to the ``latest Ansible / ansible-core 2.11 or later`` as soon as possible + (https://github.com/ansible-collections/community.mysql/pull/343). + release_summary: 'This is the patch release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules in this collection + + that have been added after the release of ``community.mysql`` 2.3.5.' + fragments: + - 001-mysql_user_fix_pars_users_with_roles_assigned.yml + - 2.3.6.yml + - 307-mysql_user_add_if_exists_to_drop.yml + - 329-mysql_role-remove-redudant-connection-closing.yml + - 338-mysql_user_fix_missing_dynamic_privileges.yml + - drop_support_of_2.9-2.10.yml + release_date: '2022-05-13' diff --git a/changelogs/fragments/001-mysql_user_fix_pars_users_with_roles_assigned.yml b/changelogs/fragments/001-mysql_user_fix_pars_users_with_roles_assigned.yml deleted file mode 100644 index 121bc46..0000000 --- a/changelogs/fragments/001-mysql_user_fix_pars_users_with_roles_assigned.yml +++ /dev/null @@ -1,2 +0,0 @@ -bugfixes: -- mysql_user - fix parsing privs when a user has roles assigned (https://github.com/ansible-collections/community.mysql/issues/231). diff --git a/changelogs/fragments/307-mysql_user_add_if_exists_to_drop.yml b/changelogs/fragments/307-mysql_user_add_if_exists_to_drop.yml deleted file mode 100644 index 8de1b17..0000000 --- a/changelogs/fragments/307-mysql_user_add_if_exists_to_drop.yml +++ /dev/null @@ -1,2 +0,0 @@ -bugfixes: -- "mysql_user - fix the possibility for a race condition that breaks certain (circular) replication configurations when ``DROP USER`` is executed on multiple nodes in the replica set. Adding ``IF EXISTS`` avoids the need to use ``sql_log_bin: no`` making the statement always replication safe (https://github.com/ansible-collections/community.mysql/pull/287)." diff --git a/changelogs/fragments/329-mysql_role-remove-redudant-connection-closing.yml b/changelogs/fragments/329-mysql_role-remove-redudant-connection-closing.yml deleted file mode 100644 index 8035b32..0000000 --- a/changelogs/fragments/329-mysql_role-remove-redudant-connection-closing.yml +++ /dev/null @@ -1,2 +0,0 @@ -bugfixes: - - "mysql_role - remove redundant connection closing (https://github.com/ansible-collections/community.mysql/pull/330)." diff --git a/changelogs/fragments/338-mysql_user_fix_missing_dynamic_privileges.yml b/changelogs/fragments/338-mysql_user_fix_missing_dynamic_privileges.yml deleted file mode 100644 index 1054ea6..0000000 --- a/changelogs/fragments/338-mysql_user_fix_missing_dynamic_privileges.yml +++ /dev/null @@ -1,2 +0,0 @@ -bugfixes: - - "mysql_user - fix missing dynamic privileges after revoke and grant privileges to user (https://github.com/ansible-collections/community.mysql/issues/120)." \ No newline at end of file diff --git a/changelogs/fragments/drop_support_of_2.9-2.10.yml b/changelogs/fragments/drop_support_of_2.9-2.10.yml deleted file mode 100644 index 8570210..0000000 --- a/changelogs/fragments/drop_support_of_2.9-2.10.yml +++ /dev/null @@ -1,2 +0,0 @@ -major_changes: -- The community.mysql collection no longer supports ``Ansible 2.9`` and ``ansible-base 2.10``. While we take no active measures to prevent usage and there are no plans to introduce incompatible code to the modules, we will stop testing against ``Ansible 2.9`` and ``ansible-base 2.10``. Both will very soon be End of Life and if you are still using them, you should consider upgrading to the ``latest Ansible / ansible-core 2.11 or later`` as soon as possible (https://github.com/ansible-collections/community.mysql/pull/343). diff --git a/galaxy.yml b/galaxy.yml index 20fd1c2..8c6997f 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,6 +1,6 @@ namespace: community name: mysql -version: 2.3.5 +version: 2.3.6 readme: README.md authors: - Ansible community