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
This commit is contained in:
pratikgadiya12 2019-06-24 12:59:18 +05:30 committed by Felix Fontein
parent 372b896ed1
commit 44058e9425
5 changed files with 199 additions and 57 deletions

View file

@ -0,0 +1,2 @@
minor_changes:
- mysql_db now supports multiple databases in dump operation (https://github.com/ansible/ansible/issues/56059)

View file

@ -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 <username> -p <password> < 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__':

View file

@ -1,6 +1,7 @@
---
# defaults file for test_mysql_db
db_name: 'data'
db_name2: 'data2'
db_user1: 'datauser1'
db_user2: 'datauser2'

View file

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

View file

@ -17,7 +17,10 @@
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
# ============================================================
- 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