atrox3d
2/23/2017 - 9:53 AM

nrbackup.py

# +----------------------------------------------------------------------+
# | 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