From 6b12435b2b4f21e724cb24f6ca0d988681824fb6 Mon Sep 17 00:00:00 2001 From: Nicolas PAYART Date: Thu, 21 Oct 2021 08:38:46 +0200 Subject: [PATCH] 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 --- .../fragments/227-db-create-special-name.yaml | 2 + plugins/modules/mysql_db.py | 5 +- .../targets/test_mysql_db/tasks/main.yml | 287 +---------------- .../tasks/state_present_absent.yml | 300 ++++++++++++++++++ 4 files changed, 309 insertions(+), 285 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/changelogs/fragments/227-db-create-special-name.yaml b/changelogs/fragments/227-db-create-special-name.yaml new file mode 100644 index 00000000..99f7e968 --- /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 aa9ade06..c2a6fd80 100644 --- a/plugins/modules/mysql_db.py +++ b/plugins/modules/mysql_db.py @@ -330,7 +330,7 @@ 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/tasks/main.yml b/tests/integration/targets/test_mysql_db/tasks/main.yml index 139d5bbc..def6a976 100644 --- a/tests/integration/targets/test_mysql_db/tasks/main.yml +++ b/tests/integration/targets/test_mysql_db/tasks/main.yml @@ -21,297 +21,18 @@ # 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 +- include: state_present_absent.yml -- 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" +- include: state_present_absent.yml db_name="db%" -- 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 }}'\"" - 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=sql file=dbdata.sql format_msg_type=ASCII file2=dump2.sql file3=dump3.sql file4=dump4.sql db_name="db%" + - 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 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 00000000..02411f03 --- /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"