Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix mysqldump ignoring errors #403

Conversation

laurent-indermuehle
Copy link
Collaborator

SUMMARY

The mysql_db module does not display mysqldump errors when compression is used.

Fixes #256

ISSUE TYPE
  • Bugfix Pull Request
COMPONENT NAME

mysql_db

ADDITIONAL INFORMATION

The issue #256 initially was met the first time because of a too big field and a max_allowed_packet to small. As this is difficult to test, the tests were written with a MySQL view pointing to a non-existent table.

# Before
changed: [testhost]

# After
fatal: [testhost]: FAILED! => {"changed": false, "msg": "mysqldump: [Warning] Using a password on the command line interface can be insecure.\nmysqldump: Got error: 1356: View 'db2.v1' references invalid table(s) or column(s) or function(s) or definer/invoker of view lack rights to use them when using LOCK TABLES\n"}

@codecov
Copy link

codecov bot commented Jun 22, 2022

Codecov Report

Merging #403 (85696fe) into main (0df46e0) will increase coverage by 0.13%.
The diff coverage is 100.00%.

@@            Coverage Diff             @@
##             main     #403      +/-   ##
==========================================
+ Coverage   77.71%   77.84%   +0.13%     
==========================================
  Files          27       27              
  Lines        2315     2320       +5     
  Branches      558      560       +2     
