MySQL数据库一款非常优秀的开源数据库,很多人都在使用。我也不例外,数据库实例创建、数据库状态检测、数据库备份等,天天做着重复、枯燥的工作。为了减轻工作量,使用Python写了一个自动管理Mysql数据库的工具。


  1. 具体功能:

    Mysql管理工具的主要功能:

        > 数据库实例创建

        > 数据库备份

        > 数据库配置检测

        > 数据库主从同步

        > 配置文件重新载入


2. 工具代码结构:

    wKiom1OPABrDW_5ZAADXifau2Gw283.jpg

     library: 将共用的功能封装成一个库。

        mysqlmanager:myman.py脚本实现基本管理Mysql的功能。


3. 工具代码展示

   library/mysql.py:  

#!/usr/local/bin/python2.7
#-*- coding:utf-8 -*-

from ConfigParser import ConfigParser
import os
import MySQLdb

def getMyVariables(cur):
    '''查询数据库配置信息'''
    cur.execute('show global variables;')
    data = cur.fetchall()
    return dict(data)

class MySQLDConfig(ConfigParser):
    '''将所有公用的功能封装成一个class'''
    def __init__(self, config, **kw):
        '''Python版本必须2.7以上,2.6版本没有allow_no_value 属性'''
        ConfigParser.__init__(self, allow_no_value=True)
        self.config = config
        self.mysqld_vars = {}
        if os.path.exists(self.config):
            self.read(self.config)
            self.get_mysqld_vars()
        else:
            self.set_mysqld_defaults_var()
        self.set_mysqld_vars(kw)

    def set_mysqld_vars(self, kw):
        '''获取配置文件信息,覆盖默认配置'''
        for k, v in kw.items():
            self.mysqld_vars[k] = v

    def get_mysqld_vars(self):
        '''获取现有配置文件信息'''
        options = self.options('mysqld')
        rst = {}
        for o in options:
            rst[o] = self.get('mysqld', o)
        self.set_mysqld_vars(rst)

    def set_mysqld_defaults_var(self):
        '''如果配置文件不存在,设置默认配置'''
        defaults = {
            "user":"mysql",
            "pid-file": "/var/run/mysqld/mysqld.pid",
            "socket": "/var/lib/mysql/mysql.sock",
            "port": "3306",
            "basedir": "/usr",
            "datadir": "/tmp/mysql",
            "tmpdir": "/tmp",
            "skip-external-locking": None,
            "bind-address": "127.0.0.1",
            "key_buffer": "16M",
            "max_allowed_packet": "16M",
            "thread_stack": "192K",
            "thread_cache_size": "8",
            "myisam-recover": "BACKUP",
            "query_cache_limit": "1M",
            "query_cache_size": "16M",
            "log_error": "/var/log/mysqld.log",
            "expire_logs_days": "10",
            "max_binlog_size": "100M"
        }
        self.set_mysqld_vars(defaults)

    def save(self):
        '''将配置信息保存至配置文件'''
        if not self.has_section('mysqld'):
            self.add_section('mysqld')
        for k, v in self.mysqld_vars.items():
            self.set('mysqld', k, v)
        with open(self.config, 'w') as fd:
            self.write(fd)

if __name__  == "__main__":
    mc = MySQLDConfig('/root/david/mysqlmanager/cnfs/my.cnf', max_connection=200, user='mysql')
    mc.set_var('skip-slave-start', None)
    mc.save()

  library/utils.py

#!/usr/local/bin/python2.7
#-*-coding:utf-8 -*-
'''
    格式时间的转换,数据库配置文件的单位(*.cnf)和数据库global(mysql>show global variables;)配置的单位不一致,需要转换
'''

unit = {'t':2**40,'g':2**30,'m':2**20,'k':2**10,'b':1}

def convertUnit(s):
    s = s.lower()
    lastchar = s[-1]
    num = int(s[:-1])
    if lastchar in unit:
        return num*unit[lastchar]
    else:
        return int(s)

def scaleUnit(d):
    for k,v in unit.items():
        num = d / v
        if (0 < num < 2**10):
            return num,k

  mysqlmanager/myman.py:

 

#!/usr/local/bin/python2.7
#-*- coding:utf-8 -*-

from os import path
from optparse import OptionParser
from subprocess import PIPE, Popen
import MySQLdb
import glob
import os
import sys
import time
import datetime
import re


DIRNAME = path.dirname(__file__)
OPSTOOLS_DIR = path.abspath(path.join(DIRNAME, '..'))
sys.path.append(OPSTOOLS_DIR)
from library.mysql import MySQLDConfig, getMyVariables

REPLICATION_USER = 'repl'
REPLICATION_PASS = '123qwe'
MYSQL_DATA_DIR = '/home/david/data'
MYSQL_CONF_DIR = '/home/david/cnfs'
MYSQL_BACK_DIR = '/home/david/backup'

