nsd-sync revision 1.1 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
29 import getopt
30 import os
31 import os.path
32 import popen2
33 import re
34 import sys
35 import time
36
37 if 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 *
43 else:
44 from bind2nsd.Config import *
45 from bind2nsd.NamedConf import *
46 from bind2nsd.NsdConf import *
47 from bind2nsd.Utils import *
48
49 if os.path.exists('../pexpect-2.1'):
50 sys.path.append('../pexpect-2.1')
51 import pexpect
52 import pxssh
53
54 #-- globals
55 conf = Config()
56
57 #-- useful functions
58 def 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
72 def 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
82 def 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
118 def 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
138 def 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
185 def 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
201 def 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
237 def 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 ---------------------------------------------------------------
259 def 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
345 if __name__ == '__main__':
346 main()
347