#!/usr/bin/env python
# Copyright (c) 2007, Secure64 Software Corporation
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
# 
#
#
#	When named.conf changes, update the NSD machine
#
#

#-- imports
import getopt
import os
import os.path
import popen2
import re
import sys
import time

if os.path.exists('../bind2nsd/Config.py'):
   sys.path.append('../bind2nsd')
   from Config import *
   from NamedConf import *
   from NsdConf import *
   from Utils import *
else:
   from bind2nsd.Config import *
   from bind2nsd.NamedConf import *
   from bind2nsd.NsdConf import *
   from bind2nsd.Utils import *

if os.path.exists('../pexpect-2.1'):
   sys.path.append('../pexpect-2.1')
import pexpect
import pxssh

#-- globals
conf = Config()

#-- useful functions
def usage():
   print 'nsd-sync %s, copyright(c) 2007, Secure64 Software Corporation' \
               % (conf.getValue('version'))
   print
   print 'usage: nsd-sync [-a|--analyze-only] [-h|--help] [-s|--sync-only]'
   print '                [-n|--now]'
   print '   -a | --analyze-only => look for and report errors, but do'
   print '                          not sync with the NSD server'
   print '   -h | --help         => print this message and quit'
   print '   -n | --now          => do not poll, sync immediately'
   print '   -s | --sync-only    => sync without translating BIND files'
   print '   -v | --verbose      => output lots of info'
   return

def rebuild_nsd_files():
   result = False
   xlate = conf.getValue('bind2nsd')
   if os.path.exists(xlate):
      result = run_cmd(xlate, 'running bind2nsd...')
   else:
      report_error('? could not find "%s" and have got to have it' % (xlate))
      report_error('  skipping rebuild of NSD files')
   return result

def scp_target():
   #-- do the scp to an actual NSD server
   report_info('=> using scp to transfer to NSD system...')
   tmpdir = conf.getValue('tmpdir') 	# must have trailing '/'
   if not os.path.exists(tmpdir) and not os.path.isdir(tmpdir):
      bail('? cannot find "%s"...' % (tmpdir))

   #-- this feels a bit dodgy due to issues in pexpect when it goes
   #   up against passwd and other such nastiness from scp/ssh -- all
   #   we should have to do is child.wait() really, but that does not
   #   work as it should.
   #
   #   NB: it turn out that you can _not_ put a '*' at the end of the 
   #   source path; pexpect.spawn() screws up and the parsing of the string
   #   and ends up ignoring everything up to the '*', meaning the command
   #   does not have the 'scp' part in it and does not get executed properly.
   #
   pwd = os.getcwd()
   os.chdir(tmpdir)
   flist = os.listdir('.')
   fnames = ' '.join(flist)
   cmd  = 'scp -r ' + fnames 
   cmd += ' ' + conf.getValue('destuser') + '@' 
   cmd += conf.getValue('dest-ip') + ':'
   report_info('=> ' + cmd)
   child = pexpect.spawn(cmd)
   if len(conf.getValue('dnspw')) > 0:
      child.expect('.*ssword:')
      child.sendline(conf.getValue('dnspw'))
   child.expect('.*' + conf.getValue('nsd_conf') + '.*')
   child.expect(pexpect.EOF)
   child.close()
   os.chdir(pwd)

   return

def cp_files(analyze):
   #-- we assume everything has already been copied to the tmpdir by bind2nsd

   if analyze:
      return

   tmpdir = conf.getValue('tmpdir') 	# must have trailing '/'
   if not os.path.exists(tmpdir) and not os.path.isdir(tmpdir):
      bail('? cannot find "%s"...' % (tmpdir))

   #-- scp the entire tmp directory
   if conf.getValue('DEMO-MODE'):
      report_info('** scp would go here, but cp -r for demonstration purposes')
      cmd = 'cp -r ' + tmpdir + '* ' + conf.getValue('destdir')
      run_cmd(cmd, 'using cp to transfer to demo system...')
   else:
      scp_target()

   return

def restart_nsd():
   if conf.getValue('DEMO-MODE'):
      cmd = conf.getValue('stop_cmd')
      run_cmd(cmd, 'stopping nsd...')

      # BOZO: rebuild is not behaving when there are errors, so the hack is
      # to remove the existing db, run the zone compiler and restart nsd
      #cmd = conf.getValue('rebuild_cmd')
      #os.system(cmd)
      cmd = 'rm -f ' + conf.getValue('database')
      run_cmd(cmd, 'removing old zonedb...')
      cmd = conf.getValue('zonec_cmd')
      run_cmd(cmd, 'rebuilding zonedb...')

      cmd = conf.getValue('start_cmd')
      run_cmd(cmd, 'starting nsd...')
   else:
      cmd = 'ssh -a -x '
      cmd += conf.getValue('destuser') + '@' + conf.getValue('dest-ip')
      child = pexpect.spawn(cmd)
      if not child.isalive():
         bail('? cannot login to NSD system at %s' % \
	      (conf.getValue('dest-ip')))
      else:
         report_info('=> restarting NSD on %s' % \
	             (conf.getValue('dest-ip')))
	 child.expect('.*ssword:')
	 child.sendline(conf.getValue('syspw'))
	 child.expect('# ')
	 report_info('=> now logged in')
	 report_info('=> issuing zonec')
	 child.sendline(conf.getValue('zonec_cmd'))
	 if isVerbose():
	    child.logfile = sys.stdout
	 child.expect('# ')
	 report_info('=> issuing stop')
	 child.sendline(conf.getValue('stop_cmd'))
	 child.expect('# ')
	 report_info('=> issuing start')
	 child.sendline(conf.getValue('start_cmd'))
	 child.expect('# ')
	 child.sendline('exit')
	 child.close()
         report_info('=> restart done')

   return

