Package ZenUtils :: Module ZenBackup
[hide private]
[frames] | no frames]

Source Code for Module ZenUtils.ZenBackup

  1  #! /usr/bin/env python 
  2  ########################################################################### 
  3  # 
  4  # This program is part of Zenoss Core, an open source monitoring platform. 
  5  # Copyright (C) 2007, 2009 Zenoss Inc. 
  6  # 
  7  # This program is free software; you can redistribute it and/or modify it 
  8  # under the terms of the GNU General Public License version 2 as published by 
  9  # the Free Software Foundation. 
 10  # 
 11  # For complete information please visit: http://www.zenoss.com/oss/ 
 12  # 
 13  ########################################################################### 
 14   
 15   
 16  __doc__='''zenbackup 
 17   
 18  Creates backup of Zope data files, Zenoss conf files and the events database. 
 19  ''' 
 20   
 21  import sys 
 22  import os 
 23  import os.path 
 24  from datetime import date 
 25  from subprocess import Popen, PIPE 
 26  import time 
 27  import logging 
 28  import ConfigParser 
 29  import tarfile 
 30   
 31  import Globals 
 32  from ZCmdBase import ZCmdBase 
 33  from Products.ZenUtils.Utils import zenPath, binPath, readable_time 
 34  from ZenBackupBase import * 
 35   
 36   
 37  MAX_UNIQUE_NAME_ATTEMPTS = 1000 
 38   
 39   
