# +----------------------------------------------------------------------+
# | Copyright (C) 2006 Chris Francy |
# +----------------------------------------------------------------------+
# | This program is free software. You can redistribute in and/or |
# | modify it under the terms of the GNU General Public License Version |
# | 2 as published by the Free Software Foundation. |
# | |
# | This program is distributed in the hope that it will be useful, |
# | but WITHOUT ANY WARRANTY, without even the implied warranty of |
# | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
# | GNU General Public License for more details. |
# | |
# | You should have received a copy of the GNU General Public License |
# | along with this program; If not, write to the Free Software |
# | Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. |
# +----------------------------------------------------------------------+
# | Authors: Chris Francy - francyci@gmail.com |
# +----------------------------------------------------------------------+
from win32com.client import Dispatch
from time import (strftime, strptime, sleep)
from string import (lstrip, expandtabs)
import smtplib
# Create a string to be used for errors
BackupError='BackupError'
def Getconfig(configpath='', compname='', hour='*', dow='*', dom='*'):
"""Retrieve configuration from the xml file and stuff it all in a
dictionary to be returned
"""
# setup our config dictionary
config = {}
config['compname']=compname
config['smptsrv']=''
config['reportemail']=''
config['name']=''
config['backupdir']=''
config['bkfversions']=0
config['logversions']=0
config['backuptype']=''
config['verify']=False
config['snapoff']=False
config['bpaths']=[]
# Define some values for error checking
BackupTypes=('normal','copy','differential','incremental','daily')
options=('verify','snapoff')
# create a xml object with the xml config
xmlconfig = Dispatch("Msxml2.DOMDocument.4.0")
# load the config file
try:
# load the config file
if not xmlconfig.Load(configpath):
raise BackupError, 'Unable to open file %s' % configpath
if ((xmlconfig.documentElement.selectSingleNode("//backup/testing") is None) or
(not '42'==xmlconfig.documentElement.selectSingleNode("//backup/testing").text) ):
raise BackupError, 'File is missing the test value'
# retrieve some global paramaters
config['smptsrv']=xmlconfig.documentElement.selectSingleNode("//backup/smptsrv").text.encode('ascii')
config['reportemail']=xmlconfig.documentElement.selectSingleNode("//backup/reportemail").text.encode('ascii')
# xsl expression that should find system elements a name matching
# the computer name provided
xslexp='/backup/system[@name="%s"]' % compname.lower()
if xmlconfig.documentElement.selectNodes( xslexp ).length == 1:
item = xmlconfig.documentElement.selectSingleNode( xslexp )
else:
raise BackupError, 'matched multiple systems!'
# get attributes
config['name']=item.getAttribute("name").encode('ascii')
config['bkfversions']=int(item.getAttribute("bkfversions"))
config['logversions']=2*int(item.getAttribute("logversions"))
config['backupdir']=item.getAttribute("backupdir").encode('ascii')
# we must have a traily slash on the backup dir!
if not config['backupdir'][-1] == '\\':
config['backupdir']=config['backupdir']+'\\'
# get Backup Paths
bpaths=item.selectNodes("bpath")
# check that we actually will be backing up something
if not bpaths.length > 0:
raise BackupError, 'something must be selected to be backed up!'
# append the dirs to the list
for bpath in bpaths:
if bpath.text.encode('ascii').strip() != '':
config['bpaths'].append(bpath.text.encode('ascii').strip())
# find a matching task and get backup type and options
tasks=item.selectNodes("task")
itemcount_tsk=0
for task in tasks:
# get values and expand as lists
task_hour = task.getAttribute("hour").encode('ascii').split(',')
task_dow = task.getAttribute("dow").encode('ascii').split(',')
task_dom = task.getAttribute("dom").encode('ascii').split(',')
# test so see if the task defined matches our conditions
if (('*' in task_dom or dom in task_dom) and
('*' in task_dow or dow in task_dow) and
('*' in task_hour or hour in task_hour)):
# found a matching task so increment the counter
itemcount_tsk=itemcount_tsk+1
# Store task properties
config['backuptype']=task.getAttribute("backuptype").encode('ascii')
task_options=task.getAttribute("options").encode('ascii').split(',')
# for each possible option see if that option is set mark the
# option true in the config dictionary
for option in options:
if option in task_options:
config[option]=True
# Set the correction failure message if we didn't find exactly one task
if itemcount_tsk > 1:
raise BackupError, 'matched multiple task!'
if itemcount_tsk == 0:
raise BackupError, 'no matching task!'
# return nothing if there was any errors else return a dictionary with the configuration
except AttributeError:
raise BackupError, 'required attribute not present in xml'
except BackupError:
raise
return None
else:
return config
def WriteSelectionsFile(config={}):
'''create the backup selection file'''
try:
# write bks file with selections
fp=open(config['bkselfile'],'wb')
for bpath in config['bpaths']:
# encode the string to utf16 don't right the Byte Order Marker and add a carriage return/line feed
# This line of code right here is the reason this script couldn't be easily written as vbscript
fp.write(bpath.encode('utf16')[2:]+'\r\0\n\0')
fp.close()
except IOError:
raise BackupError, 'Unable to write selections file'
def RemoveOldFiles(config={}):
'''Delete old files. Oldest is deleted first'''
try:
fileslog={} # dict for logs
filesbkf={} # dict for backups
# if backup dir exists get a list of files
files=oFSO.GetFolder(config['backupdir']).Files
# create a dictionary to hold some lists and info so we can sort things out
filesdict={'log':{},'bkf':{},'delfiles':[]}
for file in files:
# if the filename matches a pattern add to the approiate dict
# the index is modify date of the file and value is the name
if '.log' == file.name[-4:]:
filesdict['log'][strftime('%Y%m%d%H%M%S',strptime(str(file.DateLastModified), '%m/%d/%y %H:%M:%S'))]=file.name
if '.bkf' == file.name[-4:]:
filesdict['bkf'][strftime('%Y%m%d%H%M%S',strptime(str(file.DateLastModified), '%m/%d/%y %H:%M:%S'))]=file.name
# for both types of files
for key in ('bkf','log'):
# store a list of keys since we can't sort the tupple returned by keys()
filesdict['tmp']=filesdict[key].keys()
# sort
filesdict['tmp'].sort()
# append the name of the file to the delete files list
# since the filedict['tmp'] is a sorted list of the keys for the files
# use slicing to get all the files except the last n where
# n is 0-config[key+'versions']
for delfile in filesdict['tmp'][:0-config[key+'versions']]:
filesdict['delfiles'].append(filesdict[key][delfile])
# after all the above weirdness all we have to do is delete each file in the list
for file in filesdict['delfiles']:
oFSO.DeleteFile(config['backupdir']+file)
except IOError:
raise BackupError, 'Unable to remove old files'
def BuildCommand(config={}):
'''Use the config dictionary to build up the command line that will be used
to actually execute the backup'''
cmd=[]
cmd.append('ntbackup backup ')
cmd.append('"@%s" ' % config['bkselfile']) # selections
cmd.append('/M %s ' % config['backuptype']) # backup type
cmd.append('/L:f ') # log type
cmd.append('/j "BackupJob" ') # the job name
if config['snapoff']:
cmd.append('/SNAP:off ') # to not use shadow copying
if config['verify']:
cmd.append('/v:yes ') # verify the backup
cmd.append('/f "%s"' % config['bkfile'] ) # the path of the backup
return ''.join(cmd)
def GetEvents(starttime=''):
# WMI call to retrieve event log messages related to this backup
strComputer = "."
objWMIService = Dispatch("WbemScripting.SWbemLocator")
objSWbemServices = objWMIService.ConnectServer(strComputer,"root\cimv2")
colItems = objSWbemServices.ExecQuery(
"Select * from Win32_NTLogEvent Where Logfile = 'Application' " +
"and (SourceName='ntbackup' or SourceName='WSH') ")
events={} # dict for events
# or each event
for item in colItems:
if int(item.TimeGenerated[0:12]) >= int(starttime):
joblog=[]
joblog.append(''.join((
'Date: %s\n' % item.TimeGenerated[0:14],
'User: %s\tComputer: %s\n' % (item.ComputerName, item.User),
'Source: %s\tType: %s\n' % (item.SourceName, item.Type),
'EventID: %s\t Category: %s\n' % (item.EventCode, item.CategoryString),
'Message:\n' )).encode('ascii'))
# A loop to indent the messages and expand tabs
for line in item.Message.splitlines():
if len(lstrip(line)) > 0:
joblog.append(' %s\n' % expandtabs(line.encode('ascii'),4) )
events[str(item.TimeGenerated).encode('ascii')[0:14]] = ''.join(joblog)
# get and then sort the keys
eventkeys=events.keys()
eventkeys.sort()
# use list comprehension to return a sorted list of the events
return [ events[i] for i in eventkeys ]
def WriteLog(filename='', joblog=[]):
try:
fp=open(filename,'a')
fp.write('-----\n'.join(joblog))
fp.close()
except IOError:
raise BackupError, 'Unable to write logfile'
def EmailReport(config={}, joblog=[]):
try:
message=[]
message.append('From: %s\n' % config['reportemail'] )
message.append('To: %s\n' % config['reportemail'] )
message.append('Subject: [backup.py] %s - report for %s\n\n' % (config['compname'],starttime) )
message.append('-----\n'.join(joblog))
server = smtplib.SMTP(config['smptsrv'])
server.sendmail(config['reportemail'],config['reportemail'],''.join(message))
server.quit()
except smtplib.socket.error, msg:
raise BackupError, 'Unable to connect to smtp server'
def UploadFullLogs(config={}):
'''Copy ntbackup log files from the users profile into the backup directory'''
try:
# NT backup stores its full log files in users profile
LogFileDir=shell.ExpandEnvironmentStrings('%USERPROFILE%\\Local Settings\\Application Data\\Microsoft\\Windows NT\\NTBackup\\data')
files=oFSO.GetFolder(LogFileDir).Files
for file in files:
newfilename='%s\\fulllog-%s.log' % (config['backupdir'],
strftime('%Y%m%d%H%M',strptime(str(file.DateLastModified), '%m/%d/%y %H:%M:%S')))
file.copy(newfilename,True)
except (IOError,pywintypes.com_error):
raise BackupError, 'Unable to copy log files'
def SendHTTPReport(stat=0, msg=''):
pass
#import urllib2, urllib
#msg='backup.py checking in. %s' % msg
#reporterpath="http://reporteraddress.com/reporter.py"
#srvname=compname=shell.ExpandEnvironmentStrings('%COMPUTERNAME%')
#params = urllib.urlencode({'stat': stat, 'srv': srvname, 'msg': msg})
#try:
# f = urllib2.urlopen("%s?%s" % (reporterpath,params))
#except urllib2.HTTPError, e:
# pass
if __name__ == '__main__':
try:
# create a script shell object
shell = Dispatch("WScript.Shell")
# create a filesystem object
oFSO = Dispatch ("Scripting.FileSystemObject")
# configpath = path to wherever the config lives in your domain
configpath = '\\\\domain.org\\sysvol\\domain.org\\scripts\\ntbackup\\backup.xml'
# Get configuration
config=Getconfig(configpath=configpath,
compname=shell.ExpandEnvironmentStrings('%COMPUTERNAME%'),
hour=strftime('%H'), dow=strftime('%w'), dom=strftime('%d'))
# verify the backup folder exists
if not oFSO.FolderExists(config['backupdir']):
raise BackupError, 'Backup Directory doesn\'t exist'
# add the names for the log, selection, and backup file to the config
btime = strftime('%Y%m%d%H')
# start time
starttime = strftime('%Y%m%d%H%M')
# add filename information to the config
config['bkselfile']="%sbackup.bks" % config['backupdir']
config['bklogfile']="%sbackup%s.log" % (config['backupdir'],btime)
config['bkfile']="%sbackup%s.bkf" % (config['backupdir'],btime)
# write the selections
WriteSelectionsFile(config)
# remove old backups and logs
RemoveOldFiles(config)
# build the command to be executed based on the config
config['bkcmd']=BuildCommand(config)
# print config
# small pause so that the selections is written tot disk
sleep(2)
# joblog
joblog=[]
joblog.append('Start time: %s \nCommand: %s \n' % (starttime, config['bkcmd']))
# write the command to the event log
shell.LogEvent(4, 'Running a ntbackup with the command line\n%s' % config['bkcmd'])
# Run the backup
ret = shell.Run(config['bkcmd'], 1, True)
# note the return value and end time
joblog.append('End time: %s Return Value: %s \n' % (strftime('%Y%m%d%H%M'),ret))
sleep(2) #small pause to give events to be written
# Get events
joblog.append('Events:\n')
joblog.extend(GetEvents(starttime))
# Write Log File
WriteLog(config['bklogfile'],joblog)
# copy full logs to backup directory
UploadFullLogs(config)
# Email Job report
EmailReport(config,joblog)
# Send a status message through http
SendHTTPReport(stat=0, msg='Backup completed successfully.')
except BackupError, msg:
failmsg="Backup Failed!\n%s" % msg
# Send a eventlog message
shell.LogEvent(1, failmsg)
# Send a status message through http
SendHTTPReport(stat=1, msg=failmsg)
# Email Job report
EmailReport(config,[failmsg])
print failmsg