mysql_replication 12 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
#!/usr/bin/python
# -*- coding: utf-8 -*-

"""

Ansible module to manage mysql replication
(c) 2013, Balazs Pocze <banyek@gawker.com>
Certain parts are taken from Mark Theunissen's mysqldb module

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.
16

17 18 19 20 21 22 23 24 25 26 27 28 29
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/>.
"""

DOCUMENTATION = '''
---
module: mysql_replication

short_description: Manage MySQL replication
30 31
description:
    - Manages MySQL server replication, slave, master status get and change master host.
32
version_added: "1.3"
33 34 35 36 37
options:
    mode:
        description:
            - module operating mode. Could be getslave (SHOW SLAVE STATUS), getmaster (SHOW MASTER STATUS), changemaster (CHANGE MASTER TO), startslave (START SLAVE), stopslave (STOP SLAVE)
        required: False
38
        choices:
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
            - getslave
            - getmaster
            - changemaster
            - stopslave
            - startslave
        default: getslave
    login_user:
        description:
            - username to connect mysql host, if defined login_password also needed.
        required: False
    login_password:
        description:
            - password to connect mysql host, if defined login_user also needed.
        required: False
    login_host:
        description:
            - mysql host to connect
        required: False
    login_unix_socket:
        description:
            - unix socket to connect mysql server
    master_host:
        description:
            - same as mysql variable
    master_user:
        description:
            - same as mysql variable
    master_password:
        description:
            - same as mysql variable
    master_port:
        description:
            - same as mysql variable
    master_connect_retry:
        description:
            - same as mysql variable
    master_log_file:
        description:
            - same as mysql variable
    master_log_pos:
        description:
            - same as mysql variable
    relay_log_file:
        description:
            - same as mysql variable
    relay_log_pos:
        description:
            - same as mysql variable
    master_ssl:
        description:
            - same as mysql variable
        possible values: 0,1
    master_ssl_ca:
        description:
            - same as mysql variable
    master_ssl_capath:
        description:
            - same as mysql variable
    master_ssl_cert:
        description:
            - same as mysql variable
    master_ssl_key:
        description:
            - same as mysql variable
    master_ssl_cipher:
        description:
            - same as mysql variable

'''

109 110 111 112 113 114 115 116 117 118 119
EXAMPLES = '''
# Stop mysql slave thread
- mysql_replication: mode=stopslave

# Get master binlog file name and binlog position
- mysql_replication: mode=getmaster

# Change master to master server 192.168.1.1 and use binary log 'mysql-bin.000009' with position 4578
- mysql_replication: mode=changemaster master_host=192.168.1.1 master_log_file=mysql-bin.000009 master_log_pos=4578
'''

120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
import ConfigParser
import os
import warnings

try:
    import MySQLdb
except ImportError:
    mysqldb_found = False
else:
    mysqldb_found = True


def get_master_status(cursor):
    cursor.execute("SHOW MASTER STATUS")
    masterstatus = cursor.fetchone()
    return masterstatus


def get_slave_status(cursor):
    cursor.execute("SHOW SLAVE STATUS")
    slavestatus = cursor.fetchone()
    return slavestatus


def stop_slave(cursor):
    try:
        cursor.execute("STOP SLAVE")
        stopped = True
    except:
        stopped = False
    return stopped


def start_slave(cursor):
154
    try:
155 156 157 158 159 160 161 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 193 194 195 196 197 198 199 200 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 232 233 234 235 236 237 238 239 240 241 242
        cursor.execute("START SLAVE")
        started = True
    except:
        started = False
    return started


def changemaster(cursor, chm):
    SQLPARAM = ",".join(chm)
    cursor.execute("CHANGE MASTER TO " + SQLPARAM)


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


def load_mycnf():
    config = ConfigParser.RawConfigParser()
    mycnf = os.path.expanduser('~/.my.cnf')
    if not os.path.exists(mycnf):
        return False
    try:
        config.readfp(open(mycnf))
    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:
        passwd = config_get(config, 'client', 'password')
    except (ConfigParser.NoOptionError):
        try:
            passwd = config_get(config, 'client', 'pass')
        except (ConfigParser.NoOptionError):
            return False

    # If .my.cnf doesn't specify a user, default to user login name
    try:
        user = config_get(config, 'client', 'user')
    except (ConfigParser.NoOptionError):
        user = getpass.getuser()
    creds = dict(user=user, passwd=passwd)
    return creds


