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