40 -class ZenBackup(ZenBackupBase):
41
42 - def __init__(self):
43 ZenBackupBase.__init__(self) 44 self.log = logging.getLogger("zenbackup") 45 logging.basicConfig() 46 self.log.setLevel(self.options.logseverity)
47
48 - def runCommand(self, cmd=[], obfuscated_cmd=None):
49 """ 50 Execute a command and return the results, displaying pre and 51 post messages. 52 53 @parameter cmd: command to run 54 @type cmd: list 55 @return: results of the command (output, warnings, returncode) 56 """ 57 if obfuscated_cmd: 58 self.log.debug(' '.join(obfuscated_cmd)) 59 else: 60 self.log.debug(' '.join(cmd)) 61 62 proc = Popen(cmd, stdout=PIPE, stderr=PIPE) 63 output, warnings = proc.communicate() 64 if proc.returncode: 65 self.log.error(warnings) 66 self.log.debug(output or 'No output from command') 67 return (output, warnings, proc.returncode)
68 69
70 - def isZeoUp(self):
71 ''' 72 Returns True if Zeo appears to be running, false otherwise. 73 74 @return: whether Zeo is up or not 75 @rtype: boolean 76 ''' 77 # zeoup.py should live in either $ZOPEHOME/lib/bin/ (for the 78 # appliance) or in $ZENHOME/bin (other installs.) 79 cmd = [ binPath('python'), binPath('zeoup.py') ] 80 cmd += '-p 8100 -h localhost'.split() 81 self.log.debug("Can we access ZODB through Zeo?") 82 83 (output, warnings, returncode) = self.runCommand(cmd) 84 if returncode: 85 return False 86 return output.startswith('Elapsed time:')
87 88
89 - def readSettingsFromZeo(self):
90 ''' 91 Store the dbname, dbuser, dbpass from saved settings in the Event 92 Manager (ie ZODB) to the 'options' parsed object. 93 ''' 94 zcmd = ZCmdBase(noopts=True) 95 zem = zcmd.dmd.ZenEventManager 96 for key, default, zemAttr in CONFIG_FIELDS: 97 if not getattr(self.options, key, None): 98 setattr(self.options, key, 99 getattr(zem, zemAttr, None) or default)
100 101
102 - def saveSettings(self):
103 ''' 104 Save the database credentials to a file for use during restore. 105 ''' 106 config = ConfigParser.SafeConfigParser() 107 config.add_section(CONFIG_SECTION) 108 config.set(CONFIG_SECTION, 'dbname', self.options.dbname) 109 config.set(CONFIG_SECTION, 'dbuser', self.options.dbuser) 110 if self.options.dbpass != None: 111 config.set(CONFIG_SECTION, 'dbpass', self.options.dbpass) 112 113 creds_file = os.path.join(self.tempDir, CONFIG_FILE) 114 self.log.debug("Writing MySQL credentials to %s", creds_file) 115 f = open(creds_file, 'w') 116 try: 117 config.write(f) 118 finally: 119 f.close()
120 121
122 - def getPassArg(self):
123 ''' 124 Return string to be used as the -p (including the "-p") 125 to MySQL commands. Overrides the one in ZenBackupBase 126 127 @return: password and flag 128 @rtype: string 129 ''' 130 if self.options.dbpass == None: 131 return '' 132 return '--password=%s' % self.options.dbpass
133
134 - def getDefaultBackupFile(self):
135 """ 136 Return a name for the backup file or die trying. 137 138 @return: unique name for a backup 139 @rtype: string 140 """ 141 def getName(index=0): 142 """ 143 Try to create an unique backup file name. 144 145 @return: tar file name 146 @rtype: string 147 """ 148 return 'zenbackup_%s%s.tgz' % (date.today().strftime('%Y%m%d'), 149 (index and '_%s' % index) or '')
150 backupDir = zenPath('backups') 151 if not os.path.exists(backupDir): 152 os.mkdir(backupDir, 0750) 153 for i in range(MAX_UNIQUE_NAME_ATTEMPTS): 154 name = os.path.join(backupDir, getName(i)) 155 if not os.path.exists(name): 156 break 157 else: 158 self.log.critical('Cannot determine an unique file name to use' 159 ' in the backup directory (%s).' % backupDir + 160 ' Use --outfile to specify location for the backup' 161 ' file.\n') 162 sys.exit(-1) 163 return name
164 165
166 - def buildOptions(self):
167 """ 168 Basic options setup 169 """ 170 # pychecker can't handle strings made of multiple tokens 171 __pychecker__ = 'no-noeffect no-constCond' 172 ZenBackupBase.buildOptions(self) 173 self.parser.add_option('--dbname', 174 dest='dbname', 175 default=None, 176 help='MySQL events database name.' 177 ' By default this will be fetched from Zenoss' 178 ' unless --dont-fetch-args is set.'), 179 self.parser.add_option('--dbuser', 180 dest='dbuser', 181 default=None, 182 help='MySQL username.' 183 ' By default this will be fetched from Zenoss' 184 ' unless --dont-fetch-args is set.'), 185 self.parser.add_option('--dbpass', 186 dest='dbpass', 187 default=None, 188 help='MySQL password.' 189 ' By default this will be fetched from Zenoss' 190 ' unless --dont-fetch-args is set.'), 191 self.parser.add_option('--dont-fetch-args', 192 dest='fetchArgs', 193 default=True, 194 action='store_false', 195 help='By default dbname, dbuser and dbpass' 196 ' are retrieved from Zenoss if not' 197 ' specified and if Zenoss is available.' 198 ' This disables fetching of these values' 199 ' from Zenoss.') 200 self.parser.add_option('--file', 201 dest="file", 202 default=None, 203 help='Name of file in which the backup will be stored.' 204 ' Backups will by default be placed' 205 ' in $ZENHOME/backups/') 206 self.parser.add_option('--no-eventsdb', 207 dest="noEventsDb", 208 default=False, 209 action='store_true', 210 help='Do not include the events database' 211 ' in the backup.') 212 self.parser.add_option('--no-zodb', 213 dest="noZopeDb", 214 default=False, 215 action='store_true', 216 help='Do not include the ZODB' 217 ' in the backup.') 218 self.parser.add_option('--no-perfdata', 219 dest="noPerfData", 220 default=False, 221 action='store_true', 222 help='Do not include performance data' 223 ' in the backup.') 224 self.parser.add_option('--stdout', 225 dest="stdout", 226 default=False, 227 action='store_true', 228 help='Send backup to stdout instead of to a file.') 229 self.parser.add_option('--save-mysql-access', 230 dest='saveSettings', 231 default=False, 232 action='store_true', 233 help='Include dbname, dbuser and dbpass' 234 ' in the backup' 235 ' file for use during restore.') 236 237 self.parser.remove_option('-v') 238 self.parser.add_option('-v', '--logseverity', 239 dest='logseverity', 240 default=20, 241 type='int', 242 help='Logging severity threshold')
243 244
245 - def backupEventsDatabase(self):
246 """ 247 Backup the MySQL events database 248 """ 249 partBeginTime = time.time() 250 251 # Setup defaults for db info 252 if self.options.fetchArgs and not self.options.noEventsDb: 253 if self.isZeoUp(): 254 self.log.info('Getting MySQL dbname, user, password from ZODB.') 255 self.readSettingsFromZeo() 256 else: 257 self.log.error('Unable to get MySQL credentials from ZODB.' 258 ' Zeo may not be available.') 259 self.log.info("Skipping events database backup.") 260 return 261 262 if not self.options.dbname: 263 self.options.dbname = 'events' 264 if not self.options.dbuser: 265 self.options.dbuser = 'zenoss' 266 # A passwd of '' might be valid. A passwd of None is interpreted 267 # as no password. 268 269 # Save options to a file for use during restore 270 if self.options.saveSettings: 271 self.saveSettings() 272 273 self.log.info('Backing up events database.') 274 cmd_p1 = ['mysqldump', '-u%s' % self.options.dbuser] 275 cmd_p2 = ["--single-transaction", '--routines', self.options.dbname, 276 '--result-file=' + os.path.join(self.tempDir, 'events.sql') ] 277 cmd = cmd_p1 + [self.getPassArg()] + cmd_p2 278 obfuscated_cmd = cmd_p1 + ['*' * len(self.getPassArg())] + cmd_p2 279 280 (output, warnings, returncode) = self.runCommand(cmd, obfuscated_cmd) 281 if returncode: 282 self.log.info("Backup terminated abnormally.") 283 return -1 284 285 partEndTime = time.time() 286 subtotalTime = readable_time(partEndTime - partBeginTime) 287 self.log.info("Backup of events database completed in %s.", 288 subtotalTime)
289 290
291 - def backupZODB(self):
292 """ 293 Backup the Zope database. 294 """ 295 partBeginTime = time.time() 296 297 self.log.info('Backing up the ZODB.') 298 repozoDir = os.path.join(self.tempDir, 'repozo') 299 os.mkdir(repozoDir, 0750) 300 cmd = [binPath('python'), binPath('repozo.py'), 301 '--repository', repozoDir, '--file', 302 zenPath('var', 'Data.fs'), 303 '--backup', '--full' ] 304 (output, warnings, returncode) = self.runCommand(cmd) 305 if returncode: 306 self.log.critical("Backup terminated abnormally.") 307 return -1 308 309 partEndTime = time.time() 310 subtotalTime = readable_time(partEndTime - partBeginTime) 311 self.log.info("Backup of ZODB database completed in %s.", subtotalTime)
312 313
314 - def backupPerfData(self):
315 """ 316 Back up the RRD files storing performance data. 317 """ 318 perfDir = zenPath('perf') 319 if not os.path.isdir(perfDir): 320 self.log.warning('%s does not exist, skipping.', perfDir) 321 return 322 323 partBeginTime = time.time() 324 325 self.log.info('Backing up performance data (RRDs).') 326 tarFile = os.path.join(self.tempDir, 'perf.tar') 327 cmd = ['tar', 'cf', tarFile, perfDir] 328 (output, warnings, returncode) = self.runCommand(cmd) 329 if returncode: 330 self.log.critical("Backup terminated abnormally.") 331 return -1 332 333 partEndTime = time.time() 334 subtotalTime = readable_time(partEndTime - partBeginTime) 335 self.log.info("Backup of performance data completed in %s.", 336 subtotalTime )
337 338
339 - def packageStagingBackups(self):
340 """ 341 Gather all of the other data into one nice, neat file for easy 342 tracking. 343 """ 344 self.log.info('Packaging backup file.') 345 if self.options.file: 346 outfile = self.options.file 347 else: 348 outfile = self.getDefaultBackupFile() 349 tempHead, tempTail = os.path.split(self.tempDir) 350 tarFile = outfile 351 if self.options.stdout: 352 tarFile = '-' 353 cmd = ['tar', 'czfC', tarFile, tempHead, tempTail] 354 (output, warnings, returncode) = self.runCommand(cmd) 355 if returncode: 356 self.log.critical("Backup terminated abnormally.") 357 return -1 358 self.log.info('Backup written to %s' % outfile)
359 360
361 - def cleanupTempDir(self):
362 """ 363 Remove temporary files in staging directory. 364 """ 365 self.log.info('Cleaning up staging directory %s' % self.rootTempDir) 366 cmd = ['rm', '-r', self.rootTempDir] 367 (output, warnings, returncode) = self.runCommand(cmd) 368 if returncode: 369 self.log.critical("Backup terminated abnormally.") 370 return -1
371 372
373 - def makeBackup(self):
374 ''' 375 Create a backup of the data and configuration for a Zenoss install. 376 ''' 377 backupBeginTime = time.time() 378 379 # Create temp backup dir 380 self.rootTempDir = self.getTempDir() 381 self.tempDir = os.path.join(self.rootTempDir, BACKUP_DIR) 382 self.log.debug("Use %s as a staging directory for the backup", self.tempDir) 383 os.mkdir(self.tempDir, 0750) 384 385 if self.options.noEventsDb: 386 self.log.info('Skipping backup of events database.') 387 else: 388 self.backupEventsDatabase() 389 390 if self.options.noZopeDb: 391 self.log.info('Skipping backup of ZODB.') 392 else: 393 self.backupZODB() 394 395 # Copy /etc to backup dir (except for sockets) 396 self.log.info('Backing up config files.') 397 etcTar = tarfile.open(os.path.join(self.tempDir, 'etc.tar'), 'w') 398 etcTar.add(zenPath('etc'), 'etc') 399 etcTar.close() 400 self.log.info("Backup of config files completed.") 401 402 # Copy /ZenPacks to backup dir 403 self.log.info('Backing up ZenPacks.') 404 etcTar = tarfile.open(os.path.join(self.tempDir, 'ZenPacks.tar'), 'w') 405 etcTar.add(zenPath('ZenPacks'), 'ZenPacks') 406 etcTar.close() 407 self.log.info("Backup of ZenPacks completed.") 408 409 if self.options.noPerfData: 410 self.log.info('Skipping backup of performance data.') 411 else: 412 self.backupPerfData() 413 414 # tar, gzip and send to outfile 415 self.packageStagingBackups() 416 417 self.cleanupTempDir() 418 419 backupEndTime = time.time() 420 totalBackupTime = readable_time(backupEndTime - backupBeginTime) 421 self.log.info('Backup completed successfully in %s.', totalBackupTime) 422 return 0
423 424 425 if __name__ == '__main__': 426 zb = ZenBackup() 427 if zb.makeBackup(): 428 sys.exit(-1) 429