def opts():
    parser = OptionParser(usage="usage: %prog [options] arg1 arg2")
    parser.add_option("-c","--cmd",
        dest="cmd",
        action="store",
        default="check",
        help="Check the configuration file and database configuration parameters are different.[%options]"
    )
    parser.add_option("-n","--name",
        dest="name",
        action="store",
        default="mysqlinstance",
        help="Create Examples."
    )
    parser.add_option("-p","--port",
        dest="port",
        action="store",
        default="3306",
        help="Examples of port."
    )
    return parser.parse_args()

def checkPort(d, p):
    '''实例端口检测'''
    for m in d:
        if p == m.mysqld_vars['port']:
            return True
    return False

def setReplMaster(cur):
    '''设置slave数据库同步用户的授权'''
    sql = "GRANT REPLICATION SLAVE ON *.* TO %s@'localhost' IDENTIFIED BY '%s'" % (REPLICATION_USER, REPLICATION_PASS)
    cur.execute(sql)

def connMySQLd(mc):
    '''连接数据库'''
    host = '127.0.0.1'
    user = 'root'
    port = int(mc.mysqld_vars['port'])
    conn = MySQLdb.connect(host, port=port, user=user)
    cur = conn.cursor()
    return cur

def run_mysql(cnf):
    '''运行数据库'''
    cmd = "mysqld_safe --defaults-file=%s &" % cnf
    p = Popen(cmd, stdout=PIPE, shell=True)
    time.sleep(5)
    return p.returncode

def setOwner(p, user):
    '''设置目录权限'''
    os.system("chown -R %s:%s %s" % (user, user, p))

def mysql_install_db(cnf):
    '''数据库初始化'''
    p = Popen("mysql_install_db --defaults-file=%s" % cnf, stdout=PIPE, shell=True)
    #p = Popen("mysql_install_db --user=mysql --datadir=%s " % MYSQL_DATA_DIR, stdout=PIPE, shell=True)
    stdout, stderr = p.communicate()
    return p.returncode

def _genDict(name, port):
    '''设置文件存储目录及监听端口'''
    return {
        'pid-file': path.join(MYSQL_DATA_DIR, name, "%s.pid" % name),
        'socket': '/tmp/%s.sock' % name,
        'port': port,
        'datadir': path.join(MYSQL_DATA_DIR, name)+'/',
        'log_error': path.join(MYSQL_DATA_DIR, name)
    }

def readConfs():
    '''读取配置文件,如果配置文件不存在,使用默认配置生成配置文件'''
    confs = glob.glob(path.join(MYSQL_CONF_DIR, '*.cnf'))
    return [MySQLDConfig(c) for c in confs]

def getCNF(name):
    '''获取配置文件完整路径'''
    return path.join(MYSQL_CONF_DIR, "%s.cnf" % name)

def runMySQLdump(cmd):
    '''启动Mysql命令'''
    p = Popen(cmd, stdout=PIPE, shell=True)
    stdout, stderr = p.communicate()
    return p.returncode

def getBinlogPOS(f):
    '''获取binlog'''
    with open(f) as fd:
        f, p = findLogPos(l)
        if f and p:
            return f,p

def findLogPos(s):
    rlog = re.compile(r"MASTER_LOG_FILE='(\S+)',", re.IGNORECASE)
    rpos = re.compile(r"MASTER_LOG_POS=(\d+),?", re.IGNORECASE)
    log = rlog.search(s)
    pos = rpos.search(s)
    if log and pos:
        return log.group(1), int(pos.group(1))
    else:
        return (None, None)

def changeMaster(cur, host, port, user, mpass, mf, p):
    sql = '''CHANGE MASTER TO
        MASTER_HOST='%s',
        MASTER_PORT='%s',
        MASTER_USER='%s',
        MASTER_PASSWORD='%s',
        MASTER_LOG_FILE='%s',
        MASTER_LOG_POS=%s;''' % (host, port, user, mpass, mf, p)
        cur.execute(sql)

def createInstance(name, port, dbtype="master", **kw):
    '''创建数据库实例'''
    cnf = path.join(MYSQL_CONF_DIR, "%s.cnf" % name)
    datadir = path.join(MYSQL_DATA_DIR, name)
    exists_cnfs = readConfs()

    if checkPort(exists_cnfs, port):
        print >> sys.stderr, "port exist."
        sys.exit(-1)
    if not path.exists(cnf):
        c = _genDict(name, port)
        c.update(kw)
        mc = MySQLDConfig(cnf, **c)
        mc.save()
    else:
        mc = MySQLDConfig(cnf, **kw)

    if not path.exists(datadir):
        mysql_install_db(cnf)
        setOwner(datadir, mc.mysqld_vars['user'])
        run_mysql(cnf)
        time.sleep(3)
        cur = connMySQLd(mc)
        setReplMaster(cur)

def diffVariables(instance_name):
    '''查询数据库配置文件和数据库配置的差异'''
    cnf = getCNF(instance_name)
    if path.exists(cnf):
        mc = MySQLDConfig(cnf)
        print mc
        cur = connMySQLd(mc)
        vars = getMyVariables(cur)
        for k, v in mc.mysqld_vars.items():
            k = k.replace('-', '_')
            if k in vars and vars[k] != v:
                print k, v, vars[k]

