From 44058e942588425197208ce7976559d9a49b86d1 Mon Sep 17 00:00:00 2001 From: pratikgadiya12 Date: Mon, 24 Jun 2019 12:59:18 +0530 Subject: [PATCH] Allow multiple databases(not all) to be dumped from mysql (#56721) * Allow multiple databases(not all) to be dumped from mysql Fixes: #56059 * Altered fail message to provide atleast one database name * Minor grammatical fix --- ...w-multiple_databases_in_dump_operation.yml | 2 + .../modules/database/mysql/mysql_db.py | 124 ++++++++++-------- .../targets/mysql_db/defaults/main.yml | 1 + .../targets/mysql_db/tasks/main.yml | 6 +- .../mysql_db/tasks/state_dump_import.yml | 123 ++++++++++++++++- 5 files changed, 199 insertions(+), 57 deletions(-) create mode 100644 changelogs/fragments/56059-mysqldb-allow-multiple_databases_in_dump_operation.yml diff --git a/changelogs/fragments/56059-mysqldb-allow-multiple_databases_in_dump_operation.yml b/changelogs/fragments/56059-mysqldb-allow-multiple_databases_in_dump_operation.yml new file mode 100644 index 00000000000..f03ed31cb0e --- /dev/null +++ b/changelogs/fragments/56059-mysqldb-allow-multiple_databases_in_dump_operation.yml @@ -0,0 +1,2 @@ +minor_changes: +- mysql_db now supports multiple databases in dump operation (https://github.com/ansible/ansible/issues/56059) diff --git a/lib/ansible/modules/database/mysql/mysql_db.py b/lib/ansible/modules/database/mysql/mysql_db.py index b12d7724b2e..d4500d26313 100644 --- a/lib/ansible/modules/database/mysql/mysql_db.py +++ b/lib/ansible/modules/database/mysql/mysql_db.py @@ -22,10 +22,12 @@ version_added: "0.6" options: name: description: - - name of the database to add or remove - - name=all May only be provided if I(state) is C(dump) or C(import). - - if name=all Works like --all-databases option for mysqldump (Added in 2.0) + - name of the database to add or remove. + - I(name=all) May only be provided if I(state) is C(dump) or C(import). + - List of databases is provided with I(state=dump) only. + - if name=all Works like --all-databases option for mysqldump (Added in 2.0). required: true + type: list aliases: [ db ] state: description: @@ -81,23 +83,38 @@ EXAMPLES = r''' copy: src: dump.sql.bz2 dest: /tmp + - name: Restore database mysql_db: name: my_db state: import target: /tmp/dump.sql.bz2 +- name: Dump multiple databases + mysql_db: + state: dump + name: db_1,db_2 + target: /tmp/dump.sql + +- name: Dump multiple databases + mysql_db: + state: dump + name: + - db_1 + - db_2 + target: /tmp/dump.sql + - name: Dump all databases to hostname.sql mysql_db: state: dump name: all - target: /tmp/{{ inventory_hostname }}.sql + target: /tmp/dump.sql - name: Import file.sql similar to mysql -u -p < hostname.sql mysql_db: state: import name: all - target: /tmp/{{ inventory_hostname }}.sql + target: /tmp/dump.sql ''' import os @@ -117,12 +134,14 @@ from ansible.module_utils._text import to_native def db_exists(cursor, db): - res = cursor.execute("SHOW DATABASES LIKE %s", (db.replace("_", r"\_"),)) - return bool(res) + res = 0 + for each_db in db: + res += cursor.execute("SHOW DATABASES LIKE %s", (each_db.strip().replace("_", r"\_"),)) + return res == len(db) def db_delete(cursor, db): - query = "DROP DATABASE %s" % mysql_quote_identifier(db, 'database') + query = "DROP DATABASE %s" % mysql_quote_identifier(''.join(db), 'database') cursor.execute(query) return True @@ -150,7 +169,7 @@ def db_dump(module, host, user, password, db_name, target, all_databases, port, if all_databases: cmd += " --all-databases" else: - cmd += " %s" % shlex_quote(db_name) + cmd += " --databases {0} --skip-lock-tables".format(' '.join(db_name)) if single_transaction: cmd += " --single-transaction=true" if quick: @@ -201,7 +220,7 @@ def db_import(module, host, user, password, db_name, target, all_databases, port cmd.append("--port=%i" % port) if not all_databases: cmd.append("-D") - cmd.append(shlex_quote(db_name)) + cmd.append(shlex_quote(''.join(db_name))) comp_prog_path = None if os.path.splitext(target)[-1] == '.gz': @@ -210,7 +229,6 @@ 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) - if comp_prog_path: p1 = subprocess.Popen([comp_prog_path, '-dc', target], stdout=subprocess.PIPE, stderr=subprocess.PIPE) p2 = subprocess.Popen(cmd, stdin=p1.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -231,7 +249,7 @@ def db_import(module, host, user, password, db_name, target, all_databases, port def db_create(cursor, db, encoding, collation): query_params = dict(enc=encoding, collate=collation) - query = ['CREATE DATABASE %s' % mysql_quote_identifier(db, 'database')] + query = ['CREATE DATABASE %s' % mysql_quote_identifier(''.join(db), 'database')] if encoding: query.append("CHARACTER SET %(enc)s") if collation: @@ -253,7 +271,7 @@ def main(): login_host=dict(type='str', default='localhost'), login_port=dict(type='int', default=3306), login_unix_socket=dict(type='str'), - name=dict(type='str', required=True, aliases=['db']), + name=dict(type='list', required=True, aliases=['db']), encoding=dict(type='str', default=''), collation=dict(type='str', default=''), target=dict(type='path'), @@ -274,6 +292,9 @@ def main(): module.fail_json(msg=mysql_driver_fail_msg) db = module.params["name"] + if not db: + module.fail_json(msg="Please provide at least one database name") + encoding = module.params["encoding"] collation = module.params["collation"] state = module.params["state"] @@ -297,16 +318,20 @@ def main(): single_transaction = module.params["single_transaction"] quick = module.params["quick"] + if len(db) > 1 and state != 'dump': + module.fail_json(msg="Multiple databases is only supported with state=dump") + db_name = ' '.join(db) + if state in ['dump', 'import']: if target is None: module.fail_json(msg="with state=%s target is required" % state) - if db == 'all': - db = 'mysql' + if db == ['all']: + db = ['mysql'] all_databases = True else: all_databases = False else: - if db == 'all': + if db == ['all']: module.fail_json(msg="name is not allowed to equal 'all' unless state equals import, or dump.") try: cursor = mysql_connect(module, login_user, login_password, config_file, ssl_cert, ssl_key, ssl_ca, @@ -324,45 +349,40 @@ def main(): if db_exists(cursor, db): if state == "absent": if module.check_mode: - module.exit_json(changed=True, db=db) - else: - try: - changed = db_delete(cursor, db) - except Exception as e: - module.fail_json(msg="error deleting database: %s" % to_native(e)) - module.exit_json(changed=changed, db=db) + module.exit_json(changed=True, db=db_name) + try: + changed = db_delete(cursor, db) + except Exception as e: + module.fail_json(msg="error deleting database: %s" % to_native(e)) + module.exit_json(changed=changed, db=db_name) elif state == "dump": if module.check_mode: - module.exit_json(changed=True, db=db) + module.exit_json(changed=True, db=db_name) + 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, + ssl_ca, single_transaction, quick, ignore_tables) + if rc != 0: + module.fail_json(msg="%s" % stderr) else: - 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, - ssl_ca, single_transaction, quick, ignore_tables) - if rc != 0: - module.fail_json(msg="%s" % stderr) - else: - module.exit_json(changed=True, db=db, msg=stdout) + module.exit_json(changed=True, db=db_name, msg=stdout) elif state == "import": if module.check_mode: - module.exit_json(changed=True, db=db) + module.exit_json(changed=True, db=db_name) + 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) + if rc != 0: + module.fail_json(msg="%s" % stderr) else: - 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) - if rc != 0: - module.fail_json(msg="%s" % stderr) - else: - module.exit_json(changed=True, db=db, msg=stdout) + module.exit_json(changed=True, db=db_name, msg=stdout) elif state == "present": - if module.check_mode: - module.exit_json(changed=False, db=db) - module.exit_json(changed=False, db=db) + module.exit_json(changed=False, db=db_name) else: if state == "present": @@ -374,11 +394,11 @@ def main(): except Exception as e: module.fail_json(msg="error creating database: %s" % to_native(e), exception=traceback.format_exc()) - module.exit_json(changed=changed, db=db) + module.exit_json(changed=changed, db=db_name) elif state == "import": if module.check_mode: - module.exit_json(changed=True, db=db) + module.exit_json(changed=True, db=db_name) else: try: changed = db_create(cursor, db, encoding, collation) @@ -389,20 +409,18 @@ def main(): if rc != 0: module.fail_json(msg="%s" % stderr) else: - module.exit_json(changed=True, db=db, msg=stdout) + module.exit_json(changed=True, db=db_name, msg=stdout) except Exception as e: module.fail_json(msg="error creating database: %s" % to_native(e), exception=traceback.format_exc()) elif state == "absent": - if module.check_mode: - module.exit_json(changed=False, db=db) - module.exit_json(changed=False, db=db) + module.exit_json(changed=False, db=db_name) elif state == "dump": if module.check_mode: - module.exit_json(changed=False, db=db) - module.fail_json(msg="Cannot dump database %s - not found" % (db)) + module.exit_json(changed=False, db=db_name) + module.fail_json(msg="Cannot dump database %r - not found" % (db_name)) if __name__ == '__main__': diff --git a/test/integration/targets/mysql_db/defaults/main.yml b/test/integration/targets/mysql_db/defaults/main.yml index d2c43b5e87a..1b9d3384394 100644 --- a/test/integration/targets/mysql_db/defaults/main.yml +++ b/test/integration/targets/mysql_db/defaults/main.yml @@ -1,6 +1,7 @@ --- # defaults file for test_mysql_db db_name: 'data' +db_name2: 'data2' db_user1: 'datauser1' db_user2: 'datauser2' diff --git a/test/integration/targets/mysql_db/tasks/main.yml b/test/integration/targets/mysql_db/tasks/main.yml index bb13d4d44f2..11978488916 100644 --- a/test/integration/targets/mysql_db/tasks/main.yml +++ b/test/integration/targets/mysql_db/tasks/main.yml @@ -238,8 +238,8 @@ assert: { that: "'{{ db_user1 }}' not in result.stdout" } # ============================================================ -- include: state_dump_import.yml format_type=sql file=dbdata.sql format_msg_type=ASCII +- include: state_dump_import.yml format_type=sql file=dbdata.sql format_msg_type=ASCII file2=dump2.sql file3=dump3.sql -- include: state_dump_import.yml format_type=gz file=dbdata.gz format_msg_type=gzip +- include: state_dump_import.yml format_type=gz file=dbdata.gz format_msg_type=gzip file2=dump2.gz file3=dump3.gz -- include: state_dump_import.yml format_type=bz2 file=dbdata.bz2 format_msg_type=bzip2 +- include: state_dump_import.yml format_type=bz2 file=dbdata.bz2 format_msg_type=bzip2 file2=dump2.bz2 file3=dump3.bz2 diff --git a/test/integration/targets/mysql_db/tasks/state_dump_import.yml b/test/integration/targets/mysql_db/tasks/state_dump_import.yml index 3b1a7ff0ad4..f37871bd002 100644 --- a/test/integration/targets/mysql_db/tasks/state_dump_import.yml +++ b/test/integration/targets/mysql_db/tasks/state_dump_import.yml @@ -17,7 +17,10 @@ # along with Ansible. If not, see . # ============================================================ -- set_fact: db_file_name="{{tmp_dir}}/{{file}}" +- set_fact: + db_file_name="{{tmp_dir}}/{{file}}" + dump_file1="{{tmp_dir}}/{{file2}}" + dump_file2="{{tmp_dir}}/{{file3}}" - name: state dump/import - create database mysql_db: @@ -25,6 +28,12 @@ state: present login_unix_socket: '{{ mysql_socket }}' +- name: create database + mysql_db: + name: '{{ db_name2 }}' + state: present + login_unix_socket: '{{ mysql_socket }}' + - name: state dump/import - create table department command: mysql {{ db_name }} '-e create table department(id int, name varchar(100));' @@ -40,6 +49,12 @@ - name: state dump/import - file name should not exist file: name={{ db_file_name }} state=absent +- name: database dump file1 should not exist + file: name={{ dump_file1 }} state=absent + +- name: database dump file2 should not exist + file: name={{ dump_file2 }} state=absent + - name: state dump without department table. mysql_db: name: "{{ db_name }}" @@ -58,12 +73,78 @@ - name: state dump/import - file name should exist file: name={{ db_file_name }} state=file +- name: state dump with multiple databases in comma separated form. + mysql_db: + name: "{{ db_name }},{{ db_name2 }}" + state: dump + target: "{{ dump_file1 }}" + login_unix_socket: '{{ mysql_socket }}' + register: dump_result1 + +- name: assert successful completion of dump operation (with multiple databases in comma separated form) + assert: + that: + - "dump_result1.changed == true" + +- name: state dump - dump file1 should exist + file: name={{ dump_file1 }} state=file + +- name: state dump with multiple databases in list form via check_mode + mysql_db: + name: + - "{{ db_name }}" + - "{{ db_name2 }}" + state: dump + target: "{{ dump_file2 }}" + login_unix_socket: '{{ mysql_socket }}' + register: dump_result + check_mode: yes + +- name: assert successful completion of dump operation (with multiple databases in list form) via check mode + assert: + that: + - "dump_result.changed == true" + +- name: database dump file2 should not exist + stat: + path: "{{ dump_file2 }}" + register: stat_result + +- name: assert that check_mode does not create dump file for databases + assert: + that: + - stat_result.stat.exists is defined and not stat_result.stat.exists + +- name: state dump with multiple databases in list form. + mysql_db: + name: + - "{{ db_name }}" + - "{{ db_name2 }}" + state: dump + target: "{{ dump_file2 }}" + login_unix_socket: '{{ mysql_socket }}' + register: dump_result2 + +- name: assert successful completion of dump operation (with multiple databases in list form) + assert: + that: + - "dump_result2.changed == true" + +- name: state dump - dump file2 should exist + file: name={{ dump_file2 }} state=file + - name: state dump/import - remove database mysql_db: name: '{{ db_name }}' state: absent login_unix_socket: '{{ mysql_socket }}' +- name: remove database + mysql_db: + name: '{{ db_name2 }}' + state: absent + login_unix_socket: '{{ mysql_socket }}' + - name: test state=import to restore the database of type {{ format_type }} (expect changed=true) mysql_db: name: '{{ db_name }}' @@ -81,6 +162,34 @@ that: - "'department' not in result.stdout" +- name: test state=import to restore a database from multiple database dumped file1 + mysql_db: + name: '{{ db_name2 }}' + state: import + target: '{{ dump_file1 }}' + login_unix_socket: '{{ mysql_socket }}' + register: import_result + +- name: assert output message restored a database from dump file1 + assert: { that: "import_result.changed == true" } + +- name: remove database + mysql_db: + name: '{{ db_name2 }}' + state: absent + login_unix_socket: '{{ mysql_socket }}' + +- name: test state=import to restore a database from multiple database dumped file2 + mysql_db: + name: '{{ db_name2 }}' + state: import + target: '{{ dump_file2 }}' + login_unix_socket: '{{ mysql_socket }}' + register: import_result2 + +- name: assert output message restored a database from dump file2 + assert: { that: "import_result2.changed == true" } + - name: test state=dump to backup the database of type {{ format_type }} (expect changed=true) mysql_db: name: '{{ db_name }}' @@ -132,5 +241,17 @@ state: absent login_unix_socket: '{{ mysql_socket }}' +- name: remove database + mysql_db: + name: '{{ db_name2 }}' + state: absent + login_unix_socket: '{{ mysql_socket }}' + - name: remove file name file: name={{ db_file_name }} state=absent + +- name: remove dump file1 + file: name={{ dump_file1 }} state=absent + +- name: remove dump file2 + file: name={{ dump_file2 }} state=absent