Compare commits

...

57 commits
3.8.0 ... main

Author SHA1 Message Date
Andrew Klychkov
b26235b7d7
Release 3.13.0 commit (#705) 2025-03-21 07:02:43 +01:00
Keeper-of-the-Keys
45a29408ad
User locking (#702)
* function to check if a user is locked already

Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com>

* Add the location and logic of where I think user locking would happen.

Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com>

* Fix missing parameters for execute()

Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com>

* Add the locked attribute

Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com>

* Initial user locking integration tests

Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com>

* Add attribute documentation

Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com>

* More descriptive names in the integration tests

Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com>

* - Changes requested/suggested by @Andersson007
- Example usage
- Changelog fragment

Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com>

* Fix user_is_locked and remove host_all option.

Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com>

* Fix host of user (was % should have been localhost after deleting `host:` earlier)

Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com>

* Switch locked to named instead of positional.

Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com>

* Add check_mode support.

Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com>

* Add check_mode: true test cases

Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com>

* Fix names that included `check_mode: true`

Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com>

* Add idempotence checks

Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com>

* Switch calls to user_mod with sequences of None positional arguments to full named arguments

Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com>

* locked check should not run for roles.

Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com>

* check_mode is set at the task level and not the module level

Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com>

* Add user locking to info module and test.

Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com>

* Handle DictCursor

Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com>


* Add check_mode feedback

Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com>


* Add another builtin account to the exclusion list

Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com>

* Initial switch to default=None for locked, will need to add a test for it.

Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com>

* Add check that missing locked argument does not unlock a user

Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com>
---------

Signed-off-by: E.S. Rosenberg a.k.a. Keeper of the Keys <es.rosenberg+github@gmail.com>
2025-03-19 14:40:59 +01:00
Laurent Indermühle
dd7e297d50
Add support for MariaDB 11.4 (#703)
* fix missing symlink to mysql binaries for MariaDB 11+

* update tested version of MariaDB 11.4 instead of 10.5

* add changelog fragment

* [CI] add way to trigger workflow manually

Useful in the case we don't modifiy any files in the paths: sections of the push event.

* add version check for mariadb < 10.4.6 without mariadb* binaries

* Use same concatenation method between functions to avoid future confusion

I didn't notice that db_dump and db_import were different, thus I introduced a bug with the initialization of the variable cmd. This commit fixes that.
2025-03-10 18:55:42 +01:00
Andrew Klychkov
e9845b0a1c
Release 3.12.0 commit (#698) 2025-01-17 10:11:27 +01:00
Andrew Klychkov
960ac32adf
mysql_query: returns execution_time_ms list containing execution time per query (#697)
* mysql_query: returns execution_time_ms list containing execution time per query

* Update changelogs/fragments/0-mysql_query-returns-exec-time-ms.yml

Co-authored-by: Laurent Indermühle <laurent.indermuehle@pm.me>
2025-01-16 15:49:53 +01:00
Sergio
a45a0d006d
mysql_db: added zstd support (#696) 2025-01-16 09:35:04 +01:00
Andrew Klychkov
022ed60906
Fix linting issues (#693) 2024-12-13 09:21:06 +01:00
Laurent Indermuehle
3d3f115574
Add next expected version 2024-11-19 10:56:37 +01:00
Laurent Indermühle
e437d562c1
Release 3.11.0 commit (#692) 2024-11-19 10:51:58 +01:00
Laurent Indermühle
9057637844
mysql_info - add table count to the databases returned values (#691)
* Add tables count per database
* Add integrations tests
* Deduplicate tests between main and new task file
2024-11-19 08:51:03 +01:00
Laurent Indermühle
d613fa1993
Fix wrong documentation assertion (#690) 2024-11-18 15:44:39 +01:00
Laurent Indermühle
7d787eb238
Add contributors from last 10 PR pages (#688)
I've applied a sort on the whole file. This Patch is hard to read,
sorry. I've remove nobody! Only move!
2024-11-07 10:37:10 +01:00
Soledad208
ebb37ae7a3
sql_mode can be set in session, therefore we should look for ANSI_QUOTES in session variable instead of global variable (#677)
* issue-671: get ASNI_QUOTES from session sql_mode instead of GLOBAL sql_mode
2024-11-07 09:56:31 +01:00
Andrew Klychkov
90bd0b0a75
Update contributor's email (#684) 2024-10-24 10:57:36 +02:00
JS
93cd1850d9
Update mysql_user.py - table/privilege spacing update (#687)
* Update mysql_user.py - table/privilege spacing update

Add note for no spacing between the table and the privilege as this will make the task not idempotent in check mode but still make it idempotent when in normal mode.

* Update plugins/modules/mysql_user.py

Co-authored-by: Andrew Klychkov <aaklychkov@mail.ru>

---------

Co-authored-by: Andrew Klychkov <aaklychkov@mail.ru>
2024-10-23 10:31:40 +02:00
Andrew Klychkov
a5afa1a375
CI: add stable-2.18, fix README (#681)
* CI: add stable-2.18, fix README

* Update .github/workflows/ansible-test-plugins.yml

Co-authored-by: Laurent Indermühle <laurent.indermuehle@pm.me>

* Update .github/workflows/ansible-test-plugins.yml

Co-authored-by: Laurent Indermühle <laurent.indermuehle@pm.me>

* Update .github/workflows/ansible-test-plugins.yml

Co-authored-by: Laurent Indermühle <laurent.indermuehle@pm.me>

* Update README.md

Co-authored-by: Laurent Indermühle <laurent.indermuehle@pm.me>

---------

Co-authored-by: Laurent Indermühle <laurent.indermuehle@pm.me>
2024-09-26 14:31:08 +02:00
Maxwell G
28bf7093be
changelogs: categorize deprecations under deprecated_features (#679)
These should be put under deprecated_features so they show up properly
in the generated changelog.
2024-09-11 14:35:02 +02:00
Andrew Klychkov
a75d71a7ff
Release 3.10.3 commit (#678) 2024-09-09 15:05:25 +02:00
hubiongithub
eec6e7091f
Update user.py (#676)
* Update user.py

Added correct syntax to ed25519 password plugin.
on create user
on update user
This only accepts cleartext passwords (PASSWORD(%s)) not pregenerated ed25519 hashes.

* Update plugins/module_utils/user.py

Co-authored-by: Andrew Klychkov <aaklychkov@mail.ru>

* Update plugins/module_utils/user.py

Co-authored-by: Andrew Klychkov <aaklychkov@mail.ru>

* Update plugins/module_utils/user.py

Co-authored-by: Andrew Klychkov <aaklychkov@mail.ru>

* Update plugins/module_utils/user.py

Co-authored-by: Andrew Klychkov <aaklychkov@mail.ru>

* Update plugins/module_utils/user.py

* Update plugins/module_utils/user.py

---------

Co-authored-by: Andrew Klychkov <aaklychkov@mail.ru>
2024-09-09 15:01:26 +02:00
Andrew Klychkov
7188bea0c8
Release 3.10.2 commit (#675) 2024-09-06 08:21:45 +02:00
Andrew Klychkov
3425fdb839
mysql_user: add correct ed25519 plugin handling when creating a user (#674) 2024-09-05 12:19:33 +02:00
Andrew Klychkov
2db131f8c0
Release 3.10.1 commit (#673) 2024-09-04 07:19:59 +02:00
Andrew Klychkov
59c26211ca
mysql_user: deprecate alias user for name argument (#670)
* mysql_user: deprecate alias user for name argument
* Fix module and tests
2024-09-02 18:07:11 +02:00
Fran
0de9685cf1
Fix user plugin changes in check mode (#596)
* Fix user plugin changes in check mode

* Add auth plugin tests

* Undo local changes

* Improve task names

* Fix query

* Changes

* Add check

* Add check

* Add check

* Add one more check

* Add one more check

* Fix typo

* Change parameter

* Testing

* Remove tests

* Add tests

* Test first stteps

* Readd tests

* Test without check mode

* Test with check mode

* Test with check mode

* Testing

* Testing

* Add missing tests

* Changes for ansible-lint complaints

* Fix condition

* Update changelogs/fragments/596-fix-check-changes.yaml

Co-authored-by: Andrew Klychkov <aaklychkov@mail.ru>

* refactor

* Add more tests

* Fix newpass var

* Remove extra test

---------

Co-authored-by: Andrew Klychkov <aaklychkov@mail.ru>
2024-08-30 11:15:16 +02:00
Andrew Klychkov
87be61ccf3
CI: Fix sanity errors (#668) 2024-08-29 08:47:48 +02:00
Andrew Klychkov
37a718c66f
Release 3.10.0 commit (#667) 2024-08-22 10:45:53 +02:00
Andrew Klychkov
a9f9806728
README: Add Communication section with Forum information (#665) 2024-08-19 10:41:13 +02:00
Laurent Indermühle
cd9f4fcf57
Fix deprecated options from MySQL 8.2 (#662)
* Fix show master status for MySQL 8.2+

* Fix mysqldump option form --master-data to --source-data

* Fix incompatibility between mysqldump 8.0 and MySQL 8.4

Installing the same version between the client and the server makes
sense anyway. The incompatibility arise when you use mysqldump with
--source-data. The the tool tries to perform a SHOW MASTER STATUS which
is deprecated in MySQL 8.2+.

* Fix missing condition

* Fix unit tests

* Add a query resolver depending on implementation and version

* Sanity

* Fix SHOW REPLICA STATUS queries

* Fix mariadb's SHOW REPLICA HOSTS query

* Fix CHANGE MASTER for MySQL 8.0.23+

* Fix integration test for CHANGE MASTER

* Fix integration test for CHANGE MASTER

* Fix replication queries for MySQL 8.0.23+ and 8.4+

* Revert file edited by mistake

* Enhance tests format
2024-08-05 08:55:18 +02:00
Laurent Indermühle
c503dc5b6b
[CI] Add 2024 versions to tests (#660)
* Enable mysql_native_password for MySQL 8.2+
* Fix connection to MySQL 8 since Ubuntu 20.04 update
* Cut mysqlclient form the documentation
* Cut tests for Python 3.12 not supported by ansible-test
* Upgrade integration controller to ubuntu2204 by removing python

ansible-test uses python 3.10 if we specify ubuntu2204. Thus we lose the
ability to chose specific version of python to test. But integrations
tests are optional for a collection. And we don't catch a issue with
Python that often (ever ? I don't recall seen one).

This allow us to test MySQL 8.4, so it's a win.

* Cut tests for EoL MariaDB 10.4
* Reduce number of test in the matrix
* Cut support for intermediate LTS
* Fix python command not found with ansible-devel and add the debug

This is puzzling me. Why when using ansible devel the python command
changes? I know ansible-test install python after starting ubuntu22.04
so the way python is install must changes.

* Disable retry-on-error

When reading log we tend to look at the bottom, but doing so we find
often a idempotent error that are nothing to do with the first error.
Disabling this can greatly speedup tests and makes logs more readable.

Plus, now GHA jumps automatically at the latest error message. So with
this modification, we will always jump to the latest real error message.

* Enhance jobs title readability

We can't expand the left column on GHA, so the shorter, the better.
Use Ⓐ instead of Ansible.
2024-07-19 11:04:13 +02:00
Andrew Klychkov
83ed4af4e1
Deprecate mysqlclient/MySQLdb connector support (#655)
* Deprecate mysqlclient/MySQLdb connector support

* Update README

* Put in README that mysqlclient is deprecated
2024-07-09 08:20:47 +02:00
Andrew Klychkov
4912f1a41b
mysql_variables: fix boolean value handling (#653)
* mysql_variables: fix boolean value handling

* fix

* Fix tests

* Fix tests

* Fix

* Fix

* Fix

* Fix comment
2024-06-28 11:34:59 +02:00
Laurent Indermühle
33e8754c4e
Fix mysql_user on_new_username IndexError (#642)
* fix tuple indexerror when no accounts are found

* Fix tests for update_password not executed

* Add test for case where existing user have different password

* lint to prevent warning about jinja templating in when clause

* Refactor get_existing_authentication to return a list of all row found

Previously we were returning only the first row found. We need to be
able to see if there is a difference in the existing passwords.

* Refactor host option to be optional

This make it possible to use the same method from mysql_user to help
update_password retrieve existing password for all account with the same
username independently of their hostname. And from mysql_info to get
the password of a specif user using WHERE user = '' AND host = ''

* Add change log fragment

* Add link to the PR in the change log

* lint for ansible devel

* Fix templating type error could not cconvert to bool with ansible devel

* Revert changes made for ansible-devel that broke tests for Ansible 2.15

* Revert changes made for ansible-devel that broke tests

* Cut unnecessary set, uniqueness is ensured by the group_by in the query

* Cut auth plugin from returned values when multiple existing auths exists

Discussed here:
https://github.com/ansible-collections/community.mysql/pull/642/files#r1649720519

* fix convertion of list(dict) to list(tuple)

* Fix test for empty password on MySQL 8+
2024-06-27 22:12:01 +02:00
Laurent Indermühle
1922e7154e
[CI] Remove ansible-test custom containers (#650)
* Cut tests containers

* Cut unused flatten versions

* Fix installation of mysqlclient on Ubuntu

* Cut unused variables

* Fix package missing on Unbuntu 22.04

* Fix variable templating

* Fix test for ansible 2.17 and do remove the ignore_errors

ignore_errors is bad because it makes searching for real errors
difficult.
2024-06-24 09:36:32 +02:00
Andrew Klychkov
aafe658a85
Update README.md (#648) 2024-06-19 10:20:34 +02:00
Andrew Klychkov
f266ba59c9
mysql_info: add server_engine return value (#649)
* mysql_info: add server_engine return value

* Incorporate feedback
2024-06-19 10:17:02 +02:00
Matthieu Bourgain
0bc3e3d848
Add salt parameter to hash generation for sha256 plugins (#631)
* add salt parameter to hash generation for sha256 plugin
* technomax review modification
* no general user test for salt
2024-06-11 17:23:05 +02:00
Laurent Indermühle
50e7413b88
Fix hashed passwords being returned by get_existing_authentication() via the plugin_auth_string variable instead of plugin_hash_string (#629)
* fix returned variable from plugin_auth_string to plugin_hash_string
* Refactor to keep plugin_auth_string in addition to plugin_hash_string
* Add breaking_changes to the changelog
2024-06-06 13:05:31 +02:00
Robert Silén
6c4dca4bce
mention MariaDB (#640)
* mention MariaDB

* mention MariaDB in descriptions and notes

* nits

* chmod -x
2024-05-31 09:14:43 +02:00
Dennis Felipe Urtubia
47610347ba
Adds support for show binary log status statement (#638)
* feat: adds support for show binary log status statement

* feat: adds support for mariadb show binlog status statement
2024-05-30 17:10:36 +02:00
Dennis Felipe Urtubia
a80b805619
Adds support for CHANGE REPLICATION SOURCE TO statement (#636)
* feat: adds support for 'change replication source to' statement
2024-05-21 20:58:05 +02:00
Laurent Indermühle
6ce2f49f96
Improve get replica/primary status (#634)
* Fix case where a failed fetchone() still return a dict

* Fix test for MariaDB

* fix case where  a failed fetchone() still return a dict for primary

* Add changelog fragment
2024-05-02 10:26:04 +02:00
Laurent Indermühle
47710cfb93
Enhance support of tls_requires in mysql_user and mysql_info (#628)
* fix option name

* Add tests for users using SSL

* Rewrite get_tls_requires using mysql.user table

* Add tls_requires to users_info filter

* add more consistant test users

* Add tls tests users in cleanup task

* Fix tls_requires data structure inconsistencies between modules

* Refactor user implementation to host get_tls_requires

* fix MySQL tls_requires not removed from user passed as empty

* Fix wrong variable used to return a hashed password

* Fix sanity

* fix unit tests

* Add changelog fragment

* Add PR URI to the changelog

* Add more precise change log

* fix documentation using wrong variable as an example

* Document example returned value `tls_requires` from users_info filter

* Revert changes that will be in a separate PR

* Fix sanity
2024-04-16 10:52:24 +02:00
Laurent Indermühle
0618ff6c41
Fix sanity tests for ansible-core 2.18 (#627) 2024-04-12 09:00:43 +02:00
Laurent Indermühle
f105fd9a95
Add tests for Ansible core 2.17 (devel is 2.18 today) and bump tests dependencies (#623)
* Add tests for Ansible core 2.17 (devel is 2.18 today)

* Drop tests for Ansible core 2.14 and add 2.17

* Cut duplicate exclude

* Add back python 3.8 and 3.9 for stable2.15

* Bump action to prevent deprecation warnings

* Cut python 3.9 for devel in roles tests

* Attempt to fix GHA line folding

* fix typo

* Bump ubuntu

Latest ansible-test doesn't work with old ubuntu. See here for more
info: https://github.com/ansible-collections/collection_template/blob/main/.github/workflows/ansible-test.yml#L83-L91

* fix docker_image var assignation

* fix yamllint false positive

* Attempt to fix docker_image_multiline assignation

* Fix empty var due to scope of each command

* Attempt to fix docker_image assignation

* fix error "vars should be dict"

* Document URL of the repository for the action ansible-test-gh-action

* Disable role tests

* Document ansible-core version tested

* Cut ansible-core 2.14 from testing documentation
2024-04-11 10:46:43 +02:00
Andrew Klychkov
bfe2fdc3ff
mysql_user: fix ed25512 plugin handling (#619) 2024-03-14 07:19:39 +01:00
Andrew Klychkov
c99c19a489
README.md: update Communication guide (#617) 2024-02-27 10:27:19 +01:00
Andrew Klychkov
52a11d7235
Release 3.9.0 commit (#616) 2024-02-22 10:53:01 +01:00
tompal3
40af258d86
password_expire support for mysql_user (#598)
* initial commit for password_expire support

* sanity check and default values

* add one more if block for version check

* some changes and integration tests

* docs and sanity and integration test fix

* make integration tests work

* make integration tests work

* fix unneeded commits

* fix verify as well

* Update plugins/modules/mysql_user.py

Co-authored-by: Laurent Indermühle <laurent.indermuehle@pm.me>

* Update tests/integration/targets/test_mysql_user/tasks/test_password_expire.yml

Co-authored-by: Laurent Indermühle <laurent.indermuehle@pm.me>

* Apply suggestions from code review

Co-authored-by: Laurent Indermühle <laurent.indermuehle@pm.me>

* Update plugins/modules/mysql_user.py

Co-authored-by: Andrew Klychkov <aaklychkov@mail.ru>

* Update plugins/modules/mysql_user.py

Co-authored-by: Andrew Klychkov <aaklychkov@mail.ru>

* Update plugins/modules/mysql_user.py

Co-authored-by: Andrew Klychkov <aaklychkov@mail.ru>

* Update plugins/modules/mysql_user.py

Co-authored-by: Andrew Klychkov <aaklychkov@mail.ru>

* Update plugins/module_utils/user.py

Co-authored-by: Andrew Klychkov <aaklychkov@mail.ru>

* Update plugins/module_utils/user.py

Co-authored-by: Andrew Klychkov <aaklychkov@mail.ru>

* Update plugins/module_utils/user.py

Co-authored-by: Andrew Klychkov <aaklychkov@mail.ru>

* typo and no_log remove for password_expire* vars

* add change log fragment

* move one if statement to module initialiazation

* fix merge conflicts

* fix order

* some fixes

* set no_log to true for password word containing keys

* fix sanity error

* Update changelogs/fragments/598-password_expire-support-for-mysql_user.yml

Co-authored-by: Andrew Klychkov <aaklychkov@mail.ru>

---------

Co-authored-by: Laurent Indermühle <laurent.indermuehle@pm.me>
Co-authored-by: Andrew Klychkov <aaklychkov@mail.ru>
2024-02-22 10:31:01 +01:00
Andrew Klychkov
21fe52d8f1
CONTRIBUTING.md: add a detailed guide (#615) 2024-02-22 10:19:08 +01:00
Andrew Klychkov
32718ca295
Update MAINTAINERS file (#612) 2024-01-25 07:55:51 +01:00
Andrew Klychkov
e34209b3f8
Fix sanity issues (#609)
* Fix sanity issues

* Remove ignore entries
2024-01-23 11:27:47 +01:00
Andrew Klychkov
5ed3eaf3ee
Version 2.*.* is EOL (#605) 2024-01-19 15:51:47 +01:00
William Felipe Welter
852c19a78a
Using show all slaves status when using MariaDB to be consistent with MySQL (#602)
* Using `show all slaves status` whe using MariaDB to be consistent
with the MySQL behaviour.

* Fixing lint issues

* Fix issue by using dict attribute

* Fix unit tests

* fix lint test

* Add unit tests

* Fix unit tests

* Adding changlog fragment

* Update changelogs/fragments/602-show-all-slaves-status.yaml

Co-authored-by: Laurent Indermühle <laurent.indermuehle@pm.me>

* Refactoring change by moving common logic to the module_utils

* Fix sanity checks

* Fix sanity checks

* Adding lines to fix sanity checks

* Fixing sanity checks

* Update changelogs/fragments/602-show-all-slaves-status.yaml

Co-authored-by: Andrew Klychkov <aaklychkov@mail.ru>

* Removing is_mariadb and is_mysql functions

---------

Co-authored-by: Laurent Indermühle <laurent.indermuehle@pm.me>
Co-authored-by: Andrew Klychkov <aaklychkov@mail.ru>
2024-01-19 15:41:29 +01:00
ncc
051aa48d8d
feat[mysql_user]: add support for mysql user attributes (#604)
* add support for mysql user attributes

* fix CI

* write integration tests

* requested changes pt. 1

* requested changes pt. 2

* fix changelog fragment

---------

Co-authored-by: n-cc <ncc@github.com>
2024-01-19 15:37:28 +01:00
Laurent Indermühle
81ab18d56c
chore: fix conditional statements should not include jinja 2 templating (#599)
Thanks to @tompal3 for your contribution
2023-11-30 13:39:34 +01:00
Andrew Klychkov
8dfab12bae
README: Add forum info (#589) 2023-11-13 12:35:39 +01:00
Laurent Indermühle
0dbedf57cb
Document MySQL and MariaDB don't store roles with same manner (#584) 2023-10-26 14:21:28 +02:00
105 changed files with 4294 additions and 1685 deletions

View file

@ -1,6 +1,6 @@
--- ---
name: Plugins CI name: Plugins CI
on: on: # yamllint disable-line rule:truthy
push: push:
paths: paths:
- 'plugins/**' - 'plugins/**'
@ -13,20 +13,21 @@ on:
- '.github/workflows/ansible-test-plugins.yml' - '.github/workflows/ansible-test-plugins.yml'
schedule: schedule:
- cron: '0 6 * * *' - cron: '0 6 * * *'
workflow_dispatch:
jobs: jobs:
sanity: sanity:
name: "Sanity (Ansible: ${{ matrix.ansible }})" name: "Sanity (${{ matrix.ansible }})"
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
strategy: strategy:
matrix: matrix:
ansible: ansible:
- stable-2.14
- stable-2.15
- stable-2.16 - stable-2.16
- stable-2.17
- stable-2.18
- devel - devel
steps: steps:
# https://github.com/ansible-community/ansible-test-gh-action
- name: Perform sanity testing - name: Perform sanity testing
uses: ansible-community/ansible-test-gh-action@release/v1 uses: ansible-community/ansible-test-gh-action@release/v1
with: with:
@ -34,166 +35,134 @@ jobs:
testing-type: sanity testing-type: sanity
pull-request-change-detection: true 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: integration:
name: "Integration (Python: ${{ matrix.python }}, Ansible: ${{ matrix.ansible }}, DB: ${{ matrix.db_engine_name }} ${{ matrix.db_engine_version }}, connector: ${{ matrix.connector_name }} ${{ matrix.connector_version }})" name: "Integration (${{ matrix.ansible }}, DB: ${{ matrix.db_engine_name }} ${{ matrix.db_engine_version }}, connector: ${{ matrix.connector_name }} ${{ matrix.connector_version }})"
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
ansible: ansible:
- stable-2.14
- stable-2.15
- stable-2.16 - stable-2.16
- stable-2.17
- stable-2.18
- devel - devel
db_engine_name: db_engine_name:
- mysql - mysql
- mariadb - mariadb
db_engine_version: db_engine_version:
- 5.7.40 - '8.0.38'
- 8.0.31 - '8.4.1'
- 10.4.27 - '10.11.8'
- 10.5.18 - '11.4.5'
- 10.6.11
python:
- '3.8'
- '3.9'
- '3.10'
connector_name: connector_name:
- pymysql - pymysql
- mysqlclient - mysqlclient
connector_version: connector_version:
- 0.7.11 - '0.9.3'
- 0.9.3 - '1.0.2'
- 1.0.2 - '1.1.1'
- 2.0.1 - '2.0.1'
- 2.0.3 - '2.0.3'
- 2.1.1 - '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: exclude:
- db_engine_name: mysql
db_engine_version: 10.4.27
- db_engine_name: mysql - db_engine_name: mysql
db_engine_version: 10.5.18 db_engine_version: '10.11.8'
- db_engine_name: mysql - db_engine_name: mysql
db_engine_version: 10.6.11 db_engine_version: '11.4.5'
- db_engine_name: mariadb - db_engine_name: mariadb
db_engine_version: 5.7.40 db_engine_version: '8.0.38'
- db_engine_name: mariadb - db_engine_name: mariadb
db_engine_version: 8.0.31 db_engine_version: '8.4.1'
- connector_name: pymysql - connector_name: pymysql
connector_version: 2.0.1 connector_version: '2.0.1'
- connector_name: pymysql - connector_name: pymysql
connector_version: 2.0.3 connector_version: '2.0.3'
- connector_name: pymysql - connector_name: pymysql
connector_version: 2.1.1 connector_version: '2.1.1'
- connector_name: mysqlclient - connector_name: mysqlclient
connector_version: 0.7.11 connector_version: '0.9.3'
- connector_name: mysqlclient - connector_name: mysqlclient
connector_version: 0.9.3 connector_version: '1.0.2'
- connector_name: mysqlclient - connector_name: mysqlclient
connector_version: 1.0.2 connector_version: '1.1.1'
- db_engine_name: mariadb - db_engine_version: '8.0.38'
connector_version: 0.7.11 ansible: stable-2.17
- db_engine_version: 5.7.40 - db_engine_version: '10.11.8'
python: '3.9' ansible: stable-2.17
- db_engine_version: 5.7.40 - db_engine_version: '8.0.38'
python: '3.10' ansible: devel
- db_engine_version: 5.7.40 - db_engine_version: '10.11.8'
ansible: stable-2.14 ansible: devel
- db_engine_version: 5.7.40 - 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 ansible: stable-2.15
- db_engine_version: 5.7.40 - db_engine_version: '8.4.1'
ansible: stable-2.16
- db_engine_version: 5.7.40
ansible: devel
- db_engine_version: 8.0.31
python: '3.8'
- db_engine_version: 8.0.31
python: '3.8'
- db_engine_version: 10.4.27
python: '3.10'
- db_engine_version: 10.4.27
ansible: devel
- db_engine_version: 10.6.11
python: '3.8'
- db_engine_version: 10.6.11
python: '3.9'
- python: '3.8'
connector_version: 1.0.2
- python: '3.8'
connector_version: 2.0.3
- python: '3.8'
connector_version: 2.1.1
- python: '3.9'
connector_version: 0.7.11
- python: '3.9'
connector_version: 1.0.2
- python: '3.9'
connector_version: 2.0.1
- python: '3.9'
connector_version: 2.1.1
- python: '3.10'
connector_version: 0.7.11
- python: '3.10'
connector_version: 0.9.3
- python: '3.10'
connector_version: 2.0.1
- python: '3.10'
connector_version: 2.0.3
- python: '3.8'
ansible: stable-2.14
- python: '3.8'
ansible: stable-2.15 ansible: stable-2.15
- python: '3.8' - connector_version: '1.1.1'
ansible: stable-2.16 db_engine_version: '8.0.38'
- python: '3.8' - connector_version: '1.1.1'
ansible: devel db_engine_version: '10.11.8'
- python: '3.9'
ansible: stable-2.15
- python: '3.9'
ansible: stable-2.16
- python: '3.9'
ansible: devel
services: services:
db_primary: db_primary:
@ -206,7 +175,7 @@ jobs:
# We write our own health-cmd because the mariadb container does not # We write our own health-cmd because the mariadb container does not
# provide a healthcheck # provide a healthcheck
options: >- options: >-
--health-cmd "mysqladmin ping -P 3306 -pmsandbox |grep alive || exit 1" --health-cmd "${{ matrix.db_engine_name == 'mysql' && 'mysqladmin' || 'mariadb-admin' }} ping -P 3306 -pmsandbox |grep alive || exit 1"
--health-start-period 10s --health-start-period 10s
--health-interval 10s --health-interval 10s
--health-timeout 5s --health-timeout 5s
@ -220,7 +189,7 @@ jobs:
ports: ports:
- 3308:3306 - 3308:3306
options: >- options: >-
--health-cmd "mysqladmin ping -P 3306 -pmsandbox |grep alive || exit 1" --health-cmd "${{ matrix.db_engine_name == 'mysql' && 'mysqladmin' || 'mariadb-admin' }} ping -P 3306 -pmsandbox |grep alive || exit 1"
--health-start-period 10s --health-start-period 10s
--health-interval 10s --health-interval 10s
--health-timeout 5s --health-timeout 5s
@ -234,7 +203,7 @@ jobs:
ports: ports:
- 3309:3306 - 3309:3306
options: >- options: >-
--health-cmd "mysqladmin ping -P 3306 -pmsandbox |grep alive || exit 1" --health-cmd "${{ matrix.db_engine_name == 'mysql' && 'mysqladmin' || 'mariadb-admin' }} ping -P 3306 -pmsandbox |grep alive || exit 1"
--health-start-period 10s --health-start-period 10s
--health-interval 10s --health-interval 10s
--health-timeout 5s --health-timeout 5s
@ -246,9 +215,22 @@ jobs:
- name: Restart MySQL server with settings for replication - name: Restart MySQL server with settings for replication
run: | run: |
docker exec ${{ job.services.db_primary.id }} bash -c 'echo -e [mysqld]\\nserver-id=1\\nlog-bin=/var/lib/mysql/primary-bin > /etc/mysql/conf.d/replication.cnf' db_ver="${{ matrix.db_engine_version }}"
docker exec ${{ job.services.db_replica1.id }} bash -c 'echo -e [mysqld]\\nserver-id=2\\nlog-bin=/var/lib/mysql/replica1-bin > /etc/mysql/conf.d/replication.cnf' maj="${db_ver%.*.*}"
docker exec ${{ job.services.db_replica2.id }} bash -c 'echo -e [mysqld]\\nserver-id=3\\nlog-bin=/var/lib/mysql/replica2-bin > /etc/mysql/conf.d/replication.cnf' 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_primary.id }}
docker restart -t 30 ${{ job.services.db_replica1.id }} docker restart -t 30 ${{ job.services.db_replica1.id }}
docker restart -t 30 ${{ job.services.db_replica2.id }} docker restart -t 30 ${{ job.services.db_replica2.id }}
@ -260,48 +242,13 @@ jobs:
${{ job.services.db_primary.id }} ${{ job.services.db_primary.id }}
| grep healthy && [[ "$SECONDS" -lt 120 ]]; do sleep 1; done | grep healthy && [[ "$SECONDS" -lt 120 ]]; do sleep 1; done
- name: Compute docker_image - Set python_version_flat
run: >
echo "python_version_flat=$(echo ${{ matrix.python }}
| tr -d '.')" >> $GITHUB_ENV
- name: Compute docker_image - Set connector_version_flat
run: >
echo "connector_version_flat=$(echo ${{ matrix.connector_version }}
|tr -d .)" >> $GITHUB_ENV
- name: Compute docker_image - Set db_engine_version_flat
run: >
echo "db_engine_version_flat=$(echo ${{ matrix.db_engine_version }}
| awk -F '.' '{print $1 $2}')" >> $GITHUB_ENV
- name: Compute docker_image - Set db_client
run: >
if [[ ${{ env.db_engine_version_flat }} == 57 ]]; then
echo "db_client=my57" >> $GITHUB_ENV;
else
echo "db_client=$(echo ${{ matrix.db_engine_name }})" >> $GITHUB_ENV;
fi
- name: Set docker_image
run: >
docker_image_multiline=("
ghcr.io/ansible-collections/community.mysql\
/test-container-${{ env.db_client }}\
-py${{ env.python_version_flat }}\
-${{ matrix.connector_name }}${{ env.connector_version_flat }}\
:latest")
echo "docker_image=$(printf '%s' $docker_image_multiline)"
>> $GITHUB_ENV
- name: >- - name: >-
Perform integration testing against Perform integration testing against
Ansible version ${{ matrix.ansible }} Ansible version ${{ matrix.ansible }}
under Python ${{ matrix.python }}
uses: ansible-community/ansible-test-gh-action@release/v1 uses: ansible-community/ansible-test-gh-action@release/v1
with: with:
ansible-core-version: ${{ matrix.ansible }} ansible-core-version: ${{ matrix.ansible }}
docker-image: ubuntu2204
pre-test-cmd: >- pre-test-cmd: >-
echo Setting db_engine_name to "${{ matrix.db_engine_name }}"...; echo Setting db_engine_name to "${{ matrix.db_engine_name }}"...;
echo -n "${{ matrix.db_engine_name }}" echo -n "${{ matrix.db_engine_name }}"
@ -320,47 +267,66 @@ jobs:
echo -n "${{ matrix.connector_version }}" echo -n "${{ matrix.connector_version }}"
> tests/integration/connector_version; > tests/integration/connector_version;
echo Setting Python version to "${{ matrix.python }}"...;
echo -n "${{ matrix.python }}"
> tests/integration/python;
echo Setting Ansible version to "${{ matrix.ansible }}"...; echo Setting Ansible version to "${{ matrix.ansible }}"...;
echo -n "${{ matrix.ansible }}" echo -n "${{ matrix.ansible }}"
> tests/integration/ansible > tests/integration/ansible
docker-image: ${{ env.docker_image }}
target-python-version: ${{ matrix.python }}
testing-type: integration testing-type: integration
integration-retry-on-error: false
units: units:
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
name: Units (Ⓐ${{ matrix.ansible }}) name: Units (Ⓐ${{ matrix.ansible }}, Python${{ matrix.python }})
strategy: strategy:
# As soon as the first unit test fails, # As soon as the first unit test fails,
# cancel the others to free up the CI queue # cancel the others to free up the CI queue
fail-fast: true fail-fast: true
matrix: matrix:
ansible: ansible:
- stable-2.14
- stable-2.15
- stable-2.16 - stable-2.16
- stable-2.17
- stable-2.18
- devel - devel
python: python:
- 3.8 - '3.8'
- 3.9 - '3.9'
- '3.10'
- '3.11'
exclude: exclude:
- python: '3.8'
ansible: stable-2.14
- python: '3.8'
ansible: stable-2.15
- python: '3.8' - python: '3.8'
ansible: stable-2.16 ansible: stable-2.16
- python: '3.8'
ansible: stable-2.17
- python: '3.8' - python: '3.8'
ansible: devel 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: steps:
- name: >- - name: >-
Perform unit testing against Perform unit testing against
Ansible version ${{ matrix.ansible }} Ansible version ${{ matrix.ansible }} and
python version ${{ matrix.python }}
uses: ansible-community/ansible-test-gh-action@release/v1 uses: ansible-community/ansible-test-gh-action@release/v1
with: with:
ansible-core-version: ${{ matrix.ansible }} ansible-core-version: ${{ matrix.ansible }}

View file

@ -1,6 +1,6 @@
--- ---
name: Roles CI name: Roles CI
on: on: # yamllint disable-line rule:truthy
push: push:
paths: paths:
- 'roles/**' - 'roles/**'
@ -15,7 +15,7 @@ on:
jobs: jobs:
molecule: molecule:
name: "Molecule (Python: ${{ matrix.python }}, Ansible: ${{ matrix.ansible }}, MySQL: ${{ matrix.mysql }})" name: "Molecule (Python: ${{ matrix.python }}, Ansible: ${{ matrix.ansible }}, MySQL: ${{ matrix.mysql }})"
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
env: env:
PY_COLORS: 1 PY_COLORS: 1
ANSIBLE_FORCE_COLOR: 1 ANSIBLE_FORCE_COLOR: 1
@ -24,26 +24,36 @@ jobs:
mysql: mysql:
- 2.0.12 - 2.0.12
ansible: ansible:
- stable-2.13
- stable-2.14
- stable-2.15 - stable-2.15
- stable-2.16
- stable-2.17
- devel - devel
python: python:
- 3.8 - '3.8'
- 3.9 - '3.9'
- '3.10'
exclude: exclude:
- python: 3.8 - python: 3.8
ansible: stable-2.17
- python: 3.9
ansible: stable-2.17
- python: 3.8
ansible: devel
- python: 3.9
ansible: devel ansible: devel
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
path: ansible_collections/community/mysql path: ansible_collections/community/mysql
- name: Set up Python ${{ matrix.python }} - name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@v2 uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python }} python-version: ${{ matrix.python }}

View file

@ -1,67 +0,0 @@
---
name: Build Docker Image for ansible-test
on:
workflow_call:
inputs:
registry:
required: true
type: string
image_name:
required: true
type: string
context:
required: true
type: string
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
# Requirement to use 'context' in docker/build-push-action@v3
- name: Checkout repository
uses: actions/checkout@v3
# https://github.com/docker/login-action
- name: Log into registry ${{ inputs.registry }}
uses: docker/login-action@v2
with:
registry: ${{ inputs.registry }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# https://github.com/docker/metadata-action
- name: Extract Docker metadata (tags, labels)
id: meta
uses: docker/metadata-action@v4
with:
images:
"${{ inputs.registry }}\
/${{ github.repository }}\
/${{ inputs.image_name }}"
tags: latest
# Setting up Docker Buildx with docker-container driver is required
# at the moment to be able to use a subdirectory with Git context
#
# https://github.com/docker/setup-buildx-action
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# https://github.com/docker/build-push-action
- name: Build and push Docker image with Buildx
id: build-and-push
uses: docker/build-push-action@v3
with:
context: ${{ inputs.context }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View file

@ -1,21 +0,0 @@
---
name: Docker Image CI mariadb-py310-mysqlclient211
on:
push:
paths:
- 'test-containers/mariadb-py310-mysqlclient211/**'
- '.github/workflows/docker-image-mariadb-py310-mysqlclient211.yml'
- '.github/workflows/build-docker-image.yml'
branches-ignore:
- stable-*
jobs:
call-workflow-passing-data:
uses: ./.github/workflows/build-docker-image.yml
secrets: inherit
with:
registry: ghcr.io
image_name: test-container-mariadb-py310-mysqlclient211
context: test-containers/mariadb-py310-mysqlclient211

View file

@ -1,21 +0,0 @@
---
name: Docker Image CI mariadb-py310-pymysql102
on:
push:
paths:
- 'test-containers/mariadb-py310-pymysql102/**'
- '.github/workflows/docker-image-mariadb-py310-pymysql102.yml'
- '.github/workflows/build-docker-image.yml'
branches-ignore:
- stable-*
jobs:
call-workflow-passing-data:
uses: ./.github/workflows/build-docker-image.yml
secrets: inherit
with:
registry: ghcr.io
image_name: test-container-mariadb-py310-pymysql102
context: test-containers/mariadb-py310-pymysql102

View file

@ -1,21 +0,0 @@
---
name: Docker Image CI mariadb-py38-mysqlclient201
on:
push:
paths:
- 'test-containers/mariadb-py38-mysqlclient201/**'
- '.github/workflows/docker-image-mariadb-py38-mysqlclient201.yml'
- '.github/workflows/build-docker-image.yml'
branches-ignore:
- stable-*
jobs:
call-workflow-passing-data:
uses: ./.github/workflows/build-docker-image.yml
secrets: inherit
with:
registry: ghcr.io
image_name: test-container-mariadb-py38-mysqlclient201
context: test-containers/mariadb-py38-mysqlclient201

View file

@ -1,21 +0,0 @@
---
name: Docker Image CI mariadb-py38-pymysql093
on:
push:
paths:
- 'test-containers/mariadb-py38-pymysql093/**'
- '.github/workflows/docker-image-mariadb-py38-pymysql093.yml'
- '.github/workflows/build-docker-image.yml'
branches-ignore:
- stable-*
jobs:
call-workflow-passing-data:
uses: ./.github/workflows/build-docker-image.yml
secrets: inherit
with:
registry: ghcr.io
image_name: test-container-mariadb-py38-pymysql093
context: test-containers/mariadb-py38-pymysql093

View file

@ -1,21 +0,0 @@
---
name: Docker Image CI mariadb-py39-mysqlclient203
on:
push:
paths:
- 'test-containers/mariadb-py39-mysqlclient203/**'
- '.github/workflows/docker-image-mariadb-py39-mysqlclient203.yml'
- '.github/workflows/build-docker-image.yml'
branches-ignore:
- stable-*
jobs:
call-workflow-passing-data:
uses: ./.github/workflows/build-docker-image.yml
secrets: inherit
with:
registry: ghcr.io
image_name: test-container-mariadb-py39-mysqlclient203
context: test-containers/mariadb-py39-mysqlclient203

View file

@ -1,21 +0,0 @@
---
name: Docker Image CI mariadb-py39-pymysql093
on:
push:
paths:
- 'test-containers/mariadb-py39-pymysql093/**'
- '.github/workflows/docker-image-mariadb-py39-pymysql093.yml'
- '.github/workflows/build-docker-image.yml'
branches-ignore:
- stable-*
jobs:
call-workflow-passing-data:
uses: ./.github/workflows/build-docker-image.yml
secrets: inherit
with:
registry: ghcr.io
image_name: test-container-mariadb-py39-pymysql093
context: test-containers/mariadb-py39-pymysql093

View file

@ -1,21 +0,0 @@
---
name: Docker Image CI my57-py38-mysqlclient201
on:
push:
paths:
- 'test-containers/my57-py38-mysqlclient201/**'
- '.github/workflows/docker-image-my57-py38-mysqlclient201.yml'
- '.github/workflows/build-docker-image.yml'
branches-ignore:
- stable-*
jobs:
call-workflow-passing-data:
uses: ./.github/workflows/build-docker-image.yml
secrets: inherit
with:
registry: ghcr.io
image_name: test-container-my57-py38-mysqlclient201
context: test-containers/my57-py38-mysqlclient201

View file

@ -1,21 +0,0 @@
---
name: Docker Image CI my57-py38-pymysql0711
on:
push:
paths:
- 'test-containers/my57-py38-pymysql0711/**'
- '.github/workflows/docker-image-my57-py38-pymysql0711.yml'
- '.github/workflows/build-docker-image.yml'
branches-ignore:
- stable-*
jobs:
call-workflow-passing-data:
uses: ./.github/workflows/build-docker-image.yml
secrets: inherit
with:
registry: ghcr.io
image_name: test-container-my57-py38-pymysql0711
context: test-containers/my57-py38-pymysql0711

View file

@ -1,21 +0,0 @@
---
name: Docker Image CI my57-py38-pymysql093
on:
push:
paths:
- 'test-containers/my57-py38-pymysql093/**'
- '.github/workflows/docker-image-my57-py38-pymysql093.yml'
- '.github/workflows/build-docker-image.yml'
branches-ignore:
- stable-*
jobs:
call-workflow-passing-data:
uses: ./.github/workflows/build-docker-image.yml
secrets: inherit
with:
registry: ghcr.io
image_name: test-container-my57-py38-pymysql093
context: test-containers/my57-py38-pymysql093

View file

@ -1,21 +0,0 @@
---
name: Docker Image CI mysql-py310-mysqlclient211
on:
push:
paths:
- 'test-containers/mysql-py310-mysqlclient211/**'
- '.github/workflows/docker-image-mysql-py310-mysqlclient211.yml'
- '.github/workflows/build-docker-image.yml'
branches-ignore:
- stable-*
jobs:
call-workflow-passing-data:
uses: ./.github/workflows/build-docker-image.yml
secrets: inherit
with:
registry: ghcr.io
image_name: test-container-mysql-py310-mysqlclient211
context: test-containers/mysql-py310-mysqlclient211

View file

@ -1,21 +0,0 @@
---
name: Docker Image CI mysql-py310-pymysql102
on:
push:
paths:
- 'test-containers/mysql-py310-pymysql102/**'
- '.github/workflows/docker-image-mysql-py310-pymysql102.yml'
- '.github/workflows/build-docker-image.yml'
branches-ignore:
- stable-*
jobs:
call-workflow-passing-data:
uses: ./.github/workflows/build-docker-image.yml
secrets: inherit
with:
registry: ghcr.io
image_name: test-container-mysql-py310-pymysql102
context: test-containers/mysql-py310-pymysql102

View file

@ -1,21 +0,0 @@
---
name: Docker Image CI mysql-py38-mysqlclient201
on:
push:
paths:
- 'test-containers/mysql-py38-mysqlclient201/**'
- '.github/workflows/docker-image-mysql-py38-mysqlclient201.yml'
- '.github/workflows/build-docker-image.yml'
branches-ignore:
- stable-*
jobs:
call-workflow-passing-data:
uses: ./.github/workflows/build-docker-image.yml
secrets: inherit
with:
registry: ghcr.io
image_name: test-container-mysql-py38-mysqlclient201
context: test-containers/mysql-py38-mysqlclient201

View file

@ -1,21 +0,0 @@
---
name: Docker Image CI mysql-py38-pymysql093
on:
push:
paths:
- 'test-containers/mysql-py38-pymysql093/**'
- '.github/workflows/docker-image-mysql-py38-pymysql093.yml'
- '.github/workflows/build-docker-image.yml'
branches-ignore:
- stable-*
jobs:
call-workflow-passing-data:
uses: ./.github/workflows/build-docker-image.yml
secrets: inherit
with:
registry: ghcr.io
image_name: test-container-mysql-py38-pymysql093
context: test-containers/mysql-py38-pymysql093

View file

@ -1,21 +0,0 @@
---
name: Docker Image CI mysql-py39-mysqlclient203
on:
push:
paths:
- 'test-containers/mysql-py39-mysqlclient203/**'
- '.github/workflows/docker-image-mysql-py39-mysqlclient203.yml'
- '.github/workflows/build-docker-image.yml'
branches-ignore:
- stable-*
jobs:
call-workflow-passing-data:
uses: ./.github/workflows/build-docker-image.yml
secrets: inherit
with:
registry: ghcr.io
image_name: test-container-mysql-py39-mysqlclient203
context: test-containers/mysql-py39-mysqlclient203

View file

@ -1,21 +0,0 @@
---
name: Docker Image CI mysql-py39-pymysql093
on:
push:
paths:
- 'test-containers/mysql-py39-pymysql093/*'
- '.github/workflows/docker-image-mysql-py39-pymysql093.yml'
- '.github/workflows/build-docker-image.yml'
branches-ignore:
- stable-*
jobs:
call-workflow-passing-data:
uses: ./.github/workflows/build-docker-image.yml
secrets: inherit
with:
registry: ghcr.io
image_name: test-container-mysql-py39-pymysql093
context: test-containers/mysql-py39-pymysql093

View file

@ -1,11 +1,181 @@
======================================== ====================================================
Community MySQL Collection Release Notes Community MySQL and MariaDB Collection Release Notes
======================================== ====================================================
.. contents:: Topics .. contents:: Topics
This changelog describes changes after version 2.0.0. 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 v3.8.0
====== ======

View file

@ -1,5 +1,80 @@
# Contributing # Contributing to this project
Refer to the [Ansible Contributing guidelines](https://docs.ansible.com/ansible/devel/community/index.html) 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.
Refer to the [review checklist](https://docs.ansible.com/ansible/devel/community/collection_contributors/collection_reviewing.html) when triaging issues or reviewing PRs. 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/<module_name>/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
```

View file

@ -17,9 +17,11 @@ amitk79
amree amree
Andersson007 Andersson007
andrewhowdencom andrewhowdencom
aneustroev
ansibot ansibot
anthonyxpalermo anthonyxpalermo
antonioribeiro antonioribeiro
Aohzan
apollo13 apollo13
aquach aquach
arcmop arcmop
@ -33,6 +35,8 @@ baldpale
banyek banyek
BarbzYHOOL BarbzYHOOL
Berbe Berbe
betanummeric
bigo8525
bizmate bizmate
bjne bjne
bmalynovytch bmalynovytch
@ -46,6 +50,7 @@ candeira
caphrim007 caphrim007
cdalbergue cdalbergue
checkphi checkphi
chriscroome
chrismeyersfsu chrismeyersfsu
ChristopherGAndrews ChristopherGAndrews
cmodijk cmodijk
@ -56,13 +61,14 @@ CormacBracken
cosmix cosmix
cptMikky cptMikky
crashes crashes
d-lee
d-rupp
dagwieers dagwieers
damianmoore damianmoore
Davidffry Davidffry
denisemauldin denisemauldin
dennisurtubia
diclophis diclophis
d-lee
d-rupp
dmp1ce dmp1ce
dnelson dnelson
dramaley dramaley
@ -72,9 +78,11 @@ DSpeichert
dungdm93 dungdm93
dwagelaar dwagelaar
dylanjbarth dylanjbarth
einarc
E-M E-M
einarc
elpavel
eowin eowin
eRadical
Ernest0x Ernest0x
esamattis esamattis
Everspace Everspace
@ -82,24 +90,30 @@ F21
faitno faitno
felixfontein felixfontein
flatrocks flatrocks
FlorianPerrot
fourjay fourjay
fraff fraff
francescsanjuanmrf
g00fy- g00fy-
geerlingguy geerlingguy
georgeOsdDev georgeOsdDev
ghjm ghjm
ghost ghost
GhostLyrics
giacmir giacmir
giorgio-v giorgio-v
gkoller gkoller
gotmax23
gottwald gottwald
gstorme gstorme
gundalow gundalow
hansbaer hansbaer
hchargois hchargois
hluaces hluaces
hubiongithub
hwali hwali
hyperfocus1338 hyperfocus1338
IBims1NicerTobi
igormukhingmailcom igormukhingmailcom
imjoseangel imjoseangel
infigoKriti infigoKriti
@ -164,8 +178,8 @@ markdorison
markotitel markotitel
marktheunissen marktheunissen
markuman markuman
mattclay
matt-horwood-mayden matt-horwood-mayden
mattclay
mavimo mavimo
maxamillion maxamillion
maxbube maxbube
@ -184,11 +198,15 @@ mkrizek
mmoya mmoya
mohag mohag
mohsenSy mohsenSy
moledzki
mpdehaan mpdehaan
MRMegaNova
MRwangyd MRwangyd
mstinsky
mverwijs mverwijs
mvgrimes mvgrimes
mysqlbox mysqlbox
n-cc
netmonk netmonk
nhojpatrick nhojpatrick
nicolas-g nicolas-g
@ -202,7 +220,9 @@ organman91
p53 p53
pakal pakal
paulbadcock paulbadcock
paulcampbell-ayroc
pennycoders pennycoders
perlun
petoju petoju
petracvv petracvv
pgrenaud pgrenaud
@ -223,12 +243,14 @@ richlv
riupie riupie
rndmh3ro rndmh3ro
robertdebock robertdebock
robertsilen
robpblake robpblake
rokka-n rokka-n
Roxyrob Roxyrob
roysmith roysmith
rsicart rsicart
rthouvenin rthouvenin
rujschafer
ruudk ruudk
samccann samccann
samdoran samdoran
@ -242,6 +264,7 @@ shrikeh
sivel sivel
skalfyfan skalfyfan
skoriy88 skoriy88
SoledaD208
sperantus sperantus
spoyd spoyd
steverweber steverweber
@ -262,19 +285,22 @@ time-palominodb
timorunge timorunge
Tomasthanes Tomasthanes
tomdymond tomdymond
tompal3
Tronde Tronde
tuhoanganh tuhoanganh
tvlooy tvlooy
tyll tyll
UncertaintyP UncertaintyP
unnecessary-username unnecessary-username
v-zhuravlev
vamshi8 vamshi8
vanne vanne
vdboor vdboor
vmahadev vmahadev
v-zhuravlev webknjaz
webmat webmat
wedi wedi
wfelipew
whysthatso whysthatso
willthames willthames
windowsansiblernew windowsansiblernew

View file

@ -1,6 +1,3 @@
betanummeric betanummeric
bmalynovytch
Jorge-Rodriguez
rsicart
laurent-indermuehle laurent-indermuehle
Andersson007 (andersson007_ in #ansible-community IRC/Matrix) Andersson007

View file

@ -8,33 +8,26 @@ endif
# This match what GitHub Action will do. Disabled by default. # This match what GitHub Action will do. Disabled by default.
ifdef continue_on_errors ifdef continue_on_errors
_continue_on_errors = --retry-on-error --continue-on-error _continue_on_errors = --continue-on-error
endif endif
# Set command variables based on database engine
db_ver_tuple := $(subst ., , $(db_engine_version)) # Required for MariaDB 11+ which no longer includes mysql named compatible
db_engine_version_flat := $(word 1, $(db_ver_tuple))$(word 2, $(db_ver_tuple)) # executable symlinks
ifeq ($(db_engine_name),mysql)
con_ver_tuple := $(subst ., , $(connector_version)) _command = mysqld
connector_version_flat := $(word 1, $(con_ver_tuple))$(word 2, $(con_ver_tuple))$(word 3, $(con_ver_tuple)) _health_cmd = mysqladmin
py_ver_tuple := $(subst ., , $(python))
python_version_flat := $(word 1, $(py_ver_tuple))$(word 2, $(py_ver_tuple))
ifeq ($(db_engine_version_flat), 57)
db_client := my57
else else
db_client := $(db_engine_name) _command = mariadbd
_health_cmd = mariadb-admin
endif endif
.PHONY: test-integration .PHONY: test-integration
test-integration: test-integration:
@echo -n $(db_engine_name) > tests/integration/db_engine_name @echo -n $(db_engine_name) > tests/integration/db_engine_name
@echo -n $(db_engine_version) > tests/integration/db_engine_version @echo -n $(db_engine_version) > tests/integration/db_engine_version
@echo -n $(connector_name) > tests/integration/connector_name @echo -n $(connector_name) > tests/integration/connector_name
@echo -n $(connector_version) > tests/integration/connector_version @echo -n $(connector_version) > tests/integration/connector_version
@echo -n $(python) > tests/integration/python
@echo -n $(ansible) > tests/integration/ansible @echo -n $(ansible) > tests/integration/ansible
# Create podman network for systems missing it. Error can be ignored # Create podman network for systems missing it. Error can be ignored
@ -47,9 +40,9 @@ test-integration:
--env MYSQL_ROOT_PASSWORD=msandbox \ --env MYSQL_ROOT_PASSWORD=msandbox \
--network podman \ --network podman \
--publish 3307:3306 \ --publish 3307:3306 \
--health-cmd 'mysqladmin ping -P 3306 -pmsandbox | grep alive || exit 1' \ --health-cmd '$(_health_cmd) ping -P 3306 -pmsandbox | grep alive || exit 1' \
docker.io/library/$(db_engine_name):$(db_engine_version) \ docker.io/library/$(db_engine_name):$(db_engine_version) \
mysqld $(_command)
podman run \ podman run \
--detach \ --detach \
--replace \ --replace \
@ -58,9 +51,9 @@ test-integration:
--env MYSQL_ROOT_PASSWORD=msandbox \ --env MYSQL_ROOT_PASSWORD=msandbox \
--network podman \ --network podman \
--publish 3308:3306 \ --publish 3308:3306 \
--health-cmd 'mysqladmin ping -P 3306 -pmsandbox | grep alive || exit 1' \ --health-cmd '$(_health_cmd) ping -P 3306 -pmsandbox | grep alive || exit 1' \
docker.io/library/$(db_engine_name):$(db_engine_version) \ docker.io/library/$(db_engine_name):$(db_engine_version) \
mysqld $(_command)
podman run \ podman run \
--detach \ --detach \
--replace \ --replace \
@ -69,13 +62,26 @@ test-integration:
--env MYSQL_ROOT_PASSWORD=msandbox \ --env MYSQL_ROOT_PASSWORD=msandbox \
--network podman \ --network podman \
--publish 3309:3306 \ --publish 3309:3306 \
--health-cmd 'mysqladmin ping -P 3306 -pmsandbox | grep alive || exit 1' \ --health-cmd '$(_health_cmd) ping -P 3306 -pmsandbox | grep alive || exit 1' \
docker.io/library/$(db_engine_name):$(db_engine_version) \ docker.io/library/$(db_engine_name):$(db_engine_version) \
mysqld $(_command)
# Setup replication and restart containers # Setup replication and restart containers using the same subshell to keep variables alive
podman exec primary bash -c 'echo -e [mysqld]\\nserver-id=1\\nlog-bin=/var/lib/mysql/primary-bin > /etc/mysql/conf.d/replication.cnf' db_ver=$(db_engine_version); \
podman exec replica1 bash -c 'echo -e [mysqld]\\nserver-id=2\\nlog-bin=/var/lib/mysql/replica1-bin > /etc/mysql/conf.d/replication.cnf' maj="$${db_ver%.*.*}"; \
podman exec replica2 bash -c 'echo -e [mysqld]\\nserver-id=3\\nlog-bin=/var/lib/mysql/replica2-bin > /etc/mysql/conf.d/replication.cnf' 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 # Don't restart a container unless it is healthy
while ! podman healthcheck run primary && [[ "$$SECONDS" -lt 120 ]]; do sleep 1; done while ! podman healthcheck run primary && [[ "$$SECONDS" -lt 120 ]]; do sleep 1; done
podman restart -t 30 primary podman restart -t 30 primary
@ -94,9 +100,8 @@ test-integration:
https://github.com/ansible/ansible/archive/$(ansible).tar.gz; \ https://github.com/ansible/ansible/archive/$(ansible).tar.gz; \
set -x; \ set -x; \
ansible-test integration $(target) -v --color --coverage --diff \ ansible-test integration $(target) -v --color --coverage --diff \
--docker ghcr.io/ansible-collections/community.mysql/test-container\ --docker ubuntu2204 \
-$(db_client)-py$(python_version_flat)-$(connector_name)$(connector_version_flat):latest \ --docker-network podman $(_continue_on_errors) $(_keep_containers_alive); \
--docker-network podman $(_continue_on_errors) $(_keep_containers_alive) --python $(python); \
set +x set +x
# End of venv # End of venv
@ -104,7 +109,6 @@ test-integration:
rm tests/integration/db_engine_version rm tests/integration/db_engine_version
rm tests/integration/connector_name rm tests/integration/connector_name
rm tests/integration/connector_version rm tests/integration/connector_version
rm tests/integration/python
rm tests/integration/ansible rm tests/integration/ansible
ifndef keep_containers_alive ifndef keep_containers_alive
podman stop --time 0 --ignore primary replica1 replica2 podman stop --time 0 --ignore primary replica1 replica2

View file

@ -1,5 +1,5 @@
# MySQL collection for Ansible # 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") [![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) [![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) [![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. This collection is a part of the Ansible package.
@ -15,6 +15,22 @@ We follow the [Ansible Code of Conduct](https://docs.ansible.com/ansible/latest/
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. 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 ## 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. 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.
@ -38,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). 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 on Matrix in the `#mysql:ansible.com` [room](https://matrix.to/#/#mysql:ansible.com), the `#users:ansible.com` [room](https://matrix.to/#/#users:ansible.com) (general use questions and support), `#ansible-community:ansible.com` [room](https://matrix.to/#/#community:ansible.com) (community and collection development questions), and other Matrix rooms or corresponding bridged Libera.Chat channels. See the [Ansible Communication Guide](https://docs.ansible.com/ansible/devel/community/communication.html) for details.
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 ## 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. 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 ## Included content
@ -68,12 +76,12 @@ Every voice is important and every idea is valuable. If you have something on yo
## Releases Support Timeline ## Releases Support Timeline
It has been [decided](https://github.com/ansible-collections/community.mysql/discussions/537) to maintain each major release (1.x.y, 2.x.y, ...) for two years after the next major version is released. 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: Here is the table for the support timeline:
- 1.x.y: released 2020-08-17, EOL - 1.x.y: released 2020-08-17, EOL
- 2.x.y: released 2021-04-15, supported until 2023-12-01 - 2.x.y: released 2021-04-15, EOL
- 3.x.y: released 2021-12-01, current - 3.x.y: released 2021-12-01, current
- 4.x.y: To be released - 4.x.y: To be released
@ -82,40 +90,46 @@ Here is the table for the support timeline:
### ansible-core ### ansible-core
- stable-2.14
- stable-2.15
- stable-2.16 - stable-2.16
- stable-2.17
- stable-2.18
- current development version - 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 ### Databases
For MariaDB, only Long Term releases are tested. 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
- mysql 8.0.31
- mariadb:10.3.34 (only collection version <= 3.5.1)
- mariadb:10.4.24 (only collection version >= 3.5.2)
- mariadb:10.5.18 (only collection version >= 3.5.2)
- mariadb:10.6.11 (only collection version >= 3.5.2)
- mariadb:10.11.?? (waiting for release)
- 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 ### Database connectors
- pymysql 0.7.11 (Only tested with MySQL 5.7) - pymysql 0.7.11 (collection version < 3.10 and MySQL 5.7)
- pymysql 0.9.3 - pymysql 0.9.3
- pymysql 1.0.2 (only collection version >= 3.6.1) - pymysql 0.10.1 (for RHEL8 context)
- mysqlclient 2.0.1 - pymysql 1.0.2 (collection version >= 3.6.1)
- mysqlclient 2.0.3 (only collection version >= 3.5.2) - pymysql 1.1.1 (collection version >= 3.10.0)
- mysqlclient 2.1.1 (only collection version >= 3.5.2)
## External requirements ## 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) 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.
- [mysqlclient](https://github.com/PyMySQL/mysqlclient)
- Support for other Python MySQL connectors may be added in a future release.
## Using this collection ## Using this collection

View file

@ -19,19 +19,16 @@ For now, the makefile only supports Podman.
### Requirements ### Requirements
- python >= 3.8 and <= 3.10 - python >= 3.8
- make - make
- podman - 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 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 - Minimum 2GB of RAM
### Custom ansible-test containers ### ansible-test environment
Our integrations tests use custom containers for ansible-test. Those images have their definition file stored in the directory [test-containers](test-containers/). We build and publish the images on ghcr.io under the ansible-collection namespace: E.G.: 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.
`ghcr.io/ansible-collections/community.mysql/test-container-mariadb106-py310-mysqlclient211:latest`.
Availables images are listed [here](https://github.com/orgs/ansible-collections/packages).
### Makefile options ### Makefile options
@ -44,16 +41,15 @@ The Makefile accept the following options
- "3.8" - "3.8"
- "3.9" - "3.9"
- "3.10" - "3.10"
- Description: If `Python -V` shows an unsupported version, use this option and choose one of the version available on your system. Use `ls /usr/bin/python3*|grep -v config` to list them. - "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` - `ansible`
- Mandatory: true - Mandatory: true
- Choices: - Choices:
- "stable-2.12"
- "stable-2.13"
- "stable-2.14"
- "stable-2.15" - "stable-2.15"
- "stable-2.16" - "stable-2.16"
- "stable-2.17"
- "devel" - "devel"
- Description: Version of ansible to install in a venv to run ansible-test - Description: Version of ansible to install in a venv to run ansible-test
@ -67,39 +63,28 @@ The Makefile accept the following options
- `db_engine_version` - `db_engine_version`
- Mandatory: true - Mandatory: true
- Choices: - Choices:
- "5.7.40" <- mysql - "8.0.38" <- mysql
- "8.0.31" <- 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.4.24" <- mariadb - "10.11.8" <- mariadb
- "10.5.18" <- mariadb - "11.4.5" <- mariadb
- "10.6.11" <- 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. - 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` - `connector_name`
- Mandatory: true - Mandatory: true
- Choices: - Choices:
- "pymysql - "pymysql"
- "mysqlclient" - "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'`. - 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` - `connector_version`
- Mandatory: true - Mandatory: true
- Choices: - Choices:
- "0.7.11" <- pymysql (Only for MySQL 5.7)
- "0.9.3" <- pymysql - "0.9.3" <- pymysql
- "0.10.1" <- pymysql
- "1.0.2" <- pymysql - "1.0.2" <- pymysql
- "2.0.1" <- mysqlclient - "1.1.1" <- pymysql
- "2.0.3" <- mysqlclient
- "2.1.1" <- mysqlclient
- Description: The version of the python package of the connector to use. This value is used to filter tests meant for other connectors. - Description: The version of the python package of the connector to use. This value is used to filter tests meant for other connectors.
- `python`
- Mandatory: true
- Choices:
- "3.8"
- "3.9"
- "3.10"
- Description: The python version to use in the controller (ansible-test container).
- `target` - `target`
- Mandatory: false - Mandatory: false
- Choices: - Choices:
@ -119,30 +104,30 @@ tests will overwrite the 3 databases containers so no need to kill them in advan
- `continue_on_errors` - `continue_on_errors`
- Mandatory: false - Mandatory: false
- Description: Tells ansible-test to retry on errors and also 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` - 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: #### Makefile usage examples:
```sh ```sh
# Run all targets # Run all targets
make ansible="stable-2.12" db_engine_name="mysql" db_engine_version="5.7.40" python="3.8" connector_name="pymysql" connector_version="0.7.11" 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 # A single target
make ansible="stable-2.14" db_engine_name="mysql" db_engine_version="5.7.40" python="3.8" connector_name="pymysql" connector_version="0.7.11" target="test_mysql_info" 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 # Keep databases and ansible tests containers alives
# A single target and continue on errors # A single target and continue on errors
make ansible="stable-2.14" db_engine_name="mysql" db_engine_version="8.0.31" python="3.9" connector_name="mysqlclient" connector_version="2.0.3" target="test_mysql_query" keep_containers_alive=1 continue_on_errors=1 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: # If your system has an usupported version of Python:
make local_python_version="3.8" ansible="stable-2.14" db_engine_name="mariadb" db_engine_version="10.6.11" python="3.9" connector_name="pymysql" connector_version="0.9.3" 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 ### Run all tests
GitHub Action offer a test matrix that run every combination of Python, MySQL, MariaDB and Connector against each other. To reproduce this, this repo provides a script called *run_all_tests.py*. 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: Examples:
@ -151,18 +136,8 @@ python run_all_tests.py
``` ```
### Add a new Python, Connector or Database version ### Add a new Connector or Database version
You can look into `[.github/workflows/ansible-test-plugins.yml](https://github.com/ansible-collections/community.mysql/tree/main/.github/workflows)` to see how those containers are built using [build-docker-image.yml](https://github.com/ansible-collections/community.mysql/blob/main/.github/workflows/build-docker-image.yml) and all [docker-image-xxx.yml](https://github.com/ansible-collections/community.mysql/blob/main/.github/workflows/docker-image-mariadb103-py38-mysqlclient201.yml) files. 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)
1. Add a workflow in [.github/workflows/](.github/workflows) Be careful to not add too much tests. The matrix creates an exponential number of virtual machines!
1. Add a new folder in [test-containers](test-containers) containing a new Dockerfile. Your container must contains 3 things:
- Python
- A connector: The python package to connect to the database (pymysql, mysqlclient, ...)
- A mysql client to prepare databases before our tests starts. This client must provide both `mysql` and `mysqldump` commands.
1. Add your version in the matrix of *.github/workflows/ansible-test-plugins.yml*. You can use [run_all_tests.py](run_all_tests.py) to help you see what the matrix will be. Simply comment out the line `os.system(make_cmd)` before runing the script. You can also add `print(len(matrix))` to display how many tests there will be on GitHub Action.
1. Ask the lead maintainer to mark your new image(s) as `public` under [https://github.com/orgs/ansible-collections/packages](https://github.com/orgs/ansible-collections/packages)
After pushing your commit to the remote, the container will be built and published on ghcr.io. Have a look in the "Action" tab to see if it worked. In case of error `failed to copy: io: read/write on closed pipe` re-run the workflow, this append unfortunately a lot.
To see the docker image produced, go to the package page in the ansible-collection namespace [https://github.com/orgs/ansible-collections/packages](https://github.com/orgs/ansible-collections/packages). This page indicate a "Published x days ago" that is updated infrequently. To see the last time the container has been updated you must click on its title and look in the right hands side bellow the title "Last published".

View file

@ -97,6 +97,176 @@ releases:
- 307-mysql_user_add_if_exists_to_drop.yml - 307-mysql_user_add_if_exists_to_drop.yml
- 329-mysql_role-remove-redudant-connection-closing.yml - 329-mysql_role-remove-redudant-connection-closing.yml
release_date: '2022-04-26' 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: 3.2.0:
changes: changes:
bugfixes: bugfixes:
@ -380,3 +550,29 @@ releases:
- drop_ansible_core_2_12_and_2_13.yml - drop_ansible_core_2_12_and_2_13.yml
- lie_mysql_info_users_info.yml - lie_mysql_info_users_info.yml
release_date: '2023-10-25' 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'

View file

@ -25,5 +25,5 @@ sections:
- Bugfixes - Bugfixes
- - known_issues - - known_issues
- Known Issues - Known Issues
title: Community MySQL Collection title: Community MySQL and MariaDB Collection
trivial_section_name: trivial trivial_section_name: trivial

View file

@ -1,11 +1,11 @@
--- ---
namespace: community namespace: community
name: mysql name: mysql
version: 3.8.0 version: 3.13.0
readme: README.md readme: README.md
authors: authors:
- Ansible community - Ansible community
description: MySQL collection for Ansible description: MySQL and MariaDB collection for Ansible
license_file: COPYING license_file: COPYING
tags: tags:
- database - database

View file

@ -71,22 +71,19 @@ options:
- Whether to validate the server host name when an SSL connection is required. Corresponds to MySQL CLIs C(--ssl) switch. - 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. - Setting this to C(false) disables hostname verification. Use with caution.
- Requires pymysql >= 0.7.11. - Requires pymysql >= 0.7.11.
- This option has no effect on MySQLdb.
type: bool type: bool
version_added: '1.1.0' version_added: '1.1.0'
requirements: requirements:
- mysqlclient (Python 3.5+) or - PyMySQL (Python 2.7 and Python 3.x)
- PyMySQL (Python 2.7 and Python 3.x) or
- MySQLdb (Python 2.x)
notes: 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 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 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). for newer versions of Fedora; see M(ansible.builtin.dnf).
- Be sure you have mysqlclient, PyMySQL, or MySQLdb library installed on the target machine - 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 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 the Python 3 version of PyMySQL. If ansible discovers and uses Python 2, you need to install the Python 2
version of either PyMySQL or MySQL-python. version of PyMySQL.
- If you have trouble, it may help to force Ansible to use the Python interpreter you need by specifying - 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). U(https://docs.ansible.com/ansible/latest/reference_appendices/interpreter_discovery.html).
@ -99,9 +96,6 @@ notes:
and later uses the unix_socket authentication plugin by default that 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) 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``. 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 - "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 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: ''))." will cause the connection to fail, to prevent this set it to an empty string, (for example C(config_file: ''))."

View file

@ -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)

View file

@ -23,3 +23,54 @@ def server_supports_alter_user(cursor):
version = get_server_version(cursor) version = get_server_version(cursor)
return LooseVersion(version) >= LooseVersion("10.2") 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

View file

@ -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()

View file

@ -8,6 +8,9 @@ __metaclass__ = type
from ansible_collections.community.mysql.plugins.module_utils.version import LooseVersion from ansible_collections.community.mysql.plugins.module_utils.version import LooseVersion
from ansible_collections.community.mysql.plugins.module_utils.mysql import get_server_version from ansible_collections.community.mysql.plugins.module_utils.mysql import get_server_version
import re
import shlex
def use_old_user_mgmt(cursor): def use_old_user_mgmt(cursor):
version = get_server_version(cursor) version = get_server_version(cursor)
@ -24,3 +27,52 @@ def server_supports_alter_user(cursor):
version = get_server_version(cursor) version = get_server_version(cursor)
return LooseVersion(version) >= LooseVersion("5.6") 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

View file

@ -154,6 +154,13 @@ def mysql_connect(module, login_user=None, login_password=None, config_file='',
db_connection = mysql_driver.connect(autocommit=autocommit, **config) db_connection = mysql_driver.connect(autocommit=autocommit, **config)
else: else:
# In case of MySQLdb driver # 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): 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' # for MySQLdb < 2.1.0, use 'db' instead of 'database' and 'passwd' instead of 'password'
if 'database' in config: if 'database' in config:
@ -207,6 +214,13 @@ def get_server_version(cursor):
return version_str 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): def set_session_vars(module, cursor, session_vars):
"""Set session vars.""" """Set session vars."""
for var, value in session_vars.items(): for var, value in session_vars.items():

View file

@ -1,4 +1,6 @@
from __future__ import (absolute_import, division, print_function) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
# This code is part of Ansible, but is an independent component. # This code is part of Ansible, but is an independent component.
@ -10,12 +12,18 @@ __metaclass__ = type
# Simplified BSD License (see 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 string
import json
import re import re
from ansible.module_utils.six import iteritems from ansible.module_utils.six import iteritems
from ansible_collections.community.mysql.plugins.module_utils.mysql import ( from ansible_collections.community.mysql.plugins.module_utils.mysql import (
mysql_driver, 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): def get_mode(cursor):
cursor.execute('SELECT @@GLOBAL.sql_mode') cursor.execute('SELECT @@sql_mode')
result = cursor.fetchone() result = cursor.fetchone()
mode_str = result[0] mode_str = result[0]
if 'ANSI' in mode_str: if 'ANSI' in mode_str:
@ -44,6 +52,25 @@ def user_exists(cursor, user, host, host_all):
return count[0] > 0 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): def sanitize_requires(tls_requires):
sanitized_requires = {} sanitized_requires = {}
if tls_requires: if tls_requires:
@ -79,31 +106,6 @@ def do_not_mogrify_requires(query, params, tls_requires):
return query, params 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): def get_grants(cursor, user, host):
cursor.execute("SHOW GRANTS FOR %s@%s", (user, host)) cursor.execute("SHOW GRANTS FOR %s@%s", (user, host))
grants_line = list(filter(lambda x: "ON *.*" in x[0], cursor.fetchall()))[0] grants_line = list(filter(lambda x: "ON *.*" in x[0], cursor.fetchall()))[0]
@ -112,8 +114,12 @@ def get_grants(cursor, user, host):
return grants.split(", ") return grants.split(", ")
def get_existing_authentication(cursor, user, host): def get_existing_authentication(cursor, user, host=None):
# Return the plugin and auth_string if there is exactly one distinct existing plugin and auth_string. """ 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()") cursor.execute("SELECT VERSION()")
srv_type = cursor.fetchone() srv_type = cursor.fetchone()
# Mysql_info use a DictCursor so we must convert back to a list # Mysql_info use a DictCursor so we must convert back to a list
@ -124,54 +130,91 @@ def get_existing_authentication(cursor, user, host):
if 'mariadb' in srv_type[0].lower(): if 'mariadb' in srv_type[0].lower():
# before MariaDB 10.2.19 and 10.3.11, "password" and "authentication_string" can differ # before MariaDB 10.2.19 and 10.3.11, "password" and "authentication_string" can differ
# when using mysql_native_password # when using mysql_native_password
if host:
cursor.execute("""select plugin, auth from ( cursor.execute("""select plugin, auth from (
select plugin, password as auth from mysql.user where user=%(user)s select plugin, password as auth from mysql.user where user=%(user)s
and host=%(host)s and host=%(host)s
union select plugin, authentication_string as auth from mysql.user where user=%(user)s union select plugin, authentication_string as auth from mysql.user where user=%(user)s
and host=%(host)s) x group by plugin, auth limit 2 and host=%(host)s) x group by plugin, auth
""", {'user': user, 'host': host}) """, {'user': user, 'host': host})
else: 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 cursor.execute("""select plugin, authentication_string as auth
from mysql.user where user=%(user)s and host=%(host)s from mysql.user where user=%(user)s and host=%(host)s
group by plugin, authentication_string limit 2""", {'user': user, 'host': host}) 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() rows = cursor.fetchall()
# Mysql_info use a DictCursor so we must convert back to a list if len(rows) == 0:
# otherwise we get KeyError 0 return []
if isinstance(rows, dict):
rows = list(rows.values())
if isinstance(rows[0], tuple):
return {'plugin': rows[0][0], 'plugin_auth_string': rows[0][1]}
# Mysql_info use a DictCursor so we must convert list(dict)
# to list(tuple) otherwise we get KeyError 0
if isinstance(rows[0], dict): if isinstance(rows[0], dict):
return {'plugin': rows[0].get('plugin'), 'plugin_auth_string': rows[0].get('auth')} rows = [tuple(row.values()) for row in rows]
return None
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, def user_add(cursor, user, host, host_all, password, encrypted,
plugin, plugin_hash_string, plugin_auth_string, new_priv, plugin, plugin_hash_string, plugin_auth_string, salt, new_priv,
tls_requires, check_mode, reuse_existing_password): 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 # we cannot create users without a proper hostname
if host_all: if host_all:
return {'changed': False, 'password_changed': False} return {'changed': False, 'password_changed': False, 'attributes': attributes}
if check_mode: if module.check_mode:
return {'changed': True, 'password_changed': None} return {'changed': True, 'password_changed': None, 'attributes': attributes}
# Determine what user management method server uses # Determine what user management method server uses
impl = get_user_implementation(cursor)
old_user_mgmt = impl.use_old_user_mgmt(cursor) old_user_mgmt = impl.use_old_user_mgmt(cursor)
mogrify = do_not_mogrify_requires if old_user_mgmt else mogrify_requires mogrify = do_not_mogrify_requires if old_user_mgmt else mogrify_requires
# This is for update_password: on_new_username
used_existing_password = False used_existing_password = False
if reuse_existing_password: if reuse_existing_password:
existing_auth = get_existing_authentication(cursor, user, host) existing_auth = get_existing_authentication(cursor, user)
if existing_auth: if existing_auth:
plugin = existing_auth['plugin'] if len(existing_auth) != 1:
plugin_hash_string = existing_auth['auth_string'] 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 password = None
used_existing_password = True used_existing_password = True
plugin = existing_auth[0]['plugin'] # What if plugin differ?
if password and encrypted: if password and encrypted:
if impl.supports_identified_by_password(cursor): if impl.supports_identified_by_password(cursor):
query_with_args = "CREATE USER %s@%s IDENTIFIED BY PASSWORD %s", (user, host, password) query_with_args = "CREATE USER %s@%s IDENTIFIED BY PASSWORD %s", (user, host, password)
@ -190,6 +233,14 @@ def user_add(cursor, user, host, host_all, password, encrypted,
# Mysql and MariaDB differ in naming pam plugin and Syntax to set it # 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 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) 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: else:
query_with_args = "CREATE USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, plugin_auth_string) query_with_args = "CREATE USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, plugin_auth_string)
elif plugin: elif plugin:
@ -200,12 +251,28 @@ def user_add(cursor, user, host, host_all, password, encrypted,
query_with_args_and_tls_requires = query_with_args + (tls_requires,) query_with_args_and_tls_requires = query_with_args + (tls_requires,)
cursor.execute(*mogrify(*query_with_args_and_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: if new_priv is not None:
for db_table, priv in iteritems(new_priv): for db_table, priv in iteritems(new_priv):
privileges_grant(cursor, user, host, db_table, priv, tls_requires) privileges_grant(cursor, user, host, db_table, priv, tls_requires)
if tls_requires is not None: if tls_requires is not None:
privileges_grant(cursor, user, host, "*.*", get_grants(cursor, user, host), tls_requires) privileges_grant(cursor, user, host, "*.*", get_grants(cursor, user, host), tls_requires)
return {'changed': True, 'password_changed': not used_existing_password}
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): def is_hash(password):
@ -217,13 +284,15 @@ def is_hash(password):
def user_mod(cursor, user, host, host_all, password, encrypted, def user_mod(cursor, user, host, host_all, password, encrypted,
plugin, plugin_hash_string, plugin_auth_string, new_priv, plugin, plugin_hash_string, plugin_auth_string, salt, new_priv,
append_privs, subtract_privs, tls_requires, module, role=False, maria_role=False): append_privs, subtract_privs, attributes, tls_requires, module,
password_expire, password_expire_interval, locked=None, role=False, maria_role=False):
changed = False changed = False
msg = "User unchanged" msg = "User unchanged"
grant_option = False grant_option = False
# Determine what user management method server uses # Determine what user management method server uses
impl = get_user_implementation(cursor)
old_user_mgmt = impl.use_old_user_mgmt(cursor) old_user_mgmt = impl.use_old_user_mgmt(cursor)
if host_all and not role: if host_all and not role:
@ -278,8 +347,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
if current_pass_hash != encrypted_password: if current_pass_hash != encrypted_password:
password_changed = True password_changed = True
msg = "Password updated" msg = "Password updated"
if module.check_mode: if not module.check_mode:
return {'changed': True, 'msg': msg, 'password_changed': password_changed}
if old_user_mgmt: if old_user_mgmt:
cursor.execute("SET PASSWORD FOR %s@%s = %s", (user, host, encrypted_password)) cursor.execute("SET PASSWORD FOR %s@%s = %s", (user, host, encrypted_password))
msg = "Password updated (old style)" msg = "Password updated (old style)"
@ -301,6 +369,28 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
raise e raise e
changed = True 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 # Handle plugin authentication
if plugin and not role: if plugin and not role:
cursor.execute("SELECT plugin, authentication_string FROM mysql.user " cursor.execute("SELECT plugin, authentication_string FROM mysql.user "
@ -315,7 +405,11 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
if plugin_hash_string and current_plugin[1] != plugin_hash_string: if plugin_hash_string and current_plugin[1] != plugin_hash_string:
update = True 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, # this case can cause more updates than expected,
# as plugin can hash auth_string in any way it wants # as plugin can hash auth_string in any way it wants
# and there's no way to figure it out for # and there's no way to figure it out for
@ -323,17 +417,27 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
update = True update = True
if update: if update:
query_with_args = None
if plugin_hash_string: if plugin_hash_string:
query_with_args = "ALTER USER %s@%s IDENTIFIED WITH %s AS %s", (user, host, plugin, 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: elif plugin_auth_string:
# Mysql and MariaDB differ in naming pam plugin and syntax to set it # Mysql and MariaDB differ in naming pam plugin and syntax to set it
if plugin == 'pam': if plugin == 'pam':
query_with_args = "ALTER USER %s@%s IDENTIFIED WITH %s USING %s", (user, host, plugin, plugin_auth_string) 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: else:
query_with_args = "ALTER USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, plugin_auth_string) query_with_args = "ALTER USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, plugin_auth_string)
else: else:
query_with_args = "ALTER USER %s@%s IDENTIFIED WITH %s", (user, host, plugin) query_with_args = "ALTER USER %s@%s IDENTIFIED WITH %s", (user, host, plugin)
if not module.check_mode:
cursor.execute(*query_with_args) cursor.execute(*query_with_args)
password_changed = True password_changed = True
changed = True changed = True
@ -352,8 +456,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
if db_table not in new_priv: if db_table not in new_priv:
if user != "root" and "PROXY" not in priv: if user != "root" and "PROXY" not in priv:
msg = "Privileges updated" msg = "Privileges updated"
if module.check_mode: if not module.check_mode:
return {'changed': True, 'msg': msg, 'password_changed': password_changed}
privileges_revoke(cursor, user, host, db_table, priv, grant_option, maria_role) privileges_revoke(cursor, user, host, db_table, priv, grant_option, maria_role)
changed = True changed = True
@ -363,8 +466,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
for db_table, priv in iteritems(new_priv): for db_table, priv in iteritems(new_priv):
if db_table not in curr_priv: if db_table not in curr_priv:
msg = "New privileges granted" msg = "New privileges granted"
if module.check_mode: if not module.check_mode:
return {'changed': True, 'msg': msg, 'password_changed': password_changed}
privileges_grant(cursor, user, host, db_table, priv, tls_requires, maria_role) privileges_grant(cursor, user, host, db_table, priv, tls_requires, maria_role)
changed = True changed = True
@ -404,26 +506,82 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
if len(grant_privs) + len(revoke_privs) > 0: if len(grant_privs) + len(revoke_privs) > 0:
msg = "Privileges updated: granted %s, revoked %s" % (grant_privs, revoke_privs) msg = "Privileges updated: granted %s, revoked %s" % (grant_privs, revoke_privs)
if module.check_mode: if not module.check_mode:
return {'changed': True, 'msg': msg, 'password_changed': password_changed}
if len(revoke_privs) > 0: if len(revoke_privs) > 0:
privileges_revoke(cursor, user, host, db_table, revoke_privs, grant_option, maria_role) privileges_revoke(cursor, user, host, db_table, revoke_privs, grant_option, maria_role)
if len(grant_privs) > 0: if len(grant_privs) > 0:
privileges_grant(cursor, user, host, db_table, grant_privs, tls_requires, maria_role) 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 privilege manipulation, compare privileges from before and now
after_priv = privileges_get(cursor, user, host, maria_role) after_priv = privileges_get(cursor, user, host, maria_role)
changed = changed or (curr_priv != after_priv) 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: if role:
continue continue
# Handle TLS requirements # 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: if current_requires != tls_requires:
msg = "TLS requires updated" msg = "TLS requires updated"
if module.check_mode: if not module.check_mode:
return {'changed': True, 'msg': msg, 'password_changed': password_changed}
if not old_user_mgmt: if not old_user_mgmt:
pre_query = "ALTER USER" pre_query = "ALTER USER"
else: else:
@ -439,7 +597,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
cursor.execute(*query_with_args) cursor.execute(*query_with_args)
changed = True changed = True
return {'changed': changed, 'msg': msg, 'password_changed': password_changed} return {'changed': changed, 'msg': msg, 'password_changed': password_changed, 'attributes': final_attributes}
def user_delete(cursor, user, host, host_all, check_mode): def user_delete(cursor, user, host, host_all, check_mode):
@ -743,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', )]) priv_string = ",".join([p for p in priv if p not in ('GRANT', )])
query = ["GRANT %s ON %s" % (priv_string, db_table)] 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: if not maria_role:
query.append("TO %s@%s") query.append("TO %s@%s")
params = (user, host) params = (user, host)
@ -750,6 +916,7 @@ def privileges_grant(cursor, user, host, db_table, priv, tls_requires, maria_rol
query.append("TO %s") query.append("TO %s")
params = (user) params = (user)
impl = get_user_implementation(cursor)
if tls_requires and impl.use_old_user_mgmt(cursor): if tls_requires and impl.use_old_user_mgmt(cursor):
query, params = mogrify_requires(" ".join(query), params, tls_requires) query, params = mogrify_requires(" ".join(query), params, tls_requires)
query = [query] query = [query]
@ -886,6 +1053,7 @@ def limit_resources(module, cursor, user, host, resource_limits, check_mode):
Returns: True, if changed, False otherwise. Returns: True, if changed, False otherwise.
""" """
impl = get_user_implementation(cursor)
if not impl.server_supports_alter_user(cursor): if not impl.server_supports_alter_user(cursor):
module.fail_json(msg="The server version does not match the requirements " module.fail_json(msg="The server version does not match the requirements "
"for resource_limits parameter. See module's documentation.") "for resource_limits parameter. See module's documentation.")
@ -916,12 +1084,116 @@ def limit_resources(module, cursor, user, host, resource_limits, check_mode):
return True return True
def get_impl(cursor): def set_password_expire(cursor, user, host, password_expire, password_expire_interval):
global impl """Fuction to set passowrd expiration for user.
cursor.execute("SELECT VERSION()")
if 'mariadb' in cursor.fetchone()[0].lower(): 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 from ansible_collections.community.mysql.plugins.module_utils.implementations.mariadb import user as mariauser
impl = mariauser return mariauser
else: else:
from ansible_collections.community.mysql.plugins.module_utils.implementations.mysql import user as mysqluser from ansible_collections.community.mysql.plugins.module_utils.implementations.mysql import user as mysqluser
impl = mysqluser return mysqluser

View file

@ -11,9 +11,9 @@ __metaclass__ = type
DOCUMENTATION = r''' DOCUMENTATION = r'''
--- ---
module: mysql_db 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: description:
- Add or remove MySQL databases from a remote host. - Add or remove MySQL or MariaDB databases from a remote host.
options: options:
name: name:
description: description:
@ -46,8 +46,8 @@ options:
target: target:
description: description:
- Location, on the remote host, of the dump file to read from or write to. - 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 - Uncompressed SQL files (C(.sql)) as well as bzip2 (C(.bz2)), gzip (C(.gz)),
xz (Added in 2.0) compressed files are supported. xz (Added in 2.0) and zstd (C(.zst)) (Added in 3.12.0) compressed files are supported.
type: path type: path
single_transaction: single_transaction:
description: description:
@ -159,7 +159,7 @@ options:
pipefail: pipefail:
description: description:
- Use C(bash) instead of C(sh) and add C(-o pipefail) to catch errors from the - Use C(bash) instead of C(sh) and add C(-o pipefail) to catch errors from the
mysql_dump command when I(state=import) and compression is used. 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 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. - The default will change to C(yes) in community.mysql 4.0.0.
type: bool type: bool
@ -188,6 +188,7 @@ requirements:
- mysql (command line binary) - mysql (command line binary)
- mysqldump (command line binary) - mysqldump (command line binary)
notes: notes:
- Compatible with MariaDB or MySQL.
- Requires the mysql and mysqldump binaries on the remote host. - Requires the mysql and mysqldump binaries on the remote host.
- This module is B(not idempotent) when I(state) is C(import), - 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. and will import the dump file each time if run more than once.
@ -342,7 +343,15 @@ import traceback
from ansible.module_utils.basic import AnsibleModule 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.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.six.moves import shlex_quote
from ansible.module_utils._text import to_native from ansible.module_utils._text import to_native
@ -371,68 +380,81 @@ def db_delete(cursor, db):
def db_dump(module, host, user, password, db_name, target, all_databases, port, 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, single_transaction=None, quick=None, ignore_tables=None, hex_blob=None,
encoding=None, force=False, master_data=0, skip_lock_tables=False, encoding=None, force=False, master_data=0, skip_lock_tables=False,
dump_extra_args=None, unsafe_password=False, restrict_config_file=False, dump_extra_args=None, unsafe_password=False, restrict_config_file=False,
check_implicit_admin=False, pipefail=False): check_implicit_admin=False, pipefail=False):
cmd = module.get_bin_path('mysqldump', True)
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 defined, mysqldump demands --defaults-extra-file be the first option
if config_file: if config_file:
if restrict_config_file: if restrict_config_file:
cmd += " --defaults-file=%s" % shlex_quote(config_file) cmd.append("--defaults-file=%s" % shlex_quote(config_file))
else: else:
cmd += " --defaults-extra-file=%s" % shlex_quote(config_file) cmd.append("--defaults-extra-file=%s" % shlex_quote(config_file))
if check_implicit_admin: if check_implicit_admin:
cmd += " --user=root --password=''" cmd.append("--user=root --password=''")
else: else:
if user is not None: if user is not None:
cmd += " --user=%s" % shlex_quote(user) cmd.append("--user=%s" % shlex_quote(user))
if password is not None: if password is not None:
if not unsafe_password: if not unsafe_password:
cmd += " --password=%s" % shlex_quote(password) cmd.append("--password=%s" % shlex_quote(password))
else: else:
cmd += " --password=%s" % password cmd.append("--password=%s" % password)
if ssl_cert is not None: 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: 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: 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: if force:
cmd += " --force" cmd.append("--force")
if socket is not None: if socket is not None:
cmd += " --socket=%s" % shlex_quote(socket) cmd.append("--socket=%s" % shlex_quote(socket))
else: else:
cmd += " --host=%s --port=%i" % (shlex_quote(host), port) cmd.append("--host=%s --port=%i" % (shlex_quote(host), port))
if all_databases: if all_databases:
cmd += " --all-databases" cmd.append("--all-databases")
elif len(db_name) > 1: elif len(db_name) > 1:
cmd += " --databases {0}".format(' '.join(db_name)) cmd.append("--databases {0}".format(' '.join(db_name)))
else: else:
cmd += " %s" % shlex_quote(' '.join(db_name)) cmd.append("%s" % shlex_quote(' '.join(db_name)))
if skip_lock_tables: if skip_lock_tables:
cmd += " --skip-lock-tables" cmd.append("--skip-lock-tables")
if (encoding is not None) and (encoding != ""): 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: if single_transaction:
cmd += " --single-transaction=true" cmd.append("--single-transaction=true")
if quick: if quick:
cmd += " --quick" cmd.append("--quick")
if ignore_tables: if ignore_tables:
for an_ignored_table in 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: if hex_blob:
cmd += " --hex-blob" cmd.append("--hex-blob")
if master_data: 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: if dump_extra_args is not None:
cmd += " " + dump_extra_args cmd.append(dump_extra_args)
path = None path = None
if os.path.splitext(target)[-1] == '.gz': if os.path.splitext(target)[-1] == '.gz':
@ -441,6 +463,10 @@ def db_dump(module, host, user, password, db_name, target, all_databases, port,
path = module.get_bin_path('bzip2', True) path = module.get_bin_path('bzip2', True)
elif os.path.splitext(target)[-1] == '.xz': elif os.path.splitext(target)[-1] == '.xz':
path = module.get_bin_path('xz', True) 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: if path:
cmd = '%s | %s > %s' % (cmd, path, shlex_quote(target)) cmd = '%s | %s > %s' % (cmd, path, shlex_quote(target))
@ -460,13 +486,21 @@ def db_dump(module, host, user, password, db_name, target, all_databases, port,
def db_import(module, host, user, password, db_name, target, all_databases, port, config_file, 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, use_shell=False, unsafe_password=False, restrict_config_file=False,
check_implicit_admin=False): check_implicit_admin=False):
if not os.path.exists(target): if not os.path.exists(target):
return module.fail_json(msg="target %s does not exist on the host" % 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 # --defaults-file must go first, or errors out
if config_file: if config_file:
if restrict_config_file: if restrict_config_file:
@ -512,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) comp_prog_path = module.get_bin_path('bzip2', required=True)
elif os.path.splitext(target)[-1] == '.xz': elif os.path.splitext(target)[-1] == '.xz':
comp_prog_path = module.get_bin_path('xz', required=True) 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: if comp_prog_path:
# The line below is for returned data only: # The line below is for returned data only:
executed_commands.append('%s -dc %s | %s' % (comp_prog_path, target, cmd)) executed_commands.append('%s -dc %s | %s' % (comp_prog_path, target, cmd))
@ -577,14 +613,14 @@ def db_create(cursor, db, encoding, collation):
def main(): def main():
argument_spec = mysql_common_argument_spec() argument_spec = mysql_common_argument_spec()
argument_spec.update( 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=''), encoding=dict(type='str', default=''),
collation=dict(type='str', default=''), collation=dict(type='str', default=''),
target=dict(type='path'), target=dict(type='path'),
state=dict(type='str', default='present', choices=['absent', 'dump', 'import', 'present']), state=dict(type='str', default='present', choices=['absent', 'dump', 'import', 'present']),
single_transaction=dict(type='bool', default=False), single_transaction=dict(type='bool', default=False),
quick=dict(type='bool', default=True), 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'), hex_blob=dict(default=False, type='bool'),
force=dict(type='bool', default=False), force=dict(type='bool', default=False),
master_data=dict(type='int', default=0, choices=[0, 1, 2]), master_data=dict(type='int', default=0, choices=[0, 1, 2]),
@ -689,6 +725,9 @@ def main():
else: else:
module.fail_json(msg="unable to find %s. Exception message: %s" % (config_file, to_native(e))) 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 changed = False
if not os.path.exists(config_file): if not os.path.exists(config_file):
config_file = None config_file = None
@ -729,7 +768,8 @@ def main():
module.exit_json(changed=True, db=db_name, db_list=db) module.exit_json(changed=True, db=db_name, db_list=db)
rc, stdout, stderr = db_dump(module, login_host, login_user, rc, stdout, stderr = db_dump(module, login_host, login_user,
login_password, db, target, all_databases, 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, ssl_ca, single_transaction, quick, ignore_tables,
hex_blob, encoding, force, master_data, skip_lock_tables, hex_blob, encoding, force, master_data, skip_lock_tables,
dump_extra_args, unsafe_login_password, restrict_config_file, dump_extra_args, unsafe_login_password, restrict_config_file,
@ -750,8 +790,8 @@ def main():
rc, stdout, stderr = db_import(module, login_host, login_user, rc, stdout, stderr = db_import(module, login_host, login_user,
login_password, db, target, login_password, db, target,
all_databases, all_databases,
login_port, config_file, login_port, config_file, server_implementation,
socket, ssl_cert, ssl_key, ssl_ca, server_version, socket, ssl_cert, ssl_key, ssl_ca,
encoding, force, use_shell, unsafe_login_password, encoding, force, use_shell, unsafe_login_password,
restrict_config_file, check_implicit_admin) restrict_config_file, check_implicit_admin)
if rc != 0: if rc != 0:

View file

@ -1,18 +1,20 @@
#!/usr/bin/python #!/usr/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> # Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <andrew.a.klychkov@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # 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 from __future__ import absolute_import, division, print_function
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = r''' DOCUMENTATION = r'''
--- ---
module: mysql_info module: mysql_info
short_description: Gather information about MySQL servers short_description: Gather information about MySQL or MariaDB servers
description: description:
- Gathers information about MySQL servers. - Gathers information about MySQL or MariaDB servers.
options: options:
filter: filter:
@ -34,7 +36,7 @@ options:
exclude_fields: exclude_fields:
description: description:
- List of fields which are not needed to collect. - 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 type: list
elements: str elements: str
version_added: '0.1.0' version_added: '0.1.0'
@ -45,6 +47,7 @@ options:
default: false default: false
notes: 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. - 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). To avoid this, use I(exclude_fields=db_size).
@ -145,7 +148,7 @@ EXAMPLES = r'''
plugin: "{{ item.plugin | default(omit) }}" plugin: "{{ item.plugin | default(omit) }}"
plugin_auth_string: "{{ item.plugin_auth_string | default(omit) }}" plugin_auth_string: "{{ item.plugin_auth_string | default(omit) }}"
plugin_hash_string: "{{ item.plugin_hash_string | default(omit) }}" plugin_hash_string: "{{ item.plugin_hash_string | default(omit) }}"
tls_require: "{{ item.tls_require | default(omit) }}" tls_requires: "{{ item.tls_requires | default(omit) }}"
priv: "{{ item.priv | default(omit) }}" priv: "{{ item.priv | default(omit) }}"
resource_limits: "{{ item.resource_limits | default(omit) }}" resource_limits: "{{ item.resource_limits | default(omit) }}"
column_case_sensitive: true column_case_sensitive: true
@ -160,6 +163,12 @@ EXAMPLES = r'''
''' '''
RETURN = r''' RETURN = r'''
server_engine:
description: Database server engine.
returned: if not excluded by filter
type: str
sample: 'MariaDB'
version_added: '3.10.0'
version: version:
description: Database server version. description: Database server version.
returned: if not excluded by filter returned: if not excluded by filter
@ -196,13 +205,19 @@ databases:
returned: if not excluded by filter returned: if not excluded by filter
type: dict type: dict
sample: sample:
- { "mysql": { "size": 656594 }, "information_schema": { "size": 73728 } } - { "mysql": { "size": 656594, "tables": 31 }, "information_schema": { "size": 73728, "tables": 79 } }
contains: contains:
size: size:
description: Database size in bytes. description: Database size in bytes.
returned: if not excluded by filter returned: if not excluded by filter
type: dict type: dict
sample: { 'size': 656594 } 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: settings:
description: Global settings (variables) information. description: Global settings (variables) information.
returned: if not excluded by filter returned: if not excluded by filter
@ -239,7 +254,8 @@ users_info:
"host": "host.com", "host": "host.com",
"plugin": "mysql_native_password", "plugin": "mysql_native_password",
"priv": "db1.*:SELECT/db2.*:SELECT", "priv": "db1.*:SELECT/db2.*:SELECT",
"resource_limits": { "MAX_USER_CONNECTIONS": 100 } } "resource_limits": { "MAX_USER_CONNECTIONS": 100 },
"tls_requires": { "SSL": null } }
version_added: '3.8.0' version_added: '3.8.0'
engines: engines:
description: Information about the server's storage engines. description: Information about the server's storage engines.
@ -271,7 +287,6 @@ connector_name:
type: str type: str
sample: sample:
- "pymysql" - "pymysql"
- "MySQLdb"
version_added: '3.6.0' version_added: '3.6.0'
connector_version: connector_version:
description: Version of the python connector used by the module. When the connector is not identified, returns C(Unknown). description: Version of the python connector used by the module. When the connector is not identified, returns C(Unknown).
@ -285,6 +300,9 @@ connector_version:
from decimal import Decimal from decimal import Decimal
from ansible.module_utils.basic import AnsibleModule 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 ( from ansible_collections.community.mysql.plugins.module_utils.mysql import (
mysql_connect, mysql_connect,
mysql_common_argument_spec, mysql_common_argument_spec,
@ -292,12 +310,16 @@ from ansible_collections.community.mysql.plugins.module_utils.mysql import (
mysql_driver_fail_msg, mysql_driver_fail_msg,
get_connector_name, get_connector_name,
get_connector_version, get_connector_version,
get_server_implementation,
get_server_version,
) )
from ansible_collections.community.mysql.plugins.module_utils.user import ( from ansible_collections.community.mysql.plugins.module_utils.user import (
privileges_get, privileges_get,
get_resource_limits, get_resource_limits,
get_existing_authentication, get_existing_authentication,
get_user_implementation,
user_is_locked,
) )
from ansible.module_utils.six import iteritems from ansible.module_utils.six import iteritems
from ansible.module_utils._text import to_native from ansible.module_utils._text import to_native
@ -325,9 +347,13 @@ class MySQL_Info(object):
5. add info about the new subset with an example to RETURN block 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.module = module
self.cursor = cursor 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 = { self.info = {
'version': {}, 'version': {},
'databases': {}, 'databases': {},
@ -489,7 +515,8 @@ class MySQL_Info(object):
def __get_master_status(self): def __get_master_status(self):
"""Get master status if the instance is a master.""" """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: if res:
for line in res: for line in res:
for vname, val in iteritems(line): for vname, val in iteritems(line):
@ -497,7 +524,8 @@ class MySQL_Info(object):
def __get_slave_status(self): def __get_slave_status(self):
"""Get slave status if the instance is a slave.""" """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: if res:
for line in res: for line in res:
host = line['Master_Host'] host = line['Master_Host']
@ -518,7 +546,8 @@ class MySQL_Info(object):
def __get_slaves(self): def __get_slaves(self):
"""Get slave hosts info if the instance is a master.""" """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: if res:
for line in res: for line in res:
srv_id = line['Server_id'] srv_id = line['Server_id']
@ -596,13 +625,17 @@ class MySQL_Info(object):
priv_string.remove('*.*:USAGE') priv_string.remove('*.*:USAGE')
resource_limits = get_resource_limits(self.cursor, user, host) resource_limits = get_resource_limits(self.cursor, user, host)
copy_ressource_limits = dict.copy(resource_limits) copy_ressource_limits = dict.copy(resource_limits)
tls_requires = self.user_implementation.get_tls_requires(
self.cursor, user, host)
output_dict = { output_dict = {
'name': user, 'name': user,
'host': host, 'host': host,
'priv': '/'.join(priv_string), 'priv': '/'.join(priv_string),
'resource_limits': copy_ressource_limits, 'resource_limits': copy_ressource_limits,
'tls_requires': tls_requires,
} }
# Prevent returning a resource limit if empty # Prevent returning a resource limit if empty
@ -613,12 +646,18 @@ class MySQL_Info(object):
if len(output_dict['resource_limits']) == 0: if len(output_dict['resource_limits']) == 0:
del output_dict['resource_limits'] 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) authentications = get_existing_authentication(self.cursor, user, host)
if authentications: if authentications:
output_dict.update(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 # TODO password_option
# TODO lock_option
# but both are not supported by mysql_user atm. So no point yet. # but both are not supported by mysql_user atm. So no point yet.
output.append(output_dict) output.append(output_dict)
@ -627,40 +666,39 @@ class MySQL_Info(object):
def __get_databases(self, exclude_fields, return_empty_dbs): def __get_databases(self, exclude_fields, return_empty_dbs):
"""Get info about databases.""" """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')
res = self.__exec_sql(query) def is_field_included(field_name):
return not exclude_fields or 'db_{}'.format(field_name) not in exclude_fields
if res: def create_db_info(db_data):
for db in res: info = {}
self.info['databases'][db['name']] = {} 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
if not exclude_fields or 'db_size' not in exclude_fields: # Build the main query
if db['size'] is None: query_parts = ['SELECT table_schema AS "name"']
db['size'] = 0 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"')
self.info['databases'][db['name']]['size'] = int(db['size']) query = "{} FROM information_schema.TABLES GROUP BY table_schema".format(", ".join(query_parts))
# If empty dbs are not needed in the returned dict, exit from the method # Get and process databases with tables
if not return_empty_dbs: databases = self.__exec_sql(query) or []
return None for db in databases:
self.info['databases'][db['name']] = create_db_info(db)
# Add info about empty databases (issue #65727): # Handle empty databases if requested
res = self.__exec_sql('SHOW DATABASES') if return_empty_dbs:
if res: empty_databases = self.__exec_sql('SHOW DATABASES') or []
for db in res: for db in empty_databases:
if db['Database'] not in self.info['databases']: db_name = db['Database']
self.info['databases'][db['Database']] = {} if db_name not in self.info['databases']:
self.info['databases'][db_name] = create_db_info({})
if not exclude_fields or 'db_size' not in exclude_fields:
self.info['databases'][db['Database']]['size'] = 0
def __exec_sql(self, query, ddl=False): def __exec_sql(self, query, ddl=False):
"""Execute SQL. """Execute SQL.
@ -692,8 +730,8 @@ def main():
argument_spec = mysql_common_argument_spec() argument_spec = mysql_common_argument_spec()
argument_spec.update( argument_spec.update(
login_db=dict(type='str'), login_db=dict(type='str'),
filter=dict(type='list'), filter=dict(type='list', elements='str'),
exclude_fields=dict(type='list'), exclude_fields=dict(type='list', elements='str'),
return_empty_dbs=dict(type='bool', default=False), return_empty_dbs=dict(type='bool', default=False),
) )
@ -738,12 +776,17 @@ def main():
'Exception message: %s' % (connector_name, connector_version, config_file, to_native(e))) 'Exception message: %s' % (connector_name, connector_version, config_file, to_native(e)))
module.fail_json(msg) 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 # 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, module.exit_json(changed=False,
server_engine='MariaDB' if server_implementation == 'mariadb' else 'MySQL',
connector_name=connector_name, connector_name=connector_name,
connector_version=connector_version, connector_version=connector_version,
**mysql.get_info(filter_, exclude_fields, return_empty_dbs)) **mysql.get_info(filter_, exclude_fields, return_empty_dbs))

View file

@ -1,7 +1,7 @@
#!/usr/bin/python #!/usr/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright: (c) 2020, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> # Copyright: (c) 2020, Andrew Klychkov (@Andersson007) <andrew.a.klychkov@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # 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) from __future__ import (absolute_import, division, print_function)
@ -10,9 +10,9 @@ __metaclass__ = type
DOCUMENTATION = r''' DOCUMENTATION = r'''
--- ---
module: mysql_query module: mysql_query
short_description: Run MySQL queries short_description: Run MySQL or MariaDB queries
description: description:
- Runs arbitrary MySQL queries. - Runs arbitrary MySQL or MariaDB queries.
- Pay attention, the module does not support check mode! - Pay attention, the module does not support check mode!
All queries will be executed in autocommit mode. All queries will be executed in autocommit mode.
- To run SQL queries from a file, use M(community.mysql.mysql_db) module. - To run SQL queries from a file, use M(community.mysql.mysql_db) module.
@ -36,6 +36,7 @@ options:
- List of values to be passed as positional arguments to the query. - List of values to be passed as positional arguments to the query.
- Mutually exclusive with I(named_args). - Mutually exclusive with I(named_args).
type: list type: list
elements: raw
named_args: named_args:
description: description:
- Dictionary of key-value arguments to pass to the query. - Dictionary of key-value arguments to pass to the query.
@ -55,11 +56,12 @@ attributes:
support: none support: none
seealso: seealso:
- module: community.mysql.mysql_db - module: community.mysql.mysql_db
notes:
- Compatible with MariaDB or MySQL.
author: author:
- Andrew Klychkov (@Andersson007) - Andrew Klychkov (@Andersson007)
extends_documentation_fragment: extends_documentation_fragment:
- community.mysql.mysql - community.mysql.mysql
''' '''
EXAMPLES = r''' EXAMPLES = r'''
@ -114,8 +116,18 @@ rowcount:
returned: changed returned: changed
type: list type: list
sample: [5, 1] 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 import warnings
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
@ -136,12 +148,24 @@ DDL_QUERY_KEYWORDS = ('CREATE', 'DROP', 'ALTER', 'RENAME', 'TRUNCATE')
# Module execution. # 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(): def main():
argument_spec = mysql_common_argument_spec() argument_spec = mysql_common_argument_spec()
argument_spec.update( argument_spec.update(
query=dict(type='raw', required=True), query=dict(type='raw', required=True),
login_db=dict(type='str'), login_db=dict(type='str'),
positional_args=dict(type='list'), positional_args=dict(type='list', elements='raw'),
named_args=dict(type='dict'), named_args=dict(type='dict'),
single_transaction=dict(type='bool', default=False), single_transaction=dict(type='bool', default=False),
) )
@ -210,6 +234,7 @@ def main():
query_result = [] query_result = []
executed_queries = [] executed_queries = []
rowcount = [] rowcount = []
execution_time_ms = []
already_exists = False already_exists = False
for q in query: for q in query:
@ -220,7 +245,8 @@ def main():
category=mysql_driver.Warning) category=mysql_driver.Warning)
try: try:
cursor.execute(q, arguments) cursor, exec_time_ms = execute_and_return_time(cursor, q, arguments)
execution_time_ms.append(exec_time_ms)
except mysql_driver.Warning: except mysql_driver.Warning:
# When something is run with IF NOT EXISTS # When something is run with IF NOT EXISTS
# and there's "already exists" MySQL warning, # and there's "already exists" MySQL warning,
@ -277,6 +303,7 @@ def main():
'executed_queries': executed_queries, 'executed_queries': executed_queries,
'query_result': query_result, 'query_result': query_result,
'rowcount': rowcount, 'rowcount': rowcount,
'execution_time_ms': execution_time_ms,
} }
# Exit: # Exit:

View file

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright: (c) 2013, Balazs Pocze <banyek@gawker.com> # Copyright: (c) 2013, Balazs Pocze <banyek@gawker.com>
# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> # Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <andrew.a.klychkov@gmail.com>
# Certain parts are taken from Mark Theunissen's mysqldb module # 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) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
@ -13,17 +13,20 @@ __metaclass__ = type
DOCUMENTATION = r''' DOCUMENTATION = r'''
--- ---
module: mysql_replication module: mysql_replication
short_description: Manage MySQL replication short_description: Manage MySQL or MariaDB replication
description: 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: author:
- Balazs Pocze (@banyek) - Balazs Pocze (@banyek)
- Andrew Klychkov (@Andersson007) - Andrew Klychkov (@Andersson007)
- Dennis Urtubia (@dennisurtubia)
- Laurent Indermühle (@laurent-indermuehle)
options: options:
mode: mode:
description: description:
- Module operating mode. Could be - Module operating mode. Could be
C(changeprimary) (CHANGE MASTER TO), 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(getprimary) (SHOW MASTER STATUS),
C(getreplica) (SHOW REPLICA STATUS), C(getreplica) (SHOW REPLICA STATUS),
C(startreplica) (START REPLICA), C(startreplica) (START REPLICA),
@ -34,6 +37,7 @@ options:
type: str type: str
choices: choices:
- changeprimary - changeprimary
- changereplication
- getprimary - getprimary
- getreplica - getreplica
- startreplica - startreplica
@ -188,6 +192,7 @@ options:
version_added: '0.1.0' version_added: '0.1.0'
notes: notes:
- Compatible with MariaDB or MySQL.
- If an empty value for the parameter of string type is needed, use an empty string. - If an empty value for the parameter of string type is needed, use an empty string.
attributes: attributes:
@ -229,6 +234,13 @@ EXAMPLES = r'''
primary_log_file: mysql-bin.000009 primary_log_file: mysql-bin.000009
primary_log_pos: 4578 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 - name: Check replica status using port 3308
community.mysql.mysql_replication: community.mysql.mysql_replication:
mode: getreplica mode: getreplica
@ -272,7 +284,6 @@ EXAMPLES = r'''
community.mysql.mysql_replication: community.mysql.mysql_replication:
mode: changeprimary mode: changeprimary
fail_on_error: true fail_on_error: true
''' '''
RETURN = r''' RETURN = r'''
@ -288,7 +299,12 @@ import os
import warnings import warnings
from ansible.module_utils.basic import AnsibleModule 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 ( from ansible_collections.community.mysql.plugins.module_utils.mysql import (
get_server_version,
get_server_implementation,
mysql_connect, mysql_connect,
mysql_driver, mysql_driver,
mysql_driver_fail_msg, mysql_driver_fail_msg,
@ -299,11 +315,10 @@ from ansible.module_utils._text import to_native
executed_queries = [] executed_queries = []
def get_primary_status(cursor): def get_primary_status(cursor, command_resolver):
# TODO: when it's available to change on MySQL's side, query = command_resolver.resolve_command("SHOW MASTER STATUS")
# change MASTER to PRIMARY using the approach from cursor.execute(query)
# get_replica_status() function. Same for other functions.
cursor.execute("SHOW MASTER STATUS")
primarystatus = cursor.fetchone() primarystatus = cursor.fetchone()
return primarystatus return primarystatus
@ -388,8 +403,8 @@ def reset_replica_all(module, cursor, connection_name='', channel='', fail_on_er
return reset return reset
def reset_primary(module, cursor, fail_on_error=False): def reset_primary(module, cursor, command_resolver, fail_on_error=False):
query = 'RESET MASTER' query = command_resolver.resolve_command('RESET MASTER')
try: try:
executed_queries.append(query) executed_queries.append(query)
cursor.execute(query) cursor.execute(query)
@ -398,7 +413,7 @@ def reset_primary(module, cursor, fail_on_error=False):
reset = False reset = False
except Exception as e: except Exception as e:
if fail_on_error: 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 reset = False
return reset return reset
@ -425,11 +440,22 @@ def start_replica(module, cursor, connection_name='', channel='', fail_on_error=
return started 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: 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: 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: if channel:
query += " FOR CHANNEL '%s'" % channel query += " FOR CHANNEL '%s'" % channel
@ -449,7 +475,8 @@ def main():
'startreplica', 'startreplica',
'resetprimary', 'resetprimary',
'resetreplica', 'resetreplica',
'resetreplicaall']), 'resetreplicaall',
'changereplication']),
primary_auto_position=dict(type='bool', default=False, aliases=['master_auto_position']), primary_auto_position=dict(type='bool', default=False, aliases=['master_auto_position']),
primary_host=dict(type='str', aliases=['master_host']), primary_host=dict(type='str', aliases=['master_host']),
primary_user=dict(type='str', aliases=['master_user']), primary_user=dict(type='str', aliases=['master_user']),
@ -533,8 +560,11 @@ def main():
else: else:
module.fail_json(msg="unable to find %s. Exception message: %s" % (config_file, to_native(e))) 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()") 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 from ansible_collections.community.mysql.plugins.module_utils.implementations.mariadb import replication as impl
else: else:
from ansible_collections.community.mysql.plugins.module_utils.implementations.mysql import replication as impl from ansible_collections.community.mysql.plugins.module_utils.implementations.mysql import replication as impl
@ -549,21 +579,27 @@ def main():
primary_use_gtid = 'slave_pos' primary_use_gtid = 'slave_pos'
if mode == 'getprimary': if mode == 'getprimary':
status = get_primary_status(cursor) status = get_primary_status(cursor, command_resolver)
if not isinstance(status, dict): if status and "File" in status and "Position" in status:
status = dict(Is_Primary=False,
msg="Server is not configured as mysql primary")
else:
status['Is_Primary'] = True 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) module.exit_json(queries=executed_queries, **status)
elif mode == "getreplica": elif mode == "getreplica":
status = get_replica_status(cursor, connection_name, channel, replica_term) status = get_replica_status(cursor, connection_name, channel, replica_term)
if not isinstance(status, dict): # MySQL 8.0 uses Replica_...
status = dict(Is_Replica=False, msg="Server is not configured as mysql replica") # MariaDB 10.6 uses Slave_...
else: if status and (
"Slave_IO_Running" in status or
"Replica_IO_Running" in status):
status['Is_Replica'] = True 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) module.exit_json(queries=executed_queries, **status)
@ -571,52 +607,52 @@ def main():
chm = [] chm = []
result = {} result = {}
if primary_host is not None: 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: 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: 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: 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: 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: 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: 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: 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: if relay_log_file is not None:
chm.append("RELAY_LOG_FILE='%s'" % relay_log_file) chm.append("RELAY_LOG_FILE='%s'" % relay_log_file)
if relay_log_pos is not None: if relay_log_pos is not None:
chm.append("RELAY_LOG_POS=%s" % relay_log_pos) chm.append("RELAY_LOG_POS=%s" % relay_log_pos)
if primary_ssl is not None: if primary_ssl is not None:
if primary_ssl: if primary_ssl:
chm.append("MASTER_SSL=1") chm.append("%s=1" % command_resolver.resolve_command('MASTER_SSL'))
else: else:
chm.append("MASTER_SSL=0") chm.append("%s=0" % command_resolver.resolve_command('MASTER_SSL'))
if primary_ssl_ca is not None: 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: 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: 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: 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: 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: if primary_ssl_verify_server_cert:
chm.append("SOURCE_SSL_VERIFY_SERVER_CERT=1") chm.append("%s=1" % command_resolver.resolve_command('MASTER_SSL_VERIFY_SERVER_CERT'))
if primary_auto_position: 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: 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: try:
changeprimary(cursor, chm, connection_name, channel) changeprimary(cursor, command_resolver, chm, connection_name, channel)
except mysql_driver.Warning as e: except mysql_driver.Warning as e:
result['warning'] = to_native(e) result['warning'] = to_native(e)
except Exception as 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 result['changed'] = True
module.exit_json(queries=executed_queries, **result) module.exit_json(queries=executed_queries, **result)
elif mode == "startreplica": elif mode == "startreplica":
@ -632,7 +668,7 @@ def main():
else: else:
module.exit_json(msg="Replica already stopped", changed=False, queries=executed_queries) module.exit_json(msg="Replica already stopped", changed=False, queries=executed_queries)
elif mode == 'resetprimary': 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: if reset is True:
module.exit_json(msg="Primary reset", changed=True, queries=executed_queries) module.exit_json(msg="Primary reset", changed=True, queries=executed_queries)
else: else:
@ -649,6 +685,56 @@ def main():
module.exit_json(msg="Replica reset", changed=True, queries=executed_queries) module.exit_json(msg="Replica reset", changed=True, queries=executed_queries)
else: else:
module.exit_json(msg="Replica already reset", changed=False, queries=executed_queries) 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") warnings.simplefilter("ignore")

View file

@ -1,7 +1,7 @@
#!/usr/bin/python #!/usr/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright: (c) 2021, Andrew Klychkov <aaklychkov@mail.ru> # Copyright: (c) 2021, Andrew Klychkov <andrew.a.klychkov@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # 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 from __future__ import absolute_import, division, print_function
@ -11,10 +11,10 @@ DOCUMENTATION = r'''
--- ---
module: mysql_role module: mysql_role
short_description: Adds, removes, or updates a MySQL role short_description: Adds, removes, or updates a MySQL or MariaDB role
description: 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. - Roles are supported since MySQL 8.0.0 and MariaDB 10.0.5.
version_added: '2.2.0' version_added: '2.2.0'
@ -132,6 +132,7 @@ options:
version_added: '3.8.0' version_added: '3.8.0'
notes: 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) - Pay attention that the module runs C(SET DEFAULT ROLE ALL TO)
all the I(members) passed by default when the state has changed. 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). If you want to avoid this behavior, set I(set_default_role_all) to C(no).
@ -309,7 +310,7 @@ from ansible_collections.community.mysql.plugins.module_utils.mysql import (
) )
from ansible_collections.community.mysql.plugins.module_utils.user import ( from ansible_collections.community.mysql.plugins.module_utils.user import (
convert_priv_dict_to_str, convert_priv_dict_to_str,
get_impl, get_user_implementation,
get_mode, get_mode,
user_mod, user_mod,
privileges_grant, privileges_grant,
@ -929,10 +930,12 @@ class Role():
set_default_role_all=set_default_role_all) set_default_role_all=set_default_role_all)
if privs: if privs:
result = user_mod(self.cursor, self.name, self.host, result = user_mod(cursor=self.cursor, user=self.name, host=self.host,
None, None, None, None, None, None, host_all=None, password=None, encrypted=None, plugin=None,
privs, append_privs, subtract_privs, None, plugin_auth_string=None, plugin_hash_string=None, salt=None,
self.module, role=True, maria_role=self.is_mariadb) 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'] changed = result['changed']
if admin: if admin:
@ -1053,7 +1056,7 @@ def main():
# Set defaults # Set defaults
changed = False changed = False
get_impl(cursor) impl = get_user_implementation(cursor)
if priv is not None: if priv is not None:
try: try:

View file

@ -11,15 +11,16 @@ __metaclass__ = type
DOCUMENTATION = r''' DOCUMENTATION = r'''
--- ---
module: mysql_user 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: description:
- Adds or removes a user from a MySQL database. - Adds or removes a user from a MySQL or MariaDB database.
options: options:
name: name:
description: description:
- Name of the user (role) to add or remove. - Name of the user (role) to add or remove.
type: str type: str
required: true required: true
aliases: ['user']
password: password:
description: description:
- Set the user's password. Only for C(mysql_native_password) authentication. - Set the user's password. Only for C(mysql_native_password) authentication.
@ -45,6 +46,7 @@ options:
priv: priv:
description: description:
- "MySQL privileges string in the format: C(db.table:priv1,priv2)." - "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 - "Multiple privileges can be specified by separating each one using
a forward slash: C(db.table1:priv/db.table2:priv)." a forward slash: C(db.table1:priv/db.table2:priv)."
- The format is based on MySQL C(GRANT) statement. - The format is based on MySQL C(GRANT) statement.
@ -139,8 +141,16 @@ options:
description: description:
- User's plugin auth_string (``CREATE USER user IDENTIFIED WITH plugin BY plugin_auth_string``). - 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. - 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 type: str
version_added: '0.1.0' 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: resource_limits:
description: description:
- Limit the user for certain server resources. Provided since MySQL 5.6 / MariaDB 10.2. - Limit the user for certain server resources. Provided since MySQL 5.6 / MariaDB 10.2.
@ -155,6 +165,20 @@ options:
- Cannot be used to set global variables, use the M(community.mysql.mysql_variables) module instead. - Cannot be used to set global variables, use the M(community.mysql.mysql_variables) module instead.
type: dict type: dict
version_added: '3.6.0' 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: column_case_sensitive:
description: description:
@ -166,7 +190,24 @@ options:
type: bool type: bool
version_added: '3.8.0' 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: notes:
- Compatible with MySQL or MariaDB.
- "MySQL server installs with default I(login_user) of C(root) and no password. - "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: 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, 1) change the root user's password, without providing any I(login_user)/I(login_password) details,
@ -193,6 +234,7 @@ author:
- Lukasz Tomaszkiewicz (@tomaszkiewicz) - Lukasz Tomaszkiewicz (@tomaszkiewicz)
- kmarse (@kmarse) - kmarse (@kmarse)
- Laurent Indermühle (@laurent-indermuehle) - Laurent Indermühle (@laurent-indermuehle)
- E.S. Rosenberg (@Keeper-of-the-Keys)
extends_documentation_fragment: extends_documentation_fragment:
- community.mysql.mysql - community.mysql.mysql
@ -237,7 +279,7 @@ EXAMPLES = r'''
priv: '*.*:ALL,GRANT' priv: '*.*:ALL,GRANT'
state: present state: present
session_vars: session_vars:
wsrep_on: off wsrep_on: 'off'
- name: Create user with password, all database privileges and 'WITH GRANT OPTION' in db1 and db2 - name: Create user with password, all database privileges and 'WITH GRANT OPTION' in db1 and db2
community.mysql.mysql_user: community.mysql.mysql_user:
@ -257,6 +299,13 @@ EXAMPLES = r'''
FUNCTION my_db.my_function: EXECUTE FUNCTION my_db.my_function: EXECUTE
state: present 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 - name: Modify user to require TLS connection with a valid client certificate
community.mysql.mysql_user: community.mysql.mysql_user:
name: bob name: bob
@ -340,6 +389,13 @@ EXAMPLES = r'''
priv: '*.*:ALL' priv: '*.*:ALL'
state: present 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 - name: Limit bob's resources to 10 queries per hour and 5 connections per hour
community.mysql.mysql_user: community.mysql.mysql_user:
name: bob name: bob
@ -354,6 +410,13 @@ EXAMPLES = r'''
priv: priv:
'db1.*': DELETE '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 # Example .my.cnf file for setting the root password
# [client] # [client]
# user=root # user=root
@ -373,7 +436,6 @@ from ansible_collections.community.mysql.plugins.module_utils.mysql import (
) )
from ansible_collections.community.mysql.plugins.module_utils.user import ( from ansible_collections.community.mysql.plugins.module_utils.user import (
convert_priv_dict_to_str, convert_priv_dict_to_str,
get_impl,
get_mode, get_mode,
InvalidPrivsError, InvalidPrivsError,
limit_resources, limit_resources,
@ -395,7 +457,13 @@ from ansible.module_utils._text import to_native
def main(): def main():
argument_spec = mysql_common_argument_spec() argument_spec = mysql_common_argument_spec()
argument_spec.update( 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), password=dict(type='str', no_log=True),
encrypted=dict(type='bool', default=False), encrypted=dict(type='bool', default=False),
host=dict(type='str', default='localhost'), host=dict(type='str', default='localhost'),
@ -405,16 +473,21 @@ def main():
tls_requires=dict(type='dict'), tls_requires=dict(type='dict'),
append_privs=dict(type='bool', default=False), append_privs=dict(type='bool', default=False),
subtract_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), check_implicit_admin=dict(type='bool', default=False),
update_password=dict(type='str', default='always', choices=['always', 'on_create', 'on_new_username'], 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), sql_log_bin=dict(type='bool', default=True),
plugin=dict(default=None, type='str'), plugin=dict(default=None, type='str'),
plugin_hash_string=dict(default=None, type='str'), plugin_hash_string=dict(default=None, type='str'),
plugin_auth_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'), resource_limits=dict(type='dict'),
force_context=dict(type='bool', default=False), force_context=dict(type='bool', default=False),
session_vars=dict(type='dict'), session_vars=dict(type='dict'),
column_case_sensitive=dict(type='bool', default=None), # TODO 4.0.0 add default=True 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( module = AnsibleModule(
argument_spec=argument_spec, argument_spec=argument_spec,
@ -423,7 +496,7 @@ def main():
) )
login_user = module.params["login_user"] login_user = module.params["login_user"]
login_password = module.params["login_password"] login_password = module.params["login_password"]
user = module.params["user"] user = module.params["name"]
password = module.params["password"] password = module.params["password"]
encrypted = module.boolean(module.params["encrypted"]) encrypted = module.boolean(module.params["encrypted"])
host = module.params["host"].lower() host = module.params["host"].lower()
@ -437,6 +510,7 @@ def main():
append_privs = module.boolean(module.params["append_privs"]) append_privs = module.boolean(module.params["append_privs"])
subtract_privs = module.boolean(module.params['subtract_privs']) subtract_privs = module.boolean(module.params['subtract_privs'])
update_password = module.params['update_password'] update_password = module.params['update_password']
attributes = module.params['attributes']
ssl_cert = module.params["client_cert"] ssl_cert = module.params["client_cert"]
ssl_key = module.params["client_key"] ssl_key = module.params["client_key"]
ssl_ca = module.params["ca_cert"] ssl_ca = module.params["ca_cert"]
@ -448,9 +522,13 @@ def main():
plugin = module.params["plugin"] plugin = module.params["plugin"]
plugin_hash_string = module.params["plugin_hash_string"] plugin_hash_string = module.params["plugin_hash_string"]
plugin_auth_string = module.params["plugin_auth_string"] plugin_auth_string = module.params["plugin_auth_string"]
salt = module.params["salt"]
resource_limits = module.params["resource_limits"] resource_limits = module.params["resource_limits"]
session_vars = module.params["session_vars"] session_vars = module.params["session_vars"]
column_case_sensitive = module.params["column_case_sensitive"] 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)): 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)) module.fail_json(msg="priv parameter must be str or dict but %s was passed" % type(priv))
@ -461,6 +539,18 @@ def main():
if mysql_driver is None: if mysql_driver is None:
module.fail_json(msg=mysql_driver_fail_msg) 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 cursor = None
try: try:
if check_implicit_admin: if check_implicit_admin:
@ -490,8 +580,6 @@ def main():
if session_vars: if session_vars:
set_session_vars(module, cursor, session_vars) set_session_vars(module, cursor, session_vars)
get_impl(cursor)
if priv is not None: if priv is not None:
try: try:
mode = get_mode(cursor) mode = get_mode(cursor)
@ -500,21 +588,27 @@ def main():
priv = privileges_unpack(priv, mode, column_case_sensitive, ensure_usage=not subtract_privs) priv = privileges_unpack(priv, mode, column_case_sensitive, ensure_usage=not subtract_privs)
password_changed = False password_changed = False
final_attributes = None
if state == "present": if state == "present":
if user_exists(cursor, user, host, host_all): if user_exists(cursor, user, host, host_all):
try: try:
if update_password == "always": if update_password == "always":
result = user_mod(cursor, user, host, host_all, password, encrypted, result = user_mod(cursor, user, host, host_all, password, encrypted,
plugin, plugin_hash_string, plugin_auth_string, plugin, plugin_hash_string, plugin_auth_string, salt,
priv, append_privs, subtract_privs, tls_requires, module) priv, append_privs, subtract_privs, attributes, tls_requires, module,
password_expire, password_expire_interval, locked=locked)
else: else:
result = user_mod(cursor, user, host, host_all, None, encrypted, result = user_mod(cursor=cursor, user=user, host=host, host_all=host_all, password=None,
None, None, None, encrypted=encrypted, plugin=None, plugin_hash_string=None, plugin_auth_string=None,
priv, append_privs, subtract_privs, tls_requires, module) 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'] changed = result['changed']
msg = result['msg'] msg = result['msg']
password_changed = result['password_changed'] password_changed = result['password_changed']
final_attributes = result['attributes']
except (SQLParseError, InvalidPrivsError, mysql_driver.Error) as e: except (SQLParseError, InvalidPrivsError, mysql_driver.Error) as e:
module.fail_json(msg=to_native(e)) module.fail_json(msg=to_native(e))
@ -526,10 +620,12 @@ def main():
priv = None # avoid granting unwanted privileges priv = None # avoid granting unwanted privileges
reuse_existing_password = update_password == 'on_new_username' reuse_existing_password = update_password == 'on_new_username'
result = user_add(cursor, user, host, host_all, password, encrypted, result = user_add(cursor, user, host, host_all, password, encrypted,
plugin, plugin_hash_string, plugin_auth_string, plugin, plugin_hash_string, plugin_auth_string, salt,
priv, tls_requires, module.check_mode, reuse_existing_password) priv, attributes, tls_requires, reuse_existing_password, module,
password_expire, password_expire_interval, locked=locked)
changed = result['changed'] changed = result['changed']
password_changed = result['password_changed'] password_changed = result['password_changed']
final_attributes = result['attributes']
if changed: if changed:
msg = "User added" msg = "User added"
@ -546,7 +642,7 @@ def main():
else: else:
changed = False changed = False
msg = "User doesn't exist" msg = "User doesn't exist"
module.exit_json(changed=changed, user=user, msg=msg, password_changed=password_changed) module.exit_json(changed=changed, user=user, msg=msg, password_changed=password_changed, attributes=final_attributes)
if __name__ == '__main__': if __name__ == '__main__':

View file

@ -12,9 +12,9 @@ DOCUMENTATION = r'''
--- ---
module: mysql_variables module: mysql_variables
short_description: Manage MySQL global variables short_description: Manage MySQL or MariaDB global variables
description: description:
- Query / Set MySQL variables. - Query / Set MySQL or MariaDB variables.
author: author:
- Balazs Pocze (@banyek) - Balazs Pocze (@banyek)
options: options:
@ -26,6 +26,7 @@ options:
value: value:
description: description:
- If set, then sets variable value to this. - If set, then sets variable value to this.
- With boolean values, use C(0)|C(1) or quoted C("ON")|C("OFF").
type: str type: str
mode: mode:
description: description:
@ -54,6 +55,9 @@ seealso:
description: Complete reference of the MySQL SET command documentation. description: Complete reference of the MySQL SET command documentation.
link: https://dev.mysql.com/doc/refman/8.0/en/set-statement.html link: https://dev.mysql.com/doc/refman/8.0/en/set-statement.html
notes:
- Compatible with MariaDB or MySQL.
extends_documentation_fragment: extends_documentation_fragment:
- community.mysql.mysql - community.mysql.mysql
''' '''
@ -71,6 +75,11 @@ EXAMPLES = r'''
variable: read_only variable: read_only
value: 1 value: 1
mode: persist 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''' RETURN = r'''
@ -173,10 +182,22 @@ def setvariable(cursor, mysqlvar, value, mode='global'):
return result 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(): def main():
argument_spec = mysql_common_argument_spec() argument_spec = mysql_common_argument_spec()
argument_spec.update( argument_spec.update(
variable=dict(type='str'), variable=dict(type='str', required=True),
value=dict(type='str'), value=dict(type='str'),
mode=dict(type='str', choices=['global', 'persist', 'persist_only'], default='global'), mode=dict(type='str', choices=['global', 'persist', 'persist_only'], default='global'),
) )
@ -240,6 +261,9 @@ def main():
# Type values before using them # Type values before using them
value_wanted = typedvalue(value) value_wanted = typedvalue(value)
value_actual = typedvalue(mysqlvar_val) 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 value_in_auto_cnf = None
if var_in_mysqld_auto_cnf is not None: if var_in_mysqld_auto_cnf is not None:
value_in_auto_cnf = typedvalue(var_in_mysqld_auto_cnf) value_in_auto_cnf = typedvalue(var_in_mysqld_auto_cnf)

View file

@ -1,21 +0,0 @@
FROM quay.io/ansible/ubuntu2204-test-container:main
# ubuntu2204 comes with mariadb-client-10.6
# iproute2 # To grab docker network gateway address
# python3.10-dev # Reqs for mysqlclient
# default-libmysqlclient-dev # Reqs for mysqlclient
# build-essential # Reqs for mysqlclient
RUN apt-get update -y && \
DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
python3.10 \
python3.10-dev \
mariadb-client \
iproute2 \
default-libmysqlclient-dev \
build-essential
RUN python3.10 -m pip install --disable-pip-version-check --no-cache-dir mysqlclient==2.1.1
ENV container=docker
CMD ["/sbin/init"]

View file

@ -1,15 +0,0 @@
FROM quay.io/ansible/ubuntu2204-test-container:main
# ubuntu2204 comes with mariadb-client-10.6
# iproute2 # To grab docker network gateway address
RUN apt-get update -y && \
DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
python3.10 \
mariadb-client \
iproute2
RUN python3.10 -m pip install --disable-pip-version-check --no-cache-dir pymysql==1.0.2
ENV container=docker
CMD ["/sbin/init"]

View file

@ -1,21 +0,0 @@
FROM quay.io/ansible/ubuntu2004-test-container:main
# ubuntu2004 comes with mariadb-client-10.3
# iproute2 # To grab docker network gateway address
# python3.8-dev # Reqs for mysqlclient
# default-libmysqlclient-dev # Reqs for mysqlclient
# build-essential # Reqs for mysqlclient
RUN apt-get update -y && \
DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
python3.8 \
python3.8-dev \
mariadb-client \
iproute2 \
default-libmysqlclient-dev \
build-essential
RUN python3.8 -m pip install --disable-pip-version-check --no-cache-dir mysqlclient==2.0.1
ENV container=docker
CMD ["/sbin/init"]

View file

@ -1,15 +0,0 @@
FROM quay.io/ansible/ubuntu2004-test-container:main
# ubuntu2004 comes with mariadb-client-10.3
# iproute2 # To grab docker network gateway address
RUN apt-get update -y && \
DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
python3.8 \
mariadb-client \
iproute2
RUN python3.8 -m pip install --disable-pip-version-check --no-cache-dir pymysql==0.9.3
ENV container=docker
CMD ["/sbin/init"]

View file

@ -1,21 +0,0 @@
FROM quay.io/ansible/ubuntu2004-test-container:main
# ubuntu2004 comes with mariadb-client-10.3
# iproute2 # To grab docker network gateway address
# python3.9-dev # Reqs for mysqlclient
# default-libmysqlclient-dev # Reqs for mysqlclient
# build-essential # Reqs for mysqlclient
RUN apt-get update -y && \
DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
python3.9 \
python3.9-dev \
mariadb-client \
iproute2 \
default-libmysqlclient-dev \
build-essential
RUN python3.9 -m pip install --disable-pip-version-check --no-cache-dir mysqlclient==2.0.3
ENV container=docker
CMD ["/sbin/init"]

View file

@ -1,15 +0,0 @@
FROM quay.io/ansible/ubuntu2004-test-container:main
# ubuntu2004 comes with mariadb-client-10.3
# iproute2 # To grab docker network gateway address
RUN apt-get update -y && \
DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
python3.9 \
mariadb-client \
iproute2
RUN python3.9 -m pip install --disable-pip-version-check --no-cache-dir pymysql==0.9.3
ENV container=docker
CMD ["/sbin/init"]

View file

@ -1,21 +0,0 @@
FROM quay.io/ansible/ubuntu1804-test-container:main
# ubuntu1804 comes with mysql-client-5.7
# iproute2 # To grab docker network gateway address
# python3.8-dev # Reqs for mysqlclient
# default-libmysqlclient-dev # Reqs for mysqlclient
# build-essential # Reqs for mysqlclient
RUN apt-get update -y && \
DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
python3.8 \
python3.8-dev \
mysql-client \
iproute2 \
default-libmysqlclient-dev \
build-essential
RUN python3.8 -m pip install --disable-pip-version-check --no-cache-dir mysqlclient==2.0.1
ENV container=docker
CMD ["/sbin/init"]

View file

@ -1,21 +0,0 @@
FROM quay.io/ansible/ubuntu1804-test-container:main
# ubuntu1804 comes with mysql-client-5.7
# iproute2 # To grab docker network gateway address
# python3.8-dev # Reqs for mysqlclient
# default-libmysqlclient-dev # Reqs for mysqlclient
# build-essential # Reqs for mysqlclient
RUN apt-get update -y && \
DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
python3.8 \
python3.8-dev \
mysql-client \
iproute2 \
default-libmysqlclient-dev \
build-essential
RUN python3.8 -m pip install --disable-pip-version-check --no-cache-dir pymysql==0.7.11
ENV container=docker
CMD ["/sbin/init"]

View file

@ -1,15 +0,0 @@
FROM quay.io/ansible/ubuntu1804-test-container:main
# ubuntu1804 comes with mysql-client-5.7
# iproute2 # To grab docker network gateway address
RUN apt-get update -y && \
DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
python3.8 \
mysql-client \
iproute2
RUN python3.8 -m pip install --disable-pip-version-check --no-cache-dir pymysql==0.9.3
ENV container=docker
CMD ["/sbin/init"]

View file

@ -1,21 +0,0 @@
FROM quay.io/ansible/ubuntu2204-test-container:main
# ubuntu2204 comes with mysql-client-8
# iproute2 # To grab docker network gateway address
# python3.10-dev # Reqs for mysqlclient
# default-libmysqlclient-dev # Reqs for mysqlclient
# build-essential # Reqs for mysqlclient
RUN apt-get update -y && \
DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
python3.10 \
python3.10-dev \
mysql-client \
iproute2 \
default-libmysqlclient-dev \
build-essential
RUN python3.10 -m pip install --disable-pip-version-check --no-cache-dir mysqlclient==2.1.1
ENV container=docker
CMD ["/sbin/init"]

View file

@ -1,15 +0,0 @@
FROM quay.io/ansible/ubuntu2204-test-container:main
# ubuntu2204 comes with mysql-client-8
# iproute2 # To grab docker network gateway address
RUN apt-get update -y && \
DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
python3.10 \
mysql-client \
iproute2
RUN python3.10 -m pip install --disable-pip-version-check --no-cache-dir pymysql==1.0.2
ENV container=docker
CMD ["/sbin/init"]

View file

@ -1,21 +0,0 @@
FROM quay.io/ansible/ubuntu2004-test-container:main
# ubuntu2004 comes with mysql-client-8
# iproute2 # To grab docker network gateway address
# python3.8-dev # Reqs for mysqlclient
# default-libmysqlclient-dev # Reqs for mysqlclient
# build-essential # Reqs for mysqlclient
RUN apt-get update -y && \
DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
python3.8 \
python3.8-dev \
mysql-client \
iproute2 \
default-libmysqlclient-dev \
build-essential
RUN python3.8 -m pip install --disable-pip-version-check --no-cache-dir mysqlclient==2.0.1
ENV container=docker
CMD ["/sbin/init"]

View file

@ -1,15 +0,0 @@
FROM quay.io/ansible/ubuntu2004-test-container:main
# ubuntu2004 comes with mysql-client-8
# iproute2 # To grab docker network gateway address
RUN apt-get update -y && \
DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
python3.8 \
mysql-client \
iproute2
RUN python3.8 -m pip install --disable-pip-version-check --no-cache-dir pymysql==0.9.3
ENV container=docker
CMD ["/sbin/init"]

View file

@ -1,21 +0,0 @@
FROM quay.io/ansible/ubuntu2004-test-container:main
# ubuntu2004 comes with mysql-client-8
# iproute2 # To grab docker network gateway address
# python3.9-dev # Reqs for mysqlclient
# default-libmysqlclient-dev # Reqs for mysqlclient
# build-essential # Reqs for mysqlclient
RUN apt-get update -y && \
DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
python3.9 \
python3.9-dev \
mysql-client \
iproute2 \
default-libmysqlclient-dev \
build-essential
RUN python3.9 -m pip install --disable-pip-version-check --no-cache-dir mysqlclient==2.0.3
ENV container=docker
CMD ["/sbin/init"]

View file

@ -1,16 +0,0 @@
FROM quay.io/ansible/ubuntu2004-test-container:main
# ubuntu2004 comes with mysql-client-8
# iproute2 # To grab docker network gateway address
RUN apt-get update -y && \
DEBIAN_FRONTEND=noninteractive apt-get upgrade -y --no-install-recommends && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
python3.9 \
mysql-client \
iproute2
# cffi # To connect to MySQL 8 with Python3.9 and PyMySQL
RUN python3.9 -m pip install --disable-pip-version-check --no-cache-dir cffi pymysql==0.9.3
ENV container=docker
CMD ["/sbin/init"]

View file

@ -1,4 +1,4 @@
# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> # Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <andrew.a.klychkov@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # 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 # Initial CI tests of mysql_replication module

View file

@ -1,4 +1,4 @@
# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> # Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <andrew.a.klychkov@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# Tests for master_use_gtid parameter. # Tests for master_use_gtid parameter.

View file

@ -1,4 +1,4 @@
# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> # Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <andrew.a.klychkov@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# Needs for further tests: # Needs for further tests:

View file

@ -1,4 +1,4 @@
# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> # Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <andrew.a.klychkov@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# Preparation: # Preparation:

View file

@ -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-----

View file

@ -4,15 +4,18 @@
# and should not be used as examples of how to write Ansible roles # # and should not be used as examples of how to write Ansible roles #
#################################################################### ####################################################################
- name: Prepare the fake root folder - name: "{{ role_name }} | Main | Prepare the fake root folder"
ansible.builtin.import_tasks: ansible.builtin.import_tasks:
file: fake_root.yml file: fake_root.yml
# setvars.yml requires the iproute2 package installed by install.yml - name: "{{ role_name }} | Main | Set variables"
- name: Set variables
ansible.builtin.import_tasks: ansible.builtin.import_tasks:
file: setvars.yml file: setvars.yml
- name: Verify all components version under test - 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: ansible.builtin.import_tasks:
file: verify.yml file: verify.yml

View file

@ -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

View file

@ -1,13 +1,17 @@
--- ---
- name: "{{ role_name }} | Setvars | Extract Podman/Docker Network Gateway" - name: "{{ role_name }} | Setvars | Install tools gather network facts"
ansible.builtin.shell: ansible.builtin.package:
cmd: ip route|grep default|awk '{print $3}' name:
register: ip_route_output - iproute2
state: present
- name: "{{ role_name }} | Setvars | Gather facts"
ansible.builtin.setup:
- name: "{{ role_name }} | Setvars | Set Fact" - name: "{{ role_name }} | Setvars | Set Fact"
ansible.builtin.set_fact: ansible.builtin.set_fact:
gateway_addr: "{{ ip_route_output.stdout }}" gateway_addr: "{{ ansible_default_ipv4.gateway }}"
connector_name_lookup: >- connector_name_lookup: >-
{{ lookup( {{ lookup(
'file', 'file',
@ -28,11 +32,6 @@
'file', 'file',
'/root/ansible_collections/community/mysql/tests/integration/db_engine_version' '/root/ansible_collections/community/mysql/tests/integration/db_engine_version'
) }} ) }}
python_version_lookup: >-
{{ lookup(
'file',
'/root/ansible_collections/community/mysql/tests/integration/python'
) }}
ansible_version_lookup: >- ansible_version_lookup: >-
{{ lookup( {{ lookup(
'file', 'file',
@ -45,7 +44,6 @@
connector_version: "{{ connector_version_lookup.strip() }}" connector_version: "{{ connector_version_lookup.strip() }}"
db_engine: "{{ db_engine_name_lookup.strip() }}" db_engine: "{{ db_engine_name_lookup.strip() }}"
db_version: "{{ db_engine_version_lookup.strip() }}" db_version: "{{ db_engine_version_lookup.strip() }}"
python_version: "{{ python_version_lookup.strip() }}"
test_ansible_version: >- test_ansible_version: >-
{%- if ansible_version_lookup == 'devel' -%} {%- if ansible_version_lookup == 'devel' -%}
{{ ansible_version_lookup }} {{ ansible_version_lookup }}
@ -73,7 +71,6 @@
connector_version: {{ connector_version }} connector_version: {{ connector_version }}
db_engine: {{ db_engine }} db_engine: {{ db_engine }}
db_version: {{ db_version }} db_version: {{ db_version }}
python_version: {{ python_version }}
test_ansible_version: {{ test_ansible_version }} test_ansible_version: {{ test_ansible_version }}
ansible.builtin.debug: ansible.builtin.debug:
msg: "{{ msg.split('\n') }}" msg: "{{ msg.split('\n') }}"

View file

@ -19,8 +19,11 @@
- name: Assert that test container runs the expected MySQL/MariaDB version - name: Assert that test container runs the expected MySQL/MariaDB version
assert: assert:
that: that:
- "'{{ primary_info.version.major }}.{{ primary_info.version.minor }}\ - registred_db_version == db_version
.{{ primary_info.version.release }}' == '{{ 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 - name: Assert that mysql_info module used the expected version of pymysql
assert: assert:
@ -38,22 +41,27 @@
when: when:
- connector_name == 'mysqlclient' - connector_name == 'mysqlclient'
- name: Display the python version in use - name: Get the python version in use
command: ansible.builtin.command:
cmd: python{{ python_version }} -V cmd: python -V
changed_when: false changed_when: false
register: python_in_use failed_when: false
register: python_version_in_use
- name: Assert that expected Python is installed - name: Display the python version in use
assert: ansible.builtin.debug:
that: msg: >
- python_in_use.stdout is search(python_version) 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 - name: Assert that we run the expected ansible version
assert: assert:
that: that:
- > - ansible_running_version == test_ansible_version
vars:
ansible_running_version:
"{{ ansible_version.major }}.{{ ansible_version.minor }}" "{{ ansible_version.major }}.{{ ansible_version.minor }}"
is version(test_ansible_version, '==')
when: when:
- test_ansible_version != 'devel' # Devel will change overtime - test_ansible_version != 'devel' # Devel will change overtime

View file

@ -93,7 +93,9 @@
- name: Config overrides | Add fake host to config file - name: Config overrides | Add fake host to config file
shell: 'echo "host = {{ fake_host }}" >> {{ config_file }}' shell: 'echo "host = {{ fake_host }}" >> {{ config_file }}'
- name: Config overrides | 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: mysql_db:
login_user: '{{ mysql_user }}' login_user: '{{ mysql_user }}'
login_password: '{{ mysql_password }}' login_password: '{{ mysql_password }}'
@ -102,15 +104,17 @@
name: '{{ db_to_create }}' name: '{{ db_to_create }}'
state: absent state: absent
config_file: '{{ config_file }}' config_file: '{{ config_file }}'
config_overrides_defaults: yes config_overrides_defaults: true
register: result register: result
ignore_errors: yes failed_when:
- result is succeeded
- name: Config overrides | Must fail because login_host default has beed overriden by wrong value from config file - result.msg is not search(pattern1)
assert: - result.msg is not search(pattern2)
that: - result.msg is not search(pattern3)
- result is failed vars:
- result.msg is search("Can't connect to MySQL server on '{{ fake_host }}'") or result.msg is search("Unknown MySQL server host '{{ fake_host }}'") pattern1: Can't connect to MySQL server on '{{ fake_host }}'
pattern2: Unknown MySQL server host '{{ fake_host }}'
pattern3: Unknown server host '{{ fake_host }}'
- name: Config overrides | Clean up test database - name: Config overrides | Clean up test database
mysql_db: mysql_db:

View file

@ -111,11 +111,24 @@
check_implicit_admin: no check_implicit_admin: no
register: result register: result
- name: Dump and Import | Assert successful completion of dump operation - name: Dump and Import | Assert successful completion of dump operation for MariaDB and MySQL < 8.2
assert: assert:
that: that:
- result is changed - result is changed
- result.executed_commands[0] is search(".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: 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) - name: Dump and Import | State dump/import - file name should exist (db_file_name)
file: file:
@ -339,7 +352,7 @@
assert: assert:
that: that:
- result is changed - result is changed
- "result.db =='{{ db_name }}'" - result.db == db_name
# - name: Dump and Import | Assert database was backed up successfully # - name: Dump and Import | Assert database was backed up successfully
# command: "file {{ db_file_name }}" # command: "file {{ db_file_name }}"

View file

@ -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

View file

@ -47,7 +47,7 @@
state: import state: import
target: /root/create_procedure.sql target: /root/create_procedure.sql
# Use a query instead of mysql_user, because we want to caches differences # 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 # at the end and a bug in mysql_user would be invisible to this tests
- name: Mysql_info users_info | Prepare common tests users - name: Mysql_info users_info | Prepare common tests users
community.mysql.mysql_query: community.mysql.mysql_query:
@ -147,67 +147,96 @@
'*CB3326D5279DE7915FE5D743232165EE887883CA' '*CB3326D5279DE7915FE5D743232165EE887883CA'
- GRANT SELECT ON users_info_db.* TO users_info_multi_hosts@'host2' - 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 - name: Mysql_info users_info | Prepare tests users for MariaDB
community.mysql.mysql_user: community.mysql.mysql_query:
name: "{{ item.name }}" query:
host: "users_info.com" - >-
plugin: "{{ item.plugin | default(omit) }}" CREATE USER users_info_socket@'users_info.com' IDENTIFIED WITH
plugin_auth_string: "{{ item.plugin_auth_string | default(omit) }}" unix_socket
plugin_hash_string: "{{ item.plugin_hash_string | default(omit) }}" - GRANT ALL ON *.* to users_info_socket@'users_info.com'
tls_require: "{{ item.tls_require | default(omit) }}"
priv: "{{ item.priv }}"
resource_limits: "{{ item.resource_limits | default(omit) }}"
column_case_sensitive: true
state: present
loop:
- name: users_info_socket # Only for MariaDB
priv:
'*.*': 'ALL'
plugin: 'unix_socket'
when: when:
- db_engine == 'mariadb' - db_engine == 'mariadb'
- name: Mysql_info users_info | Prepare tests users for MySQL - name: Mysql_info users_info | Prepare tests users for MySQL
community.mysql.mysql_user: community.mysql.mysql_query:
name: "{{ item.name }}" query:
host: "users_info.com" - >-
plugin: "{{ item.plugin | default(omit) }}" CREATE USER users_info_sha256@'users_info.com' IDENTIFIED WITH
plugin_auth_string: "{{ item.plugin_auth_string | default(omit) }}" sha256_password BY 'msandbox'
plugin_hash_string: "{{ item.plugin_hash_string | default(omit) }}" - GRANT ALL ON *.* to users_info_sha256@'users_info.com'
tls_require: "{{ item.tls_require | default(omit) }}"
priv: "{{ item.priv }}"
resource_limits: "{{ item.resource_limits | default(omit) }}"
column_case_sensitive: true
state: present
loop:
- name: users_info_sha256 # Only for MySQL
priv:
'*.*': 'ALL'
plugin_auth_string:
'$5$/<w*D`L4\"F$WQiI1Pev.7atAh8udYs3wqlzgdfV8LXoy7rqSEC7NF2'
plugin: 'sha256_password'
when: when:
- db_engine == 'mysql' - db_engine == 'mysql'
- name: Mysql_info users_info | Prepare tests users for MySQL 8+ - name: Mysql_info users_info | Prepare tests users for MySQL 8+
community.mysql.mysql_user: community.mysql.mysql_query:
name: "{{ item.name }}" query:
host: "users_info.com" - >-
plugin: "{{ item.plugin | default(omit) }}" CREATE USER users_info_caching_sha2@'users_info.com' IDENTIFIED WITH
plugin_auth_string: "{{ item.plugin_auth_string | default(omit) }}" caching_sha2_password BY 'msandbox'
plugin_hash_string: "{{ item.plugin_hash_string | default(omit) }}" - GRANT ALL ON *.* to users_info_caching_sha2@'users_info.com'
tls_require: "{{ item.tls_require | default(omit) }}"
priv: "{{ item.priv }}"
resource_limits: "{{ item.resource_limits | default(omit) }}"
column_case_sensitive: true
state: present
loop:
- name: users_info_caching_sha2 # Only for MySQL 8+
priv:
'*.*': 'ALL'
plugin_auth_string:
'$A$005$61j/uF%Qb4-=O2xkeO82u2HNkF.lxDq0liO4U3xqi7bDUCbWM6HayRXWn1'
plugin: 'caching_sha2_password'
when: when:
- db_engine == 'mysql' - db_engine == 'mysql'
- db_version is version('8.0', '>=') - db_version is version('8.0', '>=')
@ -220,30 +249,34 @@
- users_info - users_info
register: result register: result
- name: Recreate users from mysql_info users_info result - name: Mysql_info users_info | Recreate users from mysql_info result
community.mysql.mysql_user: community.mysql.mysql_user:
name: "{{ item.name }}" name: "{{ item.name }}"
host: "{{ item.host }}" host: "{{ item.host }}"
plugin: "{{ item.plugin | default(omit) }}" plugin: "{{ item.plugin | default(omit) }}"
plugin_auth_string: "{{ item.plugin_auth_string | default(omit) }}" plugin_auth_string: "{{ item.plugin_auth_string | default(omit) }}"
plugin_hash_string: "{{ item.plugin_hash_string | default(omit) }}" plugin_hash_string: "{{ item.plugin_hash_string | default(omit) }}"
tls_require: "{{ item.tls_require | default(omit) }}" tls_requires: "{{ item.tls_requires | default(omit) }}"
priv: "{{ item.priv | default(omit) }}" priv: "{{ item.priv | default(omit) }}"
resource_limits: "{{ item.resource_limits | default(omit) }}" resource_limits: "{{ item.resource_limits | default(omit) }}"
column_case_sensitive: true column_case_sensitive: true
state: present state: present
locked: "{{ item.locked | default(omit) }}"
loop: "{{ result.users_info }}" loop: "{{ result.users_info }}"
loop_control: loop_control:
label: "{{ item.name }}@{{ item.host }}" label: "{{ item.name }}@{{ item.host }}"
register: recreate_users_result register: recreate_users_result
failed_when: failed_when:
- recreate_users_result is changed - >-
recreate_users_result is changed or
recreate_users_result.msg != 'User unchanged'
when: when:
- item.name != 'root' - item.name != 'root'
- item.name != 'mysql' - item.name != 'mysql'
- item.name != 'mariadb.sys' - item.name != 'mariadb.sys'
- item.name != 'mysql.sys' - item.name != 'mysql.sys'
- item.name != 'mysql.infoschema' - item.name != 'mysql.infoschema'
- item.name != 'mysql.session'
# ================================== Cleanup ============================ # ================================== Cleanup ============================
@ -265,6 +298,14 @@
- users_info_usage_only - users_info_usage_only
- users_info_columns_uppercase - users_info_columns_uppercase
- users_info_multi_hosts - 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 - name: Mysql_info users_info | Cleanup databases
community.mysql.mysql_db: community.mysql.mysql_db:

View file

@ -5,7 +5,7 @@
#################################################################### ####################################################################
# Test code for mysql_info module # Test code for mysql_info module
# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> # Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <andrew.a.klychkov@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
################### ###################
@ -56,6 +56,7 @@
- result.databases != {} - result.databases != {}
- result.engines != {} - result.engines != {}
- result.users != {} - result.users != {}
- result.server_engine == 'MariaDB' or result.server_engine == 'MySQL'
- name: mysql_info - Test connector informations display - name: mysql_info - Test connector informations display
ansible.builtin.import_tasks: ansible.builtin.import_tasks:
@ -131,94 +132,11 @@
- result.global_status is not defined - result.global_status is not defined
- result.users 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
- assert:
that:
- result is not changed
- result.databases != {}
- result.databases.mysql == {}
########################################################
# 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 is not changed
- 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 is not changed
- result.databases.acme == {}
- result.databases.mysql == {}
- name: Remove acme database
mysql_db:
<<: *mysql_params
name: acme
state: absent
- include_tasks: issue-28.yml - include_tasks: issue-28.yml
# https://github.com/ansible-collections/community.mysql/issues/204 - name: Import tasks file to tests tables count in database filter
- name: Create database containing only views ansible.builtin.import_tasks:
mysql_db: file: filter_databases.yml
<<: *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 - name: Import tasks file to tests users_info filter
ansible.builtin.import_tasks: ansible.builtin.import_tasks:

View file

@ -1,6 +1,6 @@
--- ---
# Test code for mysql_query module # Test code for mysql_query module
# Copyright: (c) 2020, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> # Copyright: (c) 2020, Andrew Klychkov (@Andersson007) <andrew.a.klychkov@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
- vars: - vars:
mysql_parameters: &mysql_params mysql_parameters: &mysql_params
@ -35,6 +35,7 @@
that: that:
- result is changed - result is changed
- result.executed_queries == ['CREATE TABLE {{ test_table1 }} (id int)'] - result.executed_queries == ['CREATE TABLE {{ test_table1 }} (id int)']
- result.execution_time_ms[0] > 0
- name: Insert test data - name: Insert test data
mysql_query: mysql_query:
@ -52,6 +53,8 @@
- result is changed - result is changed
- result.rowcount == [2, 1] - result.rowcount == [2, 1]
- result.executed_queries == ['INSERT INTO {{ test_table1 }} VALUES (1), (2)', 'INSERT INTO {{ test_table1 }} VALUES (3)'] - 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 }} - name: Check data in {{ test_table1 }}
mysql_query: mysql_query:

View file

@ -1,9 +1,10 @@
---
#################################################################### ####################################################################
# WARNING: These are designed specifically for Ansible tests # # WARNING: These are designed specifically for Ansible tests #
# and should not be used as examples of how to write Ansible roles # # and should not be used as examples of how to write Ansible roles #
#################################################################### ####################################################################
# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> # Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <andrew.a.klychkov@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # 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: # Initial CI tests of mysql_replication module:
@ -19,9 +20,14 @@
- import_tasks: mysql_replication_channel.yml - import_tasks: mysql_replication_channel.yml
when: when:
- db_engine == 'mysql' # FIXME: mariadb introduces FOR CHANNEL in 10.7 - db_engine == 'mysql' # FIXME: mariadb introduces FOR CHANNEL in 10.7
- mysql8022_and_higher == true # FIXME: mysql 5.7 should work, but our tets fails, why?
# Tests of resetprimary mode: # Tests of resetprimary mode:
- import_tasks: mysql_replication_resetprimary_mode.yml - import_tasks: mysql_replication_resetprimary_mode.yml
- include_tasks: 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', '>=')

View file

@ -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 }}'"]

View file

@ -1,5 +1,5 @@
--- ---
# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> # Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <andrew.a.klychkov@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
- vars: - vars:
@ -32,10 +32,36 @@
channel: '{{ test_channel }}' channel: '{{ test_channel }}'
register: result 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: that:
- result is changed - 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.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: # Test startreplica mode:
- name: Start replica with channel - name: Start replica with channel
@ -49,7 +75,10 @@
- assert: - assert:
that: that:
- result is changed - result is changed
- result.queries == ["START SLAVE FOR CHANNEL '{{ test_channel }}'"] or result.queries == ["START REPLICA FOR CHANNEL '{{ test_channel }}'"] - 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: # Test getreplica mode:
- name: Get standby status with channel - name: Get standby status with channel
@ -62,27 +91,40 @@
- assert: - assert:
that: that:
- replica_status.Is_Replica == true - replica_status.Is_Replica is truthy(convert_bool=True)
- replica_status.Master_Host == '{{ mysql_host }}' - replica_status.Master_Host == mysql_host_value
- replica_status.Exec_Master_Log_Pos == mysql_primary_status.Position - replica_status.Exec_Master_Log_Pos == mysql_primary_status.Position
- replica_status.Master_Port == {{ mysql_primary_port }} - replica_status.Master_Port == mysql_primary_port_value
- replica_status.Last_IO_Errno == 0 - replica_status.Last_IO_Errno == 0
- replica_status.Last_IO_Error == '' - replica_status.Last_IO_Error == ''
- replica_status.Channel_Name == '{{ test_channel }}' - replica_status.Channel_Name == test_channel_value
- replica_status is not changed - replica_status is not changed
when: mysql8022_and_higher == false 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: - assert:
that: that:
- replica_status.Is_Replica == true - replica_status.Is_Replica is truthy(convert_bool=True)
- replica_status.Source_Host == '{{ mysql_host }}' - replica_status.Source_Host == mysql_host_value
- replica_status.Exec_Source_Log_Pos == mysql_primary_status.Position - replica_status.Exec_Source_Log_Pos == mysql_primary_status.Position
- replica_status.Source_Port == {{ mysql_primary_port }} - replica_status.Source_Port == mysql_primary_port_value
- replica_status.Last_IO_Errno == 0 - replica_status.Last_IO_Errno == 0
- replica_status.Last_IO_Error == '' - replica_status.Last_IO_Error == ''
- replica_status.Channel_Name == '{{ test_channel }}' - replica_status.Channel_Name == test_channel_value
- replica_status is not changed - replica_status is not changed
when: mysql8022_and_higher == true 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: # Test stopreplica mode:
@ -97,7 +139,10 @@
- assert: - assert:
that: that:
- result is changed - result is changed
- result.queries == ["STOP SLAVE FOR CHANNEL '{{ test_channel }}'"] or result.queries == ["STOP REPLICA FOR CHANNEL '{{ test_channel }}'"] - 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 # Test reset
- name: Reset replica with channel - name: Reset replica with channel
@ -111,7 +156,10 @@
- assert: - assert:
that: that:
- result is changed - result is changed
- result.queries == ["RESET SLAVE FOR CHANNEL '{{ test_channel }}'"] or result.queries == ["RESET REPLICA FOR CHANNEL '{{ test_channel }}'"] - 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 # Test reset all
- name: Reset replica all with channel - name: Reset replica all with channel
@ -125,4 +173,7 @@
- assert: - assert:
that: that:
- result is changed - result is changed
- result.queries == ["RESET SLAVE ALL FOR CHANNEL '{{ test_channel }}'"] or result.queries == ["RESET REPLICA ALL FOR CHANNEL '{{ test_channel }}'"] - 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 }}'"]

View file

@ -1,5 +1,5 @@
--- ---
# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> # Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <andrew.a.klychkov@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
- vars: - vars:
@ -9,16 +9,6 @@
login_host: '{{ mysql_host }}' login_host: '{{ mysql_host }}'
block: block:
- name: Set mysql8022_and_higher
set_fact:
mysql8022_and_higher: false
- name: Set mysql8022_and_higher
set_fact:
mysql8022_and_higher: true
when:
- db_engine == 'mysql'
- db_version is version('8.0.22', '>=')
# We use iF NOT EXISTS because the GITHUB Action: # We use iF NOT EXISTS because the GITHUB Action:
# "ansible-community/ansible-test-gh-action" uses "--retry-on-error". # "ansible-community/ansible-test-gh-action" uses "--retry-on-error".
@ -136,11 +126,10 @@
that: 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, # primary_ssl_ca will be set as '' to check the module's behaviour for #23976,
# must be converted to an empty string # must be converted to an empty string
- name: Run replication - name: Test changeprimary mode with empty primary_ssl_ca
mysql_replication: community.mysql.mysql_replication:
<<: *mysql_params <<: *mysql_params
login_port: '{{ mysql_replica1_port }}' login_port: '{{ mysql_replica1_port }}'
mode: changeprimary mode: changeprimary
@ -151,14 +140,40 @@
primary_log_file: '{{ mysql_primary_status.File }}' primary_log_file: '{{ mysql_primary_status.File }}'
primary_log_pos: '{{ mysql_primary_status.Position }}' primary_log_pos: '{{ mysql_primary_status.Position }}'
primary_ssl_ca: '' primary_ssl_ca: ''
primary_ssl: no primary_ssl: false
register: result register: result
- name: Assert that changeprimmary is changed and return expected query - name: Assert that changeprimmary is changed and return expected query for MariaDB and MySQL < 8.0.23
assert: ansible.builtin.assert:
that: that:
- result is changed - 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=0,MASTER_SSL_CA=''"] - 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: # Test startreplica mode:
- name: Start replica - name: Start replica
@ -185,26 +200,37 @@
- name: Assert that getreplica returns expected values for MySQL older than 8.0.22 and Mariadb - name: Assert that getreplica returns expected values for MySQL older than 8.0.22 and Mariadb
assert: assert:
that: that:
- replica_status.Is_Replica == true - replica_status.Is_Replica is truthy(convert_bool=True)
- replica_status.Master_Host == '{{ mysql_host }}' - replica_status.Master_Host == mysql_host_value
- replica_status.Exec_Master_Log_Pos == mysql_primary_status.Position - replica_status.Exec_Master_Log_Pos == mysql_primary_status.Position
- replica_status.Master_Port == {{ mysql_primary_port }} - replica_status.Master_Port == mysql_primary_port_value
- replica_status.Last_IO_Errno == 0 - replica_status.Last_IO_Errno == 0
- replica_status.Last_IO_Error == '' - replica_status.Last_IO_Error == ''
- replica_status is not changed - replica_status is not changed
when: mysql8022_and_higher == false 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', '<'))
- name: Assert that getreplica returns expected values for MySQL newer than 8.0.22 - name: Assert that getreplica returns expected values for MySQL newer than 8.0.22
assert: assert:
that: that:
- replica_status.Is_Replica == true - replica_status.Is_Replica is truthy(convert_bool=True)
- replica_status.Source_Host == '{{ mysql_host }}' - replica_status.Source_Host == mysql_host_value
- replica_status.Exec_Source_Log_Pos == mysql_primary_status.Position - replica_status.Exec_Source_Log_Pos == mysql_primary_status.Position
- replica_status.Source_Port == {{ mysql_primary_port }} - replica_status.Source_Port == mysql_primary_port_value
- replica_status.Last_IO_Errno == 0 - replica_status.Last_IO_Errno == 0
- replica_status.Last_IO_Error == '' - replica_status.Last_IO_Error == ''
- replica_status is not changed - replica_status is not changed
when: mysql8022_and_higher == true 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: # Create test table and add data to it:
- name: Create test table - name: Create test table
@ -231,13 +257,18 @@
assert: assert:
that: that:
- replica_status.Exec_Master_Log_Pos != mysql_primary_status.Position - replica_status.Exec_Master_Log_Pos != mysql_primary_status.Position
when: mysql8022_and_higher == false when:
- >
db_engine == 'mariadb' or
(db_engine == 'mysql' and db_version is version('8.0.22', '<'))
- name: Assert that getreplica Log_Pos is different for MySQL newer than 8.0.22 - name: Assert that getreplica Log_Pos is different for MySQL newer than 8.0.22
assert: assert:
that: that:
- replica_status.Exec_Source_Log_Pos != mysql_primary_status.Position - replica_status.Exec_Source_Log_Pos != mysql_primary_status.Position
when: mysql8022_and_higher == true when:
- db_engine == 'mysql'
- db_version is version('8.0.22', '>=')
- name: Start replica that is already running - name: Start replica that is already running
mysql_replication: mysql_replication:
@ -306,5 +337,5 @@
- name: Assert that stopslave returns expected error message - name: Assert that stopslave returns expected error message
assert: assert:
that: that:
- result.msg == "value of mode must be one of{{ ":" }} getprimary, getreplica, changeprimary, stopreplica, startreplica, resetprimary, resetreplica, resetreplicaall, got{{ ":" }} stopslave" - result.msg == "value of mode must be one of{{ ":" }} getprimary, getreplica, changeprimary, stopreplica, startreplica, resetprimary, resetreplica, resetreplicaall, changereplication, got{{ ":" }} stopslave"
- result is failed - result is failed

View file

@ -1,4 +1,4 @@
# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> # Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <andrew.a.klychkov@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
- vars: - vars:
@ -18,10 +18,24 @@
primary_delay: '{{ test_primary_delay }}' primary_delay: '{{ test_primary_delay }}'
register: result 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: that:
- result is changed - result is changed
- result.queries == ["CHANGE MASTER TO MASTER_DELAY=60"] - 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: # Auxiliary step:
- name: Start replica - name: Start replica

View file

@ -1,4 +1,5 @@
# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <aaklychkov@mail.ru> ---
# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <andrew.a.klychkov@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
- vars: - vars:
@ -38,10 +39,24 @@
mode: resetprimary mode: resetprimary
register: result register: result
- assert: - name: Assert that reset primary is changed and query matches for MariaDB and MySQL < 8.4
ansible.builtin.assert:
that: that:
- result is changed - result is changed
- result.queries == ["RESET MASTER"] - 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: # Get primary final status:
- name: Get primary status - name: Get primary status

View file

@ -64,7 +64,7 @@
- name: Issue-265 | Remove blank mysql user with hosts=all (expect changed) - name: Issue-265 | Remove blank mysql user with hosts=all (expect changed)
mysql_user: mysql_user:
<<: *mysql_params <<: *mysql_params
user: "" name: ""
host_all: true host_all: true
state: absent state: absent
force_context: yes force_context: yes
@ -78,7 +78,7 @@
- name: Issue-265 | Remove blank mysql user with hosts=all (expect ok) - name: Issue-265 | Remove blank mysql user with hosts=all (expect ok)
mysql_user: mysql_user:
<<: *mysql_params <<: *mysql_params
user: "" name: ""
host_all: true host_all: true
force_context: yes force_context: yes
state: absent state: absent
@ -151,7 +151,7 @@
- name: Issue-265 | Remove blank mysql user with hosts=all (expect changed) - name: Issue-265 | Remove blank mysql user with hosts=all (expect changed)
mysql_user: mysql_user:
<<: *mysql_params <<: *mysql_params
user: "" name: ""
host_all: true host_all: true
state: absent state: absent
force_context: no force_context: no
@ -165,7 +165,7 @@
- name: Issue-265 | Remove blank mysql user with hosts=all (expect ok) - name: Issue-265 | Remove blank mysql user with hosts=all (expect ok)
mysql_user: mysql_user:
<<: *mysql_params <<: *mysql_params
user: "" name: ""
host_all: true host_all: true
force_context: no force_context: no
state: absent state: absent

View file

@ -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 }}"

View file

@ -43,6 +43,8 @@
- include_tasks: test_idempotency.yml - include_tasks: test_idempotency.yml
- include_tasks: test_password_expire.yml
# ============================================================ # ============================================================
# Create user with no privileges and verify default privileges are assign # Create user with no privileges and verify default privileges are assign
# #
@ -117,8 +119,8 @@
- name: Assert grant access for user1 on multiple database - name: Assert grant access for user1 on multiple database
assert: assert:
that: that:
- "'{{ item }}' in result.stdout" - item in result.stdout
with_items: "{{ db_names }}" 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'\"" command: "{{ mysql_command }} -e \"SHOW GRANTS FOR '{{ user_name_2 }}'@'localhost'\""
@ -127,8 +129,8 @@
- name: Assert grant access for user2 on multiple database - name: Assert grant access for user2 on multiple database
assert: assert:
that: that:
- "'{{ item }}' in result.stdout" - item in result.stdout
with_items: "{{db_names}}" loop: "{{db_names}}"
- include_tasks: utils/remove_user.yml - include_tasks: utils/remove_user.yml
vars: vars:
@ -267,6 +269,9 @@
tags: tags:
- issue_465 - issue_465
# Tests for user attributes
- include_tasks: test_user_attributes.yml
# Tests for the TLS requires dictionary # Tests for the TLS requires dictionary
- include_tasks: test_tls_requirements.yml - include_tasks: test_tls_requirements.yml
@ -278,6 +283,12 @@
tags: tags:
- issue-64560 - 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") # Test that mysql_user still works with force_context enabled (database set to "mysql")
# (https://github.com/ansible-collections/community.mysql/issues/265) # (https://github.com/ansible-collections/community.mysql/issues/265)
- include_tasks: issue-265.yml - include_tasks: issue-265.yml
@ -290,3 +301,11 @@
- name: Mysql_user - test column case sensitive - name: Mysql_user - test column case sensitive
ansible.builtin.import_tasks: ansible.builtin.import_tasks:
file: test_column_case_sensitive.yml 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

View file

@ -66,7 +66,7 @@
- name: Idempotency | Remove blank user with hosts=all (expect changed) - name: Idempotency | Remove blank user with hosts=all (expect changed)
mysql_user: mysql_user:
<<: *mysql_params <<: *mysql_params
user: "" name: ""
host_all: true host_all: true
state: absent state: absent
register: result register: result
@ -79,7 +79,7 @@
- name: Idempotency | Remove blank user with hosts=all (expect ok) - name: Idempotency | Remove blank user with hosts=all (expect ok)
mysql_user: mysql_user:
<<: *mysql_params <<: *mysql_params
user: "" name: ""
host_all: true host_all: true
state: absent state: absent
register: result register: result

View file

@ -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 }}"

View file

@ -76,14 +76,14 @@
that: that:
- "'SSL' in reqs" - "'SSL' in reqs"
vars: 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()}}" 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 - name: Tls reqs | Assert user2 TLS requirements
assert: assert:
that: that:
- "'X509' in reqs" - "'X509' in reqs"
vars: 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()}}" 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 - name: Tls reqs | Assert user3 TLS requirements
assert: assert:
@ -92,7 +92,7 @@
- "'/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('contains', 'ISSUER') | 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)" - "'ECDHE-ECDSA-AES256-SHA384' in (reqs | select('contains', 'CIPHER') | first)"
vars: 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(\":\")}}" 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. # 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' when: ansible_distribution != 'CentOS' or ansible_distribution_major_version != '6'
@ -129,7 +129,7 @@
assert: assert:
that: "'SSL' in reqs" that: "'SSL' in reqs"
vars: vars:
- reqs: "{{(old_result is skipped | ternary(new_result, old_result)).stdout.split('REQUIRE')[1].split(separator)[0].strip()}}" 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) - name: Tls reqs | Modify user with TLS requirements state=present (expect changed=true)
mysql_user: mysql_user:
@ -157,7 +157,7 @@
assert: assert:
that: "'X509' in reqs" that: "'X509' in reqs"
vars: vars:
- reqs: "{{(old_result is skipped | ternary(new_result, old_result)).stdout.split('REQUIRE')[1].split(separator)[0].strip()}}" 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) - name: Tls reqs | Remove TLS requirements from user (expect changed=true)
mysql_user: mysql_user:

View file

@ -127,3 +127,29 @@
update_password: on_create update_password: on_create
- username: test3 - username: test3
update_password: on_new_username 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"

View file

@ -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 }}"

View file

@ -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

View file

@ -13,6 +13,7 @@
test_plugin_auth_string: 'Fdt8fd^34ds' test_plugin_auth_string: 'Fdt8fd^34ds'
test_plugin_new_hash: '*E74368AC90460FA669F6D41BFB7F2A877DB73745' test_plugin_new_hash: '*E74368AC90460FA669F6D41BFB7F2A877DB73745'
test_plugin_new_auth_string: 'c$K01LsmK7nJnIR4!h' test_plugin_new_auth_string: 'c$K01LsmK7nJnIR4!h'
test_salt: 'TDwqdanU82d0yNtvaabb'
test_default_priv_type: 'SELECT' test_default_priv_type: 'SELECT'
test_default_priv: '*.*:{{ test_default_priv_type }}' test_default_priv: '*.*:{{ test_default_priv_type }}'
@ -23,7 +24,7 @@
# #
- name: Plugin auth | Create user with plugin auth (with hash string) - name: Plugin auth | Create user with plugin auth (with hash string)
mysql_user: community.mysql.mysql_user:
<<: *mysql_params <<: *mysql_params
name: '{{ test_user_name }}' name: '{{ test_user_name }}'
host: '%' host: '%'
@ -33,28 +34,28 @@
register: result register: result
- name: Plugin auth | Get user information (with hash string) - name: Plugin auth | Get user information (with hash string)
command: "{{ mysql_command }} -e \"SELECT user, host, plugin FROM mysql.user WHERE user = '{{ test_user_name }}' and host = '%'\"" ansible.builtin.command: "{{ mysql_command }} -e \"SELECT user, host, plugin FROM mysql.user WHERE user = '{{ test_user_name }}' and host = '%'\""
register: show_create_user register: show_create_user
- name: Plugin auth | Check that the module made a change (with hash string) - name: Plugin auth | Check that the module made a change (with hash string)
assert: ansible.builtin.assert:
that: that:
- result is changed - result is changed
- name: Plugin auth | Check that the expected plugin type is set (with hash string) - name: Plugin auth | Check that the expected plugin type is set (with hash string)
assert: ansible.builtin.assert:
that: that:
- "'{{ test_plugin_type }}' in show_create_user.stdout" - "'{{ test_plugin_type }}' in show_create_user.stdout"
when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>=')) when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>='))
- include_tasks: utils/assert_user.yml - ansible.builtin.include_tasks: utils/assert_user.yml
vars: vars:
user_name: "{{ test_user_name }}" user_name: "{{ test_user_name }}"
user_host: "%" user_host: "%"
priv: "{{ test_default_priv_type }}" priv: "{{ test_default_priv_type }}"
- name: Plugin auth | Get the MySQL version using the newly created creds - name: Plugin auth | Get the MySQL version using the newly created creds
mysql_info: community.mysql.mysql_info:
login_user: '{{ test_user_name }}' login_user: '{{ test_user_name }}'
login_password: '{{ test_plugin_auth_string }}' login_password: '{{ test_plugin_auth_string }}'
login_host: '{{ mysql_host }}' login_host: '{{ mysql_host }}'
@ -63,12 +64,12 @@
register: result register: result
- name: Plugin auth | Assert that mysql_info was successful - name: Plugin auth | Assert that mysql_info was successful
assert: ansible.builtin.assert:
that: that:
- result is succeeded - result is succeeded
- name: Plugin auth | Update the user with a different hash - name: Plugin auth | Update the user with a different hash
mysql_user: community.mysql.mysql_user:
<<: *mysql_params <<: *mysql_params
name: '{{ test_user_name }}' name: '{{ test_user_name }}'
host: '%' host: '%'
@ -77,18 +78,18 @@
register: result register: result
- name: Plugin auth | Check that the module makes the change because the hash changed - name: Plugin auth | Check that the module makes the change because the hash changed
assert: ansible.builtin.assert:
that: that:
- result is changed - result is changed
- include_tasks: utils/assert_user.yml - ansible.builtin.include_tasks: utils/assert_user.yml
vars: vars:
user_name: "{{ test_user_name }}" user_name: "{{ test_user_name }}"
user_host: "%" user_host: "%"
priv: "{{ test_default_priv_type }}" priv: "{{ test_default_priv_type }}"
- name: Plugin auth | Getting the MySQL info with the new password should work - name: Plugin auth | Getting the MySQL info with the new password should work
mysql_info: community.mysql.mysql_info:
login_user: '{{ test_user_name }}' login_user: '{{ test_user_name }}'
login_password: '{{ test_plugin_new_auth_string }}' login_password: '{{ test_plugin_new_auth_string }}'
login_host: '{{ mysql_host }}' login_host: '{{ mysql_host }}'
@ -97,12 +98,12 @@
register: result register: result
- name: Plugin auth | Assert that mysql_info was successful - name: Plugin auth | Assert that mysql_info was successful
assert: ansible.builtin.assert:
that: that:
- result is succeeded - result is succeeded
# Cleanup # Cleanup
- include_tasks: utils/remove_user.yml - ansible.builtin.include_tasks: utils/remove_user.yml
vars: vars:
user_name: "{{ test_user_name }}" user_name: "{{ test_user_name }}"
@ -111,7 +112,7 @@
# #
- name: Plugin auth | Create user with plugin auth (with hash string) - name: Plugin auth | Create user with plugin auth (with hash string)
mysql_user: community.mysql.mysql_user:
<<: *mysql_params <<: *mysql_params
name: '{{ test_user_name }}' name: '{{ test_user_name }}'
host: '%' host: '%'
@ -121,28 +122,28 @@
register: result register: result
- name: Plugin auth | Get user information - name: Plugin auth | Get user information
command: "{{ mysql_command }} -e \"SELECT user, host, plugin FROM mysql.user WHERE user = '{{ test_user_name }}' and host = '%'\"" ansible.builtin.command: "{{ mysql_command }} -e \"SELECT user, host, plugin FROM mysql.user WHERE user = '{{ test_user_name }}' and host = '%'\""
register: show_create_user register: show_create_user
- name: Plugin auth | Check that the module made a change (with hash string) - name: Plugin auth | Check that the module made a change (with hash string)
assert: ansible.builtin.assert:
that: that:
- result is changed - result is changed
- name: Plugin auth | Check that the expected plugin type is set (with hash string) - name: Plugin auth | Check that the expected plugin type is set (with hash string)
assert: ansible.builtin.assert:
that: that:
- "'{{ test_plugin_type }}' in show_create_user.stdout" - "'{{ test_plugin_type }}' in show_create_user.stdout"
when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>=')) when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>='))
- include_tasks: utils/assert_user.yml - ansible.builtin.include_tasks: utils/assert_user.yml
vars: vars:
user_name: "{{ test_user_name }}" user_name: "{{ test_user_name }}"
user_host: "%" user_host: "%"
priv: "{{ test_default_priv_type }}" priv: "{{ test_default_priv_type }}"
- name: Plugin auth | Get the MySQL version using the newly created creds - name: Plugin auth | Get the MySQL version using the newly created creds
mysql_info: community.mysql.mysql_info:
login_user: '{{ test_user_name }}' login_user: '{{ test_user_name }}'
login_password: '{{ test_plugin_auth_string }}' login_password: '{{ test_plugin_auth_string }}'
login_host: '{{ mysql_host }}' login_host: '{{ mysql_host }}'
@ -151,12 +152,12 @@
register: result register: result
- name: Plugin auth | Assert that mysql_info was successful - name: Plugin auth | Assert that mysql_info was successful
assert: ansible.builtin.assert:
that: that:
- result is succeeded - result is succeeded
- name: Plugin auth | Update the user with the same hash (no change expected) - name: Plugin auth | Update the user with the same hash (no change expected)
mysql_user: community.mysql.mysql_user:
<<: *mysql_params <<: *mysql_params
name: '{{ test_user_name }}' name: '{{ test_user_name }}'
host: '%' host: '%'
@ -166,19 +167,19 @@
# FIXME: on mariadb 10.2 there's always a change # FIXME: on mariadb 10.2 there's always a change
- name: Plugin auth | Check that the module doesn't make a change when the same hash is passed in - name: Plugin auth | Check that the module doesn't make a change when the same hash is passed in
assert: ansible.builtin.assert:
that: that:
- result is not changed - result is not changed
when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>=')) when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>='))
- include_tasks: utils/assert_user.yml - ansible.builtin.include_tasks: utils/assert_user.yml
vars: vars:
user_name: "{{ test_user_name }}" user_name: "{{ test_user_name }}"
user_host: "%" user_host: "%"
priv: "{{ test_default_priv_type }}" priv: "{{ test_default_priv_type }}"
- name: Plugin auth | Change the user using the same plugin, but switch to the same auth string in plaintext form - name: Plugin auth | Change the user using the same plugin, but switch to the same auth string in plaintext form
mysql_user: community.mysql.mysql_user:
<<: *mysql_params <<: *mysql_params
name: '{{ test_user_name }}' name: '{{ test_user_name }}'
host: '%' host: '%'
@ -188,12 +189,12 @@
# Expecting a change is currently by design (see comment in source). # Expecting a change is currently by design (see comment in source).
- name: Plugin auth | Check that the module did not change the password - name: Plugin auth | Check that the module did not change the password
assert: ansible.builtin.assert:
that: that:
- result is changed - result is changed
- name: Plugin auth | Getting the MySQL info should still work - name: Plugin auth | Getting the MySQL info should still work
mysql_info: community.mysql.mysql_info:
login_user: '{{ test_user_name }}' login_user: '{{ test_user_name }}'
login_password: '{{ test_plugin_auth_string }}' login_password: '{{ test_plugin_auth_string }}'
login_host: '{{ mysql_host }}' login_host: '{{ mysql_host }}'
@ -202,12 +203,12 @@
register: result register: result
- name: Plugin auth | Assert that mysql_info was successful - name: Plugin auth | Assert that mysql_info was successful
assert: ansible.builtin.assert:
that: that:
- result is succeeded - result is succeeded
# Cleanup # Cleanup
- include_tasks: utils/remove_user.yml - ansible.builtin.include_tasks: utils/remove_user.yml
vars: vars:
user_name: "{{ test_user_name }}" user_name: "{{ test_user_name }}"
@ -216,7 +217,7 @@
# #
- name: Plugin auth | Create user with plugin auth (with auth string) - name: Plugin auth | Create user with plugin auth (with auth string)
mysql_user: community.mysql.mysql_user:
<<: *mysql_params <<: *mysql_params
name: '{{ test_user_name }}' name: '{{ test_user_name }}'
host: '%' host: '%'
@ -226,28 +227,28 @@
register: result register: result
- name: Plugin auth | Get user information(with auth string) - name: Plugin auth | Get user information(with auth string)
command: "{{ mysql_command }} -e \"SHOW CREATE USER '{{ test_user_name }}'@'%'\"" ansible.builtin.command: "{{ mysql_command }} -e \"SHOW CREATE USER '{{ test_user_name }}'@'%'\""
register: show_create_user register: show_create_user
- name: Plugin auth | Check that the module made a change (with auth string) - name: Plugin auth | Check that the module made a change (with auth string)
assert: ansible.builtin.assert:
that: that:
- result is changed - result is changed
- name: Plugin auth | Check that the expected plugin type is set (with auth string) - name: Plugin auth | Check that the expected plugin type is set (with auth string)
assert: ansible.builtin.assert:
that: that:
- test_plugin_type in show_create_user.stdout - test_plugin_type in show_create_user.stdout
when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>=')) when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>='))
- include_tasks: utils/assert_user.yml - ansible.builtin.include_tasks: utils/assert_user.yml
vars: vars:
user_name: "{{ test_user_name }}" user_name: "{{ test_user_name }}"
user_host: "%" user_host: "%"
priv: "{{ test_default_priv_type }}" priv: "{{ test_default_priv_type }}"
- name: Plugin auth | Get the MySQL version using the newly created creds - name: Plugin auth | Get the MySQL version using the newly created creds
mysql_info: community.mysql.mysql_info:
login_user: '{{ test_user_name }}' login_user: '{{ test_user_name }}'
login_password: '{{ test_plugin_auth_string }}' login_password: '{{ test_plugin_auth_string }}'
login_host: '{{ mysql_host }}' login_host: '{{ mysql_host }}'
@ -256,12 +257,12 @@
register: result register: result
- name: Plugin auth | Assert that mysql_info was successful - name: Plugin auth | Assert that mysql_info was successful
assert: ansible.builtin.assert:
that: that:
- result is succeeded - result is succeeded
- name: Plugin auth | Update the user with the same auth string - name: Plugin auth | Update the user with the same auth string
mysql_user: community.mysql.mysql_user:
<<: *mysql_params <<: *mysql_params
name: '{{ test_user_name }}' name: '{{ test_user_name }}'
host: '%' host: '%'
@ -272,18 +273,18 @@
# This is the current expected behavior because there isn't a reliable way to hash the password in the mysql_user # 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. # module in order to be able to compare this password with the stored hash. See the source for more info.
- name: Plugin auth | The module should detect a change even though the password is the same - name: Plugin auth | The module should detect a change even though the password is the same
assert: ansible.builtin.assert:
that: that:
- result is changed - result is changed
- include_tasks: utils/assert_user.yml - ansible.builtin.include_tasks: utils/assert_user.yml
vars: vars:
user_name: "{{ test_user_name }}" user_name: "{{ test_user_name }}"
user_host: "%" user_host: "%"
priv: "{{ test_default_priv_type }}" priv: "{{ test_default_priv_type }}"
- name: Plugin auth | Change the user using the same plugin, but switch to the same auth string in hash form - name: Plugin auth | Change the user using the same plugin, but switch to the same auth string in hash form
mysql_user: community.mysql.mysql_user:
<<: *mysql_params <<: *mysql_params
name: '{{ test_user_name }}' name: '{{ test_user_name }}'
host: '%' host: '%'
@ -292,12 +293,12 @@
register: result register: result
- name: Plugin auth | Check that the module did not change the password - name: Plugin auth | Check that the module did not change the password
assert: ansible.builtin.assert:
that: that:
- result is not changed - result is not changed
- name: Plugin auth | Get the MySQL version using the newly created creds - name: Plugin auth | Get the MySQL version using the newly created creds
mysql_info: community.mysql.mysql_info:
login_user: '{{ test_user_name }}' login_user: '{{ test_user_name }}'
login_password: '{{ test_plugin_auth_string }}' login_password: '{{ test_plugin_auth_string }}'
login_host: '{{ mysql_host }}' login_host: '{{ mysql_host }}'
@ -306,12 +307,12 @@
register: result register: result
- name: Plugin auth | Assert that mysql_info was successful - name: Plugin auth | Assert that mysql_info was successful
assert: ansible.builtin.assert:
that: that:
- result is succeeded - result is succeeded
# Cleanup # Cleanup
- include_tasks: utils/remove_user.yml - ansible.builtin.include_tasks: utils/remove_user.yml
vars: vars:
user_name: "{{ test_user_name }}" user_name: "{{ test_user_name }}"
@ -320,7 +321,7 @@
# #
- name: Plugin auth | Create user with plugin auth (empty auth string) - name: Plugin auth | Create user with plugin auth (empty auth string)
mysql_user: community.mysql.mysql_user:
<<: *mysql_params <<: *mysql_params
name: '{{ test_user_name }}' name: '{{ test_user_name }}'
host: '%' host: '%'
@ -329,28 +330,28 @@
register: result register: result
- name: Plugin auth | Get user information (empty auth string) - name: Plugin auth | Get user information (empty auth string)
command: "{{ mysql_command }} -e \"SHOW CREATE USER '{{ test_user_name }}'@'%'\"" ansible.builtin.command: "{{ mysql_command }} -e \"SHOW CREATE USER '{{ test_user_name }}'@'%'\""
register: show_create_user register: show_create_user
- name: Plugin auth | Check that the module made a change (empty auth string) - name: Plugin auth | Check that the module made a change (empty auth string)
assert: ansible.builtin.assert:
that: that:
- result is changed - result is changed
- name: Plugin auth | Check that the expected plugin type is set (empty auth string) - name: Plugin auth | Check that the expected plugin type is set (empty auth string)
assert: ansible.builtin.assert:
that: that:
- "'{{ test_plugin_type }}' in show_create_user.stdout" - "'{{ test_plugin_type }}' in show_create_user.stdout"
when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>=')) when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>='))
- include_tasks: utils/assert_user.yml - ansible.builtin.include_tasks: utils/assert_user.yml
vars: vars:
user_name: "{{ test_user_name }}" user_name: "{{ test_user_name }}"
user_host: "%" user_host: "%"
priv: "{{ test_default_priv_type }}" priv: "{{ test_default_priv_type }}"
- name: Plugin auth | Get the MySQL version using an empty password for the newly created user - name: Plugin auth | Get the MySQL version using an empty password for the newly created user
mysql_info: community.mysql.mysql_info:
login_user: '{{ test_user_name }}' login_user: '{{ test_user_name }}'
login_password: '' login_password: ''
login_host: '{{ mysql_host }}' login_host: '{{ mysql_host }}'
@ -360,12 +361,12 @@
ignore_errors: true ignore_errors: true
- name: Plugin auth | Assert that mysql_info was successful - name: Plugin auth | Assert that mysql_info was successful
assert: ansible.builtin.assert:
that: that:
- result is succeeded - result is succeeded
- name: Plugin auth | Get the MySQL version using an non-empty password (should fail) - name: Plugin auth | Get the MySQL version using an non-empty password (should fail)
mysql_info: community.mysql.mysql_info:
login_user: '{{ test_user_name }}' login_user: '{{ test_user_name }}'
login_password: 'some_password' login_password: 'some_password'
login_host: '{{ mysql_host }}' login_host: '{{ mysql_host }}'
@ -375,12 +376,12 @@
ignore_errors: true ignore_errors: true
- name: Plugin auth | Assert that mysql_info failed - name: Plugin auth | Assert that mysql_info failed
assert: ansible.builtin.assert:
that: that:
- result is failed - result is failed
- name: Plugin auth | Update the user without changing the auth mechanism - name: Plugin auth | Update the user without changing the auth mechanism
mysql_user: community.mysql.mysql_user:
<<: *mysql_params <<: *mysql_params
name: '{{ test_user_name }}' name: '{{ test_user_name }}'
host: '%' host: '%'
@ -389,12 +390,12 @@
register: result register: result
- name: Plugin auth | Assert that the user wasn't changed because the auth string is still empty - name: Plugin auth | Assert that the user wasn't changed because the auth string is still empty
assert: ansible.builtin.assert:
that: that:
- result is not changed - result is not changed
# Cleanup # Cleanup
- include_tasks: utils/remove_user.yml - ansible.builtin.include_tasks: utils/remove_user.yml
vars: vars:
user_name: "{{ test_user_name }}" user_name: "{{ test_user_name }}"
@ -414,7 +415,7 @@
block: block:
- name: Plugin auth | Create user with plugin auth (empty auth string) - name: Plugin auth | Create user with plugin auth (empty auth string)
mysql_user: community.mysql.mysql_user:
<<: *mysql_params <<: *mysql_params
name: '{{ test_user_name }}' name: '{{ test_user_name }}'
plugin: '{{ test_plugin_type }}' plugin: '{{ test_plugin_type }}'
@ -422,28 +423,28 @@
register: result register: result
- name: Plugin auth | Get user information (empty auth string) - name: Plugin auth | Get user information (empty auth string)
command: "{{ mysql_command }} -e \"SHOW CREATE USER '{{ test_user_name }}'@'localhost'\"" ansible.builtin.command: "{{ mysql_command }} -e \"SHOW CREATE USER '{{ test_user_name }}'@'localhost'\""
register: show_create_user register: show_create_user
- name: Plugin auth | Check that the module made a change (empty auth string) - name: Plugin auth | Check that the module made a change (empty auth string)
assert: ansible.builtin.assert:
that: that:
- result is changed - result is changed
- name: Plugin auth | Check that the expected plugin type is set (empty auth string) - name: Plugin auth | Check that the expected plugin type is set (empty auth string)
assert: ansible.builtin.assert:
that: that:
- test_plugin_type in show_create_user.stdout - test_plugin_type in show_create_user.stdout
when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>=')) when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>='))
- include_tasks: utils/assert_user.yml - ansible.builtin.include_tasks: utils/assert_user.yml
vars: vars:
user_name: "{{ test_user_name }}" user_name: "{{ test_user_name }}"
user_host: localhost user_host: localhost
priv: "{{ test_default_priv_type }}" priv: "{{ test_default_priv_type }}"
- name: Plugin auth | Switch user to sha256_password auth plugin - name: Plugin auth | Switch user to sha256_password auth plugin
mysql_user: community.mysql.mysql_user:
<<: *mysql_params <<: *mysql_params
name: '{{ test_user_name }}' name: '{{ test_user_name }}'
plugin: sha256_password plugin: sha256_password
@ -451,27 +452,188 @@
register: result register: result
- name: Plugin auth | Get user information (sha256_password) - name: Plugin auth | Get user information (sha256_password)
command: "{{ mysql_command }} -e \"SHOW CREATE USER '{{ test_user_name }}'@'localhost'\"" ansible.builtin.command: "{{ mysql_command }} -e \"SHOW CREATE USER '{{ test_user_name }}'@'localhost'\""
register: show_create_user register: show_create_user
- name: Plugin auth | Check that the module made a change (sha256_password) - name: Plugin auth | Check that the module made a change (sha256_password)
assert: ansible.builtin.assert:
that: that:
- result is changed - result is changed
- name: Plugin auth | Check that the expected plugin type is set (sha256_password) - name: Plugin auth | Check that the expected plugin type is set (sha256_password)
assert: ansible.builtin.assert:
that: that:
- "'sha256_password' in show_create_user.stdout" - "'sha256_password' in show_create_user.stdout"
when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>=')) when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>='))
- include_tasks: utils/assert_user.yml - ansible.builtin.include_tasks: utils/assert_user.yml
vars: vars:
user_name: "{{ test_user_name }}" user_name: "{{ test_user_name }}"
user_host: localhost user_host: localhost
priv: "{{ test_default_priv_type }}" priv: "{{ test_default_priv_type }}"
# Cleanup # Cleanup
- include_tasks: utils/remove_user.yml - ansible.builtin.include_tasks: utils/remove_user.yml
vars: vars:
user_name: "{{ test_user_name }}" 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

View file

@ -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

View file

@ -1,6 +1,6 @@
--- ---
- name: Utils | Assert user password | Apply update_password to {{ username }} - name: Utils | Assert user password | Apply update_password to {{ username }}
mysql_user: community.mysql.mysql_user:
login_user: '{{ mysql_parameters.login_user }}' login_user: '{{ mysql_parameters.login_user }}'
login_password: '{{ mysql_parameters.login_password }}' login_password: '{{ mysql_parameters.login_password }}'
login_host: '{{ mysql_parameters.login_host }}' login_host: '{{ mysql_parameters.login_host }}'
@ -13,16 +13,17 @@
register: result register: result
- name: Utils | Assert user password | Assert a change occurred - name: Utils | Assert user password | Assert a change occurred
assert: ansible.builtin.assert:
that: that:
- "result.changed | bool == {{ expect_change }} | bool" - result.changed | bool == expect_change | bool
- "result.password_changed == {{ expect_password_change }}" - result.password_changed == expect_password_change
- name: Utils | Assert user password | Query user {{ username }} - name: Utils | Assert user password | Assert expect_hash is in user stdout for {{ username }}
command: "{{ mysql_command }} -BNe \"SELECT plugin, authentication_string FROM mysql.user where user='{{ username }}' and host='{{ host }}'\"" ansible.builtin.command: >-
{{ mysql_command }} -BNe "SELECT plugin, authentication_string
FROM mysql.user where user='{{ username }}' and host='{{ host }}'"
register: existing_user register: existing_user
changed_when: false
- name: Utils | Assert user password | Assert expect_hash is in user stdout failed_when: pattern not in existing_user.stdout_lines
assert: vars:
that: pattern: "mysql_native_password\t{{ expect_password_hash }}"
- "'mysql_native_password\t{{ expect_password_hash }}' in existing_user.stdout_lines"

View file

@ -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 }}"

View file

@ -1,8 +1,11 @@
--- ---
- name: set fact tls_enabled - 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 register: result
- set_fact:
- name: Set tls_enabled fact
ansible.builtin.set_fact:
tls_enabled: "{{ 'YES' in result.stdout | bool | default('false', true) }}" tls_enabled: "{{ 'YES' in result.stdout | bool | default('false', true) }}"
- vars: - vars:
@ -16,21 +19,21 @@
# ============================================================ # ============================================================
- name: get server certificate - name: get server certificate
copy: ansible.builtin.copy:
content: "{{ lookup('pipe', \"openssl s_client -starttls mysql -connect localhost:3307 -showcerts 2>/dev/null </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p'\") }}" content: "{{ lookup('pipe', \"openssl s_client -starttls mysql -connect localhost:3307 -showcerts 2>/dev/null </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p'\") }}"
dest: /tmp/cert.pem dest: /tmp/cert.pem
delegate_to: localhost delegate_to: localhost
- name: Drop mysql user if exists - name: Drop mysql user if exists
mysql_user: community.mysql.mysql_user:
<<: *mysql_params <<: *mysql_params
name: '{{ user_name_1 }}' name: '{{ user_name_1 }}'
host_all: true host_all: true
state: absent state: absent
ignore_errors: yes ignore_errors: true
- name: create user with ssl requirement - name: create user with ssl requirement
mysql_user: community.mysql.mysql_user:
<<: *mysql_params <<: *mysql_params
name: "{{ user_name_1 }}" name: "{{ user_name_1 }}"
host: '%' host: '%'
@ -40,7 +43,7 @@
SSL: SSL:
- name: attempt connection with newly created user (expect failure) - name: attempt connection with newly created user (expect failure)
mysql_variables: community.mysql.mysql_variables:
variable: '{{ set_name }}' variable: '{{ set_name }}'
login_user: '{{ user_name_1 }}' login_user: '{{ user_name_1 }}'
login_password: '{{ user_password_1 }}' login_password: '{{ user_password_1 }}'
@ -48,22 +51,24 @@
login_port: '{{ mysql_primary_port }}' login_port: '{{ mysql_primary_port }}'
ca_cert: /tmp/cert.pem ca_cert: /tmp/cert.pem
register: result register: result
ignore_errors: yes ignore_errors: true
- assert: - name: Assert that result is failed for pymysql
ansible.builtin.assert:
that: that:
- result is failed - result is failed
when: when:
- connector_name == 'pymysql' - connector_name == 'pymysql'
- assert: - name: Assert that result is success for mysqlclient
ansible.builtin.assert:
that: that:
- result is succeeded - result is succeeded
when: when:
- connector_name != 'pymysql' - connector_name != 'pymysql'
- name: attempt connection with newly created user ignoring hostname - name: attempt connection with newly created user ignoring hostname
mysql_variables: community.mysql.mysql_variables:
variable: '{{ set_name }}' variable: '{{ set_name }}'
login_user: '{{ user_name_1 }}' login_user: '{{ user_name_1 }}'
login_password: '{{ user_password_1 }}' login_password: '{{ user_password_1 }}'
@ -72,14 +77,12 @@
ca_cert: /tmp/cert.pem ca_cert: /tmp/cert.pem
check_hostname: no check_hostname: no
register: result register: result
ignore_errors: yes ignore_errors: true
failed_when:
- assert: - result is failed or 'pymysql >= 0.7.11 is required' not in result.msg
that:
- result is succeeded or 'pymysql >= 0.7.11 is required' in result.msg
- name: Drop mysql user - name: Drop mysql user
mysql_user: community.mysql.mysql_user:
<<: *mysql_params <<: *mysql_params
name: '{{ user_name_1 }}' name: '{{ user_name_1 }}'
host_all: true host_all: true

View file

@ -287,6 +287,99 @@
var_name: "{{set_name}}" var_name: "{{set_name}}"
var_value: '{{set_value}}' 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 # Verify mysql_variable fails with an incorrect login_password parameter
# #

View file

@ -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

View file

@ -1,10 +1,3 @@
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_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
plugins/module_utils/mysql.py pylint:unused-import plugins/module_utils/mysql.py pylint:unused-import
plugins/module_utils/version.py pylint:unused-import plugins/module_utils/version.py pylint:unused-import

View file

@ -1,10 +1,3 @@
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_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
plugins/module_utils/mysql.py pylint:unused-import plugins/module_utils/mysql.py pylint:unused-import
plugins/module_utils/version.py pylint:unused-import plugins/module_utils/version.py pylint:unused-import

View file

@ -1,10 +1,3 @@
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_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
plugins/module_utils/mysql.py pylint:unused-import plugins/module_utils/mysql.py pylint:unused-import
plugins/module_utils/version.py pylint:unused-import plugins/module_utils/version.py pylint:unused-import

View file

@ -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

View file

@ -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

View file

@ -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

Some files were not shown because too many files have changed in this diff Show more