==========================================
+ Hits         1799     1806       +7     
+ Misses        356      355       -1     
+ Partials      160      159       -1     
Impacted Files Coverage Δ
plugins/modules/mysql_db.py 75.50% <100.00%> (+1.10%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 0df46e0...85696fe. Read the comment docs.

@Andersson007
Copy link
Collaborator

Andersson007 commented Jun 22, 2022

@laurent-indermuehle thanks for the fix!

The CI tests have failed. Please see the logs.

Anyway I'm afraid in this implementation it'll be a breaking change.. at least for FreeBSD users as bash is not installed there by default (I read it in the official doc)
Any ideas on how to overcome it?
Maybe a new boolean option that will allow to do what you added with no by default for now to preserve backwards compatibility in systems with no bash?
Then we could properly change the default later in the next major release.

@laurent-indermuehle
Copy link
Collaborator Author

@Andersson007 I didn't thought that bash could be missing on some systems.
I like your idea of a quick fix using the bool with no by default. I'll try to implement that.

For the CI, it's my first time working with Github Actions. I see that some tests are returning a none 0 code but have no clue why. On my machine, units, integrations and sanity tests passes.

@Andersson007
Copy link
Collaborator

Andersson007 commented Jun 22, 2022

@Andersson007 I didn't thought that bash could be missing on some systems. I like your idea of a quick fix using the bool with no by default. I'll try to implement that.

Thanks!

For the CI, it's my first time working with Github Actions. I see that some tests are returning a none 0 code but have no clue why. On my machine, units, integrations and sanity tests passes.

It's because your local fork contains default versions of things we test against (listed in the setup_mysql role that prepares the test environment and is run before every target as a dependency).
There's a file https://github.com/ansible-collections/community.mysql/blob/main/.github/workflows/ansible-test-plugins.yml.
It changes the defaults in the role.

When you're debugging, the easiest (imo) algorithm is to:

  1. click Details near a failing test
  2. click the gear in the upper-right corener
  3. choose View raw logs
  4. use search in browser with FAILED, ignore things marked with ignoring

Following these steps, i can see that the error is

2022-06-22T05:52:42.2627776Z TASK [test_mysql_db : Check dumps errors | Setup test | Create 2 schemas] ******
2022-06-22T05:52:42.6390493Z �[0;31mfatal: [testhost]: FAILED! => {"changed": false, "msg": "unable to connect to database, check login_user and login_password are correct or /root/.my.cnf has the credentials. Exception message: (2003, \"Can't connect to MySQL server on 'localhost' ([Errno 99] Cannot assign requested address)\")"}�[0m
2022-06-22T05:52:42.7416213Z 

@Andersson007
Copy link
Collaborator

It looks like only tests against Python 3.6 failed

@laurent-indermuehle
Copy link
Collaborator Author

laurent-indermuehle commented Jun 22, 2022

@Andersson007 Thanks for the workflow to read GH Actions logs! It helps :)

I'm not sure what the issue is. I see so many things that looks weird to me:

  1. True instead of true or yes: https://github.com/ansible-collections/community.mysql/blob/main/tests/integration/targets/test_mysql_db/tasks/state_present_absent.yml#L22

  2. No such file or directory on Python 3.6:

TASK [test_mysql_db : remove database if it exists] 
[fatal: [testhost]: FAILED! => {
  "cmd": "'mysql -uroot -pmsandbox -P3307 --protocol=tcp -sse '\"'\"'drop database data'\"'\"''",
  "msg": "[Errno 2] No such file or directory: b\"mysql -uroot -pmsandbox -P3307 --protocol=tcp -sse 'drop database data'\": b\"mysql -uroot -pmsandbox -P3307 --protocol=tcp -sse 'drop database data'\"",
  "rc": 2}
  1. Unable to drop 'data' schema:
TASK [test_mysql_db : make sure the test database is not there] 
[changed: [testhost] => {
  "changed": true, 
  "cmd": ["mysql", "-uroot", "-pmsandbox", "-P3307", "--protocol=tcp", "data"],
  "msg": "non-zero return code", "rc": 1,
  "stderr": "mysql: ERROR 1049 (42000): Unknown database 'data'", 
}

I'm running ansible-test locally for Python 3.6. I'll see if the problem is the same on my machine.

@laurent-indermuehle
Copy link
Collaborator Author

Ok, so MySQL8 causes issue with its new authentication plugin:

TASK [test_mysql_db : state dump/import - create database]
unable to connect to database, check login_user and login_password are correct or /root/.my.cnf has the credentials. 
Exception message: cryptography is required for sha256_password or caching_sha2_password

I'm starting to wounder if using module_defaults was a good idea? https://github.com/laurent-indermuehle/community.mysql/blob/fix_mysqldump_ignore_errors/tests/integration/targets/test_mysql_db/tasks/issue_256_mysqldump_errors.yml#L6-L12

@laurent-indermuehle
Copy link
Collaborator Author

ok, I missed the blue-green 'ignoring' ;)
Many errors are the actuals tests... Searching in the row log removes the colors :P

@laurent-indermuehle
Copy link
Collaborator Author

I pushed a version with the new option "pipefail".
The tests are enhanced and continue to pass. I hope the CI will be happy thought.

Tests continues to fails using mysql 8 due to a missing package cryptography.

I added a changelog fragment and a documentation for the option. I put the "version_added" at 3.3.1 but not sure what number to put.

Do I have forgot anything?

Copy link
Collaborator

@Andersson007 Andersson007 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@laurent-indermuehle thanks!

I think it would be nice to add an example to the EXAMPLES block.

I took only a quick look today. I'll take a deeper one tomorrow or on Friday. Thanks!

changelogs/fragments/fix-256-mysql_dump-errors.yml Outdated Show resolved Hide resolved
plugins/modules/mysql_db.py Outdated Show resolved Hide resolved
plugins/modules/mysql_db.py Outdated Show resolved Hide resolved
plugins/modules/mysql_db.py Outdated Show resolved Hide resolved
@laurent-indermuehle
Copy link
Collaborator Author

@Andersson007 I'm done for today. I'm sorry for the number of commits. I'm not used to work on Ansible plugins... yet ;)

Thank you if you can review the tests and fixes.
I took the liberty to fix some stuffs related to others tests (IF EXISTS, python3-cryptography), I hope that's ok.

@Andersson007
Copy link
Collaborator

@Andersson007 I'm done for today. I'm sorry for the number of commits. I'm not used to work on Ansible plugins... yet ;)

@laurent-indermuehle no problem at all! Please do any number of commits/ask any questions needed! Thanks for working on this:)

Copy link
Collaborator

@Andersson007 Andersson007 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@laurent-indermuehle one small thing from me besides adding the example as mentioned in my yesterday's comment.

plugins/modules/mysql_db.py Outdated Show resolved Hide resolved
@laurent-indermuehle
Copy link
Collaborator Author

Great, I commited your changes. I think we are done with this PR, unless the CI tells otherwise.

I'm not 100% satisfied because, the default is highly insecure or prevent us from using compression. If a way exists to pipe commands together with Python subcommand.Popen() while preserving the return code of each command, it would be ways better. But I failed to find it. Especially in our case, where this method is wrapped by Ansible module.run_command().

Without help from an expert I can't offer a better solution.

I was happy to help, I learned alot and your guidances was very appreciated.

@Andersson007
Copy link
Collaborator

I'm not 100% satisfied because, the default is highly insecure or prevent us from using compression.

Agreed. We can change the default later in a major release

I was happy to help, I learned alot and your guidances was very appreciated.

I'm happy to help:)

@laurent-indermuehle
Copy link
Collaborator Author

After having read the documentation for the subprocess module, it seems we can use 2 Popen object and check the returned code of each.

While experimenting with that, I found the following code from mysql_db.py into the db_import() function: https://github.com/ansible-collections/community.mysql/blob/main/plugins/modules/mysql_db.py#L492-L512

I'm a bit worried about the "'Broken pipe' errors that occasionally occur" message, but aside from that, it seems to be the solution I was looking for? Right?

@Andersson007
Copy link
Collaborator

After having read the documentation for the subprocess module, it seems we can use 2 Popen object and check the returned code of each.

While experimenting with that, I found the following code from mysql_db.py into the db_import() function: https://github.com/ansible-collections/community.mysql/blob/main/plugins/modules/mysql_db.py#L492-L512

ah, forgot that the module uses the subprocess module directly

I'm a bit worried about the "'Broken pipe' errors that occasionally occur" message

Yep, sounds scary

but aside from that, it seems to be the solution I was looking for? Right?

Would you like to continue experimenting or we should merge the PR?
Of course it would be great if everything goes seamlessly without any additional options but the current solution feels (not ideal but) safer (especially considering the "Broken pipe" comment - yeah, i recalled there was the issue, then we added the use_shell to workaround it)

I'm not sure, let's ask the other maintainers, @bmalynovytch ideas?

@laurent-indermuehle
Copy link
Collaborator Author

I created a playbook and a container to test locally with a 100MB database to see if the changes affect performances.

Then I created yet another option to mysql_db called 'popen' that uses the following code to compress the dump:

import gzip
from shlex import split as shlex_split
if popen:
        p1 = subprocess.Popen(shlex_split(cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE)

        with gzip.open(shlex_quote(target), 'wb', compresslevel=5) as f:
            stdout, stderr = p1.communicate()
            f.write(stdout)
            p1.wait()

        if p1.returncode != 0:
            return p1.returncode, '', to_native(stderr)
        else:
            return 0, 'Dump done', ''

This works, the error is captured popen the same way pipefail did. But:

The good news is, that the performances are almost equivalent :

  • dump sql : 2.1s
  • dump gzip : 2.47s
  • dump gzip pipefail : 2.53s
  • dump gzip popen + gzip module : 2.9s

Without the , compresslevel=5, Python defaults to 9. Which resulted in 6+ seconds per dump. I found that 5 produce the same file size as the shell version.

I'm running out of time. I'll get back in 2 weeks. What is left to do:

  • Try to replace subprocess.Popen by module.run_command as it is recommended by Ansible best practices

  • Try to replace gzip module by chaining the output of p1 on a second subprocess, E.G.:

    compress_cmd = shlex_split(path + ' > ' + shlex_quote(target))
    p2 = module.run_command(stdin=p1.stdout, ...)
  • Try to debug broken pipe in db_import

  • Align compression code between db_dump and db_import

  • Find out how to use shlex.split from Ansible

@laurent-indermuehle
Copy link
Collaborator Author

Here is a version without the gzip module:

from shlex import split as shlex_split
[...]
    if popen:
        compress_cmd = [module.get_bin_path('gzip', True), ' > ', shlex_quote(target)]
        p1 = subprocess.Popen(shlex_split(cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        p2 = subprocess.Popen(compress_cmd, stdin=p1.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
        stdout1, stderr1 = p1.communicate()
        stdout2, stderr2 = p2.communicate()

        if p1.returncode != 0:
            return p1.returncode, '', to_native(stderr1)
        else:
            return p2.returncode, stdout2, to_native(stderr2)

Should I go this route?

I tried to use run_command(), but because it uses the .close() method, it will be impossible to pipe two run_command() together. I tried pass_fds, data, close_fds, with no luck. I got a broken pipe every time.

@Andersson007
Copy link
Collaborator

@laurent-indermuehle thanks for the experimenting!

  1. About the dependencies introduction - I would avoid it but it's only my view.

  2. About using Popen - IIRC that bug with the crashing pipe was floating, so the solution we already have in this PR feels like the safest.

@bmalynovytch @rsicart any ideas?

Copy link
Contributor

@bmalynovytch bmalynovytch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The approach is the one I would have had.
Good job !

Comment on lines -21 to 23
"{{ mysql_command }} -sse 'drop database {{ db_name }}'"
ignore_errors: True
"{{ mysql_command }} -sse 'DROP DATABASE IF EXISTS {{ db_name }}'"
ignore_errors: true

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this related to the issue ? 🤨

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No sorry. I did that when the tests were failing on python 3.6.

Should I revert that?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@laurent-indermuehle

No sorry. I did that when the tests were failing on python 3.6.

I'm curious why they fail if they are not related to the changes. If it's safe, we can leave it as is.

@Andersson007 Andersson007 merged commit 5108ca5 into ansible-collections:main Jun 30, 2022
@Andersson007
Copy link
Collaborator

@laurent-indermuehle thanks for the contribution!
@bmalynovytch thanks for reviewing!
I'll create an issue to change the default in the next major release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

mysql_db module does not detect mysqldump failure
3 participants