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