mysql_db 10 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 53 54 55
  login_port:
    description:
      - Port of the MySQL server
    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:
Jan-Piet Mens committed
79
      - Where to dump/get the C(.sql) file
asad-at-srt committed
80
    required: false
81
notes:
82
   - Requires the MySQLdb Python package on the remote host. For Ubuntu, this
Jan-Piet Mens committed
83
     is as easy as apt-get install python-mysqldb. (See M(apt).)
84
   - Both I(login_password) and I(login_user) are required when you are
85 86
     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
87
     default login of C(root) with no password.
88 89 90 91
requirements: [ ConfigParser ]
author: Mark Theunissen
'''

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

97
import ConfigParser
98
import os
Mark Theunissen committed
99
try:
100
    import MySQLdb
Mark Theunissen committed
101
except ImportError:
102 103 104
    mysqldb_found = False
else:
    mysqldb_found = True
Mark Theunissen committed
105 106 107 108 109

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

110
def db_exists(cursor, db):
Mark Theunissen committed
111 112 113
    res = cursor.execute("SHOW DATABASES LIKE %s", (db,))
    return bool(res)

114
def db_delete(cursor, db):
Alex Quach committed
115
    query = "DROP DATABASE `%s`" % db
Mark Theunissen committed
116 117 118
    cursor.execute(query)
    return True

119
def db_dump(module, host, user, password, db_name, target, port, socket=None):
120
    cmd = module.get_bin_path('mysqldump', True)
121
    cmd += " --quick --user=%s --password='%s'" %(user, password)
122 123
    if socket is not None:
        cmd += " --socket=%s" % socket
124
    else:
125
        cmd += " --host=%s --port=%s" % (host, port)
126
    cmd += " %s" % db_name
127 128 129 130 131
    if os.path.splitext(target)[-1] == '.gz':
        cmd = cmd + ' | gzip > ' + target
    elif os.path.splitext(target)[-1] == '.bz2':
        cmd = cmd + ' | bzip2 > ' + target
    else:
132
        cmd += " > %s" % target
133 134
    rc, stdout, stderr = module.run_command(cmd)
    return rc, stdout, stderr
135

136
def db_import(module, host, user, password, db_name, target, port, socket=None):
137
    cmd = module.get_bin_path('mysql', True)
138
    cmd += " --user=%s --password='%s'" %(user, password)
139 140
    if socket is not None:
        cmd += " --socket=%s" % socket
141
    else:
142
        cmd += " --host=%s --port=%s" % (host, port)
143
    cmd += " -D %s" % db_name
144 145 146 147 148
    if os.path.splitext(target)[-1] == '.gz':
        cmd = 'gunzip < ' + target + ' | ' + cmd
    elif os.path.splitext(target)[-1] == '.bz2':
        cmd = 'bunzip2 < ' + target + ' | ' + cmd
    else:
149
        cmd += " < %s" % target
150 151
    rc, stdout, stderr = module.run_command(cmd)
    return rc, stdout, stderr
152

153 154 155 156 157
def db_create(cursor, db, encoding, collation):
    if encoding:
        encoding = " CHARACTER SET %s" % encoding
    if collation:
        collation = " COLLATE %s" % collation
Alex Quach committed
158
    query = "CREATE DATABASE `%s`%s%s" % (db, encoding, collation)
Mark Theunissen committed
159 160 161
    res = cursor.execute(query)
    return True

162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
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))


193 194 195
def load_mycnf():
    config = ConfigParser.RawConfigParser()
    mycnf = os.path.expanduser('~/.my.cnf')
196 197
    if not os.path.exists(mycnf):
        return False
198
    try:
199
        config.readfp(open(mycnf))
200 201 202 203 204
    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:
205
        passwd = config_get(config, 'client', 'password')
206 207
    except (ConfigParser.NoOptionError):
        try:
208
            passwd = config_get(config, 'client', 'pass')
209 210 211
        except (ConfigParser.NoOptionError):
            return False
    try:
212
        creds = dict(user=config_get(config, 'client', 'user'),passwd=passwd)
213
    except (ConfigParser.NoOptionError):
214 215 216
        return False
    return creds

Mark Theunissen committed
217 218 219 220
# ===========================================
# Module execution.
#

221 222 223
def main():
    module = AnsibleModule(
        argument_spec = dict(
224 225 226
            login_user=dict(default=None),
            login_password=dict(default=None),
            login_host=dict(default="localhost"),
227
            login_port=dict(default="3306"),
228
            login_unix_socket=dict(default=None),
229
            db=dict(required=True, aliases=['name']),
230 231
            encoding=dict(default=""),
            collation=dict(default=""),
232 233
            target=dict(default=None),
            state=dict(default="present", choices=["absent", "present","dump", "import"]),
234 235 236 237 238 239 240
        )
    )

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

    db = module.params["db"]
241 242
    encoding = module.params["encoding"]
    collation = module.params["collation"]
243
    state = module.params["state"]
244
    target = module.params["target"]
245 246 247 248

    # 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.
249 250 251
    login_password = module.params["login_password"]
    login_user = module.params["login_user"]
    if login_user is None and login_password is None:
252 253
        mycnf_creds = load_mycnf()
        if mycnf_creds is False:
254 255
            login_user = "root"
            login_password = ""
256
        else:
257 258 259 260
            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")
261
    login_host = module.params["login_host"]
262

263 264
    if state in ['dump','import']:
        if target is None:
265
            module.fail_json(msg="with state=%s target is required" % (state))
266 267 268
        connect_to_db = db
    else:
        connect_to_db = 'mysql'
Mark Theunissen committed
269
    try:
270
        if module.params["login_unix_socket"]:
271
            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)
272
        else:
273
            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
274
        cursor = db_connection.cursor()
275
    except Exception, e:
276
        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
277

278
    changed = False
279
    if db_exists(cursor, db):
Mark Theunissen committed
280
        if state == "absent":
281
            changed = db_delete(cursor, db)
282
        elif state == "dump":
283 284
            rc, stdout, stderr = db_dump(module, login_host, login_user, 
                                        login_password, db, target, 
285
                                        port=module.params['login_port'],
286 287 288 289 290
                                        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)
291
        elif state == "import":
292 293
            rc, stdout, stderr = db_import(module, login_host, login_user, 
                                        login_password, db, target, 
294
                                        port=module.params['login_port'],
295 296 297 298 299
                                        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
300 301
    else:
        if state == "present":
302
            changed = db_create(cursor, db, encoding, collation)
303 304

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

306
# import module snippets
307
from ansible.module_utils.basic import *
308
main()