mysql_db 12.9 KB
Newer Older
Mark Theunissen committed
1
#!/usr/bin/python
2
# -*- coding: utf-8 -*-
Mark Theunissen committed
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

# (c) 2012, Mark Theunissen <mark.theunissen@gmail.com>
# Sponsored by Four Kitchens http://fourkitchens.com.
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.

22 23 24 25 26 27
DOCUMENTATION = '''
---
module: mysql_db
short_description: Add or remove MySQL databases from a remote host.
description:
   - Add or remove MySQL databases from a remote host.
28
version_added: "0.6"
29 30 31 32 33 34
options:
  name:
    description:
      - name of the database to add or remove
    required: true
    default: null
35
    aliases: [ db ]
36 37 38 39 40 41 42
  login_user:
    description:
      - The username used to authenticate with
    required: false
    default: null
  login_password:
    description:
43
      - The password used to authenticate with
44 45 46 47 48 49 50
    required: false
    default: null
  login_host:
    description:
      - Host running the database
    required: false
    default: localhost
51 52
  login_port:
    description:
53
      - Port of the MySQL server. Requires login_host be defined as other then localhost if login_port is used
54 55
    required: false
    default: 3306
56 57 58 59 60
  login_unix_socket:
    description:
      - The path to a Unix domain socket for local connections
    required: false
    default: null
61 62 63 64 65
  state:
    description:
      - The database state
    required: false
    default: present
66
    choices: [ "present", "absent", "dump", "import" ]
67 68 69 70 71 72 73 74 75 76
  collation:
    description:
      - Collation mode
    required: false
    default: null
  encoding:
    description:
      - Encoding mode
    required: false
    default: null
77 78
  target:
    description:
79 80
      - 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)) and gzip (C(.gz)) compressed files are supported.
asad-at-srt committed
81
    required: false
82
notes:
83
   - Requires the MySQLdb Python package on the remote host. For Ubuntu, this
Jan-Piet Mens committed
84
     is as easy as apt-get install python-mysqldb. (See M(apt).)
85
   - Both I(login_password) and I(login_user) are required when you are
86 87
     passing credentials. If none are present, the module will attempt to read
     the credentials from C(~/.my.cnf), and finally fall back to using the MySQL
Jan-Piet Mens committed
88
     default login of C(root) with no password.
89 90 91 92
requirements: [ ConfigParser ]
author: Mark Theunissen
'''

93 94
EXAMPLES = '''
# Create a new database with name 'bobdata'
95
- mysql_db: name=bobdata state=present
96 97 98 99

# Copy database dump file to remote host and restore it to database 'my_db'
- copy: src=dump.sql.bz2 dest=/tmp
- mysql_db: name=my_db state=import target=/tmp/dump.sql.bz2
100 101
'''

102
import ConfigParser
103
import os
Richard C Isaacson committed
104
import pipes
Mark Theunissen committed
105
try:
106
    import MySQLdb
Mark Theunissen committed
107
except ImportError:
108 109 110
    mysqldb_found = False
else:
    mysqldb_found = True
Mark Theunissen committed
111 112 113 114 115

# ===========================================
# MySQL module specific support methods.
#

116
def db_exists(cursor, db):
Mark Theunissen committed
117 118 119
    res = cursor.execute("SHOW DATABASES LIKE %s", (db,))
    return bool(res)

120
def db_delete(cursor, db):
Alex Quach committed
121
    query = "DROP DATABASE `%s`" % db
Mark Theunissen committed
122 123 124
    cursor.execute(query)
    return True

125
def db_dump(module, host, user, password, db_name, target, port, socket=None):
126
    cmd = module.get_bin_path('mysqldump', True)
127
    cmd += " --quick --user=%s --password=%s" % (pipes.quote(user), pipes.quote(password))
128
    if socket is not None:
Richard C Isaacson committed
129
        cmd += " --socket=%s" % pipes.quote(socket)
130
    else:
131
        cmd += " --host=%s --port=%s" % (pipes.quote(host), pipes.quote(port))
Richard C Isaacson committed
132
    cmd += " %s" % pipes.quote(db_name)
133
    if os.path.splitext(target)[-1] == '.gz':
Richard C Isaacson committed
134
        cmd = cmd + ' | gzip > ' + pipes.quote(target)
135
    elif os.path.splitext(target)[-1] == '.bz2':
Richard C Isaacson committed
136
        cmd = cmd + ' | bzip2 > ' + pipes.quote(target)
137
    else:
Richard C Isaacson committed
138 139
        cmd += " > %s" % pipes.quote(target)
    rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True)
