mysql_replication 14.1 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 243 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
        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),
            master_ssl=dict(default=None, choices=[0,1]),
            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 299 300 301 302 303
        cursor = db_connection.cursor()
    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")

    if mode in "getmaster":
        masterstatus = get_master_status(cursor)
        try:
304 305 306 307 308 309
            module.exit_json(
                File=masterstatus[0],
                Position=masterstatus[1],
                Binlog_Do_DB=masterstatus[2],
                Binlog_Ignore_DB=masterstatus[3]
            )
310 311 312 313 314 315
        except TypeError:
            module.fail_json(msg="Server is not configured as mysql master")

    elif mode in "getslave":
        slavestatus = get_slave_status(cursor)
        try:
316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
            module.exit_json(
                Slave_IO_State=slavestatus[0],
                Master_Host=slavestatus[1],
                Master_User=slavestatus[2],
                Master_Port=slavestatus[3],
                Connect_Retry=slavestatus[4],
                Master_Log_File=slavestatus[5],
                Read_Master_Log_Pos=slavestatus[6],
                Relay_Log_File=slavestatus[7],
                Relay_Log_Pos=slavestatus[8],
                Relay_Master_Log_File=slavestatus[9],
                Slave_IO_Running=slavestatus[10],
                Slave_SQL_Running=slavestatus[11],
                Replicate_Do_DB=slavestatus[12],
                Replicate_Ignore_DB=slavestatus[13],
                Replicate_Do_Table=slavestatus[14],
                Replicate_Ignore_Table=slavestatus[15],
                Replicate_Wild_Do_Table=slavestatus[16],
                Replicate_Wild_Ignore_Table=slavestatus[17],
                Last_Errno=slavestatus[18],
                Last_Error=slavestatus[19],
                Skip_Counter=slavestatus[20],
                Exec_Master_Log_Pos=slavestatus[21],
                Relay_Log_Space=slavestatus[22],
                Until_Condition=slavestatus[23],
                Until_Log_File=slavestatus[24],
                Until_Log_Pos=slavestatus[25],
                Master_SSL_Allowed=slavestatus[26],
                Master_SSL_CA_File=slavestatus[27],
                Master_SSL_CA_Path=slavestatus[28],
                Master_SSL_Cert=slavestatus[29],
                Master_SSL_Cipher=slavestatus[30],
                Master_SSL_Key=slavestatus[31],
                Seconds_Behind_Master=slavestatus[32],
                Master_SSL_Verify_Server_Cert=slavestatus[33],
                Last_IO_Errno=slavestatus[34],
                Last_IO_Error=slavestatus[35],
                Last_SQL_Errno=slavestatus[36],
                Last_SQL_Error=slavestatus[37],
                Replicate_Ignore_Server_Ids=slavestatus[38],
                Master_Server_Id=slavestatus[39]
            )
358 359
        except TypeError:
            module.fail_json(msg="Server is not configured as mysql slave")
360

361 362 363
    elif mode in "changemaster":
        print "Change master"
        chm=[]
364
        if master_host:
365 366 367 368 369 370 371 372 373 374
            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:
            chm.append("MASTER_PORT='" + master_port + "'")
        if master_connect_retry:
            chm.append("MASTER_CONNECT_RETRY='" + master_connect_retry + "'")
        if master_log_file:
375
            chm.append("MASTER_LOG_FILE='" + master_log_file + "'")
376
        if master_log_pos:
377
            chm.append("MASTER_LOG_POS=" + master_log_pos)
378
        if relay_log_file:
379
            chm.append("RELAY_LOG_FILE='" + relay_log_file + "'")
380
        if relay_log_pos:
381
            chm.append("RELAY_LOG_POS=" + relay_log_pos)
382
        if master_ssl:
383
            chm.append("MASTER_SSL=" + master_ssl)
384
        if master_ssl_ca:
385
            chm.append("MASTER_SSL_CA='" + master_ssl_ca + "'")
386
        if master_ssl_capath:
387
            chm.append("MASTER_SSL_CAPATH='" + master_ssl_capath + "'")
388
        if master_ssl_cert:
389
            chm.append("MASTER_SSL_CERT='" + master_ssl_cert + "'")
390
        if master_ssl_key:
391
            chm.append("MASTER_SSL_KEY='" + master_ssl_key + "'")
392
        if master_ssl_cipher:
393
            chm.append("MASTER_SSL_CIPTHER='" + master_ssl_cipher + "'")
394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411
        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)

# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()