nsd-sync revision 1.1 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