diff --git a/.github/workflows/ansible-test-plugins.yml b/.github/workflows/ansible-test-plugins.yml index b7be934..0b6c184 100644 --- a/.github/workflows/ansible-test-plugins.yml +++ b/.github/workflows/ansible-test-plugins.yml @@ -1,5 +1,6 @@ +--- name: Plugins CI -on: +on: # yamllint disable-line rule:truthy push: paths: - 'plugins/**' @@ -12,170 +13,323 @@ on: - '.github/workflows/ansible-test-plugins.yml' schedule: - cron: '0 6 * * *' - - -env: - mysql_version_file: "./ansible_collections/community/mysql/tests/integration/targets/setup_mysql/defaults/main.yml" - connector_version_file: "./ansible_collections/community/mysql/tests/integration/targets/setup_mysql/vars/main.yml" + workflow_dispatch: jobs: sanity: - name: "Sanity (Python: ${{ matrix.python }}, Ansible: ${{ matrix.ansible }})" - runs-on: ubuntu-latest + name: "Sanity (Ⓐ${{ matrix.ansible }})" + runs-on: ubuntu-22.04 strategy: matrix: ansible: - - stable-2.9 - - stable-2.10 - - stable-2.11 - - stable-2.12 - - stable-2.13 + - stable-2.16 + - stable-2.17 + - stable-2.18 - devel steps: - - - name: Check out code - uses: actions/checkout@v2 + # https://github.com/ansible-community/ansible-test-gh-action + - name: Perform sanity testing + uses: ansible-community/ansible-test-gh-action@release/v1 with: - path: ansible_collections/community/mysql - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - - name: Install ansible-base (${{ matrix.ansible }}) - run: pip install https://github.com/ansible/ansible/archive/${{ matrix.ansible }}.tar.gz --disable-pip-version-check - - - name: Run sanity tests - run: ansible-test sanity --docker -v --color - working-directory: ./ansible_collections/community/mysql + ansible-core-version: ${{ matrix.ansible }} + testing-type: sanity + pull-request-change-detection: true + # Use this to chose which version of Python vs Ansible to test: + # https://docs.ansible.com/ansible/latest/reference_appendices/release_and_maintenance.html#ansible-core-control-node-python-support integration: - name: "Integration (Python: ${{ matrix.python }}, Ansible: ${{ matrix.ansible }}, MySQL: ${{ matrix.db_engine_version }}, Connector: ${{ matrix.connector }})" - runs-on: ubuntu-latest + name: "Integration (Ⓐ${{ matrix.ansible }}, DB: ${{ matrix.db_engine_name }} ${{ matrix.db_engine_version }}, connector: ${{ matrix.connector_name }} ${{ matrix.connector_version }})" + runs-on: ubuntu-22.04 strategy: fail-fast: false matrix: - db_engine_version: - - mysql_5.7.31 - - mysql_8.0.22 - - mariadb_10.3.34 - - mariadb_10.5.9 ansible: - - stable-2.9 - - stable-2.10 - - stable-2.11 - - stable-2.12 - - stable-2.13 + - stable-2.16 + - stable-2.17 + - stable-2.18 - devel - python: - - 3.6 - - 3.8 - connector: - - pymysql==0.7.10 - - pymysql==0.9.3 - - mysqlclient==2.0.1 + db_engine_name: + - mysql + - mariadb + db_engine_version: + - '8.0.38' + - '8.4.1' + - '10.11.8' + - '11.4.5' + connector_name: + - pymysql + - mysqlclient + connector_version: + - '0.9.3' + - '1.0.2' + - '1.1.1' + - '2.0.1' + - '2.0.3' + - '2.1.1' + + include: + + # RHEL8 context + - connector_name: pymysql + connector_version: '0.10.1' + ansible: stable-2.16 + db_engine_name: mariadb + db_engine_version: '10.11.8' + + # RHEL9 context + # - connector_name: pymysql + # connector_version: '1.1.1' + # ansible: stable-2.17 + # db_engine_name: mariadb + # db_engine_version: '10.11.8' + # This tests is already included in the matrix, no need repeating + exclude: - - db_engine_version: mysql_8.0.22 - connector: pymysql==0.7.10 - - db_engine_version: mariadb_10.5.9 - 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: stable-2.13 - - python: 3.6 + + - db_engine_name: mysql + db_engine_version: '10.11.8' + + - db_engine_name: mysql + db_engine_version: '11.4.5' + + - db_engine_name: mariadb + db_engine_version: '8.0.38' + + - db_engine_name: mariadb + db_engine_version: '8.4.1' + + - connector_name: pymysql + connector_version: '2.0.1' + + - connector_name: pymysql + connector_version: '2.0.3' + + - connector_name: pymysql + connector_version: '2.1.1' + + - connector_name: mysqlclient + connector_version: '0.9.3' + + - connector_name: mysqlclient + connector_version: '1.0.2' + + - connector_name: mysqlclient + connector_version: '1.1.1' + + - db_engine_version: '8.0.38' + ansible: stable-2.17 + + - db_engine_version: '10.11.8' + ansible: stable-2.17 + + - db_engine_version: '8.0.38' ansible: devel + - db_engine_version: '10.11.8' + ansible: devel + + - db_engine_version: '8.4.1' + connector_version: '0.9.3' + + - db_engine_version: '8.4.1' + connector_version: '1.0.2' + + - db_engine_version: '8.4.1' + connector_version: '2.0.1' + + - db_engine_version: '8.4.1' + connector_version: '2.0.3' + + - db_engine_version: '10.11.8' + connector_version: '0.9.3' + + - db_engine_version: '10.11.8' + connector_version: '1.0.2' + + - db_engine_version: '10.11.8' + connector_version: '2.0.1' + + - db_engine_version: '10.11.8' + connector_version: '2.0.1' + + - db_engine_version: '10.11.8' + ansible: stable-2.15 + + - db_engine_version: '8.4.1' + ansible: stable-2.15 + + - connector_version: '1.1.1' + db_engine_version: '8.0.38' + + - connector_version: '1.1.1' + db_engine_version: '10.11.8' + + services: + db_primary: + image: docker.io/library/${{ matrix.db_engine_name }}:${{ matrix.db_engine_version }} + env: + MARIADB_ROOT_PASSWORD: msandbox + MYSQL_ROOT_PASSWORD: msandbox + ports: + - 3307:3306 + # We write our own health-cmd because the mariadb container does not + # provide a healthcheck + options: >- + --health-cmd "${{ matrix.db_engine_name == 'mysql' && 'mysqladmin' || 'mariadb-admin' }} ping -P 3306 -pmsandbox |grep alive || exit 1" + --health-start-period 10s + --health-interval 10s + --health-timeout 5s + --health-retries 6 + + db_replica1: + image: docker.io/library/${{ matrix.db_engine_name }}:${{ matrix.db_engine_version }} + env: + MARIADB_ROOT_PASSWORD: msandbox + MYSQL_ROOT_PASSWORD: msandbox + ports: + - 3308:3306 + options: >- + --health-cmd "${{ matrix.db_engine_name == 'mysql' && 'mysqladmin' || 'mariadb-admin' }} ping -P 3306 -pmsandbox |grep alive || exit 1" + --health-start-period 10s + --health-interval 10s + --health-timeout 5s + --health-retries 6 + + db_replica2: + image: docker.io/library/${{ matrix.db_engine_name }}:${{ matrix.db_engine_version }} + env: + MARIADB_ROOT_PASSWORD: msandbox + MYSQL_ROOT_PASSWORD: msandbox + ports: + - 3309:3306 + options: >- + --health-cmd "${{ matrix.db_engine_name == 'mysql' && 'mysqladmin' || 'mariadb-admin' }} ping -P 3306 -pmsandbox |grep alive || exit 1" + --health-start-period 10s + --health-interval 10s + --health-timeout 5s + --health-retries 6 + steps: - - name: Check out code - uses: actions/checkout@v2 - with: - path: ansible_collections/community/mysql + # No need to check for service health. GitHub Action took care of it. - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - - name: Install ansible-base (${{ matrix.ansible }}) - run: pip install https://github.com/ansible/ansible/archive/${{ matrix.ansible }}.tar.gz --disable-pip-version-check - - - name: Set MySQL version (${{ matrix.db_engine_version }}) + - name: Restart MySQL server with settings for replication run: | - export DB_VERSION=$(echo "${{ matrix.db_engine_version }}" | awk -F_ '{print $2}') - sed -i "s/^mysql_version:.*/mysql_version: $DB_VERSION/g" ${{ env.mysql_version_file }} - if: ${{ startsWith(matrix.db_engine_version, 'mysql') }} + db_ver="${{ matrix.db_engine_version }}" + maj="${db_ver%.*.*}" + maj_min="${db_ver%.*}" + min="${maj_min#*.}" + if [[ "${{ matrix.db_engine_name }}" == "mysql" && "$maj" -eq 8 && "$min" -ge 2 ]]; then + prima_conf='[mysqld]\\nserver-id=1\\nlog-bin=/var/lib/mysql/primary-bin\\nmysql-native-password=1' + repl1_conf='[mysqld]\\nserver-id=2\\nlog-bin=/var/lib/mysql/replica1-bin\\nmysql-native-password=1' + repl2_conf='[mysqld]\\nserver-id=3\\nlog-bin=/var/lib/mysql/replica2-bin\\nmysql-native-password=1' + else + prima_conf='[mysqld]\\nserver-id=1\\nlog-bin=/var/lib/mysql/primary-bin' + repl1_conf='[mysqld]\\nserver-id=2\\nlog-bin=/var/lib/mysql/replica1-bin' + repl2_conf='[mysqld]\\nserver-id=3\\nlog-bin=/var/lib/mysql/replica2-bin' + fi + docker exec -e cnf=$prima_conf ${{ job.services.db_primary.id }} bash -c 'echo -e ${cnf//\\n/\n} > /etc/mysql/conf.d/replication.cnf' + docker exec -e cnf=$repl1_conf ${{ job.services.db_replica1.id }} bash -c 'echo -e ${cnf//\\n/\n} > /etc/mysql/conf.d/replication.cnf' + docker exec -e cnf=$repl2_conf ${{ job.services.db_replica2.id }} bash -c 'echo -e ${cnf//\\n/\n} > /etc/mysql/conf.d/replication.cnf' + docker restart -t 30 ${{ job.services.db_primary.id }} + docker restart -t 30 ${{ job.services.db_replica1.id }} + docker restart -t 30 ${{ job.services.db_replica2.id }} - - name: Set MariaDB version (${{ matrix.db_engine_version }}) - run: | - export DB_VERSION=$(echo "${{ matrix.db_engine_version }}" | awk -F_ '{print $2}') - sed -i -e "s/^mariadb_version:.*/mariadb_version: $DB_VERSION/g" -e 's/^mariadb_install: false/mariadb_install: true/g' ${{ env.mysql_version_file }} - if: ${{ startsWith(matrix.db_engine_version, 'mariadb') }} + - name: Wait for the primary to be healthy + run: > + while ! /usr/bin/docker inspect + --format="{{if .Config.Healthcheck}}{{print .State.Health.Status}}{{end}}" + ${{ job.services.db_primary.id }} + | grep healthy && [[ "$SECONDS" -lt 120 ]]; do sleep 1; done - - name: Set Connector version (${{ matrix.connector }}) - run: "sed -i 's/^python_packages:.*/python_packages: [${{ matrix.connector }}]/' ${{ env.connector_version_file }}" - - - name: Run integration tests - run: ansible-test integration --docker -v --color --retry-on-error --continue-on-error --python ${{ matrix.python }} --diff --coverage - working-directory: ./ansible_collections/community/mysql - - - name: Generate coverage report. - run: ansible-test coverage xml -v --requirements --group-by command --group-by version - working-directory: ./ansible_collections/community/mysql - - - uses: codecov/codecov-action@v1 + - name: >- + Perform integration testing against + Ansible version ${{ matrix.ansible }} + uses: ansible-community/ansible-test-gh-action@release/v1 with: - fail_ci_if_error: false + ansible-core-version: ${{ matrix.ansible }} + docker-image: ubuntu2204 + pre-test-cmd: >- + echo Setting db_engine_name to "${{ matrix.db_engine_name }}"...; + echo -n "${{ matrix.db_engine_name }}" + > tests/integration/db_engine_name; + + echo Setting db_engine_version to \ + "${{ matrix.db_engine_version }}"...; + echo -n "${{ matrix.db_engine_version }}" + > tests/integration/db_engine_version; + + echo Setting Connector name to "${{ matrix.connector_name }}"...; + echo -n "${{ matrix.connector_name }}" + > tests/integration/connector_name; + + echo Setting Connector name to "${{ matrix.connector_version }}"...; + echo -n "${{ matrix.connector_version }}" + > tests/integration/connector_version; + + echo Setting Ansible version to "${{ matrix.ansible }}"...; + echo -n "${{ matrix.ansible }}" + > tests/integration/ansible + testing-type: integration + integration-retry-on-error: false units: - runs-on: ubuntu-latest - name: Units (Ⓐ${{ matrix.ansible }}) + runs-on: ubuntu-22.04 + name: Units (Ⓐ${{ matrix.ansible }}, Python${{ matrix.python }}) strategy: # As soon as the first unit test fails, # cancel the others to free up the CI queue fail-fast: true matrix: ansible: - - stable-2.9 - - stable-2.10 - - stable-2.11 - - stable-2.12 - - stable-2.13 + - stable-2.16 + - stable-2.17 + - stable-2.18 - devel + python: + - '3.8' + - '3.9' + - '3.10' + - '3.11' + exclude: + - python: '3.8' + ansible: stable-2.16 + + - python: '3.8' + ansible: stable-2.17 + + - python: '3.8' + ansible: devel + + - python: '3.9' + ansible: stable-2.15 + + - python: '3.9' + ansible: stable-2.17 + + - python: '3.9' + ansible: devel + + - python: '3.10' + ansible: stable-2.15 + + - python: '3.10' + ansible: stable-2.16 + + - python: '3.11' + ansible: stable-2.15 + + - python: '3.11' + ansible: stable-2.16 steps: - - name: Check out code - uses: actions/checkout@v2 + - name: >- + Perform unit testing against + Ansible version ${{ matrix.ansible }} and + python version ${{ matrix.python }} + uses: ansible-community/ansible-test-gh-action@release/v1 with: - path: ./ansible_collections/community/mysql - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - - name: Install ansible-base (${{matrix.ansible}}) - run: pip install https://github.com/ansible/ansible/archive/${{ matrix.ansible }}.tar.gz --disable-pip-version-check - - # Run the unit tests - - name: Run unit test - run: ansible-test units -v --color --docker --coverage - working-directory: ./ansible_collections/community/mysql - - # ansible-test support producing code coverage date - - name: Generate coverage report - run: ansible-test coverage xml -v --requirements --group-by command --group-by version - working-directory: ./ansible_collections/community/mysql - - # See the reports at https://codecov.io/gh/GITHUBORG/REPONAME - - uses: codecov/codecov-action@v1 - with: - fail_ci_if_error: false + ansible-core-version: ${{ matrix.ansible }} + target-python-version: ${{ matrix.python }} + testing-type: units + pull-request-change-detection: true diff --git a/.github/workflows/ansible-test-roles.yml b/.github/workflows/ansible-test-roles.yml.off similarity index 73% rename from .github/workflows/ansible-test-roles.yml rename to .github/workflows/ansible-test-roles.yml.off index 5c27416..a11d982 100644 --- a/.github/workflows/ansible-test-roles.yml +++ b/.github/workflows/ansible-test-roles.yml.off @@ -1,5 +1,6 @@ +--- name: Roles CI -on: +on: # yamllint disable-line rule:truthy push: paths: - 'roles/**' @@ -14,7 +15,7 @@ on: jobs: molecule: name: "Molecule (Python: ${{ matrix.python }}, Ansible: ${{ matrix.ansible }}, MySQL: ${{ matrix.mysql }})" - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 env: PY_COLORS: 1 ANSIBLE_FORCE_COLOR: 1 @@ -23,38 +24,36 @@ jobs: mysql: - 2.0.12 ansible: - - stable-2.9 - - stable-2.10 - - stable-2.11 - - stable-2.12 - - stable-2.13 + - stable-2.15 + - stable-2.16 + - stable-2.17 - devel python: - - 3.6 - - 3.8 + - '3.8' + - '3.9' + - '3.10' exclude: - python: 3.8 - ansible: stable-2.9 + ansible: stable-2.17 + + - python: 3.9 + ansible: stable-2.17 + - 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: stable-2.13 - - python: 3.6 + ansible: devel + + - python: 3.9 ansible: devel steps: - name: Check out code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: ansible_collections/community/mysql - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} diff --git a/.gitignore b/.gitignore index f440722..9555f5e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ /tests/output/ +/tests/integration/inventory /changelogs/.plugin-cache.yaml +*.swp # Byte-compiled / optimized / DLL files __pycache__/ @@ -133,3 +135,6 @@ dmypy.json # MacOS .DS_Store + +# IntelliJ IDEA or PyCharm +.idea/ diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..b318076 --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,489 @@ +==================================================== +Community MySQL and MariaDB Collection Release Notes +==================================================== + +.. contents:: Topics + +This changelog describes changes after version 2.0.0. + +v3.13.0 +======= + +Release Summary +--------------- + +This is a minor release of the ``community.mysql`` collection. +This changelog contains all changes to the modules and plugins in this +collection that have been made after the previous release. + +Minor Changes +------------- + +- Integration tests for MariaDB 11.4 have replaced those for 10.5. The previous version is now 10.11. +- mysql_user - add ``locked`` option to lock/unlock users, this is mainly used to have users that will act as definers on stored procedures. + +Bugfixes +-------- + +- mysql_db - fix dump and import to find MariaDB binaries (mariadb and mariadb-dump) when MariaDB 11+ is used and symbolic links to MySQL binaries are absent. + +v3.12.0 +======= + +Release Summary +--------------- + +This is a minor release of the ``community.mysql`` collection. +This changelog contains all changes to the modules and plugins in this +collection that have been made after the previous release. + +Minor Changes +------------- + +- mysql_db - added ``zstd`` (de)compression support for ``import``/``dump`` states (https://github.com/ansible-collections/community.mysql/issues/696). +- mysql_query - returns the ``execution_time_ms`` list containing execution time per query in milliseconds. + +v3.11.0 +======= + +Release Summary +--------------- + +This is a minor release of the ``community.mysql`` collection. + +This changelog contains all changes to the modules and plugins in this +collection that have been made after the previous release. + +Minor Changes +------------- + +- mysql_info - adds the count of tables for each database to the returned values. It is possible to exclude this new field using the ``db_table_count`` exclusion filter. (https://github.com/ansible-collections/community.mysql/pull/691) + +Bugfixes +-------- + +- mysql_user,mysql_role - The sql_mode ANSI_QUOTES affects how the modules mysql_user and mysql_role compare the existing privileges with the configured privileges, as well as decide whether double quotes or backticks should be used in the GRANT statements. Pointing out in issue 671, the modules mysql_user and mysql_role allow users to enable/disable ANSI_QUOTES in session variable (within a DB session, the session variable always overwrites the global one). But due to the issue, the modules do not check for ANSI_MODE in the session variable, instead, they only check in the GLOBAL one.That behavior is not only limiting the users' flexibility, but also not allowing users to explicitly disable ANSI_MODE to work around such bugs like https://bugs.mysql.com/bug.php?id=115953. (https://github.com/ansible-collections/community.mysql/issues/671) + +v3.10.3 +======= + +Release Summary +--------------- + +This is a bugfix release of the ``community.mysql`` collection. +This changelog contains all changes to the modules and plugins in this +collection that have been made after the previous release. + +Bugfixes +-------- + +- mysql_user - add correct ``ed25519`` auth plugin handling when creating a user (https://github.com/ansible-collections/community.mysql/pull/676). + +v3.10.2 +======= + +Release Summary +--------------- + +This is a bugfix release of the ``community.mysql`` collection. +This changelog contains all changes to the modules and plugins in this +collection that have been made after the previous release. + +Bugfixes +-------- + +- mysql_user - add correct ``ed25519`` auth plugin handling when creating a user (https://github.com/ansible-collections/community.mysql/issues/672). + +v3.10.1 +======= + +Release Summary +--------------- + +This is a patch release of the ``community.mysql`` collection. +Besides a bugfix, it contains an important upcoming breaking-change information. + +Deprecated Features +------------------- + +- mysql_user - the ``user`` alias of the ``name`` argument has been deprecated and will be removed in collection version 5.0.0. Use the ``name`` argument instead. + +Bugfixes +-------- + +- mysql_user - module makes changes when is executed with ``plugin_auth_string`` parameter and check mode. + +v3.10.0 +======= + +Release Summary +--------------- + +This is a minor release of the ``community.mysql`` collection. +This changelog contains all changes to the modules and plugins in this +collection that have been made after the previous release. + +Minor Changes +------------- + +- mysql_info - Add ``tls_requires`` returned value for the ``users_info`` filter (https://github.com/ansible-collections/community.mysql/pull/628). +- mysql_info - return a database server engine used (https://github.com/ansible-collections/community.mysql/issues/644). +- mysql_replication - Adds support for `CHANGE REPLICATION SOURCE TO` statement (https://github.com/ansible-collections/community.mysql/issues/635). +- mysql_replication - Adds support for `SHOW BINARY LOG STATUS` and `SHOW BINLOG STATUS` on getprimary mode. +- mysql_replication - Improve detection of IsReplica and IsPrimary by inspecting the dictionary returned from the SQL query instead of relying on variable types. This ensures compatibility with changes in the connector or the output of SHOW REPLICA STATUS and SHOW MASTER STATUS, allowing for easier maintenance if these change in the future. +- mysql_user - Add salt parameter to generate static hash for `caching_sha2_password` and `sha256_password` plugins. + +Deprecated Features +------------------- + +- collection - support of mysqlclient connector is deprecated - use PyMySQL connector instead! We will stop testing against it in collection version 4.0.0 and remove the related code in 5.0.0 (https://github.com/ansible-collections/community.mysql/issues/654). +- mysql_info - The ``users_info`` filter returned variable ``plugin_auth_string`` contains the hashed password and it's misleading, it will be removed from community.mysql 4.0.0. Use the `plugin_hash_string` return value instead (https://github.com/ansible-collections/community.mysql/pull/629). + +Bugfixes +-------- + +- mysql_info - Add ``plugin_hash_string`` to ``users_info`` filter's output. The existing ``plugin_auth_string`` contained the hashed password and thus is missleading, it will be removed from community.mysql 4.0.0. (https://github.com/ansible-collections/community.mysql/pull/629). +- mysql_user - Added a warning to update_password's on_new_username option if multiple accounts with the same username but different passwords exist (https://github.com/ansible-collections/community.mysql/pull/642). +- mysql_user - Fix ``tls_requires`` not removing ``SSL`` and ``X509`` when sets as empty (https://github.com/ansible-collections/community.mysql/pull/628). +- mysql_user - Fix idempotence when using variables from the ``users_info`` filter of ``mysql_info`` as an input (https://github.com/ansible-collections/community.mysql/pull/628). +- mysql_user - Fixed an IndexError in the update_password functionality introduced in PR https://github.com/ansible-collections/community.mysql/pull/580 and released in community.mysql 3.8.0. If you used this functionality, please avoid versions 3.8.0 to 3.9.0 (https://github.com/ansible-collections/community.mysql/pull/642). +- mysql_user - add correct ``ed25519`` auth plugin handling (https://github.com/ansible-collections/community.mysql/issues/6). +- mysql_variables - fix the module always changes on boolean values (https://github.com/ansible-collections/community.mysql/issues/652). + +v3.9.0 +====== + +Release Summary +--------------- + +This is a minor release of the ``community.mysql`` collection. +This changelog contains all changes to the modules and plugins in this +collection that have been made after the previous release. + +Major Changes +------------- + +- Collection version 2.*.* is EOL, no more bugfixes will be backported. Please consider upgrading to the latest version. + +Minor Changes +------------- + +- mysql_user - add the ``password_expire`` and ``password_expire_interval`` arguments to implement the password expiration management for mysql user (https://github.com/ansible-collections/community.mysql/pull/598). +- mysql_user - add user attribute support via the ``attributes`` parameter and return value (https://github.com/ansible-collections/community.mysql/pull/604). + +Bugfixes +-------- + +- mysql_info - the ``slave_status`` filter was returning an empty list on MariaDB with multiple replication channels. It now returns all channels by running ``SHOW ALL SLAVES STATUS`` for MariaDB servers (https://github.com/ansible-collections/community.mysql/issues/603). + +v3.8.0 +====== + +Release Summary +--------------- + +This is the minor release of the ``community.mysql`` collection. +This changelog contains all changes to the modules and plugins in this +collection that have been made after the previous release. + +Major Changes +------------- + +- The community.mysql collection no longer supports ``ansible-core 2.12`` and ``ansible-core 2.13``. 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 those versions. Both are or will soon be End of Life and if you are still using them, you should consider upgrading to the ``latest Ansible / ansible-core 2.15 or later`` as soon as possible (https://github.com/ansible-collections/community.mysql/pull/574). +- mysql_role - the ``column_case_sensitive`` argument's default value will be changed to ``true`` in community.mysql 4.0.0. If your playbook expected the column to be automatically uppercased for your roles privileges, you should set this to false explicitly (https://github.com/ansible-collections/community.mysql/issues/578). +- mysql_user - the ``column_case_sensitive`` argument's default value will be changed to ``true`` in community.mysql 4.0.0. If your playbook expected the column to be automatically uppercased for your users privileges, you should set this to false explicitly (https://github.com/ansible-collections/community.mysql/issues/577). + +Minor Changes +------------- + +- mysql_info - add filter ``users_info`` (https://github.com/ansible-collections/community.mysql/pull/580). +- mysql_role - add ``column_case_sensitive`` option to prevent field names from being uppercased (https://github.com/ansible-collections/community.mysql/pull/569). +- mysql_user - add ``column_case_sensitive`` option to prevent field names from being uppercased (https://github.com/ansible-collections/community.mysql/pull/569). + +v3.7.2 +====== + +Release Summary +--------------- + +This is a patch release of the community.mysql collection. +This changelog contains all changes to the modules and plugins in this collection +that have been made after the previous release. + +Bugfixes +-------- + +- mysql module utils - use the connection arguments ``db`` instead of ``database`` and ``passwd`` instead of ``password`` when running with MySQLdb < 2.0.0 (https://github.com/ansible-collections/community.mysql/pull/553). + +v3.7.1 +====== + +Release Summary +--------------- + +This is a patch release of the community.mysql collection. +This changelog contains all changes to the modules and plugins in this collection +that have been made after the previous release. + +Bugfixes +-------- + +- mysql module utils - use the connection arguments ``db`` instead of ``database`` and ``passwd`` instead of ``password`` when running with older mysql drivers (MySQLdb < 2.1.0 or PyMySQL < 1.0.0) (https://github.com/ansible-collections/community.mysql/pull/551). + +v3.7.0 +====== + +Release Summary +--------------- + +This is the minor release of the ``community.mysql`` collection. +This changelog contains all changes to the modules and plugins in this collection +that have been made after the previous release. + +Minor Changes +------------- + +- mysql module utils - change deprecated connection parameters ``passwd`` and ``db`` to ``password`` and ``database`` (https://github.com/ansible-collections/community.mysql/pull/177). +- mysql_user - add ``MAX_STATEMENT_TIME`` support for mariadb to the ``resource_limits`` argument (https://github.com/ansible-collections/community.mysql/issues/211). + +v3.6.0 +====== + +Release Summary +--------------- + +This is the minor release of the ``community.mysql`` collection. +This changelog contains all changes to the modules and plugins in this collection +that have been made after the previous release. + +Minor Changes +------------- + +- mysql_info - add ``connector_name`` and ``connector_version`` to returned values (https://github.com/ansible-collections/community.mysql/pull/497). +- mysql_role - enable auto_commit to avoid MySQL metadata table lock (https://github.com/ansible-collections/community.mysql/issues/479). +- mysql_user - add plugin_auth_string as optional parameter to use a specific pam service if pam/auth_pam plugin is used (https://github.com/ansible-collections/community.mysql/pull/445). +- mysql_user - add the ``session_vars`` argument to set session variables at the beginning of module execution (https://github.com/ansible-collections/community.mysql/issues/478). +- mysql_user - display a more informative invalid privilege exception. Changes the exception handling of the granting permission logic to show the query executed , params and the exception message granting privileges fails` (https://github.com/ansible-collections/community.mysql/issues/465). +- mysql_user - enable auto_commit to avoid MySQL metadata table lock (https://github.com/ansible-collections/community.mysql/issues/479). +- setup_mysql - update MySQL tarball URL (https://github.com/ansible-collections/community.mysql/pull/491). + +Bugfixes +-------- + +- mysql_user - when revoke privs consists only of ``GRANT``, a 2nd revoke query is executed with empty privs to revoke that ended in an SQL exception (https://github.com/ansible-collections/community.mysql/pull/503). +- mysql_variables - add uppercase character pattern to regex to allow GLOBAL variables containing uppercase characters. This recognizes variable names used in Galera, for example, ``wsrep_OSU_method``, which breaks the normal pattern of all lowercase characters (https://github.com/ansible-collections/community.mysql/pull/501). + +v3.5.1 +====== + +Release Summary +--------------- + +This is the patch release of the ``community.mysql`` collection. +This changelog contains all changes to the modules and plugins in this collection +that have been made after the previous release. + +Bugfixes +-------- + +- mysql_user, mysql_role - mysql/mariadb recent versions translate 'ALL PRIVILEGES' to a list of specific privileges. That caused a change every time we modified user privileges. This fix compares privs before and after user modification to avoid this infinite change (https://github.com/ansible-collections/community.mysql/issues/77). + +v3.5.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`` 3.4.0. + +Minor Changes +------------- + +- mysql_replication - add a new option: ``primary_ssl_verify_server_cert`` (https://github.com//pull/435). + +Bugfixes +-------- + +- mysql_user - grant option was revoked accidentally when modifying users. This fix revokes grant option only when privs are setup to do that (https://github.com/ansible-collections/community.mysql/issues/77#issuecomment-1209693807). + +v3.4.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`` 3.3.0. + +Major Changes +------------- + +- mysql_db - the ``pipefail`` argument's default value will be changed to ``true`` in community.mysql 4.0.0. If your target machines do not use ``bash`` as a default interpreter, set ``pipefail`` to ``false`` explicitly. However, we strongly recommend setting up ``bash`` as a default and ``pipefail=true`` as it will protect you from getting broken dumps you don't know about (https://github.com/ansible-collections/community.mysql/issues/407). + +Minor Changes +------------- + +- mysql_db - add the ``chdir`` argument to avoid failings when a dump file contains relative paths (https://github.com/ansible-collections/community.mysql/issues/395). +- mysql_db - add the ``pipefail`` argument to avoid broken dumps when ``state`` is ``dump`` and compression is used (https://github.com/ansible-collections/community.mysql/issues/256). + +Bugfixes +-------- + +- Include ``simplified_bsd.txt`` license file for various module utils. +- mysql_db - Using compression masks errors messages from mysql_dump. By default the fix is inactive to ensure retro-compatibility with system without bash. To activate the fix, use the module option ``pipefail=true`` (https://github.com/ansible-collections/community.mysql/issues/256). +- mysql_replication - when the ``primary_ssl`` argument is set to ``no``, the module will turn off SSL (https://github.com/ansible-collections/community.mysql/issues/393). + +v3.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`` 3.2.1. + +Minor Changes +------------- + +- mysql_role - add the argument ``members_must_exist`` (boolean, default true). The assertion that the users supplied in the ``members`` argument exist is only executed when the new argument ``members_must_exist`` is ``true``, to allow opt-out (https://github.com/ansible-collections/community.mysql/pull/369). +- mysql_user - Add the option ``on_new_username`` to argument ``update_password`` to reuse the password (plugin and authentication_string) when creating a new user if some user with the same name already exists. If the existing user with the same name have varying passwords, the password from the arguments is used like with ``update_password: always`` (https://github.com/ansible-collections/community.mysql/pull/365). +- mysql_user - Add the result field ``password_changed`` (boolean). It is true, when the user got a new password. When the user was created with ``update_password: on_new_username`` and an existing password was reused, ``password_changed`` is false (https://github.com/ansible-collections/community.mysql/pull/365). + +Bugfixes +-------- + +- mysql_query - fix false change reports when ``IF EXISTS/IF NOT EXISTS`` clause is used (https://github.com/ansible-collections/community.mysql/issues/268). +- mysql_role - don't add members to a role when creating the role and ``detach_members: true`` is set (https://github.com/ansible-collections/community.mysql/pull/367). +- mysql_role - in some cases (when "SHOW GRANTS" did not use backticks for quotes), no unwanted members were detached from the role (and redundant "GRANT" statements were executed for wanted members). This is fixed by querying the existing role members from the mysql.role_edges (MySQL) or mysql.roles_mapping (MariaDB) tables instead of parsing the "SHOW GRANTS" output (https://github.com/ansible-collections/community.mysql/pull/368). +- mysql_user - fix logic when ``update_password`` is set to ``on_create`` for users using ``plugin*`` arguments (https://github.com/ansible-collections/community.mysql/issues/334). The ``on_create`` sets ``password`` to None for old mysql_native_authentication but not for authentiation methods which uses the ``plugin*`` arguments. This PR changes this so ``on_create`` also exchange ``plugin``, ``plugin_hash_string``, ``plugin_auth_string`` to None in the list of arguments to change + +v3.2.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`` 3.2.0. + +Bugfixes +-------- + +- Include ``PSF-license.txt`` file for ``plugins/module_utils/_version.py``. + +v3.2.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`` 3.1.3. + +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). + +Minor Changes +------------- + +- mysql_user and mysql_role: Add the argument ``subtract_privs`` (boolean, default false, mutually exclusive with ``append_privs``). If set, the privileges given in ``priv`` are revoked and existing privileges are kept (https://github.com/ansible-collections/community.mysql/pull/333). + +Bugfixes +-------- + +- 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). + +v3.1.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`` 3.1.2. + +Bugfixes +-------- + +- mysql_replication - fails when using the `primary_use_gtid` option with `slave_pos` or `replica_pos` (https://github.com/ansible-collections/community.mysql/issues/335). +- mysql_role - remove redundant connection closing (https://github.com/ansible-collections/community.mysql/pull/330). +- 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). + +v3.1.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`` 3.1.1. + +Bugfixes +-------- + +- Collection core functions - fixes related to the mysqlclient Python connector (https://github.com/ansible-collections/community.mysql/issues/292). + +v3.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`` 3.1.0. + +Bugfixes +-------- + +- mysql_role - make the ``set_default_role_all`` parameter actually working (https://github.com/ansible-collections/community.mysql/pull/282). + +v3.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`` 3.0.0. + +Minor Changes +------------- + +- Added explicit description of the supported versions of databases and connectors. Changes to the collection are **NOT** tested against database versions older than `mysql 5.7.31` and `mariadb 10.2.37` or connector versions older than `pymysql 0.7.10` and `mysqlclient 2.0.1`. (https://github.com/ansible-collections/community.mysql/discussions/141) +- 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). + +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). + +v3.0.0 +====== + +Release Summary +--------------- + +This is the major release of the ``community.mysql`` collection. +This changelog contains all breaking changes to the modules in this collection +that have been added after the release of ``community.mysql`` 2.3.2. + +Breaking Changes / Porting Guide +-------------------------------- + +- mysql_replication - remove ``Is_Slave`` and ``Is_Master`` return values (were replaced with ``Is_Primary`` and ``Is_Replica`` (https://github.com/ansible-collections/community.mysql/issues/145). +- mysql_replication - remove the mode options values containing ``master``/``slave`` and the master_use_gtid option ``slave_pos`` (were replaced with corresponding ``primary``/``replica`` values) (https://github.com/ansible-collections/community.mysql/issues/145). +- mysql_user - remove support for the `REQUIRESSL` special privilege as it has ben superseded by the `tls_requires` option (https://github.com/ansible-collections/community.mysql/discussions/121). +- mysql_user - validate privileges using database engine directly (https://github.com/ansible-collections/community.mysql/issues/234 https://github.com/ansible-collections/community.mysql/pull/243). Do not validate privileges in this module anymore. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index edcfe55..1b6ecdf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,80 @@ -# Contributing +# Contributing to this project -Refer to the [Ansible Contributing guidelines](https://github.com/ansible/community-docs/blob/main/contributing.rst) to learn how to contribute to this collection. +In this guide, you will find information relevant for code contributions, though any other kinds of contribution mentioned in the [Ansible Contributing guidelines](https://docs.ansible.com/ansible/devel/community/index.html) are equally appreciated and valuable. + +If you have any questions after reading, please contact the community via one or more of the [available channels](https://github.com/ansible-collections/community.mysql#communication). Any feedback on this guide is very welcome. + +## Reviewing open issue and pull requests + +Refer to the [review checklist](https://docs.ansible.com/ansible/devel/community/collection_contributors/collection_reviewing.html) when triaging issues or reviewing pull requests (hereinafter PRs). + +Most important things to pay attention to: + +- Do not let major/breaking changes sneak into a minor/bugfix release! All such changes should be discussed in a dedicated issue, added to a corresponding milestone (which can be found or created in the project's Issues), and merged right before the major release. Take a look at similar issues to see what needs to be done and reflect on the steps you did/need to do in the issue. +- Every PR (except doc, refactoring, test-related, or a PR containing a new module/plugin) contains a [changelog fragment](https://docs.ansible.com/ansible/latest/community/development_process.html#creating-a-changelog-fragment). Let's give users a chance to know about the changes. +- Every new module `DOCUMENTATION` section contains the `version_added: 'x.y.z'` field. Besides the informative purpose, it is used by the changelog-generating tool to add a corresponding entry to the changelog. As the project follows SemVer, it is typically a next minor (x.y.0) version. +- Every new module argument contains the `version_added: 'x.y.z'` field. As the project follows SemVer, it is typically a next minor (x.y.0) version. +- Non-refactoring code changes (bugfixes, new features) are covered with, at least, integration tests! There can be exceptions but generally it is a requirement. + +## Code contributions + +If you want to submit a bugfix or new feature, refer to the [Quick-start development guide](https://docs.ansible.com/ansible/devel/community/create_pr_quick_start.html) first. + +## Project-specific info + +We assume you have read the [Quick-start development guide](https://docs.ansible.com/ansible/devel/community/create_pr_quick_start.html). + +In order for any submitted PR to get merged, this project requires sanity, unit, and integration tests to pass. +Codecov job is there but not required. +We use the GitHub Actions platform to run the tests. +You can see the result in the bottom of every PR in the box listing the jobs and their results: + +- Green checkmark: the test has been passed, no more action is needed. +- Red cross: the test has failed. You can see the reason by clicking the ``Details`` link. Fix them locally and push the commit. + +Generally, all jobs must be green. +Sometimes, there can be failures unrelated to a PR, for example, when a test container is unavailable or there is another part of the code that does not satisfy recently introduced additional sanity checks. +If you think the failure does not relate to your changes, put a comment about it. + +## CI testing + +The jobs are launched automatically by GitHub Actions in every PR based on the [matrix](https://github.com/ansible-collections/community.mysql/blob/main/.github/workflows/ansible-test-plugins.yml). + +As the project is included in `ansible` community package, it is a requirement for us to test against all supported `ansible-core` versions and corresponding Python versions. +To keep the matrix relevant, we are subscribed to the [news-for-maintainers](https://github.com/ansible-collections/news-for-maintainers) repository and the [Collection maintainers & contributors](https://forum.ansible.com/g/CollectionMaintainer) forum group to track announcements affecting CI. + +If our matrix is permanently outdated, for example, when supported `ansible-core` versions are missed, the collections can get excluded from the package, so keep it updated! + +Read more about our CI implementation in the [TESTING.md](https://github.com/ansible-collections/community.mysql/blob/main/TESTING.md) file. + +## Adding tests + +If you are new here, read the [Quick-start development guide](https://docs.ansible.com/ansible/devel/community/create_pr_quick_start.html) first. + +When fixing a bug, first reproduce it by adding a task as reported to a suitable file under the ``tests/integration/targets//tasks/`` directory and run the integration tests as described below. The same is relevant for new features. + +It is not necessary but if you want you can also add unit tests to a suitable file under the ``tests/units/`` directory and run them as described below. + +## Checking your code locally + +It will make your and other people's life a bit easier if you run the tests locally and fix all failures before pushing. If you're unable to run the tests locally, please create your PR as a **draft** to avoid reviewers being added automatically. + +If you are new here, read the [Quick-start development guide](https://docs.ansible.com/ansible/devel/community/create_pr_quick_start.html) first. + +We assume you [prepared your local environment](https://docs.ansible.com/ansible/devel/community/create_pr_quick_start.html#prepare-your-environment) as described in the guide before running the following commands. Otherwise, the command will fail. + +### Sanity tests + +``` console +$ ansible-test sanity path/to/changed_file.py --docker -v +``` + +### Integration tests + +See the [TESTING.md](https://github.com/ansible-collections/community.mysql/blob/main/TESTING.md) file to learn how to run integration tests against different server/connector versions. + +### Unit tests + +``` console +$ ansible-test units tests/unit/plugins/unit_test_file.py --docker +``` diff --git a/CONTRIBUTORS b/CONTRIBUTORS index cacb4ff..6d946cc 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -17,9 +17,11 @@ amitk79 amree Andersson007 andrewhowdencom +aneustroev ansibot anthonyxpalermo antonioribeiro +Aohzan apollo13 aquach arcmop @@ -33,6 +35,9 @@ baldpale banyek BarbzYHOOL Berbe +betanummeric +bigo8525 +bizmate bjne bmalynovytch bmildren @@ -45,6 +50,7 @@ candeira caphrim007 cdalbergue checkphi +chriscroome chrismeyersfsu ChristopherGAndrews cmodijk @@ -55,13 +61,14 @@ CormacBracken cosmix cptMikky crashes +d-lee +d-rupp dagwieers damianmoore Davidffry denisemauldin +dennisurtubia diclophis -d-lee -d-rupp dmp1ce dnelson dramaley @@ -71,9 +78,11 @@ DSpeichert dungdm93 dwagelaar dylanjbarth -einarc E-M +einarc +elpavel eowin +eRadical Ernest0x esamattis Everspace @@ -81,24 +90,30 @@ F21 faitno felixfontein flatrocks +FlorianPerrot fourjay fraff +francescsanjuanmrf g00fy- geerlingguy georgeOsdDev ghjm ghost +GhostLyrics giacmir giorgio-v gkoller +gotmax23 gottwald gstorme gundalow hansbaer hchargois hluaces +hubiongithub hwali hyperfocus1338 +IBims1NicerTobi igormukhingmailcom imjoseangel infigoKriti @@ -140,6 +155,7 @@ kalaisubbiah kenichi-ogawa-1988 kkeane klingac +kmarse koleo kotso kuntalFreshBooks @@ -150,6 +166,7 @@ ldesgrange leeadh LeonB leucos +lkthomas loomsen lorin lowwalker @@ -161,8 +178,8 @@ markdorison markotitel marktheunissen markuman -mattclay matt-horwood-mayden +mattclay mavimo maxamillion maxbube @@ -181,11 +198,15 @@ mkrizek mmoya mohag mohsenSy +moledzki mpdehaan +MRMegaNova MRwangyd +mstinsky mverwijs mvgrimes mysqlbox +n-cc netmonk nhojpatrick nicolas-g @@ -199,7 +220,9 @@ organman91 p53 pakal paulbadcock +paulcampbell-ayroc pennycoders +perlun petoju petracvv pgrenaud @@ -220,12 +243,14 @@ richlv riupie rndmh3ro robertdebock +robertsilen robpblake rokka-n Roxyrob roysmith rsicart rthouvenin +rujschafer ruudk samccann samdoran @@ -239,6 +264,7 @@ shrikeh sivel skalfyfan skoriy88 +SoledaD208 sperantus spoyd steverweber @@ -259,19 +285,22 @@ time-palominodb timorunge Tomasthanes tomdymond +tompal3 Tronde tuhoanganh tvlooy tyll UncertaintyP unnecessary-username +v-zhuravlev vamshi8 vanne vdboor vmahadev -v-zhuravlev +webknjaz webmat wedi +wfelipew whysthatso willthames windowsansiblernew diff --git a/MAINTAINERS b/MAINTAINERS index 97d0030..73feaa4 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -1,4 +1,3 @@ -bmalynovytch -Jorge-Rodriguez -rsicart -Andersson007 (andersson007_ in #ansible-community IRC/Matrix) +betanummeric +laurent-indermuehle +Andersson007 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b503e2f --- /dev/null +++ b/Makefile @@ -0,0 +1,116 @@ +SHELL := /bin/bash + +# To tell ansible-test and Make to not kill the containers on failure or +# end of tests. Disabled by default. +ifdef keep_containers_alive + _keep_containers_alive = --docker-terminate never +endif + +# This match what GitHub Action will do. Disabled by default. +ifdef continue_on_errors + _continue_on_errors = --continue-on-error +endif + +# Set command variables based on database engine +# Required for MariaDB 11+ which no longer includes mysql named compatible +# executable symlinks +ifeq ($(db_engine_name),mysql) + _command = mysqld + _health_cmd = mysqladmin +else + _command = mariadbd + _health_cmd = mariadb-admin +endif + +.PHONY: test-integration +test-integration: + @echo -n $(db_engine_name) > tests/integration/db_engine_name + @echo -n $(db_engine_version) > tests/integration/db_engine_version + @echo -n $(connector_name) > tests/integration/connector_name + @echo -n $(connector_version) > tests/integration/connector_version + @echo -n $(ansible) > tests/integration/ansible + + # Create podman network for systems missing it. Error can be ignored + podman network create podman || true + podman run \ + --detach \ + --replace \ + --name primary \ + --env MARIADB_ROOT_PASSWORD=msandbox \ + --env MYSQL_ROOT_PASSWORD=msandbox \ + --network podman \ + --publish 3307:3306 \ + --health-cmd '$(_health_cmd) ping -P 3306 -pmsandbox | grep alive || exit 1' \ + docker.io/library/$(db_engine_name):$(db_engine_version) \ + $(_command) + podman run \ + --detach \ + --replace \ + --name replica1 \ + --env MARIADB_ROOT_PASSWORD=msandbox \ + --env MYSQL_ROOT_PASSWORD=msandbox \ + --network podman \ + --publish 3308:3306 \ + --health-cmd '$(_health_cmd) ping -P 3306 -pmsandbox | grep alive || exit 1' \ + docker.io/library/$(db_engine_name):$(db_engine_version) \ + $(_command) + podman run \ + --detach \ + --replace \ + --name replica2 \ + --env MARIADB_ROOT_PASSWORD=msandbox \ + --env MYSQL_ROOT_PASSWORD=msandbox \ + --network podman \ + --publish 3309:3306 \ + --health-cmd '$(_health_cmd) ping -P 3306 -pmsandbox | grep alive || exit 1' \ + docker.io/library/$(db_engine_name):$(db_engine_version) \ + $(_command) + # Setup replication and restart containers using the same subshell to keep variables alive + db_ver=$(db_engine_version); \ + maj="$${db_ver%.*.*}"; \ + maj_min="$${db_ver%.*}"; \ + min="$${maj_min#*.}"; \ + if [[ "$(db_engine_name)" == "mysql" && "$$maj" -eq 8 && "$$min" -ge 2 ]]; then \ + prima_conf='[mysqld]\\nserver-id=1\\nlog-bin=/var/lib/mysql/primary-bin\\nmysql-native-password=1'; \ + repl1_conf='[mysqld]\\nserver-id=2\\nlog-bin=/var/lib/mysql/replica1-bin\\nmysql-native-password=1'; \ + repl2_conf='[mysqld]\\nserver-id=3\\nlog-bin=/var/lib/mysql/replica2-bin\\nmysql-native-password=1'; \ + else \ + prima_conf='[mysqld]\\nserver-id=1\\nlog-bin=/var/lib/mysql/primary-bin'; \ + repl1_conf='[mysqld]\\nserver-id=2\\nlog-bin=/var/lib/mysql/replica1-bin'; \ + repl2_conf='[mysqld]\\nserver-id=3\\nlog-bin=/var/lib/mysql/replica2-bin'; \ + fi; \ + podman exec -e cnf="$$prima_conf" primary bash -c 'echo -e "$${cnf//\\n/\n}" > /etc/mysql/conf.d/replication.cnf'; \ + podman exec -e cnf="$$repl1_conf" replica1 bash -c 'echo -e "$${cnf//\\n/\n}" > /etc/mysql/conf.d/replication.cnf'; \ + podman exec -e cnf="$$repl2_conf" replica2 bash -c 'echo -e "$${cnf//\\n/\n}" > /etc/mysql/conf.d/replication.cnf' + # Don't restart a container unless it is healthy + while ! podman healthcheck run primary && [[ "$$SECONDS" -lt 120 ]]; do sleep 1; done + podman restart -t 30 primary + while ! podman healthcheck run replica1 && [[ "$$SECONDS" -lt 120 ]]; do sleep 1; done + podman restart -t 30 replica1 + while ! podman healthcheck run replica2 && [[ "$$SECONDS" -lt 120 ]]; do sleep 1; done + podman restart -t 30 replica2 + while ! podman healthcheck run primary && [[ "$$SECONDS" -lt 120 ]]; do sleep 1; done + mkdir -p .venv/$(ansible) + python$(local_python_version) -m venv .venv/$(ansible) + + # Start venv (use `; \` to keep the same shell) + source .venv/$(ansible)/bin/activate; \ + python$(local_python_version) -m ensurepip; \ + python$(local_python_version) -m pip install --disable-pip-version-check \ + https://github.com/ansible/ansible/archive/$(ansible).tar.gz; \ + set -x; \ + ansible-test integration $(target) -v --color --coverage --diff \ + --docker ubuntu2204 \ + --docker-network podman $(_continue_on_errors) $(_keep_containers_alive); \ + set +x + # End of venv + + rm tests/integration/db_engine_name + rm tests/integration/db_engine_version + rm tests/integration/connector_name + rm tests/integration/connector_version + rm tests/integration/ansible +ifndef keep_containers_alive + podman stop --time 0 --ignore primary replica1 replica2 + podman rm --ignore --volumes primary replica1 replica2 +endif diff --git a/PSF-license.txt b/PSF-license.txt new file mode 100644 index 0000000..35acd7f --- /dev/null +++ b/PSF-license.txt @@ -0,0 +1,48 @@ +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 Python Software Foundation; +All Rights Reserved" are retained in Python alone or in any derivative version +prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. diff --git a/README.md b/README.md index e01d83b..df2404f 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,36 @@ -# 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) +# MySQL and MariaDB 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") [![Codecov](https://img.shields.io/codecov/c/github/ansible-collections/community.mysql)](https://codecov.io/gh/ansible-collections/community.mysql) [![Discuss on Matrix at #mysql:ansible.com](https://img.shields.io/matrix/mysql:ansible.com.svg?server_fqdn=ansible-accounts.ems.host&label=Discuss%20on%20Matrix%20at%20%23mysql:ansible.com&logo=matrix)](https://matrix.to/#/#mysql:ansible.com) This collection is a part of the Ansible package. +## Our mission + +The Ansible `community.mysql` collection goals are to produce and maintain simple, +flexible, and powerful open-source software for automating MySQL and MariaDB related tasks +providing good documentation for easy deployment and use. + ## 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. +## Communication + +* Join the Ansible forum: + * [Get Help](https://forum.ansible.com/c/help/6): get help or help others. + * [Posts tagged with 'mysql'](https://forum.ansible.com/tag/mysql): leverage tags to narrow the scope. + * [MySQL Team](https://forum.ansible.com/g/MySQLTeam): by joining the team you will automatically get subscribed to the posts tagged with [mysql](https://forum.ansible.com/tag/mysql). + * [Social Spaces](https://forum.ansible.com/c/chat/4): gather and interact with fellow enthusiasts. + * [News & Announcements](https://forum.ansible.com/c/news/5): track project-wide announcements including social events. + +* The Ansible [Bullhorn newsletter](https://docs.ansible.com/ansible/devel/community/communication.html#the-bullhorn): used to announce releases and important changes. + +* Matrix chat: + * [#mysql:ansible.com](https://matrix.to/#/#mysql:ansible.com) room: questions on how to contribute to this collection. + +For more information about communication, see the [Ansible communication guide](https://docs.ansible.com/ansible/devel/community/communication.html). + ## Contributing 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. @@ -17,7 +39,7 @@ 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](https://github.com/ansible-collections/community.mysql/blob/main/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) or ask us in the [#mysql:ansible.com room](https://matrix.to/#/#mysql:ansible.com) on [Matrix](https://docs.ansible.com/ansible/devel/community/communication.html#ansible-community-on-matrix)! ## Collection maintenance @@ -32,21 +54,13 @@ It is necessary for maintainers of this collection to be subscribed to: 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. - -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 +We, [the MySQL team](https://forum.ansible.com/g/MySQLTeam), use [the forum](https://forum.ansible.com/tag/mysql) posts tagged with `mysql` for general announcements and discussions. + 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! +Every voice is important and every idea is valuable. If you have something on your mind, create an issue or dedicated forum [discussion](https://forum.ansible.com/new-topic?title=topic%20title&body=topic%20body&category=project&tags=mysql) and let's discuss it! ## Included content @@ -59,36 +73,63 @@ 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 -- 2.9 -- 2.10 -- 2.11 -- 2.12 -- 2.13 -- devel +## Releases Support Timeline + +We maintain each major release (1.x.y, 2.x.y, ...) for two years after the next major version is released. + +Here is the table for the support timeline: + +- 1.x.y: released 2020-08-17, EOL +- 2.x.y: released 2021-04-15, EOL +- 3.x.y: released 2021-12-01, current +- 4.x.y: To be released + + +## Tested with + +### ansible-core + +- stable-2.16 +- stable-2.17 +- stable-2.18 +- current development version + +### Python + +- 3.8 (Unit tests only) +- 3.9 (Unit tests only) +- 3.10 (Sanity, Units and integrations tests) +- 3.11 (Unit tests only, collection version >= 3.10.0) ### Databases -- mysql 5.7.31 -- mysql 8.0.22 -- mariadb 10.3.34 -- mariadb 10.5.9 +For MariaDB, only Long Term releases are tested. When multiple LTS are available, we test the oldest and the newest only. Usually breaking changes introduced in the versions in between are also present in the latest version. + +- mysql 5.7.40 (collection version < 3.10.0) +- mysql 8.0.31 (collection version < 3.10.0) +- mysql 8.4.1 (collection version >= 3.10.0) !!! FAILING, no support yet !!! +- mariadb:10.3.34 (collection version < 3.5.1) +- mariadb:10.4.24 (collection version >= 3.5.2, < 3.10.0) +- mariadb:10.5.18 (collection version >= 3.5.2, < 3.10.0) +- mariadb:10.5.25 (collection version >= 3.10.0, <3.13.0) +- mariadb:10.6.11 (collection version >= 3.5.2, < 3.10.0) +- mariadb:10.11.8 (collection version >= 3.10.0) +- mariadb:11.4.5 (collection version >= 3.13.0) ### Database connectors -- pymysql 0.7.10 +- pymysql 0.7.11 (collection version < 3.10 and MySQL 5.7) - pymysql 0.9.3 -- mysqlclient 2.0.1 +- pymysql 0.10.1 (for RHEL8 context) +- pymysql 1.0.2 (collection version >= 3.6.1) +- pymysql 1.1.1 (collection version >= 3.10.0) ## External requirements -The MySQL modules rely on a MySQL connector. The list of supported drivers is below: +The MySQL modules rely on a [PyMySQL](https://github.com/PyMySQL/PyMySQL) connector. -- [PyMySQL](https://github.com/PyMySQL/PyMySQL) -- [mysqlclient](https://github.com/PyMySQL/mysqlclient) -- Support for other Python MySQL connectors may be added in a future release. +The `mysqlclient` connector support has been [deprecated](https://github.com/ansible-collections/community.mysql/issues/654) - use `PyMySQL` connector instead! We will stop testing against it in collection version 4.0.0 and remove the related code in 5.0.0. ## Using this collection diff --git a/REVIEW_CHECKLIST.md b/REVIEW_CHECKLIST.md deleted file mode 100644 index 9dccf7e..0000000 --- a/REVIEW_CHECKLIST.md +++ /dev/null @@ -1,3 +0,0 @@ -# Review Checklist - -Refer to the [Collection review checklist](https://github.com/ansible/community-docs/blob/main/review_checklist.rst). diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..45e6bba --- /dev/null +++ b/TESTING.md @@ -0,0 +1,143 @@ +# Tests + +This collection uses GitHub Actions to run ansible-test to validate its content. Three type of tests are used: Sanity, Integration and Units. + +The tests covers plugins and roles (no role available yet, but tests are ready) and can be found here: + +- Plugins: *.github/workflows/ansible-test-plugins.yml* +- Roles: *.github/workflows/ansible-test-roles.yml* (unused yet) + +Everytime you push on your fork or you create a pull request, both workflows runs. You can see the output on the "Actions" tab. + + +## Integration tests + +You can use GitHub to run ansible-test either on the community repo or your fork. But sometimes you want to quickly test a single version or a single target. To do that, you can use the Makefile present at the root of this repository. + +For now, the makefile only supports Podman. + + +### Requirements + +- python >= 3.8 +- make +- podman +- Minimum 15GB of free space on the device storing containers images and volumes. You can use this command to check: `podman system info --format='{{.Store.GraphRoot}}'|xargs findmnt --noheadings --nofsroot --output SOURCE --target|xargs df -h --output=size,used,avail,pcent,target` +- Minimum 2GB of RAM + + +### ansible-test environment + +Integration tests use the default container from ansible-test. Then required packages for the tests are installed from the `setup_controller` target located in the `tests/integration/targets` folder. + + +### Makefile options + +The Makefile accept the following options + +- `local_python_version` + - Mandatory: false + - Choices: + - "3.8" + - "3.9" + - "3.10" + - "3.11" (for stable-2.15+) + - Description: If `Python -V` shows an unsupported version, use this option to select a compatible Python version available on your system. Use `ls /usr/bin/python3*|grep -v config` to list the available versions (You may have to install one). Unsupported versions are those that are too recent for the Ansible version you are using. In such cases, you will see an error message similar to: 'This version of ansible-test cannot be executed with Python version 3.12.3. Supported Python versions are: 3.9, 3.10, 3.11'. + +- `ansible` + - Mandatory: true + - Choices: + - "stable-2.15" + - "stable-2.16" + - "stable-2.17" + - "devel" + - Description: Version of ansible to install in a venv to run ansible-test + +- `db_engine_name` + - Mandatory: true + - Choices: + - "mysql" + - "mariadb" + - Description: The name of the database engine to use for the service containers that will host a primary database and two replicas. + +- `db_engine_version` + - Mandatory: true + - Choices: + - "8.0.38" <- mysql + - "8.4.1" <- mysql (NOT WORKING YET, ansible-test uses Ubuntu 20.04 which is too old to install mysql-community-client 8.4) + - "10.11.8" <- mariadb + - "11.4.5" <- mariadb + - Description: The tag of the container to use for the service containers that will host a primary database and two replicas. Do not use short version, like `mysql:8` (don't do that) because our tests expect a full version to filter tests precisely. For instance: `when: db_version is version ('8.0.22', '>')`. You can use any tag available on [hub.docker.com/_/mysql](https://hub.docker.com/_/mysql) and [hub.docker.com/_/mariadb](https://hub.docker.com/_/mariadb) but GitHub Action will only use the versions listed above. + +- `connector_name` + - Mandatory: true + - Choices: + - "pymysql" + - "mysqlclient" + - Description: The python package of the connector to use. In addition to selecting the test container, this value is also used for tests filtering: `when: connector_name == 'pymysql'`. + +- `connector_version` + - Mandatory: true + - Choices: + - "0.9.3" <- pymysql + - "0.10.1" <- pymysql + - "1.0.2" <- pymysql + - "1.1.1" <- pymysql + - Description: The version of the python package of the connector to use. This value is used to filter tests meant for other connectors. + +- `target` + - Mandatory: false + - Choices: + - "test_mysql_db" + - "test_mysql_info" + - "test_mysql_query" + - "test_mysql_replication" + - "test_mysql_role" + - "test_mysql_user" + - "test_mysql_variables" + - Description: If omitted, all test targets will run. But you can limit the tests to a single target to speed up your tests. + +- `keep_containers_alive` + - Mandatory: false + - Description: This option keeps all tree databases containers and the ansible-test container alive at the end of tests or in case of failure. This is useful to enter one of the containers with `podman exec -it bash` for debugging. Rerunning the +tests will overwrite the 3 databases containers so no need to kill them in advance. But nothing will kill the ansible-test container. You must do that using `podman stop` and `podman rm`. Add any value to activate this option: `keep_containers_alive=1` + +- `continue_on_errors` + - Mandatory: false + - Description: Tells ansible-test to continue on errors. This is the way the GitHub Action's workflow runs the tests. This can be used to catch all errors in a single run, but you'll need to scroll up to find them. Add any value to activate this option: `continue_on_errors=1` + + +#### Makefile usage examples: + +```sh +# Run all targets +make ansible="stable-2.16" db_engine_name="mysql" db_engine_version="8.0.31" connector_name="pymysql" connector_version="1.0.2" + +# A single target +make ansible="stable-2.16" db_engine_name="mysql" db_engine_version="8.0.31" connector_name="pymysql" connector_version="1.0.2" target="test_mysql_info" + +# Keep databases and ansible tests containers alives +# A single target and continue on errors +make ansible="stable-2.17" db_engine_name="mysql" db_engine_version="8.0.31" connector_name="mysqlclient" connector_version="2.0.3" target="test_mysql_query" keep_containers_alive=1 continue_on_errors=1 + +# If your system has an usupported version of Python: +make local_python_version="3.10" ansible="stable-2.17" db_engine_name="mariadb" db_engine_version="11.4.5" connector_name="pymysql" connector_version="1.0.2" +``` + + +### Run all tests + +GitHub Action offer a test matrix that run every combination of MySQL, MariaDB and Connector against each other. To reproduce this, this repo provides a script called *run_all_tests.py*. + +Examples: + +```sh +python run_all_tests.py +``` + + +### Add a new Connector or Database version + +New components version should be added to this file: [.github/workflows/ansible-test-plugins.yml](https://github.com/ansible-collections/community.mysql/tree/main/.github/workflows) + +Be careful to not add too much tests. The matrix creates an exponential number of virtual machines! diff --git a/changelogs/CHANGELOG.rst b/changelogs/CHANGELOG.rst deleted file mode 100644 index 68a2d2e..0000000 --- a/changelogs/CHANGELOG.rst +++ /dev/null @@ -1,93 +0,0 @@ -======================================== -Community MySQL Collection Release Notes -======================================== - -.. contents:: Topics - -This changelog describes changes after version 2.0.0. - -v3.1.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`` 3.1.2. - -Bugfixes --------- - -- mysql_replication - fails when using the `primary_use_gtid` option with `slave_pos` or `replica_pos` (https://github.com/ansible-collections/community.mysql/issues/335). -- mysql_role - remove redundant connection closing (https://github.com/ansible-collections/community.mysql/pull/330). -- 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). - -v3.1.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`` 3.1.1. - -Bugfixes --------- - -- Collection core functions - fixes related to the mysqlclient Python connector (https://github.com/ansible-collections/community.mysql/issues/292). - -v3.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`` 3.1.0. - -Bugfixes --------- - -- mysql_role - make the ``set_default_role_all`` parameter actually working (https://github.com/ansible-collections/community.mysql/pull/282). - -v3.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`` 3.0.0. - -Minor Changes -------------- - -- Added explicit description of the supported versions of databases and connectors. Changes to the collection are **NOT** tested against database versions older than `mysql 5.7.31` and `mariadb 10.2.37` or connector versions older than `pymysql 0.7.10` and `mysqlclient 2.0.1`. (https://github.com/ansible-collections/community.mysql/discussions/141) -- 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). - -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). - -v3.0.0 -====== - -Release Summary ---------------- - -This is the major release of the ``community.mysql`` collection. -This changelog contains all breaking changes to the modules in this collection -that have been added after the release of ``community.mysql`` 2.3.2. - -Breaking Changes / Porting Guide --------------------------------- - -- mysql_replication - remove ``Is_Slave`` and ``Is_Master`` return values (were replaced with ``Is_Primary`` and ``Is_Replica`` (https://github.com/ansible-collections /community.mysql/issues/145). -- mysql_replication - remove the mode options values containing ``master``/``slave`` and the master_use_gtid option ``slave_pos`` (were replaced with corresponding ``primary``/``replica`` values) (https://github.com/ansible-collections/community.mysql/issues/145). -- mysql_user - remove support for the `REQUIRESSL` special privilege as it has ben superseded by the `tls_requires` option (https://github.com/ansible-collections/community.mysql/discussions/121). -- mysql_user - validate privileges using database engine directly (https://github.com/ansible-collections/community.mysql/issues/234 https://github.com/ansible-collections/community.mysql/pull/243). Do not validate privileges in this module anymore. diff --git a/changelogs/changelog.yaml b/changelogs/changelog.yaml index ceeb833..5ec7dc9 100644 --- a/changelogs/changelog.yaml +++ b/changelogs/changelog.yaml @@ -4,7 +4,7 @@ releases: changes: breaking_changes: - mysql_replication - remove ``Is_Slave`` and ``Is_Master`` return values (were - replaced with ``Is_Primary`` and ``Is_Replica`` (https://github.com/ansible-collections /community.mysql/issues/145). + replaced with ``Is_Primary`` and ``Is_Replica`` (https://github.com/ansible-collections/community.mysql/issues/145). - mysql_replication - remove the mode options values containing ``master``/``slave`` and the master_use_gtid option ``slave_pos`` (were replaced with corresponding ``primary``/``replica`` values) (https://github.com/ansible-collections/community.mysql/issues/145). @@ -97,3 +97,482 @@ releases: - 307-mysql_user_add_if_exists_to_drop.yml - 329-mysql_role-remove-redudant-connection-closing.yml release_date: '2022-04-26' + 3.10.0: + changes: + bugfixes: + - mysql_info - Add ``plugin_hash_string`` to ``users_info`` filter's output. + The existing ``plugin_auth_string`` contained the hashed password and thus + is missleading, it will be removed from community.mysql 4.0.0. (https://github.com/ansible-collections/community.mysql/pull/629). + - mysql_user - Added a warning to update_password's on_new_username option if + multiple accounts with the same username but different passwords exist (https://github.com/ansible-collections/community.mysql/pull/642). + - mysql_user - Fix ``tls_requires`` not removing ``SSL`` and ``X509`` when sets + as empty (https://github.com/ansible-collections/community.mysql/pull/628). + - mysql_user - Fix idempotence when using variables from the ``users_info`` + filter of ``mysql_info`` as an input (https://github.com/ansible-collections/community.mysql/pull/628). + - mysql_user - Fixed an IndexError in the update_password functionality introduced + in PR https://github.com/ansible-collections/community.mysql/pull/580 and + released in community.mysql 3.8.0. If you used this functionality, please + avoid versions 3.8.0 to 3.9.0 (https://github.com/ansible-collections/community.mysql/pull/642). + - mysql_user - add correct ``ed25519`` auth plugin handling (https://github.com/ansible-collections/community.mysql/issues/6). + - mysql_variables - fix the module always changes on boolean values (https://github.com/ansible-collections/community.mysql/issues/652). + deprecated_features: + - collection - support of mysqlclient connector is deprecated - use PyMySQL + connector instead! We will stop testing against it in collection version 4.0.0 + and remove the related code in 5.0.0 (https://github.com/ansible-collections/community.mysql/issues/654). + - mysql_info - The ``users_info`` filter returned variable ``plugin_auth_string`` + contains the hashed password and it's misleading, it will be removed from + community.mysql 4.0.0. Use the `plugin_hash_string` return value instead (https://github.com/ansible-collections/community.mysql/pull/629). + minor_changes: + - mysql_info - Add ``tls_requires`` returned value for the ``users_info`` filter + (https://github.com/ansible-collections/community.mysql/pull/628). + - mysql_info - return a database server engine used (https://github.com/ansible-collections/community.mysql/issues/644). + - mysql_replication - Adds support for `CHANGE REPLICATION SOURCE TO` statement + (https://github.com/ansible-collections/community.mysql/issues/635). + - mysql_replication - Adds support for `SHOW BINARY LOG STATUS` and `SHOW BINLOG + STATUS` on getprimary mode. + - mysql_replication - Improve detection of IsReplica and IsPrimary by inspecting + the dictionary returned from the SQL query instead of relying on variable + types. This ensures compatibility with changes in the connector or the output + of SHOW REPLICA STATUS and SHOW MASTER STATUS, allowing for easier maintenance + if these change in the future. + - mysql_user - Add salt parameter to generate static hash for `caching_sha2_password` + and `sha256_password` plugins. + release_summary: 'This is a minor release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules and plugins in this + + collection that have been made after the previous release.' + fragments: + - 0-mysql_user.yml + - 1-mysql_info.yml + - 2-mysql_variables.yml + - 3-deprecate_mysqlclient.yml + - 3.10.0.yml + - add_salt_param_to_gen_sha256_hash.yml + - get_primary_show_binary_log_status.yml + - improve_get_replica_primary_status.yml + - lie_fix_mysql_user_on_new_username.yml + - lie_fix_plugin_hash_string_return.yml + - mysql_user_tls_requires.yml + - supports_mysql_change_replication_source_to.yml + release_date: '2024-08-22' + 3.10.1: + changes: + bugfixes: + - mysql_user - module makes changes when is executed with ``plugin_auth_string`` + parameter and check mode. + deprecated_features: + - mysql_user - the ``user`` alias of the ``name`` argument has been deprecated + and will be removed in collection version 5.0.0. Use the ``name`` argument + instead. + release_summary: 'This is a patch release of the ``community.mysql`` collection. + + Besides a bugfix, it contains an important upcoming breaking-change information.' + fragments: + - 0-mysql_user.yml + - 3.10.1.yml + - 596-fix-check-changes.yaml + release_date: '2024-09-04' + 3.10.2: + changes: + bugfixes: + - mysql_user - add correct ``ed25519`` auth plugin handling when creating a + user (https://github.com/ansible-collections/community.mysql/issues/672). + release_summary: 'This is a bugfix release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules and plugins in this + + collection that have been made after the previous release.' + fragments: + - 0-mysql_user.yml + - 3.10.2.yml + release_date: '2024-09-06' + 3.10.3: + changes: + bugfixes: + - mysql_user - add correct ``ed25519`` auth plugin handling when creating a + user (https://github.com/ansible-collections/community.mysql/pull/676). + release_summary: 'This is a bugfix release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules and plugins in this + + collection that have been made after the previous release.' + fragments: + - 0-mysql_user.yml + - 3.10.3.yml + release_date: '2024-09-09' + 3.11.0: + changes: + bugfixes: + - mysql_user,mysql_role - The sql_mode ANSI_QUOTES affects how the modules mysql_user + and mysql_role compare the existing privileges with the configured privileges, + as well as decide whether double quotes or backticks should be used in the + GRANT statements. Pointing out in issue 671, the modules mysql_user and mysql_role + allow users to enable/disable ANSI_QUOTES in session variable (within a DB + session, the session variable always overwrites the global one). But due to + the issue, the modules do not check for ANSI_MODE in the session variable, + instead, they only check in the GLOBAL one.That behavior is not only limiting + the users' flexibility, but also not allowing users to explicitly disable + ANSI_MODE to work around such bugs like https://bugs.mysql.com/bug.php?id=115953. + (https://github.com/ansible-collections/community.mysql/issues/671) + minor_changes: + - mysql_info - adds the count of tables for each database to the returned values. + It is possible to exclude this new field using the ``db_table_count`` exclusion + filter. (https://github.com/ansible-collections/community.mysql/pull/691) + release_summary: 'This is a minor release of the ``community.mysql`` collection. + + + This changelog contains all changes to the modules and plugins in this + + collection that have been made after the previous release.' + fragments: + - 3.11.0.yml + - 591-mysql_info-db_tables_count.yml + - 671-modules_util_user.yml + release_date: '2024-11-19' + 3.12.0: + changes: + minor_changes: + - mysql_db - added ``zstd`` (de)compression support for ``import``/``dump`` + states (https://github.com/ansible-collections/community.mysql/issues/696). + - mysql_query - returns the ``execution_time_ms`` list containing execution + time per query in milliseconds. + release_summary: 'This is a minor release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules and plugins in this + + collection that have been made after the previous release.' + fragments: + - 0-mysql_query-returns-exec-time-ms.yml + - 3.12.0.yml + - 696-mysql-db-add-zstd-support.yml + release_date: '2025-01-17' + 3.13.0: + changes: + bugfixes: + - mysql_db - fix dump and import to find MariaDB binaries (mariadb and mariadb-dump) + when MariaDB 11+ is used and symbolic links to MySQL binaries are absent. + minor_changes: + - Integration tests for MariaDB 11.4 have replaced those for 10.5. The previous + version is now 10.11. + - mysql_user - add ``locked`` option to lock/unlock users, this is mainly used + to have users that will act as definers on stored procedures. + release_summary: 'This is a minor release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules and plugins in this + + collection that have been made after the previous release.' + fragments: + - 3.13.0.yml + - 702-user_locking.yaml + - tests_mariadb_11_4.yml + release_date: '2025-03-21' + 3.2.0: + changes: + bugfixes: + - 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). + 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). + minor_changes: + - 'mysql_user and mysql_role: Add the argument ``subtract_privs`` (boolean, + default false, mutually exclusive with ``append_privs``). If set, the privileges + given in ``priv`` are revoked and existing privileges are kept (https://github.com/ansible-collections/community.mysql/pull/333).' + 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`` 3.1.3.' + fragments: + - 001-mysql_user_fix_pars_users_with_roles_assigned.yml + - 3.2.0.yml + - 333-mysql_user-mysql_role-add-subtract_privileges-argument.yml + - 338-mysql_user_fix_missing_dynamic_privileges.yml + - drop_support_of_2.9-2.10.yml + release_date: '2022-05-13' + 3.2.1: + changes: + bugfixes: + - Include ``PSF-license.txt`` file for ``plugins/module_utils/_version.py``. + 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`` 3.2.0.' + fragments: + - 3.2.1.yml + - psf-license.yml + release_date: '2022-05-17' + 3.3.0: + changes: + bugfixes: + - mysql_query - fix false change reports when ``IF EXISTS/IF NOT EXISTS`` clause + is used (https://github.com/ansible-collections/community.mysql/issues/268). + - 'mysql_role - don''t add members to a role when creating the role and ``detach_members: + true`` is set (https://github.com/ansible-collections/community.mysql/pull/367).' + - 'mysql_role - in some cases (when "SHOW GRANTS" did not use backticks for + quotes), no unwanted members were detached from the role (and redundant "GRANT" + statements were executed for wanted members). This is fixed by querying the + existing role members from the mysql.role_edges (MySQL) or mysql.roles_mapping + (MariaDB) tables instead of parsing the "SHOW GRANTS" output (https://github.com/ansible-collections/community.mysql/pull/368). + + ' + - mysql_user - fix logic when ``update_password`` is set to ``on_create`` for + users using ``plugin*`` arguments (https://github.com/ansible-collections/community.mysql/issues/334). + The ``on_create`` sets ``password`` to None for old mysql_native_authentication + but not for authentiation methods which uses the ``plugin*`` arguments. This + PR changes this so ``on_create`` also exchange ``plugin``, ``plugin_hash_string``, + ``plugin_auth_string`` to None in the list of arguments to change + minor_changes: + - 'mysql_role - add the argument ``members_must_exist`` (boolean, default true). + The assertion that the users supplied in the ``members`` argument exist is + only executed when the new argument ``members_must_exist`` is ``true``, to + allow opt-out (https://github.com/ansible-collections/community.mysql/pull/369). + + ' + - 'mysql_user - Add the option ``on_new_username`` to argument ``update_password`` + to reuse the password (plugin and authentication_string) when creating a new + user if some user with the same name already exists. If the existing user + with the same name have varying passwords, the password from the arguments + is used like with ``update_password: always`` (https://github.com/ansible-collections/community.mysql/pull/365). + + ' + - 'mysql_user - Add the result field ``password_changed`` (boolean). It is true, + when the user got a new password. When the user was created with ``update_password: + on_new_username`` and an existing password was reused, ``password_changed`` + is false (https://github.com/ansible-collections/community.mysql/pull/365). + + ' + 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`` 3.2.1.' + fragments: + - 3.3.0.yml + - 322-mysql_query_fix_false_change_report.yml + - 334-mysql_user_fix_logic_on_oncreate.yml + - 365-mysql_user-add-on_new_username-and-password_changed.yml + - 367-mysql_role-fix-deatch-members.yml + - 368-mysql_role-fix-member-detection.yml + - 369_mysql_role-add-members_must_exist.yml + release_date: '2022-06-02' + 3.4.0: + changes: + bugfixes: + - Include ``simplified_bsd.txt`` license file for various module utils. + - mysql_db - Using compression masks errors messages from mysql_dump. By default + the fix is inactive to ensure retro-compatibility with system without bash. + To activate the fix, use the module option ``pipefail=true`` (https://github.com/ansible-collections/community.mysql/issues/256). + - mysql_replication - when the ``primary_ssl`` argument is set to ``no``, the + module will turn off SSL (https://github.com/ansible-collections/community.mysql/issues/393). + major_changes: + - mysql_db - the ``pipefail`` argument's default value will be changed to ``true`` + in community.mysql 4.0.0. If your target machines do not use ``bash`` as a + default interpreter, set ``pipefail`` to ``false`` explicitly. However, we + strongly recommend setting up ``bash`` as a default and ``pipefail=true`` + as it will protect you from getting broken dumps you don't know about (https://github.com/ansible-collections/community.mysql/issues/407). + minor_changes: + - mysql_db - add the ``chdir`` argument to avoid failings when a dump file contains + relative paths (https://github.com/ansible-collections/community.mysql/issues/395). + - mysql_db - add the ``pipefail`` argument to avoid broken dumps when ``state`` + is ``dump`` and compression is used (https://github.com/ansible-collections/community.mysql/issues/256). + 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`` 3.3.0.' + fragments: + - 0-mysql_db_add_chdir_argument.yml + - 1-mysql_replication_can_disable_master_ssl.yml + - 2-mysql_db_announce.yml + - 3.4.0.yml + - fix-256-mysql_dump-errors.yml + - simplified-bsd-license.yml + release_date: '2022-08-02' + 3.5.0: + changes: + bugfixes: + - mysql_user - grant option was revoked accidentally when modifying users. This + fix revokes grant option only when privs are setup to do that (https://github.com/ansible-collections/community.mysql/issues/77#issuecomment-1209693807). + minor_changes: + - 'mysql_replication - add a new option: ``primary_ssl_verify_server_cert`` + (https://github.com//pull/435).' + 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`` 3.4.0.' + fragments: + - 3.5.0.yml + - 434-do-not-revoke-grant-option-always.yaml + - 435-mysql_replication_verify_server_cert.yml + release_date: '2022-09-05' + 3.5.1: + changes: + bugfixes: + - mysql_user, mysql_role - mysql/mariadb recent versions translate 'ALL PRIVILEGES' + to a list of specific privileges. That caused a change every time we modified + user privileges. This fix compares privs before and after user modification + to avoid this infinite change (https://github.com/ansible-collections/community.mysql/issues/77). + release_summary: 'This is the patch release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules and plugins in this collection + + that have been made after the previous release.' + fragments: + - 3.5.1.yml + - 438-fix-privilege-changing-everytime.yml + release_date: '2022-09-09' + 3.6.0: + changes: + bugfixes: + - mysql_user - when revoke privs consists only of ``GRANT``, a 2nd revoke query + is executed with empty privs to revoke that ended in an SQL exception (https://github.com/ansible-collections/community.mysql/pull/503). + - mysql_variables - add uppercase character pattern to regex to allow GLOBAL + variables containing uppercase characters. This recognizes variable names + used in Galera, for example, ``wsrep_OSU_method``, which breaks the normal + pattern of all lowercase characters (https://github.com/ansible-collections/community.mysql/pull/501). + minor_changes: + - mysql_info - add ``connector_name`` and ``connector_version`` to returned + values (https://github.com/ansible-collections/community.mysql/pull/497). + - mysql_role - enable auto_commit to avoid MySQL metadata table lock (https://github.com/ansible-collections/community.mysql/issues/479). + - mysql_user - add plugin_auth_string as optional parameter to use a specific + pam service if pam/auth_pam plugin is used (https://github.com/ansible-collections/community.mysql/pull/445). + - mysql_user - add the ``session_vars`` argument to set session variables at + the beginning of module execution (https://github.com/ansible-collections/community.mysql/issues/478). + - mysql_user - display a more informative invalid privilege exception. Changes + the exception handling of the granting permission logic to show the query + executed , params and the exception message granting privileges fails` (https://github.com/ansible-collections/community.mysql/issues/465). + - mysql_user - enable auto_commit to avoid MySQL metadata table lock (https://github.com/ansible-collections/community.mysql/issues/479). + - setup_mysql - update MySQL tarball URL (https://github.com/ansible-collections/community.mysql/pull/491). + release_summary: 'This is the minor release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules and plugins in this collection + + that have been made after the previous release.' + fragments: + - 0_mysql_user_session_vars.yml + - 3.6.0.yml + - 445_add_service_name_to_plugin_pam_auth_pam_usage.yml + - 465-display_more_informative_invalid_priv_exceptiion.yml + - 479_enable_auto_commit.yml + - 479_enable_auto_commit_part2.yml + - 491_fix_download_url.yaml + - 497_mysql_info_returns_connector_name_and_version.yml + - 503-fix-revoke-grant-only.yml + - mysql_variables_allow_uppercase_identifiers.yml + release_date: '2023-02-08' + 3.7.0: + changes: + minor_changes: + - mysql module utils - change deprecated connection parameters ``passwd`` and + ``db`` to ``password`` and ``database`` (https://github.com/ansible-collections/community.mysql/pull/177). + - mysql_user - add ``MAX_STATEMENT_TIME`` support for mariadb to the ``resource_limits`` + argument (https://github.com/ansible-collections/community.mysql/issues/211). + release_summary: 'This is the minor release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules and plugins in this collection + + that have been made after the previous release.' + fragments: + - 177-change_deprecated_connection_parameters.yml + - 3.7.0.yml + - 523-add-max_statement_time_resource-limit.yml + release_date: '2023-05-05' + 3.7.1: + changes: + bugfixes: + - mysql module utils - use the connection arguments ``db`` instead of ``database`` + and ``passwd`` instead of ``password`` when running with older mysql drivers + (MySQLdb < 2.1.0 or PyMySQL < 1.0.0) (https://github.com/ansible-collections/community.mysql/pull/551). + release_summary: 'This is a patch release of the community.mysql collection. + + This changelog contains all changes to the modules and plugins in this collection + + that have been made after the previous release.' + fragments: + - 3.7.1.yml + - 551-fix_connection_arguments_driver_compatability.yaml + release_date: '2023-05-22' + 3.7.2: + changes: + bugfixes: + - mysql module utils - use the connection arguments ``db`` instead of ``database`` + and ``passwd`` instead of ``password`` when running with MySQLdb < 2.0.0 (https://github.com/ansible-collections/community.mysql/pull/553). + release_summary: 'This is a patch release of the community.mysql collection. + + This changelog contains all changes to the modules and plugins in this collection + + that have been made after the previous release.' + fragments: + - 3.7.2.yml + - 553_fix_connection_arguemnts_for_old_mysqldb_driver.yaml + release_date: '2023-05-25' + 3.8.0: + changes: + major_changes: + - The community.mysql collection no longer supports ``ansible-core 2.12`` and + ``ansible-core 2.13``. 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 those versions. Both are or will soon be End of Life and if you + are still using them, you should consider upgrading to the ``latest Ansible + / ansible-core 2.15 or later`` as soon as possible (https://github.com/ansible-collections/community.mysql/pull/574). + - mysql_role - the ``column_case_sensitive`` argument's default value will be + changed to ``true`` in community.mysql 4.0.0. If your playbook expected the + column to be automatically uppercased for your roles privileges, you should + set this to false explicitly (https://github.com/ansible-collections/community.mysql/issues/578). + - mysql_user - the ``column_case_sensitive`` argument's default value will be + changed to ``true`` in community.mysql 4.0.0. If your playbook expected the + column to be automatically uppercased for your users privileges, you should + set this to false explicitly (https://github.com/ansible-collections/community.mysql/issues/577). + minor_changes: + - mysql_info - add filter ``users_info`` (https://github.com/ansible-collections/community.mysql/pull/580). + - mysql_role - add ``column_case_sensitive`` option to prevent field names from + being uppercased (https://github.com/ansible-collections/community.mysql/pull/569). + - mysql_user - add ``column_case_sensitive`` option to prevent field names from + being uppercased (https://github.com/ansible-collections/community.mysql/pull/569). + release_summary: 'This is the minor release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules and plugins in this + + collection that have been made after the previous release.' + fragments: + - 3.8.0.yml + - 569_fix_column_uppercasing.yml + - drop_ansible_core_2_12_and_2_13.yml + - lie_mysql_info_users_info.yml + release_date: '2023-10-25' + 3.9.0: + changes: + bugfixes: + - mysql_info - the ``slave_status`` filter was returning an empty list on MariaDB + with multiple replication channels. It now returns all channels by running + ``SHOW ALL SLAVES STATUS`` for MariaDB servers (https://github.com/ansible-collections/community.mysql/issues/603). + major_changes: + - Collection version 2.*.* is EOL, no more bugfixes will be backported. Please + consider upgrading to the latest version. + minor_changes: + - mysql_user - add the ``password_expire`` and ``password_expire_interval`` + arguments to implement the password expiration management for mysql user (https://github.com/ansible-collections/community.mysql/pull/598). + - mysql_user - add user attribute support via the ``attributes`` parameter and + return value (https://github.com/ansible-collections/community.mysql/pull/604). + release_summary: 'This is a minor release of the ``community.mysql`` collection. + + This changelog contains all changes to the modules and plugins in this + + collection that have been made after the previous release.' + fragments: + - 0-stable-2-eol.yml + - 3.9.0.yml + - 598-password_expire-support-for-mysql_user.yml + - 602-show-all-slaves-status.yaml + - 604-user-attributes.yaml + release_date: '2024-02-22' diff --git a/changelogs/config.yaml b/changelogs/config.yaml index 559e6c4..40ac5f8 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 @@ -25,5 +25,5 @@ sections: - Bugfixes - - known_issues - Known Issues -title: Community MySQL Collection +title: Community MySQL and MariaDB Collection trivial_section_name: trivial diff --git a/galaxy.yml b/galaxy.yml index cbf2b3a..624c7d6 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -1,10 +1,11 @@ +--- namespace: community name: mysql -version: 3.1.3 +version: 3.13.0 readme: README.md authors: - Ansible community -description: MySQL collection for Ansible +description: MySQL and MariaDB collection for Ansible license_file: COPYING tags: - database diff --git a/plugins/doc_fragments/mysql.py b/plugins/doc_fragments/mysql.py index 4b531d4..a52243b 100644 --- a/plugins/doc_fragments/mysql.py +++ b/plugins/doc_fragments/mysql.py @@ -35,6 +35,7 @@ options: login_unix_socket: description: - The path to a Unix domain socket for local connections. + - Use this parameter to avoid the C(Please explicitly state intended protocol) error. type: str connect_timeout: description: @@ -44,6 +45,9 @@ options: config_file: description: - Specify a config file from which user and password are to be read. + - The default config file, C(~/.my.cnf), if it exists, will be read, even if I(config_file) is not specified. + - The default config file, C(~/.my.cnf), if it exists, must contain a C([client]) section as a MySQL connector requirement. + - To prevent the default config file from being read, set I(config_file) to be an empty string. type: path default: '~/.my.cnf' ca_cert: @@ -67,24 +71,21 @@ 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 option has no effect on MySQLdb. type: bool version_added: '1.1.0' requirements: - - mysqlclient (Python 3.5+) or - - PyMySQL (Python 2.7 and Python 3.x) or - - MySQLdb (Python 2.x) + - PyMySQL (Python 2.7 and Python 3.x) notes: - - Requires the PyMySQL (Python 2.7 and Python 3.X) or MySQL-python (Python 2.X) package installed on the remote host. + - Requires the PyMySQL (Python 2.7 and Python 3.X) package installed on the remote host. 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 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. + - Be sure you have PyMySQL 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. If ansible discovers and uses Python 2, you need to install the Python 2 + version of PyMySQL. - 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 + 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 @@ -95,7 +96,15 @@ notes: and later uses the unix_socket authentication plugin by default that without using I(login_unix_socket=/var/run/mysqld/mysqld.sock) (the default path) causes the error ``Host '127.0.0.1' is not allowed to connect to this MariaDB server``. - - Alternatively, you can use the mysqlclient library instead of MySQL-python (MySQLdb) - which supports both Python 2.X and Python >=3.5. - See U(https://pypi.org/project/mysqlclient/) how to install it. + - "If credentials from the config file (for example, C(/root/.my.cnf)) are not needed to connect to a database server, but + the file exists and does not contain a C([client]) section, before any other valid directives, it will be read and this + will cause the connection to fail, to prevent this set it to an empty string, (for example C(config_file: ''))." + - "To avoid the C(Please explicitly state intended protocol) error, use the I(login_unix_socket) argument, + for example, C(login_unix_socket: /run/mysqld/mysqld.sock)." + - Alternatively, to avoid using I(login_unix_socket) argument on each invocation you can specify the socket path + using the `socket` option in your MySQL config file (usually C(~/.my.cnf)) on the destination host, for + example C(socket=/var/lib/mysql/mysql.sock). +attributes: + check_mode: + description: Can run in check_mode and return changed status prediction without modifying target. ''' diff --git a/plugins/module_utils/_version.py b/plugins/module_utils/_version.py index 59ee9db..ce02717 100644 --- a/plugins/module_utils/_version.py +++ b/plugins/module_utils/_version.py @@ -3,7 +3,7 @@ # 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) +# PSF License (see PSF-license.txt or https://opensource.org/licenses/Python-2.0) # """Provides classes to represent module version numbers (one class for diff --git a/plugins/module_utils/command_resolver.py b/plugins/module_utils/command_resolver.py new file mode 100644 index 0000000..4374879 --- /dev/null +++ b/plugins/module_utils/command_resolver.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- + +from __future__ import (absolute_import, division, print_function) +from ._version import LooseVersion +__metaclass__ = type + + +class CommandResolver(): + def __init__(self, server_implementation, server_version): + self.server_implementation = server_implementation + self.server_version = LooseVersion(server_version) + + def resolve_command(self, command): + """ + Resolves the appropriate SQL command based on the server implementation and version. + + Parameters: + command (str): The base SQL command to be resolved (e.g., "SHOW SLAVE HOSTS"). + + Returns: + str: The resolved SQL command suitable for the given server implementation and version. + + Raises: + ValueError: If the command is not supported or recognized. + + Example: + Given a server implementation `mysql` and server version `8.0.23`, and a command `SHOW SLAVE HOSTS`, + the method will resolve the command based on the following table of versions: + + Table: + [ + ("mysql", "default", "SHOW SLAVES HOSTS default"), + ("mysql", "5.7.0", "SHOW SLAVES HOSTS"), + ("mysql", "8.0.22", "SHOW REPLICAS"), + ("mysql", "8.4.0", "SHOW REPLICAS 8.4"), + ("mariadb", "10.5.1", "SHOW REPLICAS HOSTS"), + ] + + Example usage: + >>> resolver = CommandResolver("mysql", "8.0.23") + >>> resolver.resolve_command("SHOW SLAVE HOSTS") + 'SHOW REPLICAS' + + In this example, the resolver will: + - Filter and sort applicable versions: [ + ("8.4.0", "SHOW REPLICAS 8.4"), + ("8.0.22", "HOW REPLICAS"), + ("5.7.0", "SHOW SLAVES HOSTS") + ] + + - Iterate through the sorted list and find the first version less than or equal to 8.0.23, + which is 8.0.22, and return the corresponding command. + """ + + # Convert the command to uppercase to ensure case-insensitive lookup + command = command.upper() + + commands = { + "SHOW MASTER STATUS": { + ("mysql", "default"): "SHOW MASTER STATUS", + ("mariadb", "default"): "SHOW MASTER STATUS", + ("mysql", "8.2.0"): "SHOW BINARY LOG STATUS", + ("mariadb", "10.5.2"): "SHOW BINLOG STATUS", + }, + "SHOW SLAVE STATUS": { + ("mysql", "default"): "SHOW SLAVE STATUS", + ("mariadb", "default"): "SHOW SLAVE STATUS", + ("mysql", "8.0.22"): "SHOW REPLICA STATUS", + ("mariadb", "10.5.1"): "SHOW REPLICA STATUS", + }, + "SHOW SLAVE HOSTS": { + ("mysql", "default"): "SHOW SLAVE HOSTS", + ("mariadb", "default"): "SHOW SLAVE HOSTS", + ("mysql", "8.0.22"): "SHOW REPLICAS", + ("mariadb", "10.5.1"): "SHOW REPLICA HOSTS", + }, + "CHANGE MASTER": { + ("mysql", "default"): "CHANGE MASTER", + ("mariadb", "default"): "CHANGE MASTER", + ("mysql", "8.0.23"): "CHANGE REPLICATION SOURCE", + }, + "MASTER_HOST": { + ("mysql", "default"): "MASTER_HOST", + ("mariadb", "default"): "MASTER_HOST", + ("mysql", "8.0.23"): "SOURCE_HOST", + }, + "MASTER_USER": { + ("mysql", "default"): "MASTER_USER", + ("mariadb", "default"): "MASTER_USER", + ("mysql", "8.0.23"): "SOURCE_USER", + }, + "MASTER_PASSWORD": { + ("mysql", "default"): "MASTER_PASSWORD", + ("mariadb", "default"): "MASTER_PASSWORD", + ("mysql", "8.0.23"): "SOURCE_PASSWORD", + }, + "MASTER_PORT": { + ("mysql", "default"): "MASTER_PORT", + ("mariadb", "default"): "MASTER_PORT", + ("mysql", "8.0.23"): "SOURCE_PORT", + }, + "MASTER_CONNECT_RETRY": { + ("mysql", "default"): "MASTER_CONNECT_RETRY", + ("mariadb", "default"): "MASTER_CONNECT_RETRY", + ("mysql", "8.0.23"): "SOURCE_CONNECT_RETRY", + }, + "MASTER_LOG_FILE": { + ("mysql", "default"): "MASTER_LOG_FILE", + ("mariadb", "default"): "MASTER_LOG_FILE", + ("mysql", "8.0.23"): "SOURCE_LOG_FILE", + }, + "MASTER_LOG_POS": { + ("mysql", "default"): "MASTER_LOG_POS", + ("mariadb", "default"): "MASTER_LOG_POS", + ("mysql", "8.0.23"): "SOURCE_LOG_POS", + }, + "MASTER_DELAY": { + ("mysql", "default"): "MASTER_DELAY", + ("mariadb", "default"): "MASTER_DELAY", + ("mysql", "8.0.23"): "SOURCE_DELAY", + }, + "MASTER_SSL": { + ("mysql", "default"): "MASTER_SSL", + ("mariadb", "default"): "MASTER_SSL", + ("mysql", "8.0.23"): "SOURCE_SSL", + }, + "MASTER_SSL_CA": { + ("mysql", "default"): "MASTER_SSL_CA", + ("mariadb", "default"): "MASTER_SSL_CA", + ("mysql", "8.0.23"): "SOURCE_SSL_CA", + }, + "MASTER_SSL_CAPATH": { + ("mysql", "default"): "MASTER_SSL_CAPATH", + ("mariadb", "default"): "MASTER_SSL_CAPATH", + ("mysql", "8.0.23"): "SOURCE_SSL_CAPATH", + }, + "MASTER_SSL_CERT": { + ("mysql", "default"): "MASTER_SSL_CERT", + ("mariadb", "default"): "MASTER_SSL_CERT", + ("mysql", "8.0.23"): "SOURCE_SSL_CERT", + }, + "MASTER_SSL_KEY": { + ("mysql", "default"): "MASTER_SSL_KEY", + ("mariadb", "default"): "MASTER_SSL_KEY", + ("mysql", "8.0.23"): "SOURCE_SSL_KEY", + }, + "MASTER_SSL_CIPHER": { + ("mysql", "default"): "MASTER_SSL_CIPHER", + ("mariadb", "default"): "MASTER_SSL_CIPHER", + ("mysql", "8.0.23"): "SOURCE_SSL_CIPHER", + }, + "MASTER_SSL_VERIFY_SERVER_CERT": { + ("mysql", "default"): "MASTER_SSL_VERIFY_SERVER_CERT", + ("mariadb", "default"): "MASTER_SSL_VERIFY_SERVER_CERT", + ("mysql", "8.0.23"): "SOURCE_SSL_VERIFY_SERVER_CERT", + }, + "MASTER_AUTO_POSITION": { + ("mysql", "default"): "MASTER_AUTO_POSITION", + ("mariadb", "default"): "MASTER_AUTO_POSITION", + ("mysql", "8.0.23"): "SOURCE_AUTO_POSITION", + }, + "RESET MASTER": { + ("mysql", "default"): "RESET MASTER", + ("mariadb", "default"): "RESET MASTER", + ("mysql", "8.4.0"): "RESET BINARY LOGS AND GTIDS", + }, + # Add more command mappings here + } + + if command in commands: + cmd_syntaxes = commands[command] + applicable_versions = [(v, cmd) for (impl, v), cmd in cmd_syntaxes.items() if impl == self.server_implementation and v != 'default'] + applicable_versions.sort(reverse=True, key=lambda x: LooseVersion(x[0])) + + for version, cmd in applicable_versions: + if self.server_version >= LooseVersion(version): + return cmd + + return cmd_syntaxes[(self.server_implementation, "default")] + raise ValueError("Unsupported command: %s" % command) diff --git a/plugins/module_utils/database.py b/plugins/module_utils/database.py index 6785030..da0375d 100644 --- a/plugins/module_utils/database.py +++ b/plugins/module_utils/database.py @@ -6,7 +6,7 @@ # # Copyright (c) 2014, Toshio Kuratomi # -# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) +# Simplified BSD License (see simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) from __future__ import (absolute_import, division, print_function) __metaclass__ = type diff --git a/plugins/module_utils/implementations/mariadb/replication.py b/plugins/module_utils/implementations/mariadb/replication.py index cee4967..a1733e7 100644 --- a/plugins/module_utils/implementations/mariadb/replication.py +++ b/plugins/module_utils/implementations/mariadb/replication.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- + +# 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 diff --git a/plugins/module_utils/implementations/mariadb/role.py b/plugins/module_utils/implementations/mariadb/role.py index a3c9ea5..d227d59 100644 --- a/plugins/module_utils/implementations/mariadb/role.py +++ b/plugins/module_utils/implementations/mariadb/role.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- + +# 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 diff --git a/plugins/module_utils/implementations/mariadb/user.py b/plugins/module_utils/implementations/mariadb/user.py index 7579157..fa9ecdf 100644 --- a/plugins/module_utils/implementations/mariadb/user.py +++ b/plugins/module_utils/implementations/mariadb/user.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- + +# 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 @@ -13,3 +17,60 @@ def use_old_user_mgmt(cursor): def supports_identified_by_password(cursor): return True + + +def server_supports_alter_user(cursor): + version = get_server_version(cursor) + + return LooseVersion(version) >= LooseVersion("10.2") + + +def server_supports_password_expire(cursor): + version = get_server_version(cursor) + + return LooseVersion(version) >= LooseVersion("10.4.3") + + +def get_tls_requires(cursor, user, host): + """Get user TLS requirements. + Reads directly from mysql.user table allowing for a more + readable code. + + Args: + cursor (cursor): DB driver cursor object. + user (str): User name. + host (str): User host name. + + Returns: Dictionary containing current TLS required + """ + tls_requires = dict() + + query = ('SELECT ssl_type, ssl_cipher, x509_issuer, x509_subject ' + 'FROM mysql.user WHERE User = %s AND Host = %s') + cursor.execute(query, (user, host)) + res = cursor.fetchone() + + # Mysql_info use a DictCursor so we must convert back to a list + # otherwise we get KeyError 0 + if isinstance(res, dict): + res = list(res.values()) + + # When user don't require SSL, res value is: ('', '', '', '') + if not any(res): + return None + + if res[0] == 'ANY': + tls_requires['SSL'] = None + + if res[0] == 'X509': + tls_requires['X509'] = None + + if res[1]: + tls_requires['CIPHER'] = res[1] + + if res[2]: + tls_requires['ISSUER'] = res[2] + + if res[3]: + tls_requires['SUBJECT'] = res[3] + return tls_requires diff --git a/plugins/module_utils/implementations/mysql/hash.py b/plugins/module_utils/implementations/mysql/hash.py new file mode 100644 index 0000000..0068a0c --- /dev/null +++ b/plugins/module_utils/implementations/mysql/hash.py @@ -0,0 +1,125 @@ +""" +Generate MySQL sha256 compatible plugins hash for a given password and salt + +based on + * https://www.akkadia.org/drepper/SHA-crypt.txt + * https://crypto.stackexchange.com/questions/77427/whats-the-algorithm-behind-mysqls-sha256-password-hashing-scheme/111174#111174 + * https://github.com/hashcat/hashcat/blob/master/tools/test_modules/m07400.pm +""" + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import hashlib + + +def _to64(v, n): + """Convert a 32-bit integer to a base-64 string""" + i64 = ( + [".", "/"] + + [chr(x) for x in range(48, 58)] + + [chr(x) for x in range(65, 91)] + + [chr(x) for x in range(97, 123)] + ) + result = "" + while n > 0: + n -= 1 + result += i64[v & 0x3F] + v >>= 6 + return result + + +def _hashlib_sha256(data): + """Return SHA-256 digest from hashlib .""" + return hashlib.sha256(data).digest() + + +def _sha256_digest(key, salt, loops): + """Return a SHA-256 digest of the concatenation of the key, the salt, and the key, repeated as necessary.""" + # https://www.akkadia.org/drepper/SHA-crypt.txt + num_bytes = 32 + bytes_key = key.encode() + bytes_salt = salt.encode() + digest_b = _hashlib_sha256(bytes_key + bytes_salt + bytes_key) + + tmp = bytes_key + bytes_salt + for i in range(len(bytes_key), 0, -num_bytes): + tmp += digest_b if i > num_bytes else digest_b[:i] + + i = len(bytes_key) + while i > 0: + tmp += digest_b if (i & 1) != 0 else bytes_key + i >>= 1 + + digest_a = _hashlib_sha256(tmp) + + tmp = b"" + for i in range(len(bytes_key)): + tmp += bytes_key + + digest_dp = _hashlib_sha256(tmp) + + byte_sequence_p = b"" + for i in range(len(bytes_key), 0, -num_bytes): + byte_sequence_p += digest_dp if i > num_bytes else digest_dp[:i] + + tmp = b"" + til = 16 + digest_a[0] + + for i in range(til): + tmp += bytes_salt + + digest_ds = _hashlib_sha256(tmp) + + byte_sequence_s = b"" + for i in range(len(bytes_salt), 0, -num_bytes): + byte_sequence_s += digest_ds if i > num_bytes else digest_ds[:i] + + digest_c = digest_a + + for i in range(loops): + tmp = byte_sequence_p if (i & 1) else digest_c + if i % 3: + tmp += byte_sequence_s + if i % 7: + tmp += byte_sequence_p + tmp += digest_c if (i & 1) else byte_sequence_p + digest_c = _hashlib_sha256(tmp) + + inc1, inc2, mod, end = (10, 21, 30, 0) + + i = 0 + tmp = "" + + while True: + tmp += _to64( + (digest_c[i] << 16) + | (digest_c[(i + inc1) % mod] << 8) + | digest_c[(i + inc1 * 2) % mod], + 4, + ) + i = (i + inc2) % mod + if i == end: + break + + tmp += _to64((digest_c[31] << 8) | digest_c[30], 3) + + return tmp + + +def mysql_sha256_password_hash(password, salt): + """Return a MySQL compatible caching_sha2_password hash in raw format.""" + if len(salt) != 20: + raise ValueError("Salt must be 20 characters long.") + + count = 5 + iteration = 1000 * count + + digest = _sha256_digest(password, salt, iteration) + return "$A${0:>03}${1}{2}".format(count, salt, digest) + + +def mysql_sha256_password_hash_hex(password, salt): + """Return a MySQL compatible caching_sha2_password hash in hex format.""" + return mysql_sha256_password_hash(password, salt).encode().hex().upper() diff --git a/plugins/module_utils/implementations/mysql/replication.py b/plugins/module_utils/implementations/mysql/replication.py index c5324da..2e50bea 100644 --- a/plugins/module_utils/implementations/mysql/replication.py +++ b/plugins/module_utils/implementations/mysql/replication.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- + +# 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 diff --git a/plugins/module_utils/implementations/mysql/role.py b/plugins/module_utils/implementations/mysql/role.py index f9686c5..932d74a 100644 --- a/plugins/module_utils/implementations/mysql/role.py +++ b/plugins/module_utils/implementations/mysql/role.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- + +# 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 diff --git a/plugins/module_utils/implementations/mysql/user.py b/plugins/module_utils/implementations/mysql/user.py index 43e400b..700c355 100644 --- a/plugins/module_utils/implementations/mysql/user.py +++ b/plugins/module_utils/implementations/mysql/user.py @@ -1,9 +1,16 @@ +# -*- coding: utf-8 -*- + +# 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 from ansible_collections.community.mysql.plugins.module_utils.version import LooseVersion from ansible_collections.community.mysql.plugins.module_utils.mysql import get_server_version +import re +import shlex + def use_old_user_mgmt(cursor): version = get_server_version(cursor) @@ -14,3 +21,58 @@ def use_old_user_mgmt(cursor): def supports_identified_by_password(cursor): version = get_server_version(cursor) return LooseVersion(version) < LooseVersion("8") + + +def server_supports_alter_user(cursor): + version = get_server_version(cursor) + + return LooseVersion(version) >= LooseVersion("5.6") + + +def server_supports_password_expire(cursor): + version = get_server_version(cursor) + + return LooseVersion(version) >= LooseVersion("5.7") + + +def get_tls_requires(cursor, user, host): + """Get user TLS requirements. + We must use SHOW GRANTS because some tls fileds are encoded. + + Args: + cursor (cursor): DB driver cursor object. + user (str): User name. + host (str): User host name. + + Returns: Dictionary containing current TLS required + """ + if not 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) + grants = cursor.fetchone() + + # Mysql_info use a DictCursor so we must convert back to a list + # otherwise we get KeyError 0 + if isinstance(grants, dict): + grants = list(grants.values()) + grants_str = ''.join(grants) + + pattern = r"(?<=\bREQUIRE\b)(.*?)(?=(?:\bPASSWORD\b|$))" + requires_match = re.search(pattern, grants_str) + requires = requires_match.group().strip() if requires_match else "" + + if requires.startswith('NONE'): + return None + + if requires.startswith('SSL'): + return {'SSL': None} + + if requires.startswith('X509'): + return {'X509': None} + + items = iter(shlex.split(requires)) + requires = dict(zip(items, items)) + return requires or None diff --git a/plugins/module_utils/mysql.py b/plugins/module_utils/mysql.py index 9492ea8..9758994 100644 --- a/plugins/module_utils/mysql.py +++ b/plugins/module_utils/mysql.py @@ -7,7 +7,7 @@ # Copyright (c), Jonathan Mainguy , 2015 # Most of this was originally added by Sven Schliesing @muffl0n in the mysql_user.py module # -# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) +# Simplified BSD License (see simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) from __future__ import (absolute_import, division, print_function) from functools import reduce @@ -23,6 +23,7 @@ try: _mysql_cursor_param = 'cursor' except ImportError: try: + # mysqlclient is called MySQLdb import MySQLdb as mysql_driver import MySQLdb.cursors _mysql_cursor_param = 'cursorclass' @@ -34,6 +35,45 @@ mysql_driver_fail_msg = ('A MySQL module is required: for Python 2.7 either PyMy 'Consider setting ansible_python_interpreter to use ' 'the intended Python version.') +from ansible_collections.community.mysql.plugins.module_utils.database import mysql_quote_identifier + + +def get_connector_name(connector): + """ (class) -> str + Return the name of the connector (pymysql or mysqlclient (MySQLdb)) + or 'Unknown' if not pymysql or MySQLdb. When adding a + connector here, also modify get_connector_version. + """ + if connector is None or not hasattr(connector, '__name__'): + return 'Unknown' + + return connector.__name__ + + +def get_connector_version(connector): + """ (class) -> str + Return the version of pymysql or mysqlclient (MySQLdb). + Return 'Unknown' if the connector name is unknown. + """ + + if connector is None: + return 'Unknown' + + connector_name = get_connector_name(connector) + + if connector_name == 'pymysql': + # pymysql has two methods: + # - __version__ that returns the string: 0.7.11.None + # - VERSION that returns the tuple (0, 7, 11, None) + v = connector.VERSION[:3] + return '.'.join(map(str, v)) + elif connector_name == 'MySQLdb': + # version_info returns the tuple (2, 1, 1, 'final', 0) + v = connector.version_info[:3] + return '.'.join(map(str, v)) + else: + return 'Unknown' + def parse_from_mysql_config_file(cnf): # Default values of comment_prefix is '#' and ';'. @@ -82,7 +122,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['passwd'] = login_password + config['password'] = login_password if ssl_cert is not None: config['ssl']['cert'] = ssl_cert if ssl_key is not None: @@ -90,22 +130,45 @@ 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['db'] = db + config['database'] = db if connect_timeout is not None: config['connect_timeout'] = connect_timeout if check_hostname is not None: - if mysql_driver.__name__ == "pymysql": + if get_connector_name(mysql_driver) == 'pymysql': version_tuple = (n for n in mysql_driver.__version__.split('.') if n != 'None') if reduce(lambda x, y: int(x) * 100 + int(y), version_tuple) >= 711: config['ssl']['check_hostname'] = check_hostname else: module.fail_json(msg='To use check_hostname, pymysql >= 0.7.11 is required on the target host') - if _mysql_cursor_param == 'cursor': + if get_connector_name(mysql_driver) == 'pymysql': # In case of PyMySQL driver: + if mysql_driver.version_info[0] < 1: + # for PyMySQL < 1.0.0, use 'db' instead of 'database' and 'passwd' instead of 'password' + if 'database' in config: + config['db'] = config['database'] + del config['database'] + if 'password' in config: + config['passwd'] = config['password'] + del config['password'] db_connection = mysql_driver.connect(autocommit=autocommit, **config) else: # In case of MySQLdb driver + + # Will be deprecated and dropped + # https://github.com/ansible-collections/community.mysql/issues/654 + module.warn('Support of mysqlcline/MySQLdb connector is deprecated. ' + 'We\'ll stop testing against it in collection version 4.0.0 ' + 'and remove the related code in 5.0.0. Use PyMySQL connector instead.') + + if mysql_driver.version_info[0] < 2 or (mysql_driver.version_info[0] == 2 and mysql_driver.version_info[1] < 1): + # for MySQLdb < 2.1.0, use 'db' instead of 'database' and 'passwd' instead of 'password' + if 'database' in config: + config['db'] = config['database'] + del config['database'] + if 'password' in config: + config['passwd'] = config['password'] + del config['password'] db_connection = mysql_driver.connect(**config) if autocommit: db_connection.autocommit(True) @@ -149,3 +212,20 @@ def get_server_version(cursor): version_str = result[0] return version_str + + +def get_server_implementation(cursor): + if 'mariadb' in get_server_version(cursor).lower(): + return "mariadb" + else: + return "mysql" + + +def set_session_vars(module, cursor, session_vars): + """Set session vars.""" + for var, value in session_vars.items(): + query = "SET SESSION %s = " % mysql_quote_identifier(var, 'vars') + try: + cursor.execute(query + "%s", (value,)) + except Exception as e: + module.fail_json(msg='Failed to execute %s%s: %s' % (query, value, e)) diff --git a/plugins/module_utils/user.py b/plugins/module_utils/user.py index 8fe0629..9de1c6d 100644 --- a/plugins/module_utils/user.py +++ b/plugins/module_utils/user.py @@ -1,4 +1,6 @@ from __future__ import (absolute_import, division, print_function) + + __metaclass__ = type # This code is part of Ansible, but is an independent component. @@ -7,15 +9,21 @@ __metaclass__ = type # 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) +# Simplified BSD License (see simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) import string +import json import re from ansible.module_utils.six import iteritems from ansible_collections.community.mysql.plugins.module_utils.mysql import ( mysql_driver, + get_server_implementation, +) +from ansible_collections.community.mysql.plugins.module_utils.implementations.mysql.hash import ( + mysql_sha256_password_hash, + mysql_sha256_password_hash_hex, ) @@ -24,7 +32,7 @@ class InvalidPrivsError(Exception): def get_mode(cursor): - cursor.execute('SELECT @@GLOBAL.sql_mode') + cursor.execute('SELECT @@sql_mode') result = cursor.fetchone() mode_str = result[0] if 'ANSI' in mode_str: @@ -44,6 +52,25 @@ def user_exists(cursor, user, host, host_all): return count[0] > 0 +def user_is_locked(cursor, user, host): + cursor.execute("SHOW CREATE USER %s@%s", (user, host)) + + # Per discussions on irc:libera.chat:#maria the query may return up to 2 rows but "ACCOUNT LOCK" should always be in the first row. + result = cursor.fetchone() + + # ACCOUNT LOCK does not have to be the last option in the CREATE USER query. + # Need to handle both DictCursor and non-DictCursor + if isinstance(result, tuple): + if result[0].find('ACCOUNT LOCK') > 0: + return True + elif isinstance(result, dict): + for res in result.values(): + if res.find('ACCOUNT LOCK') > 0: + return True + + return False + + def sanitize_requires(tls_requires): sanitized_requires = {} if tls_requires: @@ -79,31 +106,6 @@ 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] @@ -112,21 +114,107 @@ def get_grants(cursor, user, host): return grants.split(", ") +def get_existing_authentication(cursor, user, host=None): + """ Return a list of dict containing the plugin and auth_string for the + specified username. + If hostname is provided, return only the information about this particular + account. + """ + cursor.execute("SELECT VERSION()") + srv_type = cursor.fetchone() + # Mysql_info use a DictCursor so we must convert back to a list + # otherwise we get KeyError 0 + if isinstance(srv_type, dict): + srv_type = list(srv_type.values()) + + if 'mariadb' in srv_type[0].lower(): + # before MariaDB 10.2.19 and 10.3.11, "password" and "authentication_string" can differ + # when using mysql_native_password + if host: + cursor.execute("""select plugin, auth from ( + select plugin, password as auth from mysql.user where user=%(user)s + and host=%(host)s + union select plugin, authentication_string as auth from mysql.user where user=%(user)s + and host=%(host)s) x group by plugin, auth + """, {'user': user, 'host': host}) + else: + cursor.execute("""select plugin, auth from ( + select plugin, password as auth from mysql.user where user=%(user)s + union select plugin, authentication_string as auth from mysql.user where user=%(user)s + ) x group by plugin, auth + """, {'user': user}) + else: + if host: + cursor.execute("""select plugin, authentication_string as auth + from mysql.user where user=%(user)s and host=%(host)s + group by plugin, authentication_string""", {'user': user, 'host': host}) + else: + cursor.execute("""select plugin, authentication_string as auth + from mysql.user where user=%(user)s + group by plugin, authentication_string""", {'user': user}) + + rows = cursor.fetchall() + + if len(rows) == 0: + return [] + + # Mysql_info use a DictCursor so we must convert list(dict) + # to list(tuple) otherwise we get KeyError 0 + if isinstance(rows[0], dict): + rows = [tuple(row.values()) for row in rows] + + existing_auth_list = [] + + # 'plugin_auth_string' contains the hash string. Must be removed in c.mysql 4.0 + # See https://github.com/ansible-collections/community.mysql/pull/629 + for r in rows: + existing_auth_list.append({ + 'plugin': r[0], + 'plugin_auth_string': r[1], + 'plugin_hash_string': r[1]}) + + return existing_auth_list + + def user_add(cursor, user, host, host_all, password, encrypted, - plugin, plugin_hash_string, plugin_auth_string, new_priv, - tls_requires, check_mode): + plugin, plugin_hash_string, plugin_auth_string, salt, new_priv, + attributes, tls_requires, reuse_existing_password, module, + password_expire, password_expire_interval, locked=False): + # If attributes are set, perform a sanity check to ensure server supports user attributes before creating user + if attributes and not get_attribute_support(cursor): + module.fail_json(msg="user attributes were specified but the server does not support user attributes") + # we cannot create users without a proper hostname if host_all: - return False + return {'changed': False, 'password_changed': False, 'attributes': attributes} - if check_mode: - return True + if module.check_mode: + return {'changed': True, 'password_changed': None, 'attributes': attributes} # Determine what user management method server uses + impl = get_user_implementation(cursor) old_user_mgmt = impl.use_old_user_mgmt(cursor) mogrify = do_not_mogrify_requires if old_user_mgmt else mogrify_requires + # This is for update_password: on_new_username + used_existing_password = False + if reuse_existing_password: + existing_auth = get_existing_authentication(cursor, user) + if existing_auth: + if len(existing_auth) != 1: + module.warn("An account with the username %s has a different " + "password than the others existing accounts. Thus " + "on_new_username can't decide which password to " + "reuse so it will use your provided password " + "instead. If no password is provided, the account " + "will have an empty password!" % user) + used_existing_password = False + else: + plugin_hash_string = existing_auth[0]['plugin_hash_string'] + password = None + used_existing_password = True + plugin = existing_auth[0]['plugin'] # What if plugin differ? 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) @@ -142,7 +230,19 @@ def user_add(cursor, user, host, host_all, password, encrypted, 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) + # Mysql and MariaDB differ in naming pam plugin and Syntax to set it + if plugin == 'pam': # Used by MariaDB which requires the USING keyword, not BY + query_with_args = "CREATE USER %s@%s IDENTIFIED WITH %s USING %s", (user, host, plugin, plugin_auth_string) + elif plugin == 'ed25519': # Used by MariaDB which requires the USING keyword, not BY + query_with_args = "CREATE USER %s@%s IDENTIFIED WITH %s USING PASSWORD(%s)", (user, host, plugin, plugin_auth_string) + elif salt: + if plugin in ['caching_sha2_password', 'sha256_password']: + generated_hash_string = mysql_sha256_password_hash_hex(password=plugin_auth_string, salt=salt) + else: + module.fail_json(msg="salt not handled for %s authentication plugin" % plugin) + query_with_args = ("CREATE USER %s@%s IDENTIFIED WITH %s AS 0x" + generated_hash_string), (user, host, plugin) + else: + 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: @@ -151,12 +251,28 @@ def user_add(cursor, user, host, host_all, password, encrypted, query_with_args_and_tls_requires = query_with_args + (tls_requires,) cursor.execute(*mogrify(*query_with_args_and_tls_requires)) + if password_expire: + if not impl.server_supports_password_expire(cursor): + module.fail_json(msg="The server version does not match the requirements " + "for password_expire parameter. See module's documentation.") + set_password_expire(cursor, user, host, password_expire, password_expire_interval) + 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 + + final_attributes = None + + if attributes: + cursor.execute("ALTER USER %s@%s ATTRIBUTE %s", (user, host, json.dumps(attributes))) + final_attributes = attributes_get(cursor, user, host) + + if locked: + cursor.execute("ALTER USER %s@%s ACCOUNT LOCK", (user, host)) + + return {'changed': True, 'password_changed': not used_existing_password, 'attributes': final_attributes} def is_hash(password): @@ -168,13 +284,15 @@ def is_hash(password): 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): + plugin, plugin_hash_string, plugin_auth_string, salt, new_priv, + append_privs, subtract_privs, attributes, tls_requires, module, + password_expire, password_expire_interval, locked=None, role=False, maria_role=False): changed = False msg = "User unchanged" grant_option = False # Determine what user management method server uses + impl = get_user_implementation(cursor) old_user_mgmt = impl.use_old_user_mgmt(cursor) if host_all and not role: @@ -182,6 +300,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted, else: hostnames = [host] + password_changed = False for host in hostnames: # Handle clear text and hashed passwords. if not role: @@ -226,28 +345,50 @@ def user_mod(cursor, user, host, host_all, password, encrypted, encrypted_password = cursor.fetchone()[0] if current_pass_hash != encrypted_password: + password_changed = True 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 + if not module.check_mode: + 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 password expiration + if bool(password_expire): + if not impl.server_supports_password_expire(cursor): + module.fail_json(msg="The server version does not match the requirements " + "for password_expire parameter. See module's documentation.") + update = False + mariadb_role = True if "mariadb" in str(impl.__name__) else False + current_password_policy = get_password_expiration_policy(cursor, user, host, maria_role=mariadb_role) + password_expired = is_password_expired(cursor, user, host) + # Check if changes needed to be applied. + if not ((current_password_policy == -1 and password_expire == "default") or + (current_password_policy == 0 and password_expire == "never") or + (current_password_policy == password_expire_interval and password_expire == "interval") or + (password_expire == 'now' and password_expired)): + + update = True + + if not module.check_mode: + set_password_expire(cursor, user, host, password_expire, password_expire_interval) + password_changed = True changed = True # Handle plugin authentication @@ -264,7 +405,11 @@ def user_mod(cursor, user, host, host_all, password, encrypted, if plugin_hash_string and current_plugin[1] != plugin_hash_string: update = True - if plugin_auth_string and current_plugin[1] != plugin_auth_string: + if salt: + if plugin in ['caching_sha2_password', 'sha256_password']: + if current_plugin[1] != mysql_sha256_password_hash(password=plugin_auth_string, salt=salt): + update = True + elif 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 @@ -272,14 +417,29 @@ def user_mod(cursor, user, host, host_all, password, encrypted, update = True if update: + query_with_args = None 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) + # Mysql and MariaDB differ in naming pam plugin and syntax to set it + if plugin == 'pam': + query_with_args = "ALTER USER %s@%s IDENTIFIED WITH %s USING %s", (user, host, plugin, plugin_auth_string) + elif plugin == 'ed25519': + query_with_args = "ALTER USER %s@%s IDENTIFIED WITH %s USING PASSWORD(%s)", (user, host, plugin, plugin_auth_string) + elif salt: + if plugin in ['caching_sha2_password', 'sha256_password']: + generated_hash_string = mysql_sha256_password_hash_hex(password=plugin_auth_string, salt=salt) + else: + module.fail_json(msg="salt not handled for %s authentication plugin" % plugin) + query_with_args = ("ALTER USER %s@%s IDENTIFIED WITH %s AS 0x" + generated_hash_string), (user, host, plugin) + else: + 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) + if not module.check_mode: + cursor.execute(*query_with_args) + password_changed = True changed = True # Handle privileges @@ -288,74 +448,156 @@ def user_mod(cursor, user, host, host_all, password, encrypted, # 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 not append_privs and not subtract_privs: + 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: + msg = "Privileges updated" + if not module.check_mode: + 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 not subtract_privs: + for db_table, priv in iteritems(new_priv): + if db_table not in curr_priv: + msg = "New privileges granted" + if not module.check_mode: + 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. + grant_privs = [] + revoke_privs = [] if append_privs: - priv_diff = set(new_priv[db_table]) - set(curr_priv[db_table]) + # When appending privileges, only missing privileges need to be granted. Nothing is revoked. + grant_privs = list(set(new_priv[db_table]) - set(curr_priv[db_table])) + elif subtract_privs: + # When subtracting privileges, revoke only the intersection of requested and current privileges. + # No privileges are granted. + revoke_privs = list(set(new_priv[db_table]) & set(curr_priv[db_table])) else: - priv_diff = set(new_priv[db_table]) ^ set(curr_priv[db_table]) + # When replacing (neither append_privs nor subtract_privs), grant all missing privileges + # and revoke existing privileges that were not requested... + grant_privs = list(set(new_priv[db_table]) - set(curr_priv[db_table])) + revoke_privs = list(set(curr_priv[db_table]) - set(new_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) + # ... avoiding pointless revocations when ALL are granted + if 'ALL' in grant_privs or 'ALL PRIVILEGES' in grant_privs: + revoke_privs = list(set(['GRANT', 'PROXY']).intersection(set(revoke_privs))) + + # Only revoke grant option if it exists and absence is requested + # + # For more details + # https://github.com/ansible-collections/community.mysql/issues/77#issuecomment-1209693807 + grant_option = 'GRANT' in revoke_privs and 'GRANT' not in grant_privs + + if grant_privs == ['GRANT']: + # USAGE grants no privileges, it is only needed because 'WITH GRANT OPTION' cannot stand alone + grant_privs.append('USAGE') + + if len(grant_privs) + len(revoke_privs) > 0: + msg = "Privileges updated: granted %s, revoked %s" % (grant_privs, revoke_privs) + if not module.check_mode: + if len(revoke_privs) > 0: + privileges_revoke(cursor, user, host, db_table, revoke_privs, grant_option, maria_role) + if len(grant_privs) > 0: + privileges_grant(cursor, user, host, db_table, grant_privs, tls_requires, maria_role) + else: + changed = True + + # after privilege manipulation, compare privileges from before and now + after_priv = privileges_get(cursor, user, host, maria_role) + changed = changed or (curr_priv != after_priv) + + # Handle attributes + attribute_support = get_attribute_support(cursor) + final_attributes = {} + + if attributes: + if not attribute_support: + module.fail_json(msg="user attributes were specified but the server does not support user attributes") + else: + current_attributes = attributes_get(cursor, user, host) + + if current_attributes is None: + current_attributes = {} + + attributes_to_change = {} + + for key, value in attributes.items(): + if key not in current_attributes or current_attributes[key] != value: + attributes_to_change[key] = value + + if attributes_to_change: + msg = "Attributes updated: %s" % (", ".join(["%s: %s" % (key, value) for key, value in attributes_to_change.items()])) + + # Calculate final attributes by re-running attributes_get when not in check mode, and merge dictionaries when in check mode + if not module.check_mode: + cursor.execute("ALTER USER %s@%s ATTRIBUTE %s", (user, host, json.dumps(attributes_to_change))) + final_attributes = attributes_get(cursor, user, host) + else: + # Final if statements excludes items whose values are None in attributes_to_change, i.e. attributes that will be deleted + final_attributes = {k: v for d in (current_attributes, attributes_to_change) for k, v in d.items() if k not in attributes_to_change or + attributes_to_change[k] is not None} + + # Convert empty dict to None per return value requirements + final_attributes = final_attributes if final_attributes else None changed = True + else: + final_attributes = current_attributes + else: + if attribute_support: + final_attributes = attributes_get(cursor, user, host) + + if not role and locked is not None and user_is_locked(cursor, user, host) != locked: + if not module.check_mode: + if locked: + cursor.execute("ALTER USER %s@%s ACCOUNT LOCK", (user, host)) + msg = 'User locked' + else: + cursor.execute("ALTER USER %s@%s ACCOUNT UNLOCK", (user, host)) + msg = 'User unlocked' + else: + if locked: + msg = 'User will be locked' + else: + msg = 'User will be unlocked' + + changed = True if role: continue # Handle TLS requirements - current_requires = get_tls_requires(cursor, user, host) + current_requires = sanitize_requires(impl.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 not module.check_mode: + 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) + 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) + cursor.execute(*query_with_args) changed = True - return (changed, msg) + return {'changed': changed, 'msg': msg, 'password_changed': password_changed, 'attributes': final_attributes} def user_delete(cursor, user, host, host_all, check_mode): @@ -411,12 +653,29 @@ def privileges_get(cursor, user, host, maria_role=False): return x for grant in grants: + + # Mysql_info use a DictCursor so we must convert back to a list + # otherwise we get KeyError 0 + if isinstance(grant, dict): + grant = list(grant.values()) + 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: + # 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] @@ -549,7 +808,7 @@ def sort_column_order(statement): return '%s(%s)' % (priv_name, ', '.join(columns)) -def privileges_unpack(priv, mode): +def privileges_unpack(priv, mode, column_case_sensitive, ensure_usage=True): """ 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 @@ -585,9 +844,14 @@ def privileges_unpack(priv, mode): 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)) + if column_case_sensitive is True: + output[pieces[0]] = re.split(r',\s*(?=[^)]*(?:\(|$))', pieces[1]) + for i in output[pieces[0]]: + privs.append(re.sub(r'\s*\(.*\)', '', i)) + else: + 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]] @@ -595,7 +859,7 @@ def privileges_unpack(priv, mode): # Handle cases when there's privs like GRANT SELECT (colA, ...) in privs. output[pieces[0]] = normalize_col_grants(output[pieces[0]]) - if '*.*' not in output: + if ensure_usage and '*.*' not in output: output['*.*'] = ['USAGE'] return output @@ -614,17 +878,20 @@ def privileges_revoke(cursor, user, host, db_table, priv, grant_option, maria_ro 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,) + if priv_string != "": + query = ["REVOKE %s ON %s" % (priv_string, db_table)] - query = ' '.join(query) - cursor.execute(query, params) + 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) + cursor.execute("FLUSH PRIVILEGES") def privileges_grant(cursor, user, host, db_table, priv, tls_requires, maria_role=False): @@ -634,6 +901,14 @@ def privileges_grant(cursor, user, host, db_table, priv, tls_requires, maria_rol priv_string = ",".join([p for p in priv if p not in ('GRANT', )]) query = ["GRANT %s ON %s" % (priv_string, db_table)] + # MySQL and MariaDB don't store roles in the user table the same manner: + # select user, host from mysql.user; + # +------------------+-----------+ + # | user | host | + # +------------------+-----------+ + # | role_foo | % | <- MySQL + # | role_foo | | <- MariaDB + # +------------------+-----------+ if not maria_role: query.append("TO %s@%s") params = (user, host) @@ -641,6 +916,7 @@ def privileges_grant(cursor, user, host, db_table, priv, tls_requires, maria_rol query.append("TO %s") params = (user) + impl = get_user_implementation(cursor) if tls_requires and impl.use_old_user_mgmt(cursor): query, params = mogrify_requires(" ".join(query), params, tls_requires) query = [query] @@ -654,7 +930,8 @@ def privileges_grant(cursor, user, host, db_table, priv, tls_requires, maria_rol try: cursor.execute(query, params) except (mysql_driver.ProgrammingError, mysql_driver.OperationalError, mysql_driver.InternalError) as e: - raise InvalidPrivsError("Error granting privileges, invalid priv string: %s" % priv_string) + raise InvalidPrivsError("Error granting privileges, invalid priv string: %s , params: %s, query: %s ," + " exception: %s." % (priv_string, str(params), query, str(e))) def convert_priv_dict_to_str(priv): @@ -671,33 +948,6 @@ def convert_priv_dict_to_str(priv): return '/'.join(priv_list) -# Alter user is supported since MySQL 5.6 and MariaDB 10.2.0 -def server_supports_alter_user(cursor): - """Check if the server supports ALTER USER statement or doesn't. - - Args: - cursor (cursor): DB driver cursor object. - - Returns: True if supports, False otherwise. - """ - cursor.execute("SELECT VERSION()") - version_str = cursor.fetchone()[0] - version = version_str.split('.') - - if 'mariadb' in version_str.lower(): - # MariaDB 10.2 and later - if int(version[0]) * 1000 + int(version[1]) >= 10002: - return True - else: - return False - else: - # MySQL 5.6 and later - if int(version[0]) * 1000 + int(version[1]) >= 5006: - return True - else: - return False - - def get_resource_limits(cursor, user, host): """Get user resource limits. @@ -717,6 +967,11 @@ def get_resource_limits(cursor, user, host): cursor.execute(query, (user, host)) res = cursor.fetchone() + # Mysql_info use a DictCursor so we must convert back to a list + # otherwise we get KeyError 0 + if isinstance(res, dict): + res = list(res.values()) + if not res: return None @@ -726,6 +981,26 @@ def get_resource_limits(cursor, user, host): 'MAX_CONNECTIONS_PER_HOUR': res[2], 'MAX_USER_CONNECTIONS': res[3], } + + cursor.execute("SELECT VERSION()") + srv_type = cursor.fetchone() + # Mysql_info use a DictCursor so we must convert back to a list + # otherwise we get KeyError 0 + if isinstance(srv_type, dict): + srv_type = list(srv_type.values()) + + if 'mariadb' in srv_type[0].lower(): + query = ('SELECT max_statement_time AS MAX_STATEMENT_TIME ' + 'FROM mysql.user WHERE User = %s AND Host = %s') + cursor.execute(query, (user, host)) + res_max_statement_time = cursor.fetchone() + + # Mysql_info use a DictCursor so we must convert back to a list + # otherwise we get KeyError 0 + if isinstance(res_max_statement_time, dict): + res_max_statement_time = list(res_max_statement_time.values()) + current_limits['MAX_STATEMENT_TIME'] = res_max_statement_time[0] + return current_limits @@ -778,10 +1053,16 @@ def limit_resources(module, cursor, user, host, resource_limits, check_mode): Returns: True, if changed, False otherwise. """ - if not server_supports_alter_user(cursor): + impl = get_user_implementation(cursor) + if not impl.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.") + cursor.execute("SELECT VERSION()") + if 'mariadb' not in cursor.fetchone()[0].lower(): + if 'MAX_STATEMENT_TIME' in resource_limits: + module.fail_json(msg="MAX_STATEMENT_TIME resource limit is only supported by MariaDB.") + current_limits = get_resource_limits(cursor, user, host) needs_to_change = match_resource_limits(module, current_limits, resource_limits) @@ -803,12 +1084,116 @@ def limit_resources(module, cursor, user, host, resource_limits, check_mode): return True -def get_impl(cursor): - global impl - cursor.execute("SELECT VERSION()") - if 'mariadb' in cursor.fetchone()[0].lower(): +def set_password_expire(cursor, user, host, password_expire, password_expire_interval): + """Fuction to set passowrd expiration for user. + + Args: + cursor (cursor): DB driver cursor object. + user (str): User name. + host (str): User hostname. + password_expire (str): Password expiration mode. + password_expire_days (int): Invterval of days password expires. + """ + if password_expire.lower() == "never": + statement = "PASSWORD EXPIRE NEVER" + elif password_expire.lower() == "default": + statement = "PASSWORD EXPIRE DEFAULT" + elif password_expire.lower() == "interval": + statement = "PASSWORD EXPIRE INTERVAL %d DAY" % (password_expire_interval) + elif password_expire.lower() == "now": + statement = "PASSWORD EXPIRE" + + cursor.execute("ALTER USER %s@%s " + statement, (user, host)) + + +def get_password_expiration_policy(cursor, user, host, maria_role=False): + """Function to get password policy for user. + + Args: + cursor (cursor): DB driver cursor object. + user (str): User name. + host (str): User hostname. + maria_role (bool, optional): mariadb or mysql. Defaults to False. + + Returns: + policy (int): Current users password policy. + """ + if not maria_role: + statement = "SELECT IFNULL(password_lifetime, -1) FROM mysql.user \ + WHERE User = %s AND Host = %s", (user, host) + else: + statement = "SELECT JSON_EXTRACT(Priv, '$.password_lifetime') AS password_lifetime \ + FROM mysql.global_priv \ + WHERE User = %s AND Host = %s", (user, host) + cursor.execute(*statement) + policy = cursor.fetchone()[0] + return int(policy) + + +def is_password_expired(cursor, user, host): + """Function to check if password is expired + + Args: + cursor (cursor): DB driver cursor object. + user (str): User name. + host (str): User hostname. + + Returns: + expired (bool): True if expired, else False. + """ + statement = "SELECT password_expired FROM mysql.user \ + WHERE User = %s AND Host = %s", (user, host) + cursor.execute(*statement) + expired = cursor.fetchone()[0] + if str(expired) == "Y": + return True + return False + + +def get_attribute_support(cursor): + """Checks if the MySQL server supports user attributes. + + Args: + cursor (cursor): DB driver cursor object. + Returns: + True if attributes are supported, False if they are not. + """ + try: + # information_schema.tables does not hold the tables within information_schema itself + cursor.execute("SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES LIMIT 0") + cursor.fetchone() + except mysql_driver.Error: + return False + + return True + + +def attributes_get(cursor, user, host): + """Get attributes for a given user. + + Args: + cursor (cursor): DB driver cursor object. + user (str): User name. + host (str): User host name. + + Returns: + None if the user does not exist or the user has no attributes set, otherwise a dict of attributes set on the user + """ + cursor.execute("SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = %s AND host = %s", (user, host)) + + r = cursor.fetchone() + # convert JSON string stored in row into a dict - mysql enforces that user_attributes entires are in JSON format + j = json.loads(r[0]) if r and r[0] else None + + # if the attributes dict is empty, return None instead + return j if j else None + + +def get_user_implementation(cursor): + db_engine = get_server_implementation(cursor) + if db_engine == 'mariadb': from ansible_collections.community.mysql.plugins.module_utils.implementations.mariadb import user as mariauser - impl = mariauser + return mariauser else: from ansible_collections.community.mysql.plugins.module_utils.implementations.mysql import user as mysqluser - impl = mysqluser + return mysqluser 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 diff --git a/plugins/modules/mysql_db.py b/plugins/modules/mysql_db.py index c2a6fd8..6ef578c 100644 --- a/plugins/modules/mysql_db.py +++ b/plugins/modules/mysql_db.py @@ -11,9 +11,9 @@ __metaclass__ = type DOCUMENTATION = r''' --- module: mysql_db -short_description: Add or remove MySQL databases from a remote host +short_description: Add or remove MySQL or MariaDB databases from a remote host description: -- Add or remove MySQL databases from a remote host. +- Add or remove MySQL or MariaDB databases from a remote host. options: name: description: @@ -46,19 +46,19 @@ options: target: description: - Location, on the remote host, of the dump file to read from or write to. - - Uncompressed SQL files (C(.sql)) as well as bzip2 (C(.bz2)), gzip (C(.gz)) and - xz (Added in 2.0) compressed files are supported. + - Uncompressed SQL files (C(.sql)) as well as bzip2 (C(.bz2)), gzip (C(.gz)), + xz (Added in 2.0) and zstd (C(.zst)) (Added in 3.12.0) compressed files are supported. type: path single_transaction: description: - Execute the dump in a single transaction. type: bool - default: no + default: false quick: description: - Option used for dumping large tables. type: bool - default: yes + default: true ignore_tables: description: - A list of table names that will be ignored in the dump @@ -70,14 +70,14 @@ options: description: - Dump binary columns using hexadecimal notation. type: bool - default: no + default: false version_added: '0.1.0' force: description: - Continue dump or import even if we get an SQL error. - Used only when I(state) is C(dump) or C(import). type: bool - default: no + default: false version_added: '0.1.0' master_data: description: @@ -96,7 +96,7 @@ options: description: - Skip locking tables for read. Used when I(state=dump), ignored otherwise. type: bool - default: no + default: false version_added: '0.1.0' dump_extra_args: description: @@ -110,7 +110,7 @@ options: - If C(yes), the module will internally execute commands via a shell. - Used when I(state=import), ignored otherwise. type: bool - default: no + default: false version_added: '0.1.0' unsafe_login_password: description: @@ -121,7 +121,7 @@ options: - Used only when I(state) is C(import) or C(dump) and I(login_password) is passed, ignored otherwise. type: bool - default: no + default: false version_added: '0.1.0' restrict_config_file: description: @@ -132,14 +132,14 @@ options: under the hood that read named option file in addition to usual option files. - If this behavior is undesirable, use C(yes) to read only named option file. type: bool - default: no + default: false version_added: '0.1.0' 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 + default: false version_added: '0.1.0' config_overrides_defaults: description: @@ -148,8 +148,23 @@ options: - Used when I(stat) is C(present) or C(absent), ignored otherwise. - It needs Python 3.5+ as the default interpreter on a target host. type: bool - default: no + default: false version_added: '0.1.0' + chdir: + description: + - Changes the current working directory. + - Can be useful, for example, when I(state=import) and a dump file contains relative paths. + type: path + version_added: '3.4.0' + pipefail: + description: + - Use C(bash) instead of C(sh) and add C(-o pipefail) to catch errors from the + mysql_dump command when I(state=dump) and compression is used. + - The default is C(no) to prevent issues on systems without bash as a default interpreter. + - The default will change to C(yes) in community.mysql 4.0.0. + type: bool + default: false + version_added: '3.4.0' seealso: - module: community.mysql.mysql_info @@ -173,20 +188,25 @@ requirements: - mysql (command line binary) - mysqldump (command line binary) notes: - - Supports C(check_mode). + - Compatible with MariaDB or MySQL. - Requires the mysql and mysqldump binaries on the remote host. - This module is B(not idempotent) when I(state) is C(import), and will import the dump file each time if run more than once. +attributes: + check_mode: + support: full extends_documentation_fragment: - community.mysql.mysql - ''' EXAMPLES = r''' +# If you encounter the "Please explicitly state intended protocol" error, +# use the login_unix_socket argument - name: Create a new database with name 'bobdata' community.mysql.mysql_db: name: bobdata state: present + login_unix_socket: /run/mysqld/mysqld.sock - name: Create new databases with names 'foo' and 'bar' community.mysql.mysql_db: @@ -212,7 +232,7 @@ EXAMPLES = r''' name: my_db state: import target: /tmp/dump.sql.bz2 - force: yes + force: true - name: Dump multiple databases community.mysql.mysql_db: @@ -284,11 +304,18 @@ EXAMPLES = r''' - name: Try to create database as root/nopassword first. If not allowed, pass the credentials community.mysql.mysql_db: - check_implicit_admin: yes + check_implicit_admin: true login_user: bob login_password: 123456 name: bobdata state: present + +- name: Dump a database with compression and catch errors from mysqldump with bash pipefail + community.mysql.mysql_db: + state: dump + name: foo + target: /tmp/dump.sql.gz + pipefail: true ''' RETURN = r''' @@ -316,7 +343,15 @@ import traceback from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.mysql.plugins.module_utils.database import mysql_quote_identifier -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.mysql import ( + mysql_connect, + mysql_driver, + mysql_driver_fail_msg, + mysql_common_argument_spec, + get_server_implementation, + get_server_version, +) +from ansible_collections.community.mysql.plugins.module_utils.version import LooseVersion from ansible.module_utils.six.moves import shlex_quote from ansible.module_utils._text import to_native @@ -345,68 +380,81 @@ def db_delete(cursor, db): def db_dump(module, host, user, password, db_name, target, all_databases, port, - config_file, socket=None, ssl_cert=None, ssl_key=None, ssl_ca=None, + config_file, server_implementation, server_version, socket=None, + ssl_cert=None, ssl_key=None, ssl_ca=None, single_transaction=None, quick=None, ignore_tables=None, hex_blob=None, encoding=None, force=False, master_data=0, skip_lock_tables=False, dump_extra_args=None, unsafe_password=False, restrict_config_file=False, - check_implicit_admin=False): - cmd = module.get_bin_path('mysqldump', True) + check_implicit_admin=False, pipefail=False): + + cmd_str = 'mysqldump' + if server_implementation == 'mariadb' and LooseVersion(server_version) >= LooseVersion("10.4.6"): + cmd_str = 'mariadb-dump' + try: + cmd = [module.get_bin_path(cmd_str, True)] + except Exception as e: + return 1, "", "Error determining dump command: %s" % str(e) + # If defined, mysqldump demands --defaults-extra-file be the first option if config_file: if restrict_config_file: - cmd += " --defaults-file=%s" % shlex_quote(config_file) + cmd.append("--defaults-file=%s" % shlex_quote(config_file)) else: - cmd += " --defaults-extra-file=%s" % shlex_quote(config_file) + cmd.append("--defaults-extra-file=%s" % shlex_quote(config_file)) if check_implicit_admin: - cmd += " --user=root --password=''" + cmd.append("--user=root --password=''") else: if user is not None: - cmd += " --user=%s" % shlex_quote(user) + cmd.append("--user=%s" % shlex_quote(user)) if password is not None: if not unsafe_password: - cmd += " --password=%s" % shlex_quote(password) + cmd.append("--password=%s" % shlex_quote(password)) else: - cmd += " --password=%s" % password + cmd.append("--password=%s" % password) if ssl_cert is not None: - cmd += " --ssl-cert=%s" % shlex_quote(ssl_cert) + cmd.append("--ssl-cert=%s" % shlex_quote(ssl_cert)) if ssl_key is not None: - cmd += " --ssl-key=%s" % shlex_quote(ssl_key) + cmd.append("--ssl-key=%s" % shlex_quote(ssl_key)) if ssl_ca is not None: - cmd += " --ssl-ca=%s" % shlex_quote(ssl_ca) + cmd.append("--ssl-ca=%s" % shlex_quote(ssl_ca)) if force: - cmd += " --force" + cmd.append("--force") if socket is not None: - cmd += " --socket=%s" % shlex_quote(socket) + cmd.append("--socket=%s" % shlex_quote(socket)) else: - cmd += " --host=%s --port=%i" % (shlex_quote(host), port) + cmd.append("--host=%s --port=%i" % (shlex_quote(host), port)) if all_databases: - cmd += " --all-databases" + cmd.append("--all-databases") elif len(db_name) > 1: - cmd += " --databases {0}".format(' '.join(db_name)) + cmd.append("--databases {0}".format(' '.join(db_name))) else: - cmd += " %s" % shlex_quote(' '.join(db_name)) + cmd.append("%s" % shlex_quote(' '.join(db_name))) if skip_lock_tables: - cmd += " --skip-lock-tables" + cmd.append("--skip-lock-tables") if (encoding is not None) and (encoding != ""): - cmd += " --default-character-set=%s" % shlex_quote(encoding) + cmd.append("--default-character-set=%s" % shlex_quote(encoding)) if single_transaction: - cmd += " --single-transaction=true" + cmd.append("--single-transaction=true") if quick: - cmd += " --quick" + cmd.append("--quick") if ignore_tables: for an_ignored_table in ignore_tables: - cmd += " --ignore-table={0}".format(an_ignored_table) + cmd.append("--ignore-table={0}".format(an_ignored_table)) if hex_blob: - cmd += " --hex-blob" + cmd.append("--hex-blob") if master_data: - cmd += " --master-data=%s" % master_data + if (server_implementation == 'mysql' and + LooseVersion(server_version) >= LooseVersion("8.2.0")): + cmd.append("--source-data=%s" % master_data) + else: + cmd.append("--master-data=%s" % master_data) if dump_extra_args is not None: - cmd += " " + dump_extra_args + cmd.append(dump_extra_args) path = None if os.path.splitext(target)[-1] == '.gz': @@ -415,25 +463,44 @@ def db_dump(module, host, user, password, db_name, target, all_databases, port, path = module.get_bin_path('bzip2', True) elif os.path.splitext(target)[-1] == '.xz': path = module.get_bin_path('xz', True) + elif os.path.splitext(target)[-1] == '.zst': + path = module.get_bin_path('zstd', True) + + cmd = ' '.join(cmd) if path: cmd = '%s | %s > %s' % (cmd, path, shlex_quote(target)) + if pipefail: + cmd = 'set -o pipefail && ' + cmd else: cmd += " > %s" % shlex_quote(target) executed_commands.append(cmd) - rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True) + + if pipefail: + rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True, executable='bash') + else: + rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True) + return rc, stdout, stderr def db_import(module, host, user, password, db_name, target, all_databases, port, config_file, - socket=None, ssl_cert=None, ssl_key=None, ssl_ca=None, encoding=None, force=False, + server_implementation, server_version, socket=None, ssl_cert=None, ssl_key=None, ssl_ca=None, + encoding=None, force=False, use_shell=False, unsafe_password=False, restrict_config_file=False, check_implicit_admin=False): if not os.path.exists(target): return module.fail_json(msg="target %s does not exist on the host" % target) - cmd = [module.get_bin_path('mysql', True)] + cmd_str = 'mysql' + if server_implementation == 'mariadb' and LooseVersion(server_version) >= LooseVersion("10.4.6"): + cmd_str = 'mariadb' + try: + cmd = [module.get_bin_path(cmd_str, True)] + except Exception as e: + return 1, "", "Error determining mysql/mariadb command: %s" % str(e) + # --defaults-file must go first, or errors out if config_file: if restrict_config_file: @@ -442,7 +509,7 @@ def db_import(module, host, user, password, db_name, target, all_databases, port cmd.append("--defaults-extra-file=%s" % shlex_quote(config_file)) if check_implicit_admin: - cmd += " --user=root --password=''" + cmd.append("--user=root --password=''") else: if user: cmd.append("--user=%s" % shlex_quote(user)) @@ -479,6 +546,8 @@ def db_import(module, host, user, password, db_name, target, all_databases, port comp_prog_path = module.get_bin_path('bzip2', required=True) elif os.path.splitext(target)[-1] == '.xz': comp_prog_path = module.get_bin_path('xz', required=True) + elif os.path.splitext(target)[-1] == '.zst': + comp_prog_path = module.get_bin_path('zstd', required=True) if comp_prog_path: # The line below is for returned data only: executed_commands.append('%s -dc %s | %s' % (comp_prog_path, target, cmd)) @@ -544,14 +613,14 @@ def db_create(cursor, db, encoding, collation): def main(): argument_spec = mysql_common_argument_spec() argument_spec.update( - name=dict(type='list', required=True, aliases=['db']), + name=dict(type='list', elements='str', required=True, aliases=['db']), encoding=dict(type='str', default=''), collation=dict(type='str', default=''), target=dict(type='path'), state=dict(type='str', default='present', choices=['absent', 'dump', 'import', 'present']), single_transaction=dict(type='bool', default=False), quick=dict(type='bool', default=True), - ignore_tables=dict(type='list', default=[]), + ignore_tables=dict(type='list', elements='str', default=[]), hex_blob=dict(default=False, type='bool'), force=dict(type='bool', default=False), master_data=dict(type='int', default=0, choices=[0, 1, 2]), @@ -562,6 +631,8 @@ def main(): restrict_config_file=dict(type='bool', default=False), check_implicit_admin=dict(type='bool', default=False), config_overrides_defaults=dict(type='bool', default=False), + chdir=dict(type='path'), + pipefail=dict(type='bool', default=False), ) module = AnsibleModule( @@ -610,6 +681,14 @@ def main(): restrict_config_file = module.params["restrict_config_file"] check_implicit_admin = module.params['check_implicit_admin'] config_overrides_defaults = module.params['config_overrides_defaults'] + chdir = module.params['chdir'] + pipefail = module.params['pipefail'] + + if chdir: + try: + os.chdir(chdir) + except Exception as e: + module.fail_json("Cannot change the current directory to %s: %s" % (chdir, e)) if len(db) > 1 and state == 'import': module.fail_json(msg="Multiple databases are not supported with state=import") @@ -646,6 +725,9 @@ def main(): else: module.fail_json(msg="unable to find %s. Exception message: %s" % (config_file, to_native(e))) + server_implementation = get_server_implementation(cursor) + server_version = get_server_version(cursor) + changed = False if not os.path.exists(config_file): config_file = None @@ -686,11 +768,12 @@ def main(): module.exit_json(changed=True, db=db_name, db_list=db) rc, stdout, stderr = db_dump(module, login_host, login_user, login_password, db, target, all_databases, - login_port, config_file, socket, ssl_cert, ssl_key, + login_port, config_file, server_implementation, server_version, + socket, ssl_cert, ssl_key, ssl_ca, single_transaction, quick, ignore_tables, hex_blob, encoding, force, master_data, skip_lock_tables, dump_extra_args, unsafe_login_password, restrict_config_file, - check_implicit_admin) + check_implicit_admin, pipefail) if rc != 0: module.fail_json(msg="%s" % stderr) module.exit_json(changed=True, db=db_name, db_list=db, msg=stdout, @@ -707,8 +790,8 @@ def main(): rc, stdout, stderr = db_import(module, login_host, login_user, login_password, db, target, all_databases, - login_port, config_file, - socket, ssl_cert, ssl_key, ssl_ca, + login_port, config_file, server_implementation, + server_version, socket, ssl_cert, ssl_key, ssl_ca, encoding, force, use_shell, unsafe_login_password, restrict_config_file, check_implicit_admin) if rc != 0: diff --git a/plugins/modules/mysql_info.py b/plugins/modules/mysql_info.py index 6f57403..2360d01 100644 --- a/plugins/modules/mysql_info.py +++ b/plugins/modules/mysql_info.py @@ -1,25 +1,27 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) +# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) # 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_info -short_description: Gather information about MySQL servers +short_description: Gather information about MySQL or MariaDB servers description: -- Gathers information about MySQL servers. +- Gathers information about MySQL or MariaDB servers. options: filter: description: - Limit the collected information by comma separated string or YAML list. - Allowable values are C(version), C(databases), C(settings), C(global_status), - C(users), C(engines), C(master_status), C(slave_status), C(slave_hosts). + C(users), C(users_info), C(engines), C(master_status), C(slave_status), C(slave_hosts). - By default, collects all subsets. - You can use '!' before value (for example, C(!settings)) to exclude it from the information. - If you pass including and excluding values to the filter, for example, I(filter=!settings,version), @@ -34,7 +36,7 @@ options: exclude_fields: description: - List of fields which are not needed to collect. - - "Supports elements: C(db_size). Unsupported elements will be ignored." + - "Supports elements: C(db_size), C(db_table_count). Unsupported elements will be ignored." type: list elements: str version_added: '0.1.0' @@ -42,12 +44,16 @@ options: description: - Includes names of empty databases to returned dictionary. type: bool - default: no + default: false notes: +- Compatible with MariaDB or MySQL. - Calculating the size of a database might be slow, depending on the number and size of tables in it. To avoid this, use I(exclude_fields=db_size). -- Supports C(check_mode). + +attributes: + check_mode: + support: full seealso: - module: community.mysql.mysql_variables @@ -58,6 +64,7 @@ seealso: author: - Andrew Klychkov (@Andersson007) - Sebastian Gumprich (@rndmh3ro) +- Laurent Indermühle (@laurent-indermuehle) extends_documentation_fragment: - community.mysql.mysql @@ -70,15 +77,21 @@ EXAMPLES = r''' # Display only databases and users info: # ansible mysql-hosts -m mysql_info -a 'filter=databases,users' +# Display all users privileges: +# ansible mysql-hosts -m mysql_info -a 'filter=users_info' + # Display only slave status: # ansible standby -m mysql_info -a 'filter=slave_status' # Display all info from databases group except settings: # ansible databases -m mysql_info -a 'filter=!settings' +# If you encounter the "Please explicitly state intended protocol" error, +# use the login_unix_socket argument - name: Collect all possible information using passwordless root access community.mysql.mysql_info: login_user: root + login_unix_socket: /run/mysqld/mysqld.sock - name: Get MySQL version with non-default credentials community.mysql.mysql_info: @@ -93,14 +106,14 @@ EXAMPLES = r''' filter: "!settings,!users" - name: Collect info about databases and version using ~/.my.cnf as a credential file - become: yes + become: true community.mysql.mysql_info: filter: - databases - version - name: Collect info about databases and version using ~alice/.my.cnf as a credential file - become: yes + become: true community.mysql.mysql_info: config_file: /home/alice/.my.cnf filter: @@ -108,16 +121,54 @@ EXAMPLES = r''' - version - name: Collect info about databases including empty and excluding their sizes - become: yes + become: true community.mysql.mysql_info: config_file: /home/alice/.my.cnf filter: - databases exclude_fields: db_size - return_empty_dbs: yes + return_empty_dbs: true + +- name: Clone users from one server to another + block: + # Step 1 + - name: Fetch information from a source server + delegate_to: server_source + community.mysql.mysql_info: + filter: + - users_info + register: result + + # Step 2 + # Don't work with sha256_password and cache_sha2_password + - name: Clone users fetched in a previous task to a target server + community.mysql.mysql_user: + name: "{{ item.name }}" + host: "{{ item.host }}" + plugin: "{{ item.plugin | default(omit) }}" + plugin_auth_string: "{{ item.plugin_auth_string | default(omit) }}" + plugin_hash_string: "{{ item.plugin_hash_string | default(omit) }}" + tls_requires: "{{ item.tls_requires | default(omit) }}" + priv: "{{ item.priv | default(omit) }}" + resource_limits: "{{ item.resource_limits | default(omit) }}" + column_case_sensitive: true + state: present + loop: "{{ result.users_info }}" + loop_control: + label: "{{ item.name }}@{{ item.host }}" + when: + - item.name != 'root' # In case you don't want to import admin accounts + - item.name != 'mariadb.sys' + - item.name != 'mysql' ''' RETURN = r''' +server_engine: + description: Database server engine. + returned: if not excluded by filter + type: str + sample: 'MariaDB' + version_added: '3.10.0' version: description: Database server version. returned: if not excluded by filter @@ -154,13 +205,19 @@ databases: returned: if not excluded by filter type: dict sample: - - { "mysql": { "size": 656594 }, "information_schema": { "size": 73728 } } + - { "mysql": { "size": 656594, "tables": 31 }, "information_schema": { "size": 73728, "tables": 79 } } contains: size: description: Database size in bytes. returned: if not excluded by filter type: dict sample: { 'size': 656594 } + tables: + description: Count of tables and views in that database. + returned: if not excluded by filter + type: dict + sample: { 'tables': 12 } + version_added: '3.11.0' settings: description: Global settings (variables) information. returned: if not excluded by filter @@ -174,11 +231,32 @@ global_status: sample: - { "Innodb_buffer_pool_read_requests": 123, "Innodb_buffer_pool_reads": 32 } users: - description: Users information. + description: Return a dictionnary of users grouped by host and with global privileges only. returned: if not excluded by filter type: dict sample: - { "localhost": { "root": { "Alter_priv": "Y", "Alter_routine_priv": "Y" } } } +users_info: + description: + - Information about users accounts. + - The output can be used as an input of the M(community.mysql.mysql_user) plugin. + - Useful when migrating accounts to another server or to create an inventory. + - Does not support proxy privileges. If an account has proxy privileges, they won't appear in the output. + - Causes issues with authentications plugins C(sha256_password) and C(caching_sha2_password). + If the output is fed to M(community.mysql.mysql_user), the + ``plugin_auth_string`` will most likely be unreadable due to non-binary + characters. + returned: if not excluded by filter + type: dict + sample: + - { "plugin_auth_string": '*1234567', + "name": "user1", + "host": "host.com", + "plugin": "mysql_native_password", + "priv": "db1.*:SELECT/db2.*:SELECT", + "resource_limits": { "MAX_USER_CONNECTIONS": 100 }, + "tls_requires": { "SSL": null } } + version_added: '3.8.0' engines: description: Information about the server's storage engines. returned: if not excluded by filter @@ -203,16 +281,45 @@ slave_hosts: type: dict sample: - { "2": { "Host": "", "Master_id": 1, "Port": 3306 } } +connector_name: + description: Name of the python connector used by the module. When the connector is not identified, returns C(Unknown). + returned: always + type: str + sample: + - "pymysql" + version_added: '3.6.0' +connector_version: + description: Version of the python connector used by the module. When the connector is not identified, returns C(Unknown). + returned: always + type: str + sample: + - "1.0.2" + version_added: '3.6.0' ''' from decimal import Decimal from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.mysql.plugins.module_utils.command_resolver import ( + CommandResolver +) from ansible_collections.community.mysql.plugins.module_utils.mysql import ( mysql_connect, mysql_common_argument_spec, mysql_driver, mysql_driver_fail_msg, + get_connector_name, + get_connector_version, + get_server_implementation, + get_server_version, +) + +from ansible_collections.community.mysql.plugins.module_utils.user import ( + privileges_get, + get_resource_limits, + get_existing_authentication, + get_user_implementation, + user_is_locked, ) from ansible.module_utils.six import iteritems from ansible.module_utils._text import to_native @@ -240,9 +347,13 @@ class MySQL_Info(object): 5. add info about the new subset with an example to RETURN block """ - def __init__(self, module, cursor): + def __init__(self, module, cursor, server_implementation, server_version, user_implementation): self.module = module self.cursor = cursor + self.server_implementation = server_implementation + self.server_version = server_version + self.user_implementation = user_implementation + self.command_resolver = CommandResolver(self.server_implementation, self.server_version) self.info = { 'version': {}, 'databases': {}, @@ -250,6 +361,7 @@ class MySQL_Info(object): 'global_status': {}, 'engines': {}, 'users': {}, + 'users_info': {}, 'master_status': {}, 'slave_hosts': {}, 'slave_status': {}, @@ -318,6 +430,9 @@ class MySQL_Info(object): if 'users' in wanted: self.__get_users() + if 'users_info' in wanted: + self.__get_users_info() + if 'master_status' in wanted: self.__get_master_status() @@ -400,7 +515,8 @@ class MySQL_Info(object): def __get_master_status(self): """Get master status if the instance is a master.""" - res = self.__exec_sql('SHOW MASTER STATUS') + query = self.command_resolver.resolve_command("SHOW MASTER STATUS") + res = self.__exec_sql(query) if res: for line in res: for vname, val in iteritems(line): @@ -408,7 +524,8 @@ class MySQL_Info(object): def __get_slave_status(self): """Get slave status if the instance is a slave.""" - res = self.__exec_sql('SHOW SLAVE STATUS') + query = self.command_resolver.resolve_command("SHOW SLAVE STATUS") + res = self.__exec_sql(query) if res: for line in res: host = line['Master_Host'] @@ -429,7 +546,8 @@ class MySQL_Info(object): def __get_slaves(self): """Get slave hosts info if the instance is a master.""" - res = self.__exec_sql('SHOW SLAVE HOSTS') + query = self.command_resolver.resolve_command("SHOW SLAVE HOSTS") + res = self.__exec_sql(query) if res: for line in res: srv_id = line['Server_id'] @@ -456,42 +574,131 @@ class MySQL_Info(object): if vname not in ('Host', 'User'): self.info['users'][host][user][vname] = self.__convert(val) - def __get_databases(self, exclude_fields, return_empty_dbs): - """Get info about databases.""" - if not exclude_fields: - query = ('SELECT table_schema AS "name", ' - 'SUM(data_length + index_length) AS "size" ' - 'FROM information_schema.TABLES GROUP BY table_schema') - else: - if 'db_size' in exclude_fields: - query = ('SELECT table_schema AS "name" ' - 'FROM information_schema.TABLES GROUP BY table_schema') + def __get_users_info(self): + """Get user privileges, passwords, resources_limits, ... - res = self.__exec_sql(query) + Query the server to get all the users and return a string + of privileges that can be used by the mysql_user plugin. + For instance: - if res: - for db in res: - 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 - if not return_empty_dbs: + "users_info": [ + { + "host": "users_info.com", + "priv": "*.*: ALL,GRANT", + "name": "users_info_adm" + }, + { + "host": "users_info.com", + "priv": "`mysql`.*: SELECT/`users_info_db`.*: SELECT", + "name": "users_info_multi" + } + ] + """ + res = self.__exec_sql('SELECT * FROM mysql.user') + if not res: return None - # Add info about empty databases (issue #65727): - res = self.__exec_sql('SHOW DATABASES') - if res: - for db in res: - if db['Database'] not in self.info['databases']: - self.info['databases'][db['Database']] = {} + output = list() + for line in res: + user = line['User'] + host = line['Host'] - if not exclude_fields or 'db_size' not in exclude_fields: - self.info['databases'][db['Database']]['size'] = 0 + user_priv = privileges_get(self.cursor, user, host) + + if not user_priv: + self.module.warn("No privileges found for %s on host %s" % (user, host)) + continue + + priv_string = list() + for db_table, priv in user_priv.items(): + # Proxy privileges are hard to work with because of different quotes or + # backticks like ''@'', ''@'%' or even ``@``. In addition, MySQL will + # forbid you to grant a proxy privileges through TCP. + if set(priv) == {'PROXY', 'GRANT'} or set(priv) == {'PROXY'}: + continue + + unquote_db_table = db_table.replace('`', '').replace("'", '') + priv_string.append('%s:%s' % (unquote_db_table, ','.join(priv))) + + # Only keep *.* USAGE if it's the only user privilege given + if len(priv_string) > 1 and '*.*:USAGE' in priv_string: + priv_string.remove('*.*:USAGE') + + resource_limits = get_resource_limits(self.cursor, user, host) + copy_ressource_limits = dict.copy(resource_limits) + + tls_requires = self.user_implementation.get_tls_requires( + self.cursor, user, host) + + output_dict = { + 'name': user, + 'host': host, + 'priv': '/'.join(priv_string), + 'resource_limits': copy_ressource_limits, + 'tls_requires': tls_requires, + } + + # Prevent returning a resource limit if empty + if resource_limits: + for key, value in resource_limits.items(): + if value == 0: + del output_dict['resource_limits'][key] + if len(output_dict['resource_limits']) == 0: + del output_dict['resource_limits'] + + # Prevent returning tls_require if empty + if not tls_requires: + del output_dict['tls_requires'] + + authentications = get_existing_authentication(self.cursor, user, host) + if authentications: + output_dict.update(authentications[0]) + + if line.get('is_role') and line['is_role'] == 'N': + output_dict['locked'] = user_is_locked(self.cursor, user, host) + + # TODO password_option + # but both are not supported by mysql_user atm. So no point yet. + + output.append(output_dict) + + self.info['users_info'] = output + + def __get_databases(self, exclude_fields, return_empty_dbs): + """Get info about databases.""" + + def is_field_included(field_name): + return not exclude_fields or 'db_{}'.format(field_name) not in exclude_fields + + def create_db_info(db_data): + info = {} + if is_field_included('size'): + info['size'] = int(db_data.get('size', 0) or 0) + if is_field_included('table_count'): + info['tables'] = int(db_data.get('tables', 0) or 0) + return info + + # Build the main query + query_parts = ['SELECT table_schema AS "name"'] + if is_field_included('size'): + query_parts.append('SUM(data_length + index_length) AS "size"') + if is_field_included('table_count'): + query_parts.append('COUNT(table_name) as "tables"') + + query = "{} FROM information_schema.TABLES GROUP BY table_schema".format(", ".join(query_parts)) + + # Get and process databases with tables + databases = self.__exec_sql(query) or [] + for db in databases: + self.info['databases'][db['name']] = create_db_info(db) + + # Handle empty databases if requested + if return_empty_dbs: + empty_databases = self.__exec_sql('SHOW DATABASES') or [] + for db in empty_databases: + db_name = db['Database'] + if db_name not in self.info['databases']: + self.info['databases'][db_name] = create_db_info({}) def __exec_sql(self, query, ddl=False): """Execute SQL. @@ -523,8 +730,8 @@ def main(): argument_spec = mysql_common_argument_spec() argument_spec.update( login_db=dict(type='str'), - filter=dict(type='list'), - exclude_fields=dict(type='list'), + filter=dict(type='list', elements='str'), + exclude_fields=dict(type='list', elements='str'), return_empty_dbs=dict(type='bool', default=False), ) @@ -555,21 +762,34 @@ def main(): if mysql_driver is None: module.fail_json(msg=mysql_driver_fail_msg) + connector_name = get_connector_name(mysql_driver) + connector_version = get_connector_version(mysql_driver) + try: cursor, db_conn = mysql_connect(module, login_user, login_password, config_file, ssl_cert, ssl_key, ssl_ca, db, check_hostname=check_hostname, connect_timeout=connect_timeout, cursor_class='DictCursor') 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))) + msg = ('unable to connect to database using %s %s, check login_user ' + 'and login_password are correct or %s has the credentials. ' + 'Exception message: %s' % (connector_name, connector_version, config_file, to_native(e))) + module.fail_json(msg) + + server_implementation = get_server_implementation(cursor) + server_version = get_server_version(cursor) + user_implementation = get_user_implementation(cursor) ############################### # Create object and do main job - mysql = MySQL_Info(module, cursor) + mysql = MySQL_Info(module, cursor, server_implementation, server_version, user_implementation) - module.exit_json(changed=False, **mysql.get_info(filter_, exclude_fields, return_empty_dbs)) + module.exit_json(changed=False, + server_engine='MariaDB' if server_implementation == 'mariadb' else 'MySQL', + connector_name=connector_name, + connector_version=connector_version, + **mysql.get_info(filter_, exclude_fields, return_empty_dbs)) if __name__ == '__main__': diff --git a/plugins/modules/mysql_query.py b/plugins/modules/mysql_query.py index fc789c5..35beeb3 100644 --- a/plugins/modules/mysql_query.py +++ b/plugins/modules/mysql_query.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# Copyright: (c) 2020, Andrew Klychkov (@Andersson007) +# Copyright: (c) 2020, Andrew Klychkov (@Andersson007) # 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) @@ -10,9 +10,9 @@ __metaclass__ = type DOCUMENTATION = r''' --- module: mysql_query -short_description: Run MySQL queries +short_description: Run MySQL or MariaDB queries description: -- Runs arbitrary MySQL queries. +- Runs arbitrary MySQL or MariaDB queries. - Pay attention, the module does not support check mode! All queries will be executed in autocommit mode. - To run SQL queries from a file, use M(community.mysql.mysql_db) module. @@ -22,13 +22,21 @@ options: description: - SQL query to run. Multiple queries can be passed using YAML list syntax. - Must be a string or YAML list containing strings. + - If you use I(named_args) or I(positional_args) any C(%) will be interpreted + as a formatting character. All literal C(%) characters in the query should be + escaped as C(%%). + - Note that if you use the C(IF EXISTS/IF NOT EXISTS) clauses in your query + and C(mysqlclient) or C(PyMySQL 0.10.0+) connectors, the module will report + that the state has been changed even if it has not. If it is important in your + workflow, use the C(PyMySQL 0.9.3) connector instead. type: raw - required: yes + required: true positional_args: description: - List of values to be passed as positional arguments to the query. - Mutually exclusive with I(named_args). type: list + elements: raw named_args: description: - Dictionary of key-value arguments to pass to the query. @@ -42,21 +50,28 @@ options: description: - Where passed queries run in a single transaction (C(yes)) or commit them one-by-one (C(no)). type: bool - default: no + default: false +attributes: + check_mode: + support: none seealso: - module: community.mysql.mysql_db +notes: +- Compatible with MariaDB or MySQL. author: - Andrew Klychkov (@Andersson007) extends_documentation_fragment: - community.mysql.mysql - ''' EXAMPLES = r''' +# If you encounter the "Please explicitly state intended protocol" error, +# use the login_unix_socket argument - name: Simple select query to acme db community.mysql.mysql_query: login_db: acme query: SELECT * FROM orders + login_unix_socket: /run/mysqld/mysqld.sock - name: Select query to db acme with positional arguments community.mysql.mysql_query: @@ -80,7 +95,7 @@ EXAMPLES = r''' query: - INSERT INTO articles (id, story) VALUES (2, 'my_long_story') - INSERT INTO prices (id, price) VALUES (123, '100.00') - single_transaction: yes + single_transaction: true ''' RETURN = r''' @@ -101,8 +116,20 @@ rowcount: returned: changed type: list sample: [5, 1] +execution_time_ms: + description: + - A list containing execution time per query in milliseconds. + - The measurements are done right before and after passing + the query to the driver for execution. + returned: success + type: list + sample: [7104, 85] + version_added: '3.12.0' ''' +import time +import warnings + from ansible.module_utils.basic import AnsibleModule from ansible_collections.community.mysql.plugins.module_utils.mysql import ( mysql_connect, @@ -121,12 +148,24 @@ DDL_QUERY_KEYWORDS = ('CREATE', 'DROP', 'ALTER', 'RENAME', 'TRUNCATE') # Module execution. # + +def execute_and_return_time(cursor, query, args): + # Measure query execution time in milliseconds + start_time = time.perf_counter() + + cursor.execute(query, args) + + # Calculate the execution time rounding it to 4 decimal places + exec_time_ms = round((time.perf_counter() - start_time) * 1000, 4) + return cursor, exec_time_ms + + def main(): argument_spec = mysql_common_argument_spec() argument_spec.update( query=dict(type='raw', required=True), login_db=dict(type='str'), - positional_args=dict(type='list'), + positional_args=dict(type='list', elements='raw'), named_args=dict(type='dict'), single_transaction=dict(type='bool', default=False), ) @@ -195,10 +234,26 @@ def main(): query_result = [] executed_queries = [] rowcount = [] + execution_time_ms = [] + already_exists = False for q in query: try: - cursor.execute(q, arguments) + with warnings.catch_warnings(): + warnings.filterwarnings(action='error', + message='.*already exists*', + category=mysql_driver.Warning) + + try: + cursor, exec_time_ms = execute_and_return_time(cursor, q, arguments) + execution_time_ms.append(exec_time_ms) + except mysql_driver.Warning: + # When something is run with IF NOT EXISTS + # and there's "already exists" MySQL warning, + # set the flag as True. + # PyMySQL < 0.10.0 throws the warning, mysqlclient + # and PyMySQL 0.10.0+ does NOT. + already_exists = True except Exception as e: if not autocommit: @@ -208,7 +263,8 @@ def main(): module.fail_json(msg="Cannot execute SQL '%s' args [%s]: %s" % (q, arguments, to_native(e))) try: - query_result.append([dict(row) for row in cursor.fetchall()]) + if not already_exists: + query_result.append([dict(row) for row in cursor.fetchall()]) except Exception as e: if not autocommit: @@ -224,8 +280,12 @@ def main(): for keyword in DDL_QUERY_KEYWORDS: if keyword in q: - changed = True - + if already_exists: + # Indicates the entity already exists + changed = False + already_exists = False # Reset flag + else: + changed = True try: executed_queries.append(cursor._last_executed) except AttributeError: @@ -243,6 +303,7 @@ def main(): 'executed_queries': executed_queries, 'query_result': query_result, 'rowcount': rowcount, + 'execution_time_ms': execution_time_ms, } # Exit: diff --git a/plugins/modules/mysql_replication.py b/plugins/modules/mysql_replication.py index 46895e3..b902da0 100644 --- a/plugins/modules/mysql_replication.py +++ b/plugins/modules/mysql_replication.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # Copyright: (c) 2013, Balazs Pocze -# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) +# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) # Certain parts are taken from Mark Theunissen's mysqldb module # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) @@ -13,27 +13,31 @@ __metaclass__ = type DOCUMENTATION = r''' --- module: mysql_replication -short_description: Manage MySQL replication +short_description: Manage MySQL or MariaDB replication description: -- Manages MySQL server replication, replica, primary status, get and change primary host. +- Manages MySQL or MariaDB server replication, replica, primary status, get and change primary host. author: - Balazs Pocze (@banyek) - Andrew Klychkov (@Andersson007) +- Dennis Urtubia (@dennisurtubia) +- Laurent Indermühle (@laurent-indermuehle) options: mode: description: - Module operating mode. Could be - C(changeprimary) (CHANGE PRIMARY TO), - C(getprimary) (SHOW PRIMARY STATUS), - C(getreplica) (SHOW REPLICA), + C(changeprimary) (CHANGE MASTER TO) - also works for MySQL 8.0.23 and later since community.mysql 3.10.0, + C(changereplication) (CHANGE REPLICATION SOURCE TO) - only supported in MySQL 8.0.23 and later, + C(getprimary) (SHOW MASTER STATUS), + C(getreplica) (SHOW REPLICA STATUS), C(startreplica) (START REPLICA), C(stopreplica) (STOP REPLICA), - C(resetprimary) (RESET PRIMARY) - supported since community.mysql 0.1.0, + C(resetprimary) (RESET MASTER) - supported since community.mysql 0.1.0, C(resetreplica) (RESET REPLICA), C(resetreplicaall) (RESET REPLICA ALL). type: str choices: - changeprimary + - changereplication - getprimary - getreplica - startreplica @@ -92,8 +96,8 @@ options: if an encrypted connection can be established. - For details, refer to L(MySQL encrypted replication documentation,https://dev.mysql.com/doc/refman/8.0/en/replication-solutions-encrypted-connections.html). + - The default is C(false). type: bool - default: false aliases: [master_ssl] primary_ssl_ca: description: @@ -131,6 +135,12 @@ options: L(MySQL encrypted replication documentation,https://dev.mysql.com/doc/refman/8.0/en/replication-solutions-encrypted-connections.html). type: str aliases: [master_ssl_cipher] + primary_ssl_verify_server_cert: + description: + - Same as mysql variable. + type: bool + default: false + version_added: '3.5.0' primary_auto_position: description: - Whether the host uses GTID based replication or not. @@ -178,16 +188,20 @@ options: description: - Fails on error when calling mysql. type: bool - default: False + default: false version_added: '0.1.0' notes: -- If an empty value for the parameter of string type is needed, use an empty string. + - Compatible with MariaDB or MySQL. + - If an empty value for the parameter of string type is needed, use an empty string. + +attributes: + check_mode: + support: none extends_documentation_fragment: - community.mysql.mysql - seealso: - module: community.mysql.mysql_info - name: MySQL replication reference @@ -202,9 +216,12 @@ seealso: ''' EXAMPLES = r''' +# If you encounter the "Please explicitly state intended protocol" error, +# use the login_unix_socket argument - name: Stop mysql replica thread community.mysql.mysql_replication: mode: stopreplica + login_unix_socket: /run/mysqld/mysqld.sock - name: Get primary binlog file name and binlog position community.mysql.mysql_replication: @@ -217,6 +234,13 @@ EXAMPLES = r''' primary_log_file: mysql-bin.000009 primary_log_pos: 4578 +- name: Change replication source to replica server 192.0.2.1 and use binary log 'mysql-bin.000009' with position 4578 + community.mysql.mysql_replication: + mode: changereplication + 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: mode: getreplica @@ -254,13 +278,12 @@ EXAMPLES = r''' community.mysql.mysql_replication: mode: startreplica connection_name: primary-1 - fail_on_error: yes + fail_on_error: true - name: Change primary and fail on error (like when replica thread is running) community.mysql.mysql_replication: mode: changeprimary - fail_on_error: yes - + fail_on_error: true ''' RETURN = r''' @@ -276,23 +299,26 @@ import os import warnings from ansible.module_utils.basic import AnsibleModule +from ansible_collections.community.mysql.plugins.module_utils.command_resolver import ( + CommandResolver +) from ansible_collections.community.mysql.plugins.module_utils.mysql import ( + get_server_version, + get_server_implementation, mysql_connect, mysql_driver, mysql_driver_fail_msg, mysql_common_argument_spec, ) from ansible.module_utils._text import to_native -from ansible_collections.community.mysql.plugins.module_utils.version import LooseVersion executed_queries = [] -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") +def get_primary_status(cursor, command_resolver): + query = command_resolver.resolve_command("SHOW MASTER STATUS") + cursor.execute(query) + primarystatus = cursor.fetchone() return primarystatus @@ -377,8 +403,8 @@ def reset_replica_all(module, cursor, connection_name='', channel='', fail_on_er return reset -def reset_primary(module, cursor, fail_on_error=False): - query = 'RESET MASTER' +def reset_primary(module, cursor, command_resolver, fail_on_error=False): + query = command_resolver.resolve_command('RESET MASTER') try: executed_queries.append(query) cursor.execute(query) @@ -387,7 +413,7 @@ def reset_primary(module, cursor, fail_on_error=False): reset = False except Exception as e: if fail_on_error: - module.fail_json(msg="RESET MASTER failed: %s" % to_native(e)) + module.fail_json(msg="%s failed: %s" % (command_resolver.resolve_command('RESET MASTER'), to_native(e))) reset = False return reset @@ -414,11 +440,22 @@ def start_replica(module, cursor, connection_name='', channel='', fail_on_error= return started -def changeprimary(cursor, chm, connection_name='', channel=''): +def changeprimary(cursor, command_resolver, chm, connection_name='', channel=''): + query_head = command_resolver.resolve_command("CHANGE MASTER") if connection_name: - query = "CHANGE MASTER '%s' TO %s" % (connection_name, ','.join(chm)) + query = "%s '%s' TO %s" % (query_head, connection_name, ','.join(chm)) else: - query = 'CHANGE MASTER TO %s' % ','.join(chm) + query = '%s TO %s' % (query_head, ','.join(chm)) + + if channel: + query += " FOR CHANNEL '%s'" % channel + + executed_queries.append(query) + cursor.execute(query) + + +def changereplication(cursor, chm, channel=''): + query = 'CHANGE REPLICATION SOURCE TO %s' % ','.join(chm) if channel: query += " FOR CHANNEL '%s'" % channel @@ -438,7 +475,8 @@ def main(): 'startreplica', 'resetprimary', 'resetreplica', - 'resetreplicaall']), + 'resetreplicaall', + 'changereplication']), 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']), @@ -449,12 +487,13 @@ def main(): primary_log_pos=dict(type='int', aliases=['master_log_pos']), relay_log_file=dict(type='str'), relay_log_pos=dict(type='int'), - primary_ssl=dict(type='bool', default=False, aliases=['master_ssl']), + primary_ssl=dict(type='bool', 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_ssl_verify_server_cert=dict(type='bool', default=False), primary_use_gtid=dict(type='str', choices=[ 'current_pos', 'replica_pos', 'disabled'], aliases=['master_use_gtid']), primary_delay=dict(type='int', aliases=['master_delay']), @@ -484,6 +523,7 @@ def main(): 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_ssl_verify_server_cert = module.params["primary_ssl_verify_server_cert"] primary_auto_position = module.params["primary_auto_position"] ssl_cert = module.params["client_cert"] ssl_key = module.params["client_key"] @@ -520,8 +560,11 @@ def main(): else: module.fail_json(msg="unable to find %s. Exception message: %s" % (config_file, to_native(e))) + server_version = get_server_version(cursor) + server_implementation = get_server_implementation(cursor) + command_resolver = CommandResolver(server_implementation, server_version) cursor.execute("SELECT VERSION()") - if 'mariadb' in cursor.fetchone()["VERSION()"].lower(): + if server_implementation == 'mariadb': from ansible_collections.community.mysql.plugins.module_utils.implementations.mariadb import replication as impl else: from ansible_collections.community.mysql.plugins.module_utils.implementations.mysql import replication as impl @@ -536,21 +579,27 @@ def main(): primary_use_gtid = 'slave_pos' if mode == 'getprimary': - status = get_primary_status(cursor) - if not isinstance(status, dict): - status = dict(Is_Primary=False, - msg="Server is not configured as mysql primary") - else: + status = get_primary_status(cursor, command_resolver) + if status and "File" in status and "Position" in status: status['Is_Primary'] = True + else: + status = dict( + Is_Primary=False, + msg="Server is not configured as mysql primary. " + "Meaning: Binary logs are disabled") module.exit_json(queries=executed_queries, **status) elif mode == "getreplica": status = get_replica_status(cursor, connection_name, channel, replica_term) - if not isinstance(status, dict): - status = dict(Is_Replica=False, msg="Server is not configured as mysql replica") - else: + # MySQL 8.0 uses Replica_... + # MariaDB 10.6 uses Slave_... + if status and ( + "Slave_IO_Running" in status or + "Replica_IO_Running" in status): status['Is_Replica'] = True + else: + status = dict(Is_Replica=False, msg="Server is not configured as mysql replica") module.exit_json(queries=executed_queries, **status) @@ -558,47 +607,52 @@ def main(): chm = [] result = {} if primary_host is not None: - chm.append("MASTER_HOST='%s'" % primary_host) + chm.append("%s='%s'" % (command_resolver.resolve_command('MASTER_HOST'), primary_host)) if primary_user is not None: - chm.append("MASTER_USER='%s'" % primary_user) + chm.append("%s='%s'" % (command_resolver.resolve_command('MASTER_USER'), primary_user)) if primary_password is not None: - chm.append("MASTER_PASSWORD='%s'" % primary_password) + chm.append("%s='%s'" % (command_resolver.resolve_command('MASTER_PASSWORD'), primary_password)) if primary_port is not None: - chm.append("MASTER_PORT=%s" % primary_port) + chm.append("%s=%s" % (command_resolver.resolve_command('MASTER_PORT'), primary_port)) if primary_connect_retry is not None: - chm.append("MASTER_CONNECT_RETRY=%s" % primary_connect_retry) + chm.append("%s=%s" % (command_resolver.resolve_command('MASTER_CONNECT_RETRY'), primary_connect_retry)) if primary_log_file is not None: - chm.append("MASTER_LOG_FILE='%s'" % primary_log_file) + chm.append("%s='%s'" % (command_resolver.resolve_command('MASTER_LOG_FILE'), primary_log_file)) if primary_log_pos is not None: - chm.append("MASTER_LOG_POS=%s" % primary_log_pos) + chm.append("%s=%s" % (command_resolver.resolve_command('MASTER_LOG_POS'), primary_log_pos)) if primary_delay is not None: - chm.append("MASTER_DELAY=%s" % primary_delay) + chm.append("%s=%s" % (command_resolver.resolve_command('MASTER_DELAY'), 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 primary_ssl: - chm.append("MASTER_SSL=1") + if primary_ssl is not None: + if primary_ssl: + chm.append("%s=1" % command_resolver.resolve_command('MASTER_SSL')) + else: + chm.append("%s=0" % command_resolver.resolve_command('MASTER_SSL')) if primary_ssl_ca is not None: - chm.append("MASTER_SSL_CA='%s'" % primary_ssl_ca) + chm.append("%s='%s'" % (command_resolver.resolve_command('MASTER_SSL_CA'), primary_ssl_ca)) if primary_ssl_capath is not None: - chm.append("MASTER_SSL_CAPATH='%s'" % primary_ssl_capath) + chm.append("%s='%s'" % (command_resolver.resolve_command('MASTER_SSL_CAPATH'), primary_ssl_capath)) if primary_ssl_cert is not None: - chm.append("MASTER_SSL_CERT='%s'" % primary_ssl_cert) + chm.append("%s='%s'" % (command_resolver.resolve_command('MASTER_SSL_CERT'), primary_ssl_cert)) if primary_ssl_key is not None: - chm.append("MASTER_SSL_KEY='%s'" % primary_ssl_key) + chm.append("%s='%s'" % (command_resolver.resolve_command('MASTER_SSL_KEY'), primary_ssl_key)) if primary_ssl_cipher is not None: - chm.append("MASTER_SSL_CIPHER='%s'" % primary_ssl_cipher) + chm.append("%s='%s'" % (command_resolver.resolve_command('MASTER_SSL_CIPHER'), primary_ssl_cipher)) + if primary_ssl_verify_server_cert: + chm.append("%s=1" % command_resolver.resolve_command('MASTER_SSL_VERIFY_SERVER_CERT')) if primary_auto_position: - chm.append("MASTER_AUTO_POSITION=1") + chm.append("%s=1" % command_resolver.resolve_command('MASTER_AUTO_POSITION')) if primary_use_gtid is not None: - chm.append("MASTER_USE_GTID=%s" % primary_use_gtid) + chm.append("MASTER_USE_GTID=%s" % primary_use_gtid) # MariaDB only try: - changeprimary(cursor, chm, connection_name, channel) + changeprimary(cursor, command_resolver, chm, connection_name, channel) except mysql_driver.Warning as e: result['warning'] = to_native(e) except Exception as e: - module.fail_json(msg='%s. Query == CHANGE MASTER TO %s' % (to_native(e), chm)) + module.fail_json(msg='%s. Query == %s TO %s' % (to_native(e), command_resolver.resolve_command('CHANGE MASTER'), chm)) result['changed'] = True module.exit_json(queries=executed_queries, **result) elif mode == "startreplica": @@ -614,7 +668,7 @@ def main(): else: module.exit_json(msg="Replica already stopped", changed=False, queries=executed_queries) elif mode == 'resetprimary': - reset = reset_primary(module, cursor, fail_on_error) + reset = reset_primary(module, cursor, command_resolver, fail_on_error) if reset is True: module.exit_json(msg="Primary reset", changed=True, queries=executed_queries) else: @@ -631,6 +685,56 @@ def main(): module.exit_json(msg="Replica reset", changed=True, queries=executed_queries) else: module.exit_json(msg="Replica already reset", changed=False, queries=executed_queries) + elif mode == 'changereplication': + chm = [] + result = {} + if primary_host is not None: + chm.append("SOURCE_HOST='%s'" % primary_host) + if primary_user is not None: + chm.append("SOURCE_USER='%s'" % primary_user) + if primary_password is not None: + chm.append("SOURCE_PASSWORD='%s'" % primary_password) + if primary_port is not None: + chm.append("SOURCE_PORT=%s" % primary_port) + if primary_connect_retry is not None: + chm.append("SOURCE_CONNECT_RETRY=%s" % primary_connect_retry) + if primary_log_file is not None: + chm.append("SOURCE_LOG_FILE='%s'" % primary_log_file) + if primary_log_pos is not None: + chm.append("SOURCE_LOG_POS=%s" % primary_log_pos) + if primary_delay is not None: + chm.append("SOURCE_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 primary_ssl is not None: + if primary_ssl: + chm.append("SOURCE_SSL=1") + else: + chm.append("SOURCE_SSL=0") + if primary_ssl_ca is not None: + chm.append("SOURCE_SSL_CA='%s'" % primary_ssl_ca) + if primary_ssl_capath is not None: + chm.append("SOURCE_SSL_CAPATH='%s'" % primary_ssl_capath) + if primary_ssl_cert is not None: + chm.append("SOURCE_SSL_CERT='%s'" % primary_ssl_cert) + if primary_ssl_key is not None: + chm.append("SOURCE_SSL_KEY='%s'" % primary_ssl_key) + if primary_ssl_cipher is not None: + chm.append("SOURCE_SSL_CIPHER='%s'" % primary_ssl_cipher) + if primary_ssl_verify_server_cert: + chm.append("SOURCE_SSL_VERIFY_SERVER_CERT=1") + if primary_auto_position: + chm.append("SOURCE_AUTO_POSITION=1") + try: + changereplication(cursor, chm, channel) + except mysql_driver.Warning as e: + result['warning'] = to_native(e) + except Exception as e: + module.fail_json(msg='%s. Query == CHANGE REPLICATION SOURCE TO %s' % (to_native(e), chm)) + result['changed'] = True + module.exit_json(queries=executed_queries, **result) warnings.simplefilter("ignore") diff --git a/plugins/modules/mysql_role.py b/plugins/modules/mysql_role.py index 7641b07..382445c 100644 --- a/plugins/modules/mysql_role.py +++ b/plugins/modules/mysql_role.py @@ -1,7 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -# Copyright: (c) 2021, Andrew Klychkov +# 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 @@ -11,10 +11,10 @@ DOCUMENTATION = r''' --- module: mysql_role -short_description: Adds, removes, or updates a MySQL role +short_description: Adds, removes, or updates a MySQL or MariaDB role description: - - Adds, removes, or updates a MySQL role. + - Adds, removes, or updates a MySQL or MariaDB role. - Roles are supported since MySQL 8.0.0 and MariaDB 10.0.5. version_added: '2.2.0' @@ -51,9 +51,18 @@ options: append_privs: description: - Append the privileges defined by the I(priv) option to the existing ones - for this role instead of overwriting them. + for this role instead of overwriting them. Mutually exclusive with I(subtract_privs). type: bool - default: no + default: false + + subtract_privs: + description: + - Revoke the privileges defined by the I(priv) option and keep other existing privileges. + If set, invalid privileges in I(priv) are ignored. + Mutually exclusive with I(append_privs). + version_added: '3.2.0' + type: bool + default: false members: description: @@ -71,7 +80,7 @@ options: for this role instead of overwriting them. - Mutually exclusive with the I(detach_members) and I(admin) option. type: bool - default: no + default: false detach_members: description: @@ -79,7 +88,7 @@ options: instead of overwriting all the current members. - Mutually exclusive with the I(append_members) and I(admin) option. type: bool - default: no + default: false set_default_role_all: description: @@ -87,7 +96,7 @@ options: - 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 + default: true state: description: @@ -103,13 +112,34 @@ options: - 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 + default: false + + members_must_exist: + description: + - When C(yes), the module fails if any user in I(members) does not exist. + - When C(no), users in I(members) which don't exist are simply skipped. + type: bool + default: true + + column_case_sensitive: + description: + - The default is C(false). + - When C(true), the module will not uppercase the field in the privileges. + - When C(false), the field names will be upper-cased. This was the default before this + feature was introduced but since MySQL/MariaDB is case sensitive you should set this + to C(true) in most cases. + type: bool + version_added: '3.8.0' notes: + - Roles are supported since MySQL 8.0.0 and MariaDB 10.0.5. - 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). + +attributes: + check_mode: + support: full seealso: - module: community.mysql.mysql_user @@ -119,12 +149,18 @@ seealso: author: - Andrew Klychkov (@Andersson007) + - Felix Hamme (@betanummeric) + - kmarse (@kmarse) + - Laurent Indermühle (@laurent-indermuehle) extends_documentation_fragment: - community.mysql.mysql ''' EXAMPLES = r''' +# If you encounter the "Please explicitly state intended protocol" error, +# use the login_unix_socket argument, for example, login_unix_socket: /run/mysqld/mysqld.sock + # Example of a .my.cnf file content for setting a root password # [client] # user=root @@ -161,7 +197,7 @@ EXAMPLES = r''' members: - 'alice@%' - 'bob@%' - set_default_role_all: no + set_default_role_all: false # Assuming that the role developers exists, # add john to the current members @@ -169,7 +205,7 @@ EXAMPLES = r''' community.mysql.mysql_role: name: developers state: present - append_members: yes + append_members: true members: - 'joe@localhost' @@ -188,7 +224,7 @@ EXAMPLES = r''' name: readers state: present priv: 'fiction.*:UPDATE' - append_privs: yes + append_privs: true - name: Create role with the 'SELECT' and 'UPDATE' privileges in db1 and db2 community.mysql.mysql_role: @@ -204,7 +240,7 @@ EXAMPLES = r''' name: readers members: - 'joe@localhost' - detach_members: yes + detach_members: true - name: Remove the role readers if exists community.mysql.mysql_role: @@ -233,6 +269,34 @@ EXAMPLES = r''' name: business members: - marketing + +- name: Ensure the role foo does not have the DELETE privilege + community.mysql.mysql_role: + state: present + name: foo + subtract_privs: true + priv: + 'db1.*': DELETE + +- name: Add some members to a role and skip not-existent users + community.mysql.mysql_role: + state: present + name: foo + append_members: true + members_must_exist: false + members: + - 'existing_user@localhost' + - 'not_existing_user@localhost' + +- name: Detach some members from a role and ignore not-existent users + community.mysql.mysql_role: + state: present + name: foo + detach_members: true + members_must_exist: false + members: + - 'existing_user@localhost' + - 'not_existing_user@localhost' ''' RETURN = '''#''' @@ -246,7 +310,7 @@ from ansible_collections.community.mysql.plugins.module_utils.mysql import ( ) from ansible_collections.community.mysql.plugins.module_utils.user import ( convert_priv_dict_to_str, - get_impl, + get_user_implementation, get_mode, user_mod, privileges_grant, @@ -364,6 +428,11 @@ class DbServer(): msg = 'User / role `%s` with host `%s` does not exist' % (user[0], user[1]) self.module.fail_json(msg=msg) + def filter_existing_users(self, users): + for user in users: + if user in self.users: + yield user + def __get_users(self): """Get users. @@ -821,9 +890,9 @@ class Role(): 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): + append_privs=False, subtract_privs=False, + append_members=False, detach_members=False, + admin=False, set_default_role_all=True): """Update a role. Update a role if needed. @@ -837,6 +906,8 @@ class Role(): 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. + subtract_privs (bool): If True, revoke the privileges passed through privs + not touching other existing 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. @@ -859,10 +930,13 @@ class Role(): 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) + result = user_mod(cursor=self.cursor, user=self.name, host=self.host, + host_all=None, password=None, encrypted=None, plugin=None, + plugin_auth_string=None, plugin_hash_string=None, salt=None, + new_priv=privs, append_privs=append_privs, subtract_privs=subtract_privs, + attributes=None, tls_requires=None, module=self.module, password_expire=None, + password_expire_interval=None, role=True, maria_role=self.is_mariadb) + changed = result['changed'] if admin: self.role_impl.set_admin(admin) @@ -877,50 +951,11 @@ class Role(): 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 + if self.is_mariadb: + self.cursor.execute('select user, host from mysql.roles_mapping where role = %s', (self.name,)) + else: + self.cursor.execute('select TO_USER as user, TO_HOST as host from mysql.role_edges where FROM_USER = %s', (self.name,)) + return set(self.cursor.fetchall()) def main(): @@ -931,11 +966,14 @@ def main(): admin=dict(type='str'), priv=dict(type='raw'), append_privs=dict(type='bool', default=False), + subtract_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), + members_must_exist=dict(type='bool', default=True), + column_case_sensitive=dict(type='bool', default=None), # TODO 4.0.0 add default=True ) module = AnsibleModule( argument_spec=argument_spec, @@ -945,6 +983,7 @@ def main(): ('admin', 'members'), ('admin', 'append_members'), ('admin', 'detach_members'), + ('append_privs', 'subtract_privs'), ), ) @@ -958,6 +997,7 @@ def main(): connect_timeout = module.params['connect_timeout'] config_file = module.params['config_file'] append_privs = module.params['append_privs'] + subtract_privs = module.boolean(module.params['subtract_privs']) members = module.params['members'] append_members = module.params['append_members'] detach_members = module.params['detach_members'] @@ -967,6 +1007,8 @@ def main(): check_hostname = module.params['check_hostname'] db = '' set_default_role_all = module.params['set_default_role_all'] + members_must_exist = module.params['members_must_exist'] + column_case_sensitive = module.params['column_case_sensitive'] if priv and not isinstance(priv, (str, dict)): msg = ('The "priv" parameter must be str or dict ' @@ -979,6 +1021,13 @@ def main(): if mysql_driver is None: module.fail_json(msg=mysql_driver_fail_msg) + # TODO Release 4.0.0 : Remove this test and variable assignation + if column_case_sensitive is None: + column_case_sensitive = False + module.warn("Option column_case_sensitive is not provided. " + "The default is now false, so the column's name will be uppercased. " + "The default will be changed to true in community.mysql 4.0.0.") + cursor = None try: if check_implicit_admin: @@ -986,7 +1035,8 @@ def main(): cursor, db_conn = mysql_connect(module, 'root', '', config_file, ssl_cert, ssl_key, ssl_ca, db, connect_timeout=connect_timeout, - check_hostname=check_hostname) + check_hostname=check_hostname, + autocommit=True) except Exception: pass @@ -994,7 +1044,8 @@ def main(): 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) + check_hostname=check_hostname, + autocommit=True) except Exception as e: module.fail_json(msg='unable to connect to database, ' @@ -1005,7 +1056,7 @@ def main(): # Set defaults changed = False - get_impl(cursor) + impl = get_user_implementation(cursor) if priv is not None: try: @@ -1014,7 +1065,7 @@ def main(): module.fail_json(msg=to_native(e)) try: - priv = privileges_unpack(priv, mode) + priv = privileges_unpack(priv, mode, column_case_sensitive, ensure_usage=not subtract_privs) except Exception as e: module.fail_json(msg='Invalid privileges string: %s' % to_native(e)) @@ -1035,7 +1086,10 @@ def main(): if members: members = normalize_users(module, members, server.is_mariadb()) - server.check_users_in_db(members) + if members_must_exist: + server.check_users_in_db(members) + else: + members = list(server.filter_existing_users(members)) # Main job starts here role = Role(module, cursor, name, server) @@ -1043,11 +1097,15 @@ def main(): try: if state == 'present': if not role.exists: + if subtract_privs: + priv = None # avoid granting unwanted privileges + if detach_members: + members = None # avoid adding unwanted members changed = role.add(members, priv, module.check_mode, admin, set_default_role_all) else: - changed = role.update(members, priv, module.check_mode, append_privs, + changed = role.update(members, priv, module.check_mode, append_privs, subtract_privs, append_members, detach_members, admin, set_default_role_all) diff --git a/plugins/modules/mysql_user.py b/plugins/modules/mysql_user.py index 326c1fe..2a5855c 100644 --- a/plugins/modules/mysql_user.py +++ b/plugins/modules/mysql_user.py @@ -11,24 +11,26 @@ __metaclass__ = type DOCUMENTATION = r''' --- module: mysql_user -short_description: Adds or removes a user from a MySQL database +short_description: Adds or removes a user from a MySQL or MariaDB database description: - - Adds or removes a user from a MySQL database. + - Adds or removes a user from a MySQL or MariaDB database. options: name: description: - Name of the user (role) to add or remove. type: str required: true + aliases: ['user'] password: description: - - Set the user's password. + - Set the user's password. Only for C(mysql_native_password) authentication. + For other authentication plugins see the combination of I(plugin), I(plugin_hash_string), I(plugin_auth_string). type: str encrypted: description: - Indicate that the 'password' field is a `mysql_native_password` hash. type: bool - default: no + default: false host: description: - The 'host' part of the MySQL username. @@ -40,10 +42,11 @@ options: to all hostnames for a given user. - This option cannot be used when creating users. type: bool - default: no + default: false priv: description: - "MySQL privileges string in the format: C(db.table:priv1,priv2)." + - Additionally, there must be no spaces between the table and the privilege as this will yield a non-idempotent check mode. - "Multiple privileges can be specified by separating each one using a forward slash: C(db.table1:priv/db.table2:priv)." - The format is based on MySQL C(GRANT) statement. @@ -63,9 +66,17 @@ options: append_privs: description: - Append the privileges defined by priv to the existing ones for this - user instead of overwriting existing ones. + user instead of overwriting existing ones. Mutually exclusive with I(subtract_privs). type: bool - default: no + default: false + subtract_privs: + description: + - Revoke the privileges defined by the I(priv) option and keep other existing privileges. + If set, invalid privileges in I(priv) are ignored. + Mutually exclusive with I(append_privs). + version_added: '3.2.0' + type: bool + default: false tls_requires: description: - Set requirement for secure transport as a dictionary of requirements (see the examples). @@ -78,7 +89,7 @@ options: description: - Whether binary logging should be enabled or disabled for the connection. type: bool - default: yes + default: true force_context: description: - Sets the С(mysql) system database as context for the executed statements (it will be used @@ -90,7 +101,7 @@ options: - 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 + default: false version_added: '3.1.0' state: description: @@ -104,13 +115,17 @@ options: - 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 + default: false update_password: description: - - C(always) will update passwords if they differ. - - C(on_create) will only set the password for newly created users. + - C(always) will update passwords if they differ. This affects I(password) and the combination of I(plugin), I(plugin_hash_string), I(plugin_auth_string). + - C(on_create) will only set the password or the combination of I(plugin), I(plugin_hash_string), I(plugin_auth_string) for newly created users. + - "C(on_new_username) works like C(on_create), but it tries to reuse an existing password: If one different user + with the same username exists, or multiple different users with the same username and equal C(plugin) and + C(authentication_string) attribute, the existing C(plugin) and C(authentication_string) are used for the + new user instead of the I(password), I(plugin), I(plugin_hash_string) or I(plugin_auth_string) argument." type: str - choices: [ always, on_create ] + choices: [ always, on_create, on_new_username ] default: always plugin: description: @@ -125,25 +140,84 @@ options: plugin_auth_string: description: - User's plugin auth_string (``CREATE USER user IDENTIFIED WITH plugin BY plugin_auth_string``). + - If I(plugin) is ``pam`` (MariaDB) or ``auth_pam`` (MySQL) an optional I(plugin_auth_string) can be used to choose a specific PAM service. + - You need to define a I(salt) to have idempotence on password change with ``caching_sha2_password`` and ``sha256_password`` plugins. type: str version_added: '0.1.0' + salt: + description: + - Salt used to generate password hash from I(plugin_auth_string). + - Salt length must be 20 characters. + - Salt only support ``caching_sha2_password`` or ``sha256_password`` authentication I(plugin). + type: str + version_added: '3.10.0' resource_limits: description: - Limit the user for certain server resources. Provided since MySQL 5.6 / MariaDB 10.2. - "Available options are C(MAX_QUERIES_PER_HOUR: num), C(MAX_UPDATES_PER_HOUR: num), - C(MAX_CONNECTIONS_PER_HOUR: num), C(MAX_USER_CONNECTIONS: num)." + C(MAX_CONNECTIONS_PER_HOUR: num), C(MAX_USER_CONNECTIONS: num), C(MAX_STATEMENT_TIME: num) (supported only for MariaDB since collection version 3.7.0)." - Used when I(state=present), ignored otherwise. type: dict version_added: '0.1.0' + session_vars: + description: + - "Dictionary of session variables in form of C(variable: value) to set at the beginning of module execution." + - Cannot be used to set global variables, use the M(community.mysql.mysql_variables) module instead. + type: dict + version_added: '3.6.0' + password_expire: + description: + - C(never) - I(password) will never expire. + - C(default) - I(password) is defined using global system variable I(default_password_lifetime) setting. + - C(interval) - I(password) will expire in days which is defined in I(password_expire_interval). + - C(now) - I(password) will expire immediately. + type: str + choices: [ now, never, default, interval ] + version_added: '3.9.0' + password_expire_interval: + description: + - Number of days I(password) will expire. Requires I(password_expire=interval). + type: int + version_added: '3.9.0' + + column_case_sensitive: + description: + - The default is C(false). + - When C(true), the module will not uppercase the field names in the privileges. + - When C(false), the field names will be upper-cased. This is the default + - This feature was introduced because MySQL 8 and above uses case sensitive + fields names in privileges. + type: bool + version_added: '3.8.0' + + locked: + description: + - Lock account to prevent connections using it. + - This is primarily used for creating a user that will act as a DEFINER on stored procedures. + - If not specified leaves the lock state as is (for a new user creates unlocked). + type: bool + version_added: '3.13.0' + + attributes: + description: + - "Create, update, or delete user attributes (arbitrary 'key: value' comments) for the user." + - MySQL server must support the INFORMATION_SCHEMA.USER_ATTRIBUTES table. Provided since MySQL 8.0. + - To delete an existing attribute, set its value to null. + type: dict + version_added: '3.9.0' notes: + - Compatible with MySQL or MariaDB. - "MySQL server installs with default I(login_user) of C(root) and no password. To secure this user as part of an idempotent playbook, you must create at least two tasks: 1) change the root user's password, without providing any I(login_user)/I(login_password) details, 2) drop a C(~/.my.cnf) file containing the new root credentials. Subsequent runs of the playbook will then succeed by reading the new credentials from the file." - Currently, there is only support for the C(mysql_native_password) encrypted password hash module. - - Supports (check_mode). + +attributes: + check_mode: + support: full seealso: - module: community.mysql.mysql_info @@ -158,22 +232,28 @@ author: - Jonathan Mainguy (@Jmainguy) - Benjamin Malynovytch (@bmalynovytch) - Lukasz Tomaszkiewicz (@tomaszkiewicz) +- kmarse (@kmarse) +- Laurent Indermühle (@laurent-indermuehle) +- E.S. Rosenberg (@Keeper-of-the-Keys) + extends_documentation_fragment: - community.mysql.mysql - ''' EXAMPLES = r''' +# If you encounter the "Please explicitly state intended protocol" error, +# use the login_unix_socket argument - name: Removes anonymous user account for localhost community.mysql.mysql_user: name: '' host: localhost state: absent + login_unix_socket: /run/mysqld/mysqld.sock - name: Removes all anonymous user accounts community.mysql.mysql_user: name: '' - host_all: yes + host_all: true state: absent - name: Create database user with name 'bob' and password '12345' with all database privileges @@ -187,16 +267,19 @@ EXAMPLES = r''' community.mysql.mysql_user: name: bob password: '*EE0D72C1085C46C5278932678FBE2C6A782821B4' - encrypted: yes + encrypted: true priv: '*.*:ALL' state: present +# Set session var wsrep_on=off before creating the user - name: Create database user with password and all database privileges and 'WITH GRANT OPTION' community.mysql.mysql_user: name: bob password: 12345 priv: '*.*:ALL,GRANT' state: present + session_vars: + wsrep_on: 'off' - name: Create user with password, all database privileges and 'WITH GRANT OPTION' in db1 and db2 community.mysql.mysql_user: @@ -216,6 +299,13 @@ EXAMPLES = r''' FUNCTION my_db.my_function: EXECUTE state: present +- name: Modify user attributes, creating the attribute 'foo' and removing the attribute 'bar' + community.mysql.mysql_user: + name: bob + attributes: + foo: "foo" + bar: null + - name: Modify user to require TLS connection with a valid client certificate community.mysql.mysql_user: name: bob @@ -248,7 +338,7 @@ EXAMPLES = r''' If mysql allows root/nopassword login, try it without the credentials first. If it's not allowed, pass the credentials community.mysql.mysql_user: - check_implicit_admin: yes + check_implicit_admin: true login_user: root login_password: 123456 name: sally @@ -257,7 +347,7 @@ EXAMPLES = r''' - name: Ensure no user named 'sally' exists at all community.mysql.mysql_user: name: sally - host_all: yes + host_all: true state: absent - name: Specify grants composed of more than one word @@ -289,7 +379,7 @@ EXAMPLES = r''' password: 12345 priv: "*.*:USAGE" state: present - sql_log_bin: no + sql_log_bin: false - name: Create user 'bob' authenticated with plugin 'AWSAuthenticationPlugin' community.mysql.mysql_user: @@ -299,6 +389,13 @@ EXAMPLES = r''' priv: '*.*:ALL' state: present +- name: Create user 'bob' authenticated with plugin 'caching_sha2_password' and static salt + community.mysql.mysql_user: + name: bob + plugin: caching_sha2_password + plugin_auth_string: password + salt: 1234567890abcdefghij + - name: Limit bob's resources to 10 queries per hour and 5 connections per hour community.mysql.mysql_user: name: bob @@ -306,6 +403,20 @@ EXAMPLES = r''' MAX_QUERIES_PER_HOUR: 10 MAX_CONNECTIONS_PER_HOUR: 5 +- name: Ensure bob does not have the DELETE privilege + community.mysql.mysql_user: + name: bob + subtract_privs: true + priv: + 'db1.*': DELETE + +- name: Create locked user to act as a definer on procedures + community.mysql.mysql_user: + name: readonly_procedures_locked + locked: true + priv: + db1.*: SELECT + # Example .my.cnf file for setting the root password # [client] # user=root @@ -317,11 +428,14 @@ RETURN = '''#''' 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 + mysql_connect, + mysql_driver, + mysql_driver_fail_msg, + mysql_common_argument_spec, + set_session_vars, ) from ansible_collections.community.mysql.plugins.module_utils.user import ( convert_priv_dict_to_str, - get_impl, get_mode, InvalidPrivsError, limit_resources, @@ -343,7 +457,13 @@ from ansible.module_utils._text import to_native def main(): argument_spec = mysql_common_argument_spec() argument_spec.update( - user=dict(type='str', required=True, aliases=['name']), + name=dict(type='str', required=True, aliases=['user'], deprecated_aliases=[ + { + 'name': 'user', + 'version': '5.0.0', + 'collection_name': 'community.mysql', + }], + ), password=dict(type='str', no_log=True), encrypted=dict(type='bool', default=False), host=dict(type='str', default='localhost'), @@ -352,22 +472,31 @@ def main(): priv=dict(type='raw'), tls_requires=dict(type='dict'), append_privs=dict(type='bool', default=False), + subtract_privs=dict(type='bool', default=False), + attributes=dict(type='dict'), check_implicit_admin=dict(type='bool', default=False), - update_password=dict(type='str', default='always', choices=['always', 'on_create'], no_log=False), + update_password=dict(type='str', default='always', choices=['always', 'on_create', 'on_new_username'], no_log=False), sql_log_bin=dict(type='bool', default=True), plugin=dict(default=None, type='str'), plugin_hash_string=dict(default=None, type='str'), plugin_auth_string=dict(default=None, type='str'), + salt=dict(default=None, type='str'), resource_limits=dict(type='dict'), force_context=dict(type='bool', default=False), + session_vars=dict(type='dict'), + column_case_sensitive=dict(type='bool', default=None), # TODO 4.0.0 add default=True + password_expire=dict(type='str', choices=['now', 'never', 'default', 'interval'], no_log=True), + password_expire_interval=dict(type='int', required_if=[('password_expire', 'interval', True)], no_log=True), + locked=dict(type='bool'), ) module = AnsibleModule( argument_spec=argument_spec, supports_check_mode=True, + mutually_exclusive=(('append_privs', 'subtract_privs'),) ) login_user = module.params["login_user"] login_password = module.params["login_password"] - user = module.params["user"] + user = module.params["name"] password = module.params["password"] encrypted = module.boolean(module.params["encrypted"]) host = module.params["host"].lower() @@ -379,7 +508,9 @@ def main(): connect_timeout = module.params["connect_timeout"] config_file = module.params["config_file"] append_privs = module.boolean(module.params["append_privs"]) + subtract_privs = module.boolean(module.params['subtract_privs']) update_password = module.params['update_password'] + attributes = module.params['attributes'] ssl_cert = module.params["client_cert"] ssl_key = module.params["client_key"] ssl_ca = module.params["ca_cert"] @@ -391,7 +522,14 @@ def main(): plugin = module.params["plugin"] plugin_hash_string = module.params["plugin_hash_string"] plugin_auth_string = module.params["plugin_auth_string"] + salt = module.params["salt"] resource_limits = module.params["resource_limits"] + session_vars = module.params["session_vars"] + column_case_sensitive = module.params["column_case_sensitive"] + password_expire = module.params["password_expire"] + password_expire_interval = module.params["password_expire_interval"] + locked = module.boolean(module.params['locked']) + if priv and not isinstance(priv, (str, dict)): module.fail_json(msg="priv parameter must be str or dict but %s was passed" % type(priv)) @@ -401,45 +539,76 @@ def main(): if mysql_driver is None: module.fail_json(msg=mysql_driver_fail_msg) + if password_expire_interval and password_expire_interval < 1: + module.fail_json(msg="password_expire_interval value \ + should be positive number") + + if salt: + if not plugin_auth_string: + module.fail_json(msg="salt requires plugin_auth_string") + if len(salt) != 20: + module.fail_json(msg="salt must be 20 characters long") + if plugin not in ['caching_sha2_password', 'sha256_password']: + module.fail_json(msg="salt requires caching_sha2_password or sha256_password plugin") + 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) + connect_timeout=connect_timeout, check_hostname=check_hostname, autocommit=True) 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) + connect_timeout=connect_timeout, check_hostname=check_hostname, autocommit=True) 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))) + # TODO Release 4.0.0 : Remove this test and variable assignation + if column_case_sensitive is None: + column_case_sensitive = False + module.warn("Option column_case_sensitive is not provided. " + "The default is now false, so the column's name will be uppercased. " + "The default will be changed to true in community.mysql 4.0.0.") + if not sql_log_bin: cursor.execute("SET SQL_LOG_BIN=0;") - get_impl(cursor) + if session_vars: + set_session_vars(module, cursor, session_vars) if priv is not None: try: mode = get_mode(cursor) except Exception as e: module.fail_json(msg=to_native(e)) - priv = privileges_unpack(priv, mode) + priv = privileges_unpack(priv, mode, column_case_sensitive, ensure_usage=not subtract_privs) + password_changed = False + final_attributes = None if state == "present": if user_exists(cursor, user, host, host_all): try: if update_password == "always": - changed, msg = user_mod(cursor, user, host, host_all, password, encrypted, - plugin, plugin_hash_string, plugin_auth_string, - priv, append_privs, tls_requires, module) + result = user_mod(cursor, user, host, host_all, password, encrypted, + plugin, plugin_hash_string, plugin_auth_string, salt, + priv, append_privs, subtract_privs, attributes, tls_requires, module, + password_expire, password_expire_interval, locked=locked) + else: - changed, msg = user_mod(cursor, user, host, host_all, None, encrypted, - plugin, plugin_hash_string, plugin_auth_string, - priv, append_privs, tls_requires, module) + result = user_mod(cursor=cursor, user=user, host=host, host_all=host_all, password=None, + encrypted=encrypted, plugin=None, plugin_hash_string=None, plugin_auth_string=None, + salt=None, new_priv=priv, append_privs=append_privs, subtract_privs=subtract_privs, + attributes=attributes, tls_requires=tls_requires, module=module, + password_expire=password_expire, password_expire_interval=password_expire_interval, + locked=locked) + changed = result['changed'] + msg = result['msg'] + password_changed = result['password_changed'] + final_attributes = result['attributes'] except (SQLParseError, InvalidPrivsError, mysql_driver.Error) as e: module.fail_json(msg=to_native(e)) @@ -447,9 +616,16 @@ def main(): if host_all: module.fail_json(msg="host_all parameter cannot be used when adding a user") try: - changed = user_add(cursor, user, host, host_all, password, encrypted, - plugin, plugin_hash_string, plugin_auth_string, - priv, tls_requires, module.check_mode) + if subtract_privs: + priv = None # avoid granting unwanted privileges + reuse_existing_password = update_password == 'on_new_username' + result = user_add(cursor, user, host, host_all, password, encrypted, + plugin, plugin_hash_string, plugin_auth_string, salt, + priv, attributes, tls_requires, reuse_existing_password, module, + password_expire, password_expire_interval, locked=locked) + changed = result['changed'] + password_changed = result['password_changed'] + final_attributes = result['attributes'] if changed: msg = "User added" @@ -466,7 +642,7 @@ def main(): else: changed = False msg = "User doesn't exist" - module.exit_json(changed=changed, user=user, msg=msg) + module.exit_json(changed=changed, user=user, msg=msg, password_changed=password_changed, attributes=final_attributes) if __name__ == '__main__': diff --git a/plugins/modules/mysql_variables.py b/plugins/modules/mysql_variables.py index 06beee3..8632a52 100644 --- a/plugins/modules/mysql_variables.py +++ b/plugins/modules/mysql_variables.py @@ -12,9 +12,9 @@ DOCUMENTATION = r''' --- module: mysql_variables -short_description: Manage MySQL global variables +short_description: Manage MySQL or MariaDB global variables description: -- Query / Set MySQL variables. +- Query / Set MySQL or MariaDB variables. author: - Balazs Pocze (@banyek) options: @@ -22,10 +22,11 @@ options: description: - Variable name to operate. type: str - required: yes + required: true value: description: - If set, then sets variable value to this. + - With boolean values, use C(0)|C(1) or quoted C("ON")|C("OFF"). type: str mode: description: @@ -44,8 +45,9 @@ options: default: global version_added: '0.1.0' -notes: -- Does not support C(check_mode). +attributes: + check_mode: + support: none seealso: - module: community.mysql.mysql_info @@ -53,20 +55,31 @@ seealso: description: Complete reference of the MySQL SET command documentation. link: https://dev.mysql.com/doc/refman/8.0/en/set-statement.html +notes: + - Compatible with MariaDB or MySQL. + extends_documentation_fragment: - community.mysql.mysql ''' EXAMPLES = r''' +# If you encounter the "Please explicitly state intended protocol" error, +# use the login_unix_socket argument - name: Check for sync_binlog setting community.mysql.mysql_variables: variable: sync_binlog + login_unix_socket: /run/mysqld/mysqld.sock - name: Set read_only variable to 1 persistently community.mysql.mysql_variables: variable: read_only value: 1 mode: persist + +- name: Set a boolean using ON/OFF notation + mysql_variables: + variable: log_slow_replica_statements + value: "ON" # Make sure it's quoted ''' RETURN = r''' @@ -169,10 +182,22 @@ def setvariable(cursor, mysqlvar, value, mode='global'): return result +def convert_bool_setting_value_wanted(val): + """Converts passed value from 0,1,on,off to ON/OFF + as it's represented in the server. + """ + if val in ('on', 1): + val = 'ON' + elif val in ('off', 0): + val = 'OFF' + + return val + + def main(): argument_spec = mysql_common_argument_spec() argument_spec.update( - variable=dict(type='str'), + variable=dict(type='str', required=True), value=dict(type='str'), mode=dict(type='str', choices=['global', 'persist', 'persist_only'], default='global'), ) @@ -196,7 +221,7 @@ def main(): if mysqlvar is None: module.fail_json(msg="Cannot run without variable to operate with") - if match('^[0-9a-z_.]+$', mysqlvar) is None: + if match('^[0-9A-Za-z_.]+$', mysqlvar) is None: module.fail_json(msg="invalid variable name \"%s\"" % mysqlvar) if mysql_driver is None: module.fail_json(msg=mysql_driver_fail_msg) @@ -236,6 +261,9 @@ def main(): # Type values before using them value_wanted = typedvalue(value) value_actual = typedvalue(mysqlvar_val) + if value_actual in ('ON', 'OFF') and value_wanted not in ('ON', 'OFF'): + value_wanted = convert_bool_setting_value_wanted(value_wanted) + value_in_auto_cnf = None if var_in_mysqld_auto_cnf is not None: value_in_auto_cnf = typedvalue(var_in_mysqld_auto_cnf) diff --git a/run_all_tests.py b/run_all_tests.py new file mode 100755 index 0000000..94cf799 --- /dev/null +++ b/run_all_tests.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python + +import yaml +import os + +github_workflow_file = '.github/workflows/ansible-test-plugins.yml' + + +def read_github_workflow_file(): + with open(github_workflow_file, 'r') as gh_file: + try: + return yaml.safe_load(gh_file) + except yaml.YAMLError as exc: + print(exc) + + +def extract_value(target, dict_yaml): + for key, value in dict_yaml.items(): + if key == target: + return value + + +def extract_matrix(workflow_yaml): + jobs = extract_value('jobs', workflow_yaml) + integration = extract_value('integration', jobs) + strategy = extract_value('strategy', integration) + matrix = extract_value('matrix', strategy) + return matrix + + +def is_exclude(exclude_list, test_suite): + test_is_excluded = False + for excl in exclude_list: + match = 0 + + if 'ansible' in excl: + if excl.get('ansible') == test_suite.get('ansible'): + match += 1 + + if 'db_engine_name' in excl: + if excl.get('db_engine_name') == test_suite.get('db_engine_name'): + match += 1 + + if 'db_engine_version' in excl: + if excl.get('db_engine_version') == test_suite.get('db_engine_version'): + match += 1 + + if 'python' in excl: + if excl.get('python') == test_suite.get('python'): + match += 1 + + if 'connector_name' in excl: + if excl.get('connector_name') == test_suite.get('connector_name'): + match += 1 + + if 'connector_version' in excl: + if excl.get('connector_version') == test_suite.get('connector_version'): + match += 1 + + if match > 1: + test_is_excluded = True + return test_is_excluded + + return test_is_excluded + + +def main(): + workflow_yaml = read_github_workflow_file() + tests_matrix_yaml = extract_matrix(workflow_yaml) + + matrix = [] + exclude_list = tests_matrix_yaml.get('exclude') + for ansible in tests_matrix_yaml.get('ansible'): + for db_engine_name in tests_matrix_yaml.get('db_engine_name'): + for db_engine_version in tests_matrix_yaml.get('db_engine_version'): + for python in tests_matrix_yaml.get('python'): + for connector_name in tests_matrix_yaml.get('connector_name'): + for connector_version in tests_matrix_yaml.get('connector_version'): + test_suite = { + 'ansible': ansible, + 'db_engine_name': db_engine_name, + 'db_engine_version': db_engine_version, + 'python': python, + 'connector_name': connector_name, + 'connector_version': connector_version + } + if not is_exclude(exclude_list, test_suite): + matrix.append(test_suite) + + for tests in matrix: + a = tests.get('ansible') + dn = tests.get('db_engine_name') + dv = tests.get('db_engine_version') + p = tests.get('python') + cn = tests.get('connector_name') + cv = tests.get('connector_version') + make_cmd = ( + f'make ' + f'ansible="{a}" ' + f'db_engine_name="{dn}" ' + f'db_engine_version="{dv}" ' + f'python="{p}" ' + f'connector_name="{cn}" ' + f'connector_version="{cv}" ' + f'test-integration' + ) + print(f'Run tests for: Ansible: {a}, DB: {dn} {dv}, Python: {p}, Connector: {cn} {cv}') + os.system(make_cmd) + # TODO, allow for CTRL+C to break the loop more easily + # TODO, store the failures from this iteration + # TODO, display a summary of failures from every iterations + + +if __name__ == '__main__': + main() diff --git a/simplified_bsd.txt b/simplified_bsd.txt new file mode 100644 index 0000000..6810e04 --- /dev/null +++ b/simplified_bsd.txt @@ -0,0 +1,8 @@ +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/tests/integration/old_mariadb_replication/defaults/main.yml b/tests/integration/old_mariadb_replication/defaults/main.yml index 3751f4e..eb32dc1 100644 --- a/tests/integration/old_mariadb_replication/defaults/main.yml +++ b/tests/integration/old_mariadb_replication/defaults/main.yml @@ -1,3 +1,5 @@ +--- +mysql_host: "{{ gateway_addr }}" master_port: 3306 standby_port: 3307 test_db: test_db diff --git a/tests/integration/old_mariadb_replication/tasks/main.yml b/tests/integration/old_mariadb_replication/tasks/main.yml index 4ea76a9..321ba4d 100644 --- a/tests/integration/old_mariadb_replication/tasks/main.yml +++ b/tests/integration/old_mariadb_replication/tasks/main.yml @@ -1,4 +1,4 @@ -# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) +# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # Initial CI tests of mysql_replication module diff --git a/tests/integration/old_mariadb_replication/tasks/mariadb_master_use_gtid.yml b/tests/integration/old_mariadb_replication/tasks/mariadb_master_use_gtid.yml index e3e7605..8977c10 100644 --- a/tests/integration/old_mariadb_replication/tasks/mariadb_master_use_gtid.yml +++ b/tests/integration/old_mariadb_replication/tasks/mariadb_master_use_gtid.yml @@ -1,4 +1,4 @@ -# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) +# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # Tests for master_use_gtid parameter. @@ -11,7 +11,7 @@ # Auxiliary step: - name: Get master status mysql_replication: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: "{{ primary_db.port }}" mode: getmaster register: primary_status @@ -19,10 +19,10 @@ # Set master_use_gtid disabled: - name: Run replication mysql_replication: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: "{{ replica_db.port }}" mode: changemaster - master_host: 127.0.0.1 + master_host: '{{ mysql_host }}' master_port: "{{ primary_db.port }}" master_user: "{{ replication_user }}" master_password: "{{ replication_pass }}" @@ -38,13 +38,13 @@ # Start standby for further tests: - name: Start standby mysql_replication: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: "{{ primary_db.port }}" mode: startslave - name: Get standby status mysql_replication: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: "{{ replica_db.port }}" mode: getslave register: slave_status @@ -56,7 +56,7 @@ # Stop standby for further tests: - name: Stop standby mysql_replication: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: "{{ replica_db.port }}" mode: stopslave @@ -67,7 +67,7 @@ # Auxiliary step: - name: Get master status mysql_replication: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: "{{ primary_db.port }}" mode: getmaster register: primary_status @@ -75,10 +75,10 @@ # Set master_use_gtid current_pos: - name: Run replication mysql_replication: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: "{{ replica_db.port }}" mode: changemaster - master_host: 127.0.0.1 + master_host: '{{ mysql_host }}' master_port: "{{ primary_db.port }}" master_user: "{{ replication_user }}" master_password: "{{ replication_pass }}" @@ -94,13 +94,13 @@ # Start standby for further tests: - name: Start standby mysql_replication: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: "{{ primary_db.port }}" mode: startslave - name: Get standby status mysql_replication: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: "{{ replica_db.port }}" mode: getslave register: slave_status @@ -112,7 +112,7 @@ # Stop standby for further tests: - name: Stop standby mysql_replication: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: "{{ replica_db.port }}" mode: stopslave @@ -123,7 +123,7 @@ # Auxiliary step: - name: Get master status mysql_replication: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: "{{ primary_db.port }}" mode: getmaster register: primary_status @@ -131,10 +131,10 @@ # Set master_use_gtid slave_pos: - name: Run replication mysql_replication: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: "{{ replica_db.port }}" mode: changemaster - master_host: 127.0.0.1 + master_host: '{{ mysql_host }}' master_port: "{{ primary_db.port }}" master_user: "{{ replication_user }}" master_password: "{{ replication_pass }}" @@ -150,13 +150,13 @@ # Start standby for further tests: - name: Start standby mysql_replication: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: "{{ primary_db.port }}" mode: startslave - name: Get standby status mysql_replication: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: "{{ replica_db.port }}" mode: getslave register: slave_status @@ -168,6 +168,6 @@ # Stop standby for further tests: - name: Stop standby mysql_replication: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: "{{ replica_db.port }}" mode: stopslave diff --git a/tests/integration/old_mariadb_replication/tasks/mariadb_replication_connection_name.yml b/tests/integration/old_mariadb_replication/tasks/mariadb_replication_connection_name.yml index 98fa5fe..337a839 100644 --- a/tests/integration/old_mariadb_replication/tasks/mariadb_replication_connection_name.yml +++ b/tests/integration/old_mariadb_replication/tasks/mariadb_replication_connection_name.yml @@ -1,23 +1,23 @@ -# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) +# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # Needs for further tests: - name: Stop slave mysql_replication: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: "{{ replica_db.port }}" mode: stopslave - name: Reset slave all mysql_replication: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: "{{ replica_db.port }}" mode: resetslaveall # Get master log pos: - name: Get master status mysql_replication: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: "{{ primary_db.port }}" mode: getmaster register: primary_status @@ -25,10 +25,10 @@ # Test changemaster mode: - name: Run replication with connection_name mysql_replication: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: "{{ replica_db.port }}" mode: changemaster - master_host: 127.0.0.1 + master_host: '{{ mysql_host }}' master_port: "{{ primary_db.port }}" master_user: "{{ replication_user }}" master_password: "{{ replication_pass }}" @@ -45,7 +45,7 @@ # Test startslave mode: - name: Start slave with connection_name mysql_replication: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: "{{ replica_db.port }}" mode: startslave connection_name: "{{ conn_name }}" @@ -59,7 +59,7 @@ # Test getslave mode: - name: Get standby statu with connection_name mysql_replication: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: "{{ replica_db.port }}" mode: getslave connection_name: "{{ conn_name }}" @@ -68,7 +68,7 @@ - assert: that: - slave_status.Is_Slave == true - - slave_status.Master_Host == '127.0.0.1' + - slave_status.Master_Host == ''{{ mysql_host }}'' - slave_status.Exec_Master_Log_Pos == primary_status.Position - slave_status.Master_Port == {{ primary_db.port }} - slave_status.Last_IO_Errno == 0 @@ -78,7 +78,7 @@ # Test stopslave mode: - name: Stop slave with connection_name mysql_replication: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: "{{ replica_db.port }}" mode: stopslave connection_name: "{{ conn_name }}" @@ -92,7 +92,7 @@ # Test reset - name: Reset slave with connection_name mysql_replication: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: "{{ replica_db.port }}" mode: resetslave connection_name: "{{ conn_name }}" @@ -106,7 +106,7 @@ # Test reset all - name: Reset slave all with connection_name mysql_replication: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: "{{ replica_db.port }}" mode: resetslaveall connection_name: "{{ conn_name }}" diff --git a/tests/integration/old_mariadb_replication/tasks/mariadb_replication_initial.yml b/tests/integration/old_mariadb_replication/tasks/mariadb_replication_initial.yml index 86a6760..1a95a55 100644 --- a/tests/integration/old_mariadb_replication/tasks/mariadb_replication_initial.yml +++ b/tests/integration/old_mariadb_replication/tasks/mariadb_replication_initial.yml @@ -1,13 +1,13 @@ -# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) +# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # Preparation: - name: Create user for replication - shell: "echo \"GRANT REPLICATION SLAVE ON *.* TO '{{ replication_user }}'@'localhost' IDENTIFIED BY '{{ replication_pass }}'; FLUSH PRIVILEGES;\" | mysql -P {{ primary_db.port }} -h 127.0.0.1" + shell: "echo \"GRANT REPLICATION SLAVE ON *.* TO '{{ replication_user }}'@'localhost' IDENTIFIED BY '{{ replication_pass }}'; FLUSH PRIVILEGES;\" | mysql -P {{ primary_db.port }} -h '{{ mysql_host }}'" - name: Create test database mysql_db: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ primary_db.port }}' state: present name: '{{ test_db }}' @@ -16,12 +16,12 @@ shell: 'mysqldump -P {{ primary_db.port }} -h 127.0.01 --all-databases --master-data=2 > {{ dump_path }}' - name: Restore the dump to the replica - shell: 'mysql -P {{ replica_db.port }} -h 127.0.0.1 < {{ dump_path }}' + shell: "mysql -P {{ replica_db.port }} -h '{{ mysql_host }}' < {{ dump_path }}" # Test getmaster mode: - name: Get master status mysql_replication: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: "{{ primary_db.port }}" mode: getmaster register: master_status @@ -35,10 +35,10 @@ # Test changemaster mode: - name: Run replication mysql_replication: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: "{{ replica_db.port }}" mode: changemaster - master_host: 127.0.0.1 + master_host: '{{ mysql_host }}' master_port: "{{ primary_db.port }}" master_user: "{{ replication_user }}" master_password: "{{ replication_pass }}" @@ -54,7 +54,7 @@ # Test startslave mode: - name: Start slave mysql_replication: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: "{{ replica_db.port }}" mode: startslave register: result @@ -67,7 +67,7 @@ # Test getslave mode: - name: Get replica status mysql_replication: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: "{{ replica_db.port }}" mode: getslave register: slave_status @@ -75,7 +75,7 @@ - assert: that: - slave_status.Is_Slave == true - - slave_status.Master_Host == '127.0.0.1' + - slave_status.Master_Host == ''{{ mysql_host }}'' - slave_status.Exec_Master_Log_Pos == master_status.Position - slave_status.Master_Port == {{ primary_db.port }} - slave_status.Last_IO_Errno == 0 @@ -85,7 +85,7 @@ # Test stopslave mode: - name: Stop slave mysql_replication: - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: "{{ replica_db.port }}" mode: stopslave register: result diff --git a/tests/integration/targets/setup_controller/files/mysql.gpg b/tests/integration/targets/setup_controller/files/mysql.gpg new file mode 100644 index 0000000..117f1e7 --- /dev/null +++ b/tests/integration/targets/setup_controller/files/mysql.gpg @@ -0,0 +1,49 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: SKS 1.1.6 +Comment: Hostname: pgp.mit.edu + +mQINBGU2rNoBEACSi5t0nL6/Hj3d0PwsbdnbY+SqLUIZ3uWZQm6tsNhvTnahvPPZBGdl99iW +YTt2KmXp0KeN2s9pmLKkGAbacQP1RqzMFnoHawSMf0qTUVjAvhnI4+qzMDjTNSBq9fa3nHmO +YxownnrRkpiQUM/yD7/JmVENgwWb6akZeGYrXch9jd4XV3t8OD6TGzTedTki0TDNr6YZYhC7 +jUm9fK9Zs299pzOXSxRRNGd+3H9gbXizrBu4L/3lUrNf//rM7OvV9Ho7u9YYyAQ3L3+OABK9 +FKHNhrpi8Q0cbhvWkD4oCKJ+YZ54XrOG0YTg/YUAs5/3//FATI1sWdtLjJ5pSb0onV3LIbar +RTN8lC4Le/5kd3lcot9J8b3EMXL5p9OGW7wBfmNVRSUI74Vmwt+v9gyp0Hd0keRCUn8lo/1V +0YD9i92KsE+/IqoYTjnya/5kX41jB8vr1ebkHFuJ404+G6ETd0owwxq64jLIcsp/GBZHGU0R +KKAo9DRLH7rpQ7PVlnw8TDNlOtWt5EJlBXFcPL+NgWbqkADAyA/XSNeWlqonvPlYfmasnAHA +pMd9NhPQhC7hJTjCiAwG8UyWpV8Dj07DHFQ5xBbkTnKH2OrJtguPqSNYtTASbsWz09S8ujoT +DXFT17NbFM2dMIiq0a4VQB3SzH13H2io9Cbg/TzJrJGmwgoXgwARAQABtDZNeVNRTCBSZWxl +YXNlIEVuZ2luZWVyaW5nIDxteXNxbC1idWlsZEBvc3Mub3JhY2xlLmNvbT6JAlQEEwEIAD4W +IQS8pDQXw7SF3RKOxtS3s7eIqNN4XAUCZTas2gIbAwUJA8JnAAULCQgHAgYVCgkICwIEFgID +AQIeAQIXgAAKCRC3s7eIqNN4XLzoD/9PlpWtfHlI8eQTHwGsGIwFA+fgipyDElapHw3MO+K9 +VOEYRZCZSuBXHJe9kjGEVCGUDrfImvgTuNuqYmVUV+wyhP+w46W/cWVkqZKAW0hNp0TTvu3e +Dwap7gdk80VF24Y2Wo0bbiGkpPiPmB59oybGKaJ756JlKXIL4hTtK3/hjIPFnb64Ewe4YLZy +oJu0fQOyA8gXuBoalHhUQTbRpXI0XI3tpZiQemNbfBfJqXo6LP3/LgChAuOfHIQ8alvnhCwx +hNUSYGIRqx+BEbJw1X99Az8XvGcZ36VOQAZztkW7mEfH9NDPz7MXwoEvduc61xwlMvEsUIaS +fn6SGLFzWPClA98UMSJgF6sKb+JNoNbzKaZ8V5w13msLb/pq7hab72HH99XJbyKNliYj3+KA +3q0YLf+Hgt4Y4EhIJ8x2+g690Np7zJF4KXNFbi1BGloLGm78akY1rQlzpndKSpZq5KWw8FY/ +1PEXORezg/BPD3Etp0AVKff4YdrDlOkNB7zoHRfFHAvEuuqti8aMBrbRnRSG0xunMUOEhbYS +/wOOTl0g3bF9NpAkfU1Fun57N96Us2T9gKo9AiOY5DxMe+IrBg4zaydEOovgqNi2wbU0MOBQ +b23Puhj7ZCIXcpILvcx9ygjkONr75w+XQrFDNeux4Znzay3ibXtAPqEykPMZHsZ2sbkCDQRl +NqzaARAAsdvBo8WRqZ5WVVk6lReD8b6Zx83eJUkV254YX9zn5t8KDRjYOySwS75mJIaZLsv0 +YQjJk+5rt10tejyCrJIFo9CMvCmjUKtVbgmhfS5+fUDRrYCEZBBSa0Dvn68EBLiHugr+SPXF +6o1hXEUqdMCpB6oVp6X45JVQroCKIH5vsCtw2jU8S2/IjjV0V+E/zitGCiZaoZ1f6NG7ozyF +ep1CSAReZu/sssk0pCLlfCebRd9Rz3QjSrQhWYuJa+eJmiF4oahnpUGktxMD632I9aG+IMfj +tNJNtX32MbO+Se+cCtVc3cxSa/pR+89a3cb9IBA5tFF2Qoekhqo/1mmLi93Xn6uDUhl5tVxT +nB217dBT27tw+p0hjd9hXZRQbrIZUTyh3+8EMfmAjNSIeR+th86xRd9XFRr9EOqrydnALOUr +9cT7TfXWGEkFvn6ljQX7f4RvjJOTbc4jJgVFyu8K+VU6u1NnFJgDiNGsWvnYxAf7gDDbUSXE +uC2anhWvxPvpLGmsspngge4yl+3nv+UqZ9sm6LCebR/7UZ67tYz3p6xzAOVgYsYcxoIUuEZX +jHQtsYfTZZhrjUWBJ09jrMvlKUHLnS437SLbgoXVYZmcqwAWpVNOLZf+fFm4IE5aGBG5Dho2 +CZ6ujngW9Zkn98T1d4N0MEwwXa2V6T1ijzcqD7GApZUAEQEAAYkCPAQYAQgAJhYhBLykNBfD +tIXdEo7G1Lezt4io03hcBQJlNqzaAhsMBQkDwmcAAAoJELezt4io03hcXqMP/01aPT3A3Sg7 +oTQoHdCxj04ELkzrezNWGM+YwbSKrR2LoXR8zf2tBFzc2/Tl98V0+68f/eCvkvqCuOtq4392 +Ps23j9W3r5XG+GDOwDsx0gl0E+Qkw07pwdJctA6efsmnRkjF2YVO0N9MiJA1tc8NbNXpEEHJ +Z7F8Ri5cpQrGUz/AY0eae2b7QefyP4rpUELpMZPjc8Px39Fe1DzRbT+5E19TZbrpbwlSYs1i +CzS5YGFmpCRyZcLKXo3zS6N22+82cnRBSPPipiO6WaQawcVMlQO1SX0giB+3/DryfN9VuIYd +1EWCGQa3O0MVu6o5KVHwPgl9R1P6xPZhurkDpAd0b1s4fFxin+MdxwmG7RslZA9CXRPpzo7/ +fCMW8sYOH15DP+YfUckoEreBt+zezBxbIX2CGGWEV9v3UBXadRtwxYQ6sN9bqW4jm1b41vNA +17b6CVH6sVgtU3eN+5Y9an1e5jLD6kFYx+OIeqIIId/TEqwS61csY9aav4j4KLOZFCGNU0FV +ji7NQewSpepTcJwfJDOzmtiDP4vol1ApJGLRwZZZ9PB6wsOgDOoP6sr0YrDI/NNX2RyXXbgl +nQ1yJZVSH3/3eo6knG2qTthUKHCRDNKdy9Qqc1x4WWWtSRjh+zX8AvJK2q1rVLH2/3ilxe9w +cAZUlaj3id3TxquAlud4lWDz +=h5nH +-----END PGP PUBLIC KEY BLOCK----- diff --git a/tests/integration/targets/setup_controller/tasks/fake_root.yml b/tests/integration/targets/setup_controller/tasks/fake_root.yml new file mode 100644 index 0000000..49531b8 --- /dev/null +++ b/tests/integration/targets/setup_controller/tasks/fake_root.yml @@ -0,0 +1,11 @@ +--- + +- name: "{{ role_name }} | Fake root | Ensure folder" + ansible.builtin.file: + path: "{{ playbook_dir }}/root" + state: directory + +- name: "{{ role_name }} | Fake root | Ensure default file exists" + ansible.builtin.file: + path: "{{ playbook_dir }}/root/.my.cnf" + state: touch diff --git a/tests/integration/targets/setup_controller/tasks/main.yml b/tests/integration/targets/setup_controller/tasks/main.yml new file mode 100644 index 0000000..91b5f82 --- /dev/null +++ b/tests/integration/targets/setup_controller/tasks/main.yml @@ -0,0 +1,21 @@ +--- +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: "{{ role_name }} | Main | Prepare the fake root folder" + ansible.builtin.import_tasks: + file: fake_root.yml + +- name: "{{ role_name }} | Main | Set variables" + ansible.builtin.import_tasks: + file: setvars.yml + +- name: "{{ role_name }} | Main | Install requirements" + ansible.builtin.import_tasks: + file: requirements.yml + +- name: "{{ role_name }} | Main | Verify all components version under test" + ansible.builtin.import_tasks: + file: verify.yml diff --git a/tests/integration/targets/setup_controller/tasks/requirements.yml b/tests/integration/targets/setup_controller/tasks/requirements.yml new file mode 100644 index 0000000..a576ce4 --- /dev/null +++ b/tests/integration/targets/setup_controller/tasks/requirements.yml @@ -0,0 +1,54 @@ +--- + +# We use the ubuntu2204 image provided by ansible-test. + +# The GPG key is imported in the files folder from: +# https://dev.mysql.com/doc/refman/8.4/en/checking-gpg-signature.html +# Downloading the key on each iteration of the tests is too slow. +- name: Install MySQL PGP public key + ansible.builtin.copy: + src: files/mysql.gpg + dest: /usr/share/keyrings/mysql.gpg + owner: root + group: root + mode: '0644' + when: + - db_engine == 'mysql' + - db_version is version('8.4', '>=') + +- name: Add Apt signing key to keyring + ansible.builtin.apt_key: + id: A8D3785C + file: /usr/share/keyrings/mysql.gpg + state: present + when: + - db_engine == 'mysql' + - db_version is version('8.4', '>=') + +- name: Add MySQL 8.4 repository + ansible.builtin.apt_repository: + repo: deb http://repo.mysql.com/apt/ubuntu/ jammy mysql-8.4-lts mysql-tools + state: present + filename: mysql + when: + - db_engine == 'mysql' + - db_version is version('8.4', '>=') + +- name: "{{ role_name }} | Requirements | Install Linux packages" + ansible.builtin.package: + name: + - bzip2 # To test mysql_db dump compression + - "{{ db_engine }}-client" + + # The command mysql-config must be present for mysqlclient python package. + # The package libmysqlclient-dev that provides this command have a + # different name between Ubuntu 20.04 and 22.04. Luckily, libmysql++ is + # available on both. + - "{{ 'libmysql++-dev' if db_engine == 'mysql' else 'libmariadb-dev' }}" + state: present + +- name: "{{ role_name }} | Requirements | Install Python packages" + ansible.builtin.pip: + name: + - "{{ connector_name }}=={{ connector_version }}" + state: present diff --git a/tests/integration/targets/setup_controller/tasks/setvars.yml b/tests/integration/targets/setup_controller/tasks/setvars.yml new file mode 100644 index 0000000..0bb8c0e --- /dev/null +++ b/tests/integration/targets/setup_controller/tasks/setvars.yml @@ -0,0 +1,76 @@ +--- + +- name: "{{ role_name }} | Setvars | Install tools gather network facts" + ansible.builtin.package: + name: + - iproute2 + state: present + +- name: "{{ role_name }} | Setvars | Gather facts" + ansible.builtin.setup: + +- name: "{{ role_name }} | Setvars | Set Fact" + ansible.builtin.set_fact: + gateway_addr: "{{ ansible_default_ipv4.gateway }}" + connector_name_lookup: >- + {{ lookup( + 'file', + '/root/ansible_collections/community/mysql/tests/integration/connector_name' + ) }} + connector_version_lookup: >- + {{ lookup( + 'file', + '/root/ansible_collections/community/mysql/tests/integration/connector_version' + ) }} + db_engine_name_lookup: >- + {{ lookup( + 'file', + '/root/ansible_collections/community/mysql/tests/integration/db_engine_name' + ) }} + db_engine_version_lookup: >- + {{ lookup( + 'file', + '/root/ansible_collections/community/mysql/tests/integration/db_engine_version' + ) }} + ansible_version_lookup: >- + {{ lookup( + 'file', + '/root/ansible_collections/community/mysql/tests/integration/ansible' + ) }} + +- name: "{{ role_name }} | Setvars | Set Fact using above facts" + ansible.builtin.set_fact: + connector_name: "{{ connector_name_lookup.strip() }}" + connector_version: "{{ connector_version_lookup.strip() }}" + db_engine: "{{ db_engine_name_lookup.strip() }}" + db_version: "{{ db_engine_version_lookup.strip() }}" + test_ansible_version: >- + {%- if ansible_version_lookup == 'devel' -%} + {{ ansible_version_lookup }} + {%- else -%} + {{ ansible_version_lookup.split('-')[1].strip() }} + {%- endif -%} + mysql_command: >- + mysql + -h{{ gateway_addr }} + -P{{ mysql_primary_port }} + -u{{ mysql_user }} + -p{{ mysql_password }} + --protocol=tcp + mysql_command_wo_port: >- + mysql + -h{{ gateway_addr }} + -u{{ mysql_user }} + -p{{ mysql_password }} + --protocol=tcp + +- name: "{{ role_name }} | Setvars | Output test informations" + vars: + msg: |- + connector_name: {{ connector_name }} + connector_version: {{ connector_version }} + db_engine: {{ db_engine }} + db_version: {{ db_version }} + test_ansible_version: {{ test_ansible_version }} + ansible.builtin.debug: + msg: "{{ msg.split('\n') }}" diff --git a/tests/integration/targets/setup_controller/tasks/verify.yml b/tests/integration/targets/setup_controller/tasks/verify.yml new file mode 100644 index 0000000..b47e354 --- /dev/null +++ b/tests/integration/targets/setup_controller/tasks/verify.yml @@ -0,0 +1,67 @@ +--- + +- vars: + mysql_parameters: &mysql_params + login_user: root + login_password: msandbox + login_host: "{{ gateway_addr }}" + login_port: 3307 + + block: + + - name: Query Primary container over TCP for MySQL/MariaDB version + mysql_info: + <<: *mysql_params + filter: + - version + register: primary_info + + - name: Assert that test container runs the expected MySQL/MariaDB version + assert: + that: + - registred_db_version == db_version + vars: + registred_db_version: + "{{ primary_info.version.major }}.{{ primary_info.version.minor }}\ + .{{ primary_info.version.release }}" + + - name: Assert that mysql_info module used the expected version of pymysql + assert: + that: + - primary_info.connector_name == connector_name + - primary_info.connector_version == connector_version + when: + - connector_name == 'pymysql' + + - name: Assert that mysql_info module used the expected version of mysqlclient + assert: + that: + - primary_info.connector_name == 'MySQLdb' + - primary_info.connector_version == connector_version + when: + - connector_name == 'mysqlclient' + + - name: Get the python version in use + ansible.builtin.command: + cmd: python -V + changed_when: false + failed_when: false + register: python_version_in_use + + - name: Display the python version in use + ansible.builtin.debug: + msg: > + Python in use inside the test container: + ${{ python_version_in_use }} + when: + - python_version_in_use is defined + + - name: Assert that we run the expected ansible version + assert: + that: + - ansible_running_version == test_ansible_version + vars: + ansible_running_version: + "{{ ansible_version.major }}.{{ ansible_version.minor }}" + when: + - test_ansible_version != 'devel' # Devel will change overtime diff --git a/tests/integration/targets/setup_mysql/defaults/main.yml b/tests/integration/targets/setup_mysql/defaults/main.yml deleted file mode 100644 index cceb8f5..0000000 --- a/tests/integration/targets/setup_mysql/defaults/main.yml +++ /dev/null @@ -1,18 +0,0 @@ -dbdeployer_version: 1.64.0 -dbdeployer_home_dir: /opt/dbdeployer - -home_dir: /root - -mariadb_install: false - -mysql_version: 8.0.22 -mariadb_version: 10.5.4 - -mysql_base_port: 3306 - -percona_client_package: >- - {%- if mariadb_install -%} - mariadb-client - {%- else -%} - percona-server-client-5.7 - {%- endif -%} diff --git a/tests/integration/targets/setup_mysql/handlers/main.yml b/tests/integration/targets/setup_mysql/handlers/main.yml deleted file mode 100644 index 090a5e7..0000000 --- a/tests/integration/targets/setup_mysql/handlers/main.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -- name: "{{ role_name }} | handler | create dbdeployer installed file" - template: - src: installed_file.j2 - dest: "{{ dbdeployer_installed_file }}" - listen: create zookeeper installed file diff --git a/tests/integration/targets/setup_mysql/tasks/config.yml b/tests/integration/targets/setup_mysql/tasks/config.yml deleted file mode 100644 index 2b27e27..0000000 --- a/tests/integration/targets/setup_mysql/tasks/config.yml +++ /dev/null @@ -1,15 +0,0 @@ ---- -- name: "{{ role_name }} | config | download mysql tarball" - get_url: - url: "{{ install_src }}" - dest: "{{ dbdeployer_sandbox_download_dir }}/{{ install_tarball }}" - -- name: "{{ role_name }} | config | run unpack tarball" - shell: - cmd: "dbdeployer unpack {{ dbdeployer_sandbox_download_dir }}/{{ install_tarball }} --flavor {{ install_type }}" - creates: "{{ dbdeployer_sandbox_binary_dir }}/{{ install_version }}" - -- name: "{{ role_name }} | config | setup replication topology" - shell: - cmd: "dbdeployer deploy multiple {{ install_version }} --flavor {{ install_type }} --base-port {{ mysql_base_port }} --my-cnf-options=\"master_info_repository='TABLE'\" --my-cnf-options=\"relay_log_info_repository='TABLE'\"" - creates: "{{ dbdeployer_sandbox_home_dir }}/multi_msb_{{ install_version|replace('.','_') }}" diff --git a/tests/integration/targets/setup_mysql/tasks/dir.yml b/tests/integration/targets/setup_mysql/tasks/dir.yml deleted file mode 100644 index dc02879..0000000 --- a/tests/integration/targets/setup_mysql/tasks/dir.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -- name: "{{ role_name }} | dir | create dbdeployer directories" - file: - state: directory - path: "{{ item }}" - loop: - - "{{ dbdeployer_home_dir }}" - - "{{ dbdeployer_install_dir }}" - - "{{ dbdeployer_sandbox_download_dir }}" - - "{{ dbdeployer_sandbox_binary_dir }}" - - "{{ dbdeployer_sandbox_home_dir }}" diff --git a/tests/integration/targets/setup_mysql/tasks/install.yml b/tests/integration/targets/setup_mysql/tasks/install.yml deleted file mode 100644 index b64af25..0000000 --- a/tests/integration/targets/setup_mysql/tasks/install.yml +++ /dev/null @@ -1,90 +0,0 @@ ---- -- name: "{{ role_name }} | install | add apt signing key for percona" - apt_key: - keyserver: keyserver.ubuntu.com - id: 4D1BB29D63D98E422B2113B19334A25F8507EFA5 - state: present - when: install_type == 'mysql' - -- name: "{{ role_name }} | install | add percona repositories" - apt_repository: - repo: deb http://repo.percona.com/percona/apt {{ ansible_lsb.codename }} main - state: present - when: install_type == 'mysql' - -- name: "{{ role_name }} | install | add apt signing key for mariadb" - apt_key: - keyserver: keyserver.ubuntu.com - id: F1656F24C74CD1D8 - state: present - when: install_type == 'mariadb' - -- name: "{{ role_name }} | install | add mariadb repositories" - apt_repository: - repo: "deb [arch=amd64,arm64] https://downloads.mariadb.com/MariaDB/mariadb-{{ mysql_major_version }}/repo/ubuntu {{ ansible_lsb.codename }} main" - state: present - when: install_type == 'mariadb' - -- name: "{{ role_name }} | install | install packages required by percona" - apt: - name: "{{ percona_mysql_packages }}" - state: present - environment: - DEBIAN_FRONTEND: noninteractive - -- name: "{{ role_name }} | install | install packages required by mysql connector" - apt: - name: "{{ install_python_prereqs }}" - state: present - environment: - DEBIAN_FRONTEND: noninteractive - -- 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: - name: "{{ install_prereqs }}" - state: present - environment: - DEBIAN_FRONTEND: noninteractive - -- name: "{{ role_name }} | install | download and unpack dbdeployer" - unarchive: - remote_src: true - src: "{{ dbdeployer_src }}" - dest: "{{ dbdeployer_install_dir }}" - creates: "{{ dbdeployer_installed_file }}" - register: dbdeployer_tarball_install - notify: - - create zookeeper installed file - until: dbdeployer_tarball_install is not failed - retries: 6 - delay: 5 - -- name: "{{ role_name }} | install | create symlink" - file: - src: "{{ dbdeployer_install_dir }}/dbdeployer-{{ dbdeployer_version }}.linux" - dest: /usr/local/bin/dbdeployer - follow: false - state: link - -- meta: flush_handlers diff --git a/tests/integration/targets/setup_mysql/tasks/main.yml b/tests/integration/targets/setup_mysql/tasks/main.yml deleted file mode 100644 index c6a8348..0000000 --- a/tests/integration/targets/setup_mysql/tasks/main.yml +++ /dev/null @@ -1,11 +0,0 @@ ---- -#################################################################### -# WARNING: These are designed specifically for Ansible tests # -# and should not be used as examples of how to write Ansible roles # -#################################################################### - -- import_tasks: setvars.yml -- import_tasks: dir.yml -- import_tasks: install.yml -- import_tasks: config.yml -- import_tasks: verify.yml diff --git a/tests/integration/targets/setup_mysql/tasks/setvars.yml b/tests/integration/targets/setup_mysql/tasks/setvars.yml deleted file mode 100644 index cfc90c1..0000000 --- a/tests/integration/targets/setup_mysql/tasks/setvars.yml +++ /dev/null @@ -1,33 +0,0 @@ ---- -- name: "{{ role_name }} | setvars | split mysql version in parts" - set_fact: - mysql_version_parts: >- - {%- if mariadb_install -%} - {{ mariadb_version.split('.') }} - {%- else -%} - {{ mysql_version.split('.') }} - {%- endif -%} - -- name: "{{ role_name }} | setvars | get mysql major version" - set_fact: - mysql_major_version: "{{ mysql_version_parts[0] + '.' + mysql_version_parts[1] }}" - -- name: "{{ role_name }} | setvars | set the appropriate extension dependent on the mysql version" - set_fact: - mysql_compression_extension: "{{ mysql_version is version('8.0.0', '<') | ternary('gz', 'xz') }}" - -- name: "{{ role_name }} | setvars | set the install type" - set_fact: - install_type: "{{ mariadb_install | ternary('mariadb', 'mysql') }}" - -- name: "{{ role_name }} | setvars | set install_version" - set_fact: - install_version: "{{ lookup('vars', install_type + '_version') }}" - -- name: "{{ role_name }} | setvars | set install_tarball" - set_fact: - install_tarball: "{{ lookup('vars', install_type + '_tarball') }}" - -- name: "{{ role_name }} | setvars | set install_src" - set_fact: - install_src: "{{ lookup('vars', install_type + '_src') }}" diff --git a/tests/integration/targets/setup_mysql/tasks/verify.yml b/tests/integration/targets/setup_mysql/tasks/verify.yml deleted file mode 100644 index ca383d9..0000000 --- a/tests/integration/targets/setup_mysql/tasks/verify.yml +++ /dev/null @@ -1,27 +0,0 @@ ---- -- name: "{{ role_name }} | verify | confirm primary is running and get the port" - shell: "{{ dbdeployer_sandbox_home_dir }}/multi_msb_{{ install_version|replace('.','_') }}/n1 -BNe'select @@port'" - register: primary_port - -- name: "{{ role_name }} | verify | confirm replica1 is running and get the port" - shell: "{{ dbdeployer_sandbox_home_dir }}/multi_msb_{{ install_version|replace('.','_') }}/n2 -BNe'select @@port'" - register: replica1_port - -- name: "{{ role_name }} | verify | confirm replica2 is running and get the port" - shell: "{{ dbdeployer_sandbox_home_dir }}/multi_msb_{{ install_version|replace('.','_') }}/n3 -BNe'select @@port'" - register: replica2_port - -- name: "{{ role_name }} | verify | confirm primary is running on expected port" - assert: - that: - - primary_port.stdout|int == 3307 - -- name: "{{ role_name }} | verify | confirm replica1 is running on expected port" - assert: - that: - - replica1_port.stdout|int == 3308 - -- name: "{{ role_name }} | verify | confirm replica2 is running on expected port" - assert: - that: - - replica2_port.stdout|int == 3309 diff --git a/tests/integration/targets/setup_mysql/templates/installed_file.j2 b/tests/integration/targets/setup_mysql/templates/installed_file.j2 deleted file mode 100644 index 862a357..0000000 --- a/tests/integration/targets/setup_mysql/templates/installed_file.j2 +++ /dev/null @@ -1 +0,0 @@ -{{ dbdeployer_version }} diff --git a/tests/integration/targets/setup_mysql/vars/main.yml b/tests/integration/targets/setup_mysql/vars/main.yml deleted file mode 100644 index 94b43b4..0000000 --- a/tests/integration/targets/setup_mysql/vars/main.yml +++ /dev/null @@ -1,28 +0,0 @@ ---- -dbdeployer_install_dir: "{{ dbdeployer_home_dir }}/dbdeployer_{{ dbdeployer_version }}" -dbdeployer_src: "https://github.com/datacharmer/dbdeployer/releases/download/v{{ dbdeployer_version }}/dbdeployer-{{ dbdeployer_version }}.linux.tar.gz" -dbdeployer_installed_file: "{{ dbdeployer_home_dir }}/dbdeployer_installed" - -dbdeployer_sandbox_download_dir: "{{ home_dir }}/downloads" -dbdeployer_sandbox_binary_dir: "{{ home_dir }}/opt/mysql" -dbdeployer_sandbox_home_dir: "{{ home_dir }}/sandboxes" - -percona_mysql_packages: - - "{{ percona_client_package }}" - -python_packages: [pymysql == 0.9.3] - -install_prereqs: - - libaio1 - - libnuma1 - - libncurses5 - -install_python_prereqs: - - python3-dev - - default-libmysqlclient-dev - - build-essential - -mysql_tarball: "mysql-{{ mysql_version }}-linux-glibc2.12-x86_64.tar.{{ mysql_compression_extension }}" -mysql_src: "https://dev.mysql.com/get/Downloads/MySQL-{{ mysql_major_version }}/{{ mysql_tarball }}" -mariadb_tarball: "mariadb-{{ mariadb_version }}-linux-x86_64.tar.gz" -mariadb_src: "https://downloads.mariadb.com/MariaDB/mariadb-{{ mariadb_version }}/bintar-linux-x86_64/{{ mariadb_tarball }}" diff --git a/tests/integration/targets/setup_remote_tmp_dir/handlers/main.yml b/tests/integration/targets/setup_remote_tmp_dir/handlers/main.yml index 229037c..39f3239 100644 --- a/tests/integration/targets/setup_remote_tmp_dir/handlers/main.yml +++ b/tests/integration/targets/setup_remote_tmp_dir/handlers/main.yml @@ -1,5 +1,9 @@ - name: delete temporary directory include_tasks: default-cleanup.yml + tags: + - setup_remote_tmp_dir - name: delete temporary directory (windows) include_tasks: windows-cleanup.yml + tags: + - setup_remote_tmp_dir diff --git a/tests/integration/targets/setup_remote_tmp_dir/tasks/main.yml b/tests/integration/targets/setup_remote_tmp_dir/tasks/main.yml index 93d786f..5d898ab 100644 --- a/tests/integration/targets/setup_remote_tmp_dir/tasks/main.yml +++ b/tests/integration/targets/setup_remote_tmp_dir/tasks/main.yml @@ -7,9 +7,13 @@ setup: gather_subset: distribution when: ansible_facts == {} + tags: + - setup_remote_tmp_dir - include_tasks: "{{ lookup('first_found', files)}}" vars: files: - "{{ ansible_os_family | lower }}.yml" - "default.yml" + tags: + - setup_remote_tmp_dir diff --git a/tests/integration/targets/test_mysql_db/defaults/main.yml b/tests/integration/targets/test_mysql_db/defaults/main.yml index 6448e15..30ac858 100644 --- a/tests/integration/targets/test_mysql_db/defaults/main.yml +++ b/tests/integration/targets/test_mysql_db/defaults/main.yml @@ -2,6 +2,7 @@ # defaults file for test_mysql_db mysql_user: root mysql_password: msandbox +mysql_host: '{{ gateway_addr }}' mysql_primary_port: 3307 # Database names diff --git a/tests/integration/targets/test_mysql_db/meta/main.yml b/tests/integration/targets/test_mysql_db/meta/main.yml index f1174ff..aebda43 100644 --- a/tests/integration/targets/test_mysql_db/meta/main.yml +++ b/tests/integration/targets/test_mysql_db/meta/main.yml @@ -1,2 +1,2 @@ dependencies: - - setup_mysql + - setup_controller 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 90c72b5..dce0a43 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 @@ -1,42 +1,59 @@ -- set_fact: +--- +- name: Config overrides | Set facts + set_fact: db_to_create: testdb1 - config_file: "/root/.my1.cnf" + config_file: "{{ playbook_dir }}/.my1.cnf" fake_port: 9999 fake_host: "blahblah.local" - include_dir: "/root/mycnf.d" + include_dir: "{{ playbook_dir }}/mycnf.d" -- name: Create custom config file +- name: Config overrides | Create custom config file shell: 'echo "[client]" > {{ config_file }}' -- name: Add fake port to config file +- name: Config overrides | Add fake port to config file shell: 'echo "port = {{ fake_port }}" >> {{ config_file }}' -- name: Add blank line +- name: Config overrides | Add blank line shell: 'echo "" >> {{ config_file }}' when: - - (connector.name.0 is search('pymysql') and connector_ver is version('0.9.3', '>=')) or connector.name.0 is not search('pymysql') + - > + connector_name != 'pymysql' + or ( + connector_name == 'pymysql' + and connector_version is version('0.9.3', '>=') + ) -- name: Create include_dir +- name: Config overrides | Create include_dir file: path: '{{ include_dir }}' state: directory mode: '0777' when: - - (connector.name.0 is search('pymysql') and connector_ver is version('0.9.3', '>=')) or connector.name.0 is not search('pymysql') + - > + connector_name != 'pymysql' + or ( + connector_name == 'pymysql' + and connector_version is version('0.9.3', '>=') + ) -- name: Add include_dir +- name: Config overrides | Add include_dir lineinfile: path: '{{ config_file }}' line: '!includedir {{ include_dir }}' insertafter: EOF when: - - (connector.name.0 is search('pymysql') and connector_ver is version('0.9.3', '>=')) or connector.name.0 is not search('pymysql') + - > + connector_name != 'pymysql' + or ( + connector_name == 'pymysql' + and connector_version is version('0.9.3', '>=') + ) -- name: Create database using fake port to connect to, must fail +- name: Config overrides | Create database using fake port to connect to, must fail mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: '{{ db_to_create }}' state: present @@ -46,17 +63,17 @@ ignore_errors: yes register: result -- name: Must fail because login_port default has beed overriden by wrong value from config file +- name: Config overrides | Must fail because login_port default has beed overriden by wrong value from config file assert: that: - - result is failed - - result.msg is search("unable to connect to database") + - result is failed + - result.msg is search("unable to connect to database") -- name: Create database using default port +- name: Config overrides | Create database using default port mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: '{{ db_to_create }}' state: present @@ -65,42 +82,45 @@ config_overrides_defaults: no register: result -- name: Must not fail because of the default of login_port is correct +- name: Config overrides | Must not fail because of the default of login_port is correct assert: that: - - result is changed + - result is changed -- name: Reinit custom config file +- name: Config overrides | Reinit custom config file shell: 'echo "[client]" > {{ config_file }}' -- name: Add fake host to config file +- name: Config overrides | Add fake host to config file shell: 'echo "host = {{ fake_host }}" >> {{ config_file }}' -- name: Remove database using fake login_host +- name: >- + Config overrides | Fail to Remove database using fake login_host + because its default has been overriden by wrong value from config file mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: '{{ db_to_create }}' state: absent config_file: '{{ config_file }}' - config_overrides_defaults: yes + config_overrides_defaults: true register: result - ignore_errors: yes + failed_when: + - result is succeeded + - result.msg is not search(pattern1) + - result.msg is not search(pattern2) + - result.msg is not search(pattern3) + vars: + pattern1: Can't connect to MySQL server on '{{ fake_host }}' + pattern2: Unknown MySQL server host '{{ fake_host }}' + pattern3: Unknown server host '{{ fake_host }}' -- name: Must fail because login_host default has beed overriden by wrong value from config file - assert: - that: - - result is failed - - result.msg is search("Can't connect to MySQL server on '{{ fake_host }}'") or result.msg is search("Unknown MySQL server host '{{ fake_host }}'") - -# Clean up -- name: Remove test db +- name: Config overrides | Clean up test database mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: '{{ db_to_create }}' state: absent 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 9ef3af5..02e5df2 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 @@ -1,45 +1,46 @@ --- -- set_fact: - latin1_file1: "{{tmp_dir}}/{{file}}" +- name: Encoding | Set fact + set_fact: + latin1_file1: "{{ tmp_dir }}/{{ file }}" - name: Deleting Latin1 encoded Database mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: '{{ db_latin1_name }}' state: absent -- name: create Latin1 encoded database +- name: Encoding | Create Latin1 encoded database mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: '{{ db_latin1_name }}' state: present encoding: latin1 -- name: create a table in Latin1 database +- name: Encoding | Create a table in Latin1 database command: "{{ mysql_command }} {{ db_latin1_name }} -e \"create table testlatin1(id int, name varchar(100))\"" # Inserting a string in latin1 into table, , this string be tested later, # so report any change of content in the test too -- name: inserting data into Latin1 database +- name: Encoding | Inserting data into Latin1 database command: "{{ mysql_command }} {{ db_latin1_name }} -e \"insert into testlatin1 value(47,'Amédée Bôlüt')\"" -- name: selecting table +- name: Encoding | Selecting table command: "{{ mysql_command }} {{ db_latin1_name }} -e \"select * from testlatin1\"" register: output -- name: Dumping a table in Latin1 database +- name: Encoding | Dumping a table in Latin1 database mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: "{{ db_latin1_name }}" encoding: latin1 @@ -49,30 +50,30 @@ - assert: that: - - result is changed + - result is changed -- name: state dump - file name should exist +- name: Encoding | State dump - file name should exist (latin1_file1) file: name: '{{ latin1_file1 }}' state: file -- name: od the file and check of latin1 encoded string is present +- name: od the file and check of latin1 encoded string is present shell: grep -a 47 {{ latin1_file1 }} | od -c |grep "A m 351 d 351 e B 364\|A m 303 251 d 303 251 e B 303" -- name: Dropping {{ db_latin1_name }} database +- name: Encoding | Dropping {{ db_latin1_name }} database mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: '{{ db_latin1_name }}' state: absent -- name: Importing the latin1 mysql script +- name: Encoding | Importing the latin1 mysql script mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' state: import encoding: latin1 @@ -80,20 +81,25 @@ target: "{{ latin1_file1 }}" register: result -- assert: +- name: Encoding | Assert that importing latin1 is changed + assert: that: - - result is changed + - result is changed -- name: check encoding of table - shell: "{{ mysql_command }} {{ db_latin1_name }} -e \"SHOW FULL COLUMNS FROM testlatin1\"" +- name: Encoding | Check encoding of table + ansible.builtin.command: + cmd: > + {{ mysql_command }} + {{ db_latin1_name }} + -e "SHOW FULL COLUMNS FROM {{ db_latin1_name }}.testlatin1" register: output failed_when: '"latin1_swedish_ci" not in output.stdout' -- name: remove database +- name: Encoding | Clean up database mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: '{{ db_latin1_name }}' state: absent 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 74071e2..8cad28e 100644 --- a/tests/integration/targets/test_mysql_db/tasks/issue-28.yml +++ b/tests/integration/targets/test_mysql_db/tasks/issue-28.yml @@ -9,7 +9,7 @@ mysql_parameters: &mysql_params login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' when: tls_enabled block: @@ -25,6 +25,7 @@ mysql_user: <<: *mysql_params name: '{{ user_name_1 }}' + host_all: true state: absent ignore_errors: yes @@ -32,6 +33,7 @@ mysql_user: <<: *mysql_params name: "{{ user_name_1 }}" + host: "%" password: "{{ user_password_1 }}" priv: '*.*:ALL,GRANT' tls_requires: @@ -43,7 +45,7 @@ state: absent login_user: '{{ user_name_1 }}' login_password: '{{ user_password_1 }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' ca_cert: /tmp/cert.pem register: result @@ -52,12 +54,14 @@ - assert: that: - result is failed - when: connector.name.0 is search('pymysql') + when: + - connector_name == 'pymysql' - assert: that: - result is succeeded - when: connector.name.0 is not search('pymysql') + when: + - connector_name != 'pymysql' - name: attempt connection with newly created user ignoring hostname mysql_db: @@ -65,7 +69,7 @@ state: absent login_user: '{{ user_name_1 }}' login_password: '{{ user_password_1 }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' ca_cert: /tmp/cert.pem check_hostname: no @@ -80,5 +84,5 @@ mysql_user: <<: *mysql_params name: '{{ user_name_1 }}' - host: 127.0.0.1 + host_all: true state: absent diff --git a/tests/integration/targets/test_mysql_db/tasks/issue_256_mysqldump_errors.yml b/tests/integration/targets/test_mysql_db/tasks/issue_256_mysqldump_errors.yml new file mode 100644 index 0000000..ea1768a --- /dev/null +++ b/tests/integration/targets/test_mysql_db/tasks/issue_256_mysqldump_errors.yml @@ -0,0 +1,149 @@ +--- + +# When mysqldump encountered an issue, mysql_db was still happy. But the +# dump produced was empty or worse, only contained `DROP TABLE IF EXISTS...` + +- module_defaults: + community.mysql.mysql_db: &mysql_defaults + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + community.mysql.mysql_query: *mysql_defaults + + block: + + - name: Dumps errors | Setup test | Create 2 schemas + community.mysql.mysql_db: + name: + - "db1" + - "db2" + state: present + + - name: Dumps errors | Setup test | Create 2 tables + community.mysql.mysql_query: + query: + - "CREATE TABLE db1.t1 (id int)" + - "CREATE TABLE db1.t2 (id int)" + - "CREATE VIEW db2.v1 AS SELECT id from db1.t1" + + - name: Dumps errors | Full dump without compression + community.mysql.mysql_db: + state: dump + name: all + target: /tmp/full-dump.sql + register: full_dump + + - name: Dumps errors | Full dump with gunzip + community.mysql.mysql_db: + state: dump + name: all + target: /tmp/full-dump.sql.gz + register: full_dump_gz + + - name: Dumps errors | Distinct dump without compression + community.mysql.mysql_db: + state: dump + name: db2 + target: /tmp/dump-db2.sql + register: dump_db2 + + - name: Dumps errors | Distinct dump with gunzip + community.mysql.mysql_db: + state: dump + name: db2 + target: /tmp/dump-db2.sql.gz + register: dump_db2_gz + + - name: Dumps errors | Check distinct dumps are changed + ansible.builtin.assert: + that: + - dump_db2 is changed + - dump_db2_gz is changed + + # Now db2.v1 targets an inexistant table so mysqldump will fail + - name: Dumps errors | Drop t1 + community.mysql.mysql_query: + query: + - "DROP TABLE db1.t1" + + - name: Dumps errors | Full dump after drop t1 without compression + community.mysql.mysql_db: + state: dump + name: all + target: /tmp/full-dump-without-t1.sql + pipefail: true # This should do nothing + + register: full_dump_without_t1 + ignore_errors: true + + - name: Dumps errors | Full dump after drop t1 with gzip without the fix + community.mysql.mysql_db: + state: dump + name: all + target: /tmp/full-dump-without-t1.sql.gz + register: full_dump_without_t1_gz_without_fix + ignore_errors: true + + - name: Dumps errors | Full dump after drop t1 with gzip with the fix + community.mysql.mysql_db: + state: dump + name: all + target: /tmp/full-dump-without-t1.sql.gz + pipefail: true + register: full_dump_without_t1_gz_with_fix + ignore_errors: true + + - name: Dumps errors | Check full dump + ansible.builtin.assert: + that: + - full_dump_without_t1 is failed + - full_dump_without_t1.msg is search( + 'references invalid table') + - full_dump_without_t1_gz_without_fix is changed + - full_dump_without_t1_gz_with_fix is failed + - full_dump_without_t1_gz_with_fix.msg is search( + 'references invalid table') + + - name: Dumps errors | Distinct dump after drop t1 without compression + community.mysql.mysql_db: + state: dump + name: db2 + target: /tmp/dump-db2-without_t1.sql + pipefail: true # This should do nothing + register: dump_db2_without_t1 + ignore_errors: true + + - name: Dumps errors | Distinct dump after drop t1 with gzip without the fix + community.mysql.mysql_db: + state: dump + name: db2 + target: /tmp/dump-db2-without_t1.sql.gz + register: dump_db2_without_t1_gz_without_fix + ignore_errors: true + + - name: Dumps errors | Distinct dump after drop t1 with gzip with the fix + community.mysql.mysql_db: + state: dump + name: db2 + target: /tmp/dump-db2-without_t1.sql.gz + pipefail: true + register: dump_db2_without_t1_gz_with_fix + ignore_errors: true + + - name: Dumps errors | Check distinct dump + ansible.builtin.assert: + that: + - dump_db2_without_t1 is failed + - dump_db2_without_t1.msg is search( + 'references invalid table') + - dump_db2_without_t1_gz_without_fix is changed + - dump_db2_without_t1_gz_with_fix is failed + - dump_db2_without_t1_gz_with_fix.msg is search( + 'references invalid table') + - name: Dumps errors | Cleanup + community.mysql.mysql_db: + name: + - "db1" + - "db2" + state: absent diff --git a/tests/integration/targets/test_mysql_db/tasks/main.yml b/tests/integration/targets/test_mysql_db/tasks/main.yml index 958e341..544ad4d 100644 --- a/tests/integration/targets/test_mysql_db/tasks/main.yml +++ b/tests/integration/targets/test_mysql_db/tasks/main.yml @@ -1,3 +1,4 @@ +--- #################################################################### # WARNING: These are designed specifically for Ansible tests # # and should not be used as examples of how to write Ansible roles # @@ -21,10 +22,6 @@ # 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: Check state present/absent include_tasks: state_present_absent.yml vars: @@ -63,3 +60,6 @@ vars: db_name: "{{ item }}" loop: "{{ db_names }}" + +- name: Check errors from mysqldump are seen issue 256 + ansible.builtin.include_tasks: issue_256_mysqldump_errors.yml diff --git a/tests/integration/targets/test_mysql_db/tasks/multi_db_create_delete.yml b/tests/integration/targets/test_mysql_db/tasks/multi_db_create_delete.yml index cb91d32..0bd7d58 100644 --- a/tests/integration/targets/test_mysql_db/tasks/multi_db_create_delete.yml +++ b/tests/integration/targets/test_mysql_db/tasks/multi_db_create_delete.yml @@ -18,7 +18,7 @@ mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: - '{{ db1_name }}' @@ -43,7 +43,7 @@ mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: - '{{ db1_name }}' @@ -56,7 +56,7 @@ - name: assert successful completion of create database using check_mode since databases does not exist prior assert: that: - - check_mode_result.changed == true + - check_mode_result is changed - name: run command to list databases like specified database name command: "{{ mysql_command }} \"-e show databases like 'database%'\"" @@ -75,7 +75,7 @@ mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: - '{{ db1_name }}' @@ -87,7 +87,7 @@ - name: assert successful completion of create database assert: that: - - result.changed == true + - result is changed - result.db_list == ['{{ db1_name }}', '{{ db2_name }}', '{{ db3_name }}'] - name: run command to list databases like specified database name @@ -107,7 +107,7 @@ mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: - '{{ db1_name }}' @@ -120,7 +120,7 @@ - name: assert that recreation of existing databases does not make change (since recreated using check mode) assert: that: - - check_mode_result.changed == false + - check_mode_result is not changed - name: run command to list databases like specified database name command: "{{ mysql_command }} \"-e show databases like 'database%'\"" @@ -139,7 +139,7 @@ mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: - '{{ db1_name }}' @@ -151,7 +151,7 @@ - name: assert that recreation of existing databases does not make change assert: that: - - result.changed == false + - result is not changed - name: run command to list databases like specified database name command: "{{ mysql_command }} \"-e show databases like 'database%'\"" @@ -170,7 +170,7 @@ mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: - '{{ db2_name }}' @@ -180,7 +180,7 @@ - name: assert successful completion of deleting database assert: that: - - result.changed == true + - result is changed - name: run command to list databases like specified database name command: "{{ mysql_command }} \"-e show databases like 'database%'\"" @@ -199,7 +199,7 @@ mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: - '{{ db1_name }}' @@ -212,7 +212,7 @@ - name: assert successful completion of recreation of partially existing database using check mode assert: that: - - check_mode_result.changed == true + - check_mode_result is changed - name: run command to list databases like specified database name command: "{{ mysql_command }} \"-e show databases like 'database%'\"" @@ -231,7 +231,7 @@ mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: - '{{ db1_name }}' @@ -243,7 +243,7 @@ - name: assert successful completion of create database assert: that: - - result.changed == true + - result is changed - name: run command to list databases like specified database name command: "{{ mysql_command }} \"-e show databases like 'database%'\"" @@ -271,7 +271,7 @@ mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: - '{{ db1_name }}' @@ -284,7 +284,7 @@ - name: assert successful completion of dump operation using check mode assert: that: - - check_mode_dump_result.changed == true + - check_mode_dump_result is changed - name: run command to list databases like specified database name command: "{{ mysql_command }} \"-e show databases like 'database%'\"" @@ -308,7 +308,7 @@ mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: - "{{ db1_name }}" @@ -348,7 +348,7 @@ mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: - "{{ db4_name }}" @@ -384,11 +384,12 @@ # ========================================================================== # Dump existing databases + - name: Dump existing databases mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: - '{{ db1_name }}' @@ -398,13 +399,13 @@ target: '{{ dump1_file }}' register: dump_result -- name: assert successful completion of dump operation +- name: Assert successful completion of dump operation (existing database) assert: that: - - dump_result.changed == true + - dump_result is changed - dump_result.db_list == ['{{ db1_name }}', '{{ db2_name }}', '{{ db3_name }}'] -- name: run command to list databases like specified database name +- name: Run command to list databases like specified database name command: "{{ mysql_command }} \"-e show databases like 'database%'\"" register: mysql_result @@ -415,7 +416,7 @@ - "'{{ db2_name }}' in mysql_result.stdout" - "'{{ db3_name }}' in mysql_result.stdout" -- name: state dump - file name should exist +- name: State dump - file name should exist (dump1_file) file: name: '{{ dump1_file }}' state: file @@ -441,7 +442,7 @@ mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: all state: dump @@ -451,7 +452,7 @@ - name: assert successful completion of dump operation assert: that: - - dump_result.changed == true + - dump_result is changed - name: run command to list databases like specified database name command: "{{ mysql_command }} \"-e show databases like 'database%'\"" @@ -466,7 +467,7 @@ - "'{{ db4_name }}' not in mysql_result.stdout" - "'{{ db5_name }}' not in mysql_result.stdout" -- name: state dump - file name should exist +- name: state dump - file name should exist (dump2_file) file: name: '{{ dump2_file }}' state: file @@ -479,7 +480,7 @@ mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: - '{{ db2_name }}' @@ -491,7 +492,7 @@ - name: assert successful completion of delete databases which already exists using check mode assert: that: - - check_mode_result.changed == true + - check_mode_result is changed - name: run command to test state=absent for a database name command: "{{ mysql_command }} \"-e show databases like 'database%'\"" @@ -509,7 +510,7 @@ mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: - '{{ db2_name }}' @@ -520,7 +521,7 @@ - name: assert successful completion of deleting database assert: that: - - result.changed == true + - result is changed - result.db_list == ['{{ db2_name }}', '{{ db3_name }}'] - name: run command to list databases like specified database name @@ -539,7 +540,7 @@ mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: - '{{ db2_name }}' @@ -551,7 +552,7 @@ - name: assert that deletion of non existing databases does not make change (using check mode) assert: that: - - check_mode_result.changed == false + - check_mode_result is not changed - name: run command to test state=absent for a database name command: "{{ mysql_command }} \"-e show databases like 'database%'\"" @@ -569,7 +570,7 @@ mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: - '{{ db2_name }}' @@ -580,7 +581,7 @@ - name: assert that deletion of non existing databases does not make change assert: that: - - result.changed == false + - result is not changed - name: run command to list databases like specified database name command: "{{ mysql_command }} \"-e show databases like 'database%'\"" @@ -598,7 +599,7 @@ mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: - '{{ db1_name }}' @@ -612,7 +613,7 @@ - name: assert successful completion of deleting database assert: that: - - result.changed == true + - result is changed - name: run command to list databases like specified database name command: "{{ mysql_command }} \"-e show databases like 'database%'\"" diff --git a/tests/integration/targets/test_mysql_db/tasks/state_dump_import.yml b/tests/integration/targets/test_mysql_db/tasks/state_dump_import.yml index 1de7439..f8d2b4b 100644 --- a/tests/integration/targets/test_mysql_db/tasks/state_dump_import.yml +++ b/tests/integration/targets/test_mysql_db/tasks/state_dump_import.yml @@ -17,113 +17,129 @@ # along with Ansible. If not, see . # ============================================================ -- set_fact: +- name: Dump and Import | Set facts + set_fact: db_file_name: "{{ tmp_dir }}/{{ file }}" wrong_sql_file: "{{ tmp_dir }}/wrong.sql" dump_file1: "{{ tmp_dir }}/{{ file2 }}" dump_file2: "{{ tmp_dir }}/{{ file3 }}" db_user: "test" db_user_unsafe_password: "pass!word" - config_file: "/root/.my.cnf" + config_file: "{{ playbook_dir }}/root/.my.cnf" -- name: create custom config file +- name: Dump and Import | Create custom config file shell: 'echo "[client]" > {{ config_file }}' -- name: create user for test unsafe_login_password parameter +- name: Dump and Import | Create user for test unsafe_login_password parameter mysql_user: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: '{{ db_user }}' + host: '%' password: '{{ db_user_unsafe_password }}' priv: '*.*:ALL' state: present -- name: state dump/import - create database +- name: Dump and Import | State dump/import - create database mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: '{{ db_name }}' state: present check_implicit_admin: yes -- name: create database +- name: Dump and Import | Create database mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: '{{ db_name2 }}' state: present check_implicit_admin: no -- name: state dump/import - create table department +- name: Dump and Import | State dump/import - create table department command: "{{ mysql_command }} {{ db_name }} \"-e create table department(id int, name varchar(100))\"" -- name: state dump/import - create table employee +- name: Dump and Import | State dump/import - create table employee command: "{{ mysql_command }} {{ db_name }} \"-e create table employee(id int, name varchar(100))\"" -- name: state dump/import - insert data into table employee +- name: Dump and Import | State dump/import - insert data into table employee command: "{{ mysql_command }} {{ db_name }} \"-e insert into employee value(47,'Joe Smith')\"" -- name: state dump/import - insert data into table department +- name: Dump and Import | State dump/import - insert data into table department command: "{{ mysql_command }} {{ db_name }} \"-e insert into department value(2,'Engineering')\"" -- name: state dump/import - file name should not exist +- name: Dump and Import | State dump/import - file name should not exist file: name: '{{ db_file_name }}' state: absent -- name: database dump file1 should not exist +- name: Dump and Import | Database dump file1 should not exist file: name: '{{ dump_file1 }}' state: absent -- name: database dump file2 should not exist +- name: Dump and Import | Database dump file2 should not exist file: name: '{{ dump_file2 }}' state: absent -- name: state dump without department table. +- name: Dump and Import | State dump without department table. mysql_db: login_user: '{{ db_user }}' login_password: '{{ db_user_unsafe_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' unsafe_login_password: yes name: '{{ db_name }}' state: dump target: '{{ db_file_name }}' ignore_tables: - - "{{ db_name }}.department" + - "{{ db_name }}.department" force: yes master_data: 1 skip_lock_tables: yes - dump_extra_args: --skip-triggers + dump_extra_args: >- + --skip-triggers config_file: '{{ config_file }}' restrict_config_file: yes check_implicit_admin: no register: result -- name: assert successful completion of dump operation +- name: Dump and Import | Assert successful completion of dump operation for MariaDB and MySQL < 8.2 assert: that: - result is changed - - result.executed_commands[0] is search("mysqldump --defaults-file={{ config_file }} --user={{ db_user }} --password=\*\*\*\*\*\*\*\* --force --host=127.0.0.1 --port={{ mysql_primary_port }} {{ db_name }} --skip-lock-tables --quick --ignore-table={{ db_name }}.department --master-data=1 --skip-triggers") + - result.executed_commands[0] is search(".department --master-data=1 --skip-triggers") + when: + - > + db_engine == 'mariadb' or + (db_engine == 'mysql' and db_version is version('8.2', '<')) -- name: state dump/import - file name should exist +- name: Dump and Import | Assert successful completion of dump operation for MySQL >= 8.2 + assert: + that: + - result is changed + - result.executed_commands[0] is search(".department --source-data=1 --skip-triggers") + when: + - db_engine == 'mysql' + - db_version is version('8.2', '>=') + +- name: Dump and Import | State dump/import - file name should exist (db_file_name) file: name: '{{ db_file_name }}' state: file -- name: state dump with multiple databases in comma separated form. +- name: Dump and Import | State dump with multiple databases in comma separated form for MySQL. mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: "{{ db_name }},{{ db_name2 }}" state: dump @@ -131,22 +147,22 @@ check_implicit_admin: yes register: dump_result1 -- name: assert successful completion of dump operation (with multiple databases in comma separated form) +- name: Dump and Import | Assert successful completion of dump operation (with multiple databases in comma separated form) assert: that: - dump_result1 is changed - dump_result1.executed_commands[0] is search(" --user=root --password=\*\*\*\*\*\*\*\*") -- name: state dump - dump file1 should exist +- name: Dump and Import | State dump - dump file1 should exist file: name: '{{ dump_file1 }}' state: file -- name: state dump with multiple databases in list form via check_mode +- name: Dump and Import | State dump with multiple databases in list form via check_mode mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: - '{{ db_name }}' @@ -156,26 +172,26 @@ register: dump_result check_mode: yes -- name: assert successful completion of dump operation (with multiple databases in list form) via check mode +- name: Dump and Import | Assert successful completion of dump operation (with multiple databases in list form) via check mode assert: that: - - "dump_result.changed == true" + - dump_result is changed -- name: database dump file2 should not exist +- name: Dump and Import | Database dump file2 should not exist stat: path: '{{ dump_file2 }}' register: stat_result -- name: assert that check_mode does not create dump file for databases +- name: Dump and Import | Assert that check_mode does not create dump file for databases assert: that: - stat_result.stat.exists is defined and not stat_result.stat.exists -- name: state dump with multiple databases in list form. +- name: Dump and Import | State dump with multiple databases in list form. mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: - '{{ db_name }}' @@ -184,39 +200,39 @@ target: '{{ dump_file2 }}' register: dump_result2 -- name: assert successful completion of dump operation (with multiple databases in list form) +- name: Dump and Import | Assert successful completion of dump operation (with multiple databases in list form) assert: that: - - "dump_result2.changed == true" + - dump_result2 is changed -- name: state dump - dump file2 should exist +- name: Dump and Import | State dump - dump file2 should exist file: name: '{{ dump_file2 }}' state: file -- name: state dump/import - remove database +- name: Dump and Import | State dump/import - remove database mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: '{{ db_name }}' state: absent -- name: remove database +- name: Dump and Import | Remove database mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: '{{ db_name2 }}' state: absent -- name: test state=import to restore the database of type {{ format_type }} (expect changed=true) +- name: Dump and Import | Test state=import to restore the database of type {{ format_type }} (expect changed=true) mysql_db: login_user: '{{ db_user }}' login_password: '{{ db_user_unsafe_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' unsafe_login_password: yes name: '{{ db_name }}' @@ -225,20 +241,20 @@ use_shell: yes register: result -- name: show the tables +- name: Dump and Import | Show the tables command: "{{ mysql_command }} {{ db_name }} \"-e show tables\"" register: result -- name: assert that the department table is absent. +- name: Dump and Import | Assert that the department table is absent. assert: that: - "'department' not in result.stdout" -- name: test state=import to restore a database from multiple database dumped file1 +- name: Dump and Import | Test state=import to restore a database from multiple database dumped file1 mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: '{{ db_name2 }}' state: import @@ -246,34 +262,34 @@ use_shell: no register: import_result -- name: assert output message restored a database from dump file1 +- name: Dump and Import | Assert output message restored a database from dump file1 assert: that: - - "import_result.changed == true" + - import_result is changed -- name: remove database +- name: Dump and Import | Remove database mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: '{{ db_name2 }}' state: absent -- name: run command to list databases +- name: Dump and Import | Run command to list databases command: "{{ mysql_command }} \"-e show databases like 'data%'\"" register: mysql_result -- name: assert that db_name2 database does not exist +- name: Dump and Import | Assert that db_name2 database does not exist assert: that: - "'{{ db_name2 }}' not in mysql_result.stdout" -- name: test state=import to restore a database from dumped file2 (check mode) +- name: Dump and Import | Test state=import to restore a database from dumped file2 (check mode) mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: '{{ db_name2 }}' state: import @@ -281,96 +297,96 @@ register: check_import_result check_mode: yes -- name: assert output message restored a database from dump file2 (check mode) +- name: Dump and Import | Assert output message restored a database from dump file2 (check mode) assert: that: - - "check_import_result.changed == true" + - check_import_result is changed -- name: run command to list databases +- name: Dump and Import | Run command to list databases command: "{{ mysql_command }} \"-e show databases like 'data%'\"" register: mysql_result -- name: assert that db_name2 database does not exist (check mode) +- name: Dump and Import | Assert that db_name2 database does not exist (check mode) assert: that: - "'{{ db_name2 }}' not in mysql_result.stdout" -- name: test state=import to restore a database from multiple database dumped file2 +- name: Dump and Import | Test state=import to restore a database from multiple database dumped file2 mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: '{{ db_name2 }}' state: import target: '{{ dump_file2 }}' register: import_result2 -- name: assert output message restored a database from dump file2 +- name: Dump and Import | Assert output message restored a database from dump file2 assert: that: - - import_result2.changed == true + - import_result2 is changed - import_result2.db_list == ['{{ db_name2 }}'] -- name: run command to list databases +- name: Dump and Import | Run command to list databases command: "{{ mysql_command }} \"-e show databases like 'data%'\"" register: mysql_result -- name: assert that db_name2 database does exist after import +- name: Dump and Import | Assert that db_name2 database does exist after import assert: that: - "'{{ db_name2 }}' in mysql_result.stdout" -- name: test state=dump to backup the database of type {{ format_type }} (expect changed=true) +- name: Dump and Import | Test state=dump to backup the database of type {{ format_type }} (expect changed=true) mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: '{{ db_name }}' state: dump target: '{{ db_file_name }}' register: result -- name: assert output message backup the database +- name: Dump and Import | Assert output message backup the database assert: that: - - "result.changed == true" - - "result.db =='{{ db_name }}'" + - result is changed + - result.db == db_name -# - name: assert database was backed up successfully +# - name: Dump and Import | Assert database was backed up successfully # command: "file {{ db_file_name }}" # register: result # -# - name: assert file format type +# - name: Dump and Import | Assert file format type # assert: # that: # - "'{{ format_msg_type }}' in result.stdout" -- name: update database table employee +- name: Dump and Import | Update database table employee command: "{{ mysql_command }} {{ db_name }} \"-e update employee set name='John Doe' where id=47\"" -- name: test state=import to restore the database of type {{ format_type }} (expect changed=true) +- name: Dump and Import | Test state=import to restore the database of type {{ format_type }} (expect changed=true) mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: '{{ db_name }}' state: import target: '{{ db_file_name }}' register: result -- name: assert output message restore the database +- name: Dump and Import | Assert output message restore the database assert: that: - - "result.changed == true" + - result is changed -- name: select data from table employee +- name: Dump and Import | Select data from table employee command: "{{ mysql_command }} {{ db_name }} \"-e select * from employee\"" register: result -- name: assert data in database is from the restore database +- name: Dump and Import | Assert data in database is from the restore database assert: that: - "'47' in result.stdout" @@ -380,14 +396,14 @@ # Test ``force`` parameter ########################## -- name: create wrong sql file +- name: Dump and Import | Create wrong sql file shell: echo 'CREATE TABLE hello (id int); CREATE ELBAT ehlo (int id);' >> '{{ wrong_sql_file }}' -- name: try to import without force parameter, must fail +- name: Dump and Import | Try to import without force parameter, must fail mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: '{{ db_name }}' state: import @@ -398,13 +414,13 @@ - assert: that: - - result.failed == true + - result is failed -- name: try to import with force parameter +- name: Dump and Import | Try to import with force parameter mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: '{{ db_name }}' state: import @@ -416,44 +432,73 @@ that: - result is changed +######################## +# Test import with chdir + +- name: Dump and Import | Create dir + file: + path: ~/subdir + state: directory + +- name: Dump and Import | Create test dump + shell: 'echo "SOURCE ./subdir_test.sql" > ~/original_test.sql' + +- name: Dump and Import | Create test source + shell: 'echo "SELECT 1" > ~/subdir/subdir_test.sql' + +- name: Dump and Import | Try to restore without chdir argument, must fail + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_name }}' + state: import + target: '~/original_test.sql' + ignore_errors: yes + register: result +- assert: + that: + - result is failed + - result.msg is search('Failed to open file') + +- name: Dump and Import | Restore with chdir argument, must pass + mysql_db: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + name: '{{ db_name }}' + state: import + target: '~/original_test.sql' + chdir: ~/subdir + register: result +- assert: + that: + - result is succeeded + ########## # Clean up ########## -- name: remove database name +- name: Dump and Import | Clean up databases mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' - name: '{{ db_name }}' + name: '{{ item }}' state: absent + loop: + - '{{ db_name }}' + - '{{ db_name2 }}' -- 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: '{{ db_name2 }}' - state: absent - -- name: remove file name +- name: Dump and Import | Clean up files file: - name: '{{ db_file_name }}' - state: absent - -- name: remove file name - file: - name: '{{ wrong_sql_file }}' - state: absent - -- name: remove dump file1 - file: - name: '{{ dump_file1 }}' - state: absent - -- name: remove dump file2 - file: - name: '{{ dump_file2 }}' + name: '{{ item }}' state: absent + loop: + - '{{ db_file_name }}' + - '{{ wrong_sql_file }}' + - '{{ dump_file1 }}' + - '{{ dump_file2 }}' 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 index 02411f0..12633f2 100644 --- a/tests/integration/targets/test_mysql_db/tasks/state_present_absent.yml +++ b/tests/integration/targets/test_mysql_db/tasks/state_present_absent.yml @@ -1,3 +1,4 @@ +--- # test code for mysql_db module with database name containing special chars # This file is part of Ansible @@ -16,75 +17,75 @@ # along with Ansible. If not, see . # ============================================================ -- name: remove database if it exists +- name: State Present Absent | Remove database if it exists command: > - "{{ mysql_command }} -sse 'drop database {{ db_name }}'" - ignore_errors: True + "{{ mysql_command }} -sse 'DROP DATABASE IF EXISTS {{ db_name }}'" + ignore_errors: true -- name: make sure the test database is not there +- name: State Present Absent | 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) +- name: State Present Absent | 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_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: '{{ db_name }}' state: present register: result -- name: assert output message that database exist +- name: State Present Absent | 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) +- name: State Present Absent | 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 +- name: State Present Absent | Assert database exist assert: that: - "'{{ db_name }}' in result.stdout" # ============================================================ -- name: test state=absent for a database name (expect changed=true) +- name: State Present Absent | 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_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: '{{ db_name }}' state: absent register: result -- name: assert output message that database does not exist +- name: State Present Absent | 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) +- name: State Present Absent | 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 +- name: State Present Absent | Assert database does not exist assert: that: - "'{{ db_name }}' not in result.stdout" # ============================================================ -- name: test mysql_db encoding param not valid - issue 8075 +- name: State Present Absent | 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_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: datanotvalid state: present @@ -92,209 +93,210 @@ register: result ignore_errors: true -- name: assert test mysql_db encoding param not valid - issue 8075 (failed=true) +- name: State Present Absent | Assert test mysql_db encoding param not valid - issue 8075 (failed=true) assert: that: - - "result.failed == true" + - result is failed - "'Traceback' not in result.msg" - "'Unknown character set' in result.msg" # ============================================================ -- name: test mysql_db using a valid encoding utf8 (expect changed=true) +- name: State Present Absent | 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_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: 'en{{ db_name }}' state: present encoding: utf8 register: result -- name: assert output message created a database +- name: State Present Absent | 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 +- name: State Present Absent | Test database was created command: "{{ mysql_command }} -e \"SHOW CREATE DATABASE `en{{ db_name }}`\"" register: result -- name: assert created database is of encoding utf8 +- name: State Present Absent | Assert created database is of encoding utf8 assert: that: - "'utf8' in result.stdout" -- name: remove database +- name: State Present Absent | Remove database mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: 'en{{ db_name }}' state: absent # ============================================================ -- name: test mysql_db using valid encoding binary (expect changed=true) +- name: State Present Absent | 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_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: 'en{{ db_name }}' state: present encoding: binary register: result -- name: assert output message that database was created +- name: State Present Absent | 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 +- name: State Present Absent | 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 +- name: State Present Absent | Assert created database is of encoding binary assert: that: - "'binary' in result.stdout" -- name: remove database +- name: State Present Absent | Remove database mysql_db: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: 'en{{ db_name }}' state: absent # ============================================================ -- name: create user1 to access database dbuser1 +- name: State Present Absent | Create user1 to access database dbuser1 mysql_user: login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: user1 + host: '%' password: 'Hfd6fds^dfA8Ga' priv: '*.*:ALL' state: present -- name: create database dbuser1 using user1 +- name: State Present Absent | Create database dbuser1 using user1 mysql_db: login_user: user1 login_password: 'Hfd6fds^dfA8Ga' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: '{{ db_user1 }}' state: present register: result -- name: assert output message that database was created +- name: State Present Absent | Assert output message that database was created assert: that: - - "result.changed == true" + - result is changed -- name: run command to test database was created using user1 +- name: State Present Absent | 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 +- name: State Present Absent | Assert database exist assert: that: - "'{{ db_user1 }}' in result.stdout" # ============================================================ -- name: create user2 to access database with privilege select only +- name: State Present Absent | 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_host: '{{ mysql_host }}' 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) +- name: State Present Absent | 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_host: '{{ mysql_host }}' 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 +- name: State Present Absent | Assert output message that database was not created using dbuser2 assert: that: - - "result.failed == true" + - result is failed - "'Access denied' in result.msg" -- name: run command to test that database was not created +- name: State Present Absent | 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 +- name: State Present Absent | 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) +- name: State Present Absent | 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_host: '{{ mysql_host }}' 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 +- name: State Present Absent | Assert output message that database was not deleted using dbuser2 assert: that: - - "result.failed == true" + - result is failed - "'Access denied' in result.msg" -- name: run command to test database was not deleted +- name: State Present Absent | 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 +- name: State Present Absent | 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) +- name: State Present Absent | 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_host: '{{ mysql_host }}' 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 +- name: State Present Absent | 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 +- name: State Present Absent | 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 +- name: State Present Absent | Assert database does not exist assert: that: - "'{{ db_user1 }}' not in result.stdout" diff --git a/tests/integration/targets/test_mysql_info/defaults/main.yml b/tests/integration/targets/test_mysql_info/defaults/main.yml index e1b932c..e1cd880 100644 --- a/tests/integration/targets/test_mysql_info/defaults/main.yml +++ b/tests/integration/targets/test_mysql_info/defaults/main.yml @@ -2,7 +2,7 @@ # defaults file for test_mysql_info mysql_user: root mysql_password: msandbox -mysql_host: 127.0.0.1 +mysql_host: '{{ gateway_addr }}' mysql_primary_port: 3307 db_name: data diff --git a/tests/integration/targets/test_mysql_info/files/users_info_create_procedure.sql b/tests/integration/targets/test_mysql_info/files/users_info_create_procedure.sql new file mode 100644 index 0000000..5a358f0 --- /dev/null +++ b/tests/integration/targets/test_mysql_info/files/users_info_create_procedure.sql @@ -0,0 +1,7 @@ +DELIMITER // +DROP PROCEDURE IF EXISTS users_info_db.get_all_items; +CREATE PROCEDURE users_info_db.get_all_items() +BEGIN +SELECT * from users_info_db.t1; +END // +DELIMITER ; diff --git a/tests/integration/targets/test_mysql_info/meta/main.yml b/tests/integration/targets/test_mysql_info/meta/main.yml index a7ace5d..4be5f58 100644 --- a/tests/integration/targets/test_mysql_info/meta/main.yml +++ b/tests/integration/targets/test_mysql_info/meta/main.yml @@ -1,3 +1,4 @@ +--- dependencies: - - setup_mysql + - setup_controller - setup_remote_tmp_dir diff --git a/tests/integration/targets/test_mysql_info/tasks/connector_info.yml b/tests/integration/targets/test_mysql_info/tasks/connector_info.yml new file mode 100644 index 0000000..d525e8e --- /dev/null +++ b/tests/integration/targets/test_mysql_info/tasks/connector_info.yml @@ -0,0 +1,30 @@ +--- +# Added in 3.6.0 in +# https://github.com/ansible-collections/community.mysql/pull/497 + +- name: Connector info | Assert connector_name exists and has expected values + ansible.builtin.assert: + that: + - result.connector_name is defined + - result.connector_name is in ['pymysql', 'MySQLdb'] + success_msg: >- + Assertions passed, result.connector_name is {{ result.connector_name }} + fail_msg: >- + Assertion failed, result.connector_name is + {{ result.connector_name | d('Unknown')}} which is different than expected + pymysql or MySQLdb + +- name: Connector info | Assert connector_version exists and has expected values + ansible.builtin.assert: + that: + - result.connector_version is defined + - > + result.connector_version == 'Unknown' + or result.connector_version is version(connector_version, '==') + success_msg: >- + Assertions passed, result.connector_version is + {{ result.connector_version }} + fail_msg: >- + Assertion failed, result.connector_version is + {{ result.connector_version }} which is different than expected + {{ connector_version }} diff --git a/tests/integration/targets/test_mysql_info/tasks/filter_databases.yml b/tests/integration/targets/test_mysql_info/tasks/filter_databases.yml new file mode 100644 index 0000000..da1058b --- /dev/null +++ b/tests/integration/targets/test_mysql_info/tasks/filter_databases.yml @@ -0,0 +1,161 @@ +--- + +- module_defaults: + community.mysql.mysql_db: &mysql_defaults + login_user: "{{ mysql_user }}" + login_password: "{{ mysql_password }}" + login_host: "{{ mysql_host }}" + login_port: "{{ mysql_primary_port }}" + community.mysql.mysql_query: *mysql_defaults + community.mysql.mysql_info: *mysql_defaults + community.mysql.mysql_user: *mysql_defaults + + block: + + # ================================ Prepare ============================== + - name: Mysql_info databases | Prepare | Create databases + community.mysql.mysql_db: + name: + - db_tables_count_empty + - db_tables_count_1 + - db_tables_count_2 + - db_only_views # https://github.com/ansible-Getions/community.mysql/issues/204 + state: present + + - name: Mysql_info databases | Prepare | Create tables + community.mysql.mysql_query: + query: + - >- + CREATE TABLE IF NOT EXISTS db_tables_count_1.t1 + (id int, name varchar(9)) + - >- + CREATE TABLE IF NOT EXISTS db_tables_count_2.t1 + (id int, name1 varchar(9)) + - >- + CREATE TABLE IF NOT EXISTS db_tables_count_2.t2 + (id int, name1 varchar(9)) + - >- + CREATE VIEW db_only_views.v_today (today) AS SELECT CURRENT_DATE + + # ================================== Tests ============================== + + - name: Mysql_info databases | Get all non-empty databases fields + community.mysql.mysql_info: + filter: + - databases + register: result + failed_when: + - > + result.databases['db_tables_count_1'].size != 16384 or + result.databases['db_tables_count_1'].tables != 1 or + result.databases['db_tables_count_2'].size != 32768 or + result.databases['db_tables_count_2'].tables != 2 or + result.databases['db_only_views'].size != 0 or + result.databases['db_only_views'].tables != 1 or + 'db_tables_count_empty' in result.databases | dict2items + | map(attribute='key') + + - name: Mysql_info databases | Get all dbs fields except db_size + community.mysql.mysql_info: + filter: + - databases + exclude_fields: + - db_size + register: result + failed_when: + - > + result.databases['db_tables_count_1'].size is defined or + result.databases['db_tables_count_1'].tables != 1 or + result.databases['db_tables_count_2'].size is defined or + result.databases['db_tables_count_2'].tables != 2 or + result.databases['db_only_views'].size is defined or + result.databases['db_only_views'].tables != 1 or + 'db_tables_count_empty' in result.databases | dict2items + | map(attribute='key') + + # 'unsupported' element is passed to check that an unsupported value + # won't break anything (will be ignored regarding to the module's + # documentation). + - name: Mysql_info databases | Get all dbs fields with unsupported value + community.mysql.mysql_info: + filter: + - databases + exclude_fields: + - db_size + - unsupported + register: result + failed_when: + - > + result.databases['db_tables_count_1'].size is defined or + result.databases['db_tables_count_1'].tables != 1 or + result.databases['db_tables_count_2'].size is defined or + result.databases['db_tables_count_2'].tables != 2 or + result.databases['db_only_views'].size is defined or + result.databases['db_only_views'].tables != 1 or + 'db_tables_count_empty' in result.databases | dict2items + | map(attribute='key') + + - name: Mysql_info databases | Get all dbs fields except tables + community.mysql.mysql_info: + filter: + - databases + exclude_fields: + - db_table_count + register: result + failed_when: + - > + result.databases['db_tables_count_1'].size != 16384 or + result.databases['db_tables_count_1'].tables is defined or + result.databases['db_tables_count_2'].size != 32768 or + result.databases['db_tables_count_2'].tables is defined or + result.databases['db_only_views'].size != 0 or + result.databases['db_only_views'].tables is defined or + 'db_tables_count_empty' in result.databases | dict2items + | map(attribute='key') + + - name: Mysql_info databases | Get all dbs even empty ones + community.mysql.mysql_info: + filter: + - databases + return_empty_dbs: true + register: result + failed_when: + - > + result.databases['db_tables_count_1'].size != 16384 or + result.databases['db_tables_count_1'].tables != 1 or + result.databases['db_tables_count_2'].size != 32768 or + result.databases['db_tables_count_2'].tables != 2 or + result.databases['db_only_views'].size != 0 or + result.databases['db_only_views'].tables != 1 or + result.databases['db_tables_count_empty'].size != 0 or + result.databases['db_tables_count_empty'].tables != 0 + + - name: Mysql_info databases | Get all dbs even empty ones without size + community.mysql.mysql_info: + filter: + - databases + exclude_fields: + - db_size + return_empty_dbs: true + register: result + failed_when: + - > + result.databases['db_tables_count_1'].size is defined or + result.databases['db_tables_count_1'].tables != 1 or + result.databases['db_tables_count_2'].size is defined or + result.databases['db_tables_count_2'].tables != 2 or + result.databases['db_only_views'].size is defined or + result.databases['db_only_views'].tables != 1 or + result.databases['db_tables_count_empty'].size is defined or + result.databases['db_tables_count_empty'].tables != 0 + + # ================================== Cleanup ============================ + + - name: Mysql_info databases | Cleanup databases + community.mysql.mysql_db: + name: + - db_tables_count_empty + - db_tables_count_1 + - db_tables_count_2 + - db_only_views + state: absent diff --git a/tests/integration/targets/test_mysql_info/tasks/filter_users_info.yml b/tests/integration/targets/test_mysql_info/tasks/filter_users_info.yml new file mode 100644 index 0000000..558d309 --- /dev/null +++ b/tests/integration/targets/test_mysql_info/tasks/filter_users_info.yml @@ -0,0 +1,321 @@ +--- + +- module_defaults: + community.mysql.mysql_db: &mysql_defaults + login_user: "{{ mysql_user }}" + login_password: "{{ mysql_password }}" + login_host: "{{ mysql_host }}" + login_port: "{{ mysql_primary_port }}" + community.mysql.mysql_query: *mysql_defaults + community.mysql.mysql_info: *mysql_defaults + community.mysql.mysql_user: *mysql_defaults + + block: + + # ================================ Prepare ============================== + - name: Mysql_info users_info | Create databases + community.mysql.mysql_db: + name: + - users_info_db + - users_info_db2 + - users_info_db3 + state: present + + - name: Mysql_info users_info | Create tables + community.mysql.mysql_query: + query: + - >- + CREATE TABLE IF NOT EXISTS users_info_db.t1 + (id int, name varchar(9)) + - >- + CREATE TABLE IF NOT EXISTS users_info_db.T_UPPER + (id int, name1 varchar(9), NAME2 varchar(9), Name3 varchar(9)) + + # I failed to create a procedure using community.mysql.mysql_query. + # Maybe it's because we must changed the delimiter. + - name: Mysql_info users_info | Create procedure SQL file + ansible.builtin.template: + src: files/users_info_create_procedure.sql + dest: /root/create_procedure.sql + owner: root + group: root + mode: '0700' + + - name: Mysql_info users_info | Create a procedure + community.mysql.mysql_db: + name: all + state: import + target: /root/create_procedure.sql + + # Use a query instead of mysql_user, because we want to catch differences + # at the end and a bug in mysql_user would be invisible to this tests + - name: Mysql_info users_info | Prepare common tests users + community.mysql.mysql_query: + query: + - >- + CREATE USER users_info_adm@'users_info.com' IDENTIFIED WITH + mysql_native_password AS '*6C387FC3893DBA1E3BA155E74754DA6682D04747' + - > + GRANT ALL ON *.* to users_info_adm@'users_info.com' WITH GRANT + OPTION + + - >- + CREATE USER users_info_schema@'users_info.com' IDENTIFIED WITH + mysql_native_password AS '*6C387FC3893DBA1E3BA155E74754DA6682D04747' + - >- + GRANT SELECT, INSERT, UPDATE, DELETE ON users_info_db.* TO + users_info_schema@'users_info.com' + + - >- + CREATE USER users_info_table@'users_info.com' IDENTIFIED WITH + mysql_native_password AS '*6C387FC3893DBA1E3BA155E74754DA6682D04747' + - >- + GRANT SELECT, INSERT, UPDATE ON users_info_db.t1 TO + users_info_table@'users_info.com' + + - >- + CREATE USER users_info_col@'users_info.com' IDENTIFIED WITH + mysql_native_password AS '*6C387FC3893DBA1E3BA155E74754DA6682D04747' + WITH MAX_USER_CONNECTIONS 100 + - >- + GRANT SELECT (id) ON users_info_db.t1 TO + users_info_col@'users_info.com' + + - >- + CREATE USER users_info_proc@'users_info.com' IDENTIFIED WITH + mysql_native_password AS '*6C387FC3893DBA1E3BA155E74754DA6682D04747' + WITH MAX_USER_CONNECTIONS 2 MAX_CONNECTIONS_PER_HOUR 60 + - >- + GRANT EXECUTE ON PROCEDURE users_info_db.get_all_items TO + users_info_proc@'users_info.com' + + - >- + CREATE USER users_info_multi@'users_info.com' IDENTIFIED WITH + mysql_native_password AS '*6C387FC3893DBA1E3BA155E74754DA6682D04747' + - >- + GRANT SELECT ON mysql.* TO + users_info_multi@'users_info.com' + - >- + GRANT ALL ON users_info_db.* TO + users_info_multi@'users_info.com' + - >- + GRANT ALL ON users_info_db2.* TO + users_info_multi@'users_info.com' + - >- + GRANT ALL ON users_info_db3.* TO + users_info_multi@'users_info.com' + + - >- + CREATE USER users_info_usage_only@'users_info.com' IDENTIFIED WITH + mysql_native_password AS '*6C387FC3893DBA1E3BA155E74754DA6682D04747' + - >- + GRANT USAGE ON *.* TO + users_info_usage_only@'users_info.com' + + - >- + CREATE USER users_info_columns_uppercase@'users_info.com' + IDENTIFIED WITH mysql_native_password AS + '*6C387FC3893DBA1E3BA155E74754DA6682D04747' + - >- + GRANT SELECT,UPDATE(name1,NAME2,Name3) ON users_info_db.T_UPPER TO + users_info_columns_uppercase@'users_info.com' + + - >- + CREATE USER users_info_multi_hosts@'%' + IDENTIFIED WITH mysql_native_password AS + '*6C387FC3893DBA1E3BA155E74754DA6682D04747' + - GRANT SELECT ON users_info_db.* TO users_info_multi_hosts@'%' + + - >- + CREATE USER users_info_multi_hosts@'localhost' + IDENTIFIED WITH mysql_native_password AS + '*6C387FC3893DBA1E3BA155E74754DA6682D04747' + - >- + GRANT SELECT ON users_info_db.* TO + users_info_multi_hosts@'localhost' + + - >- + CREATE USER users_info_multi_hosts@'host1' + IDENTIFIED WITH mysql_native_password AS + '*6C387FC3893DBA1E3BA155E74754DA6682D04747' + - GRANT SELECT ON users_info_db.* TO users_info_multi_hosts@'host1' + + # Different password than the others users_info_multi_hosts + - >- + CREATE USER users_info_multi_hosts@'host2' + IDENTIFIED WITH mysql_native_password AS + '*CB3326D5279DE7915FE5D743232165EE887883CA' + - GRANT SELECT ON users_info_db.* TO users_info_multi_hosts@'host2' + + - >- + CREATE USER users_info_tls_none@'host' + IDENTIFIED WITH mysql_native_password AS + '*CB3326D5279DE7915FE5D743232165EE887883CA' REQUIRE NONE + - GRANT SELECT ON users_info_db.* TO users_info_tls_none@'host' + + - >- + CREATE USER users_info_tls_ssl@'host' + IDENTIFIED WITH mysql_native_password AS + '*CB3326D5279DE7915FE5D743232165EE887883CA' REQUIRE SSL + - GRANT SELECT ON users_info_db.* TO users_info_tls_ssl@'host' + + - >- + CREATE USER users_info_tls_cipher@'host' + IDENTIFIED WITH mysql_native_password AS + '*CB3326D5279DE7915FE5D743232165EE887883CA' + REQUIRE CIPHER 'ECDH-RSA-AES256-SHA384' + - GRANT SELECT ON users_info_db.* TO users_info_tls_cipher@'host' + + - >- + CREATE USER users_info_tls_x509@'host' + IDENTIFIED WITH mysql_native_password AS + '*CB3326D5279DE7915FE5D743232165EE887883CA' REQUIRE X509 + - GRANT SELECT ON users_info_db.* TO users_info_tls_x509@'host' + + - >- + CREATE USER users_info_tls_subject@'host' + IDENTIFIED WITH mysql_native_password AS + '*CB3326D5279DE7915FE5D743232165EE887883CA' + REQUIRE SUBJECT '/CN=Bob/O=MyDom/C=US/ST=Oregon/L=Portland' + - GRANT SELECT ON users_info_db.* TO users_info_tls_subject@'host' + + - >- + CREATE USER users_info_tls_issuer@'host' + IDENTIFIED WITH mysql_native_password AS + '*CB3326D5279DE7915FE5D743232165EE887883CA' + REQUIRE ISSUER '/C=FI/ST=Somewhere/L=City/ + O=CompanyX/CN=Bob/emailAddress=bob@companyx.com' + - GRANT SELECT ON users_info_db.* TO users_info_tls_issuer@'host' + + - >- + CREATE USER users_info_tls_subject_issuer@'host' + IDENTIFIED WITH mysql_native_password AS + '*CB3326D5279DE7915FE5D743232165EE887883CA' + REQUIRE SUBJECT '/CN=Bob/O=MyDom/C=US/ST=Oregon/L=Portland' + AND ISSUER '/C=FI/ST=Somewhere/L=City/ + O=CompanyX/CN=Bob/emailAddress=bob@companyx.com' + - >- + GRANT SELECT ON users_info_db.* + TO users_info_tls_subject_issuer@'host' + + - >- + CREATE USER users_info_tls_sub_issu_ciph@'host' + IDENTIFIED WITH mysql_native_password AS + '*CB3326D5279DE7915FE5D743232165EE887883CA' + REQUIRE SUBJECT '/CN=Bob/O=MyDom/C=US/ST=Oregon/L=Portland' + AND ISSUER '/C=FI/ST=Somewhere/L=City/ + O=CompanyX/CN=Bob/emailAddress=bob@companyx.com' + AND CIPHER 'ECDH-RSA-AES256-SHA384' + - >- + GRANT SELECT ON users_info_db.* + TO users_info_tls_sub_issu_ciph@'host' + + - name: Mysql_info users_info | Prepare tests users for MariaDB + community.mysql.mysql_query: + query: + - >- + CREATE USER users_info_socket@'users_info.com' IDENTIFIED WITH + unix_socket + - GRANT ALL ON *.* to users_info_socket@'users_info.com' + when: + - db_engine == 'mariadb' + + - name: Mysql_info users_info | Prepare tests users for MySQL + community.mysql.mysql_query: + query: + - >- + CREATE USER users_info_sha256@'users_info.com' IDENTIFIED WITH + sha256_password BY 'msandbox' + - GRANT ALL ON *.* to users_info_sha256@'users_info.com' + when: + - db_engine == 'mysql' + + - name: Mysql_info users_info | Prepare tests users for MySQL 8+ + community.mysql.mysql_query: + query: + - >- + CREATE USER users_info_caching_sha2@'users_info.com' IDENTIFIED WITH + caching_sha2_password BY 'msandbox' + - GRANT ALL ON *.* to users_info_caching_sha2@'users_info.com' + when: + - db_engine == 'mysql' + - db_version is version('8.0', '>=') + + # ================================== Tests ============================== + + - name: Mysql_info users_info | Collect users_info + community.mysql.mysql_info: + filter: + - users_info + register: result + + - name: Mysql_info users_info | Recreate users from mysql_info result + community.mysql.mysql_user: + name: "{{ item.name }}" + host: "{{ item.host }}" + plugin: "{{ item.plugin | default(omit) }}" + plugin_auth_string: "{{ item.plugin_auth_string | default(omit) }}" + plugin_hash_string: "{{ item.plugin_hash_string | default(omit) }}" + tls_requires: "{{ item.tls_requires | default(omit) }}" + priv: "{{ item.priv | default(omit) }}" + resource_limits: "{{ item.resource_limits | default(omit) }}" + column_case_sensitive: true + state: present + locked: "{{ item.locked | default(omit) }}" + loop: "{{ result.users_info }}" + loop_control: + label: "{{ item.name }}@{{ item.host }}" + register: recreate_users_result + failed_when: + - >- + recreate_users_result is changed or + recreate_users_result.msg != 'User unchanged' + when: + - item.name != 'root' + - item.name != 'mysql' + - item.name != 'mariadb.sys' + - item.name != 'mysql.sys' + - item.name != 'mysql.infoschema' + - item.name != 'mysql.session' + + + # ================================== Cleanup ============================ + + - name: Mysql_info users_info | Cleanup users_info + community.mysql.mysql_user: + name: "{{ item }}" + host_all: true + column_case_sensitive: true + state: absent + loop: + - users_info_adm + - users_info_schema + - users_info_table + - users_info_col + - users_info_proc + - users_info_multi + - users_info_db + - users_info_usage_only + - users_info_columns_uppercase + - users_info_multi_hosts + - users_info_tls_none + - users_info_tls_ssl + - users_info_tls_cipher + - users_info_tls_x509 + - users_info_tls_subject + - users_info_tls_issuer + - users_info_tls_subject_issuer + - users_info_tls_sub_issu_ciph + + - name: Mysql_info users_info | Cleanup databases + community.mysql.mysql_db: + name: + - users_info_db + - users_info_db2 + - users_info_db3 + state: absent + + - name: Mysql_info users_info | Cleanup sql file for the procedure + ansible.builtin.file: + path: /root/create_procedure.sql + state: absent diff --git a/tests/integration/targets/test_mysql_info/tasks/issue-28.yml b/tests/integration/targets/test_mysql_info/tasks/issue-28.yml index ec2b493..83e6883 100644 --- a/tests/integration/targets/test_mysql_info/tasks/issue-28.yml +++ b/tests/integration/targets/test_mysql_info/tasks/issue-28.yml @@ -1,7 +1,4 @@ --- -- 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: set fact tls_enabled command: "{{ mysql_command }} \"-e SHOW VARIABLES LIKE 'have_ssl';\"" @@ -13,7 +10,7 @@ mysql_parameters: &mysql_params login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' when: tls_enabled block: @@ -29,6 +26,7 @@ mysql_user: <<: *mysql_params name: '{{ user_name_1 }}' + host_all: true state: absent ignore_errors: yes @@ -36,6 +34,7 @@ mysql_user: <<: *mysql_params name: "{{ user_name_1 }}" + host: "%" password: "{{ user_password_1 }}" tls_requires: SSL: @@ -45,7 +44,7 @@ filter: version login_user: '{{ user_name_1 }}' login_password: '{{ user_password_1 }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' ca_cert: /tmp/cert.pem register: result @@ -54,19 +53,21 @@ - assert: that: - result is failed - when: connector.name.0 is search('pymysql') + when: + - connector_name == 'pymysql' - assert: that: - result is succeeded - when: connector.name.0 is not search('pymysql') + when: + - connector_name != 'pymysql' - name: attempt connection with newly created user ignoring hostname mysql_info: filter: version login_user: '{{ user_name_1 }}' login_password: '{{ user_password_1 }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' ca_cert: /tmp/cert.pem check_hostname: no @@ -81,5 +82,5 @@ mysql_user: <<: *mysql_params name: '{{ user_name_1 }}' - host: 127.0.0.1 + host_all: true state: absent diff --git a/tests/integration/targets/test_mysql_info/tasks/main.yml b/tests/integration/targets/test_mysql_info/tasks/main.yml index c3d601d..61f238f 100644 --- a/tests/integration/targets/test_mysql_info/tasks/main.yml +++ b/tests/integration/targets/test_mysql_info/tasks/main.yml @@ -1,10 +1,11 @@ +--- #################################################################### # WARNING: These are designed specifically for Ansible tests # # and should not be used as examples of how to write Ansible roles # #################################################################### # Test code for mysql_info module -# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) +# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) ################### @@ -24,14 +25,14 @@ - name: mysql_info - create default config file template: src: my.cnf.j2 - dest: /root/.my.cnf + dest: "{{ playbook_dir }}/root/.my.cnf" mode: '0400' # Create non-default MySQL config file with credentials - name: mysql_info - create non-default config file template: src: my.cnf.j2 - dest: /root/non-default_my.cnf + dest: "{{ playbook_dir }}/root/non-default_my.cnf" mode: '0400' ############### @@ -43,17 +44,23 @@ login_user: '{{ mysql_user }}' login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' + config_file: "{{ playbook_dir }}/root/.my.cnf" register: result - assert: that: - - result.changed == false - - "mysql_version in result.version.full or mariadb_version in result.version.full" - - result.settings != {} - - result.global_status != {} - - result.databases != {} - - result.engines != {} - - result.users != {} + - result is not changed + - db_version in result.version.full + - result.settings != {} + - result.global_status != {} + - result.databases != {} + - result.engines != {} + - result.users != {} + - result.server_engine == 'MariaDB' or result.server_engine == 'MySQL' + + - name: mysql_info - Test connector informations display + ansible.builtin.import_tasks: + file: connector_info.yml # Access by non-default cred file - name: mysql_info - check non-default cred file @@ -61,12 +68,12 @@ login_user: '{{ mysql_user }}' login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' - config_file: /root/non-default_my.cnf + config_file: "{{ playbook_dir }}/root/non-default_my.cnf" register: result - assert: that: - - result.changed == false + - result is not changed - result.version != {} # Remove cred files @@ -74,9 +81,9 @@ file: path: '{{ item }}' state: absent - with_items: - - /root/.my.cnf - - /root/non-default_my.cnf + loop: + - "{{ playbook_dir }}/.my.cnf" + - "{{ playbook_dir }}/non-default_my.cnf" # Access with password - name: mysql_info - check access with password @@ -86,8 +93,8 @@ - assert: that: - - result.changed == false - - result.version != {} + - result is not changed + - result.version != {} # Test excluding - name: Collect all info except settings and users @@ -98,13 +105,13 @@ - assert: that: - - result.changed == false - - result.version != {} - - result.global_status != {} - - result.databases != {} - - result.engines != {} - - result.settings is not defined - - result.users is not defined + - result is not changed + - result.version != {} + - result.global_status != {} + - result.databases != {} + - result.engines != {} + - result.settings is not defined + - result.users is not defined # Test including - name: Collect info only about version and databases @@ -117,99 +124,20 @@ - assert: that: - - result.changed == false - - result.version != {} - - result.databases != {} - - result.engines is not defined - - result.settings is not defined - - result.global_status is not defined - - result.users is not defined + - result is not changed + - result.version != {} + - result.databases != {} + - result.engines is not defined + - result.settings is not defined + - result.global_status is not defined + - result.users is not defined - # Test exclude_fields: db_size - # 'unsupported' element is passed to check that an unsupported value - # won't break anything (will be ignored regarding to the module's documentation). - - name: Collect info about databases excluding their sizes - mysql_info: - <<: *mysql_params - filter: - - databases - exclude_fields: - - db_size - - unsupported - register: result + - include_tasks: issue-28.yml - - assert: - that: - - result.changed == false - - result.databases != {} - - result.databases.mysql == {} + - name: Import tasks file to tests tables count in database filter + ansible.builtin.import_tasks: + file: filter_databases.yml - ######################################################## - # Issue #65727, empty databases must be in returned dict - # - - name: Create empty database acme - mysql_db: - <<: *mysql_params - name: acme - - - name: Collect info about databases - mysql_info: - <<: *mysql_params - filter: - - databases - return_empty_dbs: true - register: result - - # Check acme is in returned dict - - assert: - that: - - result.changed == false - - result.databases.acme.size == 0 - - result.databases.mysql != {} - - - name: Collect info about databases excluding their sizes - mysql_info: - <<: *mysql_params - filter: - - databases - exclude_fields: - - db_size - return_empty_dbs: true - register: result - - # Check acme is in returned dict - - assert: - that: - - result.changed == false - - result.databases.acme == {} - - result.databases.mysql == {} - - - name: Remove acme database - mysql_db: - <<: *mysql_params - name: acme - 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 + - name: Import tasks file to tests users_info filter + ansible.builtin.import_tasks: + file: filter_users_info.yml diff --git a/tests/integration/targets/test_mysql_query/defaults/main.yml b/tests/integration/targets/test_mysql_query/defaults/main.yml index 4ee25ff..6befdcf 100644 --- a/tests/integration/targets/test_mysql_query/defaults/main.yml +++ b/tests/integration/targets/test_mysql_query/defaults/main.yml @@ -1,5 +1,6 @@ mysql_user: root mysql_password: msandbox +mysql_host: '{{ gateway_addr }}' mysql_primary_port: 3307 db_name: data diff --git a/tests/integration/targets/test_mysql_query/meta/main.yml b/tests/integration/targets/test_mysql_query/meta/main.yml index ce08dc4..01ee3db 100644 --- a/tests/integration/targets/test_mysql_query/meta/main.yml +++ b/tests/integration/targets/test_mysql_query/meta/main.yml @@ -1,2 +1,3 @@ +--- dependencies: -- setup_mysql + - setup_controller diff --git a/tests/integration/targets/test_mysql_query/tasks/issue-28.yml b/tests/integration/targets/test_mysql_query/tasks/issue-28.yml index 61f086e..e788fea 100644 --- a/tests/integration/targets/test_mysql_query/tasks/issue-28.yml +++ b/tests/integration/targets/test_mysql_query/tasks/issue-28.yml @@ -1,7 +1,4 @@ --- -- 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: set fact tls_enabled command: "{{ mysql_command }} \"-e SHOW VARIABLES LIKE 'have_ssl';\"" @@ -13,7 +10,7 @@ mysql_parameters: &mysql_params login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' when: tls_enabled block: @@ -29,6 +26,7 @@ mysql_user: <<: *mysql_params name: '{{ user_name_1 }}' + host_all: true state: absent ignore_errors: yes @@ -36,6 +34,7 @@ mysql_user: <<: *mysql_params name: "{{ user_name_1 }}" + host: "%" password: "{{ user_password_1 }}" tls_requires: SSL: @@ -45,7 +44,7 @@ query: 'SHOW DATABASES' login_user: '{{ user_name_1 }}' login_password: '{{ user_password_1 }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' ca_cert: /tmp/cert.pem register: result @@ -54,19 +53,21 @@ - assert: that: - result is failed - when: connector.name.0 is search('pymysql') + when: + - connector_name == 'pymysql' - assert: that: - result is succeeded - when: connector.name.0 is not search('pymysql') + when: + - connector_name != 'pymysql' - name: attempt connection with newly created user ignoring hostname mysql_query: query: 'SHOW DATABASES' login_user: '{{ user_name_1 }}' login_password: '{{ user_password_1 }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' ca_cert: /tmp/cert.pem check_hostname: no @@ -81,5 +82,5 @@ mysql_user: <<: *mysql_params name: '{{ user_name_1 }}' - host: 127.0.0.1 + host: "%" state: absent diff --git a/tests/integration/targets/test_mysql_query/tasks/main.yml b/tests/integration/targets/test_mysql_query/tasks/main.yml index 6d17308..ffb54e2 100644 --- a/tests/integration/targets/test_mysql_query/tasks/main.yml +++ b/tests/integration/targets/test_mysql_query/tasks/main.yml @@ -6,4 +6,4 @@ # mysql_query module initial CI tests - import_tasks: mysql_query_initial.yml -- include: issue-28.yml +- include_tasks: issue-28.yml 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 30182fe..310f925 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 @@ -1,11 +1,12 @@ +--- # Test code for mysql_query module -# Copyright: (c) 2020, Andrew Klychkov (@Andersson007) +# Copyright: (c) 2020, Andrew Klychkov (@Andersson007) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - vars: mysql_parameters: &mysql_params login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' block: @@ -16,10 +17,11 @@ query: 'CREATE DATABASE {{ test_db }}' register: result - - assert: + - name: Assert that create db test_db is changed and returns expected query + assert: that: - - result is changed - - result.executed_queries == ['CREATE DATABASE {{ test_db }}'] + - result is changed + - result.executed_queries == ['CREATE DATABASE {{ test_db }}'] - name: Create {{ test_table1 }} mysql_query: @@ -28,26 +30,31 @@ query: 'CREATE TABLE {{ test_table1 }} (id int)' register: result - - assert: + - name: Assert that create table test_table1 is changed and returns expected query + assert: that: - - result is changed - - result.executed_queries == ['CREATE TABLE {{ test_table1 }} (id int)'] + - result is changed + - result.executed_queries == ['CREATE TABLE {{ test_table1 }} (id int)'] + - result.execution_time_ms[0] > 0 - name: Insert test data mysql_query: <<: *mysql_params login_db: '{{ test_db }}' query: - - 'INSERT INTO {{ test_table1 }} VALUES (1), (2)' - - 'INSERT INTO {{ test_table1 }} VALUES (3)' + - 'INSERT INTO {{ test_table1 }} VALUES (1), (2)' + - 'INSERT INTO {{ test_table1 }} VALUES (3)' single_transaction: yes register: result - - assert: + - name: Assert that inserting test data is changed and returns expected query and results + assert: that: - - result is changed - - result.rowcount == [2, 1] - - result.executed_queries == ['INSERT INTO {{ test_table1 }} VALUES (1), (2)', 'INSERT INTO {{ test_table1 }} VALUES (3)'] + - result is changed + - result.rowcount == [2, 1] + - result.executed_queries == ['INSERT INTO {{ test_table1 }} VALUES (1), (2)', 'INSERT INTO {{ test_table1 }} VALUES (3)'] + - result.execution_time_ms[0] > 0 + - result.execution_time_ms[1] > 0 - name: Check data in {{ test_table1 }} mysql_query: @@ -56,14 +63,15 @@ query: 'SELECT * FROM {{ test_table1 }}' register: result - - assert: + - name: Assert that query data in test_table1 is not changed and returns expected query and results + assert: that: - - result is not changed - - result.executed_queries == ['SELECT * FROM {{ test_table1 }}'] - - result.rowcount == [3] - - result.query_result[0][0].id == 1 - - result.query_result[0][1].id == 2 - - result.query_result[0][2].id == 3 + - result is not changed + - result.executed_queries == ['SELECT * FROM {{ test_table1 }}'] + - result.rowcount == [3] + - result.query_result[0][0].id == 1 + - result.query_result[0][1].id == 2 + - result.query_result[0][2].id == 3 - name: Check data in {{ test_table1 }} using positional args mysql_query: @@ -74,12 +82,13 @@ - 1 register: result - - assert: + - name: Assert that query data in test_table1 using positional args is not changed and returns expected query and results + assert: that: - - result is not changed - - result.executed_queries == ["SELECT * FROM {{ test_table1 }} WHERE id = 1"] - - result.rowcount == [1] - - result.query_result[0][0].id == 1 + - result is not changed + - result.executed_queries == ["SELECT * FROM {{ test_table1 }} WHERE id = 1"] + - result.rowcount == [1] + - result.query_result[0][0].id == 1 - name: Check data in {{ test_table1 }} using named args mysql_query: @@ -90,12 +99,13 @@ some_id: 1 register: result - - assert: + - name: Assert that query data in test_table1 using named args is not changed and returns expected query and results + assert: that: - - result is not changed - - result.executed_queries == ["SELECT * FROM {{ test_table1 }} WHERE id = 1"] - - result.rowcount == [1] - - result.query_result[0][0].id == 1 + - result is not changed + - result.executed_queries == ["SELECT * FROM {{ test_table1 }} WHERE id = 1"] + - result.rowcount == [1] + - result.query_result[0][0].id == 1 - name: Update data in {{ test_table1 }} mysql_query: @@ -107,11 +117,12 @@ new_id: 0 register: result - - assert: + - name: Assert that update data in test_table1 is changed and returns the expected query + assert: that: - - result is changed - - result.executed_queries == ['UPDATE {{ test_table1 }} SET id = 0 WHERE id = 1'] - - result.rowcount == [1] + - result is changed + - result.executed_queries == ['UPDATE {{ test_table1 }} SET id = 0 WHERE id = 1'] + - result.rowcount == [1] - name: Check the prev update - row with value 1 does not exist anymore mysql_query: @@ -122,11 +133,12 @@ some_id: 1 register: result - - assert: + - name: Assert that query that check the prev update is not changed and returns the expected query with id = 1 + assert: that: - - result is not changed - - result.executed_queries == ['SELECT * FROM {{ test_table1 }} WHERE id = 1'] - - result.rowcount == [0] + - result is not changed + - result.executed_queries == ['SELECT * FROM {{ test_table1 }} WHERE id = 1'] + - result.rowcount == [0] - name: Check the prev update - row with value - exist mysql_query: @@ -137,11 +149,12 @@ some_id: 0 register: result - - assert: + - name: Assert that query that check the prev update is not changed and returns the expected query with id = 0 + assert: that: - - result is not changed - - result.executed_queries == ['SELECT * FROM {{ test_table1 }} WHERE id = 0'] - - result.rowcount == [1] + - result is not changed + - result.executed_queries == ['SELECT * FROM {{ test_table1 }} WHERE id = 0'] + - result.rowcount == [1] - name: Update data in {{ test_table1 }} again mysql_query: @@ -153,11 +166,12 @@ new_id: 0 register: result - - assert: + - name: Assert that update data in test_table1 again is not changed and returns expected query + assert: that: - - result is not changed - - result.executed_queries == ['UPDATE {{ test_table1 }} SET id = 0 WHERE id = 1'] - - result.rowcount == [0] + - result is not changed + - result.executed_queries == ['UPDATE {{ test_table1 }} SET id = 0 WHERE id = 1'] + - result.rowcount == [0] - name: Delete data from {{ test_table1 }} mysql_query: @@ -168,11 +182,12 @@ - 'SELECT * FROM {{ test_table1 }} WHERE id = 0' register: result - - assert: + - name: Assert that delete data from test_table1 is changed an returns expected query + assert: that: - - result is changed - - result.executed_queries == ['DELETE FROM {{ test_table1 }} WHERE id = 0', 'SELECT * FROM {{ test_table1 }} WHERE id = 0'] - - result.rowcount == [1, 0] + - result is changed + - result.executed_queries == ['DELETE FROM {{ test_table1 }} WHERE id = 0', 'SELECT * FROM {{ test_table1 }} WHERE id = 0'] + - result.rowcount == [1, 0] - name: Delete data from {{ test_table1 }} again mysql_query: @@ -181,26 +196,28 @@ query: 'DELETE FROM {{ test_table1 }} WHERE id = 0' register: result - - assert: + - name: Assert that delete data from test_table1 again is not changed and returns expected query + assert: that: - - result is not changed - - result.executed_queries == ['DELETE FROM {{ test_table1 }} WHERE id = 0'] - - result.rowcount == [0] + - result is not changed + - result.executed_queries == ['DELETE FROM {{ test_table1 }} WHERE id = 0'] + - result.rowcount == [0] - name: Truncate {{ test_table1 }} mysql_query: <<: *mysql_params login_db: '{{ test_db }}' query: - - 'TRUNCATE {{ test_table1 }}' - - 'SELECT * FROM {{ test_table1 }}' + - 'TRUNCATE {{ test_table1 }}' + - 'SELECT * FROM {{ test_table1 }}' register: result - - assert: + - name: Assert that truncate test_table1 is changed and returns expected query + assert: that: - - result is changed - - result.executed_queries == ['TRUNCATE {{ test_table1 }}', 'SELECT * FROM {{ test_table1 }}'] - - result.rowcount == [0, 0] + - result is changed + - result.executed_queries == ['TRUNCATE {{ test_table1 }}', 'SELECT * FROM {{ test_table1 }}'] + - result.rowcount == [0, 0] - name: Rename {{ test_table1 }} mysql_query: @@ -209,11 +226,12 @@ query: 'RENAME TABLE {{ test_table1 }} TO {{ test_table2 }}' register: result - - assert: + - name: Assert that rename table test_table1 is changed and returns expected query + assert: that: - - result is changed - - result.executed_queries == ['RENAME TABLE {{ test_table1 }} TO {{ test_table2 }}'] - - result.rowcount == [0] + - result is changed + - result.executed_queries == ['RENAME TABLE {{ test_table1 }} TO {{ test_table2 }}'] + - result.rowcount == [0] - name: Check the prev rename mysql_query: @@ -223,9 +241,10 @@ register: result ignore_errors: yes - - assert: + - name: Assert that query old table is failed + assert: that: - - result.failed == true + - result is failed - name: Check the prev rename mysql_query: @@ -234,9 +253,10 @@ query: 'SELECT * FROM {{ test_table2 }}' register: result - - assert: + - name: Assert that query new table succeed and returns 0 row + assert: that: - - result.rowcount == [0] + - result.rowcount == [0] - name: Create {{ test_table3 }} mysql_query: @@ -257,9 +277,10 @@ query: 'SELECT id, story FROM {{ test_table3 }}' register: result - - assert: + - name: Assert that select from test_table3 returns 2 rows + assert: that: - - result.rowcount == [2] + - result.rowcount == [2] - name: Pass wrong query type mysql_query: @@ -269,10 +290,11 @@ register: result ignore_errors: yes - - assert: + - name: Assert that pass wrong query type is failed + assert: that: - - result is failed - - result.msg is search('the query option value must be a string or list') + - result is failed + - result.msg is search('the query option value must be a string or list') - name: Pass wrong query element mysql_query: @@ -284,10 +306,11 @@ register: result ignore_errors: yes - - assert: + - name: Assert that pass wrong query element is failed + assert: that: - - result is failed - - result.msg is search('the elements in query list must be strings') + - result is failed + - result.msg is search('the elements in query list must be strings') - name: Create {{ test_table4 }} mysql_query: @@ -303,10 +326,11 @@ single_transaction: yes register: result - - assert: + - name: Assert that insert test data using replace statement is changed + assert: that: - - result is changed - - result.rowcount == [1] + - result is changed + - result.rowcount == [1] - name: Replace test data mysql_query: @@ -318,8 +342,51 @@ - assert: that: - - result is changed - - result.rowcount == [2] + - result is changed + - result.rowcount == [2] + + # Issue https://github.com/ansible-collections/community.mysql/issues/268 + - name: Create table + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: "CREATE TABLE issue268 (id int)" + single_transaction: yes + + # Issue https://github.com/ansible-collections/community.mysql/issues/268 + - name: Create table with IF NOT EXISTS + mysql_query: + <<: *mysql_params + login_db: '{{ test_db }}' + query: "CREATE TABLE IF NOT EXISTS issue268 (id int)" + single_transaction: yes + register: result + + # Issue https://github.com/ansible-collections/community.mysql/issues/268 + - name: Assert that create table IF NOT EXISTS is not changed with pymysql + assert: + that: + # PyMySQL driver throws a warning for version before 0.10.0 + - result is not changed + when: + - connector_name == 'pymysql' + - connector_version is version('0.10.0', '<') + + # Issue https://github.com/ansible-collections/community.mysql/issues/268 + - name: Assert that create table IF NOT EXISTS is changed with mysqlclient + assert: + that: + # Mysqlclient 2.0.1 and pymysql 0.10.0+ drivers throws no warning, + # so it's impossible to figure out if the state was changed or not. + # We assume that it was for DDL queries by default in the code + - result is changed + when: + - > + connector_name == 'mysqlclient' + or ( + connector_name == 'pymysql' + and connector_version is version('0.10.0', '>') + ) - name: Drop db {{ test_db }} mysql_query: @@ -327,7 +394,15 @@ query: 'DROP DATABASE {{ test_db }}' register: result - - assert: + - name: Assert that drop database is changed and returns expected query + assert: that: - - result is changed - - result.executed_queries == ['DROP DATABASE {{ test_db }}'] + - result is changed + - result.executed_queries == ['DROP DATABASE {{ test_db }}'] + + always: + + - name: Clean up test_db + mysql_query: + <<: *mysql_params + query: 'DROP DATABASE IF EXISTS {{ test_db }}' diff --git a/tests/integration/targets/test_mysql_replication/defaults/main.yml b/tests/integration/targets/test_mysql_replication/defaults/main.yml index d2d2080..48fd560 100644 --- a/tests/integration/targets/test_mysql_replication/defaults/main.yml +++ b/tests/integration/targets/test_mysql_replication/defaults/main.yml @@ -1,6 +1,6 @@ mysql_user: root mysql_password: msandbox -mysql_host: 127.0.0.1 +mysql_host: '{{ gateway_addr }}' mysql_primary_port: 3307 mysql_replica1_port: 3308 mysql_replica2_port: 3309 diff --git a/tests/integration/targets/test_mysql_replication/meta/main.yml b/tests/integration/targets/test_mysql_replication/meta/main.yml index 36e111c..01ee3db 100644 --- a/tests/integration/targets/test_mysql_replication/meta/main.yml +++ b/tests/integration/targets/test_mysql_replication/meta/main.yml @@ -1,3 +1,3 @@ --- dependencies: -- setup_mysql + - setup_controller diff --git a/tests/integration/targets/test_mysql_replication/tasks/issue-265.yml b/tests/integration/targets/test_mysql_replication/tasks/issue-265.yml index 24232f3..1718b99 100644 --- a/tests/integration/targets/test_mysql_replication/tasks/issue-265.yml +++ b/tests/integration/targets/test_mysql_replication/tasks/issue-265.yml @@ -1,13 +1,10 @@ --- -- 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_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' block: @@ -29,6 +26,7 @@ mysql_user: <<: *mysql_params name: '{{ user_name_1 }}' + host: '{{ gateway_addr }}' state: absent ignore_errors: yes @@ -38,6 +36,7 @@ mysql_user: <<: *mysql_params name: "{{ user_name_1 }}" + host: '{{ gateway_addr }}' password: "{{ user_password_1 }}" priv: '*.*:ALL,GRANT' force_context: yes @@ -47,7 +46,7 @@ mode: getprimary login_user: '{{ user_name_1 }}' login_password: '{{ user_password_1 }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_replica1_port }}' register: result ignore_errors: yes @@ -60,6 +59,7 @@ mysql_user: <<: *mysql_params name: '{{ user_name_1 }}' + host: '{{ gateway_addr }}' state: absent force_context: yes @@ -68,7 +68,7 @@ mode: getprimary login_user: '{{ user_name_1 }}' login_password: '{{ user_password_1 }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_replica1_port }}' register: result ignore_errors: yes @@ -92,12 +92,12 @@ - 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' + shell: "echo \"CHANGE REPLICATION FILTER REPLICATE_IGNORE_DB = (mysql);\" | {{ mysql_command_wo_port }} -P{{ mysql_replica1_port }}" + when: db_engine == 'mysql' - name: Create replication filter MariaDB - shell: "echo \"SET GLOBAL replicate_ignore_db = 'mysql';\" | {{ mysql_command }} -P{{ mysql_replica1_port }}" - when: install_type == 'mariadb' + shell: "echo \"SET GLOBAL replicate_ignore_db = 'mysql';\" | {{ mysql_command_wo_port }} -P{{ mysql_replica1_port }}" + when: db_engine == 'mariadb' - name: Start replica mysql_replication: @@ -117,6 +117,7 @@ mysql_user: <<: *mysql_params name: "{{ user_name_1 }}" + host: "{{ gateway_addr }}" password: "{{ user_password_1 }}" priv: '*.*:ALL,GRANT' force_context: yes @@ -126,7 +127,7 @@ mode: getprimary login_user: '{{ user_name_1 }}' login_password: '{{ user_password_1 }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_replica1_port }}' register: result ignore_errors: yes @@ -139,6 +140,7 @@ mysql_user: <<: *mysql_params name: '{{ user_name_1 }}' + host: "{{ gateway_addr }}" state: absent force_context: yes @@ -157,9 +159,9 @@ - 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' + shell: "echo \"CHANGE REPLICATION FILTER REPLICATE_IGNORE_DB = ();\" | {{ mysql_command_wo_port }} -P{{ mysql_replica1_port }}" + when: db_engine == 'mysql' - name: Remove replication filter MariaDB - shell: "echo \"SET GLOBAL replicate_ignore_db = '';\" | {{ mysql_command }} -P{{ mysql_replica1_port }}" - when: install_type == 'mariadb' + shell: "echo \"SET GLOBAL replicate_ignore_db = '';\" | {{ mysql_command_wo_port }} -P{{ mysql_replica1_port }}" + when: db_engine == 'mariadb' 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 11e457b..4225a07 100644 --- a/tests/integration/targets/test_mysql_replication/tasks/issue-28.yml +++ b/tests/integration/targets/test_mysql_replication/tasks/issue-28.yml @@ -1,7 +1,4 @@ --- -- 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: set fact tls_enabled command: "{{ mysql_command }} \"-e SHOW VARIABLES LIKE 'have_ssl';\"" @@ -13,7 +10,7 @@ mysql_parameters: &mysql_params login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' when: tls_enabled block: @@ -29,6 +26,7 @@ mysql_user: <<: *mysql_params name: '{{ user_name_1 }}' + host_all: true state: absent ignore_errors: yes @@ -46,7 +44,7 @@ mode: getprimary login_user: '{{ user_name_1 }}' login_password: '{{ user_password_1 }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' ca_cert: /tmp/cert.pem register: result @@ -55,19 +53,21 @@ - assert: that: - result is failed - when: connector.name.0 is search('pymysql') + when: + - connector_name == 'pymysql' - assert: that: - result is succeeded - when: connector.name.0 is not search('pymysql') + when: + - connector_name != 'pymysql' - name: attempt connection with newly created user ignoring hostname mysql_replication: mode: getprimary login_user: '{{ user_name_1 }}' login_password: '{{ user_password_1 }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' ca_cert: /tmp/cert.pem check_hostname: no @@ -82,5 +82,5 @@ mysql_user: <<: *mysql_params name: '{{ user_name_1 }}' - host: 127.0.0.1 + host: '{{ gateway_addr }}' state: absent diff --git a/tests/integration/targets/test_mysql_replication/tasks/main.yml b/tests/integration/targets/test_mysql_replication/tasks/main.yml index 044787a..32ce553 100644 --- a/tests/integration/targets/test_mysql_replication/tasks/main.yml +++ b/tests/integration/targets/test_mysql_replication/tasks/main.yml @@ -1,16 +1,17 @@ +--- #################################################################### # WARNING: These are designed specifically for Ansible tests # # and should not be used as examples of how to write Ansible roles # #################################################################### -# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) +# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # Initial CI tests of mysql_replication module: - import_tasks: mysql_replication_initial.yml # Tests of replication filters and force_context -- include: issue-265.yml +- include_tasks: issue-265.yml # Tests of primary_delay parameter: - import_tasks: mysql_replication_primary_delay.yml @@ -18,9 +19,15 @@ # Tests of channel parameter: - import_tasks: mysql_replication_channel.yml when: - - install_type == 'mysql' # FIXME: mariadb introduces FOR CHANNEL in 10.7 + - db_engine == 'mysql' # FIXME: mariadb introduces FOR CHANNEL in 10.7 # Tests of resetprimary mode: - import_tasks: mysql_replication_resetprimary_mode.yml -- include: issue-28.yml +- include_tasks: issue-28.yml + +# Tests of changereplication mode: +- import_tasks: mysql_replication_changereplication_mode.yml + when: + - db_engine == 'mysql' + - db_version is version('8.0.23', '>=') diff --git a/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_changereplication_mode.yml b/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_changereplication_mode.yml new file mode 100644 index 0000000..2f593ca --- /dev/null +++ b/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_changereplication_mode.yml @@ -0,0 +1,65 @@ +--- + +- vars: + mysql_params: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + + block: + # Get primary log file and log pos: + - name: Get primary status + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_primary_port }}' + mode: getprimary + register: mysql_primary_status + + # Test changereplication mode: + - name: Run replication + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica1_port }}' + mode: changereplication + 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: '' + primary_ssl: no + register: result + + - name: Assert that changereplication is changed and return expected query + assert: + that: + - result is changed + - result.queries == expected_queries + vars: + expected_queries: ["CHANGE REPLICATION SOURCE TO SOURCE_HOST='{{ mysql_host }}',\ + SOURCE_USER='{{ replication_user }}',SOURCE_PASSWORD='********',\ + SOURCE_PORT={{ mysql_primary_port }},SOURCE_LOG_FILE=\ + '{{ mysql_primary_status.File }}',SOURCE_LOG_POS=\ + {{ mysql_primary_status.Position }},SOURCE_SSL=0,SOURCE_SSL_CA=''"] + + # Test changereplication mode with channel: + - name: Run replication + mysql_replication: + <<: *mysql_params + login_port: '{{ mysql_replica1_port }}' + mode: changereplication + primary_user: '{{ replication_user }}' + primary_password: '{{ replication_pass }}' + channel: '{{ test_channel }}' + + register: with_channel_result_queries + + - name: Assert that changereplication is changed and is called correctly with channel + assert: + that: + - with_channel_result_queries is changed + - with_channel_result_queries.queries == expected_queries + vars: + expected_queries: ["CHANGE REPLICATION SOURCE TO SOURCE_USER='{{ replication_user }}',\ + SOURCE_PASSWORD='********' FOR CHANNEL '{{ test_channel }}'"] 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 e314aae..0bcc6e6 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 @@ -1,11 +1,12 @@ -# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) +--- +# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - vars: mysql_params: &mysql_params login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' block: # Get primary log file and log pos: @@ -31,10 +32,36 @@ channel: '{{ test_channel }}' register: result - - assert: + - name: Assert that run replication with channel is changed and query matches for MariaDB and MySQL < 8.0.23 + ansible.builtin.assert: that: - - result is changed - - result.queries == ["CHANGE MASTER TO MASTER_HOST='{{ mysql_host }}',MASTER_USER='{{ replication_user }}',MASTER_PASSWORD='********',MASTER_PORT={{ mysql_primary_port }},MASTER_LOG_FILE='{{ mysql_primary_status.File }}',MASTER_LOG_POS={{ mysql_primary_status.Position }} FOR CHANNEL '{{ test_channel }}'"] + - result is changed + - result.queries == result_query + when: + - > + db_engine == 'mariadb' or + (db_engine == 'mysql' and db_version is version('8.0.23', '<')) + vars: + result_query: ["CHANGE MASTER TO MASTER_HOST='{{ mysql_host }}',\ + MASTER_USER='{{ replication_user }}',MASTER_PASSWORD='********',\ + MASTER_PORT={{ mysql_primary_port }},MASTER_LOG_FILE=\ + '{{ mysql_primary_status.File }}',MASTER_LOG_POS=\ + {{ mysql_primary_status.Position }} FOR CHANNEL '{{ test_channel }}'"] + + - name: Assert that run replication with channel is changed and query matches for MySQL >= 8.0.23 + ansible.builtin.assert: + that: + - result is changed + - result.queries == result_query + when: + - db_engine == 'mysql' + - db_version is version('8.0.23', '>=') + vars: + result_query: ["CHANGE REPLICATION SOURCE TO SOURCE_HOST='{{ mysql_host }}',\ + SOURCE_USER='{{ replication_user }}',SOURCE_PASSWORD='********',\ + SOURCE_PORT={{ mysql_primary_port }},SOURCE_LOG_FILE=\ + '{{ mysql_primary_status.File }}',SOURCE_LOG_POS=\ + {{ mysql_primary_status.Position }} FOR CHANNEL '{{ test_channel }}'"] # Test startreplica mode: - name: Start replica with channel @@ -47,8 +74,11 @@ - assert: that: - - result is changed - - result.queries == ["START SLAVE FOR CHANNEL '{{ test_channel }}'"] or result.queries == ["START REPLICA FOR CHANNEL '{{ test_channel }}'"] + - result is changed + - result.queries == result_query or result_query2 + vars: + result_query: ["START SLAVE FOR CHANNEL '{{ test_channel }}'"] + result_query2: ["START REPLICA FOR CHANNEL '{{ test_channel }}'"] # Test getreplica mode: - name: Get standby status with channel @@ -61,27 +91,40 @@ - assert: that: - - 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 }} - - replica_status.Last_IO_Errno == 0 - - replica_status.Last_IO_Error == '' - - replica_status.Channel_Name == '{{ test_channel }}' - - replica_status is not changed - when: mysql8022_and_higher == false + - replica_status.Is_Replica is truthy(convert_bool=True) + - replica_status.Master_Host == mysql_host_value + - replica_status.Exec_Master_Log_Pos == mysql_primary_status.Position + - replica_status.Master_Port == mysql_primary_port_value + - replica_status.Last_IO_Errno == 0 + - replica_status.Last_IO_Error == '' + - replica_status.Channel_Name == test_channel_value + - replica_status is not changed + vars: + mysql_host_value: '{{ mysql_host }}' + mysql_primary_port_value: '{{ mysql_primary_port }}' + test_channel_value: '{{ test_channel }}' + when: + - > + db_engine == 'mariadb' or + (db_engine == 'mysql' and db_version is version('8.0.22', '<')) - assert: that: - - 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 }} - - replica_status.Last_IO_Errno == 0 - - replica_status.Last_IO_Error == '' - - replica_status.Channel_Name == '{{ test_channel }}' - - replica_status is not changed - when: mysql8022_and_higher == true + - replica_status.Is_Replica is truthy(convert_bool=True) + - replica_status.Source_Host == mysql_host_value + - replica_status.Exec_Source_Log_Pos == mysql_primary_status.Position + - replica_status.Source_Port == mysql_primary_port_value + - replica_status.Last_IO_Errno == 0 + - replica_status.Last_IO_Error == '' + - replica_status.Channel_Name == test_channel_value + - replica_status is not changed + vars: + mysql_host_value: '{{ mysql_host }}' + mysql_primary_port_value: '{{ mysql_primary_port }}' + test_channel_value: '{{ test_channel }}' + when: + - db_engine == 'mysql' + - db_version is version('8.0.22', '>=') # Test stopreplica mode: @@ -95,8 +138,11 @@ - assert: that: - - result is changed - - result.queries == ["STOP SLAVE FOR CHANNEL '{{ test_channel }}'"] or result.queries == ["STOP REPLICA FOR CHANNEL '{{ test_channel }}'"] + - result is changed + - result.queries == result_query or result.queries == result_query2 + vars: + result_query: ["STOP SLAVE FOR CHANNEL '{{ test_channel }}'"] + result_query2: ["STOP REPLICA FOR CHANNEL '{{ test_channel }}'"] # Test reset - name: Reset replica with channel @@ -109,8 +155,11 @@ - assert: that: - - result is changed - - result.queries == ["RESET SLAVE FOR CHANNEL '{{ test_channel }}'"] or result.queries == ["RESET REPLICA FOR CHANNEL '{{ test_channel }}'"] + - result is changed + - result.queries == result_query or result.queries == result_query2 + vars: + result_query: ["RESET SLAVE FOR CHANNEL '{{ test_channel }}'"] + result_query2: ["RESET REPLICA FOR CHANNEL '{{ test_channel }}'"] # Test reset all - name: Reset replica all with channel @@ -123,5 +172,8 @@ - assert: that: - - result is changed - - result.queries == ["RESET SLAVE ALL FOR CHANNEL '{{ test_channel }}'"] or result.queries == ["RESET REPLICA ALL FOR CHANNEL '{{ test_channel }}'"] + - result is changed + - result.queries == result_query or result.queries == result_query2 + vars: + result_query: ["RESET SLAVE ALL FOR CHANNEL '{{ test_channel }}'"] + result_query2: ["RESET REPLICA ALL FOR CHANNEL '{{ test_channel }}'"] 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 7f6e554..00699c1 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 @@ -1,43 +1,37 @@ -# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) +--- +# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - vars: mysql_params: &mysql_params login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' block: - - name: find out the database version - mysql_info: - <<: *mysql_params - login_port: '{{ mysql_primary_port }}' - filter: version - register: db - - name: Set mysql8022_and_higher - set_fact: - mysql8022_and_higher: false - - - name: Set mysql8022_and_higher - set_fact: - mysql8022_and_higher: true - when: - - db.version.major > 8 or (db.version.major == 8 and db.version.minor > 0) or (db.version.major == 8 and db.version.minor == 0 and db.version.release >= 22) - - install_type == 'mysql' - - - name: alias mysql command to include default options - set_fact: - mysql_command: "mysql -u{{ mysql_user }} -p{{ mysql_password }} --protocol=tcp" - - # Preparation: + # We use iF NOT EXISTS because the GITHUB Action: + # "ansible-community/ansible-test-gh-action" uses "--retry-on-error". + # If test_mysql_replication fails, test will run again an without the IF + # NOT EXISTS, we see "Error 1396 (HY000): Operation CREATE USER failed..." + # which is misleading. - name: Create user for mysql replication - shell: "echo \"CREATE USER '{{ replication_user }}'@'localhost' IDENTIFIED WITH mysql_native_password BY '{{ replication_pass }}'; GRANT REPLICATION SLAVE ON *.* TO '{{ replication_user }}'@'localhost';\" | {{ mysql_command }} -P{{ mysql_primary_port }}" - when: install_type == 'mysql' + shell: + "echo \"CREATE USER IF NOT EXISTS \ + '{{ replication_user }}'@'{{ mysql_host }}' \ + IDENTIFIED WITH mysql_native_password BY '{{ replication_pass }}'; \ + GRANT REPLICATION SLAVE ON *.* TO \ + '{{ replication_user }}'@'{{ mysql_host }}';\" | {{ mysql_command }}" + when: db_engine == 'mysql' - name: Create user for mariadb replication - shell: "echo \"CREATE USER '{{ replication_user }}'@'localhost' IDENTIFIED BY '{{ replication_pass }}'; GRANT REPLICATION SLAVE ON *.* TO '{{ replication_user }}'@'localhost';\" | {{ mysql_command }} -P{{ mysql_primary_port }}" - when: install_type == 'mariadb' + shell: + "echo \"CREATE USER IF NOT EXISTS \ + '{{ replication_user }}'@'{{ mysql_host }}' \ + IDENTIFIED BY '{{ replication_pass }}'; \ + GRANT REPLICATION SLAVE ON *.* TO \ + '{{ replication_user }}'@'{{ mysql_host }}';\" | {{ mysql_command }}" + when: db_engine == 'mariadb' - name: Create test database mysql_db: @@ -47,13 +41,31 @@ name: '{{ test_db }}' - name: Dump all databases from the primary - shell: 'mysqldump -u{{ mysql_user }} -p{{ mysql_password }} -h{{ mysql_host }} --protocol=tcp -P{{ mysql_primary_port }} --all-databases --ignore-table=mysql.innodb_index_stats --ignore-table=mysql.innodb_table_stats --master-data=2 > {{ dump_path }}' + shell: + cmd: >- + mysqldump + -u{{ mysql_user }} + -p{{ mysql_password }} + -h{{ mysql_host }} + -P{{ mysql_primary_port }} + --protocol=tcp + --all-databases + --ignore-table=mysql.innodb_index_stats + --ignore-table=mysql.innodb_table_stats + --master-data=2 + > {{ dump_path }} - name: Restore the dump to replica1 - shell: '{{ mysql_command }} -P{{ mysql_replica1_port }} < {{ dump_path }}' + shell: + cmd: >- + {{ mysql_command_wo_port }} + -P{{ mysql_replica1_port }} < {{ dump_path }} - name: Restore the dump to replica2 - shell: '{{ mysql_command }} -P{{ mysql_replica2_port }} < {{ dump_path }}' + shell: + cmd: >- + {{ mysql_command_wo_port }} + -P{{ mysql_replica2_port }} < {{ dump_path }} # Test getprimary mode: - name: Get primary status @@ -63,11 +75,12 @@ mode: getprimary register: mysql_primary_status - - assert: + - name: Assert that primary is in expected state + assert: that: - - mysql_primary_status.Is_Primary == true - - mysql_primary_status.Position != 0 - - mysql_primary_status is not changed + - mysql_primary_status.Is_Primary == true + - mysql_primary_status.Position != 0 + - mysql_primary_status is not changed # Test startreplica fails without changeprimary first. This needs fail_on_error - name: Start replica and fail because primary is not specified; failing on error as requested @@ -80,9 +93,10 @@ register: result ignore_errors: yes - - assert: + - name: Assert that startreplica is failed + assert: that: - - result is failed + - result is failed # Test startreplica doesn't fail if fail_on_error: no - name: Start replica and fail without propagating it to ansible as we were asked not to @@ -93,9 +107,10 @@ fail_on_error: no register: result - - assert: + - name: Assert that startreplica succeeded + assert: that: - - result is not failed + - result is not failed # Test startreplica doesn't fail if there is no fail_on_error. # This is suboptimal because nothing happens, but it's the old behavior. @@ -106,15 +121,15 @@ mode: startreplica register: result - - assert: + - name: Assert that start replica succeeded again + assert: that: - - result is not failed + - result is not failed - # 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: + - name: Test changeprimary mode with empty primary_ssl_ca + community.mysql.mysql_replication: <<: *mysql_params login_port: '{{ mysql_replica1_port }}' mode: changeprimary @@ -125,12 +140,40 @@ primary_log_file: '{{ mysql_primary_status.File }}' primary_log_pos: '{{ mysql_primary_status.Position }}' primary_ssl_ca: '' + primary_ssl: false register: result - - assert: + - name: Assert that changeprimmary is changed and return expected query for MariaDB and MySQL < 8.0.23 + ansible.builtin.assert: that: - - result is changed - - result.queries == ["CHANGE MASTER TO MASTER_HOST='{{ mysql_host }}',MASTER_USER='{{ replication_user }}',MASTER_PASSWORD='********',MASTER_PORT={{ mysql_primary_port }},MASTER_LOG_FILE='{{ mysql_primary_status.File }}',MASTER_LOG_POS={{ mysql_primary_status.Position }},MASTER_SSL_CA=''"] + - result is changed + - result.queries == expected_queries + when: + - > + db_engine == 'mariadb' or + (db_engine == 'mysql' and db_version is version('8.0.23', '<')) + vars: + expected_queries: ["CHANGE MASTER TO MASTER_HOST='{{ mysql_host }}',\ + MASTER_USER='{{ replication_user }}',MASTER_PASSWORD='********',\ + MASTER_PORT={{ mysql_primary_port }},MASTER_LOG_FILE=\ + '{{ mysql_primary_status.File }}',MASTER_LOG_POS=\ + {{ mysql_primary_status.Position }},MASTER_SSL=0,MASTER_SSL_CA=''"] + + - name: Assert that changeprimmary is changed and return expected query for MySQL > 8.0.23 + ansible.builtin.assert: + that: + - result is changed + - result.queries == expected_queries + when: + - db_engine == 'mysql' + - db_version is version('8.0.23', '>=') + vars: + expected_queries: ["CHANGE REPLICATION SOURCE TO \ + SOURCE_HOST='{{ mysql_host }}',\ + SOURCE_USER='{{ replication_user }}',SOURCE_PASSWORD='********',\ + SOURCE_PORT={{ mysql_primary_port }},SOURCE_LOG_FILE=\ + '{{ mysql_primary_status.File }}',SOURCE_LOG_POS=\ + {{ mysql_primary_status.Position }},SOURCE_SSL=0,SOURCE_SSL_CA=''"] # Test startreplica mode: - name: Start replica @@ -140,10 +183,11 @@ mode: startreplica register: result - - assert: + - name: Assert that startreplica is changed and returns expected query + assert: that: - - result is changed - - result.queries == ["START SLAVE"] or result.queries == ["START REPLICA"] + - result is changed + - result.queries == ["START SLAVE"] or result.queries == ["START REPLICA"] # Test getreplica mode: - name: Get replica status @@ -153,38 +197,51 @@ mode: getreplica register: replica_status - - assert: + - name: Assert that getreplica returns expected values for MySQL older than 8.0.22 and Mariadb + assert: that: - - 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 }} - - replica_status.Last_IO_Errno == 0 - - replica_status.Last_IO_Error == '' - - replica_status is not changed - when: mysql8022_and_higher == false + - replica_status.Is_Replica is truthy(convert_bool=True) + - replica_status.Master_Host == mysql_host_value + - replica_status.Exec_Master_Log_Pos == mysql_primary_status.Position + - replica_status.Master_Port == mysql_primary_port_value + - replica_status.Last_IO_Errno == 0 + - replica_status.Last_IO_Error == '' + - replica_status is not changed + vars: + mysql_host_value: "{{ mysql_host }}" + mysql_primary_port_value: "{{ mysql_primary_port }}" + when: + - > + db_engine == 'mariadb' or + (db_engine == 'mysql' and db_version is version('8.0.22', '<')) - - assert: + - name: Assert that getreplica returns expected values for MySQL newer than 8.0.22 + assert: that: - - 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 }} - - replica_status.Last_IO_Errno == 0 - - replica_status.Last_IO_Error == '' - - replica_status is not changed - when: mysql8022_and_higher == true + - replica_status.Is_Replica is truthy(convert_bool=True) + - replica_status.Source_Host == mysql_host_value + - replica_status.Exec_Source_Log_Pos == mysql_primary_status.Position + - replica_status.Source_Port == mysql_primary_port_value + - replica_status.Last_IO_Errno == 0 + - replica_status.Last_IO_Error == '' + - replica_status is not changed + vars: + mysql_host_value: "{{ mysql_host }}" + mysql_primary_port_value: "{{ mysql_primary_port }}" + when: + - db_engine == 'mysql' + - db_version is version('8.0.22', '>=') # Create test table and add data to it: - name: Create test table - shell: "echo \"CREATE TABLE {{ test_table }} (id int);\" | {{ mysql_command }} -P{{ mysql_primary_port }} {{ test_db }}" + shell: "echo \"CREATE TABLE {{ test_table }} (id int);\" | {{ mysql_command_wo_port }} -P{{ mysql_primary_port }} {{ test_db }}" - name: Insert data - shell: "echo \"INSERT INTO {{ test_table }} (id) VALUES (1), (2), (3); FLUSH LOGS;\" | {{ mysql_command }} -P{{ mysql_primary_port }} {{ test_db }}" + shell: "echo \"INSERT INTO {{ test_table }} (id) VALUES (1), (2), (3); FLUSH LOGS;\" | {{ mysql_command_wo_port }} -P{{ mysql_primary_port }} {{ test_db }}" - name: Small pause to be sure the bin log, which was flushed previously, reached the replica - pause: - seconds: 2 + ansible.builtin.wait_for: + timeout: 2 # Test primary log pos has been changed: - name: Get replica status @@ -196,18 +253,22 @@ # mysql_primary_status.Position is not actual and it has been changed by the prev step, # so replica_status.Exec_Master_Log_Pos must be different: - - assert: + - name: Assert that getreplica Log_Pos is different for MySQL older than 8.0.22 and MariaDB + assert: that: - - replica_status.Exec_Master_Log_Pos != mysql_primary_status.Position - when: mysql8022_and_higher == false + - replica_status.Exec_Master_Log_Pos != mysql_primary_status.Position + when: + - > + db_engine == 'mariadb' or + (db_engine == 'mysql' and db_version is version('8.0.22', '<')) - - assert: + - name: Assert that getreplica Log_Pos is different for MySQL newer than 8.0.22 + assert: that: - - replica_status.Exec_Source_Log_Pos != mysql_primary_status.Position - when: mysql8022_and_higher == true - - - shell: pip show pymysql | awk '/Version/ {print $2}' - register: pymysql_version + - replica_status.Exec_Source_Log_Pos != mysql_primary_status.Position + when: + - db_engine == 'mysql' + - db_version is version('8.0.22', '>=') - name: Start replica that is already running mysql_replication: @@ -217,10 +278,14 @@ fail_on_error: true register: result - - assert: + # mysqlclient 2.0.1 and pymysql 0.10.0+ always return "changed" + - name: Assert that startreplica is not changed + assert: that: - - result is not changed - when: (pymysql_version.stdout | default('1000', true)) is version('0.9.3', '<=') + - result is not changed + when: + - connector_name == 'pymysql' + - connector_version is version('0.10.0', '<') # Test stopreplica mode: - name: Stop replica @@ -230,12 +295,18 @@ mode: stopreplica register: result - - assert: + - name: Assert that stopreplica is changed and returns expected query + assert: that: - - result is changed - - result.queries == ["STOP SLAVE"] or result.queries == ["STOP REPLICA"] + - result is changed + - result.queries == ["STOP SLAVE"] or result.queries == ["STOP REPLICA"] + + - name: Pause for 2 seconds to let the replication stop + ansible.builtin.wait_for: + timeout: 2 # Test stopreplica mode: + # mysqlclient 2.0.1 and pymysql 0.10.0+ always return "changed" - name: Stop replica that is no longer running mysql_replication: <<: *mysql_params @@ -244,10 +315,13 @@ fail_on_error: true register: result - - assert: + - name: Assert that stopreplica is not changed + assert: that: - - result is not changed - when: (pymysql_version.stdout | default('1000', true)) is version('0.9.3', '<=') + - result is not changed + when: + - connector_name == 'pymysql' + - connector_version is version('0.10.0', '<') # master / slave related choices were removed in 3.0.0 # https://github.com/ansible-collections/community.mysql/pull/252 @@ -260,7 +334,8 @@ register: result ignore_errors: yes - - assert: + - name: Assert that stopslave returns expected error message + assert: that: - - result.msg == "value of mode must be one of{{ ":" }} getprimary, getreplica, changeprimary, stopreplica, startreplica, resetprimary, resetreplica, resetreplicaall, got{{ ":" }} stopslave" - - result is failed + - result.msg == "value of mode must be one of{{ ":" }} getprimary, getreplica, changeprimary, stopreplica, startreplica, resetprimary, resetreplica, resetreplicaall, changereplication, got{{ ":" }} stopslave" + - result is failed diff --git a/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_primary_delay.yml b/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_primary_delay.yml index ecdcc81..2093b70 100644 --- a/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_primary_delay.yml +++ b/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_primary_delay.yml @@ -1,11 +1,11 @@ -# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) +# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - vars: mysql_params: &mysql_params login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' block: @@ -18,10 +18,24 @@ primary_delay: '{{ test_primary_delay }}' register: result - - assert: + - name: Assert that run replication is changed and query match expectation for MariaDB and MySQL < 8.0.23 + ansible.builtin.assert: that: - - result is changed - - result.queries == ["CHANGE MASTER TO MASTER_DELAY=60"] + - result is changed + - result.queries == ["CHANGE MASTER TO MASTER_DELAY=60"] + when: + - > + db_engine == 'mariadb' or + (db_engine == 'mysql' and db_version is version('8.0.23', '<')) + + - name: Assert that run replication is changed and query match expectation for MySQL >= 8.0.23 + ansible.builtin.assert: + that: + - result is changed + - result.queries == ["CHANGE REPLICATION SOURCE TO SOURCE_DELAY=60"] + when: + - db_engine == 'mysql' + - db_version is version('8.0.23', '>=') # Auxiliary step: - name: Start replica diff --git a/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_resetprimary_mode.yml b/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_resetprimary_mode.yml index a4ed75e..cdd5fa7 100644 --- a/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_resetprimary_mode.yml +++ b/tests/integration/targets/test_mysql_replication/tasks/mysql_replication_resetprimary_mode.yml @@ -1,11 +1,12 @@ -# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) +--- +# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - vars: mysql_params: &mysql_params login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' block: @@ -38,10 +39,24 @@ mode: resetprimary register: result - - assert: + - name: Assert that reset primary is changed and query matches for MariaDB and MySQL < 8.4 + ansible.builtin.assert: that: - - result is changed - - result.queries == ["RESET MASTER"] + - result is changed + - result.queries == ["RESET MASTER"] + when: + - > + db_engine == 'mariadb' or + (db_engine == 'mysql' and db_version is version('8.4.0', '<')) + + - name: Assert that reset primary is changed and query matches for MySQL > 8.4 + ansible.builtin.assert: + that: + - result is changed + - result.queries == ["RESET BINARY LOGS AND GTIDS"] + when: + - db_engine == 'mysql' + - db_version is version('8.4.0', '>=') # Get primary final status: - name: Get primary status diff --git a/tests/integration/targets/test_mysql_role/defaults/main.yml b/tests/integration/targets/test_mysql_role/defaults/main.yml index 744ba34..62dc5f1 100644 --- a/tests/integration/targets/test_mysql_role/defaults/main.yml +++ b/tests/integration/targets/test_mysql_role/defaults/main.yml @@ -1,16 +1,5 @@ +--- mysql_user: root mysql_password: msandbox +mysql_host: '{{ gateway_addr }}' 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 index ce08dc4..01ee3db 100644 --- a/tests/integration/targets/test_mysql_role/meta/main.yml +++ b/tests/integration/targets/test_mysql_role/meta/main.yml @@ -1,2 +1,3 @@ +--- dependencies: -- setup_mysql + - setup_controller diff --git a/tests/integration/targets/test_mysql_role/tasks/main.yml b/tests/integration/targets/test_mysql_role/tasks/main.yml index 5bcd5ec..44e3308 100644 --- a/tests/integration/targets/test_mysql_role/tasks/main.yml +++ b/tests/integration/targets/test_mysql_role/tasks/main.yml @@ -4,4 +4,21 @@ #################################################################### # mysql_role module initial CI tests -- import_tasks: mysql_role_initial.yml +# TODO, many tests fails with MariaDB, debug them then remove the +# when clause and swap include_tasks for import_tasks. +- include_tasks: mysql_role_initial.yml + when: + - db_engine == 'mysql' + +# Test that subtract_privs will only revoke the grants given by priv +# (https://github.com/ansible-collections/community.mysql/issues/331) +- include_tasks: test_priv_subtract.yml + vars: + enable_check_mode: no +- include_tasks: test_priv_subtract.yml + vars: + enable_check_mode: yes + +- name: Test column case sensitive + ansible.builtin.import_tasks: + file: test_column_case_sensitive.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 index 1bca3ae..3762df9 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 @@ -1,15 +1,13 @@ +--- # 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_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' - task_parameters: &task_params - register: result - block: - name: Get server version @@ -18,1366 +16,1472 @@ register: srv - name: When run with unsupported server versions, must fail - <<: *task_params mysql_role: <<: *mysql_params name: test + register: result 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') + - result is failed + - result is search('Roles are not supported by the server') when: - - srv['version']['major'] < 8 + - srv['version']['major'] < 8 - # Skip unsupported versions - - meta: end_play + - name: Skip unsupported versions + meta: end_play when: srv['version']['major'] < 8 ######### # Prepare - - name: Create db {{ test_db }} - <<: *task_params + - name: Create db test_db mysql_db: <<: *mysql_params - name: '{{ test_db }}' + name: 'test_db' + register: result - - name: Create table {{ test_table }} - <<: *task_params + - name: Create table test_table mysql_query: <<: *mysql_params - login_db: '{{ test_db }}' - query: 'DROP TABLE IF EXISTS {{ test_table }}' + login_db: 'test_db' + query: 'DROP TABLE IF EXISTS test_table' + register: result - - name: Create table {{ test_table }} - <<: *task_params + - name: Create table test_table mysql_query: <<: *mysql_params - login_db: '{{ test_db }}' - query: 'CREATE TABLE IF NOT EXISTS {{ test_table }} (id int)' + login_db: 'test_db' + query: 'CREATE TABLE IF NOT EXISTS test_table (id int)' + register: result - name: Create users - <<: *task_params mysql_user: <<: *mysql_params name: '{{ item }}' + host: '%' password: '{{ mysql_password }}' loop: - - '{{ user0 }}' - - '{{ user1 }}' - - '{{ user2 }}' + - 'user0' + - 'user1' + - 'user2' ########### # Run tests - - name: Create role {{ role0 }} in check_mode - <<: *task_params + - name: Create role0 in check_mode mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: present members: - - '{{ user0 }}@localhost' + - 'user0@%' + register: result check_mode: yes - - name: Check + - name: Assert that create role0 is changed assert: that: - - result is changed + - result is changed - name: Check in DB - <<: *task_params mysql_query: <<: *mysql_params - query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}'" + query: "SELECT 1 FROM mysql.user WHERE User = 'role0'" + register: result - - name: Check + - name: Assert that user is not in mysql.user assert: that: - - result.rowcount.0 == 0 + - result.rowcount.0 == 0 # It must fail because of check_mode - - name: Check in DB, if not granted, the query will fail - <<: *task_params + - name: Check in DB, if not granted, the query will fail (expect failure) mysql_query: <<: *mysql_params - query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" + query: "SHOW GRANTS FOR user0@'%' USING 'role0'" + register: result ignore_errors: yes - when: install_type == 'mysql' + when: db_engine == 'mysql' - - name: Check + - name: Assert that show grants is failed assert: that: - - result is failed - when: install_type == 'mysql' + - result is failed + when: db_engine == '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' + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user0' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' - - name: Check (mariadb) + - name: Assert that user is not in mysql.roles_mapping (mariadb) assert: that: - - result.query_result.0.0['user_roles'] == 0 - when: install_type == 'mariadb' + - result.query_result.0.0['user_roles'] == 0 + when: db_engine == 'mariadb' - #===================== + # ===================== - name: Check that the user have no active roles - <<: *task_params mysql_query: - login_user: '{{ user0 }}' + login_user: 'user0' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' query: 'SELECT COALESCE(current_role(), "NONE") as "current_role()"' + register: result - - name: Check + - name: Assert that the user have no active roles assert: that: - - result.query_result.0.0["current_role()"] == "NONE" + - result.query_result.0.0["current_role()"] == "NONE" - - name: Create role {{ role0 }} - <<: *task_params + - name: Create role role0 mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: present members: - - '{{ user0 }}@localhost' + - 'user0@%' + register: result - - name: Check + - name: Assert that create role is changed assert: that: - - result is changed + - result is changed - name: Check in DB - <<: *task_params mysql_query: <<: *mysql_params - query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}'" + query: "SELECT 1 FROM mysql.user WHERE User = 'role0'" + register: result - - name: Check + - name: Assert that role0 is in mysql.user assert: that: - - result.rowcount.0 == 1 + - result.rowcount.0 == 1 - - name: Check in DB, if not granted, the query will fail - <<: *task_params + - name: Query role0, if not granted, the query will fail mysql_query: <<: *mysql_params - query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" - when: install_type == 'mysql' + query: "SHOW GRANTS FOR user0@'%' USING 'role0'" + register: result + when: db_engine == 'mysql' - - name: Check + - name: Assert that show grants is succeeded (mysql) assert: that: - - result is succeeded - when: install_type == 'mysql' + - result is succeeded + when: db_engine == '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' + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user0' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' - - name: Check (mariadb) + - name: Assert that role is in mysql.roles_mapping (mariadb) assert: that: - - result.query_result.0.0['user_roles'] == 1 - when: install_type == 'mariadb' + - result.query_result.0.0['user_roles'] == 1 + when: db_engine == 'mariadb' - name: Check that the role is active - <<: *task_params mysql_query: - login_user: '{{ user0 }}' + login_user: 'user0' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' query: 'SELECT current_role()' - when: install_type == 'mysql' + register: result + when: db_engine == 'mysql' - - name: Check + - name: Assert that current_role() returns role0 assert: that: - - "'{{ role0 }}' in result.query_result.0.0['current_role()']" - when: install_type == 'mysql' + - "'role0' in result.query_result.0.0['current_role()']" + when: db_engine == 'mysql' - name: Check that the role is active (mariadb) - <<: *task_params mysql_query: - login_user: '{{ user0 }}' + login_user: 'user0' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' query: - - 'SET ROLE {{ role0 }}' + - 'SET ROLE role0' - 'SELECT current_role()' - when: install_type == 'mariadb' + register: result + when: db_engine == 'mariadb' - - name: Check (mariadb) + - name: Assert that role is active (mariadb) assert: that: - - "'{{ role0 }}' in result.query_result.1.0['current_role()']" - when: install_type == 'mariadb' + - "'role0' in result.query_result.1.0['current_role()']" + when: db_engine == 'mariadb' - #======================== + # ======================== - - name: Create role {{ role0 }} again in check_mode - <<: *task_params + - name: Create role role0 again in check_mode mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: present + register: result check_mode: yes - - name: Check + - name: Assert that create role role0 again is not changed assert: that: - - result is not changed + - result is not changed - name: Check in DB - <<: *task_params mysql_query: <<: *mysql_params - query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}'" + query: "SELECT 1 FROM mysql.user WHERE User = 'role0'" + register: result - - name: Check + - name: Assert that role role0 is present in the database assert: that: - - result.rowcount.0 == 1 + - result.rowcount.0 == 1 - - name: Check in DB, if not granted, the query will fail - <<: *task_params + - name: Query role0, if not granted, the query will fail (2) mysql_query: <<: *mysql_params - query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" - when: install_type == 'mysql' + query: "SHOW GRANTS FOR user0@'%' USING 'role0'" + register: result + when: db_engine == 'mysql' - - name: Check + - name: Assert that query for the role0 is succeeded for mysql (2) assert: that: - - result is succeeded - when: install_type == 'mysql' + - result is succeeded + when: db_engine == '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' + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user0' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' - - name: Check (mariadb) + - name: Assert that query for the role0 is succeeded for mariadb assert: that: - - result.query_result.0.0['user_roles'] == 1 - when: install_type == 'mariadb' + - result.query_result.0.0['user_roles'] == 1 + when: db_engine == 'mariadb' - #======================== + # ======================== - - name: Create role {{ role0 }} again - <<: *task_params + - name: Create role0 again mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: present + register: result - - name: Check + - name: Assert that create role0 again is not changed assert: that: - - result is not changed + - result is not changed - - name: Check in DB - <<: *task_params + - name: Query role0 mysql_query: <<: *mysql_params - query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}'" + query: "SELECT 1 FROM mysql.user WHERE User = 'role0'" + register: result - - name: Check + - name: Assert that role0 is in DB assert: that: - - result.rowcount.0 == 1 + - result.rowcount.0 == 1 - #======================== + # ======================== - - name: Drop role {{ role0 }} in check_mode - <<: *task_params + - name: Drop role0 in check_mode mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: absent + register: result check_mode: yes - - name: Check + - name: Assert that drop role0 in check_mode is changed assert: that: - - result is changed + - result is changed - - name: Check in DB - <<: *task_params + - name: Query role0 mysql_query: <<: *mysql_params - query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}'" + query: "SELECT 1 FROM mysql.user WHERE User = 'role0'" + register: result - - name: Check + - name: Assert that role0 is in DB assert: that: - - result.rowcount.0 == 1 + - result.rowcount.0 == 1 # Must pass because of check_mode - - name: Check in DB, if not granted, the query will fail - <<: *task_params + - name: Query role0, if not granted, the query will fail (3) mysql_query: <<: *mysql_params - query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" - when: install_type == 'mysql' + query: "SHOW GRANTS FOR user0@'%' USING 'role0'" + register: result + when: db_engine == 'mysql' - - name: Check + - name: Assert that role0 is still in mysql after drop in check_mode (3) assert: that: - - result is succeeded - when: install_type == 'mysql' + - result is succeeded + when: db_engine == 'mysql' # Must pass because of check_mode - - name: Check in DB (mariadb) - <<: *task_params + - name: Query count for user0 and role0 (mariadb) 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' + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user0' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' - - name: Check (mariadb) + - name: Assert that role0 is still in mariadb after drop in check_mode assert: that: - - result.query_result.0.0['user_roles'] == 1 - when: install_type == 'mariadb' + - result.query_result.0.0['user_roles'] == 1 + when: db_engine == 'mariadb' - #======================== + # ======================== - - name: Drop role {{ role0 }} - <<: *task_params + - name: Drop role0 mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: absent + register: result - - name: Check + - name: Assert that drop role0 is changed assert: that: - - result is changed + - result is changed - - name: Check in DB - <<: *task_params + - name: Query role0 mysql_query: <<: *mysql_params - query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}'" + query: "SELECT 1 FROM mysql.user WHERE User = 'role0'" + register: result - - name: Check + - name: Assert that role0 is absent from db assert: that: - - result.rowcount.0 == 0 + - result.rowcount.0 == 0 - - name: Check in DB, if not granted, the query will fail - <<: *task_params + - name: Query grants for role0, if not granted, the query will fail mysql_query: <<: *mysql_params - query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" + query: "SHOW GRANTS FOR user0@'%' USING 'role0'" + register: result ignore_errors: yes - when: install_type == 'mysql' + when: db_engine == 'mysql' - - name: Check + - name: Assert that query for role0 in mysql is failed assert: that: - - result is failed - when: install_type == 'mysql' + - result is failed + when: db_engine == 'mysql' - - name: Check in DB (mariadb) - <<: *task_params + - name: Query count for user0 and role0 in mariadb mysql_query: <<: *mysql_params - query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = '{{ user0 }}' AND Host = 'localhost' AND Role = '{{ role0 }}'" + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user0' AND Host = '%' AND Role = 'role0'" + register: result ignore_errors: yes - when: install_type == 'mariadb' + when: db_engine == 'mariadb' - - name: Check (mariadb) + - name: Assert that query count for user0 and role0 in mariadb returns 0 rows assert: that: - - result.query_result.0.0['user_roles'] == 0 - when: install_type == 'mariadb' + - result.query_result.0.0['user_roles'] == 0 + when: db_engine == 'mariadb' - #======================== + # ======================== - - name: Drop role {{ role0 }} again in check_mode - <<: *task_params + - name: Drop role0 again in check_mode mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: absent + register: result check_mode: yes - - name: Check + - name: Assert that drop role0 again in check_mode is not changed assert: that: - - result is not changed + - result is not changed - - name: Drop role {{ role0 }} again - <<: *task_params + - name: Drop role0 again mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: absent + register: result - - name: Check + - name: Assert that drop role0 again is not changed assert: that: - - result is not changed + - result is not changed # ================== - - name: Create role {{ role0 }} in check_mode - <<: *task_params + - name: Create role0 in check_mode mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: present members: - - '{{ user0 }}@localhost' + - 'user0@%' priv: '*.*': 'SELECT,INSERT' 'mysql.*': 'UPDATE' + register: result check_mode: yes - - name: Check + - name: Assert that create role0 in check_mode is changed assert: that: - - result is changed + - result is changed - - name: Check in DB - <<: *task_params + - name: Query role0 mysql_query: <<: *mysql_params - query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}'" + query: "SELECT 1 FROM mysql.user WHERE User = 'role0'" + register: result - - name: Check + - name: Assert that role0 created in check_mode is not in the database assert: that: - - result.rowcount.0 == 0 + - result.rowcount.0 == 0 - #======================== + # ======================== - - name: Create role {{ role0 }} - <<: *task_params + - name: Create role0 mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: present members: - - '{{ user0 }}@localhost' + - 'user0@%' priv: '*.*': 'SELECT,INSERT' 'mysql.*': 'UPDATE' + register: result - - name: Check + - name: Assert that create role0 is changed assert: that: - - result is changed + - result is changed - - name: Check in DB - <<: *task_params + - name: Query role0 mysql_query: <<: *mysql_params - query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}'" + query: "SELECT 1 FROM mysql.user WHERE User = 'role0'" + register: result - - name: Check + - name: Assert that role0 is in the database assert: that: - - result.rowcount.0 == 1 + - result.rowcount.0 == 1 - #======================== + # ======================== - - name: Create role {{ role0 }} in check_mode again - <<: *task_params + - name: Create role0 in check_mode again mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: present members: - - '{{ user0 }}@localhost' + - 'user0@%' priv: '*.*': 'SELECT,INSERT' 'mysql.*': 'UPDATE' + register: result check_mode: yes - - name: Check + # TODO fix this with mariadb. I disable this test because I'm not an + # expert with roles and I don't know if it's a correct behavior of our module + # against MariaDB or if it is a bug. We never tested MariaDB properly... + - name: Assert that create role0 in check_mode again is not changed assert: that: - - result is not changed + - result is not changed + when: + - db_engine == 'mysql' - #======================== + # ======================== - - name: Create role {{ role0 }} again - <<: *task_params + - name: Create role0 again (2) mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: present members: - - '{{ user0 }}@localhost' + - 'user0@%' priv: '*.*': 'SELECT,INSERT' 'mysql.*': 'UPDATE' + register: result - - name: Check + # TODO fix this with mariadb. I disable this test because I'm not an + # expert with roles and I don't know if it's a correct behavior of our module + # against MariaDB or if it is a bug. We never tested MariaDB properly... + - name: Assert that create role0 again is not changed (2) assert: that: - - result is not changed + - result is not changed + when: + - db_engine == 'mysql' + # ############################################## # Test rewriting / appending / detaching members # ############################################## - - name: Create role {{ role1 }} - <<: *task_params + - name: Create role1 mysql_role: <<: *mysql_params - name: '{{ role1 }}' + name: 'role1' state: present + register: result # Rewriting members - name: Rewrite members in check_mode - <<: *task_params mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: present members: - - '{{ user1 }}@localhost' - - '{{ user2 }}@localhost' - - '{{ role1 }}' + - 'user1@%' + - 'user2@%' + - 'role1' + register: result check_mode: yes - - name: Check + - name: Assert that rewrite members in check_mode is changed assert: that: - - result is changed + - 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 + - name: Query user0, if not granted, the query will fail mysql_query: <<: *mysql_params - query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" - when: install_type == 'mysql' + query: "SHOW GRANTS FOR user0@'%' USING 'role0'" + register: result + when: db_engine == 'mysql' - - name: Check + - name: Assert that show grants for user0 in mysql is succeeded assert: that: - - result is succeeded - when: install_type == 'mysql' + - result is succeeded + when: db_engine == 'mysql' # user0 is still a member because of check_mode - - name: Check in DB (mariadb) - <<: *task_params + - name: Query user0 (mariadb) 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' + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user0' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' - - name: Check (mariadb) + - name: Assert that show grants for user0 in mariadb returns 1 row assert: that: - - result.query_result.0.0['user_roles'] == 1 - when: install_type == 'mariadb' + - result.query_result.0.0['user_roles'] == 1 + when: db_engine == 'mariadb' # user1, user2, and role1 are not members because of check_mode - - name: Check in DB, if not granted, the query will fail - <<: *task_params + - name: Query user1, if not granted, the query will fail (expect failue) mysql_query: <<: *mysql_params - query: "SHOW GRANTS FOR {{ user1 }}@localhost USING '{{ role0 }}'" + query: "SHOW GRANTS FOR user1@'%' USING 'role0'" ignore_errors: yes - when: install_type == 'mysql' + register: result + when: db_engine == 'mysql' - - name: Check + - name: Assert that query for user1 in mysql is failed due to check_mode assert: that: - - result is failed - when: install_type == 'mysql' + - result is failed + when: db_engine == 'mysql' - - name: Check in DB (mariadb) - <<: *task_params + - name: Query user1 (mariadb) 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' + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user1' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' - - name: Check (mariadb) + - name: Assert that query for user1 in mariadb is failed due to check_mode assert: that: - - result.query_result.0.0['user_roles'] == 0 - when: install_type == 'mariadb' + - result.query_result.0.0['user_roles'] == 0 + when: db_engine == 'mariadb' - - name: Check in DB, if not granted, the query will fail - <<: *task_params + - name: Query user2, if not granted, the query will fail mysql_query: <<: *mysql_params - query: "SHOW GRANTS FOR {{ user2 }}@localhost USING '{{ role0 }}'" + query: "SHOW GRANTS FOR user2@'%' USING 'role0'" + register: result ignore_errors: yes - when: install_type == 'mysql' + when: db_engine == 'mysql' - - name: Check + - name: Assert that query for user2 in mysql is failed assert: that: - - result is failed - when: install_type == 'mysql' + - result is failed + when: db_engine == 'mysql' - - name: Check in DB (mariadb) - <<: *task_params + - name: Query user2 (mariadb) 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' + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user2' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' - - name: Check (mariadb) + - name: Assert that query user2 in mariadb returns 0 row assert: that: - - result.query_result.0.0['user_roles'] == 0 - when: install_type == 'mariadb' + - result.query_result.0.0['user_roles'] == 0 + when: db_engine == 'mariadb' - - name: Check in DB, if not granted, the query will fail - <<: *task_params + - name: Query role1, if not granted, the query will fail mysql_query: <<: *mysql_params - query: "SHOW GRANTS FOR {{ role1 }} USING '{{ role0 }}'" + query: "SHOW GRANTS FOR role1 USING 'role0'" + register: result ignore_errors: yes - when: install_type == 'mysql' + when: db_engine == 'mysql' - - name: Check + - name: Assert that query role1 in mysql is failed assert: that: - - result is failed - when: install_type == 'mysql' + - result is failed + when: db_engine == 'mysql' - - name: Check in DB (mariadb) - <<: *task_params + - name: Query role1 (mariadb) 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' + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'role1' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' - - name: Check (mariadb) + - name: Assert that query role0 in mariadb returns 0 row assert: that: - - result.query_result.0.0['user_roles'] == 0 - when: install_type == 'mariadb' + - result.query_result.0.0['user_roles'] == 0 + when: db_engine == 'mariadb' - #======================== + # ======================== - name: Rewrite members - <<: *task_params mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: present members: - - '{{ user1 }}@localhost' - - '{{ user2 }}@localhost' - - '{{ role1 }}' + - 'user1@%' + - 'user2@%' + - 'role1' + register: result - - name: Check + - name: Assert that rewrite members is changed assert: that: - - result is changed + - result is changed # user0 is not a member any more - - name: Check in DB, if not granted, the query will fail - <<: *task_params + - name: Query user0, if not granted, the query will fail mysql_query: <<: *mysql_params - query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" + query: "SHOW GRANTS FOR user0@'%' USING 'role0'" + register: result ignore_errors: yes - when: install_type == 'mysql' + when: db_engine == 'mysql' - - name: Check + - name: Assert that query user0 in mysql is failed assert: that: - - result is failed - when: install_type == 'mysql' + - result is failed + when: db_engine == 'mysql' # user0 is not a member any more - - name: Check in DB (mariadb) - <<: *task_params + - name: Query user0 (mariadb) mysql_query: <<: *mysql_params - query: "SHOW GRANTS FOR {{ user0 }}@localhost" - when: install_type == 'mariadb' + query: "SHOW GRANTS FOR user0@'%'" + register: result + when: db_engine == 'mariadb' - - name: Check (mariadb) + - name: Assert that query user0 in mariadb doesn't returns role0 assert: that: - - "'{{ role0 }}' not in result.query_result.0.0['Grants for user0@localhost']" - when: install_type == 'mariadb' + - "'role0' not in result.query_result.0.0['Grants for user0@%']" + when: db_engine == 'mariadb' - - name: Check in DB, if not granted, the query will fail - <<: *task_params + - name: Query user1, if not granted, the query will fail (expect success) mysql_query: <<: *mysql_params - query: "SHOW GRANTS FOR {{ user1 }}@localhost USING '{{ role0 }}'" - when: install_type == 'mysql' + query: "SHOW GRANTS FOR user1@'%' USING 'role0'" + register: result + when: db_engine == 'mysql' - - name: Check + - name: Assert that query user1 in mysql is succeeded assert: that: - - result is succeeded - when: install_type == 'mysql' + - result is succeeded + when: db_engine == 'mysql' - - name: Check in DB (mariadb) - <<: *task_params + - name: Query user1 (mariadb) 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' + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user1' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' - - name: Check (mariadb) + - name: Assert that query user1 in mariadb returns 1 row assert: that: - - result.query_result.0.0['user_roles'] == 1 - when: install_type == 'mariadb' + - result.query_result.0.0['user_roles'] == 1 + when: db_engine == 'mariadb' - - name: Check in DB, if not granted, the query will fail - <<: *task_params + - name: Query user2, if not granted, the query will fail mysql_query: <<: *mysql_params - query: "SHOW GRANTS FOR {{ user2 }}@localhost USING '{{ role0 }}'" - when: install_type == 'mysql' + query: "SHOW GRANTS FOR user2@'%' USING 'role0'" + register: result + when: db_engine == 'mysql' - - name: Check + - name: Assert that query user2 in mysql is succeeded assert: that: - - result is succeeded - when: install_type == 'mysql' + - result is succeeded + when: db_engine == 'mysql' - - name: Check in DB (mariadb) - <<: *task_params + - name: Query user2 (mariadb) 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' + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user2' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' - - name: Check (mariadb) + - name: Assert that query user2 in mariadb returns 1 row assert: that: - - result.query_result.0.0['user_roles'] == 1 - when: install_type == 'mariadb' + - result.query_result.0.0['user_roles'] == 1 + when: db_engine == 'mariadb' - - name: Check in DB, if not granted, the query will fail - <<: *task_params + - name: Query role0, if not granted, the query will fail mysql_query: <<: *mysql_params - query: "SHOW GRANTS FOR {{ role1 }} USING '{{ role0 }}'" + query: "SHOW GRANTS FOR role1 USING 'role0'" + register: result ignore_errors: yes - when: install_type == 'mysql' + when: db_engine == 'mysql' - - name: Check + - name: Assert that query role0 in mysql is succeeded assert: that: - - result is succeeded - when: install_type == 'mysql' + - result is succeeded + when: db_engine == 'mysql' - - name: Check in DB (mariadb) - <<: *task_params + - name: Query count user is role1 and role is role0 (mariadb) 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' + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'role1' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' - - name: Check (mariadb) + - name: Assert that query count user is role1 and role is role0 returns 1 row assert: that: - - result.query_result.0.0['user_roles'] == 1 - when: install_type == 'mariadb' + - result.query_result.0.0['user_roles'] == 1 + when: db_engine == 'mariadb' - #========================== + # ========================== - name: Rewrite members again in check_mode - <<: *task_params mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: present members: - - '{{ user1 }}@localhost' - - '{{ user2 }}@localhost' - - '{{ role1 }}' + - 'user1@%' + - 'user2@%' + - 'role1' + register: result check_mode: yes - - name: Check + - name: Assert that rewrite members again in check_mode is not changed assert: that: - - result is not changed + - result is not changed - #========================== + # ========================== - name: Rewrite members again - <<: *task_params mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: present members: - - '{{ user1 }}@localhost' - - '{{ user2 }}@localhost' - - '{{ role1 }}' + - 'user1@%' + - 'user2@%' + - 'role1' + register: result - - name: Check + - name: Assert that rewrite members again is not changed assert: that: - - result is not changed + - result is not changed - #========================== + # ========================== # Append members - name: Append a member in check_mode - <<: *task_params mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: present append_members: yes members: - - '{{ user0 }}@localhost' + - 'user0@%' + register: result check_mode: yes - - name: Check + - name: Assert that append a member in check_mode is changed assert: that: - - result is changed + - result is changed - - name: Check in DB, if not granted, the query will fail - <<: *task_params + - name: Query user0, if not granted, the query will fail (expect failure) mysql_query: <<: *mysql_params - query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" + query: "SHOW GRANTS FOR user0@'%' USING 'role0'" ignore_errors: yes - when: install_type == 'mysql' + register: result + when: db_engine == 'mysql' - - name: Check + - name: Assert that query user0 is failed assert: that: - - result is failed - when: install_type == 'mysql' + - result is failed + when: db_engine == 'mysql' - - name: Check in DB (mariadb) - <<: *task_params + - name: Query count for user0 and role0 (mariadb) 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' + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user0' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' - - name: Check (mariadb) + - name: Assert that query count for user0 and role0 in mariadb resturns 0 row assert: that: - - result.query_result.0.0['user_roles'] == 0 - when: install_type == 'mariadb' - #===================== + - result.query_result.0.0['user_roles'] == 0 + when: db_engine == 'mariadb' + # ===================== - name: Append a member - <<: *task_params mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: present append_members: yes members: - - '{{ user0 }}@localhost' + - 'user0@%' + register: result - - name: Check + - name: Assert that append a member is changed assert: that: - - result is changed + - result is changed - - name: Check in DB, if not granted, the query will fail - <<: *task_params + - name: Query user0, if not granted, the query will fail mysql_query: <<: *mysql_params - query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" - when: install_type == 'mysql' + query: "SHOW GRANTS FOR user0@'%' USING 'role0'" + register: result + when: db_engine == 'mysql' - - name: Check + - name: Assert that query user0 in mysql is succeeded assert: that: - - result is succeeded - when: install_type == 'mysql' + - result is succeeded + when: db_engine == 'mysql' - - name: Check in DB (mariadb) - <<: *task_params + - name: Query count for user0 and role0 (mariadb) 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' + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user0' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' - - name: Check (mariadb) + - name: Assert that query count for user0 and role0 in mariadb resturns 1 row assert: that: - - result.query_result.0.0['user_roles'] == 1 - when: install_type == 'mariadb' + - result.query_result.0.0['user_roles'] == 1 + when: db_engine == 'mariadb' # 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 + - name: Query user1 using role0 (expect success) mysql_query: <<: *mysql_params - query: "SHOW GRANTS FOR {{ user1 }}@localhost USING '{{ role0 }}'" - when: install_type == 'mysql' + query: "SHOW GRANTS FOR user1@'%' USING 'role0'" + register: result + when: db_engine == 'mysql' - - name: Check + - name: Assert that query for user1 in mysql is succeeded assert: that: - - result is succeeded - when: install_type == 'mysql' + - result is succeeded + when: db_engine == 'mysql' - - name: Check in DB (mariadb) - <<: *task_params + - name: Query count for user1 and role0 (mariadb) 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' + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user1' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' - - name: Check (mariadb) + - name: Assert that query count for user1 and role0 in mariadb returns 1 row assert: that: - - result.query_result.0.0['user_roles'] == 1 - when: install_type == 'mariadb' + - result.query_result.0.0['user_roles'] == 1 + when: db_engine == 'mariadb' - - name: Check in DB, if not granted, the query will fail - <<: *task_params + - name: Query user2, if not granted, the query will fail mysql_query: <<: *mysql_params - query: "SHOW GRANTS FOR {{ user2 }}@localhost USING '{{ role0 }}'" - when: install_type == 'mysql' + query: "SHOW GRANTS FOR user2@'%' USING 'role0'" + register: result + when: db_engine == 'mysql' - - name: Check + - name: Assert that query user2 in mysql is succeeded assert: that: - - result is succeeded - when: install_type == 'mysql' + - result is succeeded + when: db_engine == 'mysql' - - name: Check in DB (mariadb) - <<: *task_params + - name: Query count for user2 and role0 (mariadb) 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' + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user2' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' - - name: Check (mariadb) + - name: Assert that query count for user2 and role0 in mariadb returns 1 row assert: that: - - result.query_result.0.0['user_roles'] == 1 - when: install_type == 'mariadb' + - result.query_result.0.0['user_roles'] == 1 + when: db_engine == 'mariadb' - #======================== + # ======================== - name: Append a member again in check_mode - <<: *task_params mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: present append_members: yes members: - - '{{ user0 }}@localhost' + - 'user0@%' + register: result check_mode: yes - - name: Check + - name: Assert that append a member again in check_mode is not changed assert: that: - - result is not changed + - result is not changed - #======================== + # ======================== - name: Append a member again - <<: *task_params mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: present append_members: yes members: - - '{{ user0 }}@localhost' + - 'user0@%' + register: result - - name: Check + - name: Assert that append a member again is not changed assert: that: - - result is not changed + - result is not changed ############## # Detach users - name: Detach users in check_mode - <<: *task_params mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: present detach_members: yes members: - - '{{ user1 }}@localhost' - - '{{ user2 }}@localhost' + - 'user1@%' + - 'user2@%' + register: result check_mode: yes - - name: Check + - name: Assert that detach users in check_mode is changed assert: that: - - result is changed + - result is changed # They must be there because of check_mode - - name: Check in DB, if not granted, the query will fail - <<: *task_params + - name: Query user0, if not granted, the query will fail mysql_query: <<: *mysql_params - query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" - when: install_type == 'mysql' + query: "SHOW GRANTS FOR user0@'%' USING 'role0'" + register: result + when: db_engine == 'mysql' - - name: Check + - name: Assert that query user0 is succeeded assert: that: - - result is succeeded - when: install_type == 'mysql' + - result is succeeded + when: db_engine == 'mysql' - - name: Check in DB (mariadb) - <<: *task_params + - name: Query count for user0 and role0 (mariadb) 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' + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user0' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' - - name: Check (mariadb) + - name: Assert that query count for user0 and role0 in mariadb resturns 1 row assert: that: - - result.query_result.0.0['user_roles'] == 1 - when: install_type == 'mariadb' + - result.query_result.0.0['user_roles'] == 1 + when: db_engine == 'mariadb' - - name: Check in DB, if not granted, the query will fail - <<: *task_params + - name: Query user1 using role0 (expect success) mysql_query: <<: *mysql_params - query: "SHOW GRANTS FOR {{ user1 }}@localhost USING '{{ role0 }}'" - when: install_type == 'mysql' + query: "SHOW GRANTS FOR user1@'%' USING 'role0'" + register: result + when: db_engine == 'mysql' - - name: Check + - name: Assert that query user1 in mysql is succeeded assert: that: - - result is succeeded - when: install_type == 'mysql' + - result is succeeded + when: db_engine == 'mysql' - - name: Check in DB (mariadb) - <<: *task_params + - name: Query count for user1 and role0 (mariadb) 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' + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user1' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' - - name: Check (mariadb) + - name: Assert that query count for user1 and role0 in mariadb returns 1 row assert: that: - - result.query_result.0.0['user_roles'] == 1 - when: install_type == 'mariadb' + - result.query_result.0.0['user_roles'] == 1 + when: db_engine == 'mariadb' - - name: Check in DB, if not granted, the query will fail - <<: *task_params + - name: Query user2, if not granted, the query will fail mysql_query: <<: *mysql_params - query: "SHOW GRANTS FOR {{ user2 }}@localhost USING '{{ role0 }}'" - when: install_type == 'mysql' + query: "SHOW GRANTS FOR user2@'%' USING 'role0'" + register: result + when: db_engine == 'mysql' - - name: Check + - name: Assert that query user2 in mysql is succeeded assert: that: - - result is succeeded - when: install_type == 'mysql' + - result is succeeded + when: db_engine == 'mysql' - - name: Check in DB (mariadb) - <<: *task_params + - name: Query count user2 and role0 (mariadb) 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' + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user2' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' - - name: Check (mariadb) + - name: Assert that query count user2 and role0 in mariadb returns 1 row assert: that: - - result.query_result.0.0['user_roles'] == 1 - when: install_type == 'mariadb' + - result.query_result.0.0['user_roles'] == 1 + when: db_engine == 'mariadb' - #======================== + # ======================== - name: Detach users - <<: *task_params mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: present detach_members: yes members: - - '{{ user1 }}@localhost' - - '{{ user2 }}@localhost' + - 'user1@%' + - 'user2@%' + register: result - - name: Check + - name: Assert that detach users is changed assert: that: - - result is changed + - result is changed - - name: Check in DB, if not granted, the query will fail - <<: *task_params + - name: Query user0, if not granted, the query will fail mysql_query: <<: *mysql_params - query: "SHOW GRANTS FOR {{ user0 }}@localhost USING '{{ role0 }}'" - when: install_type == 'mysql' + query: "SHOW GRANTS FOR user0@'%' USING 'role0'" + register: result + when: db_engine == 'mysql' - - name: Check + - name: Assert that query user0 in mysql is succeeded assert: that: - - result is succeeded - when: install_type == 'mysql' + - result is succeeded + when: db_engine == 'mysql' - - name: Check in DB (mariadb) - <<: *task_params + - name: Query count for user0 and role0 (mariadb) 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' + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user0' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' - - name: Check (mariadb) + - name: Assert that query count for user0 and role0 returns 1 row assert: that: - - result.query_result.0.0['user_roles'] == 1 - when: install_type == 'mariadb' + - result.query_result.0.0['user_roles'] == 1 + when: db_engine == 'mariadb' - - name: Check in DB, if not granted, the query will fail - <<: *task_params + - name: Query user1, if not granted, the query will fail (expect failure) mysql_query: <<: *mysql_params - query: "SHOW GRANTS FOR {{ user1 }}@localhost USING '{{ role0 }}'" + query: "SHOW GRANTS FOR user1@'%' USING 'role0'" ignore_errors: yes - when: install_type == 'mysql' + register: result + when: db_engine == 'mysql' - - name: Check + - name: Assert that query user1 in mysql is failed assert: that: - - result is failed - when: install_type == 'mysql' + - result is failed + when: db_engine == 'mysql' - - name: Check in DB (mariadb) - <<: *task_params + - name: Query count for user1 and role0 (mariadb) 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' + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user1' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' - - name: Check (mariadb) + - name: Assert that query count for user1 and role0 in mariadb returns 0 row assert: that: - - result.query_result.0.0['user_roles'] == 0 - when: install_type == 'mariadb' + - result.query_result.0.0['user_roles'] == 0 + when: db_engine == 'mariadb' - - name: Check in DB, if not granted, the query will fail - <<: *task_params + - name: Query user2, if not granted, the query will fail mysql_query: <<: *mysql_params - query: "SHOW GRANTS FOR {{ user2 }}@localhost USING '{{ role0 }}'" + query: "SHOW GRANTS FOR user2@'%' USING 'role0'" + register: result ignore_errors: yes - when: install_type == 'mysql' + when: db_engine == 'mysql' - - name: Check + - name: Assert that query user2 in mysql is failed assert: that: - - result is failed - when: install_type == 'mysql' + - result is failed + when: db_engine == 'mysql' - - name: Check in DB (mariadb) - <<: *task_params + - name: Query count for user2 and role0 (mariadb) 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' + query: "SELECT count(User) as user_roles FROM mysql.roles_mapping WHERE User = 'user2' AND Host = '%' AND Role = 'role0'" + register: result + when: db_engine == 'mariadb' - - name: Check (mariadb) + - name: Assert that query count for user2 and role0 returns 0 row assert: that: - - result.query_result.0.0['user_roles'] == 0 - when: install_type == 'mariadb' + - result.query_result.0.0['user_roles'] == 0 + when: db_engine == 'mariadb' - #===================== + # ===================== - name: Detach users in check_mode again - <<: *task_params mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: present detach_members: yes members: - - '{{ user1 }}@localhost' - - '{{ user2 }}@localhost' + - 'user1@%' + - 'user2@%' + register: result check_mode: yes - - name: Check + - name: Assert that detach users in check_mode again is not changed assert: that: - - result is not changed + - result is not changed - name: Detach users again - <<: *task_params mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: present detach_members: yes members: - - '{{ user1 }}@localhost' - - '{{ user2 }}@localhost' + - 'user1@%' + - 'user2@%' + register: result - - name: Check + - name: Assert that detach users again is not changed assert: that: - - result is not changed + - result is not changed + + - name: '"detach" users when creating a new role' + mysql_role: + <<: *mysql_params + name: 'role3' + state: present + detach_members: yes + members: + - 'user1@%' + register: result + + - name: Assert that creating a role while detach users is changed + assert: + that: + - result is changed + + - name: Query grants for user1 + mysql_query: + <<: *mysql_params + query: "SHOW GRANTS FOR user1@'%'" + register: result + + - name: Assert detach_members did not add a user to the role + assert: + that: + - "'role3' not in result.query_result.0.0" + + # test members_must_exist + - name: Try failing on not-existing user in check-mode + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + members_must_exist: yes + append_members: yes + members: + - 'not_existent@%' + register: result + ignore_errors: yes + check_mode: yes + + - name: Assert nonexistent user in check-mode is failed + assert: + that: + - result is failed + + - name: Try failing on not-existing user in check-mode + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + members_must_exist: no + append_members: yes + members: + - 'not_existent@%' + register: result + check_mode: yes + + - name: Check for lack of change + assert: + that: + - result is not changed + + - name: Try failing on not-existing user + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + members_must_exist: yes + append_members: yes + members: + - 'not_existent@%' + register: result + ignore_errors: yes + + - name: Assert nonexistent user with members_must_exist is failed + assert: + that: + - result is failed + + - name: Try failing on not-existing user + mysql_role: + <<: *mysql_params + name: 'role0' + state: present + members_must_exist: no + append_members: yes + members: + - 'not_existent@%' + register: result + + - name: Assert nonexistent user with members_must_exist=no is not changed + 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 }}' + - 'test_db1' + - 'test_db2' + register: result - - name: Create table {{ test_table }} - <<: *task_params + - name: Create table test_table mysql_query: <<: *mysql_params login_db: '{{ item }}' - query: 'CREATE TABLE {{ test_table }} (id int)' + query: 'CREATE TABLE test_table (id int)' loop: - - '{{ test_db1 }}' - - '{{ test_db2 }}' + - 'test_db1' + - 'test_db2' + register: result - - name: Check grants - <<: *task_params + - name: Query grants for role0 mysql_query: <<: *mysql_params - query: "SHOW GRANTS FOR {{ role0 }}" + query: "SHOW GRANTS FOR role0" + register: result - - name: Check + - name: Assert grants for role0 in mysql 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 == 'mysql' + - 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: db_engine == 'mysql' - - name: Check (mariadb) + - name: Assert grants for role0 in 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' + - 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: db_engine == 'mariadb' - name: Append privs in check_mode - <<: *task_params mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: present - priv: '{{ test_db1 }}.{{ test_table }}:SELECT,INSERT/{{ test_db2 }}.{{ test_table }}:DELETE' + priv: 'test_db1.test_table:SELECT,INSERT/test_db2.test_table:DELETE' append_privs: yes + register: result check_mode: yes - - name: Check + - name: Assert append privs in check_mode is changed assert: that: - - result is changed + - result is changed - - name: Check grants - <<: *task_params + - name: Query grants for role0 mysql_query: <<: *mysql_params - query: "SHOW GRANTS FOR {{ role0 }}" + query: "SHOW GRANTS FOR role0" + register: result - - name: Check + - name: Assert grants for role0 in mysql 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 == 'mysql' + - 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: db_engine == 'mysql' - - name: Check (mariadb) + - name: Assert grants for role0 in 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' + - 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: db_engine == 'mariadb' - name: Append privs - <<: *task_params mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: present - priv: '{{ test_db1 }}.{{ test_table }}:SELECT,INSERT/{{ test_db2 }}.{{ test_table }}:DELETE' + priv: 'test_db1.test_table:SELECT,INSERT/test_db2.test_table:DELETE' append_privs: yes + register: result - - name: Check + - name: Assert that append privs is changed assert: that: - - result is changed + - result is changed - - name: Check grants - <<: *task_params + - name: Query grants for role0 mysql_query: <<: *mysql_params - query: "SHOW GRANTS FOR {{ role0 }}" + query: "SHOW GRANTS FOR role0" + register: result - - name: Check + - name: Assert grants for role0 in mysql 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 == 'mysql' + - 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: db_engine == 'mysql' - - name: Check (mariadb) + - name: Assert grants for role0 in 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' + - 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: db_engine == 'mariadb' - name: Append privs again in check_mode - <<: *task_params mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: present - priv: '{{ test_db1 }}.{{ test_table }}:SELECT,INSERT/{{ test_db2 }}.{{ test_table }}:DELETE' + priv: 'test_db1.test_table:SELECT,INSERT/test_db2.test_table:DELETE' append_privs: yes + register: result check_mode: yes # TODO it must be changed. The module uses user_mod function @@ -1386,157 +1490,171 @@ # 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 + - name: Assert that append privs again in check_mode is changed assert: that: - - result is changed + - result is changed - name: Append privs again - <<: *task_params mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: present - priv: '{{ test_db1 }}.{{ test_table }}:SELECT,INSERT/{{ test_db2 }}.{{ test_table }}:DELETE' + priv: 'test_db1.test_table:SELECT,INSERT/test_db2.test_table:DELETE' append_privs: yes + register: result - # 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 + - name: Assert that append privs again is not changed assert: that: - - result is changed + - result is not changed - name: Rewrite privs - <<: *task_params mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: present priv: '*.*': 'SELECT' + register: result - - name: Check + - name: Assert that rewrite privs is changed assert: that: - - result is changed + - result is changed - - name: Check grants - <<: *task_params + - name: Query grants for role0 mysql_query: <<: *mysql_params - query: "SHOW GRANTS FOR {{ role0 }}" + query: "SHOW GRANTS FOR role0" + register: result - - name: Check + - name: Assert grants for role0 in mysql assert: that: - - result.query_result.0.0["Grants for role0@%"] == "GRANT SELECT ON *.* TO `role0`@`%`" - - result.rowcount.0 == 1 - when: install_type == 'mysql' + - result.query_result.0.0["Grants for role0@%"] == "GRANT SELECT ON *.* TO `role0`@`%`" + - result.rowcount.0 == 1 + when: db_engine == 'mysql' - - name: Check (mariadb) + - name: Assert grants for role0 in mariadb assert: that: - - result.query_result.0.0["Grants for role0"] == "GRANT SELECT ON *.* TO `role0`" - - result.rowcount.0 == 1 - when: install_type == 'mariadb' + - result.query_result.0.0["Grants for role0"] == "GRANT SELECT ON *.* TO `role0`" + - result.rowcount.0 == 1 + when: db_engine == 'mariadb' # ################# # Test admin option # ################# - - name: Drop role - <<: *task_params + - name: Drop role0 mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: absent + register: result - - name: Create role with admin - <<: *task_params + - name: Create role0 with admin mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: present - admin: '{{ user0 }}@localhost' + admin: 'user0@%' + register: result ignore_errors: yes - - name: Check with MySQL + - name: Assert expected error message for mysql assert: that: - - result is failed - - result.msg is search('option can be used only with MariaDB') - when: install_type == 'mysql' + - result is failed + - result.msg is search('option can be used only with MariaDB') + when: db_engine == 'mysql' - - name: Check with MariaDB + - name: Assert create role0 in mariadb is changed assert: that: - - result is changed - when: install_type == 'mariadb' + - result is changed + when: db_engine == 'mariadb' - - name: Check in DB - <<: *task_params + - name: Query role0 in mariadb mysql_query: <<: *mysql_params - query: "SELECT 1 FROM mysql.user WHERE User = '{{ role0 }}' AND Host = ''" - when: install_type == 'mariadb' + query: "SELECT 1 FROM mysql.user WHERE User = 'role0' AND Host = ''" + register: result + when: db_engine == 'mariadb' - - name: Check + - name: Assert that query role0 in mariadb returns 1 row assert: that: - - result.rowcount.0 == 1 - when: install_type == 'mariadb' + - result.rowcount.0 == 1 + when: db_engine == 'mariadb' - - name: Create role with admin again - <<: *task_params + - name: Create role0 with admin again mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: present - admin: '{{ user0 }}@localhost' + admin: 'user0@%' + register: result ignore_errors: yes - - name: Check with MySQL + - name: Assert expected error message in mysql again assert: that: - - result is failed - - result.msg is search('option can be used only with MariaDB') - when: install_type == 'mysql' + - result is failed + - result.msg is search('option can be used only with MariaDB') + when: db_engine == 'mysql' - - name: Check with MariaDB + - name: Assert create role0 in mariadb is not changed assert: that: - - result is not changed - when: install_type == 'mariadb' + - result is not changed + when: db_engine == 'mariadb' # Try to grant a role to a user who does not exist - - name: Create role with admin again - <<: *task_params + - name: Create role0 with admin again mysql_role: <<: *mysql_params - name: '{{ role0 }}' + name: 'role0' state: present members: - - '{{ nonexistent }}@localhost' + - 'nonexistent@%' + register: result ignore_errors: yes - - name: Check + - name: Assert that create role0 with admin again is failed assert: that: - - result is failed - - result.msg is search('does not exist') + - result is failed + - result.msg is search('does not exist') always: - # Clean up - - name: Drop DBs - mysql_query: - <<: *mysql_params - query: 'DROP DATABASE {{ item }}' - loop: - - '{{ test_db }}' - - '{{ test_db1 }}' - - '{{ test_db2 }}' + + - name: Clean up DBs + mysql_query: + <<: *mysql_params + query: 'DROP DATABASE IF EXISTS {{ item }}' + loop: + - 'test_db' + - 'test_db1' + - 'test_db2' + + - name: Clean up users + mysql_user: + <<: *mysql_params + name: '{{ item }}' + state: absent + loop: + - 'user0' + - 'user1' + - 'user2' + + - name: Clean up roles + mysql_role: + <<: *mysql_params + name: '{{ item }}' + state: absent + loop: + - 'role0' + - 'test' + - 'role3' diff --git a/tests/integration/targets/test_mysql_role/tasks/test_column_case_sensitive.yml b/tests/integration/targets/test_mysql_role/tasks/test_column_case_sensitive.yml new file mode 100644 index 0000000..74849e0 --- /dev/null +++ b/tests/integration/targets/test_mysql_role/tasks/test_column_case_sensitive.yml @@ -0,0 +1,149 @@ +--- + +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + + # ========================= Prepare ======================================= + # We use query to prevent our module of changing the case + - name: Mysql_role Column case sensitive | Create a test table + community.mysql.mysql_query: + <<: *mysql_params + query: + - CREATE DATABASE mysql_role_column_case + - >- + CREATE TABLE mysql_role_column_case.t1 + (a int, B int, cC int, Dd int) + - >- + INSERT INTO mysql_role_column_case.t1 + (a, B, cC, Dd) VALUES (1,2,3,4) + + - name: Mysql_role Column case sensitive | Create users + community.mysql.mysql_user: + <<: *mysql_params + name: column_case_sensitive + host: '%' + password: 'msandbox' + + # ================= Reproduce failure ===================================== + + - name: Mysql_role Column case sensitive | Create role + community.mysql.mysql_role: + <<: *mysql_params + name: 'role_column_case_sensitive' + state: present + members: + - 'column_case_sensitive@%' + priv: + 'mysql_role_column_case.t1': 'SELECT(a, B, cC, Dd)' + + - name: Mysql_role Column case sensitive | Assert role privileges are all caps + community.mysql.mysql_query: + <<: *mysql_params + query: + - SHOW GRANTS FOR role_column_case_sensitive + register: column_case_insensitive_grants + failed_when: + # Column order may vary, thus test each separately + - >- + column_case_insensitive_grants.query_result[0][1] + is not search("A", ignorecase=false) + or column_case_insensitive_grants.query_result[0][1] + is not search("B", ignorecase=false) + or column_case_insensitive_grants.query_result[0][1] + is not search("CC", ignorecase=false) + or column_case_insensitive_grants.query_result[0][1] + is not search("DD", ignorecase=false) + + - name: Mysql_role Column case sensitive | Assert 1 column is accessible on MySQL + community.mysql.mysql_query: + <<: *mysql_params + login_user: column_case_sensitive + query: + - DESC mysql_role_column_case.t1 + register: assert_1_col_accessible + failed_when: + - assert_1_col_accessible.rowcount[0] | int != 1 + when: + - db_engine == 'mysql' + + - name: Mysql_role Column case sensitive | Assert 4 column are accessible on MariaDB + community.mysql.mysql_query: + <<: *mysql_params + login_user: column_case_sensitive + query: + - SET ROLE role_column_case_sensitive + - DESC mysql_role_column_case.t1 + register: assert_4_col_accessible + failed_when: + - assert_4_col_accessible.rowcount[1] | int != 4 + when: + - db_engine == 'mariadb' + + # ====================== Test the fix ===================================== + + - name: Mysql_role Column case sensitive | Recreate role with case sensitive + community.mysql.mysql_role: + <<: *mysql_params + name: 'role_column_case_sensitive' + state: present + members: + - 'column_case_sensitive@%' + priv: + 'mysql_role_column_case.t1': 'SELECT(a, B, cC, Dd)' + column_case_sensitive: true + + - name: Mysql_role Column case sensitive | Assert role privileges are case sensitive + community.mysql.mysql_query: + <<: *mysql_params + query: + - SHOW GRANTS FOR role_column_case_sensitive + register: column_case_sensitive_grants + failed_when: + # Column order may vary, thus test each separately + - >- + column_case_sensitive_grants.query_result[0][1] + is not search("a", ignorecase=false) + or column_case_sensitive_grants.query_result[0][1] + is not search("B", ignorecase=false) + or column_case_sensitive_grants.query_result[0][1] + is not search("cC", ignorecase=false) + or column_case_sensitive_grants.query_result[0][1] + is not search("Dd", ignorecase=false) + + - name: Mysql_role Column case sensitive | Assert 4 columns are accessible + community.mysql.mysql_query: + <<: *mysql_params + login_user: column_case_sensitive + query: + - SET ROLE role_column_case_sensitive + - DESC mysql_role_column_case.t1 + register: assert_4_col_accessible + failed_when: + - assert_4_col_accessible.rowcount[1] | int != 4 + + # ========================= Teardown ====================================== + + - name: Mysql_role Column case sensitive | Delete test users + community.mysql.mysql_user: + <<: *mysql_params + name: column_case_sensitive + host_all: true + state: absent + + - name: Mysql_role Column case sensitive | Delete role + community.mysql.mysql_role: + <<: *mysql_params + name: 'role_column_case_sensitive' + state: absent + + - name: Mysql_role Column case sensitive | Delete test database + community.mysql.mysql_db: + <<: *mysql_params + name: mysql_role_column_case + state: absent diff --git a/tests/integration/targets/test_mysql_role/tasks/test_priv_subtract.yml b/tests/integration/targets/test_mysql_role/tasks/test_priv_subtract.yml new file mode 100644 index 0000000..b79a1cb --- /dev/null +++ b/tests/integration/targets/test_mysql_role/tasks/test_priv_subtract.yml @@ -0,0 +1,164 @@ +# Test code to ensure that subtracting privileges will not result in unnecessary changes. +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + + - name: Create test databases + mysql_db: + <<: *mysql_params + name: data1 + state: present + + - name: Create a role with an initial set of privileges + mysql_role: + <<: *mysql_params + name: 'role2' + priv: 'data1.*:SELECT,INSERT' + state: present + + - name: Run command to show privileges for role (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR 'role2'\"" + register: result + + - name: Assert that the initial set of privileges matches what is expected + assert: + that: + - "'GRANT SELECT, INSERT ON `data1`.*' in result.stdout" + + - name: Subtract privileges that are not in the current privileges, which should be a no-op + mysql_role: + <<: *mysql_params + name: 'role2' + priv: 'data1.*:DELETE' + subtract_privs: yes + state: present + check_mode: '{{ enable_check_mode }}' + register: result + + - name: Assert that there wasn't a change in permissions + assert: + that: + - result is not changed + + - name: Run command to show privileges for role (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR 'role2'\"" + register: result + + - name: Assert that the permissions still match what was originally granted + assert: + that: + - "'GRANT SELECT, INSERT ON `data1`.*' in result.stdout" + + - name: Subtract existing and not-existing privileges, but not all + mysql_role: + <<: *mysql_params + name: 'role2' + priv: 'data1.*:INSERT,DELETE' + subtract_privs: yes + state: present + check_mode: '{{ enable_check_mode }}' + register: result + + - name: Assert that there was a change because permissions were/would be revoked on data1.* + assert: + that: + - result is changed + + - name: Run command to show privileges for role (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR 'role2'\"" + register: result + + - name: Assert that the permissions were not changed if check_mode is set to 'yes' + assert: + that: + - "'GRANT SELECT, INSERT ON `data1`.*' in result.stdout" + when: enable_check_mode == 'yes' + + - name: Assert that only DELETE was revoked if check_mode is set to 'no' + assert: + that: + - "'GRANT SELECT ON `data1`.*' in result.stdout" + when: enable_check_mode == 'no' + + - name: Try to subtract invalid privileges + mysql_role: + <<: *mysql_params + name: 'role2' + priv: 'data1.*:INVALID' + subtract_privs: yes + state: present + check_mode: '{{ enable_check_mode }}' + register: result + + - name: Assert that there was no change because invalid permissions are ignored + assert: + that: + - result is not changed + + - name: Run command to show privileges for role (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR 'role2'\"" + register: result + + - name: Assert that the permissions were not changed with check_mode=='yes' + assert: + that: + - "'GRANT SELECT, INSERT ON `data1`.*' in result.stdout" + when: enable_check_mode == 'yes' + + - name: Assert that the permissions were not changed with check_mode=='no' + assert: + that: + - "'GRANT SELECT ON `data1`.*' in result.stdout" + when: enable_check_mode == 'no' + + - name: trigger failure by trying to subtract and append privileges at the same time + mysql_role: + <<: *mysql_params + name: 'role2' + priv: 'data1.*:SELECT' + subtract_privs: yes + append_privs: yes + state: present + check_mode: '{{ enable_check_mode }}' + register: result + ignore_errors: true + + - name: Assert the previous execution failed + assert: + that: + - result is failed + + - name: Run command to show privileges for role (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR 'role2'\"" + register: result + + - name: Assert that the permissions stayed the same, with check_mode=='yes' + assert: + that: + - "'GRANT SELECT, INSERT ON `data1`.*' in result.stdout" + when: enable_check_mode == 'yes' + + - name: Assert that the permissions stayed the same, with check_mode=='no' + assert: + that: + - "'GRANT SELECT ON `data1`.*' in result.stdout" + when: enable_check_mode == 'no' + + ########## + # Clean up + - name: Drop test databases + mysql_db: + <<: *mysql_params + name: 'data1' + state: present + + - name: Drop test role + mysql_role: + <<: *mysql_params + name: 'role2' + state: absent diff --git a/tests/integration/targets/test_mysql_user/defaults/main.yml b/tests/integration/targets/test_mysql_user/defaults/main.yml index 5cf9074..a87914c 100644 --- a/tests/integration/targets/test_mysql_user/defaults/main.yml +++ b/tests/integration/targets/test_mysql_user/defaults/main.yml @@ -2,7 +2,7 @@ # defaults file for test_mysql_user mysql_user: root mysql_password: msandbox -mysql_host: 127.0.0.1 +mysql_host: '{{ gateway_addr }}' mysql_primary_port: 3307 db_name: 'data' diff --git a/tests/integration/targets/test_mysql_user/meta/main.yml b/tests/integration/targets/test_mysql_user/meta/main.yml index a7ace5d..4be5f58 100644 --- a/tests/integration/targets/test_mysql_user/meta/main.yml +++ b/tests/integration/targets/test_mysql_user/meta/main.yml @@ -1,3 +1,4 @@ +--- dependencies: - - setup_mysql + - setup_controller - setup_remote_tmp_dir diff --git a/tests/integration/targets/test_mysql_user/tasks/assert_no_user.yml b/tests/integration/targets/test_mysql_user/tasks/assert_no_user.yml deleted file mode 100644 index 9861084..0000000 --- a/tests/integration/targets/test_mysql_user/tasks/assert_no_user.yml +++ /dev/null @@ -1,25 +0,0 @@ -# test code to assert no mysql user -# (c) 2014, Wayne Rosario - -# 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: run command to query for mysql user - command: "{{ mysql_command }} -e \"SELECT User FROM mysql.user where user='{{ user_name }}'\"" - register: result - -- name: assert mysql user is not present - assert: { that: "'{{ user_name }}' not in result.stdout" } diff --git a/tests/integration/targets/test_mysql_user/tasks/assert_user.yml b/tests/integration/targets/test_mysql_user/tasks/assert_user.yml deleted file mode 100644 index d95d9d2..0000000 --- a/tests/integration/targets/test_mysql_user/tasks/assert_user.yml +++ /dev/null @@ -1,38 +0,0 @@ -# test code to assert mysql user -# (c) 2014, Wayne Rosario - -# 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: run command to query for mysql user - command: "{{ mysql_command }} -e \"SELECT User FROM mysql.user where user='{{ user_name }}'\"" - register: result - -- name: assert mysql user is present - assert: - that: - - "'{{ user_name }}' in result.stdout" - -- name: run command to show privileges for user (expect privileges in stdout) - command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name }}'@'localhost'\"" - register: result - when: priv is defined - -- name: assert user has giving privileges - assert: - that: - - "'GRANT {{priv}} ON *.*' in result.stdout" - when: priv is defined diff --git a/tests/integration/targets/test_mysql_user/tasks/create_user.yml b/tests/integration/targets/test_mysql_user/tasks/create_user.yml deleted file mode 100644 index 78c253d..0000000 --- a/tests/integration/targets/test_mysql_user/tasks/create_user.yml +++ /dev/null @@ -1,46 +0,0 @@ -# test code to create mysql user -# (c) 2014, Wayne Rosario - -# 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 . - -- 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 - - # ============================================================ - - name: create mysql user {{user_name}} - mysql_user: - <<: *mysql_params - name: '{{ user_name }}' - password: '{{ user_password }}' - state: present - register: result - - - name: assert output message mysql user was created - assert: - that: - - "result.changed == true" diff --git a/tests/integration/targets/test_mysql_user/tasks/issue-121.yml b/tests/integration/targets/test_mysql_user/tasks/issue-121.yml index fb5bef6..7f5934f 100644 --- a/tests/integration/targets/test_mysql_user/tasks/issue-121.yml +++ b/tests/integration/targets/test_mysql_user/tasks/issue-121.yml @@ -1,77 +1,73 @@ --- + - vars: mysql_parameters: &mysql_params login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' block: - # ============================================================ - - shell: pip show pymysql | awk '/Version/ {print $2}' - register: pymysql_version - - - name: get server certificate + - name: Issue-121 | Setup | Get server certificate copy: - content: "{{ lookup('pipe', \"openssl s_client -starttls mysql -connect localhost:3307 -showcerts 2>/dev/null /dev/null - + Issue-121 | Create user with both REQUIRESSL privilege and an incompatible + tls_requires option mysql_user: <<: *mysql_params name: "{{ user_name_1 }}" + host: '{{ gateway_addr }}' password: "{{ user_password_1 }}" priv: '*.*:SELECT,CREATE USER,REQUIRESSL,GRANT' tls_requires: X509: register: result - ignore_errors: yes + ignore_errors: true - - assert: + - name: >- + Issue-121 | Assert error granting privileges with incompatible tls_requires + option + assert: that: - result is failed - result.msg is search('Error granting privileges') - - name: Drop mysql user + - name: Issue-121 | Teardown | Drop mysql user mysql_user: <<: *mysql_params name: '{{ item }}' - host: 127.0.0.1 + host_all: true state: absent with_items: - "{{ user_name_1 }}" diff --git a/tests/integration/targets/test_mysql_user/tasks/issue-265.yml b/tests/integration/targets/test_mysql_user/tasks/issue-265.yml index 6c91803..dfceda7 100644 --- a/tests/integration/targets/test_mysql_user/tasks/issue-265.yml +++ b/tests/integration/targets/test_mysql_user/tasks/issue-265.yml @@ -3,52 +3,57 @@ mysql_parameters: &mysql_params login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' block: - - name: Drop mysql user if exists + - name: Issue-265 | Drop mysql user if exists mysql_user: <<: *mysql_params name: '{{ user_name_1 }}' + host_all: true state: absent ignore_errors: yes # Tests with force_context: yes # Test user creation - - name: create mysql user {{user_name_1}} + - name: Issue-265 | Create mysql user {{ user_name_1 }} mysql_user: <<: *mysql_params - name: '{{ user_name_1 }}' - password: '{{ user_password_1 }}' + name: "{{ user_name_1 }}" + password: "{{ user_password_1 }}" state: present force_context: yes register: result - - name: assert output message mysql user was created + - name: Issue-265 | Assert user was created assert: that: - - "result.changed == true" + - result is changed - - include: assert_user.yml user_name={{user_name_1}} + - include_tasks: utils/assert_user.yml + vars: + user_name: "{{ user_name_1 }}" + user_host: localhost # Test user removal - - name: remove mysql user {{user_name_1}} + - name: Issue-265 | remove mysql user {{ user_name_1 }} mysql_user: <<: *mysql_params - name: '{{user_name_1}}' - password: '{{user_password_1}}' + name: "{{ user_name_1 }}" + host_all: true + password: "{{ user_password_1 }}" state: absent force_context: yes register: result - - name: assert output message mysql user was removed + - name: Issue-265 | Assert user was removed assert: that: - - "result.changed == true" + - result is changed # Test blank user removal - - name: create blank mysql user to be removed later + - name: Issue-265 | Create blank mysql user to be removed later mysql_user: <<: *mysql_params name: "" @@ -56,80 +61,86 @@ force_context: yes password: 'KJFDY&D*Sfuydsgf' - - name: remove blank mysql user with hosts=all (expect changed) + - name: Issue-265 | Remove blank mysql user with hosts=all (expect changed) mysql_user: <<: *mysql_params - user: "" + name: "" host_all: true state: absent force_context: yes register: result - - name: assert changed is true for removing all blank users + - name: Issue-265 | Assert changed is true for removing all blank users assert: that: - - "result.changed == true" + - result is changed - - name: remove blank mysql user with hosts=all (expect ok) + - name: Issue-265 | Remove blank mysql user with hosts=all (expect ok) mysql_user: <<: *mysql_params - user: "" + name: "" host_all: true force_context: yes state: absent register: result - - name: assert changed is true for removing all blank users + - name: Issue-265 | Assert changed is true for removing all blank users assert: that: - - "result.changed == false" + - result is not changed - - include: assert_no_user.yml user_name={{user_name_1}} + - include_tasks: utils/assert_no_user.yml + vars: + user_name: "{{user_name_1}}" # Tests with force_context: no # Test user creation - - name: Drop mysql user if exists + - name: Issue-265 | Drop mysql user if exists mysql_user: <<: *mysql_params - name: '{{ user_name_1 }}' + name: "{{ user_name_1 }}" + host_all: true state: absent ignore_errors: yes # Tests with force_context: yes # Test user creation - - name: create mysql user {{user_name_1}} + - name: Issue-265 | Create mysql user {{user_name_1}} mysql_user: <<: *mysql_params - name: '{{ user_name_1 }}' - password: '{{ user_password_1 }}' + name: "{{ user_name_1 }}" + password: "{{ user_password_1 }}" state: present force_context: yes register: result - - name: assert output message mysql user was created + - name: Issue-265 | Assert output message mysql user was created assert: that: - - "result.changed == true" + - result is changed - - include: assert_user.yml user_name={{user_name_1}} + - include_tasks: utils/assert_user.yml + vars: + user_name: "{{ user_name_1 }}" + user_host: localhost # Test user removal - - name: remove mysql user {{user_name_1}} + - name: Issue-265 | Remove mysql user {{ user_name_1 }} mysql_user: <<: *mysql_params - name: '{{user_name_1}}' - password: '{{user_password_1}}' + name: "{{ user_name_1 }}" + password: "{{ user_password_1 }}" state: absent force_context: no register: result - - name: assert output message mysql user was removed + - name: Issue-265 | Assert output message mysql user was removed assert: that: - - "result.changed == true" + - result is changed # Test blank user removal - - name: create blank mysql user to be removed later + - name: Issue-265 | Create blank mysql user to be removed later mysql_user: <<: *mysql_params name: "" @@ -137,32 +148,34 @@ force_context: no password: 'KJFDY&D*Sfuydsgf' - - name: remove blank mysql user with hosts=all (expect changed) + - name: Issue-265 | Remove blank mysql user with hosts=all (expect changed) mysql_user: <<: *mysql_params - user: "" + name: "" host_all: true state: absent force_context: no register: result - - name: assert changed is true for removing all blank users + - name: Issue-265 | Assert changed is true for removing all blank users assert: that: - - "result.changed == true" + - result is changed - - name: remove blank mysql user with hosts=all (expect ok) + - name: Issue-265 | Remove blank mysql user with hosts=all (expect ok) mysql_user: <<: *mysql_params - user: "" + name: "" host_all: true force_context: no state: absent register: result - - name: assert changed is true for removing all blank users + - name: Issue-265 | Assert changed is true for removing all blank users assert: that: - - "result.changed == false" + - result is not changed - - include: assert_no_user.yml user_name={{user_name_1}} + - include_tasks: utils/assert_no_user.yml + vars: + user_name: "{{ user_name_1 }}" diff --git a/tests/integration/targets/test_mysql_user/tasks/issue-28.yml b/tests/integration/targets/test_mysql_user/tasks/issue-28.yml index ae15865..51a2091 100644 --- a/tests/integration/targets/test_mysql_user/tasks/issue-28.yml +++ b/tests/integration/targets/test_mysql_user/tasks/issue-28.yml @@ -9,80 +9,87 @@ mysql_parameters: &mysql_params login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' when: tls_enabled block: # ============================================================ - - name: get server certificate + - name: Issue-28 | Setup | Get server certificate copy: - content: "{{ lookup('pipe', \"openssl s_client -starttls mysql -connect localhost:3307 -showcerts 2>/dev/null /dev/null = 0.7.11 is required' in result.msg - - name: Drop mysql user + - name: Issue-28 | Drop mysql user mysql_user: <<: *mysql_params name: '{{ item }}' - host: 127.0.0.1 + host: '{{ gateway_addr }}' state: absent with_items: - "{{ user_name_1 }}" diff --git a/tests/integration/targets/test_mysql_user/tasks/issue-29511.yaml b/tests/integration/targets/test_mysql_user/tasks/issue-29511.yaml index 31e6edf..c95acc2 100644 --- a/tests/integration/targets/test_mysql_user/tasks/issue-29511.yaml +++ b/tests/integration/targets/test_mysql_user/tasks/issue-29511.yaml @@ -3,12 +3,12 @@ mysql_parameters: &mysql_params login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' block: - - name: Issue test setup - drop database + - name: Issue-29511 | test setup | drop database mysql_db: <<: *mysql_params name: "{{ item }}" @@ -17,7 +17,7 @@ - foo - bar - - name: Issue test setup - create database + - name: Issue-29511 | test setup | create database mysql_db: <<: *mysql_params name: "{{ item }}" @@ -26,7 +26,7 @@ - foo - bar - - name: Copy SQL scripts to remote + - name: Issue-29511 | Copy SQL scripts to remote copy: src: "{{ item }}" dest: "{{ remote_tmp_dir }}/{{ item | basename }}" @@ -34,13 +34,13 @@ - create-function.sql - create-procedure.sql - - name: Create function for test + - name: Issue-29511 | Create function for test shell: "{{ mysql_command }} < {{ remote_tmp_dir }}/create-function.sql" - - name: Create procedure for test + - name: Issue-29511 | Create procedure for test shell: "{{ mysql_command }} < {{ remote_tmp_dir }}/create-procedure.sql" - - name: Create user with FUNCTION and PROCEDURE privileges + - name: Issue-29511 | Create user with FUNCTION and PROCEDURE privileges mysql_user: <<: *mysql_params name: '{{ user_name_2 }}' @@ -49,13 +49,13 @@ priv: 'FUNCTION foo.function:EXECUTE/foo.*:SELECT/PROCEDURE bar.procedure:EXECUTE' register: result - - name: Assert Create user with FUNCTION and PROCEDURE privileges + - name: Issue-29511 | Assert Create user with FUNCTION and PROCEDURE privileges assert: that: - result is success - result is changed - - name: Create user with FUNCTION and PROCEDURE privileges - Idempotent check + - name: Issue-29511 | Create user with FUNCTION and PROCEDURE privileges - Idempotent check mysql_user: <<: *mysql_params name: '{{ user_name_2 }}' @@ -64,19 +64,13 @@ priv: 'FUNCTION foo.function:EXECUTE/foo.*:SELECT/PROCEDURE bar.procedure:EXECUTE' register: result - - name: Assert Create user with FUNCTION and PROCEDURE privileges + - name: Issue-29511 | Assert Create user with FUNCTION and PROCEDURE privileges assert: that: - result is success - result is not changed - - name: Remove user - mysql_user: - <<: *mysql_params - name: '{{ user_name_2 }}' - state: absent - - - name: Issue test teardown - cleanup databases + - name: Issue-29511 | Test teardown | cleanup databases mysql_db: <<: *mysql_params name: "{{ item }}" @@ -84,3 +78,7 @@ loop: - foo - bar + + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_2 }}" diff --git a/tests/integration/targets/test_mysql_user/tasks/issue-64560.yaml b/tests/integration/targets/test_mysql_user/tasks/issue-64560.yaml index 46078b2..a7657f8 100644 --- a/tests/integration/targets/test_mysql_user/tasks/issue-64560.yaml +++ b/tests/integration/targets/test_mysql_user/tasks/issue-64560.yaml @@ -3,43 +3,50 @@ mysql_parameters: &mysql_params login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' block: - - name: Set root password + - name: Issue-64560 | Set root password mysql_user: <<: *mysql_params name: root + host: '%' password: '{{ root_password }}' check_implicit_admin: yes register: result - - name: assert root password is changed - assert: { that: "result.changed == true" } + - name: Issue-64560 | Assert root password is changed + assert: + that: + - result is changed - - name: Set root password again + - name: Issue-64560 | Set root password again mysql_user: login_user: '{{ mysql_user }}' login_password: '{{ root_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: root + host: '%' password: '{{ root_password }}' check_implicit_admin: yes register: result - - name: Assert root password is not changed - assert: { that: "result.changed == false" } + - name: Issue-64560 | Assert root password is not changed + assert: + that: + - result is not changed - - name: Set root password again + - name: Issue-64560 | Set root password again mysql_user: login_user: '{{ mysql_user }}' login_password: '{{ root_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' name: root + host: '%' password: '{{ mysql_password }}' check_implicit_admin: yes register: result diff --git a/tests/integration/targets/test_mysql_user/tasks/issue-671.yaml b/tests/integration/targets/test_mysql_user/tasks/issue-671.yaml new file mode 100644 index 0000000..3696cf0 --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/issue-671.yaml @@ -0,0 +1,112 @@ +--- +# Due to https://bugs.mysql.com/bug.php?id=115953, in Mysql 8, if ANSI_QUOTES is enabled, +# backticks will be used instead of double quotes to quote functions or procedures name. +# As a consequence, mysql_user and mysql_roles will always report "changed" for functions +# and procedures no matter the privileges are granted or not. +# Workaround for the mysql bug 116953 is removing ANSI_QUOTES from the module's session +# sql_mode. But because issue 671, ANSI_QUOTES is always got from GLOBAL sql_mode, thus +# this workaround can't work. Even without the Mysql bug, because sql_mode in session +# precedes GLOBAL sql_mode. we should check for sql_mode in session variable instead of +# the GLOBAL one. +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + - name: Issue-671| test setup | drop database + community.mysql.mysql_db: + <<: *mysql_params + name: "{{ item }}" + state: absent + loop: + - foo + - bar + + - name: Issue-671| test setup | create database + community.mysql.mysql_db: + <<: *mysql_params + name: "{{ item }}" + state: present + loop: + - foo + - bar + + - name: Issue-671| test setup | get value of GLOBAL.sql_mode + community.mysql.mysql_query: + <<: *mysql_params + query: 'select @@GLOBAL.sql_mode AS sql_mode' + register: sql_mode_orig + + - name: Issue-671| Assert sql_mode_orig + ansible.builtin.assert: + that: + - sql_mode_orig.query_result[0][0].sql_mode != None + + - name: Issue-671| enable sql_mode ANSI_QUOTES + community.mysql.mysql_variables: + <<: *mysql_params + variable: sql_mode + value: '{{ sql_mode_orig.query_result[0][0].sql_mode }},ANSI_QUOTES' + mode: "{% if db_engine == 'mariadb' %}global{% else %}persist{% endif %}" + + - name: Issue-671| Copy SQL scripts to remote + ansible.builtin.copy: + src: "{{ item }}" + dest: "{{ remote_tmp_dir }}/{{ item | basename }}" + loop: + - create-function.sql + - create-procedure.sql + + - name: Issue-671| Create function for test + ansible.builtin.shell: + cmd: "{{ mysql_command }} < {{ remote_tmp_dir }}/create-function.sql" + + - name: Issue-671| Create procedure for test + ansible.builtin.shell: + cmd: "{{ mysql_command }} < {{ remote_tmp_dir }}/create-procedure.sql" + + - name: Issue-671| Create user with FUNCTION and PROCEDURE privileges + community.mysql.mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + password: '{{ user_password_2 }}' + state: present + priv: 'FUNCTION foo.function:EXECUTE/foo.*:SELECT/PROCEDURE bar.procedure:EXECUTE' + + - name: Issue-671| Grant the privileges again, remove ANSI_QUOTES from the session variable + community.mysql.mysql_user: + <<: *mysql_params + session_vars: + sql_mode: "" + name: '{{ user_name_2 }}' + password: '{{ user_password_2 }}' + state: present + priv: 'FUNCTION foo.function:EXECUTE/foo.*:SELECT/PROCEDURE bar.procedure:EXECUTE' + register: result + failed_when: + - result is failed or result is changed + + - name: Issue-671| Test teardown | cleanup databases + community.mysql.mysql_db: + <<: *mysql_params + name: "{{ item }}" + state: absent + loop: + - foo + - bar + + - name: Issue-671| set sql_mode back to original value + community.mysql.mysql_variables: + <<: *mysql_params + variable: sql_mode + value: '{{ sql_mode_orig.query_result[0][0].sql_mode }}' + mode: "{% if db_engine == 'mariadb' %}global{% else %}persist{% endif %}" + + - name: Issue-671| Teardown user_name_2 + ansible.builtin.include_tasks: + file: utils/remove_user.yml + vars: + user_name: "{{ user_name_2 }}" \ No newline at end of file diff --git a/tests/integration/targets/test_mysql_user/tasks/main.yml b/tests/integration/targets/test_mysql_user/tasks/main.yml index e949fe6..7212886 100644 --- a/tests/integration/targets/test_mysql_user/tasks/main.yml +++ b/tests/integration/targets/test_mysql_user/tasks/main.yml @@ -1,3 +1,4 @@ +--- #################################################################### # WARNING: These are designed specifically for Ansible tests # # and should not be used as examples of how to write Ansible roles # @@ -24,124 +25,83 @@ # ============================================================ # create mysql user and verify user is added to mysql database # -- 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" - vars: mysql_parameters: &mysql_params login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' block: - - include: issue-121.yml + - include_tasks: issue-121.yml - - include: issue-28.yml + - include_tasks: issue-28.yml - - include: create_user.yml user_name={{user_name_1}} user_password={{ user_password_1 }} + - include_tasks: test_resource_limits.yml - - include: resource_limits.yml + - include_tasks: test_idempotency.yml - - include: assert_user.yml user_name={{user_name_1}} - - - include: remove_user.yml user_name={{user_name_1}} user_password={{ user_password_1 }} - - - include: assert_no_user.yml user_name={{user_name_1}} - - # ============================================================ - # Create mysql user that already exist on mysql database - # - - include: create_user.yml user_name={{user_name_1}} user_password={{ user_password_1 }} - - - name: create mysql user that already exist (expect changed=false) - mysql_user: - <<: *mysql_params - name: '{{user_name_1}}' - password: '{{user_password_1}}' - state: present - register: result - - - name: assert output message mysql user was not created - assert: { that: "result.changed == false" } - - # ============================================================ - # remove mysql user and verify user is removed from mysql database - # - - name: remove mysql user state=absent (expect changed=true) - mysql_user: - <<: *mysql_params - name: '{{ user_name_1 }}' - password: '{{ user_password_1 }}' - state: absent - register: result - - - name: assert output message mysql user was removed - assert: - that: - - "result.changed == true" - - - include: assert_no_user.yml user_name={{user_name_1}} - - # ============================================================ - # remove mysql user that does not exist on mysql database - # - - name: remove mysql user that does not exist state=absent (expect changed=false) - mysql_user: - <<: *mysql_params - name: '{{ user_name_1 }}' - password: '{{ user_password_1 }}' - state: absent - register: result - - - name: assert output message mysql user that does not exist - assert: - that: - - "result.changed == false" - - - include: assert_no_user.yml user_name={{user_name_1}} + - include_tasks: test_password_expire.yml # ============================================================ # Create user with no privileges and verify default privileges are assign # - - name: create user with select privilege state=present (expect changed=true) + - name: create user with DEFAULT privilege state=present (expect changed=true) mysql_user: <<: *mysql_params - name: '{{ user_name_1 }}' - password: '{{ user_password_1 }}' + name: "{{ user_name_1 }}" + password: "{{ user_password_1 }}" state: present register: result - - include: assert_user.yml user_name={{user_name_1}} priv=USAGE + - include_tasks: utils/assert_user.yml + vars: + user_name: "{{ user_name_1 }}" + user_host: localhost + priv: USAGE - - include: remove_user.yml user_name={{user_name_1}} user_password={{ user_password_1 }} + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_1 }}" + + - include_tasks: utils/assert_no_user.yml + vars: + user_name: "{{ user_name_1 }}" - - include: assert_no_user.yml user_name={{user_name_1}} # ============================================================ # Create user with select privileges and verify select privileges are assign # - - name: create user with select privilege state=present (expect changed=true) + - name: Create user with SELECT privilege state=present (expect changed=true) mysql_user: <<: *mysql_params - name: '{{ user_name_2 }}' - password: '{{ user_password_2 }}' + name: "{{ user_name_2 }}" + password: "{{ user_password_2 }}" state: present priv: '*.*:SELECT' register: result - - include: assert_user.yml user_name={{user_name_2}} priv=SELECT + - include_tasks: utils/assert_user.yml + vars: + user_name: "{{ user_name_2 }}" + user_host: localhost + priv: SELECT - - include: remove_user.yml user_name={{user_name_2}} user_password={{ user_password_2 }} + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_2 }}" + + - include_tasks: utils/assert_no_user.yml + vars: + user_name: "{{ user_name_2 }}" - - include: assert_no_user.yml user_name={{user_name_2}} # ============================================================ # Assert user has access to multiple databases # - - name: give users access to multiple databases + - name: Give users access to multiple databases mysql_user: <<: *mysql_params name: '{{ item[0] }}' @@ -149,34 +109,38 @@ append_privs: yes password: '{{ user_password_1 }}' with_nested: - - [ '{{ user_name_1 }}', '{{ user_name_2 }}'] + - ['{{ user_name_1 }}', '{{ user_name_2 }}'] - "{{db_names}}" - - name: show grants access for user1 on multiple database + - name: Show grants access for user1 on multiple database command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_1 }}'@'localhost'\"" register: result - - name: assert grant access for user1 on multiple database + - name: Assert grant access for user1 on multiple database assert: that: - - "'{{ item }}' in result.stdout" - with_items: "{{db_names}}" + - item in result.stdout + loop: "{{ db_names }}" - - name: show grants access for user2 on multiple database + - name: Show grants access for user2 on multiple database command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_2 }}'@'localhost'\"" register: result - - name: assert grant access for user2 on multiple database + - name: Assert grant access for user2 on multiple database assert: that: - - "'{{ item }}' in result.stdout" - with_items: "{{db_names}}" + - item in result.stdout + loop: "{{db_names}}" - - include: remove_user.yml user_name={{user_name_1}} user_password={{ user_password_1 }} + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_1 }}" - - include: remove_user.yml user_name={{user_name_2}} user_password={{ user_password_1 }} + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_2 }}" - - name: give user access to database via wildcard + - name: Give user SELECT access to database via wildcard mysql_user: <<: *mysql_params name: '{{ user_name_1 }}' @@ -184,7 +148,7 @@ append_privs: yes password: '{{ user_password_1 }}' - - name: show grants access for user1 on multiple database + - name: Show grants access for user1 on database via wildcard command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_1 }}'@'localhost'\"" register: result @@ -199,8 +163,8 @@ <<: *mysql_params name: '{{ user_name_1 }}' priv: - - unsuitable - - type + - unsuitable + - type append_privs: yes host_all: yes password: '{{ user_password_1 }}' @@ -213,7 +177,7 @@ - result is failed - result.msg is search('priv parameter must be str or dict') - - name: change user access to database via wildcard + - name: Change SELECT to INSERT for user access to database via wildcard mysql_user: <<: *mysql_params name: '{{ user_name_1 }}' @@ -222,7 +186,7 @@ host_all: yes password: '{{ user_password_1 }}' - - name: show grants access for user1 on multiple database + - name: Show grants access for user1 on database via wildcard command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_1 }}'@'localhost'\"" register: result @@ -232,50 +196,84 @@ - "'%db' in result.stdout" - "'INSERT' in result.stdout" - - include: remove_user.yml user_name={{user_name_1}} user_password={{ user_password_1 }} + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{user_name_1}}" # ============================================================ # Test plaintext and encrypted password scenarios. # - - include: test_user_password.yml + - include_tasks: test_user_password.yml # ============================================================ # Test plugin authentication scenarios. # # FIXME: mariadb sql syntax for create/update user is not compatible - - include: test_user_plugin_auth.yml - when: install_type == 'mysql' + - include_tasks: test_user_plugin_auth.yml + when: db_engine == 'mysql' # ============================================================ # Assert create user with SELECT privileges, attempt to create database and update privileges to create database # - - include: test_privs.yml current_privilege=SELECT current_append_privs=no + - include_tasks: test_privs.yml + vars: + current_privilege: SELECT + current_append_privs: no # ============================================================ # Assert creating user with SELECT privileges, attempt to create database and append privileges to create database # - - include: test_privs.yml current_privilege=DROP current_append_privs=yes + - include_tasks: test_privs.yml + vars: + current_privilege: DROP + current_append_privs: yes # ============================================================ # Assert create user with SELECT privileges, attempt to create database and update privileges to create database # - - include: test_privs.yml current_privilege='UPDATE,ALTER' current_append_privs=no + - include_tasks: test_privs.yml + vars: + current_privilege: 'UPDATE,ALTER' + current_append_privs: no # ============================================================ # Assert creating user with SELECT privileges, attempt to create database and append privileges to create database # - - include: test_privs.yml current_privilege='INSERT,DELETE' current_append_privs=yes + - include_tasks: test_privs.yml + vars: + current_privilege: 'INSERT,DELETE' + current_append_privs: yes # Tests for the priv parameter with dict value (https://github.com/ansible/ansible/issues/57533) - - include: test_priv_dict.yml + - include_tasks: test_priv_dict.yml # Test that append_privs will not attempt to make a change where current privileges are a superset of new privileges # (https://github.com/ansible-collections/community.mysql/issues/69) - - include: test_priv_append.yml enable_check_mode=no - - include: test_priv_append.yml enable_check_mode=yes + - include_tasks: test_priv_append.yml + vars: + enable_check_mode: no + - include_tasks: test_priv_append.yml + vars: + enable_check_mode: yes + + # Test that subtract_privs will only revoke the grants given by priv + # (https://github.com/ansible-collections/community.mysql/issues/331) + - include_tasks: test_priv_subtract.yml + vars: + enable_check_mode: no + - include_tasks: test_priv_subtract.yml + vars: + enable_check_mode: yes + + - import_tasks: test_privs_issue_465.yml + tags: + - issue_465 + + # Tests for user attributes + - include_tasks: test_user_attributes.yml # Tests for the TLS requires dictionary - - include: tls_requirements.yml + - include_tasks: test_tls_requirements.yml - import_tasks: issue-29511.yaml tags: @@ -284,7 +282,30 @@ - import_tasks: issue-64560.yaml tags: - issue-64560 + + - name: Test ANSI_QUOTES + ansible.builtin.import_tasks: + file: issue-671.yaml + tags: + - issue-671 # 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 + - include_tasks: issue-265.yml + + # https://github.com/ansible-collections/community.mysql/issues/231 + - include_tasks: test_user_grants_with_roles_applied.yml + + - include_tasks: test_revoke_only_grant.yml + + - name: Mysql_user - test column case sensitive + ansible.builtin.import_tasks: + file: test_column_case_sensitive.yml + + - name: Mysql_user - test update_password + ansible.builtin.import_tasks: + file: test_update_password.yml + + - name: Mysql_user - test user_locking + ansible.builtin.import_tasks: + file: test_user_locking.yml diff --git a/tests/integration/targets/test_mysql_user/tasks/remove_user.yml b/tests/integration/targets/test_mysql_user/tasks/remove_user.yml deleted file mode 100644 index 45a0ad4..0000000 --- a/tests/integration/targets/test_mysql_user/tasks/remove_user.yml +++ /dev/null @@ -1,74 +0,0 @@ -# test code to remove mysql user -# (c) 2014, Wayne Rosario - -# 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 . - -- 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: remove mysql user {{user_name}} - mysql_user: - <<: *mysql_params - name: '{{user_name}}' - password: '{{user_password}}' - state: absent - register: result - - - name: assert output message mysql user was removed - assert: - that: - - "result.changed == true" - - # ============================================================ - - name: create blank mysql user to be removed later - mysql_user: - <<: *mysql_params - name: "" - state: present - password: 'KJFDY&D*Sfuydsgf' - - - name: remove blank mysql user with hosts=all (expect changed) - mysql_user: - <<: *mysql_params - user: "" - host_all: true - state: absent - 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 - state: absent - register: result - - - name: assert changed is true for removing all blank users - assert: - that: - - "result.changed == false" diff --git a/tests/integration/targets/test_mysql_user/tasks/resource_limits.yml b/tests/integration/targets/test_mysql_user/tasks/resource_limits.yml deleted file mode 100644 index 736adb3..0000000 --- a/tests/integration/targets/test_mysql_user/tasks/resource_limits.yml +++ /dev/null @@ -1,118 +0,0 @@ -# test code for resource_limits parameter -- 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 {{ user_name_1 }} if exists - mysql_user: - <<: *mysql_params - name: '{{ user_name_1 }}' - state: absent - - - name: Create mysql user {{ user_name_1 }} with resource limits in check_mode - mysql_user: - <<: *mysql_params - name: '{{ user_name_1 }}' - password: '{{ user_password_1 }}' - state: present - resource_limits: - MAX_QUERIES_PER_HOUR: 10 - MAX_CONNECTIONS_PER_HOUR: 5 - check_mode: yes - register: result - - - assert: - that: - - result is changed - - - name: Create mysql user {{ user_name_1 }} with resource limits in actual mode - mysql_user: - <<: *mysql_params - name: '{{ user_name_1 }}' - password: '{{ user_password_1 }}' - state: present - resource_limits: - MAX_QUERIES_PER_HOUR: 10 - MAX_CONNECTIONS_PER_HOUR: 5 - register: result - - - assert: - that: - - result is changed - - - name: Check - mysql_query: - <<: *mysql_params - query: > - SELECT User FROM mysql.user WHERE User = '{{ user_name_1 }}' AND Host = 'localhost' - AND max_questions = 10 AND max_connections = 5 - register: result - - - assert: - that: - - result.rowcount[0] == 1 - - - name: Try to set the same limits again in check mode - mysql_user: - <<: *mysql_params - name: '{{ user_name_1 }}' - password: '{{ user_password_1 }}' - state: present - resource_limits: - MAX_QUERIES_PER_HOUR: 10 - MAX_CONNECTIONS_PER_HOUR: 5 - check_mode: yes - register: result - - - assert: - that: - - result is not changed - - - name: Try to set the same limits again in actual mode - mysql_user: - <<: *mysql_params - name: '{{ user_name_1 }}' - password: '{{ user_password_1 }}' - state: present - resource_limits: - MAX_QUERIES_PER_HOUR: 10 - MAX_CONNECTIONS_PER_HOUR: 5 - register: result - - - assert: - that: - - result is not changed - - - name: Change limits - mysql_user: - <<: *mysql_params - name: '{{ user_name_1 }}' - password: '{{ user_password_1 }}' - state: present - resource_limits: - MAX_QUERIES_PER_HOUR: 5 - MAX_CONNECTIONS_PER_HOUR: 5 - register: result - - - assert: - that: - - result is changed - - - name: Check - mysql_query: - <<: *mysql_params - query: > - SELECT User FROM mysql.user WHERE User = '{{ user_name_1 }}' AND Host = 'localhost' - AND max_questions = 5 AND max_connections = 5 - register: result - - - assert: - that: - - result.rowcount[0] == 1 - - when: (ansible_distribution == 'Ubuntu' and ansible_distribution_major_version >= '18') or (ansible_distribution == 'CentOS' and ansible_distribution_major_version >= '8') diff --git a/tests/integration/targets/test_mysql_user/tasks/test_column_case_sensitive.yml b/tests/integration/targets/test_mysql_user/tasks/test_column_case_sensitive.yml new file mode 100644 index 0000000..68e95aa --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/test_column_case_sensitive.yml @@ -0,0 +1,134 @@ +--- + +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + + # ========================= Prepare ======================================= + # We use query to prevent our module of changing the case + - name: Mysql_user Column case sensitive | Create a test table + community.mysql.mysql_query: + <<: *mysql_params + query: + - CREATE DATABASE mysql_user_column_case + - >- + CREATE TABLE mysql_user_column_case.t1 + (a int, B int, cC int, Dd int) + - >- + INSERT INTO mysql_user_column_case.t1 + (a, B, cC, Dd) VALUES (1,2,3,4) + + # ================= Reproduce failure ===================================== + + - name: Mysql_user Column case sensitive | Create test user + community.mysql.mysql_user: + <<: *mysql_params + name: column_case_sensitive + host: '%' + password: 'msandbox' + priv: + 'mysql_user_column_case.t1': 'SELECT(a, B, cC, Dd)' + + - name: Mysql_user Column case sensitive | Assert user privileges are all caps + community.mysql.mysql_query: + <<: *mysql_params + query: + - SHOW GRANTS FOR column_case_sensitive@'%' + register: column_case_insensitive_grants + failed_when: + # Column order may vary, thus test each separately + - >- + column_case_insensitive_grants.query_result[0][1] + is not search("A", ignorecase=false) + or column_case_insensitive_grants.query_result[0][1] + is not search("B", ignorecase=false) + or column_case_insensitive_grants.query_result[0][1] + is not search("CC", ignorecase=false) + or column_case_insensitive_grants.query_result[0][1] + is not search("DD", ignorecase=false) + + - name: Mysql_user Column case sensitive | Assert 1 column is accessible on MySQL 5.7 + community.mysql.mysql_query: + <<: *mysql_params + login_user: column_case_sensitive + query: + - DESC mysql_user_column_case.t1 + register: assert_1_col_accessible + failed_when: + - assert_1_col_accessible.rowcount[0] | int != 1 + when: + - db_engine == 'mysql' and db_version is version('5.7', '<=') + + - name: Mysql_user Column case sensitive | Assert 4 column are accessible on MariaDB and MySQL 8+ + community.mysql.mysql_query: + <<: *mysql_params + login_user: column_case_sensitive + query: + - DESC mysql_user_column_case.t1 + register: assert_4_col_accessible + failed_when: + - assert_4_col_accessible.rowcount[0] | int != 4 + when: + - >- + db_engine == 'mariadb' + or (db_engine == 'mysql' and db_version is version('8.0', '>=')) + + # ======================== Test fix ====================================== + + - name: Mysql_user Column case sensitive | Create users with case sensitive + community.mysql.mysql_user: + <<: *mysql_params + name: column_case_sensitive + host: '%' + password: 'msandbox' + priv: + 'mysql_user_column_case.t1': 'SELECT(a, B, cC, Dd)' + column_case_sensitive: true + + - name: Mysql_user Column case sensitive | Assert user privileges are case sensitive + community.mysql.mysql_query: + <<: *mysql_params + query: + - SHOW GRANTS FOR column_case_sensitive@'%' + register: column_case_sensitive_grants + failed_when: + # Column order may vary, thus test each separately + - >- + column_case_sensitive_grants.query_result[0][1] + is not search("a", ignorecase=false) + or column_case_sensitive_grants.query_result[0][1] + is not search("B", ignorecase=false) + or column_case_sensitive_grants.query_result[0][1] + is not search("cC", ignorecase=false) + or column_case_sensitive_grants.query_result[0][1] + is not search("Dd", ignorecase=false) + + - name: Mysql_user Column case sensitive | Assert 4 columns are accessible + community.mysql.mysql_query: + <<: *mysql_params + login_user: column_case_sensitive + query: + - DESC mysql_user_column_case.t1 + register: assert_4_col_accessible + failed_when: + - assert_4_col_accessible.rowcount[0] | int != 4 + + # ========================= Teardown ====================================== + + - name: Mysql_user Column case sensitive | Delete test users + community.mysql.mysql_user: + <<: *mysql_params + name: column_case_sensitive + host_all: true + state: absent + + - name: Mysql_user Column case sensitive | Delete test database + community.mysql.mysql_db: + <<: *mysql_params + name: mysql_user_column_case + state: absent diff --git a/tests/integration/targets/test_mysql_user/tasks/test_idempotency.yml b/tests/integration/targets/test_mysql_user/tasks/test_idempotency.yml new file mode 100644 index 0000000..f76934b --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/test_idempotency.yml @@ -0,0 +1,90 @@ +--- +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + # ======================================================================== + # Creation + # ======================================================================== + - include_tasks: utils/create_user.yml + vars: + user_name: "{{ user_name_1 }}" + user_password: "{{ user_password_1 }}" + + - name: Idempotency | Create user that already exist (expect changed=false) + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + password: "{{ user_password_1 }}" + state: present + register: result + + - name: Idempotency | Assert create user task is not changed + assert: {that: [result is not changed]} + + # ======================================================================== + # Removal + # ======================================================================== + - name: Idempotency | Remove user (expect changed=true) + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + state: absent + register: result + + - name: Idempotency | Assert remove user task is changed + ansible.builtin.assert: + that: + - result is changed + + - name: Idempotency | Remove user that doesn't exists (expect changed=false) + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + state: absent + register: result + + - name: Idempotency | Assert remove user task is not changed + ansible.builtin.assert: + that: + - result is not changed + + # ======================================================================== + # Removal with host_all + # ======================================================================== + + # Create blank user to be removed later + - include_tasks: utils/create_user.yml + vars: + user_name: "" + user_password: 'KJFDY&D*Sfuysf' + + - name: Idempotency | Remove blank user with hosts=all (expect changed) + mysql_user: + <<: *mysql_params + name: "" + host_all: true + state: absent + register: result + + - name: Idempotency | Assert removing all blank users is changed + ansible.builtin.assert: + that: + - result is changed + + - name: Idempotency | Remove blank user with hosts=all (expect ok) + mysql_user: + <<: *mysql_params + name: "" + host_all: true + state: absent + register: result + + - name: Idempotency | Assert removing all blank users is not changed + ansible.builtin.assert: + that: + - result is not changed diff --git a/tests/integration/targets/test_mysql_user/tasks/test_password_expire.yml b/tests/integration/targets/test_mysql_user/tasks/test_password_expire.yml new file mode 100644 index 0000000..7e70ece --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/test_password_expire.yml @@ -0,0 +1,174 @@ +--- +# Tests scenarios for password_expire + +- vars: + mysql_parameters: &mysql_params + login_user: "{{ mysql_user }}" + login_password: "{{ mysql_password }}" + login_host: "{{ mysql_host }}" + login_port: "{{ mysql_primary_port }}" + + block: + - include_tasks: utils/assert_user_password_expire.yml + vars: + username: "{{ item.username }}" + host: "{{ item.host | default('localhost')}}" + password_expire: "{{ item.password_expire }}" + password: "{{ user_password_1 }}" + expect_change: "{{ item.expect_change }}" + expect_password_expire_change: "{{ item.expect_password_expire_change }}" + expected_password_lifetime: "{{ item.expected_password_lifetime }}" + password_expire_interval: "{{ item.password_expire_interval | default(omit) }}" + expected_password_expired: "{{ item.expected_password_expired }}" + check_mode: "{{ item.check_mode | default(omit) }}" + loop: + # all variants set the password when nothing exists + # never expires + - username: "{{ user_name_1 }}" + host: "%" + password_expire: never + expect_change: true + expected_password_lifetime: "0" + expected_password_expired: "N" + # expires ussing default policy + - username: "{{ user_name_2 }}" + password_expire: default + expect_change: true + expected_password_lifetime: "-1" + expected_password_expired: "N" + # expires ussing interval + - username: "{{ user_name_3 }}" + password_expire: interval + password_expire_interval: "10" + expect_change: true + expected_password_lifetime: "10" + expected_password_expired: "N" + + # assert idempotency + - username: "{{ user_name_1 }}" + host: "%" + password_expire: never + expect_change: false + expected_password_lifetime: "0" + expected_password_expired: "N" + - username: "{{ user_name_2 }}" + password_expire: default + expect_change: false + expected_password_lifetime: "-1" + expected_password_expired: "N" + - username: "{{ user_name_3 }}" + password_expire: interval + password_expire_interval: "10" + expect_change: false + expected_password_lifetime: "10" + expected_password_expired: "N" + + # assert change is made + - username: "{{ user_name_3 }}" + password_expire: never + expect_change: true + expected_password_lifetime: "0" + expected_password_expired: "N" + - username: "{{ user_name_1 }}" + host: "%" + password_expire: default + expect_change: true + expected_password_lifetime: "-1" + expected_password_expired: "N" + - username: "{{ user_name_2 }}" + password_expire: interval + password_expire_interval: "100" + expect_change: true + expected_password_lifetime: "100" + expected_password_expired: "N" + + # assert password expires now + - username: "{{ user_name_1 }}" + host: "%" + password_expire: now + expect_change: true + expected_password_lifetime: "-1" # password lifetime should be the same + expected_password_expired: "Y" + - username: "{{ user_name_2 }}" + password_expire: now + expect_change: true + expected_password_lifetime: "100" # password lifetime should be the same + expected_password_expired: "Y" + + # assert idempotency password expires now + - username: "{{ user_name_1 }}" + host: "%" + password_expire: now + expect_change: false + expected_password_lifetime: "-1" # password lifetime should be the same + expected_password_expired: "Y" + - username: "{{ user_name_2 }}" + password_expire: now + expect_change: false + expected_password_lifetime: "100" # password lifetime should be the same + expected_password_expired: "Y" + + # assert check_mode + - username: "{{ user_name_3 }}" + password_expire: interval + password_expire_interval: 10 + check_mode: true + expect_change: false + expected_password_lifetime: "0" + expected_password_expired: "N" + + - name: password_expire | Set password_expire = interval without password_expire_interval + community.mysql.mysql_user: + <<: *mysql_params + name: '{{ user_name_4 }}' + host: '%' + password: '{{ user_password_4 }}' + password_expire: interval + state: present + register: result + ignore_errors: true + + - name: password_expire | Assert that action fails if 'password_expire_interval' not set + ansible.builtin.assert: + that: + - result is failed + + - name: password_expire | Set password_expire_interval < 1 + community.mysql.mysql_user: + <<: *mysql_params + name: '{{ user_name_4 }}' + host: '%' + password: '{{ user_password_4 }}' + password_expire: interval + password_expire_interval: -1 + state: present + register: result + ignore_errors: true + + - name: password_expire | Assert that action fails if 'password_expire_interval' is < 1 + ansible.builtin.assert: + that: + - result is failed + - "'should be positive number' in result.msg" + + - name: password_expire | check mode for user creation + community.mysql.mysql_user: + <<: *mysql_params + name: '{{ user_name_4 }}' + host: '%' + password: '{{ user_password_4 }}' + password_expire: interval + password_expire_interval: 20 + state: present + register: result + check_mode: True + failed_when: result is changed + + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ item.username }}" + loop: + - username: "{{ user_name_1 }}" + - username: "{{ user_name_2 }}" + - username: "{{ user_name_3 }}" + - username: "{{ user_name_4 }}" diff --git a/tests/integration/targets/test_mysql_user/tasks/test_priv_append.yml b/tests/integration/targets/test_mysql_user/tasks/test_priv_append.yml index cd10147..76b4ab1 100644 --- a/tests/integration/targets/test_mysql_user/tasks/test_priv_append.yml +++ b/tests/integration/targets/test_mysql_user/tasks/test_priv_append.yml @@ -1,45 +1,48 @@ +--- # Test code to ensure that appending privileges will not result in unnecessary changes when the current privileges # are a superset of the new privileges that have been defined. - vars: mysql_parameters: &mysql_params login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' block: - - name: Create test databases + - name: Priv append | Create test databases mysql_db: <<: *mysql_params name: '{{ item }}' state: present loop: - - data1 - - data2 + - data1 + - data2 - - name: Create a user with an initial set of privileges + - name: Priv append | Create a user with an initial set of privileges mysql_user: <<: *mysql_params name: '{{ user_name_4 }}' + host: '%' password: '{{ user_password_4 }}' priv: 'data1.*:SELECT,INSERT/data2.*:SELECT,DELETE' state: present - - name: Run command to show privileges for user (expect privileges in stdout) - command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_4 }}'@'localhost'\"" + - name: Priv append | Run command to show privileges for user (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_4 }}'@'%'\"" register: result - - name: Assert that the initial set of privileges matches what is expected + - name: Priv append | Assert that the initial set of privileges matches what is expected assert: that: - "'GRANT SELECT, INSERT ON `data1`.*' in result.stdout" - "'GRANT SELECT, DELETE ON `data2`.*' in result.stdout" - - name: Append privileges that are a subset of the current privileges, which should be a no-op + - name: Priv append | Append privileges that are a subset of the current privileges, which should be a no-op mysql_user: <<: *mysql_params name: '{{ user_name_4 }}' + host: '%' password: '{{ user_password_4 }}' priv: 'data1.*:SELECT/data2.*:SELECT' append_privs: yes @@ -47,25 +50,26 @@ check_mode: '{{ enable_check_mode }}' register: result - - name: Assert that there wasn't a change in permissions + - name: Priv append | Assert that there wasn't a change in permissions assert: that: - - "result.changed == false" + - result is not changed - - name: Run command to show privileges for user (expect privileges in stdout) - command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_4 }}'@'localhost'\"" + - name: Priv append | Run command to show privileges for user (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_4 }}'@'%'\"" register: result - - name: Assert that the permissions still match what was originally granted + - name: Priv append | Assert that the permissions still match what was originally granted assert: that: - "'GRANT SELECT, INSERT ON `data1`.*' in result.stdout" - "'GRANT SELECT, DELETE ON `data2`.*' in result.stdout" - - name: Append privileges that are not included in the current set of privileges to test that privileges are updated + - name: Priv append | Append privileges that are not included in the current set of privileges to test that privileges are updated mysql_user: <<: *mysql_params name: '{{ user_name_4 }}' + host: '%' password: '{{ user_password_4 }}' priv: 'data1.*:DELETE/data2.*:SELECT' append_privs: yes @@ -73,33 +77,34 @@ check_mode: '{{ enable_check_mode }}' register: result - - name: Assert that there was a change because permissions were added to data1.* + - name: Priv append | Assert that there was a change because permissions were added to data1.* assert: that: - - "result.changed == true" + - result is changed - - name: Run command to show privileges for user (expect privileges in stdout) - command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_4 }}'@'localhost'\"" + - name: Priv append | Run command to show privileges for user (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_4 }}'@'%'\"" register: result - - name: Assert that the permissions were changed as expected if check_mode is set to 'no' + - name: Priv append | Assert that the permissions were changed as expected if check_mode is set to 'no' assert: that: - "'GRANT SELECT, INSERT, DELETE ON `data1`.*' in result.stdout" - "'GRANT SELECT, DELETE ON `data2`.*' in result.stdout" when: enable_check_mode == 'no' - - name: Assert that the permissions were not actually changed if check_mode is set to 'yes' + - name: Priv append | Assert that the permissions were not actually changed if check_mode is set to 'yes' assert: that: - "'GRANT SELECT, INSERT ON `data1`.*' in result.stdout" - "'GRANT SELECT, DELETE ON `data2`.*' in result.stdout" when: enable_check_mode == 'yes' - - name: Try to append invalid privileges + - name: Priv append | Try to append invalid privileges mysql_user: <<: *mysql_params name: '{{ user_name_4 }}' + host: '%' password: '{{ user_password_4 }}' priv: 'data1.*:INVALID/data2.*:SELECT' append_privs: yes @@ -108,7 +113,7 @@ register: result ignore_errors: true - - name: Assert that there wasn't a change in privileges if check_mode is set to 'no' + - name: Priv append | Assert that there wasn't a change in privileges if check_mode is set to 'no' assert: that: - result is failed @@ -123,11 +128,9 @@ name: '{{ item }}' state: present loop: - - data1 - - data2 + - data1 + - data2 - - name: Drop test user - mysql_user: - <<: *mysql_params - name: '{{ user_name_4 }}' - state: absent + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_4 }}" diff --git a/tests/integration/targets/test_mysql_user/tasks/test_priv_dict.yml b/tests/integration/targets/test_mysql_user/tasks/test_priv_dict.yml index d54c946..f162f6b 100644 --- a/tests/integration/targets/test_mysql_user/tasks/test_priv_dict.yml +++ b/tests/integration/targets/test_mysql_user/tasks/test_priv_dict.yml @@ -1,24 +1,25 @@ +--- - vars: mysql_parameters: &mysql_params login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' block: # Tests for priv parameter value passed as a dict - - name: Create test databases + - name: Priv dict | Create test databases mysql_db: <<: *mysql_params name: '{{ item }}' state: present loop: - - data1 - - data2 - - data3 + - data1 + - data2 + - data3 - - name: Create user with privileges + - name: Priv dict | Create user with privileges mysql_user: <<: *mysql_params name: '{{ user_name_3 }}' @@ -28,7 +29,7 @@ "data2.*": "SELECT" state: present - - name: Run command to show privileges for user (expect privileges in stdout) + - name: Priv dict | Run command to show privileges for user (expect privileges in stdout) command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_3 }}'@'localhost'\"" register: result @@ -39,12 +40,12 @@ - "'GRANT SELECT ON `data2`.*' in result.stdout" # Issue https://github.com/ansible-collections/community.mysql/issues/99 - - name: Create test table test_table_issue99 + - name: Priv dict | Create test table test_table_issue99 mysql_query: <<: *mysql_params query: "CREATE TABLE IF NOT EXISTS data3.test_table_issue99 (a INT, b INT, c INT)" - - name: Grant select on a column + - name: Priv dict | Grant select on a column mysql_user: <<: *mysql_params name: '{{ user_name_3 }}' @@ -52,11 +53,12 @@ 'data3.test_table_issue99': 'SELECT (a)' register: result - - assert: + - name: Priv dict | Assert that select on a column is changed + assert: that: - - result is changed + - result is changed - - name: Grant select on the column again + - name: Priv dict | Grant select on the column again mysql_user: <<: *mysql_params name: '{{ user_name_3 }}' @@ -64,12 +66,12 @@ 'data3.test_table_issue99': 'SELECT (a)' register: result - - assert: + - name: Priv dict | Assert that select on the column is not changed + assert: that: - - result is not changed + - result is not changed - - - name: Grant select on columns + - name: Priv dict | Grant select on columns mysql_user: <<: *mysql_params name: '{{ user_name_3 }}' @@ -77,11 +79,12 @@ 'data3.test_table_issue99': 'SELECT (a, b),INSERT' register: result - - assert: + - name: Priv dict | Assert select on columns is changed + assert: that: - - result is changed + - result is changed - - name: Grant select on columns again + - name: Priv dict | Grant select on columns again mysql_user: <<: *mysql_params name: '{{ user_name_3 }}' @@ -89,11 +92,12 @@ 'data3.test_table_issue99': 'SELECT (a, b),INSERT' register: result - - assert: + - name: Priv dict | Assert that select on columns again is not changed + assert: that: - - result is not changed + - result is not changed - - name: Grant privs on columns + - name: Priv dict | Grant privs on columns mysql_user: <<: *mysql_params name: '{{ user_name_3 }}' @@ -101,11 +105,12 @@ 'data3.test_table_issue99': 'SELECT (a, b), INSERT (a, b), UPDATE' register: result - - assert: + - name: Priv dict | Assert that grant privs on columns is changed + assert: that: - - result is changed + - result is changed - - name: Grant same privs on columns again, note that the column order is different + - name: Priv dict | Grant same privs on columns again, note that the column order is different mysql_user: <<: *mysql_params name: '{{ user_name_3 }}' @@ -113,21 +118,22 @@ 'data3.test_table_issue99': 'SELECT (a, b), UPDATE, INSERT (b, a)' register: result - - assert: + - name: Priv dict | Assert that grants same privs with different order is not changed + assert: that: - - result is not changed + - result is not changed - - name: Run command to show privileges for user (expect privileges in stdout) + - name: Priv dict | 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 + - name: Priv dict | Assert user has giving privileges assert: that: - "'GRANT SELECT (`A`, `B`), INSERT (`A`, `B`), UPDATE' in result.stdout" when: "'(`A`, `B`)' in result.stdout" - - name: Assert user has giving privileges + - name: Priv dict | Assert user has giving privileges assert: that: - "'GRANT SELECT (A, B), INSERT (A, B), UPDATE' in result.stdout" @@ -135,18 +141,16 @@ ########## # Clean up - - name: Drop test databases + - name: Priv dict | Drop test databases mysql_db: <<: *mysql_params name: '{{ item }}' state: present loop: - - data1 - - data2 - - data3 + - data1 + - data2 + - data3 - - name: Drop test user - mysql_user: - <<: *mysql_params - name: '{{ user_name_3 }}' - state: absent + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_3 }}" diff --git a/tests/integration/targets/test_mysql_user/tasks/test_priv_subtract.yml b/tests/integration/targets/test_mysql_user/tasks/test_priv_subtract.yml new file mode 100644 index 0000000..c63396a --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/test_priv_subtract.yml @@ -0,0 +1,177 @@ +--- +# Test code to ensure that subtracting privileges will not result in unnecessary changes. +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + + - name: Priv substract | Create test databases + mysql_db: + <<: *mysql_params + name: '{{ item }}' + state: present + loop: + - data1 + + - name: Priv substract | Create a user with an initial set of privileges + mysql_user: + <<: *mysql_params + name: '{{ user_name_4 }}' + host: '%' + password: '{{ user_password_4 }}' + priv: 'data1.*:SELECT,INSERT' + state: present + + - name: Priv substract | Run command to show privileges for user (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_4 }}'@'%'\"" + register: result + + - name: Priv substract | Assert that the initial set of privileges matches what is expected + assert: + that: + - "'GRANT SELECT, INSERT ON `data1`.*' in result.stdout" + + - name: Priv substract | Subtract privileges that are not in the current privileges, which should be a no-op + mysql_user: + <<: *mysql_params + name: '{{ user_name_4 }}' + host: '%' + password: '{{ user_password_4 }}' + priv: 'data1.*:DELETE' + subtract_privs: yes + state: present + check_mode: '{{ enable_check_mode }}' + register: result + + - name: Priv substract | Assert that there wasn't a change in permissions + assert: + that: + - result is not changed + + - name: Priv substract | Run command to show privileges for user (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_4 }}'@'%'\"" + register: result + + - name: Priv substract | Assert that the permissions still match what was originally granted + assert: + that: + - "'GRANT SELECT, INSERT ON `data1`.*' in result.stdout" + + - name: Priv substract | Subtract existing and not-existing privileges, but not all + mysql_user: + <<: *mysql_params + name: '{{ user_name_4 }}' + host: '%' + password: '{{ user_password_4 }}' + priv: 'data1.*:INSERT,DELETE' + subtract_privs: yes + state: present + check_mode: '{{ enable_check_mode }}' + register: result + + - name: Priv substract | Assert that there was a change because permissions were/would be revoked on data1.* + assert: + that: + - result is changed + + - name: Priv substract | Run command to show privileges for user (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_4 }}'@'%'\"" + register: result + + - name: Priv substract | Assert that the permissions were not changed if check_mode is set to 'yes' + assert: + that: + - "'GRANT SELECT, INSERT ON `data1`.*' in result.stdout" + when: enable_check_mode == 'yes' + + - name: Priv substract | Assert that only DELETE was revoked if check_mode is set to 'no' + assert: + that: + - "'GRANT SELECT ON `data1`.*' in result.stdout" + when: enable_check_mode == 'no' + + - name: Priv substract | Try to subtract invalid privileges + mysql_user: + <<: *mysql_params + name: '{{ user_name_4 }}' + host: '%' + password: '{{ user_password_4 }}' + priv: 'data1.*:INVALID' + subtract_privs: yes + state: present + check_mode: '{{ enable_check_mode }}' + register: result + + - name: Priv substract | Assert that there was no change because invalid permissions are ignored + assert: + that: + - result is not changed + + - name: Priv substract | Run command to show privileges for user (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_4 }}'@'%'\"" + register: result + + - name: Priv substract | Assert that the permissions were not changed with check_mode=='yes' + assert: + that: + - "'GRANT SELECT, INSERT ON `data1`.*' in result.stdout" + when: enable_check_mode == 'yes' + + - name: Priv substract | Assert that the permissions were not changed with check_mode=='no' + assert: + that: + - "'GRANT SELECT ON `data1`.*' in result.stdout" + when: enable_check_mode == 'no' + + - name: Priv substract | Trigger failure by trying to subtract and append privileges at the same time + mysql_user: + <<: *mysql_params + name: '{{ user_name_4 }}' + host: '%' + password: '{{ user_password_4 }}' + priv: 'data1.*:SELECT' + subtract_privs: yes + append_privs: yes + state: present + check_mode: '{{ enable_check_mode }}' + register: result + ignore_errors: true + + - name: Priv substract | Assert the previous execution failed + assert: + that: + - result is failed + + - name: Priv substract | Run command to show privileges for user (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_4 }}'@'%'\"" + register: result + + - name: Priv substract | Assert that the permissions stayed the same, with check_mode=='yes' + assert: + that: + - "'GRANT SELECT, INSERT ON `data1`.*' in result.stdout" + when: enable_check_mode == 'yes' + + - name: Priv substract | Assert that the permissions stayed the same, with check_mode=='no' + assert: + that: + - "'GRANT SELECT ON `data1`.*' in result.stdout" + when: enable_check_mode == 'no' + + ########## + # Clean up + - name: Priv substract | Drop test databases + mysql_db: + <<: *mysql_params + name: '{{ item }}' + state: present + loop: + - data1 + + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_4 }}" diff --git a/tests/integration/targets/test_mysql_user/tasks/test_privs.yml b/tests/integration/targets/test_mysql_user/tasks/test_privs.yml index 89d9358..95d44aa 100644 --- a/tests/integration/targets/test_mysql_user/tasks/test_privs.yml +++ b/tests/integration/targets/test_mysql_user/tasks/test_privs.yml @@ -1,3 +1,4 @@ +--- # test code for privileges for mysql_user module # (c) 2014, Wayne Rosario @@ -20,56 +21,62 @@ mysql_parameters: &mysql_params login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' block: # ============================================================ - - name: create user with basic select privileges + - name: Privs | Create user with basic select privileges mysql_user: <<: *mysql_params name: '{{ user_name_2 }}' + host: '%' password: '{{ user_password_2 }}' priv: '*.*:SELECT' state: present when: current_append_privs == "yes" - - include: assert_user.yml user_name={{user_name_2}} priv='SELECT' + - include_tasks: utils/assert_user.yml + vars: + user_name: "{{ user_name_2 }}" + user_host: "%" + priv: 'SELECT' when: current_append_privs == "yes" - - name: create user with current privileges (expect changed=true) + - name: Privs | Create user with current privileges (expect changed=true) mysql_user: <<: *mysql_params name: '{{ user_name_2 }}' + host: '%' password: '{{ user_password_2 }}' - priv: '*.*:{{current_privilege}}' - append_privs: '{{current_append_privs}}' + priv: '*.*:{{ current_privilege }}' + append_privs: '{{ current_append_privs }}' state: present register: result - - name: assert output message for current privileges + - name: Privs | Assert output message for current privileges assert: that: - - "result.changed == true" + - result is changed - - name: run command to show privileges for user (expect privileges in stdout) - command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{user_name_2}}'@'localhost'\"" + - name: Privs | Run command to show privileges for user (expect privileges in stdout) + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{user_name_2}}'@'%'\"" register: result - - name: assert user has correct privileges + - name: Privs | Assert user has correct privileges assert: that: - - "'GRANT {{current_privilege | replace(',', ', ')}} ON *.*' in result.stdout" + - "'GRANT {{ current_privilege | replace(',', ', ') }} ON *.*' in result.stdout" when: current_append_privs == "no" - - name: assert user has correct privileges + - name: Privs | Assert user has correct privileges assert: that: - - "'GRANT SELECT, {{current_privilege | replace(',', ', ')}} ON *.*' in result.stdout" + - "'GRANT SELECT, {{ current_privilege | replace(',', ', ') }} ON *.*' in result.stdout" when: current_append_privs == "yes" - - name: create database using user current privileges + - name: Privs | Create database using user current privileges mysql_db: login_user: '{{ user_name_2 }}' login_password: '{{ user_password_2 }}' @@ -79,56 +86,59 @@ state: present ignore_errors: true - - name: run command to test that database was not created + - name: Privs | Run command to test that database was not created command: "{{ mysql_command }} -e \"show databases like '{{ db_name }}'\"" register: result - - name: assert database was not created + - name: Privs | Assert database was not created assert: that: - - "'{{ db_name }}' not in result.stdout" + - db_name not in result.stdout # ============================================================ - - name: Add privs to a specific table (expect changed) + - name: Privs | Add privs to a specific table (expect changed) mysql_user: <<: *mysql_params name: '{{ user_name_2 }}' + host: '%' password: '{{ user_password_2 }}' priv: 'jmainguy.jmainguy:ALL' state: present register: result - - name: Assert that priv changed + - name: Privs | Assert that priv changed assert: that: - - "result.changed == true" + - result is changed - - name: Add privs to a specific table (expect ok) + - name: Privs | Add privs to a specific table (expect ok) mysql_user: <<: *mysql_params name: '{{ user_name_2 }}' + host: '%' password: '{{ user_password_2 }}' priv: 'jmainguy.jmainguy:ALL' state: present register: result - - name: Assert that priv did not change + - name: Privs | Assert that priv did not change assert: that: - - "result.changed == false" + - result is not changed # ============================================================ - - name: update user with all privileges + - name: Privs | Grant ALL to user {{ user_name_2 }} mysql_user: <<: *mysql_params name: '{{ user_name_2 }}' + host: '%' password: '{{ user_password_2 }}' priv: '*.*:ALL' state: present - # - include: assert_user.yml user_name={{user_name_2}} priv='ALL PRIVILEGES' + # - include_tasks: utils/assert_user.yml user_name={{user_name_2}} user_host=% priv='ALL PRIVILEGES' - - name: create database using user + - name: Privs | Create database using user {{ user_name_2 }} mysql_db: login_user: '{{ user_name_2 }}' login_password: '{{ user_password_2 }}' @@ -137,10 +147,10 @@ name: '{{ db_name }}' state: present - - name: run command to test database was created using user new privileges + - name: Privs | Run command to test database was created using user new privileges command: "{{ mysql_command }} -e \"SHOW CREATE DATABASE {{ db_name }}\"" - - name: drop database using user + - name: Privs | Drop database using user {{ user_name_2 }} mysql_db: login_user: '{{ user_name_2 }}' login_password: '{{ user_password_2 }}' @@ -150,38 +160,96 @@ state: absent # ============================================================ - - name: update user with a long privileges list (mysql has a special multiline grant output) + - name: Privs | Update user with a long privileges list (mysql has a special multiline grant output) mysql_user: <<: *mysql_params name: '{{ user_name_2 }}' + host: '%' password: '{{ user_password_2 }}' priv: '*.*:CREATE USER,FILE,PROCESS,RELOAD,REPLICATION CLIENT,REPLICATION SLAVE,SHOW DATABASES,SHUTDOWN,SUPER,CREATE,DROP,EVENT,LOCK TABLES,INSERT,UPDATE,DELETE,SELECT,SHOW VIEW,GRANT' state: present register: result - - name: Assert that priv changed + - name: Privs | Assert that priv changed assert: that: - - "result.changed == true" + - result is changed - - name: Test idempotency (expect ok) + - name: Privs | Test idempotency with a long privileges list (expect ok) mysql_user: <<: *mysql_params name: '{{ user_name_2 }}' + host: '%' password: '{{ user_password_2 }}' priv: '*.*:CREATE USER,FILE,PROCESS,RELOAD,REPLICATION CLIENT,REPLICATION SLAVE,SHOW DATABASES,SHUTDOWN,SUPER,CREATE,DROP,EVENT,LOCK TABLES,INSERT,UPDATE,DELETE,SELECT,SHOW VIEW,GRANT' state: present register: result - # FIXME: on mariadb 10.5 there's always a change - - name: Assert that priv did not change + # FIXME: on mysql >=8 and mariadb >=10.5.2 there's always a change because + # the REPLICATION CLIENT privilege was renamed to BINLOG MONITOR + - name: Privs | Assert that priv did not change assert: that: - - "result.changed == false" - when: install_type == 'mysql' or (install_type == 'mariadb' and mariadb_version is version('10.2', '==')) + - result is not changed + + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_2 }}" # ============================================================ - - name: update user with invalid privileges + - name: Privs | Grant all privileges with grant option + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + password: '{{ user_password_2 }}' + priv: '*.*:ALL,GRANT' + state: present + register: result + + - name: Privs | Assert that priv changed + assert: + that: + - result is changed + + - name: Privs | Collect user info by host + community.mysql.mysql_info: + <<: *mysql_params + filter: "users" + register: mysql_info_about_users + + - name: Privs | Assert that 'GRANT' permission is present + assert: + that: + - mysql_info_about_users.users.localhost.{{ user_name_2 }}.Grant_priv == 'Y' + + - name: Privs | Test idempotency (expect ok) + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + password: '{{ user_password_2 }}' + priv: '*.*:ALL,GRANT' + state: present + register: result + + # FIXME: on mysql >=8 there's always a change (ALL PRIVILEGES -> specific privileges) + - name: Privs | Assert that priv did not change + assert: + that: + - result is not changed + + - name: Privs | Collect user info by host + community.mysql.mysql_info: + <<: *mysql_params + filter: "users" + register: mysql_info_about_users + + - name: Privs | Assert that 'GRANT' permission is present (by host) + assert: + that: + - mysql_info_about_users.users.localhost.{{ user_name_2 }}.Grant_priv == 'Y' + + # ============================================================ + - name: Privs | Update user with invalid privileges mysql_user: <<: *mysql_params name: '{{ user_name_2 }}' @@ -191,15 +259,12 @@ register: result ignore_errors: yes - - name: Assert that priv did not change + - name: Privs | Assert that priv did not change assert: that: - result is failed - "'Error granting privileges' in result.msg" - - name: remove username - mysql_user: - <<: *mysql_params - name: '{{ user_name_2 }}' - password: '{{ user_password_2 }}' - state: absent + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_2 }}" diff --git a/tests/integration/targets/test_mysql_user/tasks/test_privs_issue_465.yml b/tests/integration/targets/test_mysql_user/tasks/test_privs_issue_465.yml new file mode 100644 index 0000000..2e6a41e --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/test_privs_issue_465.yml @@ -0,0 +1,31 @@ +--- +# test code for privileges for mysql_user module - issue 465 + +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + + # ============================================================ + - name: Privs issue 465 | Create a user with parameters that will always cause an exception + mysql_user: + <<: *mysql_params + name: user_issue_465 + password: a_test_password_465 + priv: '*.{{ db_name }}:SELECT' + state: present + ignore_errors: true + register: result + + - name: Privs issue 465 | Assert output message for current privileges + assert: + that: + - result is failed + - result.msg is search('invalid priv string') + - result.msg is search('params') + - result.msg is search('query') + - result.msg is search('exception') diff --git a/tests/integration/targets/test_mysql_user/tasks/test_resource_limits.yml b/tests/integration/targets/test_mysql_user/tasks/test_resource_limits.yml new file mode 100644 index 0000000..a390a4e --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/test_resource_limits.yml @@ -0,0 +1,279 @@ +--- +# test code for resource_limits parameter +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + + - name: Resource limits | Drop mysql user {{ user_name_1 }} if exists + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + host_all: true + state: absent + + - name: Resource limits | Create mysql user {{ user_name_1 }} with resource limits in check_mode + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + state: present + resource_limits: + MAX_QUERIES_PER_HOUR: 10 + MAX_CONNECTIONS_PER_HOUR: 5 + check_mode: yes + register: result + + - name: Resource limits | Assert that create user with resource limits is changed + assert: + that: + - result is changed + + - name: Resource limits | Create mysql user {{ user_name_1 }} with resource limits in actual mode + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + state: present + resource_limits: + MAX_QUERIES_PER_HOUR: 10 + MAX_CONNECTIONS_PER_HOUR: 5 + register: result + + - assert: + that: + - result is changed + + - name: Resource limits | Retrieve user + mysql_query: + <<: *mysql_params + query: > + SELECT User FROM mysql.user + WHERE User = '{{ user_name_1 }}' + AND Host = 'localhost' + AND max_questions = 10 + AND max_connections = 5 + register: result + + - name: Resource limits | Assert that rowcount is 1 + assert: + that: + - result.rowcount[0] == 1 + + - name: Resource limits | Try to set the same limits again in check mode + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + state: present + resource_limits: + MAX_QUERIES_PER_HOUR: 10 + MAX_CONNECTIONS_PER_HOUR: 5 + check_mode: yes + register: result + + - name: Resource limits | Assert that set same limits again is not changed + assert: + that: + - result is not changed + + - name: Resource limits | Try to set the same limits again in actual mode + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + state: present + resource_limits: + MAX_QUERIES_PER_HOUR: 10 + MAX_CONNECTIONS_PER_HOUR: 5 + register: result + + - name: Resource limits | Assert that set same limits again in actual mode is not changed + assert: + that: + - result is not changed + + - name: Resource limits | Change limits + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + state: present + resource_limits: + MAX_QUERIES_PER_HOUR: 5 + MAX_CONNECTIONS_PER_HOUR: 5 + register: result + + - name: Resource limits | Assert limits changed + assert: + that: + - result is changed + + - name: Resource limits | Get user limits + mysql_query: + <<: *mysql_params + query: > + SELECT User FROM mysql.user + WHERE User = '{{ user_name_1 }}' + AND Host = 'localhost' + AND max_questions = 5 + AND max_connections = 5 + register: result + + - name: Resource limits | Assert limit row count + assert: + that: + - result.rowcount[0] == 1 + + - name: Resource limits | Drop mysql user {{ user_name_1 }} if exists + community.mysql.mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + host_all: true + state: absent + + - name: Resource limits | Create mysql user {{ user_name_1 }} with MAX_STATEMENT_TIME in check_mode + community.mysql.mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + state: present + resource_limits: + MAX_QUERIES_PER_HOUR: 10 + MAX_STATEMENT_TIME: 1 + check_mode: true + register: result + ignore_errors: true + + - name: Resource limits | Assert that create user with MAX_STATEMENT_TIME is changed for mariadb + ansible.builtin.assert: + that: + - result is changed + when: db_engine == 'mariadb' + + - name: Resource limits | Assert that create user with MAX_STATEMENT_TIME is failed for mysql + ansible.builtin.assert: + that: + - result is failed + when: db_engine == 'mysql' + + - name: Resource limits | Create mysql user {{ user_name_1 }} with MAX_STATEMENT_TIME in actual mode + community.mysql.mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + state: present + resource_limits: + MAX_QUERIES_PER_HOUR: 10 + MAX_STATEMENT_TIME: 1 + register: result + ignore_errors: true + + - name: Resource limits | Assert that create user with MAX_STATEMENT_TIME is changed for MariaDB + ansible.builtin.assert: + that: + - result is changed + when: db_engine == 'mariadb' + + - name: Resource limits | Assert that create user with MAX_STATEMENT_TIME is failed for MySQL + ansible.builtin.assert: + that: + - result is failed + when: db_engine == 'mysql' + + - name: Resource limits | Retrieve user with MAX_STATEMENT_TIME + community.mysql.mysql_query: + <<: *mysql_params + query: > + SELECT User FROM mysql.user + WHERE User = '{{ user_name_1 }}' + AND Host = 'localhost' + AND max_questions = 10 + AND max_statement_time = 1 + register: result + when: db_engine == 'mariadb' + + - name: Resource limits | Assert that rowcount is 1 with MAX_STATEMENT_TIME + ansible.builtin.assert: + that: + - result.rowcount[0] == 1 + when: db_engine == 'mariadb' + + - name: Resource limits | Try to set the same limits with MAX_STATEMENT_TIME again in check mode + community.mysql.mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + state: present + resource_limits: + MAX_QUERIES_PER_HOUR: 10 + MAX_STATEMENT_TIME: 1 + check_mode: true + register: result + when: db_engine == 'mariadb' + + - name: Resource limits | Assert that set same limits with MAX_STATEMENT_TIME again is not changed + ansible.builtin.assert: + that: + - result is not changed + when: db_engine == 'mariadb' + + - name: Resource limits | Try to set the same limits with MAX_STATEMENT_TIME again in actual mode + community.mysql.mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + state: present + resource_limits: + MAX_QUERIES_PER_HOUR: 10 + MAX_STATEMENT_TIME: 1 + register: result + when: db_engine == 'mariadb' + + - name: Resource limits | Assert that set same limits with MAX_STATEMENT_TIME again in actual mode is not changed + ansible.builtin.assert: + that: + - result is not changed + when: db_engine == 'mariadb' + + - name: Resource limits | Change limits with MAX_STATEMENT_TIME + community.mysql.mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + state: present + resource_limits: + MAX_QUERIES_PER_HOUR: 5 + MAX_STATEMENT_TIME: 2 + register: result + when: db_engine == 'mariadb' + + - name: Resource limits | Assert limits with MAX_STATEMENT_TIME changed + ansible.builtin.assert: + that: + - result is changed + when: db_engine == 'mariadb' + + - name: Resource limits | Get user limits with MAX_STATEMENT_TIME + community.mysql.mysql_query: + <<: *mysql_params + query: > + SELECT User FROM mysql.user + WHERE User = '{{ user_name_1 }}' + AND Host = 'localhost' + AND max_questions = 5 + AND max_statement_time = 2 + register: result + when: db_engine == 'mariadb' + + - name: Resource limits | Assert limit with MAX_STATEMENT_TIME row count + ansible.builtin.assert: + that: + - result.rowcount[0] == 1 + when: db_engine == 'mariadb' + + when: (ansible_distribution == 'Ubuntu' and ansible_distribution_major_version >= '18') or (ansible_distribution == 'CentOS' and ansible_distribution_major_version >= '8') diff --git a/tests/integration/targets/test_mysql_user/tasks/test_revoke_only_grant.yml b/tests/integration/targets/test_mysql_user/tasks/test_revoke_only_grant.yml new file mode 100644 index 0000000..b192273 --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/test_revoke_only_grant.yml @@ -0,0 +1,54 @@ +--- +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + block: + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_1 }}" + + - name: Revoke only grants | Create user with two grants + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + password: "{{ user_password_1 }}" + update_password: on_create + priv: '*.*:SELECT,GRANT' + + - name: Revoke only grants | Revoke grant priv from db_user1 + register: result + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + password: "{{ user_password_1 }}" + update_password: on_create + priv: '*.*:SELECT' + + - name: Revoke only grants | Assert that db_user1 only have one priv left + assert: + that: + - result is not failed + - result is changed + + - name: Revoke only grants | Update db_user1 again to test idempotence + register: result + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + password: "{{ user_password_1 }}" + update_password: on_create + priv: '*.*:SELECT' + + - name: Revoke only grants | Assert that task is idempotent + assert: + that: + - result is succeeded + - result is not changed + + always: + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_1 }}" diff --git a/tests/integration/targets/test_mysql_user/tasks/test_tls_requirements.yml b/tests/integration/targets/test_mysql_user/tasks/test_tls_requirements.yml new file mode 100644 index 0000000..e7c25ce --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/test_tls_requirements.yml @@ -0,0 +1,199 @@ +--- +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + + - name: Tls reqs | Create user with TLS requirements in check mode (expect changed=true) + mysql_user: + <<: *mysql_params + name: "{{ user_name_1 }}" + password: "{{ user_password_1 }}" + tls_requires: + SSL: + check_mode: yes + register: result + + - name: Tls reqs | Assert check mode user create reports changed state + assert: + that: + - result is changed + + - include_tasks: utils/assert_no_user.yml + vars: + user_name: "{{user_name_1}}" + + - name: Tls reqs | Create user with TLS requirements state=present (expect changed=true) + mysql_user: + <<: *mysql_params + name: '{{ item[0] }}' + password: '{{ user_password_1 }}' + tls_requires: '{{ item[1] }}' + with_together: + - [ '{{ user_name_1 }}', '{{ user_name_2 }}', '{{ user_name_3 }}'] + - + - SSL: + - X509: + - subject: '/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' + cipher: 'ECDHE-ECDSA-AES256-SHA384' + issuer: '/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' + + - block: + - name: Tls reqs | Retrieve TLS requirements for users in old database version + command: "{{ mysql_command }} -L -N -s -e \"SHOW GRANTS for '{{ item }}'@'localhost'\"" + register: old_result + with_items: ['{{ user_name_1 }}', '{{ user_name_2 }}', '{{ user_name_3 }}'] + + - name: Tls reqs | Set old database separator + set_fact: + separator: '\n' + # Semantically: when mysql version <= 5.6 or MariaDB version <= 10.1 + when: + - (db_engine == 'mysql' and db_version is version('5.6', '<=')) + or (db_engine == 'mariadb' and db_version is version('10.1', '<=')) + + - block: + - name: Tls reqs | Retrieve TLS requirements for users in new database version + command: "{{ mysql_command }} -L -N -s -e \"SHOW CREATE USER '{{ item }}'@'localhost'\"" + register: new_result + with_items: ['{{ user_name_1 }}', '{{ user_name_2 }}', '{{ user_name_3 }}'] + + - name: Tls reqs | Set new database separator + set_fact: + separator: 'PASSWORD' + # Semantically: when mysql version >= 5.7 or MariaDB version >= 10.2 + when: + - (db_engine == 'mysql' and db_version is version('5.7', '>=')) + or (db_engine == 'mariadb' and db_version is version('10.2', '>=')) + + - block: + - name: Tls reqs | Assert user1 TLS requirements + assert: + that: + - "'SSL' in reqs" + vars: + reqs: "{{ ((old_result.results[0] is skipped | ternary(new_result, old_result)).results | selectattr('item', 'contains', user_name_1) | first).stdout.split('REQUIRE')[1].split(separator)[0].strip() }}" + + - name: Tls reqs | Assert user2 TLS requirements + assert: + that: + - "'X509' in reqs" + vars: + reqs: "{{ ((old_result.results[0] is skipped | ternary(new_result, old_result)).results | selectattr('item', 'contains', user_name_2) | first).stdout.split('REQUIRE')[1].split(separator)[0].strip() }}" + + - name: Tls reqs | Assert user3 TLS requirements + assert: + that: + - "'/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('contains', 'SUBJECT') | first)" + - "'/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('contains', 'ISSUER') | first)" + - "'ECDHE-ECDSA-AES256-SHA384' in (reqs | select('contains', 'CIPHER') | first)" + vars: + reqs: "{{ ((old_result.results[0] is skipped | ternary(new_result, old_result)).results | selectattr('item', 'contains', user_name_3) | first).stdout.split('REQUIRE')[1].split(separator)[0].replace(\"' \", \"':\").split(\":\") }}" + # CentOS 6 uses an older version of jinja that does not provide the selectattr filter. + when: ansible_distribution != 'CentOS' or ansible_distribution_major_version != '6' + + - name: Tls reqs | Modify user with TLS requirements state=present in check mode (expect changed=true) + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + tls_requires: + X509: + check_mode: yes + register: result + + - name: Tls reqs | Assert check mode user update reports changed state + assert: + that: + - result is changed + + - name: Tls reqs | Retrieve TLS requirements for users in old database version + command: "{{ mysql_command }} -L -N -s -e \"SHOW GRANTS for '{{ user_name_1 }}'@'localhost'\"" + register: old_result + when: + - (db_engine == 'mysql' and db_version is version('5.6', '<=')) + or (db_engine == 'mariadb' and db_version is version('10.2', '<')) + + - name: Tls reqs | Retrieve TLS requirements for users in new database version + command: "{{ mysql_command }} -L -N -s -e \"SHOW CREATE USER '{{ user_name_1 }}'@'localhost'\"" + register: new_result + when: + - (db_engine == 'mysql' and db_version is version('5.7', '>=')) + or (db_engine == 'mariadb' and db_version is version('10.2', '>=')) + + - name: Tls reqs | Assert user1 TLS requirements was not changed + assert: + that: "'SSL' in reqs" + vars: + reqs: "{{ (old_result is skipped | ternary(new_result, old_result)).stdout.split('REQUIRE')[1].split(separator)[0].strip() }}" + + - name: Tls reqs | Modify user with TLS requirements state=present (expect changed=true) + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + tls_requires: + X509: + + - name: Tls reqs | Retrieve TLS requirements for users in old database version + command: "{{ mysql_command }} -L -N -s -e \"SHOW GRANTS for '{{ user_name_1 }}'@'localhost'\"" + register: old_result + when: + - (db_engine == 'mysql' and db_version is version('5.6', '<=')) + or (db_engine == 'mariadb' and db_version is version('10.2', '<')) + + - name: Tls reqs | Retrieve TLS requirements for users in new database version + command: "{{ mysql_command }} -L -N -s -e \"SHOW CREATE USER '{{ user_name_1 }}'@'localhost'\"" + register: new_result + when: + - (db_engine == 'mysql' and db_version is version('5.7', '>=')) + or (db_engine == 'mariadb' and db_version is version('10.2', '>=')) + + - name: Tls reqs | Assert user1 TLS requirements + assert: + that: "'X509' in reqs" + vars: + reqs: "{{ (old_result is skipped | ternary(new_result, old_result)).stdout.split('REQUIRE')[1].split(separator)[0].strip() }}" + + - name: Tls reqs | Remove TLS requirements from user (expect changed=true) + mysql_user: + <<: *mysql_params + name: '{{ user_name_1 }}' + password: '{{ user_password_1 }}' + tls_requires: + + - name: Tls reqs | Retrieve TLS requirements for users + command: "{{ mysql_command }} -L -N -s -e \"SHOW CREATE USER '{{ user_name_1 }}'@'localhost'\"" + register: result + + - name: Tls reqs | Assert user1 TLS requirements + assert: + that: "'REQUIRE ' not in result.stdout or 'REQUIRE NONE' in result.stdout" + + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{user_name_1}}" + + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{user_name_2}}" + + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{user_name_3}}" + + - include_tasks: utils/assert_no_user.yml + vars: + user_name: "{{user_name_1}}" + + - include_tasks: utils/assert_no_user.yml + vars: + user_name: "{{user_name_2}}" + + - include_tasks: utils/assert_no_user.yml + vars: + user_name: "{{user_name_3}}" diff --git a/tests/integration/targets/test_mysql_user/tasks/test_update_password.yml b/tests/integration/targets/test_mysql_user/tasks/test_update_password.yml new file mode 100644 index 0000000..adaa7c7 --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/test_update_password.yml @@ -0,0 +1,155 @@ +--- +# Tests scenarios for both plaintext and encrypted user passwords. + +- vars: + mysql_parameters: + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + test_password1: kbB9tcx5WOGVGfzV + test_password1_hash: '*AF6A7F9D038475C17EE46564F154104877EE5037' + test_password2: XBYjpHmjIctMxl1y + test_password2_hash: '*9E22D1B35C68BDDF398B8F28AE482E5A865BAC0A' + test_password3: tem33JfR5Yx98BB + test_password3_hash: '*C7E7C2710702F20336F8D93BC0670C8FB66BDBC7' + + + block: + - include_tasks: utils/assert_user_password.yml + vars: + username: "{{ item.username }}" + host: "%" + update_password: "{{ item.update_password }}" + password: "{{ test_password1 }}" + expect_change: "{{ item.expect_change }}" + expect_password_change: "{{ item.expect_change }}" + expect_password_hash: "{{ test_password1_hash }}" + loop: + # all variants set the password when nothing exists + - username: test1 + update_password: always + expect_change: true + - username: test2 + update_password: on_create + expect_change: true + - username: test3 + update_password: on_new_username + expect_change: true + + # assert idempotency + - username: test1 + update_password: always + expect_change: false + - username: test2 + update_password: on_create + expect_change: false + - username: test3 + update_password: on_new_username + expect_change: false + + # same user, new password + - include_tasks: utils/assert_user_password.yml + vars: + username: "{{ item.username }}" + host: "%" + update_password: "{{ item.update_password }}" + password: "{{ test_password2 }}" + expect_change: "{{ item.expect_change }}" + expect_password_change: "{{ item.expect_change }}" + expect_password_hash: "{{ item.expect_password_hash }}" + loop: + - username: test1 + update_password: always + expect_change: true + expect_password_hash: "{{ test_password2_hash }}" + - username: test2 + update_password: on_create + expect_change: false + expect_password_hash: "{{ test_password1_hash }}" + - username: test3 + update_password: on_new_username + expect_change: false + expect_password_hash: "{{ test_password1_hash }}" + + # new user, new password + - include_tasks: utils/assert_user_password.yml + vars: + username: "{{ item.username }}" + host: '::1' + update_password: "{{ item.update_password }}" + password: "{{ item.password }}" + expect_change: "{{ item.expect_change }}" + expect_password_change: "{{ item.expect_password_change }}" + expect_password_hash: "{{ item.expect_password_hash }}" + loop: + - username: test1 + update_password: always + expect_change: true + expect_password_change: true + password: "{{ test_password1 }}" + expect_password_hash: "{{ test_password1_hash }}" + - username: test2 + update_password: on_create + expect_change: true + expect_password_change: true + password: "{{ test_password2 }}" + expect_password_hash: "{{ test_password2_hash }}" + - username: test3 + update_password: on_new_username + expect_change: true + expect_password_change: false + password: "{{ test_password2 }}" + expect_password_hash: "{{ test_password1_hash }}" + + # prepare for next test: ensure all users have varying passwords + - username: test3 + update_password: always + expect_change: true + expect_password_change: true + password: "{{ test_password2 }}" + expect_password_hash: "{{ test_password2_hash }}" + + # another new user, another new password and multiple existing users with varying passwords + - include_tasks: utils/assert_user_password.yml + vars: + username: "{{ item.username }}" + host: '2001:db8::1' + update_password: "{{ item.update_password }}" + password: "{{ test_password3 }}" + expect_change: true + expect_password_change: true + expect_password_hash: "{{ test_password3_hash }}" + loop: + - username: test1 + update_password: always + - username: test2 + update_password: on_create + - username: test3 + update_password: on_new_username + + # another new user, another new password and multiple existing users with + # varying passwords without providing a password + - name: update_password | Create account with on_new_username while omit password + community.mysql.mysql_user: + login_user: '{{ mysql_parameters.login_user }}' + login_password: '{{ mysql_parameters.login_password }}' + login_host: '{{ mysql_parameters.login_host }}' + login_port: '{{ mysql_parameters.login_port }}' + state: present + name: test3 + host: '10.10.10.10' + update_password: on_new_username + + - name: update_password | Assert create account with on_new_username while omit password produce empty auth string + ansible.builtin.command: >- + {{ mysql_command }} -BNe "SELECT user, host, plugin, authentication_string + FROM mysql.user where user='test3' and host='10.10.10.10'" + register: test3_info + changed_when: false + failed_when: + # MariaDB default plugin is mysql_native_password + - "'test3\t10.10.10.10\tmysql_native_password\t' != test3_info.stdout" + + # MySQL 8+ default plugin is caching_sha2_password + - "'test3\t10.10.10.10\tcaching_sha2_password\t' != test3_info.stdout" diff --git a/tests/integration/targets/test_mysql_user/tasks/test_user_attributes.yml b/tests/integration/targets/test_mysql_user/tasks/test_user_attributes.yml new file mode 100644 index 0000000..b5cec10 --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/test_user_attributes.yml @@ -0,0 +1,474 @@ +--- +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + + - when: db_engine == 'mariadb' + block: + + # ============================================================ + # Fail creating a user with mariadb + # + + # Check mode + - name: Attributes | Attempt to create user with attributes with mariadb in check mode + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + password: '{{ user_password_2 }}' + attributes: + key1: "value1" + ignore_errors: yes + register: result_module + check_mode: yes + + - name: Attributes | Run query to verify user creation with attributes fails with mariadb in check mode + mysql_query: + <<: *mysql_params + query: 'SELECT user FROM mysql.user WHERE user = "{{ user_name_2 }}" AND host = "%"' + ignore_errors: yes + register: result_query + + - name: Attributes | Assert that creating user with attributes fails with mariadb in check mode + assert: + that: + - result_module is failed + - not result_query.query_result[0] + + # Real mode + - name: Attributes | Attempt to create user with attributes with mariadb + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + password: '{{ user_password_2 }}' + attributes: + key1: "value1" + ignore_errors: yes + register: result_module + + - name: Attributes | Run query to verify user creation with attributes fails with mariadb + mysql_query: + <<: *mysql_params + query: 'SELECT user FROM mysql.user WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query + + - name: Attributes | Assert that creating user with attributes fails with mariadb + assert: + that: + - result_module is failed + - not result_query.query_result[0] + + - when: db_engine == 'mysql' + block: + + # ============================================================ + # Create user with no attributes (test attributes return type) + # + + # Check mode + - name: Attributes | Test creating a user with no attributes in check mode + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + password: '{{ user_password_2 }}' + register: result_module + check_mode: yes + + - name: Attributes | Run query to verify user creation with no attributes did not take place in check mode + mysql_query: + <<: *mysql_params + query: 'SELECT user FROM mysql.user WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query + + - name: Attributes | Assert that user would have been created without attributes + assert: + that: + - result_module is changed + - result_module.attributes is none + - not result_query.query_result[0] + + # Real mode + - name: Attributes | Test creating a user with no attributes + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + password: '{{ user_password_2 }}' + register: result_module + + - name: Attributes | Run query to verify created user without attributes + mysql_query: + <<: *mysql_params + query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query + + - name: Attributes | Assert that user was created without attributes + assert: + that: + - result_module is changed + - result_module.attributes is none + - result_query.query_result[0][0]['ATTRIBUTE'] is none + + # Clean up user to allow it to be recreated with attributes + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_2 }}" + + # ============================================================ + # Create user with attributes + # + + # Check mode + - name: Attributes | Test creating a user with attributes in check mode + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + password: '{{ user_password_2 }}' + attributes: + key1: "value1" + register: result_module + check_mode: yes + + - name: Attributes | Run query to verify user creation did not take place in check mode + mysql_query: + <<: *mysql_params + query: 'SELECT user FROM mysql.user WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query + + - name: Attributes | Assert that user would have been created with attributes + assert: + that: + - result_module is changed + - result_module.attributes.key1 == "value1" + - not result_query.query_result[0] + + # Real mode + - name: Attributes | Test creating a user with attributes + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + password: '{{ user_password_2 }}' + attributes: + key1: "value1" + register: result_module + + - name: Attributes | Run query to verify created user attributes + mysql_query: + <<: *mysql_params + query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query + + - name: Attributes | Assert that user was created with attributes + assert: + that: + - result_module is changed + - result_module.attributes.key1 == "value1" + - (result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml)['key1'] == "value1" + + # ============================================================ + # Append attributes on an existing user + # + + # Check mode + - name: Attributes | Test appending attributes to an existing user in check mode + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + attributes: + key2: "value2" + register: result_module + check_mode: yes + + - name: Attributes | Run query to check appended attributes in check mode + mysql_query: + <<: *mysql_params + query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query + + - name: Attributes | Assert that attribute would have been appended and existing attribute stays + assert: + that: + - result_module is changed + - result_module.attributes.key1 == "value1" + - result_module.attributes.key2 == "value2" + - "'key2' not in result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml" + + # Real mode + - name: Attributes | Test appending attributes to an existing user + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + attributes: + key2: "value2" + register: result_module + + - name: Attributes | Run query to check appended attributes + mysql_query: + <<: *mysql_params + query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query + + - name: Attributes | Assert that new attribute was appended and existing attribute stays + assert: + that: + - result_module is changed + - result_module.attributes.key1 == "value1" + - result_module.attributes.key2 == "value2" + - (result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml)['key1'] == "value1" + - (result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml)['key2'] == "value2" + + # ============================================================ + # Test updating existing attributes + # + + # Check mode + - name: Attributes | Test updating attributes on an existing user in check mode + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + attributes: + key2: "new_value2" + check_mode: yes + register: result_module + + - name: Attributes | Run query to verify updated attribute in check mode + mysql_query: + <<: *mysql_params + query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query + + - name: Attributes | Assert that attribute would have been updated + assert: + that: + - result_module is changed + - result_module.attributes.key2 == "new_value2" + - (result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml)['key2'] == "value2" + + # Real mode + - name: Attributes | Test updating attributes on an existing user + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + attributes: + key2: "new_value2" + register: result_module + + - name: Attributes | Run query to verify updated attribute + mysql_query: + <<: *mysql_params + query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query + + - name: Attributes | Assert that attribute was updated + assert: + that: + - result_module is changed + - result_module.attributes.key2 == "new_value2" + - (result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml)['key2'] == "new_value2" + + # ============================================================ + # Test attribute idempotency when specifying attributes + # + + # Check mode + - name: Attributes | Test attribute idempotency by trying to change an already correct attribute in check mode + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + attributes: + key1: "value1" + register: result_module + check_mode: yes + + - name: Attributes | Run query to verify idempotency of already correct attribute in check mode + mysql_query: + <<: *mysql_params + query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query + + - name: Attributes | Assert that attribute would not have been updated + assert: + that: + - result_module is not changed + - result_module.attributes.key1 == "value1" + - (result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml)['key1'] == "value1" + + # Real mode + - name: Attributes | Test attribute idempotency by trying to change an already correct attribute + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + attributes: + key1: "value1" + register: result_module + + - name: Attributes | Run query to verify idempotency of already correct attribute + mysql_query: + <<: *mysql_params + query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query + + - name: Attributes | Assert that attribute was not updated + assert: + that: + - result_module is not changed + - result_module.attributes.key1 == "value1" + - (result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml)['key1'] == "value1" + + # ============================================================ + # Test attribute idempotency when not specifying attribute parameter + # + + # Check mode + - name: Attributes | Test attribute idempotency by not specifying attribute parameter in check mode + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + register: result_module + check_mode: yes + + - name: Attributes | Run query to verify idempotency when not specifying attribute parameter in check mode + mysql_query: + <<: *mysql_params + query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query + + - name: Attributes | Assert that attribute is returned in check mode + assert: + that: + - result_module is not changed + - result_module.attributes.key1 == "value1" + - (result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml)['key1'] == "value1" + + # Real mode + - name: Attributes | Test attribute idempotency by not specifying attribute parameter + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + register: result_module + + - name: Attributes | Run query to verify idempotency when not specifying attribute parameter + mysql_query: + <<: *mysql_params + query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query + + - name: Attributes | Assert that attribute is returned + assert: + that: + - result_module is not changed + - result_module.attributes.key1 == "value1" + - (result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml)['key1'] == "value1" + + # ============================================================ + # Test deleting attributes + # + + # Check mode + - name: Attributes | Test deleting attributes on an existing user in check mode + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + attributes: + key2: null + register: result_module + check_mode: yes + + - name: Attributes | Run query to verify deleted attribute in check mode + mysql_query: + <<: *mysql_params + query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query + + - name: Attributes | Assert that attribute would have been deleted + assert: + that: + - result_module is changed + - "'key2' not in result_module.attributes" + - (result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml)['key2'] == "new_value2" + + # Real mode + - name: Attributes | Test deleting attributes on an existing user + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + attributes: + key2: null + register: result_module + + - name: Attributes | Run query to verify deleted attribute + mysql_query: + <<: *mysql_params + query: 'SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = "{{ user_name_2 }}" AND host = "%"' + register: result_query + + - name: Attributes | Assert that attribute was deleted + assert: + that: + - result_module is changed + - "'key2' not in result_module.attributes" + - "'key2' not in result_query.query_result[0][0]['ATTRIBUTE'] | from_yaml" + + # ============================================================ + # Test attribute return value when no attributes exist + # + + # Check mode + - name: Attributes | Test attributes return value when no attributes exist in check mode + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + attributes: + key1: null + register: result_module + check_mode: yes + + - name: Attributes | Assert attributes return value when no attributes exist in check mode + assert: + that: + - result_module is changed + - result_module.attributes is none + + # Real mode + - name: Attributes | Test attributes return value when no attributes exist + mysql_user: + <<: *mysql_params + name: '{{ user_name_2 }}' + host: '%' + attributes: + key1: null + register: result_module + + - name: Attributes | Assert attributes return value when no attributes exist + assert: + that: + - result_module is changed + - result_module.attributes is none + + # ============================================================ + # Cleanup + # + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_2 }}" 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..c9714b7 --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/test_user_grants_with_roles_applied.yml @@ -0,0 +1,92 @@ +--- +# https://github.com/ansible-collections/community.mysql/issues/231 +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + + - name: User grants with roles applied | Skip unsupported versions + meta: end_play + when: + - db_engine == 'mysql' + - db_version is version('8.0.0', '<') + + - name: User grants with roles applied | Create test databases + mysql_db: + <<: *mysql_params + name: '{{ item }}' + state: present + loop: + - data1 + - data2 + + - name: User grants with roles applied | Create user with privileges + mysql_user: + <<: *mysql_params + name: '{{ user_name_3 }}' + password: '{{ user_password_3 }}' + priv: + "data1.*": "SELECT" + "data2.*": "SELECT" + state: present + + - name: User grants with roles applied | 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: User grants with roles applied | Create role + mysql_role: + <<: *mysql_params + name: test231 + members: + - '{{ user_name_3 }}@localhost' + + - name: User grants with roles applied | Try to change privs + mysql_user: + <<: *mysql_params + name: '{{ user_name_3 }}' + priv: + "data1.*": "INSERT" + "data2.*": "INSERT" + state: present + + - name: User grants with roles applied | 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: User grants with roles applied | Assert user has giving privileges + assert: + that: + - "'GRANT INSERT ON `data1`.*' in result.stdout" + - "'GRANT INSERT ON `data2`.*' in result.stdout" + + ########## + # Clean up + - name: User grants with roles applied | Drop test databases + mysql_db: + <<: *mysql_params + name: '{{ item }}' + state: absent + loop: + - data1 + - data2 + + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ user_name_3 }}" + + - name: User grants with roles applied | Drop test role + mysql_role: + <<: *mysql_params + name: test231 + state: absent diff --git a/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml b/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml new file mode 100644 index 0000000..3990610 --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/test_user_locking.yml @@ -0,0 +1,200 @@ +--- + +- vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + + block: + + # ========================= Prepare ======================================= + - name: Mysql_user Lock user | Create a test database + community.mysql.mysql_db: + <<: *mysql_params + name: mysql_lock_user_test + state: present + + # ========================== Tests ======================================== + + - name: Mysql_user Lock user | create locked | Create test user + community.mysql.mysql_user: + <<: *mysql_params + name: mysql_locked_user + password: 'msandbox' + locked: true + priv: + 'mysql_lock_user_test.*': 'SELECT' + + - name: Mysql_user Lock user | create locked | Assert that test user is locked + community.mysql.mysql_query: + <<: *mysql_params + query: + - SHOW CREATE USER 'mysql_locked_user'@'localhost' + register: locked_user_creation + failed_when: + - locked_user_creation.query_result[0][0] is not search('ACCOUNT LOCK') + + - name: 'Mysql_user Lock user | create locked | Idempotence check' + check_mode: true + community.mysql.mysql_user: + <<: *mysql_params + name: mysql_locked_user + locked: true + priv: + 'mysql_lock_user_test.*': 'SELECT' + register: idempotence_check + failed_when: idempotence_check is changed + + - name: 'Mysql_user Lock user | create locked | Check that absense of locked does not unlock the user' + check_mode: true + community.mysql.mysql_user: + <<: *mysql_params + name: mysql_locked_user + priv: + 'mysql_lock_user_test.*': 'SELECT' + register: idempotence_check + failed_when: idempotence_check is changed + + - name: 'Mysql_user Lock user | create locked | Unlock test user check_mode: true' + check_mode: true + community.mysql.mysql_user: + <<: *mysql_params + name: mysql_locked_user + locked: false + priv: + 'mysql_lock_user_test.*': 'SELECT' + + - name: Mysql_user Lock user | create locked | Assert that test user is locked + community.mysql.mysql_query: + <<: *mysql_params + query: + - SHOW CREATE USER 'mysql_locked_user'@'localhost' + register: locked_user_creation + failed_when: + - locked_user_creation.query_result[0][0] is not search('ACCOUNT LOCK') + + - name: Mysql_user Lock user | create locked | Unlock test user + community.mysql.mysql_user: + <<: *mysql_params + name: mysql_locked_user + locked: false + priv: + 'mysql_lock_user_test.*': 'SELECT' + + - name: Mysql_user Lock user | create locked | Assert that test user is not locked + community.mysql.mysql_query: + <<: *mysql_params + query: + - SHOW CREATE USER 'mysql_locked_user'@'localhost' + register: locked_user_creation + failed_when: + - locked_user_creation.query_result[0][0] is search('ACCOUNT LOCK') + + - name: Mysql_user Lock user | create locked | Remove test user + community.mysql.mysql_user: + <<: *mysql_params + name: mysql_locked_user + state: absent + + - name: Mysql_user Lock user | create unlocked | Create test user + community.mysql.mysql_user: + <<: *mysql_params + name: mysql_locked_user + password: 'msandbox' + locked: false + priv: + 'mysql_lock_user_test.*': 'SELECT' + + - name: Mysql_user Lock user | create unlocked | Assert that test user is not locked + community.mysql.mysql_query: + <<: *mysql_params + query: + - SHOW CREATE USER 'mysql_locked_user'@'localhost' + register: locked_user_creation + failed_when: + - locked_user_creation.query_result[0][0] is search('ACCOUNT LOCK') + + - name: 'Mysql_user Lock user | create unlocked | Idempotence check' + check_mode: true + community.mysql.mysql_user: + <<: *mysql_params + name: mysql_locked_user + locked: false + priv: + 'mysql_lock_user_test.*': 'SELECT' + register: idempotence_check + failed_when: idempotence_check is changed + + - name: 'Mysql_user Lock user | create unlocked | Lock test user check_mode: true' + check_mode: true + community.mysql.mysql_user: + <<: *mysql_params + name: mysql_locked_user + locked: true + priv: + 'mysql_lock_user_test.*': 'SELECT' + + - name: Mysql_user Lock user | create unlocked | Assert that test user is not locked + community.mysql.mysql_query: + <<: *mysql_params + query: + - SHOW CREATE USER 'mysql_locked_user'@'localhost' + register: locked_user_creation + failed_when: + - locked_user_creation.query_result[0][0] is search('ACCOUNT LOCK') + + - name: Mysql_user Lock user | create unlocked | Lock test user + community.mysql.mysql_user: + <<: *mysql_params + name: mysql_locked_user + locked: true + priv: + 'mysql_lock_user_test.*': 'SELECT' + + - name: Mysql_user Lock user | create unlocked | Assert that test user is locked + community.mysql.mysql_query: + <<: *mysql_params + query: + - SHOW CREATE USER 'mysql_locked_user'@'localhost' + register: locked_user_creation + failed_when: + - locked_user_creation.query_result[0][0] is not search('ACCOUNT LOCK') + + - name: Mysql_user Lock user | create unlocked | Remove test user + community.mysql.mysql_user: + <<: *mysql_params + name: mysql_locked_user + state: absent + + - name: Mysql_user Lock user | create default | Create test user + community.mysql.mysql_user: + <<: *mysql_params + name: mysql_locked_user + password: 'msandbox' + priv: + 'mysql_lock_user_test.*': 'SELECT' + + - name: Mysql_user Lock user | create default | Assert that test user is not locked + community.mysql.mysql_query: + <<: *mysql_params + query: + - SHOW CREATE USER 'mysql_locked_user'@'localhost' + register: locked_user_creation + failed_when: + - locked_user_creation.query_result[0][0] is search('ACCOUNT LOCK') + + - name: Mysql_user Lock user | create default | Remove test user + community.mysql.mysql_user: + <<: *mysql_params + name: mysql_locked_user + state: absent + + # ========================= Teardown ====================================== + + - name: Mysql_user Lock user | Delete test database + community.mysql.mysql_db: + <<: *mysql_params + name: mysql_lock_user_test + state: absent diff --git a/tests/integration/targets/test_mysql_user/tasks/test_user_password.yml b/tests/integration/targets/test_mysql_user/tasks/test_user_password.yml index f3b0e06..cffc052 100644 --- a/tests/integration/targets/test_mysql_user/tasks/test_user_password.yml +++ b/tests/integration/targets/test_mysql_user/tasks/test_user_password.yml @@ -1,10 +1,11 @@ +--- # Tests scenarios for both plaintext and encrypted user passwords. - vars: mysql_parameters: &mysql_params login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' test_user_name: 'test_user_password' initial_password: 'a5C8SN*DBa0%a75sGz' @@ -20,23 +21,28 @@ # Test setting plaintext password and changing it. # - - name: Create user with initial password + - name: Password | Create user with initial password mysql_user: <<: *mysql_params name: '{{ test_user_name }}' + host: '%' password: '{{ initial_password }}' priv: '{{ test_default_priv }}' state: present register: result - - name: Assert that a change occurred because the user was added + - name: Password | Assert that a change occurred because the user was added assert: that: - - "result.changed == true" + - result is changed - - include: assert_user.yml user_name={{ test_user_name }} priv={{ test_default_priv_type }} + - include_tasks: utils/assert_user.yml + vars: + user_name: "{{ test_user_name }}" + user_host: "%" + priv: "{{ test_default_priv_type }}" - - name: Get the MySQL version using the newly created used creds + - name: Password | Get the MySQL version using the newly created used creds mysql_info: login_user: '{{ test_user_name }}' login_password: '{{ initial_password }}' @@ -46,43 +52,53 @@ register: result ignore_errors: true - - name: Assert that mysql_info was successful + - name: Password | Assert that mysql_info was successful assert: that: - - "result.failed == false" + - result is succeeded - - name: Run mysql_user again without any changes + - name: Password | Run mysql_user again without any changes mysql_user: <<: *mysql_params - name: '{{ test_user_name }}' - password: '{{ initial_password }}' - priv: '{{ test_default_priv }}' + name: "{{ test_user_name }}" + host: "%" + password: "{{ initial_password }}" + priv: "{{ test_default_priv }}" state: present register: result - - name: Assert that there weren't any changes because username/password didn't change + - name: Password | Assert that there weren't any changes because username/password didn't change assert: that: - - "result.changed == false" + - result is not changed - - include: assert_user.yml user_name={{ test_user_name }} priv={{ test_default_priv_type }} + - include_tasks: utils/assert_user.yml + vars: + user_name: "{{ test_user_name }}" + user_host: "%" + priv: "{{ test_default_priv_type }}" - - name: Update the user password + - name: Password | Update the user password mysql_user: <<: *mysql_params - name: '{{ test_user_name }}' - password: '{{ new_password }}' + name: "{{ test_user_name }}" + host: "%" + password: "{{ new_password }}" state: present register: result - - name: Assert that a change occurred because the password was updated + - name: Password | Assert that a change occurred because the password was updated assert: that: - - "result.changed == true" + - result is changed - - include: assert_user.yml user_name={{ test_user_name }} priv={{ test_default_priv_type }} + - include_tasks: utils/assert_user.yml + vars: + user_name: "{{ test_user_name }}" + user_host: "%" + priv: "{{ test_default_priv_type }}" - - name: Get the MySQL version data using the original password (should fail) + - name: Password | Get the MySQL version data using the original password (should fail) mysql_info: login_user: '{{ test_user_name }}' login_password: '{{ initial_password }}' @@ -92,12 +108,12 @@ register: result ignore_errors: true - - name: Assert that the mysql_info module failed because we used the old password + - name: Password | Assert that the mysql_info module failed because we used the old password assert: that: - - "result.failed == true" + - result is failed - - name: Get the MySQL version data using the new password (should work) + - name: Password | Get the MySQL version data using the new password (should work) mysql_info: login_user: '{{ test_user_name }}' login_password: '{{ new_password }}' @@ -107,19 +123,21 @@ register: result ignore_errors: true - - name: Assert that the mysql_info module succeeded because we used the new password + - name: Password | Assert that the mysql_info module succeeded because we used the new password assert: that: - - "result.failed == false" + - result is succeeded # Cleanup - - include: remove_user.yml user_name={{ test_user_name }} user_password={{ new_password }} + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ test_user_name }}" # ============================================================ # Test setting a plaintext password and then the same password encrypted to ensure there isn't a change detected. # - - name: Create user with initial password + - name: Password | Create user with initial password mysql_user: <<: *mysql_params name: '{{ test_user_name }}' @@ -128,14 +146,18 @@ state: present register: result - - name: Assert that a change occurred because the user was added + - name: Password | Assert that a change occurred because the user was added assert: that: - - "result.changed == true" + - result is changed - - include: assert_user.yml user_name={{ test_user_name }} priv={{ test_default_priv_type }} + - include_tasks: utils/assert_user.yml + vars: + user_name: "{{ test_user_name }}" + user_host: "localhost" + priv: "{{ test_default_priv_type }}" - - name: Pass in the same password as before, but in the encrypted form (no change expected) + - name: Password | Pass in the same password as before, but in the encrypted form (no change expected) mysql_user: <<: *mysql_params name: '{{ test_user_name }}' @@ -145,36 +167,43 @@ state: present register: result - - name: Assert that there weren't any changes because username/password didn't change + - name: Password | Assert that there weren't any changes because username/password didn't change assert: that: - - "result.changed == false" + - result is not changed # Cleanup - - include: remove_user.yml user_name={{ test_user_name }} user_password={{ new_password }} + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ test_user_name }}" # ============================================================ # Test setting an encrypted password and then the same password in plaintext to ensure there isn't a change. # - - name: Create user with initial password + - name: Password | Create user with initial password mysql_user: <<: *mysql_params name: '{{ test_user_name }}' + host: "%" password: '{{ initial_password_encrypted }}' encrypted: yes priv: '{{ test_default_priv }}' state: present register: result - - name: Assert that a change occurred because the user was added + - name: Password | Assert that a change occurred because the user was added assert: that: - - "result.changed == true" + - result is changed - - include: assert_user.yml user_name={{ test_user_name }} priv={{ test_default_priv_type }} + - include_tasks: utils/assert_user.yml + vars: + user_name: "{{ test_user_name }}" + user_host: "%" + priv: "{{ test_default_priv_type }}" - - name: Get the MySQL version data using the new creds + - name: Password | Get the MySQL version data using the new creds mysql_info: login_user: '{{ test_user_name }}' login_password: '{{ initial_password }}' @@ -184,60 +213,64 @@ register: result ignore_errors: true - - name: Assert that the mysql_info module succeeded because we used the new password + - name: Password | Assert that the mysql_info module succeeded because we used the new password assert: that: - - "result.failed == false" + - result is succeeded - - name: Pass in the same password as before, but in the encrypted form (no change expected) + - name: Password | Pass in the same password as before, but in the encrypted form (no change expected) mysql_user: <<: *mysql_params name: '{{ test_user_name }}' + host: "%" password: '{{ initial_password }}' state: present register: result - - name: Assert that there weren't any changes because username/password didn't change + - name: Password | Assert that there weren't any changes because username/password didn't change assert: that: - - "result.changed == false" + - result is not changed # Cleanup - - include: remove_user.yml user_name={{ test_user_name }} user_password={{ new_password }} + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ test_user_name }}" # ============================================================ # Test setting an empty password. # - - name: Create user with empty password + - name: Password | Create user with empty password mysql_user: <<: *mysql_params - name: '{{ test_user_name }}' - priv: '{{ test_default_priv }}' + name: "{{ test_user_name }}" + host: "%" + priv: "{{ test_default_priv }}" state: present register: result - - name: Assert that a change occurred because the user was added + - name: Password | Assert that a change occurred because the user was added assert: that: - - "result.changed == true" + - result is changed - - name: Get the MySQL version using an empty password for the newly created user + - name: Password | Get the MySQL version using an empty password for the newly created user mysql_info: - login_user: '{{ test_user_name }}' - login_password: '' - login_host: '{{ mysql_host }}' - login_port: '{{ mysql_primary_port }}' + login_user: "{{ test_user_name }}" + login_password: "" + login_host: "{{ mysql_host }}" + login_port: "{{ mysql_primary_port }}" filter: version register: result ignore_errors: true - - name: Assert that mysql_info was successful + - name: Password | Assert that mysql_info was successful assert: that: - - "result.failed == false" + - result is succeeded - - name: Get the MySQL version using an non-empty password (should fail) + - name: Password | Get the MySQL version using an non-empty password (should fail) mysql_info: login_user: '{{ test_user_name }}' login_password: 'some_password' @@ -247,23 +280,26 @@ register: result ignore_errors: true - - name: Assert that mysql_info failed + - name: Password | Assert that mysql_info failed assert: that: - - "result.failed == true" + - result is failed - - name: Update the user without changing the password + - name: Password | Update the user without changing the password mysql_user: <<: *mysql_params name: '{{ test_user_name }}' + host: "%" priv: '{{ test_default_priv }}' state: present register: result - - name: Assert that the user wasn't changed because the password is still empty + - name: Password | Assert that the user wasn't changed because the password is still empty assert: that: - - "result.changed == false" + - result is not changed # Cleanup - - include: remove_user.yml user_name={{ test_user_name }} user_password='' + - include_tasks: utils/remove_user.yml + vars: + user_name: "{{ test_user_name }}" diff --git a/tests/integration/targets/test_mysql_user/tasks/test_user_plugin_auth.yml b/tests/integration/targets/test_mysql_user/tasks/test_user_plugin_auth.yml index 3b95d17..f6f3c2e 100644 --- a/tests/integration/targets/test_mysql_user/tasks/test_user_plugin_auth.yml +++ b/tests/integration/targets/test_mysql_user/tasks/test_user_plugin_auth.yml @@ -1,10 +1,11 @@ +--- # Test user plugin auth scenarios. - vars: mysql_parameters: &mysql_params login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' test_user_name: 'test_user_plugin_auth' test_plugin_type: 'mysql_native_password' @@ -12,6 +13,7 @@ test_plugin_auth_string: 'Fdt8fd^34ds' test_plugin_new_hash: '*E74368AC90460FA669F6D41BFB7F2A877DB73745' test_plugin_new_auth_string: 'c$K01LsmK7nJnIR4!h' + test_salt: 'TDwqdanU82d0yNtvaabb' test_default_priv_type: 'SELECT' test_default_priv: '*.*:{{ test_default_priv_type }}' @@ -21,34 +23,39 @@ # Test plugin auth initially setting a hash and then changing to a different hash. # - - name: Create user with plugin auth (with hash string) - mysql_user: + - name: Plugin auth | Create user with plugin auth (with hash string) + community.mysql.mysql_user: <<: *mysql_params name: '{{ test_user_name }}' + host: '%' plugin: '{{ test_plugin_type }}' plugin_hash_string: '{{ test_plugin_hash }}' priv: '{{ test_default_priv }}' register: result - - name: Get user information - command: "{{ mysql_command }} -e \"SELECT user, host, plugin FROM mysql.user WHERE user = '{{ test_user_name }}' and host = 'localhost'\"" + - name: Plugin auth | Get user information (with hash string) + ansible.builtin.command: "{{ mysql_command }} -e \"SELECT user, host, plugin FROM mysql.user WHERE user = '{{ test_user_name }}' and host = '%'\"" register: show_create_user - - name: Check that the module made a change - assert: + - name: Plugin auth | Check that the module made a change (with hash string) + ansible.builtin.assert: that: - - "result.changed == true" + - result is changed - - name: Check that the expected plugin type is set - assert: + - name: Plugin auth | Check that the expected plugin type is set (with hash string) + ansible.builtin.assert: that: - "'{{ test_plugin_type }}' in show_create_user.stdout" - when: install_type == 'mysql' or (install_type == 'mariadb' and mariadb_version is version('10.3', '>=')) + when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>=')) - - include: assert_user.yml user_name={{ test_user_name }} priv={{ test_default_priv_type }} + - ansible.builtin.include_tasks: utils/assert_user.yml + vars: + user_name: "{{ test_user_name }}" + user_host: "%" + priv: "{{ test_default_priv_type }}" - - name: Get the MySQL version using the newly created creds - mysql_info: + - name: Plugin auth | Get the MySQL version using the newly created creds + community.mysql.mysql_info: login_user: '{{ test_user_name }}' login_password: '{{ test_plugin_auth_string }}' login_host: '{{ mysql_host }}' @@ -56,28 +63,33 @@ filter: version register: result - - name: Assert that mysql_info was successful - assert: + - name: Plugin auth | Assert that mysql_info was successful + ansible.builtin.assert: that: - - "result.failed == false" + - result is succeeded - - name: Update the user with a different hash - mysql_user: + - name: Plugin auth | Update the user with a different hash + community.mysql.mysql_user: <<: *mysql_params name: '{{ test_user_name }}' + host: '%' plugin: '{{ test_plugin_type }}' plugin_hash_string: '{{ test_plugin_new_hash }}' register: result - - name: Check that the module makes the change because the hash changed - assert: + - name: Plugin auth | Check that the module makes the change because the hash changed + ansible.builtin.assert: that: - - "result.changed == true" + - result is changed - - include: assert_user.yml user_name={{ test_user_name }} priv={{ test_default_priv_type }} + - ansible.builtin.include_tasks: utils/assert_user.yml + vars: + user_name: "{{ test_user_name }}" + user_host: "%" + priv: "{{ test_default_priv_type }}" - - name: Getting the MySQL info with the new password should work - mysql_info: + - name: Plugin auth | Getting the MySQL info with the new password should work + community.mysql.mysql_info: login_user: '{{ test_user_name }}' login_password: '{{ test_plugin_new_auth_string }}' login_host: '{{ mysql_host }}' @@ -85,46 +97,53 @@ filter: version register: result - - name: Assert that mysql_info was successful - assert: + - name: Plugin auth | Assert that mysql_info was successful + ansible.builtin.assert: that: - - "result.failed == false" + - result is succeeded # Cleanup - - include: remove_user.yml user_name={{ test_user_name }} user_password={{ test_plugin_new_auth_string }} + - ansible.builtin.include_tasks: utils/remove_user.yml + vars: + user_name: "{{ test_user_name }}" # ============================================================ # Test plugin auth initially setting a hash and then switching to a plaintext auth string. # - - name: Create user with plugin auth (with hash string) - mysql_user: + - name: Plugin auth | Create user with plugin auth (with hash string) + community.mysql.mysql_user: <<: *mysql_params name: '{{ test_user_name }}' + host: '%' plugin: '{{ test_plugin_type }}' plugin_hash_string: '{{ test_plugin_hash }}' priv: '{{ test_default_priv }}' register: result - - name: Get user information - command: "{{ mysql_command }} -e \"SELECT user, host, plugin FROM mysql.user WHERE user = '{{ test_user_name }}' and host = 'localhost'\"" + - name: Plugin auth | Get user information + ansible.builtin.command: "{{ mysql_command }} -e \"SELECT user, host, plugin FROM mysql.user WHERE user = '{{ test_user_name }}' and host = '%'\"" register: show_create_user - - name: Check that the module made a change - assert: + - name: Plugin auth | Check that the module made a change (with hash string) + ansible.builtin.assert: that: - - "result.changed == true" + - result is changed - - name: Check that the expected plugin type is set - assert: + - name: Plugin auth | Check that the expected plugin type is set (with hash string) + ansible.builtin.assert: that: - "'{{ test_plugin_type }}' in show_create_user.stdout" - when: install_type == 'mysql' or (install_type == 'mariadb' and mariadb_version is version('10.3', '>=')) + when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>=')) - - include: assert_user.yml user_name={{ test_user_name }} priv={{ test_default_priv_type }} + - ansible.builtin.include_tasks: utils/assert_user.yml + vars: + user_name: "{{ test_user_name }}" + user_host: "%" + priv: "{{ test_default_priv_type }}" - - name: Get the MySQL version using the newly created creds - mysql_info: + - name: Plugin auth | Get the MySQL version using the newly created creds + community.mysql.mysql_info: login_user: '{{ test_user_name }}' login_password: '{{ test_plugin_auth_string }}' login_host: '{{ mysql_host }}' @@ -132,44 +151,50 @@ filter: version register: result - - name: Assert that mysql_info was successful - assert: + - name: Plugin auth | Assert that mysql_info was successful + ansible.builtin.assert: that: - - "result.failed == false" + - result is succeeded - - name: Update the user with the same hash (no change expected) - mysql_user: + - name: Plugin auth | Update the user with the same hash (no change expected) + community.mysql.mysql_user: <<: *mysql_params name: '{{ test_user_name }}' + host: '%' plugin: '{{ test_plugin_type }}' plugin_hash_string: '{{ test_plugin_hash }}' register: result # FIXME: on mariadb 10.2 there's always a change - - name: Check that the module doesn't make a change when the same hash is passed in - assert: + - name: Plugin auth | Check that the module doesn't make a change when the same hash is passed in + ansible.builtin.assert: that: - - "result.changed == false" - when: install_type == 'mysql' or (install_type == 'mariadb' and mariadb_version is version('10.3', '>=')) + - result is not changed + when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>=')) - - include: assert_user.yml user_name={{ test_user_name }} priv={{ test_default_priv_type }} + - ansible.builtin.include_tasks: utils/assert_user.yml + vars: + user_name: "{{ test_user_name }}" + user_host: "%" + priv: "{{ test_default_priv_type }}" - - name: Change the user using the same plugin, but switch to the same auth string in plaintext form - mysql_user: + - name: Plugin auth | Change the user using the same plugin, but switch to the same auth string in plaintext form + community.mysql.mysql_user: <<: *mysql_params name: '{{ test_user_name }}' + host: '%' plugin: '{{ test_plugin_type }}' plugin_auth_string: '{{ test_plugin_auth_string }}' register: result # Expecting a change is currently by design (see comment in source). - - name: Check that the module did not change the password - assert: + - name: Plugin auth | Check that the module did not change the password + ansible.builtin.assert: that: - - "result.changed == true" + - result is changed - - name: Getting the MySQL info should still work - mysql_info: + - name: Plugin auth | Getting the MySQL info should still work + community.mysql.mysql_info: login_user: '{{ test_user_name }}' login_password: '{{ test_plugin_auth_string }}' login_host: '{{ mysql_host }}' @@ -177,46 +202,53 @@ filter: version register: result - - name: Assert that mysql_info was successful - assert: + - name: Plugin auth | Assert that mysql_info was successful + ansible.builtin.assert: that: - - "result.failed == false" + - result is succeeded # Cleanup - - include: remove_user.yml user_name={{ test_user_name }} user_password={{ test_plugin_auth_string }} + - ansible.builtin.include_tasks: utils/remove_user.yml + vars: + user_name: "{{ test_user_name }}" # ============================================================ # Test plugin auth initially setting a plaintext auth string and then switching to a hash. # - - name: Create user with plugin auth (with auth string) - mysql_user: + - name: Plugin auth | Create user with plugin auth (with auth string) + community.mysql.mysql_user: <<: *mysql_params name: '{{ test_user_name }}' + host: '%' plugin: '{{ test_plugin_type }}' plugin_auth_string: '{{ test_plugin_auth_string }}' priv: '{{ test_default_priv }}' register: result - - name: Get user information - command: "{{ mysql_command }} -e \"SHOW CREATE USER '{{ test_user_name }}'@'localhost'\"" + - name: Plugin auth | Get user information(with auth string) + ansible.builtin.command: "{{ mysql_command }} -e \"SHOW CREATE USER '{{ test_user_name }}'@'%'\"" register: show_create_user - - name: Check that the module made a change - assert: + - name: Plugin auth | Check that the module made a change (with auth string) + ansible.builtin.assert: that: - - "result.changed == true" + - result is changed - - name: Check that the expected plugin type is set - assert: + - name: Plugin auth | Check that the expected plugin type is set (with auth string) + ansible.builtin.assert: that: - - "'{{ test_plugin_type }}' in show_create_user.stdout" - when: install_type == 'mysql' or (install_type == 'mariadb' and mariadb_version is version('10.3', '>=')) + - test_plugin_type in show_create_user.stdout + when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>=')) - - include: assert_user.yml user_name={{ test_user_name }} priv={{ test_default_priv_type }} + - ansible.builtin.include_tasks: utils/assert_user.yml + vars: + user_name: "{{ test_user_name }}" + user_host: "%" + priv: "{{ test_default_priv_type }}" - - name: Get the MySQL version using the newly created creds - mysql_info: + - name: Plugin auth | Get the MySQL version using the newly created creds + community.mysql.mysql_info: login_user: '{{ test_user_name }}' login_password: '{{ test_plugin_auth_string }}' login_host: '{{ mysql_host }}' @@ -224,43 +256,49 @@ filter: version register: result - - name: Assert that mysql_info was successful - assert: + - name: Plugin auth | Assert that mysql_info was successful + ansible.builtin.assert: that: - - "result.failed == false" + - result is succeeded - - name: Update the user with the same auth string - mysql_user: + - name: Plugin auth | Update the user with the same auth string + community.mysql.mysql_user: <<: *mysql_params name: '{{ test_user_name }}' + host: '%' plugin: '{{ test_plugin_type }}' plugin_auth_string: '{{ test_plugin_auth_string }}' register: result # This is the current expected behavior because there isn't a reliable way to hash the password in the mysql_user # module in order to be able to compare this password with the stored hash. See the source for more info. - - name: The module should detect a change even though the password is the same - assert: + - name: Plugin auth | The module should detect a change even though the password is the same + ansible.builtin.assert: that: - - "result.changed == true" + - result is changed - - include: assert_user.yml user_name={{ test_user_name }} priv={{ test_default_priv_type }} + - ansible.builtin.include_tasks: utils/assert_user.yml + vars: + user_name: "{{ test_user_name }}" + user_host: "%" + priv: "{{ test_default_priv_type }}" - - name: Change the user using the same plugin, but switch to the same auth string in hash form - mysql_user: + - name: Plugin auth | Change the user using the same plugin, but switch to the same auth string in hash form + community.mysql.mysql_user: <<: *mysql_params name: '{{ test_user_name }}' + host: '%' plugin: '{{ test_plugin_type }}' plugin_hash_string: '{{ test_plugin_hash }}' register: result - - name: Check that the module did not change the password - assert: + - name: Plugin auth | Check that the module did not change the password + ansible.builtin.assert: that: - - "result.changed == false" + - result is not changed - - name: Get the MySQL version using the newly created creds - mysql_info: + - name: Plugin auth | Get the MySQL version using the newly created creds + community.mysql.mysql_info: login_user: '{{ test_user_name }}' login_password: '{{ test_plugin_auth_string }}' login_host: '{{ mysql_host }}' @@ -268,45 +306,52 @@ filter: version register: result - - name: Assert that mysql_info was successful - assert: + - name: Plugin auth | Assert that mysql_info was successful + ansible.builtin.assert: that: - - "result.failed == false" + - result is succeeded # Cleanup - - include: remove_user.yml user_name={{ test_user_name }} user_password={{ test_plugin_auth_string }} + - ansible.builtin.include_tasks: utils/remove_user.yml + vars: + user_name: "{{ test_user_name }}" # ============================================================ # Test plugin auth with an empty auth string. # - - name: Create user with plugin auth (empty auth string) - mysql_user: + - name: Plugin auth | Create user with plugin auth (empty auth string) + community.mysql.mysql_user: <<: *mysql_params name: '{{ test_user_name }}' + host: '%' plugin: '{{ test_plugin_type }}' priv: '{{ test_default_priv }}' register: result - - name: Get user information - command: "{{ mysql_command }} -e \"SHOW CREATE USER '{{ test_user_name }}'@'localhost'\"" + - name: Plugin auth | Get user information (empty auth string) + ansible.builtin.command: "{{ mysql_command }} -e \"SHOW CREATE USER '{{ test_user_name }}'@'%'\"" register: show_create_user - - name: Check that the module made a change - assert: + - name: Plugin auth | Check that the module made a change (empty auth string) + ansible.builtin.assert: that: - - "result.changed == true" + - result is changed - - name: Check that the expected plugin type is set - assert: + - name: Plugin auth | Check that the expected plugin type is set (empty auth string) + ansible.builtin.assert: that: - "'{{ test_plugin_type }}' in show_create_user.stdout" - when: install_type == 'mysql' or (install_type == 'mariadb' and mariadb_version is version('10.3', '>=')) + when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>=')) - - include: assert_user.yml user_name={{ test_user_name }} priv={{ test_default_priv_type }} + - ansible.builtin.include_tasks: utils/assert_user.yml + vars: + user_name: "{{ test_user_name }}" + user_host: "%" + priv: "{{ test_default_priv_type }}" - - name: Get the MySQL version using an empty password for the newly created user - mysql_info: + - name: Plugin auth | Get the MySQL version using an empty password for the newly created user + community.mysql.mysql_info: login_user: '{{ test_user_name }}' login_password: '' login_host: '{{ mysql_host }}' @@ -315,13 +360,13 @@ register: result ignore_errors: true - - name: Assert that mysql_info was successful - assert: + - name: Plugin auth | Assert that mysql_info was successful + ansible.builtin.assert: that: - - "result.failed == false" + - result is succeeded - - name: Get the MySQL version using an non-empty password (should fail) - mysql_info: + - name: Plugin auth | Get the MySQL version using an non-empty password (should fail) + community.mysql.mysql_info: login_user: '{{ test_user_name }}' login_password: 'some_password' login_host: '{{ mysql_host }}' @@ -330,89 +375,265 @@ register: result ignore_errors: true - - name: Assert that mysql_info failed - assert: + - name: Plugin auth | Assert that mysql_info failed + ansible.builtin.assert: that: - - "result.failed == true" + - result is failed - - name: Update the user without changing the auth mechanism - mysql_user: + - name: Plugin auth | Update the user without changing the auth mechanism + community.mysql.mysql_user: <<: *mysql_params name: '{{ test_user_name }}' + host: '%' plugin: '{{ test_plugin_type }}' state: present register: result - - name: Assert that the user wasn't changed because the auth string is still empty - assert: + - name: Plugin auth | Assert that the user wasn't changed because the auth string is still empty + ansible.builtin.assert: that: - - "result.changed == false" + - result is not changed # Cleanup - - include: remove_user.yml user_name={{ test_user_name }} user_password={{ test_plugin_auth_string }} + - ansible.builtin.include_tasks: utils/remove_user.yml + vars: + user_name: "{{ test_user_name }}" # ============================================================ # Test plugin auth switching from one type of plugin to another without an auth string or hash. The only other # plugins that are loaded by default are sha2*, but these aren't compatible with pymysql < 0.9, so skip these tests # for those versions. # - - name: Get pymysql version - shell: pip show pymysql | awk '/Version/ {print $2}' - register: pymysql_version - - - name: Test plugin auth switching which doesn't work on pymysql < 0.9 - when: pymysql_version.stdout == "" or (pymysql_version.stdout != "" and pymysql_version.stdout is version('0.9', '>=')) + - name: Plugin auth | Test plugin auth switching which doesn't work on pymysql < 0.9 + when: + - > + connector_name != 'pymysql' + or ( + connector_name == 'pymysql' + and connector_version is version('0.9', '>=') + ) block: - - name: Create user with plugin auth (empty auth string) - mysql_user: - <<: *mysql_params - name: '{{ test_user_name }}' - plugin: '{{ test_plugin_type }}' - priv: '{{ test_default_priv }}' - register: result + - name: Plugin auth | Create user with plugin auth (empty auth string) + community.mysql.mysql_user: + <<: *mysql_params + name: '{{ test_user_name }}' + plugin: '{{ test_plugin_type }}' + priv: '{{ test_default_priv }}' + register: result - - name: Get user information - command: "{{ mysql_command }} -e \"SHOW CREATE USER '{{ test_user_name }}'@'localhost'\"" - register: show_create_user + - name: Plugin auth | Get user information (empty auth string) + ansible.builtin.command: "{{ mysql_command }} -e \"SHOW CREATE USER '{{ test_user_name }}'@'localhost'\"" + register: show_create_user - - name: Check that the module made a change - assert: - that: - - "result.changed == true" + - name: Plugin auth | Check that the module made a change (empty auth string) + ansible.builtin.assert: + that: + - result is changed - - name: Check that the expected plugin type is set - assert: - that: - - "'{{ test_plugin_type }}' in show_create_user.stdout" - when: install_type == 'mysql' or (install_type == 'mariadb' and mariadb_version is version('10.3', '>=')) + - name: Plugin auth | Check that the expected plugin type is set (empty auth string) + ansible.builtin.assert: + that: + - test_plugin_type in show_create_user.stdout + when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>=')) - - include: assert_user.yml user_name={{ test_user_name }} priv={{ test_default_priv_type }} + - ansible.builtin.include_tasks: utils/assert_user.yml + vars: + user_name: "{{ test_user_name }}" + user_host: localhost + priv: "{{ test_default_priv_type }}" - - name: Switch user to sha256_password auth plugin - mysql_user: - <<: *mysql_params - name: '{{ test_user_name }}' - plugin: sha256_password - priv: '{{ test_default_priv }}' - register: result + - name: Plugin auth | Switch user to sha256_password auth plugin + community.mysql.mysql_user: + <<: *mysql_params + name: '{{ test_user_name }}' + plugin: sha256_password + priv: '{{ test_default_priv }}' + register: result - - name: Get user information - command: "{{ mysql_command }} -e \"SHOW CREATE USER '{{ test_user_name }}'@'localhost'\"" - register: show_create_user + - name: Plugin auth | Get user information (sha256_password) + ansible.builtin.command: "{{ mysql_command }} -e \"SHOW CREATE USER '{{ test_user_name }}'@'localhost'\"" + register: show_create_user - - name: Check that the module made a change - assert: - that: - - "result.changed == true" + - name: Plugin auth | Check that the module made a change (sha256_password) + ansible.builtin.assert: + that: + - result is changed - - name: Check that the expected plugin type is set - assert: - that: + - name: Plugin auth | Check that the expected plugin type is set (sha256_password) + ansible.builtin.assert: + that: - "'sha256_password' in show_create_user.stdout" - when: install_type == 'mysql' or (install_type == 'mariadb' and mariadb_version is version('10.3', '>=')) + when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>=')) - - include: assert_user.yml user_name={{ test_user_name }} priv={{ test_default_priv_type }} + - ansible.builtin.include_tasks: utils/assert_user.yml + vars: + user_name: "{{ test_user_name }}" + user_host: localhost + priv: "{{ test_default_priv_type }}" - # Cleanup - - include: remove_user.yml user_name={{ test_user_name }} user_password={{ test_plugin_auth_string }} + # Cleanup + - ansible.builtin.include_tasks: utils/remove_user.yml + vars: + user_name: "{{ test_user_name }}" + + # ============================================================ + # Test plugin auth with a salt + # + - name: Plugin auth | Create user with plugin auth and salt + community.mysql.mysql_user: + <<: *mysql_params + name: "{{ test_user_name }}" + host: "%" + plugin: caching_sha2_password + plugin_auth_string: "{{ test_plugin_auth_string }}" + salt: "{{ test_salt }}" + priv: "{{ test_default_priv }}" + + - name: Plugin auth | Connect with user and password + ansible.builtin.command: '{{ mysql_command }} -u {{ test_user_name }} -p{{ test_plugin_auth_string }} -e "SELECT 1"' + + - name: Plugin auth | Alter user with same plugin auth and same salt + community.mysql.mysql_user: + <<: *mysql_params + name: "{{ test_user_name }}" + host: "%" + plugin: caching_sha2_password + plugin_auth_string: "{{ test_plugin_auth_string }}" + salt: "{{ test_salt }}" + priv: "{{ test_default_priv }}" + register: result + failed_when: result is changed + + - name: Cleanup user + ansible.builtin.include_tasks: utils/remove_user.yml + vars: + user_name: "{{ test_user_name }}" + + - name: Plugin auth | Create user with too short salt (should fail) + community.mysql.mysql_user: + <<: *mysql_params + name: "{{ test_user_name }}" + host: "%" + plugin: caching_sha2_password + plugin_auth_string: "{{ test_plugin_auth_string }}" + salt: "1234567890az" + priv: "{{ test_default_priv }}" + register: result + failed_when: result is success + + - name: Plugin auth | Create user with salt and no plugin auth string (should fail) + community.mysql.mysql_user: + <<: *mysql_params + name: "{{ test_user_name }}" + host: "%" + plugin: caching_sha2_password + salt: "{{ test_salt }}" + priv: "{{ test_default_priv }}" + register: result + failed_when: result is success + + - name: Plugin auth | Create user with salt and plugin not handled by internal hash generation (should fail) + community.mysql.mysql_user: + <<: *mysql_params + name: "{{ test_user_name }}" + host: "%" + plugin: mysql_native_password + plugin_auth_string: "{{ test_plugin_auth_string }}" + salt: "{{ test_salt }}" + priv: "{{ test_default_priv }}" + register: result + failed_when: result is success + + # ============================================================ + # Test auth plugin change + # + + - name: Plugin auth | Test plugin auth switching which doesn't work on pymysql < 0.9 + when: + - > + connector_name != 'pymysql' + or ( + connector_name == 'pymysql' + and connector_version is version('0.9', '>=') + ) + block: + + - name: Cleanup user + ansible.builtin.include_tasks: utils/remove_user.yml + vars: + user_name: "{{ test_user_name }}" + + - name: Plugin auth | Create user with mysql_native_password + community.mysql.mysql_user: + <<: *mysql_params + name: "{{ test_user_name }}" + host: "%" + plugin: "{{ test_plugin_type }}" + password: "{{ test_plugin_auth_string }}" + priv: "{{ test_default_priv }}" + + - name: Plugin auth | Check that the expected plugin type is set + ansible.builtin.include_tasks: utils/assert_plugin.yml + vars: + user_name: "{{ test_user_name }}" + plugin_type: "{{ test_plugin_type }}" + + - name: Plugin auth | Connect with user and password + ansible.builtin.command: '{{ mysql_command }} -u {{ test_user_name }} -p{{ test_plugin_auth_string }} -e "SELECT 1"' + changed_when: false + + - name: Plugin auth | Change auth user plugin in check mode + community.mysql.mysql_user: + <<: *mysql_params + name: "{{ test_user_name }}" + host: '%' + plugin: caching_sha2_password + plugin_auth_string: "{{ test_plugin_auth_string }}" + salt: "{{ test_salt }}" + priv: "{{ test_default_priv }}" + check_mode: true + register: result + failed_when: result is not changed + + - name: Plugin auth | Check that the expected plugin type is set (not changed) + ansible.builtin.include_tasks: utils/assert_plugin.yml + vars: + user_name: "{{ test_user_name }}" + plugin_type: "{{ test_plugin_type }}" + + - name: Plugin auth | Change auth user plugin + community.mysql.mysql_user: + <<: *mysql_params + name: "{{ test_user_name }}" + host: '%' + plugin: caching_sha2_password + plugin_auth_string: "{{ test_plugin_auth_string }}" + salt: "{{ test_salt }}" + priv: "{{ test_default_priv }}" + register: result + failed_when: result is not changed + + - name: Plugin auth | Check that the expected (new) plugin type is set + ansible.builtin.include_tasks: utils/assert_plugin.yml + vars: + user_name: "{{ test_user_name }}" + plugin_type: caching_sha2_password + + - name: Plugin auth | Change auth user plugin again (should not change) + community.mysql.mysql_user: + <<: *mysql_params + name: "{{ test_user_name }}" + host: '%' + plugin: caching_sha2_password + plugin_auth_string: "{{ test_plugin_auth_string }}" + salt: "{{ test_salt }}" + priv: "{{ test_default_priv }}" + register: result + failed_when: result is changed + + - name: Plugin auth | Check that the expected (not changed) plugin type is set + ansible.builtin.include_tasks: utils/assert_plugin.yml + vars: + user_name: "{{ test_user_name }}" + plugin_type: caching_sha2_password diff --git a/tests/integration/targets/test_mysql_user/tasks/tls_requirements.yml b/tests/integration/targets/test_mysql_user/tasks/tls_requirements.yml deleted file mode 100644 index 7bf142e..0000000 --- a/tests/integration/targets/test_mysql_user/tasks/tls_requirements.yml +++ /dev/null @@ -1,187 +0,0 @@ ---- -- 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: find out the database version - mysql_info: - <<: *mysql_params - filter: version - register: db_version - - - name: Drop mysql user {{ item }} if exists - mysql_user: - <<: *mysql_params - name: '{{ item }}' - state: absent - with_items: ['{{ user_name_1 }}', '{{ user_name_2 }}', '{{ user_name_3 }}'] - - - name: create user with TLS requirements in check mode (expect changed=true) - mysql_user: - <<: *mysql_params - name: "{{ user_name_1 }}" - password: "{{ user_password_1 }}" - tls_requires: - SSL: - check_mode: yes - register: result - - - name: Assert check mode user create reports changed state - assert: - that: - - result is changed - - - include: assert_no_user.yml user_name={{user_name_1}} - - - name: create user with TLS requirements state=present (expect changed=true) - mysql_user: - <<: *mysql_params - name: '{{ item[0] }}' - password: '{{ user_password_1 }}' - tls_requires: '{{ item[1] }}' - with_together: - - [ '{{ user_name_1 }}', '{{ user_name_2 }}', '{{ user_name_3 }}'] - - - - SSL: - - X509: - - subject: '/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' - cipher: 'ECDHE-ECDSA-AES256-SHA384' - issuer: '/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' - - - block: - - name: retrieve TLS requirements for users in old database version - command: "{{ mysql_command }} -L -N -s -e \"SHOW GRANTS for '{{ item }}'@'localhost'\"" - register: old_result - with_items: ['{{ user_name_1 }}', '{{ user_name_2 }}', '{{ user_name_3 }}'] - - - name: set old database separator - set_fact: - separator: '\n' - # Semantically: when mysql version <= 5.6 or MariaDB version <= 10.1 - when: db_version.version.major <= 5 and db_version.version.minor <= 6 or db_version.version.major == 10 and db_version.version.minor < 2 - - - block: - - name: retrieve TLS requirements for users in new database version - command: "{{ mysql_command }} -L -N -s -e \"SHOW CREATE USER '{{ item }}'@'localhost'\"" - register: new_result - with_items: ['{{ user_name_1 }}', '{{ user_name_2 }}', '{{ user_name_3 }}'] - - - name: set new database separator - set_fact: - separator: 'PASSWORD' - # Semantically: when mysql version >= 5.7 or MariaDB version >= 10.2 - when: db_version.version.major == 5 and db_version.version.minor >= 7 or db_version.version.major > 5 and db_version.version.major < 10 or db_version.version.major == 10 and db_version.version.minor >= 2 - - - block: - - name: assert user1 TLS requirements - assert: - that: - - "'SSL' in reqs" - vars: - - reqs: "{{((old_result.results[0] is skipped | ternary(new_result, old_result)).results | selectattr('item', 'contains', user_name_1) | first).stdout.split('REQUIRE')[1].split(separator)[0].strip()}}" - - - name: assert user2 TLS requirements - assert: - that: - - "'X509' in reqs" - vars: - - reqs: "{{((old_result.results[0] is skipped | ternary(new_result, old_result)).results | selectattr('item', 'contains', user_name_2) | first).stdout.split('REQUIRE')[1].split(separator)[0].strip()}}" - - - name: assert user3 TLS requirements - assert: - that: - - "'/CN=alice/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('contains', 'SUBJECT') | first)" - - "'/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('contains', 'ISSUER') | first)" - - "'ECDHE-ECDSA-AES256-SHA384' in (reqs | select('contains', 'CIPHER') | first)" - vars: - - reqs: "{{((old_result.results[0] is skipped | ternary(new_result, old_result)).results | selectattr('item', 'contains', user_name_3) | first).stdout.split('REQUIRE')[1].split(separator)[0].replace(\"' \", \"':\").split(\":\")}}" - # CentOS 6 uses an older version of jinja that does not provide the selectattr filter. - when: ansible_distribution != 'CentOS' or ansible_distribution_major_version != '6' - - - name: modify user with TLS requirements state=present in check mode (expect changed=true) - mysql_user: - <<: *mysql_params - name: '{{ user_name_1 }}' - password: '{{ user_password_1 }}' - tls_requires: - X509: - check_mode: yes - register: result - - - name: Assert check mode user update reports changed state - assert: - that: - - result is changed - - - name: retrieve TLS requirements for users in old database version - command: "{{ mysql_command }} -L -N -s -e \"SHOW GRANTS for '{{ user_name_1 }}'@'localhost'\"" - register: old_result - when: db_version.version.major <= 5 and db_version.version.minor <= 6 or db_version.version.major == 10 and db_version.version.minor < 2 - - - name: retrieve TLS requirements for users in new database version - command: "{{ mysql_command }} -L -N -s -e \"SHOW CREATE USER '{{ user_name_1 }}'@'localhost'\"" - register: new_result - when: db_version.version.major == 5 and db_version.version.minor >= 7 or db_version.version.major > 5 and db_version.version.major < 10 or db_version.version.major == 10 and db_version.version.minor >= 2 - - - name: assert user1 TLS requirements was not changed - assert: - that: "'SSL' in reqs" - vars: - - reqs: "{{(old_result is skipped | ternary(new_result, old_result)).stdout.split('REQUIRE')[1].split(separator)[0].strip()}}" - - - name: modify user with TLS requirements state=present (expect changed=true) - mysql_user: - <<: *mysql_params - name: '{{ user_name_1 }}' - password: '{{ user_password_1 }}' - tls_requires: - X509: - - - name: retrieve TLS requirements for users in old database version - command: "{{ mysql_command }} -L -N -s -e \"SHOW GRANTS for '{{ user_name_1 }}'@'localhost'\"" - register: old_result - when: db_version.version.major <= 5 and db_version.version.minor <= 6 or db_version.version.major == 10 and db_version.version.minor < 2 - - - name: retrieve TLS requirements for users in new database version - command: "{{ mysql_command }} -L -N -s -e \"SHOW CREATE USER '{{ user_name_1 }}'@'localhost'\"" - register: new_result - when: db_version.version.major == 5 and db_version.version.minor >= 7 or db_version.version.major > 5 and db_version.version.major < 10 or db_version.version.major == 10 and db_version.version.minor >= 2 - - - name: assert user1 TLS requirements - assert: - that: "'X509' in reqs" - vars: - - reqs: "{{(old_result is skipped | ternary(new_result, old_result)).stdout.split('REQUIRE')[1].split(separator)[0].strip()}}" - - - name: remove TLS requirements from user (expect changed=true) - mysql_user: - <<: *mysql_params - name: '{{ user_name_1 }}' - password: '{{ user_password_1 }}' - tls_requires: - - - name: retrieve TLS requirements for users - command: "{{ mysql_command }} -L -N -s -e \"SHOW CREATE USER '{{ user_name_1 }}'@'localhost'\"" - register: result - - - name: assert user1 TLS requirements - assert: - that: "'REQUIRE ' not in result.stdout or 'REQUIRE NONE' in result.stdout" - - - include: remove_user.yml user_name={{user_name_1}} user_password={{ user_password_1 }} - - - include: remove_user.yml user_name={{user_name_2}} user_password={{ user_password_1 }} - - - include: remove_user.yml user_name={{user_name_3}} user_password={{ user_password_1 }} - - - include: assert_no_user.yml user_name={{user_name_1}} - - - include: assert_no_user.yml user_name={{user_name_2}} - - - include: assert_no_user.yml user_name={{user_name_3}} diff --git a/tests/integration/targets/test_mysql_user/tasks/utils/assert_no_user.yml b/tests/integration/targets/test_mysql_user/tasks/utils/assert_no_user.yml new file mode 100644 index 0000000..6fc4fbc --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/utils/assert_no_user.yml @@ -0,0 +1,8 @@ +--- +- name: Utils | Assert no user | Query for user {{ user_name }} + command: "{{ mysql_command }} -e \"SELECT User FROM mysql.user where user='{{ user_name }}'\"" + register: result + +- name: Utils | Assert no user | Assert mysql user is not present + assert: + that: user_name not in result.stdout diff --git a/tests/integration/targets/test_mysql_user/tasks/utils/assert_plugin.yml b/tests/integration/targets/test_mysql_user/tasks/utils/assert_plugin.yml new file mode 100644 index 0000000..7d3b5a1 --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/utils/assert_plugin.yml @@ -0,0 +1,11 @@ +--- + +- name: Utils | Assert plugin | Query for user {{ user_name }} + ansible.builtin.command: "{{ mysql_command }} -e \"SELECT plugin FROM mysql.user where user='{{ user_name }}'\"" + register: result + changed_when: False + +- name: Utils | Assert plugin | Assert plugin is correct + ansible.builtin.assert: + that: + - plugin_type in result.stdout diff --git a/tests/integration/targets/test_mysql_user/tasks/utils/assert_user.yml b/tests/integration/targets/test_mysql_user/tasks/utils/assert_user.yml new file mode 100644 index 0000000..e6bd23f --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/utils/assert_user.yml @@ -0,0 +1,21 @@ +--- + +- name: Utils | Assert user | Query for user {{ user_name }} + command: "{{ mysql_command }} -e \"SELECT user FROM mysql.user where user='{{ user_name }}'\"" + register: result + +- name: Utils | Assert user | Assert user is present + assert: + that: + - user_name in result.stdout + +- name: Utils | Assert user | Query for privileges of user {{ user_name }} + command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name }}'@'{{ user_host }}'\"" + register: result + when: priv is defined + +- name: Utils | Assert user | Assert user has given privileges + ansible.builtin.assert: + that: + - "'GRANT {{ priv }} ON *.*' in result.stdout" + when: priv is defined diff --git a/tests/integration/targets/test_mysql_user/tasks/utils/assert_user_password.yml b/tests/integration/targets/test_mysql_user/tasks/utils/assert_user_password.yml new file mode 100644 index 0000000..e6bd695 --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/utils/assert_user_password.yml @@ -0,0 +1,29 @@ +--- +- name: Utils | Assert user password | Apply update_password to {{ username }} + community.mysql.mysql_user: + login_user: '{{ mysql_parameters.login_user }}' + login_password: '{{ mysql_parameters.login_password }}' + login_host: '{{ mysql_parameters.login_host }}' + login_port: '{{ mysql_parameters.login_port }}' + state: present + name: "{{ username }}" + host: "{{ host }}" + password: "{{ password }}" + update_password: "{{ update_password }}" + register: result + +- name: Utils | Assert user password | Assert a change occurred + ansible.builtin.assert: + that: + - result.changed | bool == expect_change | bool + - result.password_changed == expect_password_change + +- name: Utils | Assert user password | Assert expect_hash is in user stdout for {{ username }} + ansible.builtin.command: >- + {{ mysql_command }} -BNe "SELECT plugin, authentication_string + FROM mysql.user where user='{{ username }}' and host='{{ host }}'" + register: existing_user + changed_when: false + failed_when: pattern not in existing_user.stdout_lines + vars: + pattern: "mysql_native_password\t{{ expect_password_hash }}" diff --git a/tests/integration/targets/test_mysql_user/tasks/utils/assert_user_password_expire.yml b/tests/integration/targets/test_mysql_user/tasks/utils/assert_user_password_expire.yml new file mode 100644 index 0000000..3798802 --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/utils/assert_user_password_expire.yml @@ -0,0 +1,56 @@ +--- +- name: Utils | Assert user password_expire | Create modify {{ username }} with password_expire + community.mysql.mysql_user: + login_user: "{{ mysql_parameters.login_user }}" + login_password: "{{ mysql_parameters.login_password }}" + login_host: "{{ mysql_parameters.login_host }}" + login_port: "{{ mysql_parameters.login_port }}" + state: present + name: "{{ username }}" + host: "{{ host }}" + password: "{{ password }}" + password_expire: "{{ password_expire }}" + password_expire_interval: "{{ password_expire_interval | default(omit) }}" + register: result + check_mode: "{{ check_mode | default(false) }}" + failed_when: result.changed != expect_change_value + vars: + expect_change_value: "{{ expect_change }}" + +- name: Utils | Assert user password_lifetime | Query user '{{ username }}' + ansible.builtin.command: + cmd: > + {{ mysql_command }} -BNe "SELECT IFNULL(password_lifetime, -1) + FROM mysql.user where user='{{ username }}' and host='{{ host }}'" + register: password_lifetime + when: + - db_engine == 'mysql' + - db_version is version('5.7.0', '>=') + failed_when: expected_password_lifetime_value not in password_lifetime.stdout_lines + vars: + expected_password_lifetime_value: "{{ expected_password_lifetime }}" + +- name: Utils | Assert user password_lifetime | Query user '{{ username }}' + ansible.builtin.command: + "{{ mysql_command }} -BNe \"SELECT JSON_EXTRACT(Priv, '$.password_lifetime') AS password_lifetime \ + FROM mysql.global_priv \ + WHERE user='{{ username }}' and host='{{ host }}'\"" + register: password_lifetime + when: + - db_engine == 'mariadb' + - db_version is version('10.4.3', '>=') + failed_when: expected_password_lifetime_value not in password_lifetime.stdout_lines + vars: + expected_password_lifetime_value: "{{ expected_password_lifetime }}" + +- name: Utils | Assert user password_expired | Query user '{{ username }}' + ansible.builtin.command: + cmd: > + {{ mysql_command }} -BNe "SELECT password_expired FROM mysql.user + WHERE user='{{ username }}' and host='{{ host }}'" + register: password_expired + when: (db_engine == 'mysql' and db_version is version('5.7.0', '>=')) or + (db_engine == 'mariadb' and db_version is version('10.4.3', '>=')) + failed_when: expected_password_expired_value not in password_expired.stdout_lines + vars: + expected_password_expired_value: "{{ expected_password_expired }}" diff --git a/tests/integration/targets/test_mysql_user/tasks/utils/create_user.yml b/tests/integration/targets/test_mysql_user/tasks/utils/create_user.yml new file mode 100644 index 0000000..b255ec4 --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/utils/create_user.yml @@ -0,0 +1,12 @@ +--- + +- name: Utils | Create user {{ user_name }} + mysql_user: + login_user: "{{ mysql_user }}" + login_password: "{{ mysql_password }}" + login_host: "{{ mysql_host }}" + login_port: "{{ mysql_primary_port }}" + name: "{{ user_name }}" + host: "{{ user_host | default(omit) }}" + password: "{{ user_password }}" + state: present diff --git a/tests/integration/targets/test_mysql_user/tasks/utils/remove_user.yml b/tests/integration/targets/test_mysql_user/tasks/utils/remove_user.yml new file mode 100644 index 0000000..473cece --- /dev/null +++ b/tests/integration/targets/test_mysql_user/tasks/utils/remove_user.yml @@ -0,0 +1,12 @@ +--- + +- name: Utils | Remove user {{ user_name }} + mysql_user: + login_user: "{{ mysql_user }}" + login_password: "{{ mysql_password }}" + login_host: "{{ mysql_host }}" + login_port: "{{ mysql_primary_port }}" + name: "{{ user_name }}" + host_all: true + state: absent + ignore_errors: true diff --git a/tests/integration/targets/test_mysql_variables/defaults/main.yml b/tests/integration/targets/test_mysql_variables/defaults/main.yml index 6d0e2ec..779eead 100644 --- a/tests/integration/targets/test_mysql_variables/defaults/main.yml +++ b/tests/integration/targets/test_mysql_variables/defaults/main.yml @@ -2,6 +2,7 @@ # defaults file for test_mysql_variables mysql_user: root mysql_password: msandbox +mysql_host: '{{ gateway_addr }}' mysql_primary_port: 3307 user_name_1: 'db_user1' diff --git a/tests/integration/targets/test_mysql_variables/meta/main.yml b/tests/integration/targets/test_mysql_variables/meta/main.yml index f1174ff..01ee3db 100644 --- a/tests/integration/targets/test_mysql_variables/meta/main.yml +++ b/tests/integration/targets/test_mysql_variables/meta/main.yml @@ -1,2 +1,3 @@ +--- dependencies: - - setup_mysql + - setup_controller diff --git a/tests/integration/targets/test_mysql_variables/tasks/assert_fail_msg.yml b/tests/integration/targets/test_mysql_variables/tasks/assert_fail_msg.yml index 4a840b9..a09bcdb 100644 --- a/tests/integration/targets/test_mysql_variables/tasks/assert_fail_msg.yml +++ b/tests/integration/targets/test_mysql_variables/tasks/assert_fail_msg.yml @@ -22,4 +22,4 @@ - name: assert message failure (expect failed=true) assert: that: - - "output.failed == true" + - output is failed diff --git a/tests/integration/targets/test_mysql_variables/tasks/assert_var.yml b/tests/integration/targets/test_mysql_variables/tasks/assert_var.yml index 5419f34..e64c5a7 100644 --- a/tests/integration/targets/test_mysql_variables/tasks/assert_var.yml +++ b/tests/integration/targets/test_mysql_variables/tasks/assert_var.yml @@ -1,3 +1,4 @@ +--- # test code to assert variables in mysql_variables module # (c) 2014, Wayne Rosario @@ -19,18 +20,18 @@ # ============================================================ # Assert mysql variable name and value from mysql database # -- name: assert output message changed value +- name: Assert output message changed value assert: that: - - "output.changed == {{ changed }}" + - "output.changed | bool == changed | bool" -- name: run mysql command to show variable +- name: Run mysql command to show variable command: "{{ mysql_command }} \"-e show variables like '{{ var_name }}'\"" register: result -- name: assert output mysql variable name and value +- name: Assert output mysql variable name and value assert: that: - - "result.changed == true" + - result is changed - "'{{ var_name }}' in result.stdout" - "'{{ var_value }}' in result.stdout" diff --git a/tests/integration/targets/test_mysql_variables/tasks/assert_var_output.yml b/tests/integration/targets/test_mysql_variables/tasks/assert_var_output.yml index f84a468..6f26386 100644 --- a/tests/integration/targets/test_mysql_variables/tasks/assert_var_output.yml +++ b/tests/integration/targets/test_mysql_variables/tasks/assert_var_output.yml @@ -22,7 +22,7 @@ - name: assert output message changed value assert: that: - - "output.changed == {{ changed }}" + - "output.changed | bool == changed | bool" - set_fact: key_name: "{{ var_name }}" @@ -35,6 +35,6 @@ - name: assert output variable info match mysql variable info assert: that: - - "result.changed == true" + - result is changed - "key_name in result.stdout" - "key_value in result.stdout" diff --git a/tests/integration/targets/test_mysql_variables/tasks/issue-28.yml b/tests/integration/targets/test_mysql_variables/tasks/issue-28.yml index 93c2125..89d3d26 100644 --- a/tests/integration/targets/test_mysql_variables/tasks/issue-28.yml +++ b/tests/integration/targets/test_mysql_variables/tasks/issue-28.yml @@ -1,82 +1,89 @@ --- - name: set fact tls_enabled - command: "{{ mysql_command }} \"-e SHOW VARIABLES LIKE 'have_ssl';\"" + ansible.builtin.command: + cmd: "{{ mysql_command }} \"-e SHOW VARIABLES LIKE 'have_ssl';\"" register: result -- set_fact: + +- name: Set tls_enabled fact + ansible.builtin.set_fact: tls_enabled: "{{ 'YES' in result.stdout | bool | default('false', true) }}" - vars: mysql_parameters: &mysql_params login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' when: tls_enabled block: # ============================================================ - name: get server certificate - copy: + ansible.builtin.copy: content: "{{ lookup('pipe', \"openssl s_client -starttls mysql -connect localhost:3307 -showcerts 2>/dev/null = 0.7.11 is required' in result.msg + ignore_errors: true + failed_when: + - result is failed or 'pymysql >= 0.7.11 is required' not in result.msg - name: Drop mysql user - mysql_user: + community.mysql.mysql_user: <<: *mysql_params name: '{{ user_name_1 }}' - host: 127.0.0.1 + host_all: true state: absent diff --git a/tests/integration/targets/test_mysql_variables/tasks/main.yml b/tests/integration/targets/test_mysql_variables/tasks/main.yml index 9c4cd7d..052b279 100644 --- a/tests/integration/targets/test_mysql_variables/tasks/main.yml +++ b/tests/integration/targets/test_mysql_variables/tasks/main.yml @@ -5,4 +5,4 @@ - import_tasks: mysql_variables.yml -- include: issue-28.yml +- include_tasks: issue-28.yml diff --git a/tests/integration/targets/test_mysql_variables/tasks/mysql_variables.yml b/tests/integration/targets/test_mysql_variables/tasks/mysql_variables.yml index a857f12..8194172 100644 --- a/tests/integration/targets/test_mysql_variables/tasks/mysql_variables.yml +++ b/tests/integration/targets/test_mysql_variables/tasks/mysql_variables.yml @@ -23,15 +23,11 @@ mysql_parameters: &mysql_params login_user: '{{ mysql_user }}' login_password: '{{ mysql_password }}' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' block: - - 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" - - set_fact: set_name: 'version' @@ -41,14 +37,18 @@ variable: '{{ set_name }}' register: result - - include: assert_var_output.yml changed=false output={{ result }} var_name={{ set_name }} + - include_tasks: assert_var_output.yml + vars: + changed: false + output: "{{ result }}" + var_name: "{{ set_name }}" # ============================================================ # Verify mysql_variable successfully updates a variable (issue:4568) # - set_fact: - set_name: 'delay_key_write' - set_value: 'ON' + set_name: 'delay_key_write' + set_value: 'ON' - name: set mysql variable mysql_variables: @@ -63,14 +63,19 @@ value: '{{ set_value }}' register: result - - include: assert_var.yml changed=false output={{ result }} var_name={{ set_name }} var_value={{ set_value }} + - include_tasks: assert_var.yml + vars: + changed: false + output: "{{ result }}" + var_name: "{{ set_name }}" + var_value: "{{ set_value }}" # ============================================================ # Verify mysql_variable successfully updates a variable using single quotes # - set_fact: - set_name: 'wait_timeout' - set_value: '300' + set_name: 'wait_timeout' + set_value: '300' - name: set mysql variable to a temp value mysql_variables: @@ -89,14 +94,19 @@ that: - result.queries == ["SET GLOBAL `{{ set_name }}` = {{ set_value }}"] - - include: assert_var.yml changed=true output={{ result }} var_name={{ set_name }} var_value='{{ set_value }}' + - include_tasks: assert_var.yml + vars: + changed: true + output: "{{ result }}" + var_name: "{{ set_name }}" + var_value: '{{ set_value }}' # ============================================================ # Verify mysql_variable successfully updates a variable using double quotes # - set_fact: - set_name: "wait_timeout" - set_value: "400" + set_name: "wait_timeout" + set_value: "400" - name: set mysql variable to a temp value mysql_variables: @@ -111,14 +121,19 @@ value: '{{ set_value }}' register: result - - include: assert_var.yml changed=true output={{ result }} var_name={{ set_name }} var_value='{{ set_value }}' + - include_tasks: assert_var.yml + vars: + changed: true + output: "{{ result }}" + var_name: "{{ set_name }}" + var_value: '{{ set_value }}' # ============================================================ # Verify mysql_variable successfully updates a variable using no quotes # - set_fact: - set_name: wait_timeout - set_value: 500 + set_name: wait_timeout + set_value: 500 - name: set mysql variable to a temp value mysql_variables: @@ -133,7 +148,12 @@ value: '{{ set_value }}' register: result - - include: assert_var.yml changed=true output={{ result }} var_name={{ set_name }} var_value='{{ set_value }}' + - include_tasks: assert_var.yml + vars: + changed: true + output: "{{ result }}" + var_name: "{{ set_name }}" + var_value: '{{ set_value }}' # ============================================================ # Verify mysql_variable successfully updates a variable using an expression (e.g. 1024*4) @@ -146,24 +166,38 @@ register: result ignore_errors: true - - include: assert_fail_msg.yml output={{ result }} msg='Incorrect argument type to variable' + - include_tasks: assert_fail_msg.yml + vars: + output: "{{ result }}" + msg: 'Incorrect argument type to variable' # ============================================================ # Verify mysql_variable fails when setting an incorrect value (out of range) # - - name: set mysql variable value to a number out of range + - name: Set mysql variable value to a number out of range mysql_variables: <<: *mysql_params variable: max_connect_errors - value: -1 + value: '-1' register: oor_result ignore_errors: true - - include: assert_var.yml changed=true output={{ oor_result }} var_name=max_connect_errors var_value=1 - when: connector.name.0 is not search('pymysql') + - include_tasks: assert_var.yml + vars: + changed: true + output: "{{ oor_result }}" + var_name: max_connect_errors + var_value: 1 + when: + - connector_name == 'mysqlclient' + - db_engine == 'mysql' # mysqlclient returns "changed" with MariaDB - - include: assert_fail_msg.yml output={{ oor_result }} msg='Truncated incorrect' - when: connector.name.0 is search('pymysql') + - include_tasks: assert_fail_msg.yml + vars: + output: "{{ oor_result }}" + msg: 'Truncated incorrect' + when: + - connector_name == 'pymsql' # ============================================================ # Verify mysql_variable fails when setting an incorrect value (incorrect type) @@ -176,7 +210,10 @@ register: nvv_result ignore_errors: true - - include: assert_fail_msg.yml output={{ nvv_result }} msg='Incorrect argument type to variable' + - include_tasks: assert_fail_msg.yml + vars: + output: "{{ nvv_result }}" + msg: 'Incorrect argument type to variable' # ============================================================ # Verify mysql_variable fails when setting an unknown variable @@ -189,7 +226,10 @@ register: result ignore_errors: true - - include: assert_fail_msg.yml output={{ result }} msg='Variable not available' + - include_tasks: assert_fail_msg.yml + vars: + output: "{{ result }}" + msg: 'Variable not available' # ============================================================ # Verify mysql_variable fails when setting a read-only variable @@ -202,14 +242,17 @@ register: result ignore_errors: true - - include: assert_fail_msg.yml output={{ result }} msg='read only variable' + - include_tasks: assert_fail_msg.yml + vars: + output: "{{ result }}" + msg: 'read only variable' #============================================================= # Verify mysql_variable works with the login_user and login_password parameters # - set_fact: - set_name: wait_timeout - set_value: 77 + set_name: wait_timeout + set_value: 77 - name: query mysql_variable using login_user and password_password mysql_variables: @@ -217,7 +260,11 @@ variable: '{{ set_name }}' register: result - - include: assert_var_output.yml changed=false output={{ result }} var_name={{ set_name }} + - include_tasks: assert_var_output.yml + vars: + changed: false + output: "{{ result }}" + var_name: "{{ set_name }}" - name: set mysql variable to temp value using user login and password (expect changed=true) mysql_variables: @@ -233,39 +280,143 @@ value: '{{set_value}}' register: result - - include: assert_var.yml changed=true output={{result}} var_name={{set_name}} var_value='{{set_value}}' + - include_tasks: assert_var.yml + vars: + changed: true + output: "{{result}}" + var_name: "{{set_name}}" + var_value: '{{set_value}}' + + #========================================================================= + # Bugfix https://github.com/ansible-collections/community.mysql/issues/652 + + - name: Get server version + register: result + mysql_info: + <<: *mysql_params + + - name: Set variable name when running on MySQL + set_fact: + log_slow_statements: log_slow_replica_statements + when: result.server_engine == 'MySQL' + + - name: Set variable name when running on MariaDB + set_fact: + log_slow_statements: log_slow_slave_statements + when: result.server_engine == 'MariaDB' + + - name: Set a boolean value using ON + mysql_variables: + <<: *mysql_params + variable: "{{ log_slow_statements }}" + value: "ON" + register: result + + - name: Check that it changed + assert: + that: + - result is changed or result.msg == "Variable is already set to requested value." + - result.msg == "Variable is already set to requested value." or result.queries == ["SET GLOBAL `{{ log_slow_statements }}` = ON"] + + - name: Set a boolean value again using ON + mysql_variables: + <<: *mysql_params + variable: "{{ log_slow_statements }}" + value: "ON" + register: result + + - name: Check that it didn't change + assert: + that: + - result is not changed + + - name: Set a boolean value again using 1 + mysql_variables: + <<: *mysql_params + variable: "{{ log_slow_statements }}" + value: 1 + register: result + + - name: Check that it didn't change + assert: + that: + - result is not changed + + - name: Set a boolean value using OFF + mysql_variables: + <<: *mysql_params + variable: "{{ log_slow_statements }}" + value: "OFF" + register: result + + - name: Check that it changed + assert: + that: + - result is changed + - result.queries == ["SET GLOBAL `{{ log_slow_statements }}` = OFF"] + + - name: Set a boolean value again using 0 + mysql_variables: + <<: *mysql_params + variable: "{{ log_slow_statements }}" + value: 0 + register: result + + - name: Check that it didn't change + assert: + that: + - result is not changed + + - name: Set a boolean value using on + mysql_variables: + <<: *mysql_params + variable: "{{ log_slow_statements }}" + value: "on" + register: result + + - name: Check that it changed + assert: + that: + - result is changed + - result.queries == ["SET GLOBAL `{{ log_slow_statements }}` = ON"] #============================================================ # Verify mysql_variable fails with an incorrect login_password parameter # - set_fact: - set_name: connect_timeout - set_value: 10 + set_name: connect_timeout + set_value: 10 - name: query mysql_variable using incorrect login_password mysql_variables: login_user: '{{ mysql_user }}' login_password: 'wrongpassword' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' variable: '{{ set_name }}' register: result ignore_errors: true - - include: assert_fail_msg.yml output={{ result }} msg='unable to connect to database' + - include_tasks: assert_fail_msg.yml + vars: + output: "{{ result }}" + msg: 'unable to connect to database' - name: update mysql variable value using incorrect login_password (expect failed=true) mysql_variables: login_user: '{{ mysql_user }}' login_password: 'wrongpassword' - login_host: 127.0.0.1 + login_host: '{{ mysql_host }}' login_port: '{{ mysql_primary_port }}' variable: '{{ set_name }}' value: '{{ set_value }}' register: result ignore_errors: true - - include: assert_fail_msg.yml output={{ result }} msg='unable to connect to database' + - include_tasks: assert_fail_msg.yml + vars: + output: "{{ result }}" + msg: 'unable to connect to database' #============================================================ # Verify mysql_variable fails with an incorrect login_host parameter @@ -281,7 +432,10 @@ register: result ignore_errors: true - - include: assert_fail_msg.yml output={{ result }} msg='unable to connect to database' + - include_tasks: assert_fail_msg.yml + vars: + output: "{{ result }}" + msg: 'unable to connect to database' - block: @@ -300,7 +454,12 @@ that: - result.queries == ["SET PERSIST `{{ set_name }}` = {{ set_value }}"] - - include: assert_var.yml changed=true output={{ result }} var_name={{ set_name }} var_value='{{ set_value }}' + - include_tasks: assert_var.yml + vars: + changed: true + output: "{{ result }}" + var_name: "{{ set_name }}" + var_value: '{{ set_value }}' - name: try to update mysql variable value (expect changed=false) in persist mode again mysql_variables: @@ -310,7 +469,12 @@ mode: persist register: result - - include: assert_var.yml changed=false output={{ result }} var_name={{ set_name }} var_value='{{ set_value }}' + - include_tasks: assert_var.yml + vars: + changed: false + output: "{{ result }}" + var_name: "{{ set_name }}" + var_value: '{{ set_value }}' - name: set mysql variable to a temp value mysql_variables: @@ -357,11 +521,16 @@ mode: persist_only register: result - - include: assert_var.yml changed=true output={{ result }} var_name={{ set_name }} var_value='{{ def_val }}' + - include_tasks: assert_var.yml + vars: + changed: true + output: "{{ result }}" + var_name: "{{ set_name }}" + var_value: '{{ def_val }}' when: - - mysql_version is version('8.0', '>=') - - install_type == 'mysql' + - db_engine == 'mysql' + - db_version is version('8.0', '>=') # Bugfix of https://github.com/ansible/ansible/issues/54239 # - name: set variable containing dot diff --git a/tests/integration/test_connection.yml b/tests/integration/test_connection.yml new file mode 100644 index 0000000..160cfba --- /dev/null +++ b/tests/integration/test_connection.yml @@ -0,0 +1,81 @@ +--- + +- name: Playbook to test bug to connect to MySQL/MariaDB server + hosts: all + gather_facts: false + vars: + mysql_parameters: &mysql_params + login_user: '{{ mysql_user }}' + login_password: '{{ mysql_password }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + tasks: + + # Create default MySQL config file with credentials + - name: mysql_info - create default config file + template: + src: my.cnf.j2 + dest: /root/.my.cnf + mode: '0400' + + # Create non-default MySQL config file with credentials + - name: mysql_info - create non-default config file + template: + src: tests/integration/targets/test_mysql_info/templates/my.cnf.j2 + dest: /root/non-default_my.cnf + mode: '0400' + + ############### + # Do tests + + # Access by default cred file + - name: mysql_info - collect default cred file + mysql_info: + login_user: '{{ mysql_user }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + register: result + + - assert: + that: + - result is not changed + - db_version in result.version.full + - result.settings != {} + - result.global_status != {} + - result.databases != {} + - result.engines != {} + - result.users != {} + + # Access by non-default cred file + - name: mysql_info - check non-default cred file + mysql_info: + login_user: '{{ mysql_user }}' + login_host: '{{ mysql_host }}' + login_port: '{{ mysql_primary_port }}' + config_file: /root/non-default_my.cnf + register: result + + - assert: + that: + - result is not changed + - result.version != {} + + # Remove cred files + - name: mysql_info - remove cred files + file: + path: '{{ item }}' + state: absent + with_items: + - /root/.my.cnf + - /root/non-default_my.cnf + + # Access with password + - name: mysql_info - check access with password + mysql_info: + <<: *mysql_params + register: result + + - assert: + that: + - result is not changed + - result.version != {} diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt deleted file mode 100644 index c0323af..0000000 --- a/tests/sanity/ignore-2.10.txt +++ /dev/null @@ -1,8 +0,0 @@ -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 diff --git a/tests/sanity/ignore-2.11.txt b/tests/sanity/ignore-2.11.txt deleted file mode 100644 index c0323af..0000000 --- a/tests/sanity/ignore-2.11.txt +++ /dev/null @@ -1,8 +0,0 @@ -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 diff --git a/tests/sanity/ignore-2.12.txt b/tests/sanity/ignore-2.12.txt deleted file mode 100644 index c0323af..0000000 --- a/tests/sanity/ignore-2.12.txt +++ /dev/null @@ -1,8 +0,0 @@ -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 diff --git a/tests/sanity/ignore-2.13.txt b/tests/sanity/ignore-2.13.txt deleted file mode 100644 index c0323af..0000000 --- a/tests/sanity/ignore-2.13.txt +++ /dev/null @@ -1,8 +0,0 @@ -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 diff --git a/tests/sanity/ignore-2.14.txt b/tests/sanity/ignore-2.14.txt deleted file mode 100644 index c0323af..0000000 --- a/tests/sanity/ignore-2.14.txt +++ /dev/null @@ -1,8 +0,0 @@ -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 diff --git a/tests/sanity/ignore-2.15.txt b/tests/sanity/ignore-2.15.txt new file mode 100644 index 0000000..152162d --- /dev/null +++ b/tests/sanity/ignore-2.15.txt @@ -0,0 +1,3 @@ +plugins/modules/mysql_db.py validate-modules:use-run-command-not-popen +plugins/module_utils/mysql.py pylint:unused-import +plugins/module_utils/version.py pylint:unused-import diff --git a/tests/sanity/ignore-2.16.txt b/tests/sanity/ignore-2.16.txt new file mode 100644 index 0000000..152162d --- /dev/null +++ b/tests/sanity/ignore-2.16.txt @@ -0,0 +1,3 @@ +plugins/modules/mysql_db.py validate-modules:use-run-command-not-popen +plugins/module_utils/mysql.py pylint:unused-import +plugins/module_utils/version.py pylint:unused-import diff --git a/tests/sanity/ignore-2.17.txt b/tests/sanity/ignore-2.17.txt new file mode 100644 index 0000000..152162d --- /dev/null +++ b/tests/sanity/ignore-2.17.txt @@ -0,0 +1,3 @@ +plugins/modules/mysql_db.py validate-modules:use-run-command-not-popen +plugins/module_utils/mysql.py pylint:unused-import +plugins/module_utils/version.py pylint:unused-import diff --git a/tests/sanity/ignore-2.18.txt b/tests/sanity/ignore-2.18.txt new file mode 100644 index 0000000..152162d --- /dev/null +++ b/tests/sanity/ignore-2.18.txt @@ -0,0 +1,3 @@ +plugins/modules/mysql_db.py validate-modules:use-run-command-not-popen +plugins/module_utils/mysql.py pylint:unused-import +plugins/module_utils/version.py pylint:unused-import diff --git a/tests/sanity/ignore-2.19.txt b/tests/sanity/ignore-2.19.txt new file mode 100644 index 0000000..152162d --- /dev/null +++ b/tests/sanity/ignore-2.19.txt @@ -0,0 +1,3 @@ +plugins/modules/mysql_db.py validate-modules:use-run-command-not-popen +plugins/module_utils/mysql.py pylint:unused-import +plugins/module_utils/version.py pylint:unused-import diff --git a/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt deleted file mode 100644 index dabd55d..0000000 --- a/tests/sanity/ignore-2.9.txt +++ /dev/null @@ -1,3 +0,0 @@ -plugins/modules/mysql_db.py validate-modules:use-run-command-not-popen -plugins/modules/mysql_user.py validate-modules:parameter-type-not-in-doc -plugins/modules/mysql_user.py validate-modules:undocumented-parameter diff --git a/tests/unit/plugins/module_utils/test_command_resolver.py b/tests/unit/plugins/module_utils/test_command_resolver.py new file mode 100644 index 0000000..9653418 --- /dev/null +++ b/tests/unit/plugins/module_utils/test_command_resolver.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from ansible_collections.community.mysql.plugins.module_utils.command_resolver import ( + CommandResolver, +) + + +@pytest.mark.parametrize( + 'server_implementation,server_version,command,expected_output,expected_exception,expected_message', + [ + ('mysql', '1.0.0', 'SHOW NOTHING', '', ValueError, 'Unsupported command: SHOW NOTHING'), + ('mysql', '8.0.20', 'SHOW MASTER STATUS', 'SHOW MASTER STATUS', None, None), # Case insensitive + ('mysql', '8.0.20', 'show master status', 'SHOW MASTER STATUS', None, None), # Case insensitive + ('mysql', '8.0.20', 'SHOW master STATUS', 'SHOW MASTER STATUS', None, None), # Case insensitive + ('mysql', '8.2.0', 'SHOW MASTER STATUS', 'SHOW BINARY LOG STATUS', None, None), + ('mysql', '9.0.0', 'SHOW MASTER STATUS', 'SHOW BINARY LOG STATUS', None, None), + ('mariadb', '10.4.23', 'SHOW MASTER STATUS', 'SHOW MASTER STATUS', None, None), # Default + ('mariadb', '10.5.1', 'SHOW MASTER STATUS', 'SHOW MASTER STATUS', None, None), # Default + ('mariadb', '10.5.2', 'SHOW MASTER STATUS', 'SHOW BINLOG STATUS', None, None), + ('mariadb', '10.6.17', 'SHOW MASTER STATUS', 'SHOW BINLOG STATUS', None, None), + ('mysql', '8.4.1', 'CHANGE MASTER', 'CHANGE REPLICATION SOURCE', None, None), + ] +) +def test_resolve_command(server_implementation, server_version, command, expected_output, expected_exception, expected_message): + """ + Tests that the CommandResolver method resolve_command return the correct query. + """ + resolver = CommandResolver(server_implementation, server_version) + if expected_exception: + with pytest.raises(expected_exception) as excinfo: + resolver.resolve_command(command) + assert str(excinfo.value) == expected_message + else: + assert resolver.resolve_command(command) == expected_output diff --git a/tests/unit/plugins/module_utils/test_mariadb_replication.py b/tests/unit/plugins/module_utils/test_mariadb_replication.py index deb3099..513d8cf 100644 --- a/tests/unit/plugins/module_utils/test_mariadb_replication.py +++ b/tests/unit/plugins/module_utils/test_mariadb_replication.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright: (c) 2020, Andrew Klychkov (@Andersson007) +# Copyright: (c) 2020, Andrew Klychkov (@Andersson007) from __future__ import (absolute_import, division, print_function) __metaclass__ = type diff --git a/tests/unit/plugins/module_utils/test_mysql.py b/tests/unit/plugins/module_utils/test_mysql.py index ac4de24..5410575 100644 --- a/tests/unit/plugins/module_utils/test_mysql.py +++ b/tests/unit/plugins/module_utils/test_mysql.py @@ -1,9 +1,10 @@ from __future__ import (absolute_import, division, print_function) + __metaclass__ = type import pytest -from ansible_collections.community.mysql.plugins.module_utils.mysql import get_server_version +from ansible_collections.community.mysql.plugins.module_utils.mysql import get_server_version, get_server_implementation from ..utils import dummy_cursor_class @@ -22,3 +23,21 @@ def test_get_server_version(cursor_return_version, cursor_return_type): """ cursor = dummy_cursor_class(cursor_return_version, cursor_return_type) assert get_server_version(cursor) == cursor_return_version + + +@pytest.mark.parametrize( + 'cursor_return_version,cursor_return_type,server_implementation', + [ + ('5.7.0-mysql', 'dict', 'mysql'), + ('8.0.0-mysql', 'list', 'mysql'), + ('10.5.0-mariadb', 'dict', 'mariadb'), + ('10.5.1-mariadb', 'list', 'mariadb'), + ] +) +def test_get_server_implamentation(cursor_return_version, cursor_return_type, server_implementation): + """ + Test that server implementation are handled properly by get_server_implementation() whether the server version returned as a list or dict. + """ + cursor = dummy_cursor_class(cursor_return_version, cursor_return_type) + + assert get_server_implementation(cursor) == server_implementation diff --git a/tests/unit/plugins/module_utils/test_mysql_replication.py b/tests/unit/plugins/module_utils/test_mysql_replication.py index 96d4d9a..c4126a5 100644 --- a/tests/unit/plugins/module_utils/test_mysql_replication.py +++ b/tests/unit/plugins/module_utils/test_mysql_replication.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright: (c) 2020, Andrew Klychkov (@Andersson007) +# Copyright: (c) 2020, Andrew Klychkov (@Andersson007) from __future__ import (absolute_import, division, print_function) __metaclass__ = type diff --git a/tests/unit/plugins/module_utils/test_mysql_user.py b/tests/unit/plugins/module_utils/test_mysql_user.py index f0a7b32..bb1ec24 100644 --- a/tests/unit/plugins/module_utils/test_mysql_user.py +++ b/tests/unit/plugins/module_utils/test_mysql_user.py @@ -4,18 +4,14 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type import pytest -try: - from unittest.mock import MagicMock -except ImportError: - from mock import MagicMock from ansible_collections.community.mysql.plugins.module_utils.user import ( handle_grant_on_col, has_grant_on_col, normalize_col_grants, - sort_column_order + sort_column_order, + privileges_unpack, ) -from ..utils import dummy_cursor_class @pytest.mark.parametrize( @@ -97,3 +93,21 @@ def test_handle_grant_on_col(privileges, start, end, output): def test_normalize_col_grants(input_, expected): """Tests normalize_col_grants function.""" assert normalize_col_grants(input_) == expected + + +@pytest.mark.parametrize( + 'priv,expected,mode,column_case_sensitive,ensure_usage', + [ + ('mydb.*:SELECT', {'"mydb".*': ['SELECT']}, 'ANSI', False, False), + ('mydb.*:SELECT', {'`mydb`.*': ['SELECT']}, 'NOTANSI', False, False), + ('mydb.*:SELECT', {'"mydb".*': ['SELECT'], '*.*': ['USAGE']}, 'ANSI', False, True), + ('mydb.*:SELECT', {'`mydb`.*': ['SELECT'], '*.*': ['USAGE']}, 'NOTANSI', False, True), + ('mydb.*:SELECT (a)', {'`mydb`.*': ['SELECT (A)']}, 'NOTANSI', False, False), + ('mydb.*:UPDATE (b, a)', {'`mydb`.*': ['UPDATE (a, b)']}, 'NOTANSI', True, False), + ('mydb.*:SELECT (b, a, c)', {'`mydb`.*': ['SELECT (A, B, C)']}, 'NOTANSI', False, False), + ('mydb.*:SELECT (b, a, c)', {'`mydb`.*': ['SELECT (a, b, c)']}, 'NOTANSI', True, False), + ] +) +def test_privileges_unpack(priv, mode, column_case_sensitive, ensure_usage, expected): + """Tests privileges_unpack function.""" + assert privileges_unpack(priv, mode, column_case_sensitive, ensure_usage) == expected diff --git a/tests/unit/plugins/modules/test_mysql_info.py b/tests/unit/plugins/modules/test_mysql_info.py index 7aa9577..7b2de1c 100644 --- a/tests/unit/plugins/modules/test_mysql_info.py +++ b/tests/unit/plugins/modules/test_mysql_info.py @@ -14,15 +14,15 @@ from ansible_collections.community.mysql.plugins.modules.mysql_info import MySQL @pytest.mark.parametrize( - 'suffix,cursor_output', + 'suffix,cursor_output,server_implementation,server_version,user_implementation', [ - ('mysql', '5.5.1-mysql'), - ('log', '5.7.31-log'), - ('mariadb', '10.5.0-mariadb'), - ('', '8.0.22'), + ('mysql', '5.5.1-mysql', 'mysql', '5.5.1', 'mysql'), + ('log', '5.7.31-log', 'mysql', '5.7.31', 'mysql'), + ('mariadb', '10.5.0-mariadb', 'mariadb', '10.5.0', 'mariadb'), + ('', '8.0.22', 'mysql', '8.0.22', 'mysql'), ] ) -def test_get_info_suffix(suffix, cursor_output): +def test_get_info_suffix(suffix, cursor_output, server_implementation, server_version, user_implementation): def __cursor_return_value(input_parameter): if input_parameter == "SHOW GLOBAL VARIABLES": cursor.fetchall.return_value = [{"Variable_name": "version", "Value": cursor_output}] @@ -32,6 +32,6 @@ def test_get_info_suffix(suffix, cursor_output): cursor = MagicMock() cursor.execute.side_effect = __cursor_return_value - info = MySQL_Info(MagicMock(), cursor) + info = MySQL_Info(MagicMock(), cursor, server_implementation, server_version, user_implementation) assert info.get_info([], [], False)['version']['suffix'] == suffix diff --git a/tests/unit/plugins/modules/test_mysql_variables.py b/tests/unit/plugins/modules/test_mysql_variables.py new file mode 100644 index 0000000..8960173 --- /dev/null +++ b/tests/unit/plugins/modules/test_mysql_variables.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import pytest + +from ansible_collections.community.mysql.plugins.modules.mysql_variables import ( + convert_bool_setting_value_wanted, +) + + +@pytest.mark.parametrize( + 'value,output', + [ + (1, 'ON'), + (0, 'OFF'), + (2, 2), + ('on', 'ON'), + ('off', 'OFF'), + ('ON', 'ON'), + ('OFF', 'OFF'), + ] +) +def test_convert_bool_value(value, output): + assert convert_bool_setting_value_wanted(value) == output