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