def main():
    module = AnsibleModule(
            argument_spec = dict(
            login_user=dict(default=None),
            login_password=dict(default=None),
            login_host=dict(default="localhost"),
            login_unix_socket=dict(default=None),
            mode=dict(default="getslave", choices=["getmaster", "getslave", "changemaster", "stopslave", "startslave"]),
            master_host=dict(default=None),
            master_user=dict(default=None),
            master_password=dict(default=None),
            master_port=dict(default=None),
            master_connect_retry=dict(default=None),
            master_log_file=dict(default=None),
            master_log_pos=dict(default=None),
            relay_log_file=dict(default=None),
            relay_log_pos=dict(default=None),
243
            master_ssl=dict(default=False, type='bool'),
244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295
            master_ssl_ca=dict(default=None),
            master_ssl_capath=dict(default=None),
            master_ssl_cert=dict(default=None),
            master_ssl_key=dict(default=None),
            master_ssl_cipher=dict(default=None),
        )
    )
    user = module.params["login_user"]
    password = module.params["login_password"]
    host = module.params["login_host"]
    mode = module.params["mode"]
    master_host = module.params["master_host"]
    master_user = module.params["master_user"]
    master_password = module.params["master_password"]
    master_port = module.params["master_port"]
    master_connect_retry = module.params["master_connect_retry"]
    master_log_file = module.params["master_log_file"]
    master_log_pos = module.params["master_log_pos"]
    relay_log_file = module.params["relay_log_file"]
    relay_log_pos = module.params["relay_log_pos"]
    master_ssl = module.params["master_ssl"]
    master_ssl_ca = module.params["master_ssl_ca"]
    master_ssl_capath = module.params["master_ssl_capath"]
    master_ssl_cert = module.params["master_ssl_cert"]
    master_ssl_key = module.params["master_ssl_key"]
    master_ssl_cipher = module.params["master_ssl_cipher"]

    if not mysqldb_found:
        module.fail_json(msg="the python mysqldb module is required")
    else:
        warnings.filterwarnings('error', category=MySQLdb.Warning)

    # 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.
    login_password = module.params["login_password"]
    login_user = module.params["login_user"]
    if login_user is None and login_password is None:
        mycnf_creds = load_mycnf()
        if mycnf_creds is False:
            login_user = "root"
            login_password = ""
        else:
            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")

    try:
        if module.params["login_unix_socket"]:
            db_connection = MySQLdb.connect(host=module.params["login_host"], unix_socket=module.params["login_unix_socket"], user=login_user, passwd=login_password, db="mysql")
        else:
296
            db_connection = MySQLdb.connect(host=module.params["login_host"], user=login_user, passwd=login_password, db="mysql")
297 298
    except Exception, e:
        module.fail_json(msg="unable to connect to database, check login_user and login_password are correct or ~/.my.cnf has the credentials")
Dylan Martin committed
299 300 301 302
    try:
        cursor = db_connection.cursor(cursorclass=MySQLdb.cursors.DictCursor)
    except Exception, e:
        module.fail_json(msg="Trouble getting DictCursor from db_connection: %s" % e)
303 304 305 306

    if mode in "getmaster":
        masterstatus = get_master_status(cursor)
        try:
Dylan Martin committed
307
            module.exit_json( **masterstatus )
308 309
        except TypeError:
            module.fail_json(msg="Server is not configured as mysql master")
310 311 312 313

    elif mode in "getslave":
        slavestatus = get_slave_status(cursor)
        try:
Dylan Martin committed
314
            module.exit_json( **slavestatus )
315 316
        except TypeError:
            module.fail_json(msg="Server is not configured as mysql slave")
317

318 319 320
    elif mode in "changemaster":
        print "Change master"
        chm=[]
321
        if master_host:
322 323 324 325 326 327
            chm.append("MASTER_HOST='" + master_host + "'")
        if master_user:
            chm.append("MASTER_USER='" + master_user + "'")
        if master_password:
            chm.append("MASTER_PASSWORD='" + master_password + "'")
        if master_port:
328
            chm.append("MASTER_PORT=" + master_port)
329 330 331
        if master_connect_retry:
            chm.append("MASTER_CONNECT_RETRY='" + master_connect_retry + "'")
        if master_log_file:
332
            chm.append("MASTER_LOG_FILE='" + master_log_file + "'")
333
        if master_log_pos:
334
            chm.append("MASTER_LOG_POS=" + master_log_pos)
335
        if relay_log_file:
336
            chm.append("RELAY_LOG_FILE='" + relay_log_file + "'")
337
        if relay_log_pos:
338
            chm.append("RELAY_LOG_POS=" + relay_log_pos)
339
        if master_ssl:
340
            chm.append("MASTER_SSL=1")
341
        if master_ssl_ca:
342
            chm.append("MASTER_SSL_CA='" + master_ssl_ca + "'")
343
        if master_ssl_capath:
344
            chm.append("MASTER_SSL_CAPATH='" + master_ssl_capath + "'")
345
        if master_ssl_cert:
346
            chm.append("MASTER_SSL_CERT='" + master_ssl_cert + "'")
347
        if master_ssl_key:
348
            chm.append("MASTER_SSL_KEY='" + master_ssl_key + "'")
349
        if master_ssl_cipher:
350
            chm.append("MASTER_SSL_CIPHER='" + master_ssl_cipher + "'")
351 352 353 354 355 356 357 358 359 360 361 362 363 364 365
        changemaster(cursor,chm)
        module.exit_json(changed=True)
    elif mode in "startslave":
        started = start_slave(cursor)
        if started is True:
            module.exit_json(msg="Slave started ", changed=True)
        else:
            module.exit_json(msg="Slave already started (Or cannot be started)", changed=False)
    elif mode in "stopslave":
        stopped = stop_slave(cursor)
        if stopped is True:
            module.exit_json(msg="Slave stopped", changed=True)
        else:
            module.exit_json(msg="Slave already stopped", changed=False)

366
# import module snippets
367
from ansible.module_utils.basic import *
368
main()