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

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