Home | History | Annotate | Line # | Download | only in scripts
      1  1.1  christos #!/usr/bin/env python
      2  1.1  christos # Copyright (c) 2007, Secure64 Software Corporation
      3  1.1  christos # 
      4  1.1  christos # Permission is hereby granted, free of charge, to any person obtaining a copy
      5  1.1  christos # of this software and associated documentation files (the "Software"), to deal
      6  1.1  christos # in the Software without restriction, including without limitation the rights
      7  1.1  christos # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
      8  1.1  christos # copies of the Software, and to permit persons to whom the Software is
      9  1.1  christos # furnished to do so, subject to the following conditions:
     10  1.1  christos # 
     11  1.1  christos # The above copyright notice and this permission notice shall be included in
     12  1.1  christos # all copies or substantial portions of the Software.
     13  1.1  christos # 
     14  1.1  christos # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
     15  1.1  christos # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
     16  1.1  christos # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
     17  1.1  christos # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
     18  1.1  christos # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
     19  1.1  christos # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
     20  1.1  christos # THE SOFTWARE.
     21  1.1  christos # 
     22  1.1  christos #
     23  1.1  christos #
     24  1.1  christos #	When named.conf changes, update the NSD machine
     25  1.1  christos #
     26  1.1  christos #
     27  1.1  christos 
     28  1.1  christos #-- imports
     29  1.1  christos import getopt
     30  1.1  christos import os
     31  1.1  christos import os.path
     32  1.1  christos import popen2
     33  1.1  christos import re
     34  1.1  christos import sys
     35  1.1  christos import time
     36  1.1  christos 
     37  1.1  christos if os.path.exists('../bind2nsd/Config.py'):
     38  1.1  christos    sys.path.append('../bind2nsd')
     39  1.1  christos    from Config import *
     40  1.1  christos    from NamedConf import *
     41  1.1  christos    from NsdConf import *
     42  1.1  christos    from Utils import *
     43  1.1  christos else:
     44  1.1  christos    from bind2nsd.Config import *
     45  1.1  christos    from bind2nsd.NamedConf import *
     46  1.1  christos    from bind2nsd.NsdConf import *
     47  1.1  christos    from bind2nsd.Utils import *
     48  1.1  christos 
     49  1.1  christos if os.path.exists('../pexpect-2.1'):
     50  1.1  christos    sys.path.append('../pexpect-2.1')
     51  1.1  christos import pexpect
     52  1.1  christos import pxssh
     53  1.1  christos 
     54  1.1  christos #-- globals
     55  1.1  christos conf = Config()
     56  1.1  christos 
     57  1.1  christos #-- useful functions
     58  1.1  christos def usage():
     59  1.1  christos    print 'nsd-sync %s, copyright(c) 2007, Secure64 Software Corporation' \
     60  1.1  christos                % (conf.getValue('version'))
     61  1.1  christos    print
     62  1.1  christos    print 'usage: nsd-sync [-a|--analyze-only] [-h|--help] [-s|--sync-only]'
     63  1.1  christos    print '                [-n|--now]'
     64  1.1  christos    print '   -a | --analyze-only => look for and report errors, but do'
     65  1.1  christos    print '                          not sync with the NSD server'
     66  1.1  christos    print '   -h | --help         => print this message and quit'
     67  1.1  christos    print '   -n | --now          => do not poll, sync immediately'
     68  1.1  christos    print '   -s | --sync-only    => sync without translating BIND files'
     69  1.1  christos    print '   -v | --verbose      => output lots of info'
     70  1.1  christos    return
     71  1.1  christos 
     72  1.1  christos def rebuild_nsd_files():
     73  1.1  christos    result = False
     74  1.1  christos    xlate = conf.getValue('bind2nsd')
     75  1.1  christos    if os.path.exists(xlate):
     76  1.1  christos       result = run_cmd(xlate, 'running bind2nsd...')
     77  1.1  christos    else:
     78  1.1  christos       report_error('? could not find "%s" and have got to have it' % (xlate))
     79  1.1  christos       report_error('  skipping rebuild of NSD files')
     80  1.1  christos    return result
     81  1.1  christos 
     82  1.1  christos def scp_target():
     83  1.1  christos    #-- do the scp to an actual NSD server
     84  1.1  christos    report_info('=> using scp to transfer to NSD system...')
     85  1.1  christos    tmpdir = conf.getValue('tmpdir') 	# must have trailing '/'
     86  1.1  christos    if not os.path.exists(tmpdir) and not os.path.isdir(tmpdir):
     87  1.1  christos       bail('? cannot find "%s"...' % (tmpdir))
     88  1.1  christos 
     89  1.1  christos    #-- this feels a bit dodgy due to issues in pexpect when it goes
     90  1.1  christos    #   up against passwd and other such nastiness from scp/ssh -- all
     91  1.1  christos    #   we should have to do is child.wait() really, but that does not
     92  1.1  christos    #   work as it should.
     93  1.1  christos    #
     94  1.1  christos    #   NB: it turn out that you can _not_ put a '*' at the end of the 
     95  1.1  christos    #   source path; pexpect.spawn() screws up and the parsing of the string
     96  1.1  christos    #   and ends up ignoring everything up to the '*', meaning the command
     97  1.1  christos    #   does not have the 'scp' part in it and does not get executed properly.
     98  1.1  christos    #
     99  1.1  christos    pwd = os.getcwd()
    100  1.1  christos    os.chdir(tmpdir)
    101  1.1  christos    flist = os.listdir('.')
    102  1.1  christos    fnames = ' '.join(flist)
    103  1.1  christos    cmd  = 'scp -r ' + fnames 
    104  1.1  christos    cmd += ' ' + conf.getValue('destuser') + '@' 
    105  1.1  christos    cmd += conf.getValue('dest-ip') + ':'
    106  1.1  christos    report_info('=> ' + cmd)
    107  1.1  christos    child = pexpect.spawn(cmd)
    108  1.1  christos    if len(conf.getValue('dnspw')) > 0:
    109  1.1  christos       child.expect('.*ssword:')
    110  1.1  christos       child.sendline(conf.getValue('dnspw'))
    111  1.1  christos    child.expect('.*' + conf.getValue('nsd_conf') + '.*')
    112  1.1  christos    child.expect(pexpect.EOF)
    113  1.1  christos    child.close()
    114  1.1  christos    os.chdir(pwd)
    115  1.1  christos 
    116  1.1  christos    return
    117  1.1  christos 
    118  1.1  christos def cp_files(analyze):
    119  1.1  christos    #-- we assume everything has already been copied to the tmpdir by bind2nsd
    120  1.1  christos 
    121  1.1  christos    if analyze:
    122  1.1  christos       return
    123  1.1  christos 
    124  1.1  christos    tmpdir = conf.getValue('tmpdir') 	# must have trailing '/'
    125  1.1  christos    if not os.path.exists(tmpdir) and not os.path.isdir(tmpdir):
    126  1.1  christos       bail('? cannot find "%s"...' % (tmpdir))
    127  1.1  christos 
    128  1.1  christos    #-- scp the entire tmp directory
    129  1.1  christos    if conf.getValue('DEMO-MODE'):
    130  1.1  christos       report_info('** scp would go here, but cp -r for demonstration purposes')
    131  1.1  christos       cmd = 'cp -r ' + tmpdir + '* ' + conf.getValue('destdir')
    132  1.1  christos       run_cmd(cmd, 'using cp to transfer to demo system...')
    133  1.1  christos    else:
    134  1.1  christos       scp_target()
    135  1.1  christos 
    136  1.1  christos    return
    137  1.1  christos 
    138  1.1  christos def restart_nsd():
    139  1.1  christos    if conf.getValue('DEMO-MODE'):
    140  1.1  christos       cmd = conf.getValue('stop_cmd')
    141  1.1  christos       run_cmd(cmd, 'stopping nsd...')
    142  1.1  christos 
    143  1.1  christos       # BOZO: rebuild is not behaving when there are errors, so the hack is
    144  1.1  christos       # to remove the existing db, run the zone compiler and restart nsd
    145  1.1  christos       #cmd = conf.getValue('rebuild_cmd')
    146  1.1  christos       #os.system(cmd)
    147  1.1  christos       cmd = 'rm -f ' + conf.getValue('database')
    148  1.1  christos       run_cmd(cmd, 'removing old zonedb...')
    149  1.1  christos       cmd = conf.getValue('zonec_cmd')
    150  1.1  christos       run_cmd(cmd, 'rebuilding zonedb...')
    151  1.1  christos 
    152  1.1  christos       cmd = conf.getValue('start_cmd')
    153  1.1  christos       run_cmd(cmd, 'starting nsd...')
    154  1.1  christos    else:
    155  1.1  christos       cmd = 'ssh -a -x '
    156  1.1  christos       cmd += conf.getValue('destuser') + '@' + conf.getValue('dest-ip')
    157  1.1  christos       child = pexpect.spawn(cmd)
    158  1.1  christos       if not child.isalive():
    159  1.1  christos          bail('? cannot login to NSD system at %s' % \
    160  1.1  christos 	      (conf.getValue('dest-ip')))
    161  1.1  christos       else:
    162  1.1  christos          report_info('=> restarting NSD on %s' % \
    163  1.1  christos 	             (conf.getValue('dest-ip')))
    164  1.1  christos 	 child.expect('.*ssword:')
    165  1.1  christos 	 child.sendline(conf.getValue('syspw'))
    166  1.1  christos 	 child.expect('# ')
    167  1.1  christos 	 report_info('=> now logged in')
    168  1.1  christos 	 report_info('=> issuing zonec')
    169  1.1  christos 	 child.sendline(conf.getValue('zonec_cmd'))
    170  1.1  christos 	 if isVerbose():
    171  1.1  christos 	    child.logfile = sys.stdout
    172  1.1  christos 	 child.expect('# ')
    173  1.1  christos 	 report_info('=> issuing stop')
    174  1.1  christos 	 child.sendline(conf.getValue('stop_cmd'))
    175  1.1  christos 	 child.expect('# ')
    176  1.1  christos 	 report_info('=> issuing start')
    177  1.1  christos 	 child.sendline(conf.getValue('start_cmd'))
    178  1.1  christos 	 child.expect('# ')
    179  1.1  christos 	 child.sendline('exit')
    180  1.1  christos 	 child.close()
    181  1.1  christos          report_info('=> restart done')
    182  1.1  christos 
    183  1.1  christos    return
    184  1.1  christos 
    185  1.1  christos def quick_parse():
    186  1.1  christos    #-- build an in-core representation of the named.conf file
    187  1.1  christos    named_root  = conf.getValue('named_root')
    188  1.1  christos    named_fname = conf.getValue('named_conf')
    189  1.1  christos    report_info('=> parsing named.conf file \"%s\"...' % (named_fname))
    190  1.1  christos 
    191  1.1  christos    pwd = os.getcwd()
    192  1.1  christos    if os.path.exists(named_root) and os.path.isdir(named_root):
    193  1.1  christos       os.chdir(named_root)
    194  1.1  christos    else:
    195  1.1  christos       bail('? cannot find the named root directory "%s"' % (named_root))
    196  1.1  christos    named = NamedConf(named_fname)
    197  1.1  christos    os.chdir(pwd)
    198  1.1  christos 
    199  1.1  christos    return named
    200  1.1  christos 
    201  1.1  christos def run_named_check(named):
    202  1.1  christos    #-- run named-checkconf on the config file and then run named-checkzone 
    203  1.1  christos    #   on each zone file
    204  1.1  christos    chkconf = conf.getValue('named-checkconf')
    205  1.1  christos    if os.path.exists(chkconf):
    206  1.1  christos       fname = conf.getValue('named_root')
    207  1.1  christos       fname += '/' + conf.getValue('named_conf')
    208  1.1  christos       report_info('=> running "%s" on "%s"...' % (chkconf, fname))
    209  1.1  christos       (output, errors) = run_cmd_capture(chkconf + ' ' + fname)
    210  1.1  christos       if len(errors) > 0:
    211  1.1  christos          report_info('? errors found --->')
    212  1.1  christos 	 report_info(errors)
    213  1.1  christos       else:
    214  1.1  christos          report_info('   all is well.')
    215  1.1  christos    else:
    216  1.1  christos       report_error("? wanted to run named-checkconf, dude, but it's not there.")
    217  1.1  christos 
    218  1.1  christos    chkzone = conf.getValue('named-checkzone')
    219  1.1  christos    if os.path.exists(chkzone):
    220  1.1  christos       zdict = named.getZones()
    221  1.1  christos       zlist = zdict.keys()
    222  1.1  christos       zlist.sort()
    223  1.1  christos       rname = named.getOptions().getDirectory().replace('"','')
    224  1.1  christos       report_info('=> running "%s" on all zones...' % (chkzone))
    225  1.1  christos       prog = re.compile(':[0-9][0-9]*:')
    226  1.1  christos       for ii in zlist:
    227  1.1  christos          zone = zdict[ii].getName()
    228  1.1  christos 	 zfile = rname + '/' + zdict[ii].getFile()
    229  1.1  christos          (output, errors) = run_cmd_capture(chkzone + ' ' + zone + ' ' + zfile)
    230  1.1  christos          if len(output) > 0 and prog.search(output) != None:
    231  1.1  christos 	    report_info(output.strip())
    232  1.1  christos    else:
    233  1.1  christos       report_error("? wanted to run named-checkzone, dude, but it's not there.")
    234  1.1  christos 
    235  1.1  christos    return
    236  1.1  christos 
    237  1.1  christos def run_zonec():
    238  1.1  christos    zonec = conf.getValue('zonec_cmd')
    239  1.1  christos    if os.path.exists(zonec):
    240  1.1  christos       report_info('=> running the zone compiler "%s"...' % (zonec))
    241  1.1  christos       fname = conf.getValue('nsd_conf')
    242  1.1  christos       tmpdir = conf.getValue('tmpdir')
    243  1.1  christos       cmd = zonec + ' -c ' + tmpdir + '/' + fname + ' -d ' + tmpdir
    244  1.1  christos       cmd += ' -f ' + tmpdir + '/zone.db'
    245  1.1  christos       os.system('rm -f ' + tmpdir + '/zone.db')
    246  1.1  christos       (output, errors) = run_cmd_capture(cmd)
    247  1.1  christos       if len(errors) > 0:
    248  1.1  christos          report_info('? errors found --->')
    249  1.1  christos 	 report_info(errors)
    250  1.1  christos       else:
    251  1.1  christos          report_info('   all is well.')
    252  1.1  christos    else:
    253  1.1  christos       report_error("? hmph.  wanted to run zonec, but it's not there.")
    254  1.1  christos 
    255  1.1  christos    return
    256  1.1  christos 
    257  1.1  christos 
    258  1.1  christos #-- main ---------------------------------------------------------------
    259  1.1  christos def main():
    260  1.1  christos 
    261  1.1  christos    try:
    262  1.1  christos       opts, args = getopt.getopt(sys.argv[1:],
    263  1.1  christos                                  'ahnsv',
    264  1.1  christos 				 ['analyze-only', 'help', 'now', 'sync-only',
    265  1.1  christos 				  'verbose']
    266  1.1  christos 				)
    267  1.1  christos    except getopt.GetoptError:
    268  1.1  christos       usage()
    269  1.1  christos       sys.exit(1)
    270  1.1  christos 
    271  1.1  christos    now = False
    272  1.1  christos    analyze_only = False
    273  1.1  christos    sync_only = False
    274  1.1  christos    for ii, val in opts:
    275  1.1  christos       if ii in ('-a', '--analyze-only'):
    276  1.1  christos          analyze_only = True
    277  1.1  christos       if ii in ('-h', '--help'):
    278  1.1  christos          usage()
    279  1.1  christos 	 sys.exit(0)
    280  1.1  christos       if ii in ('-n', '--now'):
    281  1.1  christos          now = True
    282  1.1  christos       if ii in ('-s', '--sync-only'):
    283  1.1  christos          sync_only = True
    284  1.1  christos       if ii in ('-v', '--verbose'):
    285  1.1  christos          set_verbosity(True)
    286  1.1  christos 
    287  1.1  christos    last_stat = {}
    288  1.1  christos    this_stat = {}
    289  1.1  christos 
    290  1.1  christos    #-- don't poll unless we need to...
    291  1.1  christos    if now:
    292  1.1  christos       rebuild_nsd_files()
    293  1.1  christos       cp_files(analyze_only)
    294  1.1  christos       restart_nsd()
    295  1.1  christos       sys.exit(0)
    296  1.1  christos 
    297  1.1  christos    #-- ...and don't poll if we just need to sync up to the machine...
    298  1.1  christos    if sync_only:
    299  1.1  christos       cp_files(analyze_only)
    300  1.1  christos       restart_nsd()
    301  1.1  christos       sys.exit(0)
    302  1.1  christos 
    303  1.1  christos    #-- ...and don't poll if we're just checking things out...
    304  1.1  christos    if analyze_only:
    305  1.1  christos       #-- well, and do a couple of extra things, too
    306  1.1  christos       set_verbosity(True)
    307  1.1  christos       report_info( \
    308  1.1  christos             'nsd-sync %s, copyright(c) 2007, Secure64 Software Corporation' \
    309  1.1  christos             % (conf.getValue('version')))
    310  1.1  christos       named = quick_parse()
    311  1.1  christos       rebuild_nsd_files()
    312  1.1  christos       run_named_check(named)
    313  1.1  christos       cp_files(analyze_only)
    314  1.1  christos       run_zonec()
    315  1.1  christos       sys.exit(0)
    316  1.1  christos 
    317  1.1  christos    #-- apparently we need to poll...
    318  1.1  christos    tmplist = conf.getValue('named_watchlist').split()
    319  1.1  christos    watchlist = []
    320  1.1  christos    for ii in tmplist:
    321  1.1  christos       watchlist.append(ii.strip())
    322  1.1  christos    while True:
    323  1.1  christos       for ii in watchlist:
    324  1.1  christos          if ii in last_stat.keys():
    325  1.1  christos             statinfo = os.stat(ii)
    326  1.1  christos             this_stat[ii] = (statinfo.st_size, statinfo.st_mtime)
    327  1.1  christos             (old_size, old_mtime) = last_stat[ii]
    328  1.1  christos             (new_size, new_mtime) = this_stat[ii]
    329  1.1  christos             if old_size != new_size or old_mtime != new_mtime:
    330  1.1  christos                report_info('aha! "%s" has changed!' % (ii))
    331  1.1  christos 	       last_stat[ii] = (new_size, new_mtime)
    332  1.1  christos 	       rebuild_nsd_files()
    333  1.1  christos                cp_files(analyze_only)
    334  1.1  christos 	       restart_nsd()
    335  1.1  christos          else:
    336  1.1  christos             statinfo = os.stat(ii)
    337  1.1  christos             last_stat[ii] = (statinfo.st_size, statinfo.st_mtime)
    338  1.1  christos             this_stat[ii] = last_stat[ii]
    339  1.1  christos    
    340  1.1  christos       time.sleep(int(conf.getValue('sleep_time')))
    341  1.1  christos 
    342  1.1  christos    sys.exit(0)
    343  1.1  christos 
    344  1.1  christos #-- just in case
    345  1.1  christos if __name__ == '__main__':
    346  1.1  christos    main()
    347