#
# wbadmin.py
#


# install Windows Server Backup (command line)
# servermanagercmd.exe -install Backup-Features

# There can be two ways to check if the backup had failed from the events:
# http://technet.microsoft.com/en-us/library/dd364735(WS.10).aspx
#1. For every backup run, successful or failed, event-id 14 is generated. The event-data for this ID contains the overall status of the backup in the field 'HRESULT'. This should be 0 for a successful backup, non-zero otherwise.
#
# 2. You can configure the Task Scheduler to trigger ONEVENT when any of the failing events is published. They are all the other event-ids (except id 4) in the link you provided above.

import os, sys, subprocess, time, platform
from datetime import datetime, timedelta
import xml.dom.minidom

import win32wnet
import win32netcon 


from archiver import *

import cron

def getvalue(parentnode, nodename):
      return parentnode.getElementsByTagName(nodename)[0].childNodes[0].data

# ===========================================================================
class Wbadmin(Archiver):

    name='wbadmin'
    exe='wbadmin.exe'
    
    types=dict(copy='copy', vssfull='vssfull')
    
    # -----------------------------------------------------------------------
    def __init__(self):
        Archiver.__init__(self)
        
        self.selection=None
        self.verify=None
        self.restricted=None
        self.program_exe=None
        self.type=None
        self.target=None
        self.user=None
        self.password=None
        self.options=[]
        systemroot=os.environ.get('SystemRoot', os.environ.get('windir', 'C:\\windows'))
        self.wbadmin_bin=os.path.join(systemroot, 'system32', 'wbadmin.exe')
        self.wevtutil_bin=os.path.join(systemroot, 'system32', 'WEVTUTIL.EXE')
                
    # -----------------------------------------------------------------------
    def load(self, job, manager):
        
        log=manager.log
        now=manager.now

        errors, warnings, extra={}, {}, ''

        
        
        #
        # check job config
        #

        self.destination=job.get('destination', None)
        if self.destination:
            self.destination=self.destination.replace('\n','')
            try:
                self.destination=Destinations(self.destination, self)
            except (DestinationSyntaxError, cron.CronException), e:
                errors['destination']='syntax error: %s' % (str(e), )

        self.include=job.get('include', None)
        if self.include and not self.destination:
            errors['include']='destination required'

        self.user=job.get('user', None)
        if self.user and not self.destination:
            errors['user']='destination required'
            
        self.password=job.get('password', None)
        if self.user and not self.destination:
            errors['password']='destination required'

        if (self.user and not self.password) or (not self.user and self.password):
            errors['password']='login and pasword must be set together'

        options=job.get('options', '')
        try:
            options=quoted_string_list(options)
        except ValueError:
            errors['options']='not a valid quoted string list'
        else:
            self.options=options

        self.wbadmin_bin=job.get('wbadmin_bin', self.wbadmin_bin)
        self.wevtutil_bin=job.get('wevtutil_bin', self.wevtutil_bin)

