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