diff --git a/database/mysql_replication b/database/mysql_replication new file mode 100644 index 00000000000..68357e3755e --- /dev/null +++ b/database/mysql_replication @@ -0,0 +1,399 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" + +Ansible module to manage mysql replication +(c) 2013, Balazs Pocze +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. + +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 . +""" + +DOCUMENTATION = ''' +--- +module: mysql_replication + +short_description: Manage MySQL replication +description: + - Manages MySQL server replication, slave, master status get and change master host. +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 + choices: + - 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 + +''' + +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): + try: + 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: + db_connection = MySQLdb.connect(host=module.params["login_host"], user=login_user, passwd=login_password, db="mysql") + 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: + module.exit_json( + File=masterstatus[0], + Position=masterstatus[1], + Binlog_Do_DB=masterstatus[2], + Binlog_Ignore_DB=masterstatus[3] + ) + except TypeError: + module.fail_json(msg="Server is not configured as mysql master") + + elif mode in "getslave": + slavestatus = get_slave_status(cursor) + try: + 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] + ) + except TypeError: + module.fail_json(msg="Server is not configured as mysql slave") + + elif mode in "changemaster": + print "Change master" + chm=[] + if master_host: + 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: + chm.append("MASTER_LOG_FILE='" + master_log_file + "'") + if master_log_pos: + chm.append("MASTER_LOG_POS=" + master_log_pos) + if relay_log_file: + chm.append("RELAY_LOG_FILE='" + relay_log_file + "'") + if relay_log_pos: + chm.append("RELAY_LOG_POS=" + relay_log_pos) + if master_ssl: + chm.append("MASTER_SSL=" + master_ssl) + if master_ssl_ca: + chm.append("MASTER_SSL_CA='" + master_ssl_ca + "'") + if master_ssl_capath: + chm.append("MASTER_SSL_CAPATH='" + master_ssl_capath + "'") + if master_ssl_cert: + chm.append("MASTER_SSL_CERT='" + master_ssl_cert + "'") + if master_ssl_key: + chm.append("MASTER_SSL_KEY='" + master_ssl_key + "'") + if master_ssl_cipher: + chm.append("MASTER_SSL_CIPTHER='" + master_ssl_cipher + "'") + 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 +#<> +main()