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
on:
on: # yamllint disable-line rule:truthy
push:
paths:
- 'plugins/**'
@ -13,20 +13,21 @@ on:
- '.github/workflows/ansible-test-plugins.yml'
schedule:
- cron: '0 6 * * *'
workflow_dispatch:
jobs:
sanity:
name: "Sanity (Ansible: ${{ matrix.ansible }})"
runs-on: ubuntu-20.04
name: "Sanity (${{ matrix.ansible }})"
runs-on: ubuntu-22.04
strategy:
matrix:
ansible:
- stable-2.14
- stable-2.15
- stable-2.16
- stable-2.17
- stable-2.18
- devel
steps:
# https://github.com/ansible-community/ansible-test-gh-action
- name: Perform sanity testing
uses: ansible-community/ansible-test-gh-action@release/v1
with:
@ -34,166 +35,134 @@ jobs:
testing-type: sanity
pull-request-change-detection: true
# Use this to chose which version of Python vs Ansible to test:
# https://docs.ansible.com/ansible/latest/reference_appendices/release_and_maintenance.html#ansible-core-control-node-python-support
integration:
name: "Integration (Python: ${{ matrix.python }}, Ansible: ${{ matrix.ansible }}, DB: ${{ matrix.db_engine_name }} ${{ matrix.db_engine_version }}, connector: ${{ matrix.connector_name }} ${{ matrix.connector_version }})"
runs-on: ubuntu-20.04
name: "Integration (${{ matrix.ansible }}, DB: ${{ matrix.db_engine_name }} ${{ matrix.db_engine_version }}, connector: ${{ matrix.connector_name }} ${{ matrix.connector_version }})"
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
ansible:
- stable-2.14
- stable-2.15
- stable-2.16
- stable-2.17
- stable-2.18
- devel
db_engine_name:
- mysql
- mariadb
db_engine_version:
- 5.7.40
- 8.0.31
- 10.4.27
- 10.5.18
- 10.6.11
python:
- '3.8'
- '3.9'
- '3.10'
- '8.0.38'
- '8.4.1'
- '10.11.8'
- '11.4.5'
connector_name:
- pymysql
- mysqlclient
connector_version:
- 0.7.11
- 0.9.3
- 1.0.2
- 2.0.1
- 2.0.3
- 2.1.1
- '0.9.3'
- '1.0.2'
- '1.1.1'
- '2.0.1'
- '2.0.3'
- '2.1.1'
include:
# RHEL8 context
- connector_name: pymysql
connector_version: '0.10.1'
ansible: stable-2.16
db_engine_name: mariadb
db_engine_version: '10.11.8'
# RHEL9 context
# - connector_name: pymysql
# connector_version: '1.1.1'
# ansible: stable-2.17
# db_engine_name: mariadb
# db_engine_version: '10.11.8'
# This tests is already included in the matrix, no need repeating
exclude:
- db_engine_name: mysql
db_engine_version: 10.4.27
- db_engine_name: mysql
db_engine_version: 10.5.18
db_engine_version: '10.11.8'
- db_engine_name: mysql
db_engine_version: 10.6.11
db_engine_version: '11.4.5'
- db_engine_name: mariadb
db_engine_version: 5.7.40
db_engine_version: '8.0.38'
- db_engine_name: mariadb
db_engine_version: 8.0.31
db_engine_version: '8.4.1'
- connector_name: pymysql
connector_version: 2.0.1
connector_version: '2.0.1'
- connector_name: pymysql
connector_version: 2.0.3
connector_version: '2.0.3'
- connector_name: pymysql
connector_version: 2.1.1
connector_version: '2.1.1'
- connector_name: mysqlclient
connector_version: 0.7.11
connector_version: '0.9.3'
- connector_name: mysqlclient
connector_version: 0.9.3
connector_version: '1.0.2'
- connector_name: mysqlclient
connector_version: 1.0.2
connector_version: '1.1.1'
- db_engine_name: mariadb
connector_version: 0.7.11
- db_engine_version: '8.0.38'
ansible: stable-2.17
- db_engine_version: 5.7.40
python: '3.9'
- db_engine_version: '10.11.8'
ansible: stable-2.17
- db_engine_version: 5.7.40
python: '3.10'
- db_engine_version: '8.0.38'
ansible: devel
- db_engine_version: 5.7.40
ansible: stable-2.14
- db_engine_version: '10.11.8'
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
- db_engine_version: 5.7.40
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'
- db_engine_version: '8.4.1'
ansible: stable-2.15
- python: '3.8'
ansible: stable-2.16
- connector_version: '1.1.1'
db_engine_version: '8.0.38'
- python: '3.8'
ansible: devel
- python: '3.9'
ansible: stable-2.15
- python: '3.9'
ansible: stable-2.16
- python: '3.9'
ansible: devel
- connector_version: '1.1.1'
db_engine_version: '10.11.8'
services:
db_primary:
@ -206,7 +175,7 @@ jobs:
# We write our own health-cmd because the mariadb container does not
# provide a healthcheck
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-interval 10s
--health-timeout 5s
@ -220,7 +189,7 @@ jobs:
ports:
- 3308:3306
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-interval 10s
--health-timeout 5s
@ -234,7 +203,7 @@ jobs:
ports:
- 3309:3306
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-interval 10s
--health-timeout 5s
@ -246,9 +215,22 @@ jobs:
- name: Restart MySQL server with settings for replication
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'
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'
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'
db_ver="${{ matrix.db_engine_version }}"
maj="${db_ver%.*.*}"
maj_min="${db_ver%.*}"
min="${maj_min#*.}"
if [[ "${{ matrix.db_engine_name }}" == "mysql" && "$maj" -eq 8 && "$min" -ge 2 ]]; then
prima_conf='[mysqld]\\nserver-id=1\\nlog-bin=/var/lib/mysql/primary-bin\\nmysql-native-password=1'
repl1_conf='[mysqld]\\nserver-id=2\\nlog-bin=/var/lib/mysql/replica1-bin\\nmysql-native-password=1'
repl2_conf='[mysqld]\\nserver-id=3\\nlog-bin=/var/lib/mysql/replica2-bin\\nmysql-native-password=1'
else
prima_conf='[mysqld]\\nserver-id=1\\nlog-bin=/var/lib/mysql/primary-bin'
repl1_conf='[mysqld]\\nserver-id=2\\nlog-bin=/var/lib/mysql/replica1-bin'
repl2_conf='[mysqld]\\nserver-id=3\\nlog-bin=/var/lib/mysql/replica2-bin'
fi
docker exec -e cnf=$prima_conf ${{ job.services.db_primary.id }} bash -c 'echo -e ${cnf//\\n/\n} > /etc/mysql/conf.d/replication.cnf'
docker exec -e cnf=$repl1_conf ${{ job.services.db_replica1.id }} bash -c 'echo -e ${cnf//\\n/\n} > /etc/mysql/conf.d/replication.cnf'
docker exec -e cnf=$repl2_conf ${{ job.services.db_replica2.id }} bash -c 'echo -e ${cnf//\\n/\n} > /etc/mysql/conf.d/replication.cnf'
docker restart -t 30 ${{ job.services.db_primary.id }}
docker restart -t 30 ${{ job.services.db_replica1.id }}
docker restart -t 30 ${{ job.services.db_replica2.id }}
@ -260,48 +242,13 @@ jobs:
${{ job.services.db_primary.id }}
| 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: >-
Perform integration testing against
Ansible version ${{ matrix.ansible }}
under Python ${{ matrix.python }}
uses: ansible-community/ansible-test-gh-action@release/v1
with:
ansible-core-version: ${{ matrix.ansible }}
docker-image: ubuntu2204
pre-test-cmd: >-
echo Setting db_engine_name to "${{ matrix.db_engine_name }}"...;
echo -n "${{ matrix.db_engine_name }}"
@ -320,47 +267,66 @@ jobs:
echo -n "${{ matrix.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 -n "${{ matrix.ansible }}"
> tests/integration/ansible
docker-image: ${{ env.docker_image }}
target-python-version: ${{ matrix.python }}
testing-type: integration
integration-retry-on-error: false
units:
runs-on: ubuntu-20.04
name: Units (Ⓐ${{ matrix.ansible }})
runs-on: ubuntu-22.04
name: Units (Ⓐ${{ matrix.ansible }}, Python${{ matrix.python }})
strategy:
# As soon as the first unit test fails,
# cancel the others to free up the CI queue
fail-fast: true
matrix:
ansible:
- stable-2.14
- stable-2.15
- stable-2.16
- stable-2.17
- stable-2.18
- devel
python:
- 3.8
- 3.9
- '3.8'
- '3.9'
- '3.10'
- '3.11'
exclude:
- python: '3.8'
ansible: stable-2.14
- python: '3.8'
ansible: stable-2.15
- python: '3.8'
ansible: stable-2.16
- python: '3.8'
ansible: stable-2.17
- python: '3.8'
ansible: devel
- python: '3.9'
ansible: stable-2.15
- python: '3.9'
ansible: stable-2.17
- python: '3.9'
ansible: devel
- python: '3.10'
ansible: stable-2.15
- python: '3.10'
ansible: stable-2.16
- python: '3.11'
ansible: stable-2.15
- python: '3.11'
ansible: stable-2.16
steps:
- name: >-
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
with:
ansible-core-version: ${{ matrix.ansible }}

View file

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

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

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

View file

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

View file

