1
2
3
4
5
6
7
8
9
10
11
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
40
42 ZenBackupBase.__init__(self)
43 self.log = logging.getLogger("zenbackup")
44 logging.basicConfig()
45 self.log.setLevel(self.options.logseverity)
46
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
55
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
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
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
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
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
147 """
148 Basic options setup
149 """
150
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
238 """
239 Backup the MySQL events database
240 """
241 partBeginTime = time.time()
242
243
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
259
260
261
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
288 """
289 Backup the zenpacks dir
290 """
291
292 if not self.options.noZopeDb:
293
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
300
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
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
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
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
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
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
391 '''
392 Create a backup of the data and configuration for a Zenoss install.
393 '''
394 backupBeginTime = time.time()
395
396
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
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
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