140
    return rc, stdout, stderr
141

142
def db_import(module, host, user, password, db_name, target, port, socket=None):
143 144 145
    if not os.path.exists(target):
        return module.fail_json(msg="target %s does not exist on the host" % target)

146
    cmd = module.get_bin_path('mysql', True)
147
    cmd += " --user=%s --password=%s" % (pipes.quote(user), pipes.quote(password))
148
    if socket is not None:
Richard C Isaacson committed
149
        cmd += " --socket=%s" % pipes.quote(socket)
150
    else:
Richard C Isaacson committed
151 152
        cmd += " --host=%s --port=%s" % (pipes.quote(host), pipes.quote(port))
    cmd += " -D %s" % pipes.quote(db_name)
153
    if os.path.splitext(target)[-1] == '.gz':
154 155 156 157 158
        gunzip_path = module.get_bin_path('gunzip')
        if gunzip_path:
            rc, stdout, stderr = module.run_command('%s %s' % (gunzip_path, target))
            if rc != 0:
                return rc, stdout, stderr
159
            cmd += " < %s" % pipes.quote(os.path.splitext(target)[0])
160 161 162 163 164
            rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True)
            if rc != 0:
                return rc, stdout, stderr
            gzip_path = module.get_bin_path('gzip')
            if gzip_path:
165
                rc, stdout, stderr = module.run_command('%s %s' % (gzip_path, os.path.splitext(target)[0]))
166 167 168 169
            else:
                module.fail_json(msg="gzip command not found")
        else:
            module.fail_json(msg="gunzip command not found")
170
    elif os.path.splitext(target)[-1] == '.bz2':
171 172 173 174 175
        bunzip2_path = module.get_bin_path('bunzip2')
        if bunzip2_path:
            rc, stdout, stderr = module.run_command('%s %s' % (bunzip2_path, target))
            if rc != 0:
                return rc, stdout, stderr
176
            cmd += " < %s" % pipes.quote(os.path.splitext(target)[0])
177 178 179 180 181
            rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True)
            if rc != 0:
                return rc, stdout, stderr
            bzip2_path = module.get_bin_path('bzip2')
            if bzip2_path:
182
                rc, stdout, stderr = module.run_command('%s %s' % (bzip2_path, os.path.splitext(target)[0]))
183 184 185 186
            else:
                module.fail_json(msg="bzip2 command not found")
        else:
            module.fail_json(msg="bunzip2 command not found")
187
    else:
Richard C Isaacson committed
188
        cmd += " < %s" % pipes.quote(target)
189
        rc, stdout, stderr = module.run_command(cmd, use_unsafe_shell=True)
190
    return rc, stdout, stderr
191

192 193 194 195 196
def db_create(cursor, db, encoding, collation):
    if encoding:
        encoding = " CHARACTER SET %s" % encoding
    if collation:
        collation = " COLLATE %s" % collation
Alex Quach committed
197
    query = "CREATE DATABASE `%s`%s%s" % (db, encoding, collation)
Mark Theunissen committed
198 199 200
    res = cursor.execute(query)
    return True

201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
def strip_quotes(s):
    """ Remove surrounding single or double quotes

    >>> print strip_quotes('hello')
    hello
    >>> print strip_quotes('"hello"')
    hello
    >>> print strip_quotes("'hello'")
    hello
    >>> print strip_quotes("'hello")
    'hello

    """
    single_quote = "'"
    double_quote = '"'

    if s.startswith(single_quote) and s.endswith(single_quote):
        s = s.strip(single_quote)
    elif s.startswith(double_quote) and s.endswith(double_quote):
        s = s.strip(double_quote)
    return s


def config_get(config, section, option):
    """ Calls ConfigParser.get and strips quotes

    See: http://dev.mysql.com/doc/refman/5.0/en/option-files.html
    """
    return strip_quotes(config.get(section, option))


232 233 234
def load_mycnf():
    config = ConfigParser.RawConfigParser()
    mycnf = os.path.expanduser('~/.my.cnf')
235 236
    if not os.path.exists(mycnf):
        return False
237
    try:
238
        config.readfp(open(mycnf))
239 240 241 242 243
    except (IOError):
        return False
    # We support two forms of passwords in .my.cnf, both pass= and password=,
    # as these are both supported by MySQL.
    try:
244
        passwd = config_get(config, 'client', 'password')
245 246
    except (ConfigParser.NoOptionError):
        try:
247
            passwd = config_get(config, 'client', 'pass')
248 249 250
        except (ConfigParser.NoOptionError):
            return False
    try:
251
        creds = dict(user=config_get(config, 'client', 'user'),passwd=passwd)