@ -1,5 +1,5 @@
# MySQL collection for Ansible
[![Plugins CI](https://github.com/ansible-collections/community.mysql/workflows/Plugins%20CI/badge.svg?event=push)](https://github.com/ansible-collections/community.mysql/actions?query=workflow%3A"Plugins+CI") [![Roles CI](https://github.com/ansible-collections/community.mysql/workflows/Roles%20CI/badge.svg?event=push)](https://github.com/ansible-collections/community.mysql/actions?query=workflow%3A"Roles+CI") [![Codecov](https://img.shields.io/codecov/c/github/ansible-collections/community.mysql)](https://codecov.io/gh/ansible-collections/community.mysql) [![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)
# MySQL and MariaDB collection for Ansible
[![Plugins CI](https://github.com/ansible-collections/community.mysql/workflows/Plugins%20CI/badge.svg?event=push)](https://github.com/ansible-collections/community.mysql/actions?query=workflow%3A"Plugins+CI") [![Codecov](https://img.shields.io/codecov/c/github/ansible-collections/community.mysql)](https://codecov.io/gh/ansible-collections/community.mysql) [![Discuss on Matrix at #mysql:ansible.com](https://img.shields.io/matrix/mysql:ansible.com.svg?server_fqdn=ansible-accounts.ems.host&label=Discuss%20on%20Matrix%20at%20%23mysql:ansible.com&logo=matrix)](https://matrix.to/#/#mysql:ansible.com)
This collection is a part of the Ansible package.
@ -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.
## Communication
* Join the Ansible forum:
* [Get Help](https://forum.ansible.com/c/help/6): get help or help others.
* [Posts tagged with 'mysql'](https://forum.ansible.com/tag/mysql): leverage tags to narrow the scope.
* [MySQL Team](https://forum.ansible.com/g/MySQLTeam): by joining the team you will automatically get subscribed to the posts tagged with [mysql](https://forum.ansible.com/tag/mysql).
* [Social Spaces](https://forum.ansible.com/c/chat/4): gather and interact with fellow enthusiasts.
* [News & Announcements](https://forum.ansible.com/c/news/5): track project-wide announcements including social events.
* The Ansible [Bullhorn newsletter](https://docs.ansible.com/ansible/devel/community/communication.html#the-bullhorn): used to announce releases and important changes.
* Matrix chat:
* [#mysql:ansible.com](https://matrix.to/#/#mysql:ansible.com) room: questions on how to contribute to this collection.
For more information about communication, see the [Ansible communication guide](https://docs.ansible.com/ansible/devel/community/communication.html).
## Contributing
The content of this collection is made by [people](https://github.com/ansible-collections/community.mysql/blob/main/CONTRIBUTORS) just like you, a community of individuals collaborating on making the world better through developing automation software.
@ -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).
## 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
We, [the MySQL team](https://forum.ansible.com/g/MySQLTeam), use [the forum](https://forum.ansible.com/tag/mysql) posts tagged with `mysql` for general announcements and discussions.
The process of decision making in this collection is based on discussing and finding consensus among participants.
Every voice is important and every idea is valuable. If you have something on your mind, create an issue or dedicated discussion and let's discuss it!
Every voice is important and every idea is valuable. If you have something on your mind, create an issue or dedicated forum [discussion](https://forum.ansible.com/new-topic?title=topic%20title&body=topic%20body&category=project&tags=mysql) and let's discuss it!
## Included content
@ -68,12 +76,12 @@ Every voice is important and every idea is valuable. If you have something on yo
## 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:
- 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
- 4.x.y: To be released
@ -82,40 +90,46 @@ Here is the table for the support timeline:
### ansible-core
- stable-2.14
- stable-2.15
- stable-2.16
- stable-2.17
- stable-2.18
- current development version
### Python
- 3.8 (Unit tests only)
- 3.9 (Unit tests only)
- 3.10 (Sanity, Units and integrations tests)
- 3.11 (Unit tests only, collection version >= 3.10.0)
### Databases
For MariaDB, only Long Term releases are tested.
- 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)
For MariaDB, only Long Term releases are tested. When multiple LTS are available, we test the oldest and the newest only. Usually breaking changes introduced in the versions in between are also present in the latest version.
- mysql 5.7.40 (collection version < 3.10.0)
- mysql 8.0.31 (collection version < 3.10.0)
- mysql 8.4.1 (collection version >= 3.10.0) !!! FAILING, no support yet !!!
- mariadb:10.3.34 (collection version < 3.5.1)
- mariadb:10.4.24 (collection version >= 3.5.2, < 3.10.0)
- mariadb:10.5.18 (collection version >= 3.5.2, < 3.10.0)
- mariadb:10.5.25 (collection version >= 3.10.0, <3.13.0)
- mariadb:10.6.11 (collection version >= 3.5.2, < 3.10.0)
- mariadb:10.11.8 (collection version >= 3.10.0)
- mariadb:11.4.5 (collection version >= 3.13.0)
### Database connectors
- pymysql 0.7.11 (Only tested with MySQL 5.7)
- pymysql 0.7.11 (collection version < 3.10 and MySQL 5.7)
- pymysql 0.9.3
- pymysql 1.0.2 (only collection version >= 3.6.1)
- mysqlclient 2.0.1
- mysqlclient 2.0.3 (only collection version >= 3.5.2)
- mysqlclient 2.1.1 (only collection version >= 3.5.2)
- pymysql 0.10.1 (for RHEL8 context)
- pymysql 1.0.2 (collection version >= 3.6.1)
- pymysql 1.1.1 (collection version >= 3.10.0)
## External requirements
The MySQL modules rely on a MySQL connector. The list of supported drivers is below:
The MySQL modules rely on a [PyMySQL](https://github.com/PyMySQL/PyMySQL) connector.
- [PyMySQL](https://github.com/PyMySQL/PyMySQL)
- [mysqlclient](https://github.com/PyMySQL/mysqlclient)
- Support for other Python MySQL connectors may be added in a future release.
The `mysqlclient` connector support has been [deprecated](https://github.com/ansible-collections/community.mysql/issues/654) - use `PyMySQL` connector instead! We will stop testing against it in collection version 4.0.0 and remove the related code in 5.0.0.
## Using this collection

View file

@ -19,19 +19,16 @@ For now, the makefile only supports Podman.
### Requirements
- python >= 3.8 and <= 3.10
- python >= 3.8
- make
- podman
- Minimum 15GB of free space on the device storing containers images and volumes. You can use this command to check: `podman system info --format='{{.Store.GraphRoot}}'|xargs findmnt --noheadings --nofsroot --output SOURCE --target|xargs df -h --output=size,used,avail,pcent,target`
- Minimum 2GB of RAM
### 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.:
`ghcr.io/ansible-collections/community.mysql/test-container-mariadb106-py310-mysqlclient211:latest`.
Availables images are listed [here](https://github.com/orgs/ansible-collections/packages).
Integration tests use the default container from ansible-test. Then required packages for the tests are installed from the `setup_controller` target located in the `tests/integration/targets` folder.
### Makefile options
@ -44,16 +41,15 @@ The Makefile accept the following options
- "3.8"
- "3.9"
- "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`
- Mandatory: true
- Choices:
- "stable-2.12"
- "stable-2.13"
- "stable-2.14"
- "stable-2.15"
- "stable-2.16"
- "stable-2.17"
- "devel"
- 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`
- Mandatory: true
- Choices:
- "5.7.40" <- mysql
- "8.0.31" <- mysql
- "10.4.24" <- mariadb
- "10.5.18" <- mariadb
- "10.6.11" <- mariadb
- "8.0.38" <- mysql
- "8.4.1" <- mysql (NOT WORKING YET, ansible-test uses Ubuntu 20.04 which is too old to install mysql-community-client 8.4)
- "10.11.8" <- mariadb
- "11.4.5" <- mariadb
- Description: The tag of the container to use for the service containers that will host a primary database and two replicas. Do not use short version, like `mysql:8` (don't do that) because our tests expect a full version to filter tests precisely. For instance: `when: db_version is version ('8.0.22', '>')`. You can use any tag available on [hub.docker.com/_/mysql](https://hub.docker.com/_/mysql) and [hub.docker.com/_/mariadb](https://hub.docker.com/_/mariadb) but GitHub Action will only use the versions listed above.
- `connector_name`
- Mandatory: true
- Choices:
- "pymysql
- "pymysql"
- "mysqlclient"
- Description: The python package of the connector to use. In addition to selecting the test container, this value is also used for tests filtering: `when: connector_name == 'pymysql'`.
- `connector_version`
- Mandatory: true
- Choices:
- "0.7.11" <- pymysql (Only for MySQL 5.7)
- "0.9.3" <- pymysql
- "0.10.1" <- pymysql
- "1.0.2" <- pymysql
- "2.0.1" <- mysqlclient
- "2.0.3" <- mysqlclient
- "2.1.1" <- mysqlclient
- "1.1.1" <- pymysql
- Description: The version of the python package of the connector to use. This value is used to filter tests meant for other connectors.
- `python`
- Mandatory: true
- Choices:
- "3.8"
- "3.9"
- "3.10"
- Description: The python version to use in the controller (ansible-test container).
- `target`
- Mandatory: false
- Choices:
@ -119,30 +104,30 @@ tests will overwrite the 3 databases containers so no need to kill them in advan
- `continue_on_errors`
- 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:
```sh
# 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
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
# 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:
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
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:
@ -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)
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".
Be careful to not add too much tests. The matrix creates an exponential number of virtual machines!

View file

@ -97,6 +97,176 @@ releases:
- 307-mysql_user_add_if_exists_to_drop.yml
- 329-mysql_role-remove-redudant-connection-closing.yml
release_date: '2022-04-26'
3.10.0:
changes:
bugfixes:
- mysql_info - Add ``plugin_hash_string`` to ``users_info`` filter's output.
The existing ``plugin_auth_string`` contained the hashed password and thus
is missleading, it will be removed from community.mysql 4.0.0. (https://github.com/ansible-collections/community.mysql/pull/629).
- mysql_user - Added a warning to update_password's on_new_username option if
multiple accounts with the same username but different passwords exist (https://github.com/ansible-collections/community.mysql/pull/642).
- mysql_user - Fix ``tls_requires`` not removing ``SSL`` and ``X509`` when sets
as empty (https://github.com/ansible-collections/community.mysql/pull/628).
- mysql_user - Fix idempotence when using variables from the ``users_info``
filter of ``mysql_info`` as an input (https://github.com/ansible-collections/community.mysql/pull/628).
- mysql_user - Fixed an IndexError in the update_password functionality introduced
in PR https://github.com/ansible-collections/community.mysql/pull/580 and
released in community.mysql 3.8.0. If you used this functionality, please
avoid versions 3.8.0 to 3.9.0 (https://github.com/ansible-collections/community.mysql/pull/642).
- mysql_user - add correct ``ed25519`` auth plugin handling (https://github.com/ansible-collections/community.mysql/issues/6).
- mysql_variables - fix the module always changes on boolean values (https://github.com/ansible-collections/community.mysql/issues/652).
deprecated_features:
- collection - support of mysqlclient connector is deprecated - use PyMySQL
connector instead! We will stop testing against it in collection version 4.0.0
and remove the related code in 5.0.0 (https://github.com/ansible-collections/community.mysql/issues/654).
- mysql_info - The ``users_info`` filter returned variable ``plugin_auth_string``
contains the hashed password and it's misleading, it will be removed from
community.mysql 4.0.0. Use the `plugin_hash_string` return value instead (https://github.com/ansible-collections/community.mysql/pull/629).
minor_changes:
- mysql_info - Add ``tls_requires`` returned value for the ``users_info`` filter
(https://github.com/ansible-collections/community.mysql/pull/628).
- mysql_info - return a database server engine used (https://github.com/ansible-collections/community.mysql/issues/644).
- mysql_replication - Adds support for `CHANGE REPLICATION SOURCE TO` statement
(https://github.com/ansible-collections/community.mysql/issues/635).
- mysql_replication - Adds support for `SHOW BINARY LOG STATUS` and `SHOW BINLOG
STATUS` on getprimary mode.
- mysql_replication - Improve detection of IsReplica and IsPrimary by inspecting
the dictionary returned from the SQL query instead of relying on variable
types. This ensures compatibility with changes in the connector or the output
of SHOW REPLICA STATUS and SHOW MASTER STATUS, allowing for easier maintenance
if these change in the future.
- mysql_user - Add salt parameter to generate static hash for `caching_sha2_password`
and `sha256_password` plugins.
release_summary: 'This is a minor release of the ``community.mysql`` collection.
This changelog contains all changes to the modules and plugins in this
collection that have been made after the previous release.'
fragments:
- 0-mysql_user.yml
- 1-mysql_info.yml
- 2-mysql_variables.yml
- 3-deprecate_mysqlclient.yml
- 3.10.0.yml
- add_salt_param_to_gen_sha256_hash.yml
- get_primary_show_binary_log_status.yml
- improve_get_replica_primary_status.yml
- lie_fix_mysql_user_on_new_username.yml
- lie_fix_plugin_hash_string_return.yml
- mysql_user_tls_requires.yml
- supports_mysql_change_replication_source_to.yml
release_date: '2024-08-22'
3.10.1:
changes:
bugfixes:
- mysql_user - module makes changes when is executed with ``plugin_auth_string``
parameter and check mode.
deprecated_features:
- mysql_user - the ``user`` alias of the ``name`` argument has been deprecated
and will be removed in collection version 5.0.0. Use the ``name`` argument
instead.
release_summary: 'This is a patch release of the ``community.mysql`` collection.
Besides a bugfix, it contains an important upcoming breaking-change information.'
fragments:
- 0-mysql_user.yml
- 3.10.1.yml
- 596-fix-check-changes.yaml
release_date: '2024-09-04'
3.10.2:
changes:
bugfixes:
- mysql_user - add correct ``ed25519`` auth plugin handling when creating a
user (https://github.com/ansible-collections/community.mysql/issues/672).
release_summary: 'This is a bugfix release of the ``community.mysql`` collection.
This changelog contains all changes to the modules and plugins in this
collection that have been made after the previous release.'
fragments:
- 0-mysql_user.yml
- 3.10.2.yml
release_date: '2024-09-06'
3.10.3:
changes:
bugfixes:
- mysql_user - add correct ``ed25519`` auth plugin handling when creating a
user (https://github.com/ansible-collections/community.mysql/pull/676).
release_summary: 'This is a bugfix release of the ``community.mysql`` collection.
This changelog contains all changes to the modules and plugins in this
collection that have been made after the previous release.'
fragments:
- 0-mysql_user.yml
- 3.10.3.yml
release_date: '2024-09-09'
3.11.0:
changes:
bugfixes:
- mysql_user,mysql_role - The sql_mode ANSI_QUOTES affects how the modules mysql_user
and mysql_role compare the existing privileges with the configured privileges,
as well as decide whether double quotes or backticks should be used in the
GRANT statements. Pointing out in issue 671, the modules mysql_user and mysql_role
allow users to enable/disable ANSI_QUOTES in session variable (within a DB
session, the session variable always overwrites the global one). But due to
the issue, the modules do not check for ANSI_MODE in the session variable,
instead, they only check in the GLOBAL one.That behavior is not only limiting
the users' flexibility, but also not allowing users to explicitly disable
ANSI_MODE to work around such bugs like https://bugs.mysql.com/bug.php?id=115953.
(https://github.com/ansible-collections/community.mysql/issues/671)
minor_changes:
- mysql_info - adds the count of tables for each database to the returned values.
It is possible to exclude this new field using the ``db_table_count`` exclusion
filter. (https://github.com/ansible-collections/community.mysql/pull/691)
release_summary: 'This is a minor release of the ``community.mysql`` collection.
This changelog contains all changes to the modules and plugins in this
collection that have been made after the previous release.'
fragments:
- 3.11.0.yml
- 591-mysql_info-db_tables_count.yml
- 671-modules_util_user.yml
release_date: '2024-11-19'
3.12.0:
changes:
minor_changes:
- mysql_db - added ``zstd`` (de)compression support for ``import``/``dump``
states (https://github.com/ansible-collections/community.mysql/issues/696).
- mysql_query - returns the ``execution_time_ms`` list containing execution
time per query in milliseconds.
release_summary: 'This is a minor release of the ``community.mysql`` collection.
This changelog contains all changes to the modules and plugins in this
collection that have been made after the previous release.'
fragments:
- 0-mysql_query-returns-exec-time-ms.yml
- 3.12.0.yml
- 696-mysql-db-add-zstd-support.yml
release_date: '2025-01-17'
3.13.0:
changes:
bugfixes:
- mysql_db - fix dump and import to find MariaDB binaries (mariadb and mariadb-dump)
when MariaDB 11+ is used and symbolic links to MySQL binaries are absent.
minor_changes:
- Integration tests for MariaDB 11.4 have replaced those for 10.5. The previous
version is now 10.11.
- mysql_user - add ``locked`` option to lock/unlock users, this is mainly used
to have users that will act as definers on stored procedures.
release_summary: 'This is a minor release of the ``community.mysql`` collection.
This changelog contains all changes to the modules and plugins in this
collection that have been made after the previous release.'
fragments:
- 3.13.0.yml
- 702-user_locking.yaml
- tests_mariadb_11_4.yml
release_date: '2025-03-21'
3.2.0:
changes:
bugfixes:
@ -380,3 +550,29 @@ releases:
- drop_ansible_core_2_12_and_2_13.yml
- lie_mysql_info_users_info.yml
release_date: '2023-10-25'
3.9.0:
changes:
bugfixes:
- mysql_info - the ``slave_status`` filter was returning an empty list on MariaDB
with multiple replication channels. It now returns all channels by running
``SHOW ALL SLAVES STATUS`` for MariaDB servers (https://github.com/ansible-collections/community.mysql/issues/603).
major_changes:
- Collection version 2.*.* is EOL, no more bugfixes will be backported. Please
consider upgrading to the latest version.
minor_changes:
- mysql_user - add the ``password_expire`` and ``password_expire_interval``
arguments to implement the password expiration management for mysql user (https://github.com/ansible-collections/community.mysql/pull/598).
- mysql_user - add user attribute support via the ``attributes`` parameter and
return value (https://github.com/ansible-collections/community.mysql/pull/604).
release_summary: 'This is a minor release of the ``community.mysql`` collection.
This changelog contains all changes to the modules and plugins in this
collection that have been made after the previous release.'
fragments:
- 0-stable-2-eol.yml
- 3.9.0.yml
- 598-password_expire-support-for-mysql_user.yml
- 602-show-all-slaves-status.yaml
- 604-user-attributes.yaml
release_date: '2024-02-22'

View file

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

View file

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

View file

@ -71,24 +71,21 @@ options:
- Whether to validate the server host name when an SSL connection is required. Corresponds to MySQL CLIs C(--ssl) switch.
- Setting this to C(false) disables hostname verification. Use with caution.
- Requires pymysql >= 0.7.11.
- This option has no effect on MySQLdb.
type: bool
version_added: '1.1.0'
requirements:
- mysqlclient (Python 3.5+) or
- PyMySQL (Python 2.7 and Python 3.x) or
- MySQLdb (Python 2.x)
- PyMySQL (Python 2.7 and Python 3.x)
notes:
- Requires the PyMySQL (Python 2.7 and Python 3.X) or MySQL-python (Python 2.X) package installed on the remote host.
- Requires the PyMySQL (Python 2.7 and Python 3.X) package installed on the remote host.
The Python package may be installed with apt-get install python-pymysql (Ubuntu; see M(ansible.builtin.apt)) or
yum install python2-PyMySQL (RHEL/CentOS/Fedora; see M(ansible.builtin.yum)). You can also use dnf install python2-PyMySQL
for newer versions of Fedora; see M(ansible.builtin.dnf).
- Be sure you have mysqlclient, PyMySQL, or MySQLdb library installed on the target machine
for the Python interpreter Ansible discovers. For example if ansible discovers and uses Python 3, you need to install
the Python 3 version of PyMySQL or mysqlclient. If ansible discovers and uses Python 2, you need to install the Python 2
version of either PyMySQL or MySQL-python.
- Be sure you have PyMySQL library installed on the target machine
for the Python interpreter Ansible discovers. For example if ansible discovers and uses Python 3, you need to install
the Python 3 version of PyMySQL. If ansible discovers and uses Python 2, you need to install the Python 2
version of PyMySQL.
- If you have trouble, it may help to force Ansible to use the Python interpreter you need by specifying
C(ansible_python_interpreter). For more information, see
C(ansible_python_interpreter). For more information, see
U(https://docs.ansible.com/ansible/latest/reference_appendices/interpreter_discovery.html).
- Both C(login_password) and C(login_user) are required when you are
passing credentials. If none are present, the module will attempt to read
@ -99,9 +96,6 @@ notes:
and later uses the unix_socket authentication plugin by default that
without using I(login_unix_socket=/var/run/mysqld/mysqld.sock) (the default path)
causes the error ``Host '127.0.0.1' is not allowed to connect to this MariaDB server``.
- Alternatively, you can use the mysqlclient library instead of MySQL-python (MySQLdb)
which supports both Python 2.X and Python >=3.5.
See U(https://pypi.org/project/mysqlclient/) how to install it.
- "If credentials from the config file (for example, C(/root/.my.cnf)) are not needed to connect to a database server, but
the file exists and does not contain a C([client]) section, before any other valid directives, it will be read and this
will cause the connection to fail, to prevent this set it to an empty string, (for example C(config_file: ''))."

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)
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.mysql import get_server_version
import re
import shlex
def use_old_user_mgmt(cursor):
version = get_server_version(cursor)
@ -24,3 +27,52 @@ def server_supports_alter_user(cursor):
version = get_server_version(cursor)
return LooseVersion(version) >= LooseVersion("5.6")
def server_supports_password_expire(cursor):
version = get_server_version(cursor)
return LooseVersion(version) >= LooseVersion("5.7")
def get_tls_requires(cursor, user, host):
"""Get user TLS requirements.
We must use SHOW GRANTS because some tls fileds are encoded.
Args:
cursor (cursor): DB driver cursor object.
user (str): User name.
host (str): User host name.
Returns: Dictionary containing current TLS required
"""
if not use_old_user_mgmt(cursor):
query = "SHOW CREATE USER '%s'@'%s'" % (user, host)
else:
query = "SHOW GRANTS for '%s'@'%s'" % (user, host)
cursor.execute(query)
grants = cursor.fetchone()
# Mysql_info use a DictCursor so we must convert back to a list
# otherwise we get KeyError 0
if isinstance(grants, dict):
grants = list(grants.values())
grants_str = ''.join(grants)
pattern = r"(?<=\bREQUIRE\b)(.*?)(?=(?:\bPASSWORD\b|$))"
requires_match = re.search(pattern, grants_str)
requires = requires_match.group().strip() if requires_match else ""
if requires.startswith('NONE'):
return None
if requires.startswith('SSL'):
return {'SSL': None}
if requires.startswith('X509'):
return {'X509': None}
items = iter(shlex.split(requires))
requires = dict(zip(items, items))
return requires or None

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)
else:
# In case of MySQLdb driver
# Will be deprecated and dropped
# https://github.com/ansible-collections/community.mysql/issues/654
module.warn('Support of mysqlcline/MySQLdb connector is deprecated. '
'We\'ll stop testing against it in collection version 4.0.0 '
'and remove the related code in 5.0.0. Use PyMySQL connector instead.')
if mysql_driver.version_info[0] < 2 or (mysql_driver.version_info[0] == 2 and mysql_driver.version_info[1] < 1):
# for MySQLdb < 2.1.0, use 'db' instead of 'database' and 'passwd' instead of 'password'
if 'database' in config:
@ -207,6 +214,13 @@ def get_server_version(cursor):
return version_str
def get_server_implementation(cursor):
if 'mariadb' in get_server_version(cursor).lower():
return "mariadb"
else:
return "mysql"
def set_session_vars(module, cursor, session_vars):
"""Set session vars."""
for var, value in session_vars.items():

View file

@ -1,4 +1,6 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
# This code is part of Ansible, but is an independent component.
@ -10,12 +12,18 @@ __metaclass__ = type
# Simplified BSD License (see simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
import string
import json
import re
from ansible.module_utils.six import iteritems
from ansible_collections.community.mysql.plugins.module_utils.mysql import (
mysql_driver,
get_server_implementation,
)
from ansible_collections.community.mysql.plugins.module_utils.implementations.mysql.hash import (
mysql_sha256_password_hash,
mysql_sha256_password_hash_hex,
)
@ -24,7 +32,7 @@ class InvalidPrivsError(Exception):
def get_mode(cursor):
cursor.execute('SELECT @@GLOBAL.sql_mode')
cursor.execute('SELECT @@sql_mode')
result = cursor.fetchone()
mode_str = result[0]
if 'ANSI' in mode_str:
@ -44,6 +52,25 @@ def user_exists(cursor, user, host, host_all):
return count[0] > 0
def user_is_locked(cursor, user, host):
cursor.execute("SHOW CREATE USER %s@%s", (user, host))
# Per discussions on irc:libera.chat:#maria the query may return up to 2 rows but "ACCOUNT LOCK" should always be in the first row.
result = cursor.fetchone()
# ACCOUNT LOCK does not have to be the last option in the CREATE USER query.
# Need to handle both DictCursor and non-DictCursor
if isinstance(result, tuple):
if result[0].find('ACCOUNT LOCK') > 0:
return True
elif isinstance(result, dict):
for res in result.values():
if res.find('ACCOUNT LOCK') > 0:
return True
return False
def sanitize_requires(tls_requires):
sanitized_requires = {}
if tls_requires:
@ -79,31 +106,6 @@ def do_not_mogrify_requires(query, params, tls_requires):
return query, params
def get_tls_requires(cursor, user, host):
if user:
if not impl.use_old_user_mgmt(cursor):
query = "SHOW CREATE USER '%s'@'%s'" % (user, host)
else:
query = "SHOW GRANTS for '%s'@'%s'" % (user, host)
cursor.execute(query)
require_list = [tuple[0] for tuple in filter(lambda x: "REQUIRE" in x[0], cursor.fetchall())]
require_line = require_list[0] if require_list else ""
pattern = r"(?<=\bREQUIRE\b)(.*?)(?=(?:\bPASSWORD\b|$))"
requires_match = re.search(pattern, require_line)
requires = requires_match.group().strip() if requires_match else ""
if any((requires.startswith(req) for req in ('SSL', 'X509', 'NONE'))):
requires = requires.split()[0]
if requires == 'NONE':
requires = None
else:
import shlex
items = iter(shlex.split(requires))
requires = dict(zip(items, items))
return requires or None
def get_grants(cursor, user, host):
cursor.execute("SHOW GRANTS FOR %s@%s", (user, host))
grants_line = list(filter(lambda x: "ON *.*" in x[0], cursor.fetchall()))[0]
@ -112,8 +114,12 @@ def get_grants(cursor, user, host):
return grants.split(", ")
def get_existing_authentication(cursor, user, host):
# Return the plugin and auth_string if there is exactly one distinct existing plugin and auth_string.
def get_existing_authentication(cursor, user, host=None):
""" Return a list of dict containing the plugin and auth_string for the
specified username.
If hostname is provided, return only the information about this particular
account.
"""
cursor.execute("SELECT VERSION()")
srv_type = cursor.fetchone()
# Mysql_info use a DictCursor so we must convert back to a list
@ -124,54 +130,91 @@ def get_existing_authentication(cursor, user, host):
if 'mariadb' in srv_type[0].lower():
# before MariaDB 10.2.19 and 10.3.11, "password" and "authentication_string" can differ
# when using mysql_native_password
cursor.execute("""select plugin, auth from (
select plugin, password as auth from mysql.user where user=%(user)s
and host=%(host)s
union select plugin, authentication_string as auth from mysql.user where user=%(user)s
and host=%(host)s) x group by plugin, auth limit 2
""", {'user': user, 'host': host})
if host:
cursor.execute("""select plugin, auth from (
select plugin, password as auth from mysql.user where user=%(user)s
and host=%(host)s
union select plugin, authentication_string as auth from mysql.user where user=%(user)s
and host=%(host)s) x group by plugin, auth
""", {'user': user, 'host': host})
else:
cursor.execute("""select plugin, auth from (
select plugin, password as auth from mysql.user where user=%(user)s
union select plugin, authentication_string as auth from mysql.user where user=%(user)s
) x group by plugin, auth
""", {'user': user})
else:
cursor.execute("""select plugin, authentication_string as auth
from mysql.user where user=%(user)s and host=%(host)s
group by plugin, authentication_string limit 2""", {'user': user, 'host': host})
if host:
cursor.execute("""select plugin, authentication_string as auth
from mysql.user where user=%(user)s and host=%(host)s
group by plugin, authentication_string""", {'user': user, 'host': host})
else:
cursor.execute("""select plugin, authentication_string as auth
from mysql.user where user=%(user)s
group by plugin, authentication_string""", {'user': user})
rows = cursor.fetchall()
# Mysql_info use a DictCursor so we must convert back to a list
# otherwise we get KeyError 0
if isinstance(rows, dict):
rows = list(rows.values())
if isinstance(rows[0], tuple):
return {'plugin': rows[0][0], 'plugin_auth_string': rows[0][1]}
if len(rows) == 0:
return []
# Mysql_info use a DictCursor so we must convert list(dict)
# to list(tuple) otherwise we get KeyError 0
if isinstance(rows[0], dict):
return {'plugin': rows[0].get('plugin'), 'plugin_auth_string': rows[0].get('auth')}
return None
rows = [tuple(row.values()) for row in rows]
existing_auth_list = []
# 'plugin_auth_string' contains the hash string. Must be removed in c.mysql 4.0
# See https://github.com/ansible-collections/community.mysql/pull/629
for r in rows:
existing_auth_list.append({
'plugin': r[0],
'plugin_auth_string': r[1],
'plugin_hash_string': r[1]})
return existing_auth_list
def user_add(cursor, user, host, host_all, password, encrypted,
plugin, plugin_hash_string, plugin_auth_string, new_priv,
tls_requires, check_mode, reuse_existing_password):
plugin, plugin_hash_string, plugin_auth_string, salt, new_priv,
attributes, tls_requires, reuse_existing_password, module,
password_expire, password_expire_interval, locked=False):
# If attributes are set, perform a sanity check to ensure server supports user attributes before creating user
if attributes and not get_attribute_support(cursor):
module.fail_json(msg="user attributes were specified but the server does not support user attributes")
# we cannot create users without a proper hostname
if host_all:
return {'changed': False, 'password_changed': False}
return {'changed': False, 'password_changed': False, 'attributes': attributes}
if check_mode:
return {'changed': True, 'password_changed': None}
if module.check_mode:
return {'changed': True, 'password_changed': None, 'attributes': attributes}
# Determine what user management method server uses
impl = get_user_implementation(cursor)
old_user_mgmt = impl.use_old_user_mgmt(cursor)
mogrify = do_not_mogrify_requires if old_user_mgmt else mogrify_requires
# This is for update_password: on_new_username
used_existing_password = False
if reuse_existing_password:
existing_auth = get_existing_authentication(cursor, user, host)
existing_auth = get_existing_authentication(cursor, user)
if existing_auth:
plugin = existing_auth['plugin']
plugin_hash_string = existing_auth['auth_string']
password = None
used_existing_password = True
if len(existing_auth) != 1:
module.warn("An account with the username %s has a different "
"password than the others existing accounts. Thus "
"on_new_username can't decide which password to "
"reuse so it will use your provided password "
"instead. If no password is provided, the account "
"will have an empty password!" % user)
used_existing_password = False
else:
plugin_hash_string = existing_auth[0]['plugin_hash_string']
password = None
used_existing_password = True
plugin = existing_auth[0]['plugin'] # What if plugin differ?
if password and encrypted:
if impl.supports_identified_by_password(cursor):
query_with_args = "CREATE USER %s@%s IDENTIFIED BY PASSWORD %s", (user, host, password)
@ -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
if plugin == 'pam': # Used by MariaDB which requires the USING keyword, not BY
query_with_args = "CREATE USER %s@%s IDENTIFIED WITH %s USING %s", (user, host, plugin, plugin_auth_string)
elif plugin == 'ed25519': # Used by MariaDB which requires the USING keyword, not BY
query_with_args = "CREATE USER %s@%s IDENTIFIED WITH %s USING PASSWORD(%s)", (user, host, plugin, plugin_auth_string)
elif salt:
if plugin in ['caching_sha2_password', 'sha256_password']:
generated_hash_string = mysql_sha256_password_hash_hex(password=plugin_auth_string, salt=salt)
else:
module.fail_json(msg="salt not handled for %s authentication plugin" % plugin)
query_with_args = ("CREATE USER %s@%s IDENTIFIED WITH %s AS 0x" + generated_hash_string), (user, host, plugin)
else:
query_with_args = "CREATE USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, plugin_auth_string)
elif plugin:
@ -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,)
cursor.execute(*mogrify(*query_with_args_and_tls_requires))
if password_expire:
if not impl.server_supports_password_expire(cursor):
module.fail_json(msg="The server version does not match the requirements "
"for password_expire parameter. See module's documentation.")
set_password_expire(cursor, user, host, password_expire, password_expire_interval)
if new_priv is not None:
for db_table, priv in iteritems(new_priv):
privileges_grant(cursor, user, host, db_table, priv, tls_requires)
if tls_requires is not None:
privileges_grant(cursor, user, host, "*.*", get_grants(cursor, user, host), tls_requires)
return {'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):
@ -217,13 +284,15 @@ def is_hash(password):
def user_mod(cursor, user, host, host_all, password, encrypted,
plugin, plugin_hash_string, plugin_auth_string, new_priv,
append_privs, subtract_privs, tls_requires, module, role=False, maria_role=False):
plugin, plugin_hash_string, plugin_auth_string, salt, new_priv,
append_privs, subtract_privs, attributes, tls_requires, module,
password_expire, password_expire_interval, locked=None, role=False, maria_role=False):
changed = False
msg = "User unchanged"
grant_option = False
# Determine what user management method server uses
impl = get_user_implementation(cursor)
old_user_mgmt = impl.use_old_user_mgmt(cursor)
if host_all and not role:
@ -278,27 +347,48 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
if current_pass_hash != encrypted_password:
password_changed = True
msg = "Password updated"
if module.check_mode:
return {'changed': True, 'msg': msg, 'password_changed': password_changed}
if old_user_mgmt:
cursor.execute("SET PASSWORD FOR %s@%s = %s", (user, host, encrypted_password))
msg = "Password updated (old style)"
else:
try:
cursor.execute("ALTER USER %s@%s IDENTIFIED WITH mysql_native_password AS %s", (user, host, encrypted_password))
msg = "Password updated (new style)"
except (mysql_driver.Error) as e:
# https://stackoverflow.com/questions/51600000/authentication-string-of-root-user-on-mysql
# Replacing empty root password with new authentication mechanisms fails with error 1396
if e.args[0] == 1396:
cursor.execute(
"UPDATE mysql.user SET plugin = %s, authentication_string = %s, Password = '' WHERE User = %s AND Host = %s",
('mysql_native_password', encrypted_password, user, host)
)
cursor.execute("FLUSH PRIVILEGES")
msg = "Password forced update"
else:
raise e
if not module.check_mode:
if old_user_mgmt:
cursor.execute("SET PASSWORD FOR %s@%s = %s", (user, host, encrypted_password))
msg = "Password updated (old style)"
else:
try:
cursor.execute("ALTER USER %s@%s IDENTIFIED WITH mysql_native_password AS %s", (user, host, encrypted_password))
msg = "Password updated (new style)"
except (mysql_driver.Error) as e:
# https://stackoverflow.com/questions/51600000/authentication-string-of-root-user-on-mysql
# Replacing empty root password with new authentication mechanisms fails with error 1396
if e.args[0] == 1396:
cursor.execute(
"UPDATE mysql.user SET plugin = %s, authentication_string = %s, Password = '' WHERE User = %s AND Host = %s",
('mysql_native_password', encrypted_password, user, host)
)
cursor.execute("FLUSH PRIVILEGES")
msg = "Password forced update"
else:
raise e
changed = True
# Handle password expiration
if bool(password_expire):
if not impl.server_supports_password_expire(cursor):
module.fail_json(msg="The server version does not match the requirements "
"for password_expire parameter. See module's documentation.")
update = False
mariadb_role = True if "mariadb" in str(impl.__name__) else False
current_password_policy = get_password_expiration_policy(cursor, user, host, maria_role=mariadb_role)
password_expired = is_password_expired(cursor, user, host)
# Check if changes needed to be applied.
if not ((current_password_policy == -1 and password_expire == "default") or
(current_password_policy == 0 and password_expire == "never") or
(current_password_policy == password_expire_interval and password_expire == "interval") or
(password_expire == 'now' and password_expired)):
update = True
if not module.check_mode:
set_password_expire(cursor, user, host, password_expire, password_expire_interval)
password_changed = True
changed = True
# Handle plugin authentication
@ -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:
update = True
if plugin_auth_string and current_plugin[1] != plugin_auth_string:
if salt:
if plugin in ['caching_sha2_password', 'sha256_password']:
if current_plugin[1] != mysql_sha256_password_hash(password=plugin_auth_string, salt=salt):
update = True
elif plugin_auth_string and current_plugin[1] != plugin_auth_string:
# this case can cause more updates than expected,
# as plugin can hash auth_string in any way it wants
# and there's no way to figure it out for
@ -323,18 +417,28 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
update = True
if update:
query_with_args = None
if plugin_hash_string:
query_with_args = "ALTER USER %s@%s IDENTIFIED WITH %s AS %s", (user, host, plugin, plugin_hash_string)
elif plugin_auth_string:
# Mysql and MariaDB differ in naming pam plugin and syntax to set it
if plugin == 'pam':
query_with_args = "ALTER USER %s@%s IDENTIFIED WITH %s USING %s", (user, host, plugin, plugin_auth_string)
elif plugin == 'ed25519':
query_with_args = "ALTER USER %s@%s IDENTIFIED WITH %s USING PASSWORD(%s)", (user, host, plugin, plugin_auth_string)
elif salt:
if plugin in ['caching_sha2_password', 'sha256_password']:
generated_hash_string = mysql_sha256_password_hash_hex(password=plugin_auth_string, salt=salt)
else:
module.fail_json(msg="salt not handled for %s authentication plugin" % plugin)
query_with_args = ("ALTER USER %s@%s IDENTIFIED WITH %s AS 0x" + generated_hash_string), (user, host, plugin)
else:
query_with_args = "ALTER USER %s@%s IDENTIFIED WITH %s BY %s", (user, host, plugin, plugin_auth_string)
else:
query_with_args = "ALTER USER %s@%s IDENTIFIED WITH %s", (user, host, plugin)
cursor.execute(*query_with_args)
if not module.check_mode:
cursor.execute(*query_with_args)
password_changed = True
changed = True
@ -352,9 +456,8 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
if db_table not in new_priv:
if user != "root" and "PROXY" not in priv:
msg = "Privileges updated"
if module.check_mode:
return {'changed': True, 'msg': msg, 'password_changed': password_changed}
privileges_revoke(cursor, user, host, db_table, priv, grant_option, maria_role)
if not module.check_mode:
privileges_revoke(cursor, user, host, db_table, priv, grant_option, maria_role)
changed = True
# If the user doesn't currently have any privileges on a db.table, then
@ -363,9 +466,8 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
for db_table, priv in iteritems(new_priv):
if db_table not in curr_priv:
msg = "New privileges granted"
if module.check_mode:
return {'changed': True, 'msg': msg, 'password_changed': password_changed}
privileges_grant(cursor, user, host, db_table, priv, tls_requires, maria_role)
if not module.check_mode:
privileges_grant(cursor, user, host, db_table, priv, tls_requires, maria_role)
changed = True
# If the db.table specification exists in both the user's current privileges
@ -404,42 +506,98 @@ def user_mod(cursor, user, host, host_all, password, encrypted,
if len(grant_privs) + len(revoke_privs) > 0:
msg = "Privileges updated: granted %s, revoked %s" % (grant_privs, revoke_privs)
if module.check_mode:
return {'changed': True, 'msg': msg, 'password_changed': password_changed}
if len(revoke_privs) > 0:
privileges_revoke(cursor, user, host, db_table, revoke_privs, grant_option, maria_role)
if len(grant_privs) > 0:
privileges_grant(cursor, user, host, db_table, grant_privs, tls_requires, maria_role)
if not module.check_mode:
if len(revoke_privs) > 0:
privileges_revoke(cursor, user, host, db_table, revoke_privs, grant_option, maria_role)
if len(grant_privs) > 0:
privileges_grant(cursor, user, host, db_table, grant_privs, tls_requires, maria_role)
else:
changed = True
# after privilege manipulation, compare privileges from before and now
after_priv = privileges_get(cursor, user, host, maria_role)
changed = changed or (curr_priv != after_priv)
# Handle attributes
attribute_support = get_attribute_support(cursor)
final_attributes = {}
if attributes:
if not attribute_support:
module.fail_json(msg="user attributes were specified but the server does not support user attributes")
else:
current_attributes = attributes_get(cursor, user, host)
if current_attributes is None:
current_attributes = {}
attributes_to_change = {}
for key, value in attributes.items():
if key not in current_attributes or current_attributes[key] != value:
attributes_to_change[key] = value
if attributes_to_change:
msg = "Attributes updated: %s" % (", ".join(["%s: %s" % (key, value) for key, value in attributes_to_change.items()]))
# Calculate final attributes by re-running attributes_get when not in check mode, and merge dictionaries when in check mode
if not module.check_mode:
cursor.execute("ALTER USER %s@%s ATTRIBUTE %s", (user, host, json.dumps(attributes_to_change)))
final_attributes = attributes_get(cursor, user, host)
else:
# Final if statements excludes items whose values are None in attributes_to_change, i.e. attributes that will be deleted
final_attributes = {k: v for d in (current_attributes, attributes_to_change) for k, v in d.items() if k not in attributes_to_change or
attributes_to_change[k] is not None}
# Convert empty dict to None per return value requirements
final_attributes = final_attributes if final_attributes else None
changed = True
else:
final_attributes = current_attributes
else:
if attribute_support:
final_attributes = attributes_get(cursor, user, host)
if not role and locked is not None and user_is_locked(cursor, user, host) != locked:
if not module.check_mode:
if locked:
cursor.execute("ALTER USER %s@%s ACCOUNT LOCK", (user, host))
msg = 'User locked'
else:
cursor.execute("ALTER USER %s@%s ACCOUNT UNLOCK", (user, host))
msg = 'User unlocked'
else:
if locked:
msg = 'User will be locked'
else:
msg = 'User will be unlocked'
changed = True
if role:
continue
# Handle TLS requirements
current_requires = get_tls_requires(cursor, user, host)
current_requires = sanitize_requires(impl.get_tls_requires(cursor, user, host))
if current_requires != tls_requires:
msg = "TLS requires updated"
if module.check_mode:
return {'changed': True, 'msg': msg, 'password_changed': password_changed}
if not old_user_mgmt:
pre_query = "ALTER USER"
else:
pre_query = "GRANT %s ON *.* TO" % ",".join(get_grants(cursor, user, host))
if not module.check_mode:
if not old_user_mgmt:
pre_query = "ALTER USER"
else:
pre_query = "GRANT %s ON *.* TO" % ",".join(get_grants(cursor, user, host))
if tls_requires is not None:
query = " ".join((pre_query, "%s@%s"))
query_with_args = mogrify_requires(query, (user, host), tls_requires)
else:
query = " ".join((pre_query, "%s@%s REQUIRE NONE"))
query_with_args = query, (user, host)
if tls_requires is not None:
query = " ".join((pre_query, "%s@%s"))
query_with_args = mogrify_requires(query, (user, host), tls_requires)
else:
query = " ".join((pre_query, "%s@%s REQUIRE NONE"))
query_with_args = query, (user, host)
cursor.execute(*query_with_args)
cursor.execute(*query_with_args)
changed = True
return {'changed': 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):
@ -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', )])
query = ["GRANT %s ON %s" % (priv_string, db_table)]
# MySQL and MariaDB don't store roles in the user table the same manner:
# select user, host from mysql.user;
# +------------------+-----------+
# | user | host |
# +------------------+-----------+
# | role_foo | % | <- MySQL
# | role_foo | | <- MariaDB
# +------------------+-----------+
if not maria_role:
query.append("TO %s@%s")
params = (user, host)
@ -750,6 +916,7 @@ def privileges_grant(cursor, user, host, db_table, priv, tls_requires, maria_rol
query.append("TO %s")
params = (user)
impl = get_user_implementation(cursor)
if tls_requires and impl.use_old_user_mgmt(cursor):
query, params = mogrify_requires(" ".join(query), params, tls_requires)
query = [query]
@ -886,6 +1053,7 @@ def limit_resources(module, cursor, user, host, resource_limits, check_mode):
Returns: True, if changed, False otherwise.
"""
impl = get_user_implementation(cursor)
if not impl.server_supports_alter_user(cursor):
module.fail_json(msg="The server version does not match the requirements "
"for resource_limits parameter. See module's documentation.")
@ -916,12 +1084,116 @@ def limit_resources(module, cursor, user, host, resource_limits, check_mode):
return True
def get_impl(cursor):
global impl
cursor.execute("SELECT VERSION()")
if 'mariadb' in cursor.fetchone()[0].lower():
def set_password_expire(cursor, user, host, password_expire, password_expire_interval):
"""Fuction to set passowrd expiration for user.
Args:
cursor (cursor): DB driver cursor object.
user (str): User name.
host (str): User hostname.
password_expire (str): Password expiration mode.
password_expire_days (int): Invterval of days password expires.
"""
if password_expire.lower() == "never":
statement = "PASSWORD EXPIRE NEVER"
elif password_expire.lower() == "default":
statement = "PASSWORD EXPIRE DEFAULT"
elif password_expire.lower() == "interval":
statement = "PASSWORD EXPIRE INTERVAL %d DAY" % (password_expire_interval)
elif password_expire.lower() == "now":
statement = "PASSWORD EXPIRE"
cursor.execute("ALTER USER %s@%s " + statement, (user, host))
def get_password_expiration_policy(cursor, user, host, maria_role=False):
"""Function to get password policy for user.
Args:
cursor (cursor): DB driver cursor object.
user (str): User name.
host (str): User hostname.
maria_role (bool, optional): mariadb or mysql. Defaults to False.
Returns:
policy (int): Current users password policy.
"""
if not maria_role:
statement = "SELECT IFNULL(password_lifetime, -1) FROM mysql.user \
WHERE User = %s AND Host = %s", (user, host)
else:
statement = "SELECT JSON_EXTRACT(Priv, '$.password_lifetime') AS password_lifetime \
FROM mysql.global_priv \
WHERE User = %s AND Host = %s", (user, host)
cursor.execute(*statement)
policy = cursor.fetchone()[0]
return int(policy)
def is_password_expired(cursor, user, host):
"""Function to check if password is expired
Args:
cursor (cursor): DB driver cursor object.
user (str): User name.
host (str): User hostname.
Returns:
expired (bool): True if expired, else False.
"""
statement = "SELECT password_expired FROM mysql.user \
WHERE User = %s AND Host = %s", (user, host)
cursor.execute(*statement)
expired = cursor.fetchone()[0]
if str(expired) == "Y":
return True
return False
def get_attribute_support(cursor):
"""Checks if the MySQL server supports user attributes.
Args:
cursor (cursor): DB driver cursor object.
Returns:
True if attributes are supported, False if they are not.
"""
try:
# information_schema.tables does not hold the tables within information_schema itself
cursor.execute("SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES LIMIT 0")
cursor.fetchone()
except mysql_driver.Error:
return False
return True
def attributes_get(cursor, user, host):
"""Get attributes for a given user.
Args:
cursor (cursor): DB driver cursor object.
user (str): User name.
host (str): User host name.
Returns:
None if the user does not exist or the user has no attributes set, otherwise a dict of attributes set on the user
"""
cursor.execute("SELECT attribute FROM INFORMATION_SCHEMA.USER_ATTRIBUTES WHERE user = %s AND host = %s", (user, host))
r = cursor.fetchone()
# convert JSON string stored in row into a dict - mysql enforces that user_attributes entires are in JSON format
j = json.loads(r[0]) if r and r[0] else None
# if the attributes dict is empty, return None instead
return j if j else None
def get_user_implementation(cursor):
db_engine = get_server_implementation(cursor)
if db_engine == 'mariadb':
from ansible_collections.community.mysql.plugins.module_utils.implementations.mariadb import user as mariauser
impl = mariauser
return mariauser
else:
from ansible_collections.community.mysql.plugins.module_utils.implementations.mysql import user as mysqluser
impl = mysqluser
return mysqluser

View file

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

View file

@ -1,18 +1,20 @@
#!/usr/bin/python
# -*- 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)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: mysql_info
short_description: Gather information about MySQL servers
short_description: Gather information about MySQL or MariaDB servers
description:
- Gathers information about MySQL servers.
- Gathers information about MySQL or MariaDB servers.
options:
filter:
@ -34,7 +36,7 @@ options:
exclude_fields:
description:
- List of fields which are not needed to collect.
- "Supports elements: C(db_size). Unsupported elements will be ignored."
- "Supports elements: C(db_size), C(db_table_count). Unsupported elements will be ignored."
type: list
elements: str
version_added: '0.1.0'
@ -45,6 +47,7 @@ options:
default: false
notes:
- Compatible with MariaDB or MySQL.
- Calculating the size of a database might be slow, depending on the number and size of tables in it.
To avoid this, use I(exclude_fields=db_size).
@ -145,7 +148,7 @@ EXAMPLES = r'''
plugin: "{{ item.plugin | default(omit) }}"
plugin_auth_string: "{{ item.plugin_auth_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) }}"
resource_limits: "{{ item.resource_limits | default(omit) }}"
column_case_sensitive: true
@ -160,6 +163,12 @@ EXAMPLES = 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:
description: Database server version.
returned: if not excluded by filter
@ -196,13 +205,19 @@ databases:
returned: if not excluded by filter
type: dict
sample:
- { "mysql": { "size": 656594 }, "information_schema": { "size": 73728 } }
- { "mysql": { "size": 656594, "tables": 31 }, "information_schema": { "size": 73728, "tables": 79 } }
contains:
size:
description: Database size in bytes.
returned: if not excluded by filter
type: dict
sample: { 'size': 656594 }
tables:
description: Count of tables and views in that database.
returned: if not excluded by filter
type: dict
sample: { 'tables': 12 }
version_added: '3.11.0'
settings:
description: Global settings (variables) information.
returned: if not excluded by filter
@ -239,7 +254,8 @@ users_info:
"host": "host.com",
"plugin": "mysql_native_password",
"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'
engines:
description: Information about the server's storage engines.
@ -271,7 +287,6 @@ connector_name:
type: str
sample:
- "pymysql"
- "MySQLdb"
version_added: '3.6.0'
connector_version:
description: Version of the python connector used by the module. When the connector is not identified, returns C(Unknown).
@ -285,6 +300,9 @@ connector_version:
from decimal import Decimal
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.mysql.plugins.module_utils.command_resolver import (
CommandResolver
)
from ansible_collections.community.mysql.plugins.module_utils.mysql import (
mysql_connect,
mysql_common_argument_spec,
@ -292,12 +310,16 @@ from ansible_collections.community.mysql.plugins.module_utils.mysql import (
mysql_driver_fail_msg,
get_connector_name,
get_connector_version,
get_server_implementation,
get_server_version,
)
from ansible_collections.community.mysql.plugins.module_utils.user import (
privileges_get,
get_resource_limits,
get_existing_authentication,
get_user_implementation,
user_is_locked,
)
from ansible.module_utils.six import iteritems
from ansible.module_utils._text import to_native
@ -325,9 +347,13 @@ class MySQL_Info(object):
5. add info about the new subset with an example to RETURN block
"""
def __init__(self, module, cursor):
def __init__(self, module, cursor, server_implementation, server_version, user_implementation):
self.module = module
self.cursor = cursor
self.server_implementation = server_implementation
self.server_version = server_version
self.user_implementation = user_implementation
self.command_resolver = CommandResolver(self.server_implementation, self.server_version)
self.info = {
'version': {},
'databases': {},
@ -489,7 +515,8 @@ class MySQL_Info(object):
def __get_master_status(self):
"""Get master status if the instance is a master."""
res = self.__exec_sql('SHOW MASTER STATUS')
query = self.command_resolver.resolve_command("SHOW MASTER STATUS")
res = self.__exec_sql(query)
if res:
for line in res:
for vname, val in iteritems(line):
@ -497,7 +524,8 @@ class MySQL_Info(object):
def __get_slave_status(self):
"""Get slave status if the instance is a slave."""
res = self.__exec_sql('SHOW SLAVE STATUS')
query = self.command_resolver.resolve_command("SHOW SLAVE STATUS")
res = self.__exec_sql(query)
if res:
for line in res:
host = line['Master_Host']
@ -518,7 +546,8 @@ class MySQL_Info(object):
def __get_slaves(self):
"""Get slave hosts info if the instance is a master."""
res = self.__exec_sql('SHOW SLAVE HOSTS')
query = self.command_resolver.resolve_command("SHOW SLAVE HOSTS")
res = self.__exec_sql(query)
if res:
for line in res:
srv_id = line['Server_id']
@ -596,13 +625,17 @@ class MySQL_Info(object):
priv_string.remove('*.*:USAGE')
resource_limits = get_resource_limits(self.cursor, user, host)
copy_ressource_limits = dict.copy(resource_limits)
tls_requires = self.user_implementation.get_tls_requires(
self.cursor, user, host)
output_dict = {
'name': user,
'host': host,
'priv': '/'.join(priv_string),
'resource_limits': copy_ressource_limits,
'tls_requires': tls_requires,
}
# Prevent returning a resource limit if empty
@ -613,12 +646,18 @@ class MySQL_Info(object):
if len(output_dict['resource_limits']) == 0:
del output_dict['resource_limits']
# Prevent returning tls_require if empty
if not tls_requires:
del output_dict['tls_requires']
authentications = get_existing_authentication(self.cursor, user, host)
if authentications:
output_dict.update(authentications)
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 lock_option
# but both are not supported by mysql_user atm. So no point yet.
output.append(output_dict)
@ -627,40 +666,39 @@ class MySQL_Info(object):
def __get_databases(self, exclude_fields, return_empty_dbs):
"""Get info about databases."""
if not exclude_fields:
query = ('SELECT table_schema AS "name", '
'SUM(data_length + index_length) AS "size" '
'FROM information_schema.TABLES GROUP BY table_schema')
else:
if 'db_size' in exclude_fields:
query = ('SELECT table_schema AS "name" '
'FROM information_schema.TABLES GROUP BY table_schema')
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:
for db in res:
self.info['databases'][db['name']] = {}
def create_db_info(db_data):
info = {}
if is_field_included('size'):
info['size'] = int(db_data.get('size', 0) or 0)
if is_field_included('table_count'):
info['tables'] = int(db_data.get('tables', 0) or 0)
return info
if not exclude_fields or 'db_size' not in exclude_fields:
if db['size'] is None:
db['size'] = 0
# Build the main query
query_parts = ['SELECT table_schema AS "name"']
if is_field_included('size'):
query_parts.append('SUM(data_length + index_length) AS "size"')
if is_field_included('table_count'):
query_parts.append('COUNT(table_name) as "tables"')
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
if not return_empty_dbs:
return None
# Get and process databases with tables
databases = self.__exec_sql(query) or []
for db in databases:
self.info['databases'][db['name']] = create_db_info(db)
# Add info about empty databases (issue #65727):
res = self.__exec_sql('SHOW DATABASES')
if res:
for db in res:
if db['Database'] not in self.info['databases']:
self.info['databases'][db['Database']] = {}
if not exclude_fields or 'db_size' not in exclude_fields:
self.info['databases'][db['Database']]['size'] = 0
# Handle empty databases if requested
if return_empty_dbs:
empty_databases = self.__exec_sql('SHOW DATABASES') or []
for db in empty_databases:
db_name = db['Database']
if db_name not in self.info['databases']:
self.info['databases'][db_name] = create_db_info({})
def __exec_sql(self, query, ddl=False):
"""Execute SQL.
@ -692,8 +730,8 @@ def main():
argument_spec = mysql_common_argument_spec()
argument_spec.update(
login_db=dict(type='str'),
filter=dict(type='list'),
exclude_fields=dict(type='list'),
filter=dict(type='list', elements='str'),
exclude_fields=dict(type='list', elements='str'),
return_empty_dbs=dict(type='bool', default=False),
)
@ -738,12 +776,17 @@ def main():
'Exception message: %s' % (connector_name, connector_version, config_file, to_native(e)))
module.fail_json(msg)
server_implementation = get_server_implementation(cursor)
server_version = get_server_version(cursor)
user_implementation = get_user_implementation(cursor)
###############################
# Create object and do main job
mysql = MySQL_Info(module, cursor)
mysql = MySQL_Info(module, cursor, server_implementation, server_version, user_implementation)
module.exit_json(changed=False,
server_engine='MariaDB' if server_implementation == 'mariadb' else 'MySQL',
connector_name=connector_name,
connector_version=connector_version,
**mysql.get_info(filter_, exclude_fields, return_empty_dbs))

View file

@ -1,7 +1,7 @@
#!/usr/bin/python
# -*- 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)
from __future__ import (absolute_import, division, print_function)
@ -10,9 +10,9 @@ __metaclass__ = type
DOCUMENTATION = r'''
---
module: mysql_query
short_description: Run MySQL queries
short_description: Run MySQL or MariaDB queries
description:
- Runs arbitrary MySQL queries.
- Runs arbitrary MySQL or MariaDB queries.
- Pay attention, the module does not support check mode!
All queries will be executed in autocommit mode.
- To run SQL queries from a file, use M(community.mysql.mysql_db) module.
@ -36,6 +36,7 @@ options:
- List of values to be passed as positional arguments to the query.
- Mutually exclusive with I(named_args).
type: list
elements: raw
named_args:
description:
- Dictionary of key-value arguments to pass to the query.
@ -55,11 +56,12 @@ attributes:
support: none
seealso:
- module: community.mysql.mysql_db
notes:
- Compatible with MariaDB or MySQL.
author:
- Andrew Klychkov (@Andersson007)
extends_documentation_fragment:
- community.mysql.mysql
'''
EXAMPLES = r'''
@ -114,8 +116,18 @@ rowcount:
returned: changed
type: list
sample: [5, 1]
execution_time_ms:
description:
- A list containing execution time per query in milliseconds.
- The measurements are done right before and after passing
the query to the driver for execution.
returned: success
type: list
sample: [7104, 85]
version_added: '3.12.0'
'''
import time
import warnings
from ansible.module_utils.basic import AnsibleModule
@ -136,12 +148,24 @@ DDL_QUERY_KEYWORDS = ('CREATE', 'DROP', 'ALTER', 'RENAME', 'TRUNCATE')
# Module execution.
#
def execute_and_return_time(cursor, query, args):
# Measure query execution time in milliseconds
start_time = time.perf_counter()
cursor.execute(query, args)
# Calculate the execution time rounding it to 4 decimal places
exec_time_ms = round((time.perf_counter() - start_time) * 1000, 4)
return cursor, exec_time_ms
def main():
argument_spec = mysql_common_argument_spec()
argument_spec.update(
query=dict(type='raw', required=True),
login_db=dict(type='str'),
positional_args=dict(type='list'),
positional_args=dict(type='list', elements='raw'),
named_args=dict(type='dict'),
single_transaction=dict(type='bool', default=False),
)
@ -210,6 +234,7 @@ def main():
query_result = []
executed_queries = []
rowcount = []
execution_time_ms = []
already_exists = False
for q in query:
@ -220,7 +245,8 @@ def main():
category=mysql_driver.Warning)
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:
# When something is run with IF NOT EXISTS
# and there's "already exists" MySQL warning,
@ -277,6 +303,7 @@ def main():
'executed_queries': executed_queries,
'query_result': query_result,
'rowcount': rowcount,
'execution_time_ms': execution_time_ms,
}
# Exit:

View file

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# 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
# 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'''
---
module: mysql_replication
short_description: Manage MySQL replication
short_description: Manage MySQL or MariaDB replication
description:
- Manages MySQL server replication, replica, primary status, get and change primary host.
- Manages MySQL or MariaDB server replication, replica, primary status, get and change primary host.
author:
- Balazs Pocze (@banyek)
- Andrew Klychkov (@Andersson007)
- Dennis Urtubia (@dennisurtubia)
- Laurent Indermühle (@laurent-indermuehle)
options:
mode:
description:
- Module operating mode. Could be
C(changeprimary) (CHANGE 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(getreplica) (SHOW REPLICA STATUS),
C(startreplica) (START REPLICA),
@ -34,6 +37,7 @@ options:
type: str
choices:
- changeprimary
- changereplication
- getprimary
- getreplica
- startreplica
@ -188,7 +192,8 @@ options:
version_added: '0.1.0'
notes:
- If an empty value for the parameter of string type is needed, use an empty string.
- Compatible with MariaDB or MySQL.
- If an empty value for the parameter of string type is needed, use an empty string.
attributes:
check_mode:
@ -229,6 +234,13 @@ EXAMPLES = r'''
primary_log_file: mysql-bin.000009
primary_log_pos: 4578
- name: Change replication source to replica server 192.0.2.1 and use binary log 'mysql-bin.000009' with position 4578
community.mysql.mysql_replication:
mode: changereplication
primary_host: 192.0.2.1
primary_log_file: mysql-bin.000009
primary_log_pos: 4578
- name: Check replica status using port 3308
community.mysql.mysql_replication:
mode: getreplica
@ -272,7 +284,6 @@ EXAMPLES = r'''
community.mysql.mysql_replication:
mode: changeprimary
fail_on_error: true
'''
RETURN = r'''
@ -288,7 +299,12 @@ import os
import warnings
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.mysql.plugins.module_utils.command_resolver import (
CommandResolver
)
from ansible_collections.community.mysql.plugins.module_utils.mysql import (
get_server_version,
get_server_implementation,
mysql_connect,
mysql_driver,
mysql_driver_fail_msg,
@ -299,11 +315,10 @@ from ansible.module_utils._text import to_native
executed_queries = []
def get_primary_status(cursor):
# TODO: when it's available to change on MySQL's side,
# change MASTER to PRIMARY using the approach from
# get_replica_status() function. Same for other functions.
cursor.execute("SHOW MASTER STATUS")
def get_primary_status(cursor, command_resolver):
query = command_resolver.resolve_command("SHOW MASTER STATUS")
cursor.execute(query)
primarystatus = cursor.fetchone()
return primarystatus
@ -388,8 +403,8 @@ def reset_replica_all(module, cursor, connection_name='', channel='', fail_on_er
return reset
def reset_primary(module, cursor, fail_on_error=False):
query = 'RESET MASTER'
def reset_primary(module, cursor, command_resolver, fail_on_error=False):
query = command_resolver.resolve_command('RESET MASTER')
try:
executed_queries.append(query)
cursor.execute(query)
@ -398,7 +413,7 @@ def reset_primary(module, cursor, fail_on_error=False):
reset = False
except Exception as e:
if fail_on_error:
module.fail_json(msg="RESET MASTER failed: %s" % to_native(e))
module.fail_json(msg="%s failed: %s" % (command_resolver.resolve_command('RESET MASTER'), to_native(e)))
reset = False
return reset
@ -425,11 +440,22 @@ def start_replica(module, cursor, connection_name='', channel='', fail_on_error=
return started
def changeprimary(cursor, chm, connection_name='', channel=''):
def changeprimary(cursor, command_resolver, chm, connection_name='', channel=''):
query_head = command_resolver.resolve_command("CHANGE MASTER")
if connection_name:
query = "CHANGE MASTER '%s' TO %s" % (connection_name, ','.join(chm))
query = "%s '%s' TO %s" % (query_head, connection_name, ','.join(chm))
else:
query = 'CHANGE MASTER TO %s' % ','.join(chm)
query = '%s TO %s' % (query_head, ','.join(chm))
if channel:
query += " FOR CHANNEL '%s'" % channel
executed_queries.append(query)
cursor.execute(query)
def changereplication(cursor, chm, channel=''):
query = 'CHANGE REPLICATION SOURCE TO %s' % ','.join(chm)
if channel:
query += " FOR CHANNEL '%s'" % channel
@ -449,7 +475,8 @@ def main():
'startreplica',
'resetprimary',
'resetreplica',
'resetreplicaall']),
'resetreplicaall',
'changereplication']),
primary_auto_position=dict(type='bool', default=False, aliases=['master_auto_position']),
primary_host=dict(type='str', aliases=['master_host']),
primary_user=dict(type='str', aliases=['master_user']),
@ -533,8 +560,11 @@ def main():
else:
module.fail_json(msg="unable to find %s. Exception message: %s" % (config_file, to_native(e)))
server_version = get_server_version(cursor)
server_implementation = get_server_implementation(cursor)
command_resolver = CommandResolver(server_implementation, server_version)
cursor.execute("SELECT VERSION()")
if 'mariadb' in cursor.fetchone()["VERSION()"].lower():
if server_implementation == 'mariadb':
from ansible_collections.community.mysql.plugins.module_utils.implementations.mariadb import replication as impl
else:
from ansible_collections.community.mysql.plugins.module_utils.implementations.mysql import replication as impl
@ -549,21 +579,27 @@ def main():
primary_use_gtid = 'slave_pos'
if mode == 'getprimary':
status = get_primary_status(cursor)
if not isinstance(status, dict):
status = dict(Is_Primary=False,
msg="Server is not configured as mysql primary")
else:
status = get_primary_status(cursor, command_resolver)
if status and "File" in status and "Position" in status:
status['Is_Primary'] = True
else:
status = dict(
Is_Primary=False,
msg="Server is not configured as mysql primary. "
"Meaning: Binary logs are disabled")
module.exit_json(queries=executed_queries, **status)
elif mode == "getreplica":
status = get_replica_status(cursor, connection_name, channel, replica_term)
if not isinstance(status, dict):
status = dict(Is_Replica=False, msg="Server is not configured as mysql replica")
else:
# MySQL 8.0 uses Replica_...
# MariaDB 10.6 uses Slave_...
if status and (
"Slave_IO_Running" in status or
"Replica_IO_Running" in status):
status['Is_Replica'] = True
else:
status = dict(Is_Replica=False, msg="Server is not configured as mysql replica")
module.exit_json(queries=executed_queries, **status)
@ -571,52 +607,52 @@ def main():
chm = []
result = {}
if primary_host is not None:
chm.append("MASTER_HOST='%s'" % primary_host)
chm.append("%s='%s'" % (command_resolver.resolve_command('MASTER_HOST'), primary_host))
if primary_user is not None:
chm.append("MASTER_USER='%s'" % primary_user)
chm.append("%s='%s'" % (command_resolver.resolve_command('MASTER_USER'), primary_user))
if primary_password is not None:
chm.append("MASTER_PASSWORD='%s'" % primary_password)
chm.append("%s='%s'" % (command_resolver.resolve_command('MASTER_PASSWORD'), primary_password))
if primary_port is not None:
chm.append("MASTER_PORT=%s" % primary_port)
chm.append("%s=%s" % (command_resolver.resolve_command('MASTER_PORT'), primary_port))
if primary_connect_retry is not None:
chm.append("MASTER_CONNECT_RETRY=%s" % primary_connect_retry)
chm.append("%s=%s" % (command_resolver.resolve_command('MASTER_CONNECT_RETRY'), primary_connect_retry))
if primary_log_file is not None:
chm.append("MASTER_LOG_FILE='%s'" % primary_log_file)
chm.append("%s='%s'" % (command_resolver.resolve_command('MASTER_LOG_FILE'), primary_log_file))
if primary_log_pos is not None:
chm.append("MASTER_LOG_POS=%s" % primary_log_pos)
chm.append("%s=%s" % (command_resolver.resolve_command('MASTER_LOG_POS'), primary_log_pos))
if primary_delay is not None:
chm.append("MASTER_DELAY=%s" % primary_delay)
chm.append("%s=%s" % (command_resolver.resolve_command('MASTER_DELAY'), primary_delay))
if relay_log_file is not None:
chm.append("RELAY_LOG_FILE='%s'" % relay_log_file)
if relay_log_pos is not None:
chm.append("RELAY_LOG_POS=%s" % relay_log_pos)
if primary_ssl is not None:
if primary_ssl:
chm.append("MASTER_SSL=1")
chm.append("%s=1" % command_resolver.resolve_command('MASTER_SSL'))
else:
chm.append("MASTER_SSL=0")
chm.append("%s=0" % command_resolver.resolve_command('MASTER_SSL'))
if primary_ssl_ca is not None:
chm.append("MASTER_SSL_CA='%s'" % primary_ssl_ca)
chm.append("%s='%s'" % (command_resolver.resolve_command('MASTER_SSL_CA'), primary_ssl_ca))
if primary_ssl_capath is not None:
chm.append("MASTER_SSL_CAPATH='%s'" % primary_ssl_capath)
chm.append("%s='%s'" % (command_resolver.resolve_command('MASTER_SSL_CAPATH'), primary_ssl_capath))
if primary_ssl_cert is not None:
chm.append("MASTER_SSL_CERT='%s'" % primary_ssl_cert)
chm.append("%s='%s'" % (command_resolver.resolve_command('MASTER_SSL_CERT'), primary_ssl_cert))
if primary_ssl_key is not None:
chm.append("MASTER_SSL_KEY='%s'" % primary_ssl_key)
chm.append("%s='%s'" % (command_resolver.resolve_command('MASTER_SSL_KEY'), primary_ssl_key))
if primary_ssl_cipher is not None:
chm.append("MASTER_SSL_CIPHER='%s'" % primary_ssl_cipher)
chm.append("%s='%s'" % (command_resolver.resolve_command('MASTER_SSL_CIPHER'), primary_ssl_cipher))
if primary_ssl_verify_server_cert:
chm.append("SOURCE_SSL_VERIFY_SERVER_CERT=1")
chm.append("%s=1" % command_resolver.resolve_command('MASTER_SSL_VERIFY_SERVER_CERT'))
if primary_auto_position:
chm.append("MASTER_AUTO_POSITION=1")
chm.append("%s=1" % command_resolver.resolve_command('MASTER_AUTO_POSITION'))
if primary_use_gtid is not None:
chm.append("MASTER_USE_GTID=%s" % primary_use_gtid)
chm.append("MASTER_USE_GTID=%s" % primary_use_gtid) # MariaDB only
try:
changeprimary(cursor, chm, connection_name, channel)
changeprimary(cursor, command_resolver, chm, connection_name, channel)
except mysql_driver.Warning as e:
result['warning'] = to_native(e)
except Exception as e:
module.fail_json(msg='%s. Query == CHANGE MASTER TO %s' % (to_native(e), chm))
module.fail_json(msg='%s. Query == %s TO %s' % (to_native(e), command_resolver.resolve_command('CHANGE MASTER'), chm))
result['changed'] = True
module.exit_json(queries=executed_queries, **result)
elif mode == "startreplica":
@ -632,7 +668,7 @@ def main():
else:
module.exit_json(msg="Replica already stopped", changed=False, queries=executed_queries)
elif mode == 'resetprimary':
reset = reset_primary(module, cursor, fail_on_error)
reset = reset_primary(module, cursor, command_resolver, fail_on_error)
if reset is True:
module.exit_json(msg="Primary reset", changed=True, queries=executed_queries)
else:
@ -649,6 +685,56 @@ def main():
module.exit_json(msg="Replica reset", changed=True, queries=executed_queries)
else:
module.exit_json(msg="Replica already reset", changed=False, queries=executed_queries)
elif mode == 'changereplication':
chm = []
result = {}
if primary_host is not None:
chm.append("SOURCE_HOST='%s'" % primary_host)
if primary_user is not None:
chm.append("SOURCE_USER='%s'" % primary_user)
if primary_password is not None:
chm.append("SOURCE_PASSWORD='%s'" % primary_password)
if primary_port is not None:
chm.append("SOURCE_PORT=%s" % primary_port)
if primary_connect_retry is not None:
chm.append("SOURCE_CONNECT_RETRY=%s" % primary_connect_retry)
if primary_log_file is not None:
chm.append("SOURCE_LOG_FILE='%s'" % primary_log_file)
if primary_log_pos is not None:
chm.append("SOURCE_LOG_POS=%s" % primary_log_pos)
if primary_delay is not None:
chm.append("SOURCE_DELAY=%s" % primary_delay)
if relay_log_file is not None:
chm.append("RELAY_LOG_FILE='%s'" % relay_log_file)
if relay_log_pos is not None:
chm.append("RELAY_LOG_POS=%s" % relay_log_pos)
if primary_ssl is not None:
if primary_ssl:
chm.append("SOURCE_SSL=1")
else:
chm.append("SOURCE_SSL=0")
if primary_ssl_ca is not None:
chm.append("SOURCE_SSL_CA='%s'" % primary_ssl_ca)
if primary_ssl_capath is not None:
chm.append("SOURCE_SSL_CAPATH='%s'" % primary_ssl_capath)
if primary_ssl_cert is not None:
chm.append("SOURCE_SSL_CERT='%s'" % primary_ssl_cert)
if primary_ssl_key is not None:
chm.append("SOURCE_SSL_KEY='%s'" % primary_ssl_key)
if primary_ssl_cipher is not None:
chm.append("SOURCE_SSL_CIPHER='%s'" % primary_ssl_cipher)
if primary_ssl_verify_server_cert:
chm.append("SOURCE_SSL_VERIFY_SERVER_CERT=1")
if primary_auto_position:
chm.append("SOURCE_AUTO_POSITION=1")
try:
changereplication(cursor, chm, channel)
except mysql_driver.Warning as e:
result['warning'] = to_native(e)
except Exception as e:
module.fail_json(msg='%s. Query == CHANGE REPLICATION SOURCE TO %s' % (to_native(e), chm))
result['changed'] = True
module.exit_json(queries=executed_queries, **result)
warnings.simplefilter("ignore")

View file

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

View file

@ -11,15 +11,16 @@ __metaclass__ = type
DOCUMENTATION = r'''
---
module: mysql_user
short_description: Adds or removes a user from a MySQL database
short_description: Adds or removes a user from a MySQL or MariaDB database
description:
- Adds or removes a user from a MySQL database.
- Adds or removes a user from a MySQL or MariaDB database.
options:
name:
description:
- Name of the user (role) to add or remove.
type: str
required: true
aliases: ['user']
password:
description:
- Set the user's password. Only for C(mysql_native_password) authentication.
@ -45,6 +46,7 @@ options:
priv:
description:
- "MySQL privileges string in the format: C(db.table:priv1,priv2)."
- Additionally, there must be no spaces between the table and the privilege as this will yield a non-idempotent check mode.
- "Multiple privileges can be specified by separating each one using
a forward slash: C(db.table1:priv/db.table2:priv)."
- The format is based on MySQL C(GRANT) statement.
@ -139,8 +141,16 @@ options:
description:
- User's plugin auth_string (``CREATE USER user IDENTIFIED WITH plugin BY plugin_auth_string``).
- If I(plugin) is ``pam`` (MariaDB) or ``auth_pam`` (MySQL) an optional I(plugin_auth_string) can be used to choose a specific PAM service.
- You need to define a I(salt) to have idempotence on password change with ``caching_sha2_password`` and ``sha256_password`` plugins.
type: str
version_added: '0.1.0'
salt:
description:
- Salt used to generate password hash from I(plugin_auth_string).
- Salt length must be 20 characters.
- Salt only support ``caching_sha2_password`` or ``sha256_password`` authentication I(plugin).
type: str
version_added: '3.10.0'
resource_limits:
description:
- Limit the user for certain server resources. Provided since MySQL 5.6 / MariaDB 10.2.
@ -155,6 +165,20 @@ options:
- Cannot be used to set global variables, use the M(community.mysql.mysql_variables) module instead.
type: dict
version_added: '3.6.0'
password_expire:
description:
- C(never) - I(password) will never expire.
- C(default) - I(password) is defined using global system variable I(default_password_lifetime) setting.
- C(interval) - I(password) will expire in days which is defined in I(password_expire_interval).
- C(now) - I(password) will expire immediately.
type: str
choices: [ now, never, default, interval ]
version_added: '3.9.0'
password_expire_interval:
description:
- Number of days I(password) will expire. Requires I(password_expire=interval).
type: int
version_added: '3.9.0'
column_case_sensitive:
description:
@ -166,7 +190,24 @@ options:
type: bool
version_added: '3.8.0'
locked:
description:
- Lock account to prevent connections using it.
- This is primarily used for creating a user that will act as a DEFINER on stored procedures.
- If not specified leaves the lock state as is (for a new user creates unlocked).
type: bool
version_added: '3.13.0'
attributes:
description:
- "Create, update, or delete user attributes (arbitrary 'key: value' comments) for the user."
- MySQL server must support the INFORMATION_SCHEMA.USER_ATTRIBUTES table. Provided since MySQL 8.0.
- To delete an existing attribute, set its value to null.
type: dict
version_added: '3.9.0'
notes:
- Compatible with MySQL or MariaDB.
- "MySQL server installs with default I(login_user) of C(root) and no password.
To secure this user as part of an idempotent playbook, you must create at least two tasks:
1) change the root user's password, without providing any I(login_user)/I(login_password) details,
@ -193,6 +234,7 @@ author:
- Lukasz Tomaszkiewicz (@tomaszkiewicz)
- kmarse (@kmarse)
- Laurent Indermühle (@laurent-indermuehle)
- E.S. Rosenberg (@Keeper-of-the-Keys)
extends_documentation_fragment:
- community.mysql.mysql
@ -237,7 +279,7 @@ EXAMPLES = r'''
priv: '*.*:ALL,GRANT'
state: present
session_vars:
wsrep_on: off
wsrep_on: 'off'
- name: Create user with password, all database privileges and 'WITH GRANT OPTION' in db1 and db2
community.mysql.mysql_user:
@ -257,6 +299,13 @@ EXAMPLES = r'''
FUNCTION my_db.my_function: EXECUTE
state: present
- name: Modify user attributes, creating the attribute 'foo' and removing the attribute 'bar'
community.mysql.mysql_user:
name: bob
attributes:
foo: "foo"
bar: null
- name: Modify user to require TLS connection with a valid client certificate
community.mysql.mysql_user:
name: bob
@ -340,6 +389,13 @@ EXAMPLES = r'''
priv: '*.*:ALL'
state: present
- name: Create user 'bob' authenticated with plugin 'caching_sha2_password' and static salt
community.mysql.mysql_user:
name: bob
plugin: caching_sha2_password
plugin_auth_string: password
salt: 1234567890abcdefghij
- name: Limit bob's resources to 10 queries per hour and 5 connections per hour
community.mysql.mysql_user:
name: bob
@ -354,6 +410,13 @@ EXAMPLES = r'''
priv:
'db1.*': DELETE
- name: Create locked user to act as a definer on procedures
community.mysql.mysql_user:
name: readonly_procedures_locked
locked: true
priv:
db1.*: SELECT
# Example .my.cnf file for setting the root password
# [client]
# user=root
@ -373,7 +436,6 @@ from ansible_collections.community.mysql.plugins.module_utils.mysql import (
)
from ansible_collections.community.mysql.plugins.module_utils.user import (
convert_priv_dict_to_str,
get_impl,
get_mode,
InvalidPrivsError,
limit_resources,
@ -395,7 +457,13 @@ from ansible.module_utils._text import to_native
def main():
argument_spec = mysql_common_argument_spec()
argument_spec.update(
user=dict(type='str', required=True, aliases=['name']),
name=dict(type='str', required=True, aliases=['user'], deprecated_aliases=[
{
'name': 'user',
'version': '5.0.0',
'collection_name': 'community.mysql',
}],
),
password=dict(type='str', no_log=True),
encrypted=dict(type='bool', default=False),
host=dict(type='str', default='localhost'),
@ -405,16 +473,21 @@ def main():
tls_requires=dict(type='dict'),
append_privs=dict(type='bool', default=False),
subtract_privs=dict(type='bool', default=False),
attributes=dict(type='dict'),
check_implicit_admin=dict(type='bool', default=False),
update_password=dict(type='str', default='always', choices=['always', 'on_create', 'on_new_username'], no_log=False),
sql_log_bin=dict(type='bool', default=True),
plugin=dict(default=None, type='str'),
plugin_hash_string=dict(default=None, type='str'),
plugin_auth_string=dict(default=None, type='str'),
salt=dict(default=None, type='str'),
resource_limits=dict(type='dict'),
force_context=dict(type='bool', default=False),
session_vars=dict(type='dict'),
column_case_sensitive=dict(type='bool', default=None), # TODO 4.0.0 add default=True
password_expire=dict(type='str', choices=['now', 'never', 'default', 'interval'], no_log=True),
password_expire_interval=dict(type='int', required_if=[('password_expire', 'interval', True)], no_log=True),
locked=dict(type='bool'),
)
module = AnsibleModule(
argument_spec=argument_spec,
@ -423,7 +496,7 @@ def main():
)
login_user = module.params["login_user"]
login_password = module.params["login_password"]
user = module.params["user"]
user = module.params["name"]
password = module.params["password"]
encrypted = module.boolean(module.params["encrypted"])
host = module.params["host"].lower()
@ -437,6 +510,7 @@ def main():
append_privs = module.boolean(module.params["append_privs"])
subtract_privs = module.boolean(module.params['subtract_privs'])
update_password = module.params['update_password']
attributes = module.params['attributes']
ssl_cert = module.params["client_cert"]
ssl_key = module.params["client_key"]
ssl_ca = module.params["ca_cert"]
@ -448,9 +522,13 @@ def main():
plugin = module.params["plugin"]
plugin_hash_string = module.params["plugin_hash_string"]
plugin_auth_string = module.params["plugin_auth_string"]
salt = module.params["salt"]
resource_limits = module.params["resource_limits"]
session_vars = module.params["session_vars"]
column_case_sensitive = module.params["column_case_sensitive"]
password_expire = module.params["password_expire"]
password_expire_interval = module.params["password_expire_interval"]
locked = module.boolean(module.params['locked'])
if priv and not isinstance(priv, (str, dict)):
module.fail_json(msg="priv parameter must be str or dict but %s was passed" % type(priv))
@ -461,6 +539,18 @@ def main():
if mysql_driver is None:
module.fail_json(msg=mysql_driver_fail_msg)
if password_expire_interval and password_expire_interval < 1:
module.fail_json(msg="password_expire_interval value \
should be positive number")
if salt:
if not plugin_auth_string:
module.fail_json(msg="salt requires plugin_auth_string")
if len(salt) != 20:
module.fail_json(msg="salt must be 20 characters long")
if plugin not in ['caching_sha2_password', 'sha256_password']:
module.fail_json(msg="salt requires caching_sha2_password or sha256_password plugin")
cursor = None
try:
if check_implicit_admin:
@ -490,8 +580,6 @@ def main():
if session_vars:
set_session_vars(module, cursor, session_vars)
get_impl(cursor)
if priv is not None:
try:
mode = get_mode(cursor)
@ -500,21 +588,27 @@ def main():
priv = privileges_unpack(priv, mode, column_case_sensitive, ensure_usage=not subtract_privs)
password_changed = False
final_attributes = None
if state == "present":
if user_exists(cursor, user, host, host_all):
try:
if update_password == "always":
result = user_mod(cursor, user, host, host_all, password, encrypted,
plugin, plugin_hash_string, plugin_auth_string,
priv, append_privs, subtract_privs, tls_requires, module)
plugin, plugin_hash_string, plugin_auth_string, salt,
priv, append_privs, subtract_privs, attributes, tls_requires, module,
password_expire, password_expire_interval, locked=locked)
else:
result = user_mod(cursor, user, host, host_all, None, encrypted,
None, None, None,
priv, append_privs, subtract_privs, tls_requires, module)
result = user_mod(cursor=cursor, user=user, host=host, host_all=host_all, password=None,
encrypted=encrypted, plugin=None, plugin_hash_string=None, plugin_auth_string=None,
salt=None, new_priv=priv, append_privs=append_privs, subtract_privs=subtract_privs,
attributes=attributes, tls_requires=tls_requires, module=module,
password_expire=password_expire, password_expire_interval=password_expire_interval,
locked=locked)
changed = result['changed']
msg = result['msg']
password_changed = result['password_changed']
final_attributes = result['attributes']
except (SQLParseError, InvalidPrivsError, mysql_driver.Error) as e:
module.fail_json(msg=to_native(e))
@ -526,10 +620,12 @@ def main():
priv = None # avoid granting unwanted privileges
reuse_existing_password = update_password == 'on_new_username'
result = user_add(cursor, user, host, host_all, password, encrypted,
plugin, plugin_hash_string, plugin_auth_string,
priv, tls_requires, module.check_mode, reuse_existing_password)
plugin, plugin_hash_string, plugin_auth_string, salt,
priv, attributes, tls_requires, reuse_existing_password, module,
password_expire, password_expire_interval, locked=locked)
changed = result['changed']
password_changed = result['password_changed']
final_attributes = result['attributes']
if changed:
msg = "User added"
@ -546,7 +642,7 @@ def main():
else:
changed = False
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__':

View file

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

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)
# 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)
# 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)
# 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)
# 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 #
####################################################################
- name: Prepare the fake root folder
- name: "{{ role_name }} | Main | Prepare the fake root folder"
ansible.builtin.import_tasks:
file: fake_root.yml
# setvars.yml requires the iproute2 package installed by install.yml
- name: Set variables
- name: "{{ role_name }} | Main | Set variables"
ansible.builtin.import_tasks:
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:
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"
ansible.builtin.shell:
cmd: ip route|grep default|awk '{print $3}'
register: ip_route_output
- name: "{{ role_name }} | Setvars | Install tools gather network facts"
ansible.builtin.package:
name:
- iproute2
state: present
- name: "{{ role_name }} | Setvars | Gather facts"
ansible.builtin.setup:
- name: "{{ role_name }} | Setvars | Set Fact"
ansible.builtin.set_fact:
gateway_addr: "{{ ip_route_output.stdout }}"
gateway_addr: "{{ ansible_default_ipv4.gateway }}"
connector_name_lookup: >-
{{ lookup(
'file',
@ -28,11 +32,6 @@
'file',
'/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: >-
{{ lookup(
'file',
@ -45,7 +44,6 @@
connector_version: "{{ connector_version_lookup.strip() }}"
db_engine: "{{ db_engine_name_lookup.strip() }}"
db_version: "{{ db_engine_version_lookup.strip() }}"
python_version: "{{ python_version_lookup.strip() }}"
test_ansible_version: >-
{%- if ansible_version_lookup == 'devel' -%}
{{ ansible_version_lookup }}
@ -73,7 +71,6 @@
connector_version: {{ connector_version }}
db_engine: {{ db_engine }}
db_version: {{ db_version }}
python_version: {{ python_version }}
test_ansible_version: {{ test_ansible_version }}
ansible.builtin.debug:
msg: "{{ msg.split('\n') }}"

View file

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

View file

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

View file

@ -111,11 +111,24 @@
check_implicit_admin: no
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:
that:
- result is changed
- 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)
file:
@ -339,7 +352,7 @@
assert:
that:
- result is changed
- "result.db =='{{ db_name }}'"
- result.db == db_name
# - name: Dump and Import | Assert database was backed up successfully
# 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
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
- name: Mysql_info users_info | Prepare common tests users
community.mysql.mysql_query:
@ -147,67 +147,96 @@
'*CB3326D5279DE7915FE5D743232165EE887883CA'
- GRANT SELECT ON users_info_db.* TO users_info_multi_hosts@'host2'
- >-
CREATE USER users_info_tls_none@'host'
IDENTIFIED WITH mysql_native_password AS
'*CB3326D5279DE7915FE5D743232165EE887883CA' REQUIRE NONE
- GRANT SELECT ON users_info_db.* TO users_info_tls_none@'host'
- >-
CREATE USER users_info_tls_ssl@'host'
IDENTIFIED WITH mysql_native_password AS
'*CB3326D5279DE7915FE5D743232165EE887883CA' REQUIRE SSL
- GRANT SELECT ON users_info_db.* TO users_info_tls_ssl@'host'
- >-
CREATE USER users_info_tls_cipher@'host'
IDENTIFIED WITH mysql_native_password AS
'*CB3326D5279DE7915FE5D743232165EE887883CA'
REQUIRE CIPHER 'ECDH-RSA-AES256-SHA384'
- GRANT SELECT ON users_info_db.* TO users_info_tls_cipher@'host'
- >-
CREATE USER users_info_tls_x509@'host'
IDENTIFIED WITH mysql_native_password AS
'*CB3326D5279DE7915FE5D743232165EE887883CA' REQUIRE X509
- GRANT SELECT ON users_info_db.* TO users_info_tls_x509@'host'
- >-
CREATE USER users_info_tls_subject@'host'
IDENTIFIED WITH mysql_native_password AS
'*CB3326D5279DE7915FE5D743232165EE887883CA'
REQUIRE SUBJECT '/CN=Bob/O=MyDom/C=US/ST=Oregon/L=Portland'
- GRANT SELECT ON users_info_db.* TO users_info_tls_subject@'host'
- >-
CREATE USER users_info_tls_issuer@'host'
IDENTIFIED WITH mysql_native_password AS
'*CB3326D5279DE7915FE5D743232165EE887883CA'
REQUIRE ISSUER '/C=FI/ST=Somewhere/L=City/
O=CompanyX/CN=Bob/emailAddress=bob@companyx.com'
- GRANT SELECT ON users_info_db.* TO users_info_tls_issuer@'host'
- >-
CREATE USER users_info_tls_subject_issuer@'host'
IDENTIFIED WITH mysql_native_password AS
'*CB3326D5279DE7915FE5D743232165EE887883CA'
REQUIRE SUBJECT '/CN=Bob/O=MyDom/C=US/ST=Oregon/L=Portland'
AND ISSUER '/C=FI/ST=Somewhere/L=City/
O=CompanyX/CN=Bob/emailAddress=bob@companyx.com'
- >-
GRANT SELECT ON users_info_db.*
TO users_info_tls_subject_issuer@'host'
- >-
CREATE USER users_info_tls_sub_issu_ciph@'host'
IDENTIFIED WITH mysql_native_password AS
'*CB3326D5279DE7915FE5D743232165EE887883CA'
REQUIRE SUBJECT '/CN=Bob/O=MyDom/C=US/ST=Oregon/L=Portland'
AND ISSUER '/C=FI/ST=Somewhere/L=City/
O=CompanyX/CN=Bob/emailAddress=bob@companyx.com'
AND CIPHER 'ECDH-RSA-AES256-SHA384'
- >-
GRANT SELECT ON users_info_db.*
TO users_info_tls_sub_issu_ciph@'host'
- name: Mysql_info users_info | Prepare tests users for MariaDB
community.mysql.mysql_user:
name: "{{ item.name }}"
host: "users_info.com"
plugin: "{{ item.plugin | default(omit) }}"
plugin_auth_string: "{{ item.plugin_auth_string | default(omit) }}"
plugin_hash_string: "{{ item.plugin_hash_string | default(omit) }}"
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'
community.mysql.mysql_query:
query:
- >-
CREATE USER users_info_socket@'users_info.com' IDENTIFIED WITH
unix_socket
- GRANT ALL ON *.* to users_info_socket@'users_info.com'
when:
- db_engine == 'mariadb'
- name: Mysql_info users_info | Prepare tests users for MySQL
community.mysql.mysql_user:
name: "{{ item.name }}"
host: "users_info.com"
plugin: "{{ item.plugin | default(omit) }}"
plugin_auth_string: "{{ item.plugin_auth_string | default(omit) }}"
plugin_hash_string: "{{ item.plugin_hash_string | default(omit) }}"
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'
community.mysql.mysql_query:
query:
- >-
CREATE USER users_info_sha256@'users_info.com' IDENTIFIED WITH
sha256_password BY 'msandbox'
- GRANT ALL ON *.* to users_info_sha256@'users_info.com'
when:
- db_engine == 'mysql'
- name: Mysql_info users_info | Prepare tests users for MySQL 8+
community.mysql.mysql_user:
name: "{{ item.name }}"
host: "users_info.com"
plugin: "{{ item.plugin | default(omit) }}"
plugin_auth_string: "{{ item.plugin_auth_string | default(omit) }}"
plugin_hash_string: "{{ item.plugin_hash_string | default(omit) }}"
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'
community.mysql.mysql_query:
query:
- >-
CREATE USER users_info_caching_sha2@'users_info.com' IDENTIFIED WITH
caching_sha2_password BY 'msandbox'
- GRANT ALL ON *.* to users_info_caching_sha2@'users_info.com'
when:
- db_engine == 'mysql'
- db_version is version('8.0', '>=')
@ -220,30 +249,34 @@
- users_info
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:
name: "{{ item.name }}"
host: "{{ item.host }}"
plugin: "{{ item.plugin | default(omit) }}"
plugin_auth_string: "{{ item.plugin_auth_string | default(omit) }}"
plugin_hash_string: "{{ item.plugin_hash_string | default(omit) }}"
tls_require: "{{ item.tls_require | default(omit) }}"
tls_requires: "{{ item.tls_requires | default(omit) }}"
priv: "{{ item.priv | default(omit) }}"
resource_limits: "{{ item.resource_limits | default(omit) }}"
column_case_sensitive: true
state: present
locked: "{{ item.locked | default(omit) }}"
loop: "{{ result.users_info }}"
loop_control:
label: "{{ item.name }}@{{ item.host }}"
register: recreate_users_result
failed_when:
- recreate_users_result is changed
- >-
recreate_users_result is changed or
recreate_users_result.msg != 'User unchanged'
when:
- item.name != 'root'
- item.name != 'mysql'
- item.name != 'mariadb.sys'
- item.name != 'mysql.sys'
- item.name != 'mysql.infoschema'
- item.name != 'mysql.session'
# ================================== Cleanup ============================
@ -265,6 +298,14 @@
- users_info_usage_only
- users_info_columns_uppercase
- users_info_multi_hosts
- users_info_tls_none
- users_info_tls_ssl
- users_info_tls_cipher
- users_info_tls_x509
- users_info_tls_subject
- users_info_tls_issuer
- users_info_tls_subject_issuer
- users_info_tls_sub_issu_ciph
- name: Mysql_info users_info | Cleanup databases
community.mysql.mysql_db:

View file

@ -5,7 +5,7 @@
####################################################################
# 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)
###################
@ -56,6 +56,7 @@
- result.databases != {}
- result.engines != {}
- result.users != {}
- result.server_engine == 'MariaDB' or result.server_engine == 'MySQL'
- name: mysql_info - Test connector informations display
ansible.builtin.import_tasks:
@ -131,94 +132,11 @@
- result.global_status is not defined
- result.users is not defined
# Test exclude_fields: db_size
# 'unsupported' element is passed to check that an unsupported value
# won't break anything (will be ignored regarding to the module's documentation).
- name: Collect info about databases excluding their sizes
mysql_info:
<<: *mysql_params
filter:
- databases
exclude_fields:
- db_size
- unsupported
register: result
- 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
# https://github.com/ansible-collections/community.mysql/issues/204
- name: Create database containing only views
mysql_db:
<<: *mysql_params
name: allviews
- name: Create view
mysql_query:
<<: *mysql_params
login_db: allviews
query: 'CREATE VIEW v_today (today) AS SELECT CURRENT_DATE'
- name: Fetch info
mysql_info:
<<: *mysql_params
register: result
- name: Check
assert:
that:
- result.databases.allviews.size == 0
- name: Import tasks file to tests tables count in database filter
ansible.builtin.import_tasks:
file: filter_databases.yml
- name: Import tasks file to tests users_info filter
ansible.builtin.import_tasks:

View file

@ -1,6 +1,6 @@
---
# 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)
- vars:
mysql_parameters: &mysql_params
@ -35,6 +35,7 @@
that:
- result is changed
- result.executed_queries == ['CREATE TABLE {{ test_table1 }} (id int)']
- result.execution_time_ms[0] > 0
- name: Insert test data
mysql_query:
@ -52,6 +53,8 @@
- result is changed
- result.rowcount == [2, 1]
- result.executed_queries == ['INSERT INTO {{ test_table1 }} VALUES (1), (2)', 'INSERT INTO {{ test_table1 }} VALUES (3)']
- result.execution_time_ms[0] > 0
- result.execution_time_ms[1] > 0
- name: Check data in {{ test_table1 }}
mysql_query:

View file

@ -1,9 +1,10 @@
---
####################################################################
# WARNING: These are designed specifically for Ansible tests #
# and should not be used as examples of how to write Ansible roles #
####################################################################
# Copyright: (c) 2019, Andrew Klychkov (@Andersson007) <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)
# Initial CI tests of mysql_replication module:
@ -18,10 +19,15 @@
# Tests of channel parameter:
- import_tasks: mysql_replication_channel.yml
when:
- 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?
- db_engine == 'mysql' # FIXME: mariadb introduces FOR CHANNEL in 10.7
# Tests of resetprimary mode:
- import_tasks: mysql_replication_resetprimary_mode.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)
- vars:
@ -32,10 +32,36 @@
channel: '{{ test_channel }}'
register: result
- assert:
- name: Assert that run replication with channel is changed and query matches for MariaDB and MySQL < 8.0.23
ansible.builtin.assert:
that:
- result is changed
- result.queries == ["CHANGE MASTER TO MASTER_HOST='{{ mysql_host }}',MASTER_USER='{{ replication_user }}',MASTER_PASSWORD='********',MASTER_PORT={{ mysql_primary_port }},MASTER_LOG_FILE='{{ mysql_primary_status.File }}',MASTER_LOG_POS={{ mysql_primary_status.Position }} FOR CHANNEL '{{ test_channel }}'"]
- result is changed
- result.queries == result_query
when:
- >
db_engine == 'mariadb' or
(db_engine == 'mysql' and db_version is version('8.0.23', '<'))
vars:
result_query: ["CHANGE MASTER TO MASTER_HOST='{{ mysql_host }}',\
MASTER_USER='{{ replication_user }}',MASTER_PASSWORD='********',\
MASTER_PORT={{ mysql_primary_port }},MASTER_LOG_FILE=\
'{{ mysql_primary_status.File }}',MASTER_LOG_POS=\
{{ mysql_primary_status.Position }} FOR CHANNEL '{{ test_channel }}'"]
- name: Assert that run replication with channel is changed and query matches for MySQL >= 8.0.23
ansible.builtin.assert:
that:
- result is changed
- result.queries == result_query
when:
- db_engine == 'mysql'
- db_version is version('8.0.23', '>=')
vars:
result_query: ["CHANGE REPLICATION SOURCE TO SOURCE_HOST='{{ mysql_host }}',\
SOURCE_USER='{{ replication_user }}',SOURCE_PASSWORD='********',\
SOURCE_PORT={{ mysql_primary_port }},SOURCE_LOG_FILE=\
'{{ mysql_primary_status.File }}',SOURCE_LOG_POS=\
{{ mysql_primary_status.Position }} FOR CHANNEL '{{ test_channel }}'"]
# Test startreplica mode:
- name: Start replica with channel
@ -48,8 +74,11 @@
- assert:
that:
- result is changed
- result.queries == ["START SLAVE FOR CHANNEL '{{ test_channel }}'"] or result.queries == ["START REPLICA FOR CHANNEL '{{ test_channel }}'"]
- result is changed
- result.queries == result_query or result_query2
vars:
result_query: ["START SLAVE FOR CHANNEL '{{ test_channel }}'"]
result_query2: ["START REPLICA FOR CHANNEL '{{ test_channel }}'"]
# Test getreplica mode:
- name: Get standby status with channel
@ -62,27 +91,40 @@
- assert:
that:
- replica_status.Is_Replica == true
- replica_status.Master_Host == '{{ mysql_host }}'
- replica_status.Exec_Master_Log_Pos == mysql_primary_status.Position
- replica_status.Master_Port == {{ mysql_primary_port }}
- replica_status.Last_IO_Errno == 0
- replica_status.Last_IO_Error == ''
- replica_status.Channel_Name == '{{ test_channel }}'
- replica_status is not changed
when: mysql8022_and_higher == false
- replica_status.Is_Replica is truthy(convert_bool=True)
- replica_status.Master_Host == mysql_host_value
- replica_status.Exec_Master_Log_Pos == mysql_primary_status.Position
- replica_status.Master_Port == mysql_primary_port_value
- replica_status.Last_IO_Errno == 0
- replica_status.Last_IO_Error == ''
- replica_status.Channel_Name == test_channel_value
- replica_status is not changed
vars:
mysql_host_value: '{{ mysql_host }}'
mysql_primary_port_value: '{{ mysql_primary_port }}'
test_channel_value: '{{ test_channel }}'
when:
- >
db_engine == 'mariadb' or
(db_engine == 'mysql' and db_version is version('8.0.22', '<'))
- assert:
that:
- replica_status.Is_Replica == true
- replica_status.Source_Host == '{{ mysql_host }}'
- replica_status.Exec_Source_Log_Pos == mysql_primary_status.Position
- replica_status.Source_Port == {{ mysql_primary_port }}
- replica_status.Last_IO_Errno == 0
- replica_status.Last_IO_Error == ''
- replica_status.Channel_Name == '{{ test_channel }}'
- replica_status is not changed
when: mysql8022_and_higher == true
- replica_status.Is_Replica is truthy(convert_bool=True)
- replica_status.Source_Host == mysql_host_value
- replica_status.Exec_Source_Log_Pos == mysql_primary_status.Position
- replica_status.Source_Port == mysql_primary_port_value
- replica_status.Last_IO_Errno == 0
- replica_status.Last_IO_Error == ''
- replica_status.Channel_Name == test_channel_value
- replica_status is not changed
vars:
mysql_host_value: '{{ mysql_host }}'
mysql_primary_port_value: '{{ mysql_primary_port }}'
test_channel_value: '{{ test_channel }}'
when:
- db_engine == 'mysql'
- db_version is version('8.0.22', '>=')
# Test stopreplica mode:
@ -96,8 +138,11 @@
- assert:
that:
- result is changed
- result.queries == ["STOP SLAVE FOR CHANNEL '{{ test_channel }}'"] or result.queries == ["STOP REPLICA FOR CHANNEL '{{ test_channel }}'"]
- result is changed
- result.queries == result_query or result.queries == result_query2
vars:
result_query: ["STOP SLAVE FOR CHANNEL '{{ test_channel }}'"]
result_query2: ["STOP REPLICA FOR CHANNEL '{{ test_channel }}'"]
# Test reset
- name: Reset replica with channel
@ -110,8 +155,11 @@
- assert:
that:
- result is changed
- result.queries == ["RESET SLAVE FOR CHANNEL '{{ test_channel }}'"] or result.queries == ["RESET REPLICA FOR CHANNEL '{{ test_channel }}'"]
- result is changed
- result.queries == result_query or result.queries == result_query2
vars:
result_query: ["RESET SLAVE FOR CHANNEL '{{ test_channel }}'"]
result_query2: ["RESET REPLICA FOR CHANNEL '{{ test_channel }}'"]
# Test reset all
- name: Reset replica all with channel
@ -124,5 +172,8 @@
- assert:
that:
- result is changed
- result.queries == ["RESET SLAVE ALL FOR CHANNEL '{{ test_channel }}'"] or result.queries == ["RESET REPLICA ALL FOR CHANNEL '{{ test_channel }}'"]
- result is changed
- result.queries == result_query or result.queries == result_query2
vars:
result_query: ["RESET SLAVE ALL FOR CHANNEL '{{ test_channel }}'"]
result_query2: ["RESET REPLICA ALL FOR CHANNEL '{{ test_channel }}'"]

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)
- vars:
@ -9,16 +9,6 @@
login_host: '{{ mysql_host }}'
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:
# "ansible-community/ansible-test-gh-action" uses "--retry-on-error".
@ -136,11 +126,10 @@
that:
- result is not failed
# Test changeprimary mode:
# primary_ssl_ca will be set as '' to check the module's behaviour for #23976,
# must be converted to an empty string
- name: Run replication
mysql_replication:
- name: Test changeprimary mode with empty primary_ssl_ca
community.mysql.mysql_replication:
<<: *mysql_params
login_port: '{{ mysql_replica1_port }}'
mode: changeprimary
@ -151,14 +140,40 @@
primary_log_file: '{{ mysql_primary_status.File }}'
primary_log_pos: '{{ mysql_primary_status.Position }}'
primary_ssl_ca: ''
primary_ssl: no
primary_ssl: false
register: result
- name: Assert that changeprimmary is changed and return expected query
assert:
- name: Assert that changeprimmary is changed and return expected query for MariaDB and MySQL < 8.0.23
ansible.builtin.assert:
that:
- result is changed
- result.queries == ["CHANGE MASTER TO MASTER_HOST='{{ mysql_host }}',MASTER_USER='{{ replication_user }}',MASTER_PASSWORD='********',MASTER_PORT={{ mysql_primary_port }},MASTER_LOG_FILE='{{ mysql_primary_status.File }}',MASTER_LOG_POS={{ mysql_primary_status.Position }},MASTER_SSL=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:
- name: Start replica
@ -185,26 +200,37 @@
- name: Assert that getreplica returns expected values for MySQL older than 8.0.22 and Mariadb
assert:
that:
- replica_status.Is_Replica == true
- replica_status.Master_Host == '{{ mysql_host }}'
- replica_status.Is_Replica is truthy(convert_bool=True)
- replica_status.Master_Host == mysql_host_value
- replica_status.Exec_Master_Log_Pos == mysql_primary_status.Position
- replica_status.Master_Port == {{ mysql_primary_port }}
- replica_status.Master_Port == mysql_primary_port_value
- replica_status.Last_IO_Errno == 0
- replica_status.Last_IO_Error == ''
- 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
assert:
that:
- replica_status.Is_Replica == true
- replica_status.Source_Host == '{{ mysql_host }}'
- replica_status.Is_Replica is truthy(convert_bool=True)
- replica_status.Source_Host == mysql_host_value
- replica_status.Exec_Source_Log_Pos == mysql_primary_status.Position
- replica_status.Source_Port == {{ mysql_primary_port }}
- replica_status.Source_Port == mysql_primary_port_value
- replica_status.Last_IO_Errno == 0
- replica_status.Last_IO_Error == ''
- 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:
- name: Create test table
@ -231,13 +257,18 @@
assert:
that:
- 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
assert:
that:
- 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
mysql_replication:
@ -306,5 +337,5 @@
- name: Assert that stopslave returns expected error message
assert:
that:
- result.msg == "value of mode must be one of{{ ":" }} getprimary, getreplica, changeprimary, stopreplica, startreplica, resetprimary, resetreplica, resetreplicaall, got{{ ":" }} stopslave"
- result.msg == "value of mode must be one of{{ ":" }} getprimary, getreplica, changeprimary, stopreplica, startreplica, resetprimary, resetreplica, resetreplicaall, changereplication, got{{ ":" }} stopslave"
- 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)
- vars:
@ -18,10 +18,24 @@
primary_delay: '{{ test_primary_delay }}'
register: result
- assert:
- name: Assert that run replication is changed and query match expectation for MariaDB and MySQL < 8.0.23
ansible.builtin.assert:
that:
- result is changed
- result.queries == ["CHANGE MASTER TO MASTER_DELAY=60"]
- result is changed
- result.queries == ["CHANGE MASTER TO MASTER_DELAY=60"]
when:
- >
db_engine == 'mariadb' or
(db_engine == 'mysql' and db_version is version('8.0.23', '<'))
- name: Assert that run replication is changed and query match expectation for MySQL >= 8.0.23
ansible.builtin.assert:
that:
- result is changed
- result.queries == ["CHANGE REPLICATION SOURCE TO SOURCE_DELAY=60"]
when:
- db_engine == 'mysql'
- db_version is version('8.0.23', '>=')
# Auxiliary step:
- name: Start replica

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)
- vars:
@ -38,10 +39,24 @@
mode: resetprimary
register: result
- assert:
- name: Assert that reset primary is changed and query matches for MariaDB and MySQL < 8.4
ansible.builtin.assert:
that:
- result is changed
- result.queries == ["RESET MASTER"]
- result is changed
- result.queries == ["RESET MASTER"]
when:
- >
db_engine == 'mariadb' or
(db_engine == 'mysql' and db_version is version('8.4.0', '<'))
- name: Assert that reset primary is changed and query matches for MySQL > 8.4
ansible.builtin.assert:
that:
- result is changed
- result.queries == ["RESET BINARY LOGS AND GTIDS"]
when:
- db_engine == 'mysql'
- db_version is version('8.4.0', '>=')
# Get primary final status:
- name: Get primary status

View file

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

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

View file

@ -66,7 +66,7 @@
- name: Idempotency | Remove blank user with hosts=all (expect changed)
mysql_user:
<<: *mysql_params
user: ""
name: ""
host_all: true
state: absent
register: result
@ -79,7 +79,7 @@
- name: Idempotency | Remove blank user with hosts=all (expect ok)
mysql_user:
<<: *mysql_params
user: ""
name: ""
host_all: true
state: absent
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:
- "'SSL' in reqs"
vars:
- reqs: "{{((old_result.results[0] is skipped | ternary(new_result, old_result)).results | selectattr('item', 'contains', user_name_1) | first).stdout.split('REQUIRE')[1].split(separator)[0].strip()}}"
reqs: "{{ ((old_result.results[0] is skipped | ternary(new_result, old_result)).results | selectattr('item', 'contains', user_name_1) | first).stdout.split('REQUIRE')[1].split(separator)[0].strip() }}"
- name: Tls reqs | Assert user2 TLS requirements
assert:
that:
- "'X509' in reqs"
vars:
- reqs: "{{((old_result.results[0] is skipped | ternary(new_result, old_result)).results | selectattr('item', 'contains', user_name_2) | first).stdout.split('REQUIRE')[1].split(separator)[0].strip()}}"
reqs: "{{ ((old_result.results[0] is skipped | ternary(new_result, old_result)).results | selectattr('item', 'contains', user_name_2) | first).stdout.split('REQUIRE')[1].split(separator)[0].strip() }}"
- name: Tls reqs | Assert user3 TLS requirements
assert:
@ -92,7 +92,7 @@
- "'/CN=org/O=MyDom, Inc./C=US/ST=Oregon/L=Portland' in (reqs | select('contains', 'ISSUER') | first)"
- "'ECDHE-ECDSA-AES256-SHA384' in (reqs | select('contains', 'CIPHER') | first)"
vars:
- reqs: "{{((old_result.results[0] is skipped | ternary(new_result, old_result)).results | selectattr('item', 'contains', user_name_3) | first).stdout.split('REQUIRE')[1].split(separator)[0].replace(\"' \", \"':\").split(\":\")}}"
reqs: "{{ ((old_result.results[0] is skipped | ternary(new_result, old_result)).results | selectattr('item', 'contains', user_name_3) | first).stdout.split('REQUIRE')[1].split(separator)[0].replace(\"' \", \"':\").split(\":\") }}"
# CentOS 6 uses an older version of jinja that does not provide the selectattr filter.
when: ansible_distribution != 'CentOS' or ansible_distribution_major_version != '6'
@ -129,7 +129,7 @@
assert:
that: "'SSL' in reqs"
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)
mysql_user:
@ -157,7 +157,7 @@
assert:
that: "'X509' in reqs"
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)
mysql_user:

View file

@ -127,3 +127,29 @@
update_password: on_create
- username: test3
update_password: on_new_username
# another new user, another new password and multiple existing users with
# varying passwords without providing a password
- name: update_password | Create account with on_new_username while omit password
community.mysql.mysql_user:
login_user: '{{ mysql_parameters.login_user }}'
login_password: '{{ mysql_parameters.login_password }}'
login_host: '{{ mysql_parameters.login_host }}'
login_port: '{{ mysql_parameters.login_port }}'
state: present
name: test3
host: '10.10.10.10'
update_password: on_new_username
- name: update_password | Assert create account with on_new_username while omit password produce empty auth string
ansible.builtin.command: >-
{{ mysql_command }} -BNe "SELECT user, host, plugin, authentication_string
FROM mysql.user where user='test3' and host='10.10.10.10'"
register: test3_info
changed_when: false
failed_when:
# MariaDB default plugin is mysql_native_password
- "'test3\t10.10.10.10\tmysql_native_password\t' != test3_info.stdout"
# MySQL 8+ default plugin is caching_sha2_password
- "'test3\t10.10.10.10\tcaching_sha2_password\t' != test3_info.stdout"

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_new_hash: '*E74368AC90460FA669F6D41BFB7F2A877DB73745'
test_plugin_new_auth_string: 'c$K01LsmK7nJnIR4!h'
test_salt: 'TDwqdanU82d0yNtvaabb'
test_default_priv_type: 'SELECT'
test_default_priv: '*.*:{{ test_default_priv_type }}'
@ -23,7 +24,7 @@
#
- name: Plugin auth | Create user with plugin auth (with hash string)
mysql_user:
community.mysql.mysql_user:
<<: *mysql_params
name: '{{ test_user_name }}'
host: '%'
@ -33,28 +34,28 @@
register: result
- 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
- name: Plugin auth | Check that the module made a change (with hash string)
assert:
ansible.builtin.assert:
that:
- result is changed
- name: Plugin auth | Check that the expected plugin type is set (with hash string)
assert:
ansible.builtin.assert:
that:
- "'{{ test_plugin_type }}' in show_create_user.stdout"
when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>='))
- include_tasks: utils/assert_user.yml
- ansible.builtin.include_tasks: utils/assert_user.yml
vars:
user_name: "{{ test_user_name }}"
user_host: "%"
priv: "{{ test_default_priv_type }}"
- name: Plugin auth | Get the MySQL version using the newly created creds
mysql_info:
community.mysql.mysql_info:
login_user: '{{ test_user_name }}'
login_password: '{{ test_plugin_auth_string }}'
login_host: '{{ mysql_host }}'
@ -63,12 +64,12 @@
register: result
- name: Plugin auth | Assert that mysql_info was successful
assert:
ansible.builtin.assert:
that:
- result is succeeded
- name: Plugin auth | Update the user with a different hash
mysql_user:
community.mysql.mysql_user:
<<: *mysql_params
name: '{{ test_user_name }}'
host: '%'
@ -77,18 +78,18 @@
register: result
- name: Plugin auth | Check that the module makes the change because the hash changed
assert:
ansible.builtin.assert:
that:
- result is changed
- include_tasks: utils/assert_user.yml
- ansible.builtin.include_tasks: utils/assert_user.yml
vars:
user_name: "{{ test_user_name }}"
user_host: "%"
priv: "{{ test_default_priv_type }}"
- 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_password: '{{ test_plugin_new_auth_string }}'
login_host: '{{ mysql_host }}'
@ -97,12 +98,12 @@
register: result
- name: Plugin auth | Assert that mysql_info was successful
assert:
ansible.builtin.assert:
that:
- result is succeeded
# Cleanup
- include_tasks: utils/remove_user.yml
- ansible.builtin.include_tasks: utils/remove_user.yml
vars:
user_name: "{{ test_user_name }}"
@ -111,7 +112,7 @@
#
- name: Plugin auth | Create user with plugin auth (with hash string)
mysql_user:
community.mysql.mysql_user:
<<: *mysql_params
name: '{{ test_user_name }}'
host: '%'
@ -121,28 +122,28 @@
register: result
- 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
- name: Plugin auth | Check that the module made a change (with hash string)
assert:
ansible.builtin.assert:
that:
- result is changed
- name: Plugin auth | Check that the expected plugin type is set (with hash string)
assert:
ansible.builtin.assert:
that:
- "'{{ test_plugin_type }}' in show_create_user.stdout"
when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>='))
- include_tasks: utils/assert_user.yml
- ansible.builtin.include_tasks: utils/assert_user.yml
vars:
user_name: "{{ test_user_name }}"
user_host: "%"
priv: "{{ test_default_priv_type }}"
- name: Plugin auth | Get the MySQL version using the newly created creds
mysql_info:
community.mysql.mysql_info:
login_user: '{{ test_user_name }}'
login_password: '{{ test_plugin_auth_string }}'
login_host: '{{ mysql_host }}'
@ -151,12 +152,12 @@
register: result
- name: Plugin auth | Assert that mysql_info was successful
assert:
ansible.builtin.assert:
that:
- result is succeeded
- name: Plugin auth | Update the user with the same hash (no change expected)
mysql_user:
community.mysql.mysql_user:
<<: *mysql_params
name: '{{ test_user_name }}'
host: '%'
@ -166,19 +167,19 @@
# 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
assert:
ansible.builtin.assert:
that:
- result is not changed
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:
user_name: "{{ test_user_name }}"
user_host: "%"
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
mysql_user:
community.mysql.mysql_user:
<<: *mysql_params
name: '{{ test_user_name }}'
host: '%'
@ -188,12 +189,12 @@
# Expecting a change is currently by design (see comment in source).
- name: Plugin auth | Check that the module did not change the password
assert:
ansible.builtin.assert:
that:
- result is changed
- name: Plugin auth | Getting the MySQL info should still work
mysql_info:
community.mysql.mysql_info:
login_user: '{{ test_user_name }}'
login_password: '{{ test_plugin_auth_string }}'
login_host: '{{ mysql_host }}'
@ -202,12 +203,12 @@
register: result
- name: Plugin auth | Assert that mysql_info was successful
assert:
ansible.builtin.assert:
that:
- result is succeeded
# Cleanup
- include_tasks: utils/remove_user.yml
- ansible.builtin.include_tasks: utils/remove_user.yml
vars:
user_name: "{{ test_user_name }}"
@ -216,7 +217,7 @@
#
- name: Plugin auth | Create user with plugin auth (with auth string)
mysql_user:
community.mysql.mysql_user:
<<: *mysql_params
name: '{{ test_user_name }}'
host: '%'
@ -226,28 +227,28 @@
register: result
- 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
- name: Plugin auth | Check that the module made a change (with auth string)
assert:
ansible.builtin.assert:
that:
- result is changed
- name: Plugin auth | Check that the expected plugin type is set (with auth string)
assert:
ansible.builtin.assert:
that:
- test_plugin_type in show_create_user.stdout
when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>='))
- include_tasks: utils/assert_user.yml
- ansible.builtin.include_tasks: utils/assert_user.yml
vars:
user_name: "{{ test_user_name }}"
user_host: "%"
priv: "{{ test_default_priv_type }}"
- name: Plugin auth | Get the MySQL version using the newly created creds
mysql_info:
community.mysql.mysql_info:
login_user: '{{ test_user_name }}'
login_password: '{{ test_plugin_auth_string }}'
login_host: '{{ mysql_host }}'
@ -256,12 +257,12 @@
register: result
- name: Plugin auth | Assert that mysql_info was successful
assert:
ansible.builtin.assert:
that:
- result is succeeded
- name: Plugin auth | Update the user with the same auth string
mysql_user:
community.mysql.mysql_user:
<<: *mysql_params
name: '{{ test_user_name }}'
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
# 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
assert:
ansible.builtin.assert:
that:
- result is changed
- include_tasks: utils/assert_user.yml
- ansible.builtin.include_tasks: utils/assert_user.yml
vars:
user_name: "{{ test_user_name }}"
user_host: "%"
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
mysql_user:
community.mysql.mysql_user:
<<: *mysql_params
name: '{{ test_user_name }}'
host: '%'
@ -292,12 +293,12 @@
register: result
- name: Plugin auth | Check that the module did not change the password
assert:
ansible.builtin.assert:
that:
- result is not changed
- name: Plugin auth | Get the MySQL version using the newly created creds
mysql_info:
community.mysql.mysql_info:
login_user: '{{ test_user_name }}'
login_password: '{{ test_plugin_auth_string }}'
login_host: '{{ mysql_host }}'
@ -306,12 +307,12 @@
register: result
- name: Plugin auth | Assert that mysql_info was successful
assert:
ansible.builtin.assert:
that:
- result is succeeded
# Cleanup
- include_tasks: utils/remove_user.yml
- ansible.builtin.include_tasks: utils/remove_user.yml
vars:
user_name: "{{ test_user_name }}"
@ -320,7 +321,7 @@
#
- name: Plugin auth | Create user with plugin auth (empty auth string)
mysql_user:
community.mysql.mysql_user:
<<: *mysql_params
name: '{{ test_user_name }}'
host: '%'
@ -329,28 +330,28 @@
register: result
- 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
- name: Plugin auth | Check that the module made a change (empty auth string)
assert:
ansible.builtin.assert:
that:
- result is changed
- name: Plugin auth | Check that the expected plugin type is set (empty auth string)
assert:
ansible.builtin.assert:
that:
- "'{{ test_plugin_type }}' in show_create_user.stdout"
when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>='))
- include_tasks: utils/assert_user.yml
- ansible.builtin.include_tasks: utils/assert_user.yml
vars:
user_name: "{{ test_user_name }}"
user_host: "%"
priv: "{{ test_default_priv_type }}"
- 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_password: ''
login_host: '{{ mysql_host }}'
@ -360,12 +361,12 @@
ignore_errors: true
- name: Plugin auth | Assert that mysql_info was successful
assert:
ansible.builtin.assert:
that:
- result is succeeded
- 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_password: 'some_password'
login_host: '{{ mysql_host }}'
@ -375,12 +376,12 @@
ignore_errors: true
- name: Plugin auth | Assert that mysql_info failed
assert:
ansible.builtin.assert:
that:
- result is failed
- name: Plugin auth | Update the user without changing the auth mechanism
mysql_user:
community.mysql.mysql_user:
<<: *mysql_params
name: '{{ test_user_name }}'
host: '%'
@ -389,12 +390,12 @@
register: result
- name: Plugin auth | Assert that the user wasn't changed because the auth string is still empty
assert:
ansible.builtin.assert:
that:
- result is not changed
# Cleanup
- include_tasks: utils/remove_user.yml
- ansible.builtin.include_tasks: utils/remove_user.yml
vars:
user_name: "{{ test_user_name }}"
@ -414,7 +415,7 @@
block:
- name: Plugin auth | Create user with plugin auth (empty auth string)
mysql_user:
community.mysql.mysql_user:
<<: *mysql_params
name: '{{ test_user_name }}'
plugin: '{{ test_plugin_type }}'
@ -422,28 +423,28 @@
register: result
- 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
- name: Plugin auth | Check that the module made a change (empty auth string)
assert:
ansible.builtin.assert:
that:
- result is changed
- name: Plugin auth | Check that the expected plugin type is set (empty auth string)
assert:
ansible.builtin.assert:
that:
- test_plugin_type in show_create_user.stdout
when: db_engine == 'mysql' or (db_engine == 'mariadb' and db_version is version('10.3', '>='))
- include_tasks: utils/assert_user.yml
- ansible.builtin.include_tasks: utils/assert_user.yml
vars:
user_name: "{{ test_user_name }}"
user_host: localhost
priv: "{{ test_default_priv_type }}"
- name: Plugin auth | Switch user to sha256_password auth plugin
mysql_user:
community.mysql.mysql_user:
<<: *mysql_params
name: '{{ test_user_name }}'
plugin: sha256_password
@ -451,27 +452,188 @@
register: result
- 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
- name: Plugin auth | Check that the module made a change (sha256_password)
assert:
ansible.builtin.assert:
that:
- result is changed
- name: Plugin auth | Check that the expected plugin type is set (sha256_password)
assert:
ansible.builtin.assert:
that:
- "'sha256_password' in show_create_user.stdout"
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:
user_name: "{{ test_user_name }}"
user_host: localhost
priv: "{{ test_default_priv_type }}"
# Cleanup
- include_tasks: utils/remove_user.yml
- ansible.builtin.include_tasks: utils/remove_user.yml
vars:
user_name: "{{ test_user_name }}"
# ============================================================
# Test plugin auth with a salt
#
- name: Plugin auth | Create user with plugin auth and salt
community.mysql.mysql_user:
<<: *mysql_params
name: "{{ test_user_name }}"
host: "%"
plugin: caching_sha2_password
plugin_auth_string: "{{ test_plugin_auth_string }}"
salt: "{{ test_salt }}"
priv: "{{ test_default_priv }}"
- name: Plugin auth | Connect with user and password
ansible.builtin.command: '{{ mysql_command }} -u {{ test_user_name }} -p{{ test_plugin_auth_string }} -e "SELECT 1"'
- name: Plugin auth | Alter user with same plugin auth and same salt
community.mysql.mysql_user:
<<: *mysql_params
name: "{{ test_user_name }}"
host: "%"
plugin: caching_sha2_password
plugin_auth_string: "{{ test_plugin_auth_string }}"
salt: "{{ test_salt }}"
priv: "{{ test_default_priv }}"
register: result
failed_when: result is changed
- name: Cleanup user
ansible.builtin.include_tasks: utils/remove_user.yml
vars:
user_name: "{{ test_user_name }}"
- name: Plugin auth | Create user with too short salt (should fail)
community.mysql.mysql_user:
<<: *mysql_params
name: "{{ test_user_name }}"
host: "%"
plugin: caching_sha2_password
plugin_auth_string: "{{ test_plugin_auth_string }}"
salt: "1234567890az"
priv: "{{ test_default_priv }}"
register: result
failed_when: result is success
- name: Plugin auth | Create user with salt and no plugin auth string (should fail)
community.mysql.mysql_user:
<<: *mysql_params
name: "{{ test_user_name }}"
host: "%"
plugin: caching_sha2_password
salt: "{{ test_salt }}"
priv: "{{ test_default_priv }}"
register: result
failed_when: result is success
- name: Plugin auth | Create user with salt and plugin not handled by internal hash generation (should fail)
community.mysql.mysql_user:
<<: *mysql_params
name: "{{ test_user_name }}"
host: "%"
plugin: mysql_native_password
plugin_auth_string: "{{ test_plugin_auth_string }}"
salt: "{{ test_salt }}"
priv: "{{ test_default_priv }}"
register: result
failed_when: result is success
# ============================================================
# Test auth plugin change
#
- name: Plugin auth | Test plugin auth switching which doesn't work on pymysql < 0.9
when:
- >
connector_name != 'pymysql'
or (
connector_name == 'pymysql'
and connector_version is version('0.9', '>=')
)
block:
- name: Cleanup user
ansible.builtin.include_tasks: utils/remove_user.yml
vars:
user_name: "{{ test_user_name }}"
- name: Plugin auth | Create user with mysql_native_password
community.mysql.mysql_user:
<<: *mysql_params
name: "{{ test_user_name }}"
host: "%"
plugin: "{{ test_plugin_type }}"
password: "{{ test_plugin_auth_string }}"
priv: "{{ test_default_priv }}"
- name: Plugin auth | Check that the expected plugin type is set
ansible.builtin.include_tasks: utils/assert_plugin.yml
vars:
user_name: "{{ test_user_name }}"
plugin_type: "{{ test_plugin_type }}"
- name: Plugin auth | Connect with user and password
ansible.builtin.command: '{{ mysql_command }} -u {{ test_user_name }} -p{{ test_plugin_auth_string }} -e "SELECT 1"'
changed_when: false
- name: Plugin auth | Change auth user plugin in check mode
community.mysql.mysql_user:
<<: *mysql_params
name: "{{ test_user_name }}"
host: '%'
plugin: caching_sha2_password
plugin_auth_string: "{{ test_plugin_auth_string }}"
salt: "{{ test_salt }}"
priv: "{{ test_default_priv }}"
check_mode: true
register: result
failed_when: result is not changed
- name: Plugin auth | Check that the expected plugin type is set (not changed)
ansible.builtin.include_tasks: utils/assert_plugin.yml
vars:
user_name: "{{ test_user_name }}"
plugin_type: "{{ test_plugin_type }}"
- name: Plugin auth | Change auth user plugin
community.mysql.mysql_user:
<<: *mysql_params
name: "{{ test_user_name }}"
host: '%'
plugin: caching_sha2_password
plugin_auth_string: "{{ test_plugin_auth_string }}"
salt: "{{ test_salt }}"
priv: "{{ test_default_priv }}"
register: result
failed_when: result is not changed
- name: Plugin auth | Check that the expected (new) plugin type is set
ansible.builtin.include_tasks: utils/assert_plugin.yml
vars:
user_name: "{{ test_user_name }}"
plugin_type: caching_sha2_password
- name: Plugin auth | Change auth user plugin again (should not change)
community.mysql.mysql_user:
<<: *mysql_params
name: "{{ test_user_name }}"
host: '%'
plugin: caching_sha2_password
plugin_auth_string: "{{ test_plugin_auth_string }}"
salt: "{{ test_salt }}"
priv: "{{ test_default_priv }}"
register: result
failed_when: result is changed
- name: Plugin auth | Check that the expected (not changed) plugin type is set
ansible.builtin.include_tasks: utils/assert_plugin.yml
vars:
user_name: "{{ test_user_name }}"
plugin_type: caching_sha2_password

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

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
command: "{{ mysql_command }} \"-e SHOW VARIABLES LIKE 'have_ssl';\""
ansible.builtin.command:
cmd: "{{ mysql_command }} \"-e SHOW VARIABLES LIKE 'have_ssl';\""
register: result
- set_fact:
- name: Set tls_enabled fact
ansible.builtin.set_fact:
tls_enabled: "{{ 'YES' in result.stdout | bool | default('false', true) }}"
- vars:
@ -16,21 +19,21 @@
# ============================================================
- 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'\") }}"
dest: /tmp/cert.pem
delegate_to: localhost
- name: Drop mysql user if exists
mysql_user:
community.mysql.mysql_user:
<<: *mysql_params
name: '{{ user_name_1 }}'
host_all: true
state: absent
ignore_errors: yes
ignore_errors: true
- name: create user with ssl requirement
mysql_user:
community.mysql.mysql_user:
<<: *mysql_params
name: "{{ user_name_1 }}"
host: '%'
@ -40,7 +43,7 @@
SSL:
- name: attempt connection with newly created user (expect failure)
mysql_variables:
community.mysql.mysql_variables:
variable: '{{ set_name }}'
login_user: '{{ user_name_1 }}'
login_password: '{{ user_password_1 }}'
@ -48,22 +51,24 @@
login_port: '{{ mysql_primary_port }}'
ca_cert: /tmp/cert.pem
register: result
ignore_errors: yes
ignore_errors: true
- assert:
- name: Assert that result is failed for pymysql
ansible.builtin.assert:
that:
- result is failed
when:
- connector_name == 'pymysql'
- assert:
- name: Assert that result is success for mysqlclient
ansible.builtin.assert:
that:
- result is succeeded
when:
- connector_name != 'pymysql'
- name: attempt connection with newly created user ignoring hostname
mysql_variables:
community.mysql.mysql_variables:
variable: '{{ set_name }}'
login_user: '{{ user_name_1 }}'
login_password: '{{ user_password_1 }}'
@ -72,14 +77,12 @@
ca_cert: /tmp/cert.pem
check_hostname: no
register: result
ignore_errors: yes
- assert:
that:
- result is succeeded or 'pymysql >= 0.7.11 is required' in result.msg
ignore_errors: true
failed_when:
- result is failed or 'pymysql >= 0.7.11 is required' not in result.msg
- name: Drop mysql user
mysql_user:
community.mysql.mysql_user:
<<: *mysql_params
name: '{{ user_name_1 }}'
host_all: true

View file

@ -47,8 +47,8 @@
# Verify mysql_variable successfully updates a variable (issue:4568)
#
- set_fact:
set_name: 'delay_key_write'
set_value: 'ON'
set_name: 'delay_key_write'
set_value: 'ON'
- name: set mysql variable
mysql_variables:
@ -74,8 +74,8 @@
# Verify mysql_variable successfully updates a variable using single quotes
#
- set_fact:
set_name: 'wait_timeout'
set_value: '300'
set_name: 'wait_timeout'
set_value: '300'
- name: set mysql variable to a temp value
mysql_variables:
@ -105,8 +105,8 @@
# Verify mysql_variable successfully updates a variable using double quotes
#
- set_fact:
set_name: "wait_timeout"
set_value: "400"
set_name: "wait_timeout"
set_value: "400"
- name: set mysql variable to a temp value
mysql_variables:
@ -132,8 +132,8 @@
# Verify mysql_variable successfully updates a variable using no quotes
#
- set_fact:
set_name: wait_timeout
set_value: 500
set_name: wait_timeout
set_value: 500
- name: set mysql variable to a temp value
mysql_variables:
@ -251,8 +251,8 @@
# Verify mysql_variable works with the login_user and login_password parameters
#
- set_fact:
set_name: wait_timeout
set_value: 77
set_name: wait_timeout
set_value: 77
- name: query mysql_variable using login_user and password_password
mysql_variables:
@ -287,12 +287,105 @@
var_name: "{{set_name}}"
var_value: '{{set_value}}'
#=========================================================================
# Bugfix https://github.com/ansible-collections/community.mysql/issues/652
- name: Get server version
register: result
mysql_info:
<<: *mysql_params
- name: Set variable name when running on MySQL
set_fact:
log_slow_statements: log_slow_replica_statements
when: result.server_engine == 'MySQL'
- name: Set variable name when running on MariaDB
set_fact:
log_slow_statements: log_slow_slave_statements
when: result.server_engine == 'MariaDB'
- name: Set a boolean value using ON
mysql_variables:
<<: *mysql_params
variable: "{{ log_slow_statements }}"
value: "ON"
register: result
- name: Check that it changed
assert:
that:
- result is changed or result.msg == "Variable is already set to requested value."
- result.msg == "Variable is already set to requested value." or result.queries == ["SET GLOBAL `{{ log_slow_statements }}` = ON"]
- name: Set a boolean value again using ON
mysql_variables:
<<: *mysql_params
variable: "{{ log_slow_statements }}"
value: "ON"
register: result
- name: Check that it didn't change
assert:
that:
- result is not changed
- name: Set a boolean value again using 1
mysql_variables:
<<: *mysql_params
variable: "{{ log_slow_statements }}"
value: 1
register: result
- name: Check that it didn't change
assert:
that:
- result is not changed
- name: Set a boolean value using OFF
mysql_variables:
<<: *mysql_params
variable: "{{ log_slow_statements }}"
value: "OFF"
register: result
- name: Check that it changed
assert:
that:
- result is changed
- result.queries == ["SET GLOBAL `{{ log_slow_statements }}` = OFF"]
- name: Set a boolean value again using 0
mysql_variables:
<<: *mysql_params
variable: "{{ log_slow_statements }}"
value: 0
register: result
- name: Check that it didn't change
assert:
that:
- result is not changed
- name: Set a boolean value using on
mysql_variables:
<<: *mysql_params
variable: "{{ log_slow_statements }}"
value: "on"
register: result
- name: Check that it changed
assert:
that:
- result is changed
- result.queries == ["SET GLOBAL `{{ log_slow_statements }}` = ON"]
#============================================================
# Verify mysql_variable fails with an incorrect login_password parameter
#
- set_fact:
set_name: connect_timeout
set_value: 10
set_name: connect_timeout
set_value: 10
- name: query mysql_variable using incorrect login_password
mysql_variables:

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_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/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_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/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_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/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