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