#!/usr/bin/python2.4 ## mysql-hot-rsync : v0.1 ## ## http://devsec.org/software/mysql-hot-rsync/ ## ## Thor Kooda ## 2006-04-19 ## USAGE: mysql-hot-rsync [db [table ...] ] ## ABOUT: ## - uses rsync to save a safe backup of live mysql databases and/or tables ## NOTES: ## - if no db is passed, it will save all databases ## - if no tables are passed, it will save all tables ## ALGORITHM: ## - compiles a dict of dbs and their tables to be processed: ## {'db1': ['table1', 'table2', 'table3'], ## 'db2': ['tableA', 'tableB', 'tableC']} ## - processes the dict of dbs/tables import sys import os import MySQLdb import re import subprocess # defaults mysql_user = "root" mysql_pass = "XXXXXXXXXX" rsync_cmd = "/usr/bin/rsync" rsync_args = "--quiet --perms --links --times --numeric-ids" # mysql connect function def do_connect(): try: con = MySQLdb.Connect( user = mysql_user, passwd = mysql_pass ) except: print >>sys.stderr, "error: mysql connect" sys.exit( 1 ) return con # mysql query function def do_query( cur, query ): try: cur.execute( query ) except: print >>sys.stderr, "error: mysql query: '%s'" % query sys.exit( 1 ) # function to obtain datadir def get_datadir(): cur = con.cursor() do_query( cur, "SHOW VARIABLES LIKE 'datadir'" ) var_datadir = cur.fetchone()[1] cur.close() return var_datadir # build dict of db(s) and table(s) with db names as the keys and table lists def get_tree(): tree = dict() # if we've got the dbs and some tables.. if len(sys.argv) >= 4: tree[ sys.argv[ 2 ] ] = sys.argv[ 3 : ] # store db and table(s) return tree # if we've got just the db.. if len(sys.argv) >= 3: tree[ sys.argv[ 2 ] ] = [] # store only db else: # no dbs yet, find+store all dbs cur = con.cursor() do_query( cur, "SHOW DATABASES" ) for db in cur.fetchall(): tree[ db[0] ] = [] cur.close() # store list of tables for each db for db in tree.keys(): cur = con.cursor() do_query( cur, "SHOW TABLES FROM %s" % db ) for table in cur.fetchall(): tree[ db ].append( table[0] ) cur.close() return tree # function to backup single table def do_backup_table( db, table ): path_db_dir = var_datadir + "/" + db if not os.access( path_db_dir, os.R_OK ): print >>sys.stderr, "warning: skipping unreadable database dir: '%s'" % path_db_dir return False # build list of file names to backup (this could probably be done by a rsync pattern) regex = "^%s\.*" % table pattern = re.compile( regex ) src_files = [] for root, dirs, files in os.walk( path_db_dir ): for file in files: if pattern.match( file ): file_path = "%s/%s/%s" % ( var_datadir, db, file ) if not os.access( file_path, os.R_OK ): print >>sys.stderr, "warning: skipping unreadable file: '%s'" % file_path continue src_files.append( file ) return do_backup_table_files( db, table, src_files ) # flush+lock+flush table.. def do_table_lock( cur, db, table ): do_query( cur, "USE %s" % db ) do_query( cur, "FLUSH TABLE %s" % table ) # flush one table do_query( cur, "LOCK TABLE %s WRITE" % table ) # lock one table do_query( cur, "FLUSH TABLE %s" % table ) # flush one table # unlock table.. def do_table_unlock( cur ): do_query( cur, "UNLOCK TABLES" ) # unlock all tables # function to backup all files for one table def do_backup_table_files( db, table, src_files ): cur = con.cursor() do_table_lock( cur, db, table ) for file in src_files: ## tkooda : 2006-04-19 : XXX FIXME: sanitize (strip "..", "/", and quotes from) db/table names cmd = "%s %s '%s/%s/%s' '%s/%s/'" % ( rsync_cmd, rsync_args, var_datadir, db, file, dest_dir, db) try: retcode = subprocess.call(cmd, shell=True) if retcode < 0: print >>sys.stderr, "error: child was terminated by signal", -retcode elif retcode > 0: print >>sys.stderr, "warning: rsync comand returned: '%s' ('%s')" % ( retcode, cmd ) except OSError, e: print >>sys.stderr, "error: execution failed:", e do_table_unlock( cur ) cur.close() return True ################################################## # main() # check argv if len(sys.argv) < 2: print >>sys.stderr, "usage: %s [db [table ... ] ]" % sys.argv[ 0 ] sys.exit( 2 ) # check destination dir dest_dir = sys.argv[ 1 ] if not os.access( dest_dir, os.W_OK ): print >>sys.stderr, "error: nonwritable destination dir: '%s'" % dest_dir sys.exit( 1 ) # mysql connect con = do_connect() # get mysql datadir var_datadir = get_datadir() # get dict of dbs/tables tree = get_tree() #print >>sys.stderr, tree # DEBUG for db in tree.keys(): if not os.access( var_datadir + "/" + db, os.R_OK ): print >>sys.stderr, "warning: skipping invalid database: '%s'" % db continue if not tree[ db ]: print >>sys.stderr, "warning: skipping database with no tables: '%s'" % db continue for table in tree[ db ]: if do_backup_table( db, table ): print >>sys.stderr, "sync: %s.%s" % ( db, table )