def setVariable(instance_name, variable, value):
    '''重新加载配置'''
    cnf = getCNF(instance_name)
    if path.exists(cnf):
        mc = MySQLDConfig(cnf)
        cur = connMySQLd(mc)
        cur.execute('set global %s = %s' % (variable, value))
        mc.set_var(variable, value)
        mc.save()

def backupMySQL(instance_name):
    '''备份数据库'''
    cnf = getCNF(instance_name)
    if path.exists(cnf):
        mc = MySQLDConfig(cnf)
    now = datetime.datetime.now()
    timestamp = now.strftime('%Y-%m-%d-%H%M%S')
    backup_file = path.join(MYSQL_BACK_DIR, instance_name, timestamp+'.sql')
    _dir = path.dirname(backup_file)
    if not path.exists(_dir):
        os.makedirs(_dir)
    cmd = 'mysqldump -A -x -F --master-data=1 --host=127.0.0.1 --user=root --port=%s > %s' % (mc.mysqld_vars['port'], backup_file)
    runMySQLdump(cmd)

def restoreMySQL(instance_name, instance_port, sqlfile, **kw):
    createInstance(instance_name, instance_port, **kw)
    cnf = getCNF(instance_name)
    if path.exists(cnf):
        mc = MySQLDConfig(cnf)
        cur = connMySQLd(mc)
        cmd = "mysql -h 127.0.0.1 -P %s -u root < %s" % (mc.mysqld_vars['port'], sqlfile)
        f, p = getBinlogPOS(sqlfile)
        runMySQLdump(cmd)
        changeMaster(cur, 
                     host=kw['master-host'],
                     port=kw['master-port'],
                     user=REPLICATION_USER,
                     mpass=REPLICATION_PASS,
                     mf=f, 
                     p=p)

def _init():
    '''查询mysql几个目录是否存在,如果不存在,自动创建'''
    if not path.exists(MYSQL_DATA_DIR):
        os.makedirs(MYSQL_DATA_DIR)
    if not path.exists(MYSQL_CONF_DIR):
        os.makedirs(MYSQL_CONF_DIR)
    if not path.exists(MYSQL_BACK_DIR):
        os.makedirs(MYSQL_BACK_DIR)

def main():
    opt, args = opts()
    instance_name = opt.name
    instance_port = opt.port
    command = opt.cmd
    if command == "create":
        if not args:
            createInstance(instance_name, instance_port)
        else:
            dbtype = args[0]
            serverid = args[1]
            mysqld_options = {'server-id':serverid}
            if dbtype == 'master':
                mysqld_options['log-bin'] = 'mysql-bin'
            elif dbtype == 'slave':
                master_host = args[2]
                master_port = args[3]
                mysqld_options['master-host'] = master_host
                mysqld_options['master-port'] = master_port
                mysqld_options['master-user'] = REPLICATION_USER
                mysqld_options['master-password'] = REPLICATION_PASS
                mysqld_options['skip-slave-start'] = None
                mysqld_options['replicate-ignore-db'] = 'mysql'
                mysqld_options['read-only'] = None
            createInstance(instance_name, instance_port, dbtype=dbtype, **mysqld_options)
    elif command == 'check':
        diffVariables(instance_name)
    elif command == 'adjust':
        variable = args[0]
        value = args[1]
        setVariable(instance_name, variable, value)
    elif command == 'backup':
        backupMySQL(instance_name)
    elif command == 'restore':
        serverid == args[0]
        mhost = args[1]
        mport = args[2]
        sqlfile = args[3]
        mysqld_options = {
            "master-host":mhost,
            "master-port":mport,
            "server-id":serverid,
            "skip-slave-start":None,
        }
        restoreMySQL(instance_name, instance_port, sqlfile, **mysqld_options)

if __name__ == "__main__":
    print main()

 

4.测试


帮助信息:

wKioL1OPCB7QCQ7iAAGdj56CGo8501.jpg

创建master实例:

wKiom1OPCU_gwpJ6AACHuSb_shY805.jpg

wKiom1OPCcSiaK7oAANn59ZVoes573.jpg

创建slave实例:

wKioL1OPCl2DVGySAACT5pZ7SyU813.jpgwKioL1OPCpjB7UQlAALV8bynwiE615.jpgwKioL1OPC1mgzItYAAMKXu_xs-8742.jpg


检测配置文件和数据库加载配置差异:

wKioL1OPPK2D23rwAAIg_aWL0io634.jpg  由于单位格式不同,所以出现了差异,可以结合library/utils.py 进行单位换算


数据库备份:

wKiom1OPQC7wXIyLAADdH9B_NjM301.jpgwKioL1OPQBHDW8nEAAC0eePWtuk141.jpg 


需要注意:

     1. python版本必须2.7及2.7以上版本。

         2. MYSQL_DATA_DIR 目录不能放在/root目录下,如果放在root目录下,初始化数据库的时候会报错(权限问题)。我在这犯过错.

 

  如果哪里有错误,或者不足的地方。还请大家多多沟通。