[Python] MySQL 数据库自动化备份工具

作者 huhamhire,暂无评论,2013年3月24日 15:57 程序实践

好像又有一段时间木有在我的 log 上写文章了,唉~~最近一段时间的确事情比较多,下周还要交 Imagine Cup 的中期材料,关于 VPS 折腾系列的文章进度的确耽误了一些,准备后面有时间的话再继续补上。

今天这一篇是关于 VPS 数据库维护的内容,也是"VPS维护DIY"这个系列的开篇了。说这个系列是 DIY 其实一点也不为过,主要是因为这个系列的文章重点会介绍一些本人自己在维护 VPS 时写的一些脚本工具啥的。由于本人水平实在有限,写的代码多多少少会有些疏漏,欢迎各位看官前来吐槽。

下面我们进入正题。

在搭建完成 VPS 以后,剩下的工作主要就是日常维护了。在维护中相当重要的一个方面就是数据库的备份,毕竟我们谁也不能保证 VPS 上的数据永远的安全,多一个备份总是不错的。也许指不定哪天 VPS 遭到攻击,或者是代理商那里出现了一些问题,都很有可能会造成数据的丢失,所以数据的备份不能轻视。基于这样的原因,咱就来折腾一下 VPS 的数据备份呗。

备份其实并不复杂,主要也就是对数据库进行备份了。偷懒的话用 phpmyadmin 直接可以通过图形界面进行简单的备份和还原操作,不过 phpmyadmin 受制于网络上的传输限制,不宜作为一项长久之计。剩下的方法就考虑 MySQL 的 mysqldump 功能,也分两种情况,一种是直接远程执行 mysqldump 操作来备份数据库,另一种是在服务器上执行 dump 操作后将备份文件下载。前一种的话,如果有比较强大的网络接入的话还是可行的,不过显然大部分人都没有这个条件,当然我也是属于这类的。由于条件的限制,我还是选择了后一种方案。

备份的原理很简单。因为我现在的数据也没有到海量的状态,将数据库通过 dump 进行完整备份,使用独立文件归档应该是比较可行的方案,小容量的数据咱们也是在木有必要进行增量备份。所以,备份就相当于将数据 dump 出来,随后归档下载存放即可。操作比较简单,不过我本着折腾的原则,同时也是为了以后可以偷些懒,就花了点时间用 Python 写了一个实现上述功能的工具。

具体代码如下(mysql_dump.py):


  1 #!/usr/bin/python
  2 # -*- coding: UTF-8 -*-
  3 #
  4 # Created by huhamhire [me@huhamhire.com]
  5 #
  6 import paramiko
  7 import os
  8 import time
  9 import getpass
 10 
 11 class SSH_MySQL_Dump():
 12     U"""一个通过 SSH 登录远程服务器备份 MySQL 数据库,并能将备份文件打包下载到本
 13     地的对象
 14 
 15     说明:  构造对象需提供 config 配置参数
 16 
 17            构造时使用 self.sconnect() 建立 SSH 及 SFTP 连接。使用
 18            self.ssh_exec_commands() 执行相关操作。使用 self.sftp_down() 下载备份
 19            文件。
 20 
 21     依赖:  os, time, paramiko, getpass
 22     """
 23     #连接及操作设置参数
 24     server = ''
 25     user = ''
 26     passwd = ''
 27     ssh_key = ''
 28     dbadmin = ''
 29     dbapasswd = ''
 30     dblist = []
 31 
 32     #目录设置参数
 33     dump_dir = '/tmp/'
 34     dump_pre = 'mysqldump'
 35 
 36     #备份时间
 37     dumptime = ''
 38 
 39     #终端显示宽度
 40     terminal_width = 80 - 2
 41 
 42     #连接对象
 43     ssh = None
 44     sftp = None
 45 
 46     #错误标志
 47     err = False
 48 
 49     def __init__(self, config):
 50         U"""构造 SSH_MySQL_Dump 对象
 51 
 52         说明:  传入的配置字典设定对象参数
 53 
 54         参数:  config: 配置参数字典
 55         类型:  config: dict - {"server": "sql.example.com",
 56                                "dbadmin": "dbadmin", "dbapasswd": "dbapasswd",
 57                                "dblist": list, "user": "user",
 58                                "passwd": "passwd", "ssh_key": "ssh_key"}
 59         """
 60         #设置配置参数
 61         try:
 62             self.server = config['server']
 63             self.dbadmin = config['dbadmin']
 64             self.dbapasswd = config['dbapasswd']
 65             self.dblist = config['dblist']
 66             self.user = config['user']
 67 
 68             if 'passwd' in config.keys():
 69                 self.passwd = config['passwd']
 70             else:
 71                 self.ssh_key = config['ssh_key']
 72         except:
 73             print("Configuration Error!")
 74             exit()
 75 
 76         #初始化备份时间
 77         self.dumptime = time.strftime("%Y%m%d%H%M")
 78 
 79         #建立连接
 80         self.ssh, self.sftp = self.sconnect()
 81 
 82     def __del__(self):
 83         U"""对象析构函数
 84 
 85         说明:  对象实例回收时关闭对象自己的 SSH 与 SFTP 连接
 86         """
 87         try:
 88             self.ssh.close()
 89         except:
 90             pass
 91         try:
 92             self.sftp.close()
 93         except:
 94             pass
 95 
 96         #信息反馈
 97         if self.err == False:
 98             print('\nComplete!')
 99         else:
100             print('\nBackup Failed!')
101 
102     def sconnect(self):
103         U"""建立 SSH 与 SFTP 连接
104 
105         说明:  根据对象连接参数建立 SSH 与 SFTP 连接,返回连接对象
106 
107         返回:  ssh: SSH 连接对象
108                sftp: SFTP 连接对象
109         类型:  ssh: paramiko.SSHClient
110                sftp: paramiko.SFTPClient
111         """
112         print('Connecting to "%s"' % (self.server))
113         print('Using username "%s"' % (self.user))
114         ssh = paramiko.SSHClient()
115         ssh.load_system_host_keys()
116         ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
117         privatekeyfile = ''
118         privatekeyfile = os.path.expanduser(self.ssh_key)
119 
120         #选择证书或密码连接,若存在证书,则优先使用证书加密连接
121         try:
122             try:
123                 try:
124                     mykey = paramiko.RSAKey.from_private_key_file(privatekeyfile)
125                 #缺少 Private Key 密码
126                 except paramiko.PasswordRequiredException:
127                     key_passwd = getpass.unix_getpass('Password for your SSH key:')
128                     mykey = paramiko.RSAKey.from_private_key_file(privatekeyfile, password = key_passwd)
129                 except:
130                     raise
131                 print('Authenticating with private key.')
132                 ssh.connect(self.server, 22, self.user, pkey = mykey, timeout = 5)
133             #SSH Private Key 不正确
134             except paramiko.SSHException:
135                 print('Server refused our key. Try using password.')
136                 raise
137             #无效的 SSH Private Key
138             except (IOError, paramiko.BadHostKeyException):
139                 print('Invalid SSH key. Try using password.')
140                 raise
141         #尝试使用密码连接 SSH
142         except:
143             try:
144                 print('Authenticating with password.')
145                 ssh.connect(self.server, 22, self.user, self.passwd, timeout = 5)
146             #密码错误
147             except (paramiko.AuthenticationException, paramiko.SSHException):
148                 self.passwd = getpass.getpass('Incorrect password. Enter your password:')
149                 print('Authenticating with password.')
150                 ssh.connect(self.server, 22, self.user, self.passwd, timeout = 5)
151             #连接错误
152             except:
153                print('Connection Failed!')
154                self.err = True
155                exit()
156         print('Connection established\n')
157 
158         #建立 SFTP 连接
159         sftp = ssh.open_sftp()
160 
161         return (ssh,sftp)
162 
163     def create_dump_commands(self, backtime = None):
164         U"""生成 bash 备份命令
165 
166         说明:  根据备份时间生成 MySQL dump 操作及相关文件操作命令及说明
167 
168         参数:  backtime: 备份执行时间,默认为对象自己的备份时间
169         类型:  backtime: str - YYYYMMDDhhmm
170 
171         返回:  cmds: 操作命令元组列表
172         类型:  cmds: list - [('Command 1', 'Comment 1'),..]
173         """
174         #设置默认备份时间
175         if backtime == None:
176             backtime = self.dumptime
177 
178         cmds = []
179         dumpre = self.dump_dir + self.dump_pre
180         cmds.append(('rm -f %s*' % (dumpre), 'Remove old file(s)'))
181         cmds.append(('mkdir %s' % (dumpre), 'Prepare dump directory'))
182 
183         #逐一备份数据库
184         cmds.append((':', '\nDumping Database(s):'))
185         cmds.append((':', '=' * self.terminal_width))
186         dump_total = len(self.dblist)
187         dump_count = 0
188         for database in self.dblist:
189             dump_count += 1
190             dbdumpfile = '%s_backup_%s.sql' % (database, backtime)
191             dbdumpinfo = ' %s : %s' % (database, dbdumpfile)
192             dbdumppart = '%d/%d ' % (dump_count, dump_total)
193             cmds.append(('mysqldump --user=%s --password=%s --opt %s > %s/%s' % (self.dbadmin, self.dbapasswd, database, dumpre, dbdumpfile),
194                 dbdumpinfo + ' ' * (self.terminal_width - len(dbdumpinfo) - len(dbdumppart)) + dbdumppart))
195 
196         #备份信息汇总
197         cmds.append((':', '\nDump Summary:\n' + '=' * self.terminal_width))
198         cmds.append((':', 'Dump\t%d database(s)\n' % (dump_total)))
199 
200         #打包备份文件
201         cmds.append(('tar czvf %s_%s.tar.gz -C %s %s' % (dumpre, backtime, self.dump_dir[:-1], self.dump_pre), 'Pack up dump file(s):'))
202         cmds.append(('rm -rf %s' % (dumpre), 'Remove temp directory\n'))
203         return cmds
204 
205     def create_downlist(self, backtime = None):
206         U"""生成下载文件列表
207 
208         说明:  根据备份时间,文件前缀生成备份文件的文件名以供下载
209 
210         参数:  backtime: 备份执行时间,默认为对象自己的备份时间
211         类型:  backtime: str - YYYYMMDDhhmm
212 
213         返回:  list: 下载文件字典列表。包含文件路径与文件名
214         类型:  list: list - [{'filename':'filename', 'filedir':'filedir'},..]
215         """
216         #设置默认备份时间
217         if backtime == None:
218             backtime = self.dumptime
219 
220         #生成文件列表
221         list = []
222         list.append({'filename':'%s_%s.tar.gz' % (self.dump_pre, backtime), 'filedir':self.dump_dir})
223         return list
224 
225     def ssh_exec_commands(self, ssh = None, dump_commands = None):
226         U"""执行 SSH 操作命令
227 
228         说明:  根据备份时间生成 MySQL dump 操作及相关文件操作命令
229 
230         参数:  ssh: SSH 连接客户端对象,默认为对象自己的 SSH 连接
231         类型:  ssh: paramiko.SSHClient
232 
233         返回:  cmds: 操作命令元组列表
234         类型:  cmds: list - [('Command 1', 'Comment 1'),..]
235         """
236         #设置默认 SSH 连接
237         if ssh == None:
238             ssh = self.ssh
239 
240         #生成操作命令
241         if dump_commands == None:
242             dump_commands = self.create_dump_commands()
243 
244         #执行操作
245         for cmd in dump_commands:
246             if cmd[0] != ':':
247                 #输出操作说明
248                 print(cmd[1])
249                 stdin, stdout, stderr = ssh.exec_command(cmd[0])
250                 output = stdout.readlines()
251                 for out in output:
252                     print('  ' + out),
253             else:
254                 #输出操作说明
255                 print(cmd[1])
256 
257     def sftp_down(self, sftp = None, filelist = None):
258         U"""SFTP文件下载函数
259 
260         说明:  通过 SFTP 链接下载服务器上的所需文件,进度反馈类似于 yum 管理器
261 
262         参数:  sftp: SFTP 连接客户端对象,默认为对象自己的 SFTP 连接
263                filelist: 所需文件的字典列表。包含文件路径与文件名(可选参数)
264         类型:  sftp: paramiko.SFTPClient
265                filelist: list - [{'filename':'filename', 'filedir':'filedir'},..]
266         """
267         #设置默认 SFTP 连接
268         if sftp == None:
269             sftp = self.sftp
270 
271         #生成下载文件列表
272         if filelist == None:
273             filelist = self.create_downlist()
274 
275         #下载dump文件
276         print('Downloading backup file(s):')
277         file_count = 0
278         total_size = 0
279         total_count = len(filelist)
280         for file in filelist:
281             file_count += 1
282 
283             #下载信息
284             down_info = '(%d/%d): %s' % (file_count, total_count, file['filename'])
285             print(down_info).ljust(self.terminal_width),
286 
287             #下载文件
288             sftp.get(file['filedir'] + file['filename'], './' + file['filename'], self.callback)
289 
290             #统计下载大小
291             file_size = sftp.stat(file['filedir'] + file['filename']).__getattribute__('st_size')
292             total_size += file_size
293             file_size = self.convert_size(file_size).ljust(7)
294             print('\r' + down_info + ' ' * (self.terminal_width - 14 - len(down_info)) + '[OK] | %s' % (file_size))
295         print('\nTotal download size: %s' % (self.convert_size(total_size)))
296 
297     def callback(self, done, total):
298         U"""进度反馈回调函数
299 
300         说明:  通过获取已上传的字节数 done 以及上传文件的总大小 total 对比显示上
301                传进度
302 
303         参数:  done: 已上传的文件大小(字节)
304                total: 上传文件总大小(字节)
305         类型:  done: int
306                total: int
307         """
308         #输出行宽
309         line_width = 40
310 
311         #生成进度条
312         prog_len = line_width - 20
313         progress = ('=' * int(prog_len * done / total) + '-' * int(2 * prog_len * done / total % 2)).ljust(prog_len)
314 
315         #文件大小转换
316         total = self.convert_size(total).ljust(7)
317         done = self.convert_size(done).rjust(7)
318 
319         #进度显示
320         print('\b' * (line_width + 1)+ '[%s] %s | %s' % (progress, done, total)).rjust(line_width),
321 
322     def convert_size(self, filesize):
323         U"""文件尺寸转换
324 
325         说明:  根据文件尺寸字节数 filesize 格式化输出相应单位文件大小 size
326 
327         参数:  filesize: 文件大小(字节)
328         类型:  filesize: int
329 
330         返回:  size: 文件大小(B|kB|MB)
331         类型:  size: str
332         """
333         size = filesize
334         size = float(size)
335         if size < 1000:
336             size = '%dB' % (size)
337         #格式化kB
338         elif size < 10 * 1024:
339             size = '%.2f kB' % (size / 1024)
340         elif size < 100 * 1024:
341             size = '%.1f kB' % (size / 1024)
342         elif size < 1000 * 1024:
343             size = "%d kB" % int(size / 1024)
344         #格式化MB
345         elif size < 10 * 1024 ** 2:
346             size = "%.2f MB" % (size / (1024 ** 2))
347         elif size < 100 * 1024 ** 2:
348             size = "%.1f MB" % (size / (1024 ** 2))
349         else:
350             size = "%d MB" % int(size / (1024 ** 2))
351         return size
352 
353 if __name__=='__main__':
354     server = 'sql.example.com'
355     username = "root"
356     ssh_key = u'./id_rsa'
357     dbadmin = 'root'
358     dbapasswd = 'password'
359     dblist = ['db1', 'db2', 'db3']
360     #passwd = 'passwd'
361 
362     config = {"server": server, "dbadmin": dbadmin, "dbapasswd": dbapasswd,
363               "dblist": dblist, "user": username, "ssh_key": ssh_key}
364 
365     backup = SSH_MySQL_Dump(config)
366     backup.ssh_exec_commands()
367     backup.sftp_down()

因为这个工具是按照对象来实现的,可能就木有很好地体现出Python的间接性,当然也有一部分原因是我水平还不行,至少不是一个正统的Python程序员。目前的结构还是相当简单的,只有一个核心的处理模块。在本地执行这个脚本,随后该脚本会通过SSH登录VPS,执行dump命令备份文件并归档整理后,通过同样的连接使用SFTP将文件下载到本地。

具体使用时,只需要在代码最后输入正确的配置信息,即可实现其应有的功能。进行SSH连接时支持证书和密码两种方式,当然优先选择的还是证书模式,要使用密码方式的话,只需要设置下密码即可。为了方便维护,主体对象数据传入是通过一个简单的配置数据字典在构造对象实例的时候传入的,其余详细内容可以参考代码中的注视部分,这里就不再多说了。

设置完成后,在工作目录使用下面的命令即可自动执行操作:


python mysql_dump.py

备份执行效果可以参考下图,Terminal 的交互信息算是学了点 Linux 的风格:

musql_dump

目前我实现的基本功能就这样一些吧,准备以后有需求了再进行扩展,反正按 OOP 方式写的东西扩展性都是不错的。

关键词:MySQL , Python , 工具 DIY
登录后进行评论