# check login password ?
# or use warning if target dont exist 


        if not errors:

            if self.destination:
                self.type, self.target=self.destination.match(now, self.night_shift)
            else:
                self.type, self.target='default', None
    
            if self.type!=None and self.type!='none':
                
                self.wba_args=[ self.exe, 'start', 'backup', '-quiet' ]
                
                if self.target:
                    self.wba_args.append('-backupTarget:%s' % ( self.target, ))
                    
                if self.include:
                    self.wba_args.append('-include:%s' % ( self.include, ))
                
                if self.target.startswith('\\\\') and self.user and self.password:
                    self.wba_args+=[ '-user:%s' % ( self.user, ), '-password:%s' % ( self.password,) ]
                    
                if self.type.lower()=='vssfull':
                    self.wba_args.append('-vssFull')
                    
                self.wba_args.extend(self.options)
                extra+='cmdline=%s\n' % '  '.join(self.wba_args)
    
        return errors, warnings, extra

    # -----------------------------------------------------------------------
    def run(self, command, job, manager):

        log=manager.log
        now=manager.now

        import windows

        start=int(time.time())

        # FIXME: should the self.cmd_args be encoded ?

        wba_del_process=None           
        if self.name=='wbadminsys' and self.keepversions:
            wba_del_args=[ self.exe, 'delete', 'systemstatebackup', '-quiet', '-keepVersions:%d'%(self.keepversions-1,), '-backupTarget:%s' % ( self.target, ) ]
            log.info('run: %s', ' '.join(wba_del_args))
            wba_del_process=subprocess.Popen(wba_del_args, executable=self.wbadmin_bin, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            wba_del_out, wba_del_err=wba_del_process.communicate()
        
        log.info('run: %s', ' '.join(self.wba_args))
        wba_process=subprocess.Popen(self.wba_args, executable=self.wbadmin_bin, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        wba_out, wba_err=wba_process.communicate()
        
        end=int(time.time())
        
        # get the Events
        # Sometime I miss the last event, retry 3 times at least
        time.sleep(1)
        count=3 # try only once now
        while count>0:
            count-=1 
            wevt_args=[ 'WEVTUTIL.EXE', 'qe', 'Microsoft-Windows-Backup', '/rd:true', '/e:root', '/f:RenderedXml',# '/f:XML',
                        '/q:*[System[TimeCreated[timediff(@SystemTime)<=%d]]]' % (1000*int(time.time()-start+1), ), 
                      ]  
            log.info('run: %s', ' '.join(wevt_args))
            wevt_process=subprocess.Popen(wevt_args, executable=self.wevtutil_bin, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            wevt_out, wevt_err=wevt_process.communicate()
    
            dom=xml.dom.minidom.parseString(wevt_out)
        
            # search all Event, for HRESULT in EventID==14
            ev_status, hresult, msg='ERR', '', ''
            computer=platform.node()
            # TODO: check if computer name match
            for node in dom.getElementsByTagName('Event'):
                system=node.getElementsByTagName('System')[0]
                if getvalue(system, 'EventID')=='14':
                    eventdata=node.getElementsByTagName('EventData')[0]
                    for data in eventdata.getElementsByTagName('Data'):
                        if data.getAttribute('Name')=='HRESULT':
                            hresult=data.childNodes[0].data
                            
                    renderinginfo=node.getElementsByTagName('RenderingInfo')[0]
                    msg=getvalue(renderinginfo, 'Message')
                if hresult=='0':
                    ev_status='OK'
                break
            
            if hresult!=None:
                break

            if count>0:
                log.info('EventID 14 missed, retry')
                time.sleep(10)
            else:
                if wba_process.returncode==0:
                    log.warning('EventID 14 not found')
                else:
                    log.info('EventID 14 not found')

        status=u''
        status+='name=%s\r\n' % job['name']
        status+='program=%s\r\n' % self.name
        status+='version=%s\r\n' % self.__version__
        status+='hostname=%s\r\n' % platform.node()

        status+='status=%s\r\n' % ev_status
        
        status+='exit_code=%r\r\n' % wba_process.returncode
        status+='hresult=%s\r\n' % hresult
        status+='message=%s\r\n' % msg
        
        total_free_bytes, dir_out, dir_up_out, total_size=None, None, None, None
        
        if self.target:
            status+='target=%s\r\n' % self.target
            
            if self.target[-1]==':':
                target_dir=os.path.join(self.target+'\\', 'WindowsImageBackup', platform.node())
            else:
                target_dir=os.path.join(self.target, 'WindowsImageBackup', platform.node())
            if self.name=='wbadminsys':
                target_dir=os.path.join(target_dir, 'SystemStateBackup')
                
            # print 'ASX target_dir', target_dir
    
            # free_space
            try:
                target_full_filename=''
                total_free_bytes=free_space(self.target, log)
                # Search the Backup directory 
                dir_up_out, _ts=list_dir(target_dir, log)
                for filename in os.listdir(target_dir): #
                    full_filename=os.path.join(target_dir, filename)
                    stat=os.stat(full_filename)
                    if os.path.isdir(full_filename) and filename.lower().startswith('backup '):
                        if target_full_filename<full_filename:
                            target_full_filename=full_filename

                if target_full_filename:
                    dir_out, total_size=list_dir(target_full_filename, log)  
               
            except Exception, e:
                if self.user and self.password:
                    try:
                        win32wnet.WNetAddConnection2(win32netcon.RESOURCETYPE_DISK, None, self.target, None, self.user, self.password)
                        try:
                            total_free_bytes=free_space(self.target, log)
                            dir_up_out, _ts=list_dir(target_dir, log)
                            for filename in os.listdir(target_dir): #
                                full_filename=os.path.join(target_dir, filename)
                                stat=os.stat(full_filename)
                                if os.path.isdir(full_filename) and filename.lower().startswith('backup '):
                                    dir_out, total_size=list_dir(full_filename, log)
                                    break  
                        finally:
                            win32wnet.WNetCancelConnection2(self.target, 0, False)
                    except Exception, e:
                        log.error('checking target directory: %s', e)
                        status+='target_free_space=%s\r\n' % e
                else:
                    log.error('checking target directory: %s', e)
                    status+='target_free_space=%s\r\n' % e
    
            if total_free_bytes:
                status+='target_free_space=%d\r\n' % total_free_bytes
            if total_size:
                status+='target_size=%d\r\n' % total_size

        status+='start=%s\r\n' % time.ctime(start)
        status+='end=%s\r\n' % time.ctime(end)
        status+='wba_cmd=%s\r\n' % ' '.join(self.wba_args)
        status+='wevt_cmd=%s\r\n' % ' '.join(wevt_args)
        status+='wevt_exit_code=%s\r\n' % wevt_process.returncode
        
        if wba_del_process:
            
            status+='wba_del_cmd=%s\r\n' % ' '.join(wba_del_args)
            status+='wba_del_exit_code=%s\r\n' % wba_del_process.returncode
        
        attachements=[ # the type, subtype and coding is not used
                    ]
        
        if dir_up_out:
            attachements.append(('dir_up.txt', None, dir_up_out.encode('utf-8'), 'text', 'plain', 'utf-8'))

        if dir_out:
            attachements.append(('dir.txt', None, dir_out.encode('utf-8'), 'text', 'plain', 'utf-8'))
            
        if wba_out:
            attachements.append(('wba_out.txt', None, wba_out, 'text', 'plain', 'cp850'))
        if wba_err:
            attachements.append(('wba_err.txt', None, wba_err, 'text', 'plain', 'cp850'))

        if wba_del_process and wba_del_out:
            attachements.append(('wba_del_out.txt', None, wba_del_out, 'text', 'plain', 'cp850'))
        if wba_del_process and wba_del_err:
            attachements.append(('wba_del_err.txt', None, wba_del_err, 'text', 'plain', 'cp850'))
        
        if wevt_out:
            attachements.append(('wevt_out.txt', None, wevt_out, 'text', 'plain', 'cp850'))
        if wevt_err:
            attachements.append(('wevt_err.txt', None, wevt_err, 'text', 'plain', 'cp850'))
        
        return ev_status, status, attachements



class WbadminSys(Wbadmin):

    name='wbadminsys'
    exe='wbadmin.exe'
    
    types=dict(systemstate='systemstate')

    # -----------------------------------------------------------------------
    def load(self, job, manager):
        
        log=manager.log
        now=manager.now

        errors, warnings, extra={}, {}, ''

        #
        # check job config
        #

        self.destination=job.get('destination', None)
        if not self.destination:
            errors['destination']='destination required'
        else:
            self.destination=self.destination.replace('\n','')
            try:
                self.destination=Destinations(self.destination, self)
            except (DestinationSyntaxError, cron.CronException), e:
                errors['destination']='syntax error: %s' % (str(e), )

        self.keepversions=job.get('keepversions', None)
        if self.keepversions:
            try:
                self.keepversions=int(self.keepversions)
                if self.keepversions<1:
                    errors['keepversions']='must be >= 1'
            except Exception:
                errors['keepversions']='must be an integer'

        if not errors:

            if self.destination:
                self.type, self.target=self.destination.match(now, self.night_shift)
    
            if self.type!=None and self.type!='none':
                
                self.wba_args=[ self.exe, 'start', 'systemstatebackup', '-quiet', '-backupTarget:%s' % ( self.target, ) ] 
                
                extra+='cmdline=%s\n' % '  '.join(self.wba_args)
    
        return errors, warnings, extra



if __name__ == '__main__':
    if True:
        import win32wnet
        from win32netcon import RESOURCETYPE_DISK as DISK

        drive_letter = None
        path = r'\\pcasx\c$'
        win32wnet.WNetAddConnection2(DISK, drive_letter, path, None, r'pcasx\test', 'test72')
        
        total_size=0
        target_dir=path
        dir_out=u'  epoch   |          time          |      size     |    filename\r\n'
        for filename in os.listdir(target_dir): # if target_dir is unicode, return os.listdir return unicode
            full_filename=os.path.join(target_dir, filename)
            stat=os.stat(full_filename)
            if os.path.isdir(full_filename):
                size='<DIR>'
            else:
                size='%15d' % stat.st_size
                total_size+=stat.st_size
            dir_out+='%d %s %15s %s\r\n' % (stat.st_mtime, time.ctime(stat.st_mtime), size, filename)
        
        print total_size
        print dir_out
        
    
    elif False:
        dom=xml.dom.minidom.parse('wevt_out_r.xml')
        # search all Event
        for i, node in enumerate(dom.getElementsByTagName('Event')):
            system=node.getElementsByTagName('System')[0]
            eventdata=node.getElementsByTagName('EventData')[0]
            renderinginfo=node.getElementsByTagName('RenderingInfo')[0]
            print i, getvalue(system, 'EventID'), getvalue(renderinginfo, 'Message')
            if getvalue(system, 'EventID')=='14':
                for data in eventdata.getElementsByTagName('Data'):
                    if data.getAttribute('Name')=='HRESULT':
                        print '%r - %r' % (data.getAttribute('Name'), data.childNodes[0].data)
                        
                #print 'HRESULT', getvalue(eventdata, 'HRESULT')


    
    # -----------------------------------------------------------------------
    