252
    except (ConfigParser.NoOptionError):
253 254 255
        return False
    return creds

Mark Theunissen committed
256 257 258 259
# ===========================================
# Module execution.
#

260 261 262
def main():
    module = AnsibleModule(
        argument_spec = dict(
263 264 265
            login_user=dict(default=None),
            login_password=dict(default=None),
            login_host=dict(default="localhost"),
266
            login_port=dict(default="3306"),
267
            login_unix_socket=dict(default=None),
268
            name=dict(required=True, aliases=['db']),
269 270
            encoding=dict(default=""),
            collation=dict(default=""),
271 272
            target=dict(default=None),
            state=dict(default="present", choices=["absent", "present","dump", "import"]),
273 274 275 276 277 278
        )
    )

    if not mysqldb_found:
        module.fail_json(msg="the python mysqldb module is required")

279
    db = module.params["name"]
280 281
    encoding = module.params["encoding"]
    collation = module.params["collation"]
282
    state = module.params["state"]
283
    target = module.params["target"]
284 285 286 287

    # Either the caller passes both a username and password with which to connect to
    # mysql, or they pass neither and allow this module to read the credentials from
    # ~/.my.cnf.
288 289 290
    login_password = module.params["login_password"]
    login_user = module.params["login_user"]
    if login_user is None and login_password is None:
291 292
        mycnf_creds = load_mycnf()
        if mycnf_creds is False:
293 294
            login_user = "root"
            login_password = ""
295
        else:
296 297 298 299
            login_user = mycnf_creds["user"]
            login_password = mycnf_creds["passwd"]
    elif login_password is None or login_user is None:
        module.fail_json(msg="when supplying login arguments, both login_user and login_password must be provided")
300
    login_host = module.params["login_host"]
301

302 303
    if state in ['dump','import']:
        if target is None:
304
            module.fail_json(msg="with state=%s target is required" % (state))
305 306 307
        connect_to_db = db
    else:
        connect_to_db = 'mysql'
Mark Theunissen committed
308
    try:
309
        if module.params["login_unix_socket"]:
310
            db_connection = MySQLdb.connect(host=module.params["login_host"], unix_socket=module.params["login_unix_socket"], user=login_user, passwd=login_password, db=connect_to_db)
311 312
        elif module.params["login_port"] != "3306" and module.params["login_host"] == "localhost":
            module.fail_json(msg="login_host is required when login_port is defined, login_host cannot be localhost when login_port is defined")
313
        else:
314
            db_connection = MySQLdb.connect(host=module.params["login_host"], port=int(module.params["login_port"]), user=login_user, passwd=login_password, db=connect_to_db)
Mark Theunissen committed
315
        cursor = db_connection.cursor()
316
    except Exception, e:
317 318 319 320 321
        if "Unknown database" in str(e):
                errno, errstr = e.args
                module.fail_json(msg="ERROR: %s %s" % (errno, errstr))
        else:
                module.fail_json(msg="unable to connect, check login_user and login_password are correct, or alternatively check ~/.my.cnf contains credentials")
Mark Theunissen committed
322

323
    changed = False
324
    if db_exists(cursor, db):
Mark Theunissen committed
325
        if state == "absent":
326 327 328 329
            try:
                changed = db_delete(cursor, db)
            except Exception, e:
                module.fail_json(msg="error deleting database: " + str(e))
330
        elif state == "dump":
331 332
            rc, stdout, stderr = db_dump(module, login_host, login_user, 
                                        login_password, db, target, 
333
                                        port=module.params['login_port'],
334 335 336 337 338
                                        socket=module.params['login_unix_socket'])
            if rc != 0:
                module.fail_json(msg="%s" % stderr)
            else:
                module.exit_json(changed=True, db=db, msg=stdout)
339
        elif state == "import":
340 341
            rc, stdout, stderr = db_import(module, login_host, login_user, 
                                        login_password, db, target, 
342
                                        port=module.params['login_port'],
343 344 345 346 347
                                        socket=module.params['login_unix_socket'])
            if rc != 0:
                module.fail_json(msg="%s" % stderr)
            else:
                module.exit_json(changed=True, db=db, msg=stdout)
Mark Theunissen committed
348 349
    else:
        if state == "present":
350 351 352 353
            try:
                changed = db_create(cursor, db, encoding, collation)
            except Exception, e:
                module.fail_json(msg="error creating database: " + str(e))
354 355

    module.exit_json(changed=changed, db=db)
Mark Theunissen committed
356

357
# import module snippets
358
from ansible.module_utils.basic import *
359
main()