def quick_parse():
   #-- build an in-core representation of the named.conf file
   named_root  = conf.getValue('named_root')
   named_fname = conf.getValue('named_conf')
   report_info('=> parsing named.conf file \"%s\"...' % (named_fname))

   pwd = os.getcwd()
   if os.path.exists(named_root) and os.path.isdir(named_root):
      os.chdir(named_root)
   else:
      bail('? cannot find the named root directory "%s"' % (named_root))
   named = NamedConf(named_fname)
   os.chdir(pwd)

   return named

def run_named_check(named):
   #-- run named-checkconf on the config file and then run named-checkzone 
   #   on each zone file
   chkconf = conf.getValue('named-checkconf')
   if os.path.exists(chkconf):
      fname = conf.getValue('named_root')
      fname += '/' + conf.getValue('named_conf')
      report_info('=> running "%s" on "%s"...' % (chkconf, fname))
      (output, errors) = run_cmd_capture(chkconf + ' ' + fname)
      if len(errors) > 0:
         report_info('? errors found --->')
	 report_info(errors)
      else:
         report_info('   all is well.')
   else:
      report_error("? wanted to run named-checkconf, dude, but it's not there.")

   chkzone = conf.getValue('named-checkzone')
   if os.path.exists(chkzone):
      zdict = named.getZones()
      zlist = zdict.keys()
      zlist.sort()
      rname = named.getOptions().getDirectory().replace('"','')
      report_info('=> running "%s" on all zones...' % (chkzone))
      prog = re.compile(':[0-9][0-9]*:')
      for ii in zlist:
         zone = zdict[ii].getName()
	 zfile = rname + '/' + zdict[ii].getFile()
         (output, errors) = run_cmd_capture(chkzone + ' ' + zone + ' ' + zfile)
         if len(output) > 0 and prog.search(output) != None:
	    report_info(output.strip())
   else:
      report_error("? wanted to run named-checkzone, dude, but it's not there.")

   return

def run_zonec():
   zonec = conf.getValue('zonec_cmd')
   if os.path.exists(zonec):
      report_info('=> running the zone compiler "%s"...' % (zonec))
      fname = conf.getValue('nsd_conf')
      tmpdir = conf.getValue('tmpdir')
      cmd = zonec + ' -c ' + tmpdir + '/' + fname + ' -d ' + tmpdir
      cmd += ' -f ' + tmpdir + '/zone.db'
      os.system('rm -f ' + tmpdir + '/zone.db')
      (output, errors) = run_cmd_capture(cmd)
      if len(errors) > 0:
         report_info('? errors found --->')
	 report_info(errors)
      else:
         report_info('   all is well.')
   else:
      report_error("? hmph.  wanted to run zonec, but it's not there.")

   return


#-- main ---------------------------------------------------------------
def main():

   try:
      opts, args = getopt.getopt(sys.argv[1:],
                                 'ahnsv',
				 ['analyze-only', 'help', 'now', 'sync-only',
				  'verbose']
				)
   except getopt.GetoptError:
      usage()
      sys.exit(1)

   now = False
   analyze_only = False
   sync_only = False
   for ii, val in opts:
      if ii in ('-a', '--analyze-only'):
         analyze_only = True
      if ii in ('-h', '--help'):
         usage()
	 sys.exit(0)
      if ii in ('-n', '--now'):
         now = True
      if ii in ('-s', '--sync-only'):
         sync_only = True
      if ii in ('-v', '--verbose'):
         set_verbosity(True)

   last_stat = {}
   this_stat = {}

   #-- don't poll unless we need to...
   if now:
      rebuild_nsd_files()
      cp_files(analyze_only)
      restart_nsd()
      sys.exit(0)

   #-- ...and don't poll if we just need to sync up to the machine...
   if sync_only:
      cp_files(analyze_only)
      restart_nsd()
      sys.exit(0)

   #-- ...and don't poll if we're just checking things out...
   if analyze_only:
      #-- well, and do a couple of extra things, too
      set_verbosity(True)
      report_info( \
            'nsd-sync %s, copyright(c) 2007, Secure64 Software Corporation' \
            % (conf.getValue('version')))
      named = quick_parse()
      rebuild_nsd_files()
      run_named_check(named)
      cp_files(analyze_only)
      run_zonec()
      sys.exit(0)

   #-- apparently we need to poll...
   tmplist = conf.getValue('named_watchlist').split()
   watchlist = []
   for ii in tmplist:
      watchlist.append(ii.strip())
   while True:
      for ii in watchlist:
         if ii in last_stat.keys():
            statinfo = os.stat(ii)
            this_stat[ii] = (statinfo.st_size, statinfo.st_mtime)
            (old_size, old_mtime) = last_stat[ii]
            (new_size, new_mtime) = this_stat[ii]
            if old_size != new_size or old_mtime != new_mtime:
               report_info('aha! "%s" has changed!' % (ii))
	       last_stat[ii] = (new_size, new_mtime)
	       rebuild_nsd_files()
               cp_files(analyze_only)
	       restart_nsd()
         else:
            statinfo = os.stat(ii)
            last_stat[ii] = (statinfo.st_size, statinfo.st_mtime)
            this_stat[ii] = last_stat[ii]
   
      time.sleep(int(conf.getValue('sleep_time')))

   sys.exit(0)

#-- just in case
if __name__ == '__main__':
   main()
