From f5e8fbb3f549c47de68a881867bace8d554949b3 Mon Sep 17 00:00:00 2001 From: Daniel Rupp Date: Mon, 10 Jan 2022 16:03:25 +0100 Subject: [PATCH] Add default database "mysql" to mysql_user (#266) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add default database "mysql" to mysql_user Since permissions are stored in the "mysql" database anyway this should not change the behaviour of the module. But replication / binlog filters which rely on the current database will be able to filter the statements correctly afterwards. Prior to this change they were not executed in any database context and could not be filtered in any way by the existing methods in MySQL. * Added changelog fragment * Update changelogs/fragments/266-default-database-for-mysql-user Thanks! Co-authored-by: Andrew Klychkov * Update mysql_user.py Make the change a configureable boolean * Update 266-default-database-for-mysql-user update changelog fragment * Update 266-default-database-for-mysql-user it´s not a bugfix anymore * Update plugins/modules/mysql_user.py Co-authored-by: Andrew Klychkov * Update plugins/modules/mysql_user.py Co-authored-by: Andrew Klychkov * Update plugins/modules/mysql_user.py Co-authored-by: Andrew Klychkov * Update plugins/modules/mysql_user.py Co-authored-by: Andrew Klychkov * renamed new option to force_context enhanced description added tests * fixed changelog * Update plugins/modules/mysql_user.py Co-authored-by: Andrew Klychkov * Update plugins/modules/mysql_user.py Co-authored-by: Andrew Klychkov * added more tests * removed first test attempts again (from issue-28.yml) created new tests for testing with and without replication * added force_context: no testing * forgot to add the new part to main.yml * found a copy&paste issue * fix include naming * Made sure the tests work in local testing * MariaDB handles online replication filters differently * fix changelog * Update changelogs/fragments/266-default-database-for-mysql-user.yml Co-authored-by: Andrew Klychkov * Update changelogs/fragments/266-default-database-for-mysql-user.yml Co-authored-by: Andrew Klychkov Co-authored-by: Andrew Klychkov --- .../266-default-database-for-mysql-user.yml | 2 + plugins/modules/mysql_user.py | 16 ++ .../tasks/issue-265.yml | 165 +++++++++++++++++ .../test_mysql_replication/tasks/main.yml | 3 + .../test_mysql_user/tasks/issue-265.yml | 168 ++++++++++++++++++ .../targets/test_mysql_user/tasks/main.yml | 4 + 6 files changed, 358 insertions(+) create mode 100644 changelogs/fragments/266-default-database-for-mysql-user.yml create mode 100644 tests/integration/targets/test_mysql_replication/tasks/issue-265.yml create mode 100644 tests/integration/targets/test_mysql_user/tasks/issue-265.yml diff --git a/changelogs/fragments/266-default-database-for-mysql-user.yml b/changelogs/fragments/266-default-database-for-mysql-user.yml new file mode 100644 index 00000000..b552a6ec --- /dev/null +++ b/changelogs/fragments/266-default-database-for-mysql-user.yml @@ -0,0 +1,2 @@ +minor_changes: +- mysql_user - added the ``force_context`` boolean option to set the default database context for the queries to be the ``mysql`` database. This way replication/binlog filters can catch the statements (https://github.com/ansible-collections/community.mysql/issues/265). diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index 10e37fc0..e1d0a927 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -74,6 +74,19 @@ - Whether binary logging should be enabled or disabled for the connection. type: bool default: yes + force_context: + description: + - Sets the С(mysql) system database as context for the executed statements (it will be used + as a database to connect to). Useful if you use binlog / replication filters in MySQL as + per default the statements can not be caught by a binlog / replication filter, they require + a database to be set to work, otherwise the replication can break down. + - See U(https://dev.mysql.com/doc/refman/8.0/en/replication-options-binary-log.html#option_mysqld_binlog-ignore-db) + for a description on how binlog filters work (filtering on the primary). + - See U(https://dev.mysql.com/doc/refman/8.0/en/replication-options-replica.html#option_mysqld_replicate-ignore-db) + for a description on how replication filters work (filtering on the replica). + type: bool + default: no + version_added: '3.1.0' state: description: - Whether the user should exist. @@ -341,6 +354,7 @@ def main(): plugin_hash_string=dict(default=None, type='str'), plugin_auth_string=dict(default=None, type='str'), resource_limits=dict(type='dict'), + force_context=dict(type='bool', default=False), ) module = AnsibleModule( argument_spec=argument_spec, @@ -366,6 +380,8 @@ def main(): ssl_ca = module.params["ca_cert"] check_hostname = module.params["check_hostname"] db = '' + if module.params["force_context"]: + db = 'mysql' sql_log_bin = module.params["sql_log_bin"] plugin = module.params["plugin"] plugin_hash_string = module.params["plugin_hash_string"] diff --git a/tests/integration/targets/test_mysql_replication/tasks/issue-265.yml b/tests/integration/targets/test_mysql_replication/tasks/issue-265.yml new file mode 100644 index 00000000..24232f35 --- /dev/null +++ b/tests/integration/targets/test_mysql_replication/tasks/issue-265.yml @@ -0,0 +1,165 @@ +--- +- name: alias mysql command to include default options + set_fact: + mysql_command: "mysql -u{{ mysql_user }} -p{{ mysql_password }} --protocol=tcp" + +- 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: + + # start replica so it is available for testing + + - name: Start replica + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica1_port }}' + mode: startreplica + register: result + + - assert: + that: + - result is changed + - result.queries == ["START SLAVE"] or result.queries == ["START REPLICA"] + + - name: Drop {{ user_name_1 }} if exists + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + state: absent + ignore_errors: yes + + # First test + # check if user creation works with force_context and is replicated + - name: create user with force_context + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + password: "{{ user_password_1 }}" + priv: '*.*:ALL,GRANT' + force_context: yes + + - name: attempt connection on replica1 with newly created user (expect success) + mysql_replication: + mode: getprimary + login_user: '{{ user_name_1 }}' + login_password: '{{ user_password_1 }}' + login_host: 127.0.0.1 + login_port: '{{ mysql_replica1_port }}' + register: result + ignore_errors: yes + + - assert: + that: + - result is succeeded + + - name: Drop user + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + state: absent + force_context: yes + + - name: attempt connection on replica with freshly removed user (expect failure) + mysql_replication: + mode: getprimary + login_user: '{{ user_name_1 }}' + login_password: '{{ user_password_1 }}' + login_host: 127.0.0.1 + login_port: '{{ mysql_replica1_port }}' + register: result + ignore_errors: yes + + - assert: + that: + - result is failed + + # Prepare replica1 for testing with a replication filter in place + # Stop replication, create a filter and restart replication on replica1. + - name: Stop replica + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica1_port }}' + mode: stopreplica + register: result + + - assert: + that: + - result is changed + - result.queries == ["STOP SLAVE"] or result.queries == ["STOP REPLICA"] + + - name: Create replication filter MySQL + shell: "echo \"CHANGE REPLICATION FILTER REPLICATE_IGNORE_DB = (mysql);\" | {{ mysql_command }} -P{{ mysql_replica1_port }}" + when: install_type == 'mysql' + + - name: Create replication filter MariaDB + shell: "echo \"SET GLOBAL replicate_ignore_db = 'mysql';\" | {{ mysql_command }} -P{{ mysql_replica1_port }}" + when: install_type == 'mariadb' + + - name: Start replica + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica1_port }}' + mode: startreplica + register: result + + - assert: + that: + - result is changed + - result.queries == ["START SLAVE"] or result.queries == ["START REPLICA"] + + # Second test + # Filter in place, ready to test if user creation is filtered with force_context + - name: create user with force_context + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + password: "{{ user_password_1 }}" + priv: '*.*:ALL,GRANT' + force_context: yes + + - name: attempt connection on replica with newly created user (expect failure) + mysql_replication: + mode: getprimary + login_user: '{{ user_name_1 }}' + login_password: '{{ user_password_1 }}' + login_host: 127.0.0.1 + login_port: '{{ mysql_replica1_port }}' + register: result + ignore_errors: yes + + - assert: + that: + - result is failed + + - name: Drop user + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + state: absent + force_context: yes + + # restore normal replica1 operation + # Stop replication and remove the filter + - name: Stop replica + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica1_port }}' + mode: stopreplica + register: result + + - assert: + that: + - result is changed + - result.queries == ["STOP SLAVE"] or result.queries == ["STOP REPLICA"] + + - name: Remove replication filter MySQL + shell: "echo \"CHANGE REPLICATION FILTER REPLICATE_IGNORE_DB = ();\" | {{ mysql_command }} -P{{ mysql_replica1_port }}" + when: install_type == 'mysql' + + - name: Remove replication filter MariaDB + shell: "echo \"SET GLOBAL replicate_ignore_db = '';\" | {{ mysql_command }} -P{{ mysql_replica1_port }}" + when: install_type == 'mariadb' diff --git a/tests/integration/targets/test_mysql_replication/tasks/main.yml b/tests/integration/targets/test_mysql_replication/tasks/main.yml index 32b59be2..044787ab 100644 --- a/tests/integration/targets/test_mysql_replication/tasks/main.yml +++ b/tests/integration/targets/test_mysql_replication/tasks/main.yml @@ -9,6 +9,9 @@ # Initial CI tests of mysql_replication module: - import_tasks: mysql_replication_initial.yml +# Tests of replication filters and force_context +- include: issue-265.yml + # Tests of primary_delay parameter: - import_tasks: mysql_replication_primary_delay.yml diff --git a/tests/integration/targets/test_mysql_user/tasks/issue-265.yml b/tests/integration/targets/test_mysql_user/tasks/issue-265.yml new file mode 100644 index 00000000..6c91803e --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/issue-265.yml @@ -0,0 +1,168 @@ +--- +- 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: Drop mysql user if exists + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + state: absent + ignore_errors: yes + + # Tests with force_context: yes + # Test user creation + - name: create mysql user {{user_name_1}} + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + state: present + force_context: yes + register: result + + - name: assert output message mysql user was created + assert: + that: + - "result.changed == true" + + - include: assert_user.yml user_name={{user_name_1}} + + # Test user removal + - name: remove mysql user {{user_name_1}} + mysql_user: + <<: *mysql_params + name: '{{user_name_1}}' + password: '{{user_password_1}}' + state: absent + force_context: yes + register: result + + - name: assert output message mysql user was removed + assert: + that: + - "result.changed == true" + + # Test blank user removal + - name: create blank mysql user to be removed later + mysql_user: + <<: *mysql_params + name: "" + state: present + force_context: yes + password: 'KJFDY&D*Sfuydsgf' + + - name: remove blank mysql user with hosts=all (expect changed) + mysql_user: + <<: *mysql_params + user: "" + host_all: true + state: absent + force_context: yes + register: result + + - name: assert changed is true for removing all blank users + assert: + that: + - "result.changed == true" + + - name: remove blank mysql user with hosts=all (expect ok) + mysql_user: + <<: *mysql_params + user: "" + host_all: true + force_context: yes + state: absent + register: result + + - name: assert changed is true for removing all blank users + assert: + that: + - "result.changed == false" + + - include: assert_no_user.yml user_name={{user_name_1}} + + # Tests with force_context: no + # Test user creation + - name: Drop mysql user if exists + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + state: absent + ignore_errors: yes + + # Tests with force_context: yes + # Test user creation + - name: create mysql user {{user_name_1}} + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + state: present + force_context: yes + register: result + + - name: assert output message mysql user was created + assert: + that: + - "result.changed == true" + + - include: assert_user.yml user_name={{user_name_1}} + + # Test user removal + - name: remove mysql user {{user_name_1}} + mysql_user: + <<: *mysql_params + name: '{{user_name_1}}' + password: '{{user_password_1}}' + state: absent + force_context: no + register: result + + - name: assert output message mysql user was removed + assert: + that: + - "result.changed == true" + + # Test blank user removal + - name: create blank mysql user to be removed later + mysql_user: + <<: *mysql_params + name: "" + state: present + force_context: no + password: 'KJFDY&D*Sfuydsgf' + + - name: remove blank mysql user with hosts=all (expect changed) + mysql_user: + <<: *mysql_params + user: "" + host_all: true + state: absent + force_context: no + register: result + + - name: assert changed is true for removing all blank users + assert: + that: + - "result.changed == true" + + - name: remove blank mysql user with hosts=all (expect ok) + mysql_user: + <<: *mysql_params + user: "" + host_all: true + force_context: no + state: absent + register: result + + - name: assert changed is true for removing all blank users + assert: + that: + - "result.changed == false" + + - include: assert_no_user.yml user_name={{user_name_1}} diff --git a/tests/integration/targets/test_mysql_user/tasks/main.yml b/tests/integration/targets/test_mysql_user/tasks/main.yml index 9fd5ccd6..e949fe66 100644 --- a/tests/integration/targets/test_mysql_user/tasks/main.yml +++ b/tests/integration/targets/test_mysql_user/tasks/main.yml @@ -284,3 +284,7 @@ - import_tasks: issue-64560.yaml tags: - issue-64560 + + # Test that mysql_user still works with force_context enabled (database set to "mysql") + # (https://github.com/ansible-collections/community.mysql/issues/265) + - include: issue-265.yml