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 0000000..b552a6e --- /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 10e37fc..e1d0a92 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -74,6 +74,19 @@ options: - 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 0000000..24232f3 --- /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 32b59be..044787a 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 0000000..6c91803 --- /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 9fd5ccd..e949fe6 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