400 lines
14 KiB
Text
400 lines
14 KiB
Text
|
#!/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.
|
||
|
|
||
|
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
|
||
|
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
|
||
|
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
|
||
|
main()
|