-
Notifications
You must be signed in to change notification settings - Fork 5
/
ndm
executable file
·1035 lines (939 loc) · 50.7 KB
/
ndm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/python3
"""
ndm manages a (json-formatted) database that contains network configuration details.
The network configuration consists of network element information (DNS, DHCP, timeserver,
router, etc) and hosts.
A host definition contains the device MAC address, its assigned IP address,
and other network configuration information.
ndm uses the database to build configuration files for DNS and DHCP servers.
Supported DNS and DHCP servers pairs include:
* bind9 (DNS) and isc-dhcp-server (DHCP)
* dnsmasq (both DNS and DHCP)
Most of ndm's commands manage the database. Three commands are used to manage
the DNS/DHCP configuration files and servers:
* ndm build - Builds new configuration files from the database into a temp directory (/tmp/ndm.root)
* ndm diff - Compares the just-built configuration files with the ones running on the system
* ndm install - Installs the new configuration files from /tmp/ndm.root into the system and restarts the services
"""
import argparse
import datetime
import json
import os
import pwd
import re
import shutil
import socket
import subprocess
import sys
import time
import uuid
def perrorexit(emsg):
raise SystemExit(emsg)
def qdelfile(fn):
try:
os.remove(fn)
except OSError:
pass
def qrename(src, dst):
try:
os.rename(src, dst)
except OSError:
pass
def dosystem(docmd):
r = subprocess.run(docmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, text=True)
return r
def mktmpdir(pd):
# Makes a tmp directory /tmp/ndm.{username}
if pd.args.tmp == None:
pd.tmp = "/tmp/ndm.{}".format(pwd.getpwuid(os.getuid())[0])
os.makedirs(pd.tmp, mode=0o700, exist_ok=True)
else:
pd.tmp = pd.args.tmp
def ipinvert(ipaddr):
# Invert 3 IP address octets
els = ipaddr.split(".")
return '.'.join([els[2], els[1], els[0]])
def getmyipaddr(ifname):
import fcntl
import struct
#FM!
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
ipaddr = socket.inet_ntoa(fcntl.ioctl(
s.fileno(),
0x8915, # SIOCGIFADDR
struct.pack('256s', ifname[:15].encode('ascii') )
)[20:24])
except:
ipaddr = ""
return ipaddr
def listnormal(thestring, sepchar):
# Normalize a list in a string to be a list of items separated by the given seperator character
s1 = re.sub(' +', ' ', thestring.strip()) # Replace multiple spaces with a single space
outstring = sepchar.join(s1.split(','))
if outstring == thestring: outstring = sepchar.join(s1.split(' '))
return re.sub(' +', ' ', outstring.strip()) # Replace multiple spaces with a single space (again, just to be sure ;)
def mkbakfn(fn):
return "{}.bak".format(fn)
def mktmpfn(tmpdir, fn):
# Build full filename string /tmp/ndm.{username}/fn
return "{}/{}".format(tmpdir, os.path.basename(fn))
def printhost(pd, keyname, hname, printfmt):
print(printfmt.format(keyname, pd.db['hosts'][keyname]['hostname'][hname]['macaddr'],\
hname, pd.db['hosts'][keyname]['hostname'][hname]['flags'],\
pd.db['hosts'][keyname]['hostname'][hname]['note'], pd.db['hosts'][keyname]['dhcphostopt']))
def printnode(pd, keyname, printfmt):
# keyname is an IP address (normal) or a hostname (if CNAME)
# If an IP address, print all the hosts that have this IP address
if keyname in pd.db['hosts']:
for hname in sorted(pd.db['hosts'][keyname]['hostname']):
printhost(pd, keyname, hname, printfmt)
else:
if keyname in pd.db['cname']:
for hname in sorted(pd.db['cname'][keyname]['hostname']):
print(printfmt.format(keyname, hname, "", "+cname", "", ""))
def dodiff(pd, fnm):
# fnm is full file spec of file in the file system
print("*** <<{} | {}>> ***\n".format(fnm, mktmpfn(pd.tmp, fnm)))
r = dosystem("diff {} {}".format(fnm, mktmpfn(pd.tmp, fnm)))
# diff returns: 0:identical, 1:different, 2:trouble
rs = r.stdout if (r.returncode == 0) or (r.returncode == 1) else r.stderr
for line in rs.split("\n"):
print(line)
return True
def loadmodule(pd, modname, dnsordhcp):
# Loads modname and instantiates the class
# Done this way to avoid scoping issues
try:
# Load the module
exec("from {} import ndm{}".format(modname, dnsordhcp))
except (ModuleNotFoundError, ImportError, Exception) as X:
perrorexit("? Error importing '{}' for {}\n {}".format(modname,dnsordhcp.upper(), X))
# Instantiate the class ndm{dns|dhcp} into pd.{dns|dhcp}
try:
exec("pd.{} = ndm{}(pd)".format(dnsordhcp, dnsordhcp))
except Exception as X:
perrorexit("? Error instantiating class 'ndm{}' in file {} for {}\n {}".format(dnsordhcp, modname, dnsordhcp.upper(),X))
def cknownos(pd):
if not pd.db['cfg']['os'] in pd.knownos: perrorexit("? ndm doesn't support OS '{}' for this command\n 'sudo ndm config --os type' to set".format(pd.db['cfg']['os']))
class pdat:
def __init__(self):
self.version = "V2.12"
self.dbversion = "2"
self.snetdbversion = "1"
self.dns = None
self.dhcp = None
self.args = None
self.myuuid = 0
self.ndmcmd = ""
self.os = "" #Runtime coding shortcut
self.parser = None
self.tmp = "/tmp"
self.configfile = ""
self.defaultconfigfile = "/etc/dbndm.json"
self.snetconfigfile = "/etc/dbndmsnet.json"
self.db = None
self.dbmodified = False
self.snetdb = None
self.cmdmodified = False #db modified b/c db config cmd switch
self.dnsmasqfh = None
# These functions available to ndmdns and ndmdhcp classes
self.xqdelfile = qdelfile
self.xdosystem = dosystem
self.xqrename = qrename
self.xcopy = shutil.copy
self.xmktmpfn = mktmpfn
self.xipinvert = ipinvert
self.xmkbakfn = mkbakfn
self.xperrorexit = perrorexit
self.listformat = "{:<16} {:<17} {:<16} {:<16} {:<16} {}"
self.dbinitialize = '{"cfg":{"bindoptions":"", "dbversion":"","dns":"", "dhcp":"", "os":"", "domain":"me", "subnet":"", "internals":"",\
"subnetmask":"", "gateway":"", "timeserver":"", "dnsip":"", "dnsfqdn":"", "mxfqdn":"", "dhcplease":"86400",\
"dhcpsubnet":"","dnslistenport":"53", "myip":"", "netdev":"eth0", "externaldns":"", "hostfqdn":"",\
"blockdomains":"", "dnsinclude":"", "dhcpinclude":"", "dhcpglobalopt":"", "dhcphostopt":{}},"hosts":{},"cname":{}}'
self.defaulthosts = {"127.0.0.1":"localhost", "::1":"localhost ipv6-localhost ipv6loopback",\
"fe00::0":"ipv6-localnet", "ff00::0":"ipv6-mcastprefix",\
"ff02::1":"ipv6-allnodes", "ff02::2":"ipv6-allrouters", "ff02::3":"ipv6-allhosts"}
self.knownos = { 'raspbian', 'raspios' , 'ubuntu', 'debian' } # 'centos' not complete
# "servicename":"python module name", Loaded at start of command execution
self.knowndns = { "bind":"ndmdnsbind", "dnsmasq":"ndmdnsmasq" }
self.knowndhcp = { "isc-dhcp-server":"ndmdhcpisc", "dnsmasq":"ndmdnsmasq", "none":"ndmdhcpnone" }
def _ckconfig(self):
# OK if these VV options are not set. All others must be set
ignore = ['dhcpglobalinclude', 'dhcppoolinclude', 'dhcphostopt', 'dhcpglobalopt', 'hostfqdn', 'dnsip', 'blockdomains', 'bindoptions', 'subnetmask', 'dnsinclude', 'dhcpinclude', 'internals']
missing = ""
for i in self.db['cfg']:
if not i in ignore:
if self.db['cfg'][i] == "":
if missing != "":
missing += ', ' + i
else:
missing = i
if missing != "": perrorexit("? Configuration incomplete: {}\n Use 'sudo ndm config' to define missing items".format(missing))
def dbaddhost(self, eipaddr, emac, edhcphostopt, ehost, eopts, enote):
if "cname" in eopts:
if not eipaddr in self.db['cname']:
self.db['cname'][eipaddr] = {'hostname':{ehost:{}}}
else:
self.db['cname'][eipaddr]['hostname'][ehost] = {}
else:
if not eipaddr in self.db['hosts']:
self.db['hosts'][eipaddr] = {'hostname':{ehost:{'macaddr':emac, 'flags':eopts, 'note':enote}}, 'dhcphostopt':edhcphostopt }
else:
self.db['hosts'][eipaddr]['hostname'][ehost] = {'macaddr':emac, 'flags':eopts, 'note':enote}
self.dbmodified = True
def dbread(self, ckcfg=True):
self.configfile = self.args.db if self.args.db != None else self.defaultconfigfile
if not self.configfile.endswith(".json"):
self.configfile = "{}.json".format(self.configfile)
if self.args.create:
if os.path.isfile(self.configfile): perrorexit("? ndm configuration database {} already exists".format(self.configfile))
self.db = json.loads(self.dbinitialize)
for ip in self.defaulthosts: self.dbaddhost(ip, "", "", self.defaulthosts[ip],"+hostsonly+nodomain", "")
self.db['cfg']['dbversion'] = self.dbversion
self.dbmodified = True
else:
try:
with open(self.configfile, 'r') as dbf:
try:
self.db = json.load(dbf)
except ValueError:
perrorexit("? ndm configuration database '{}' has invalid syntax".format(self.configfile))
except:
perrorexit("? Unable to open ndm configuration database '{}'\n Use 'sudo ndm config --create' to create it".format(self.configfile))
if ckcfg: self._ckconfig()
#
# Database V2 upgrade if needed
# * Set default DHCP and DNS services if none configured
# * Add dnsinclude and copy from bindinclude if present
# * Delete bindinclude
# * Change externaldns to space-separated list
#
if not 'version' in self.db['cfg']:
self.db['cfg']['version'] = self.version
self.db['cfg']['hostname'] = socket.gethostname()
self.db['cfg']['dns'] = "bind" #Upgrade, so set these to what they should be
self.db['cfg']['dhcp'] = "isc-dhcp-server"
if not 'dnsinclude' in self.db['cfg']: self.db['cfg']['dnsinclude'] = self.db['cfg']['bindinclude']
if 'bindinclude' in self.db['cfg']: del(self.db['cfg']['bindinclude'])
if 'servicewait' in self.db['cfg']: del(self.db['cfg']['servicewait'])
pd.db['cfg']['externaldns'] = listnormal(pd.db['cfg']['externaldns'], " ") # Change to space-separated list
self.db['cfg']['netdev'] = "eth0"
self.dbmodified = True
#
# "Rename" 'dhcpkey' to DNSUpdateKey
#
if 'dhcpkey' in self.db['cfg']:
self.db['cfg']['DNSUpdateKey'] = self.db['cfg']['dhcpkey']
del(self.db['cfg']['dhcpkey'])
self.dbmodified = True
#
# Make sure keys added later are present
#
for ckey in ['bindoptions', 'dhcpglobalinclude', 'dhcppoolinclude']:
if not ckey in self.db['cfg']:
self.db['cfg'][ckey] = ""
self.dbmodified = True
#
# Do best-guess OS detection. Validity checked when needed (diff, install)
#
if self.db['cfg']['os'] == "":
ostype = ""
try:
with open("/etc/os-release") as f:
for line in f:
els = line.split('=')
if els[0] == "ID":
ostype = els[1].rstrip()
break
except:
ostype = ""
ostype = ostype.replace('"','').lower()
if ostype == "raspbian": ostype = "raspios"
pd.db['cfg']['os'] = ostype
#
# Put something in subnetmask
#
if self.db['cfg']['subnetmask'] == "": self.db['cfg']['subnetmask'] = "/24"
if self.db['cfg']['hostname'] == "": self.db['cfg']['hostname'] = socket.gethostname()
if self.db['cfg']['hostfqdn'] == "":
hostfqdn = socket.getfqdn()
if hostfqdn != self.db['cfg']['hostname']: # ne if we got a real fqdn
self.db['cfg']['hostfqdn'] = hostfqdn
self.dbmodified = True
try:
if self.db['cfg']['myip'] == "":
if self.args.myip != None: self.db['cfg']['myip'] = self.args.myip
if self.db['cfg']['myip'] == "": self.db['cfg']['myip'] = getmyipaddr(self.db['cfg']['netdev'])
except:
self.db['cfg']['myip'] = "127.0.0.1"
if self.db['cfg']['myip'].startswith("127.0"):
perrorexit("% Cannot get our IP address; please use 'sudo ndm config --create --myip my.ip.ad.dr'")
if self.db['cfg']['subnet'] == "":
ips = self.db['cfg']['myip'].split(".")
self.db['cfg']['subnet'] = '.'.join([ips[0], ips[1], ips[2]])
self.dbmodified = True
# If my MAC address ever comes out wrong, this magic is the cause!
if self.args.create: self.dbaddhost(self.db['cfg']['myip'],\
':'.join(['{:02x}'.format((uuid.getnode() >> ele) & 0xff) for ele in range(0,8*6,8)][::-1]),\
"", self.db['cfg']['hostname'], "", "")
if (self.db['cfg']['dnsip'] == "") or (self.db['cfg']['dnsip'].startswith("127.0")):
self.db['cfg']['dnsip'] = self.db['cfg']['myip']
self.dbmodified = True
def writedbjson(self, cfile, jdata):
tmpf = "{}.tmp".format(cfile)
bakf = mkbakfn(cfile)
try:
with open(tmpf, 'w') as dbf:
json.dump(jdata, dbf, indent=4, sort_keys=True)
dbf.write("\n") #Final eol
except:
perrorexit("? Error writing '{}'".format(tmpf))
qdelfile(bakf)
qrename(cfile, bakf)
qrename(tmpf, cfile)
def dbwrite(self, force=False):
if not (self.dbmodified or force): return True
self.writedbjson(self.configfile, pd.db)
self.dbmodified = False
def dbimport(self):
try:
ifl = open(self.args.importnet, 'r')
except:
perrorexit("? Cannot find import file '{}'".format(self.args.importnet))
hostsadded = 0
for line in ifl:
if line.split('#')[0] == "": continue # ignore comment lines
if "CONFIG=" in line:
els = line.rstrip().split('=')
self.db = json.loads(els[1])
if not 'hosts' in self.db: self.db['hosts'] = {}
if not 'cname' in self.db: self.db['cname'] = {}
else:
line = "{},,,,,".format(line.split('#')[0].rstrip()) # Extra commas guard against missing stuff on line
# Split and get parts: IPAddr[0], macaddr[1], hostname[2], flags[3], note[4], dhcphostopt[5]
els = line.split(',')
hostsadded += 1
self.dbaddhost(els[0], els[1], els[5], els[2].lower(), els[3], els[4])
if self.args.verbose: printhost(pd, els[0], els[2], self.listformat)
ifl.close()
print("% Added/updated {} hosts".format(hostsadded))
return True
def dbfindhost(self, hostname):
# Only looks at hosts, not CNAMEs
for ip in self.db['hosts']:
if hostname in self.db['hosts'][ip]['hostname']:
return ip
return ""
def dbdoallnodes(self, dofunc, listfmt):
lowoct = []
for keyname in self.db['hosts']:
if self.db['cfg']['subnet'] in keyname:
j = int(keyname.split(".")[3])
if not j in lowoct: lowoct.append(j)
# Do the addresses in the subnet
for i in sorted(lowoct):
keyname = "{}.{}".format(self.db['cfg']['subnet'], i)
dofunc(self, keyname, listfmt)
# Do not-in-our-subnet addresses, etc.
for keyname in sorted(self.db['hosts']):
if not self.db['cfg']['subnet'] in keyname:
dofunc(self, keyname, listfmt)
# Do CNAMEs
for keyname in sorted(self.db['cname']):
dofunc(self, keyname, self.listformat)
return True
def snetdbread(self, mustexist):
try:
with open(self.snetconfigfile, 'r') as snf:
try:
self.snetdb = json.load(snf)
except ValueError:
perrorexit("? ndm network configuration database '{}' has invalid syntax".format(self.snetconfigfile))
return True
except:
if mustexist: perrorexit("? Secondary subnet database '{}' not found".format(pd.snetconfigfile))
pass
return False
def snetdbwrite(self):
self.writedbjson(self.snetconfigfile, self.snetdb)
def getmodifyhostname(pd, keyname, hname, withwhat):
if len(pd.db['hosts'][keyname]['hostname']) > 1:
s1 = "" if withwhat == "" else "with {}".format(withwhat)
if hname == "": perrorexit("? '{}' has multiple hosts; --hostname must be specified {}".format(keyname, s1))
if not hname in pd.db['hosts'][keyname]['hostname']: perrorexit("? Hostname '{}' is not assigned to IP {}".format(hname, keyname))
return hname
else:
return list(pd.db['hosts'][keyname]['hostname'].keys())[0] #Fetch solitary hostname
def cmd_add(pd):
keyname = pd.args.ip
if keyname is None: perrorexit("? IP address must be provided")
if pd.args.hostname is None: perrorexit("? --hostname must be provided")
pd.args.hostname = pd.args.hostname.lower()
theip = pd.dbfindhost(pd.args.hostname)
if theip != "": perrorexit("? Hostname '{}' is already assigned to IP '{}'".format(pd.args.hostname, theip))
eopts = ""
if pd.args.nodhcp: eopts += "+nodhcp"
if pd.args.dhcponly: eopts += "+dhcponly"
if pd.args.hostsonly: eopts += "+hostsonly"
if pd.args.zoneonly: eopts += "+zoneonly"
if pd.args.cname: eopts += "+cname+zoneonly"
if pd.args.nodomain: eopts += "+nodomain"
if pd.args.note == None: pd.args.note = ""
if pd.args.dhcphostopt == None: pd.args.dhcphostopt = ""
if pd.args.mac == None: pd.args.mac = ""
pd.dbaddhost(eipaddr=keyname, emac=pd.args.mac, edhcphostopt=pd.args.dhcphostopt,\
ehost=pd.args.hostname, eopts=eopts, enote=pd.args.note)
pd.dbwrite()
return True
def cksubnetdetails(pd):
if pd.snetdb is None: return
snetissues = {}
for sn in pd.snetdb['subnet']:
for item in [ 'myip', 'dhcpsubnet', 'dns', 'gateway', 'timeserver' ]:
if pd.snetdb['subnet'][sn][item] == "":
if not sn in snetissues: snetissues[sn] = []
snetissues[sn].append(item)
if len(snetissues) == 0: return
print("? Errors found in subnet definition(s)")
for sn in snetissues:
eitems = ""
for s in snetissues[sn]:
eitems = "{} {}".format(eitems, s)
print(" Subnet: {} missing: {}".format(sn, eitems))
perrorexit("? Correct subnet definition(s) before doing build")
def buildoutputhost(pd, keyname, listfmt):
# Called by dbdoallnodes for each host
# Calls the DNS and DHCP host output for each one
if keyname in pd.db['hosts']:
for hn in pd.db['hosts'][keyname]['hostname']:
pd.dns.emithost(keyname, hn)
pd.dhcp.emithost(keyname, hn)
else:
if keyname in pd.db['cname']:
for hname in sorted(pd.db['cname'][keyname]['hostname']):
pd.dns.emitcname(keyname, hname)
def cmd_build(pd):
# cknownos(pd)
if pd.db['cfg']['subnet'] == "":
perrorexit("? Subnet not set; use {} --config --subnet nn.nn.nn".format(pd.ndmcmd))
# Enables build command settings to override config
if pd.args.dnsinclude != None:
svdnsincl = pd.db['cfg']['dnsinclude']
pd.db['cfg']['dnsinclude'] = pd.args.dnsinclude
if pd.args.dhcpinclude != None:
svdhcpincl = pd.db['cfg']['dhcpinclude']
pd.db['cfg']['dhcpinclude'] = pd.args.dhcpinclude
cksubnetdetails(pd)
pd.dns.prebuild()
pd.dhcp.prebuild()
pd.dns.startbuild()
pd.dhcp.startbuild()
pd.dbdoallnodes(buildoutputhost, "")
pd.dns.endbuild()
pd.dhcp.endbuild()
# Reset dnsinclude and dhcpinclude if modified, in case the DB gets written. Ugly
if pd.args.dnsinclude != None: pd.db['cfg']['dnsinclude'] = svdnsincl
if pd.args.dhcpinclude != None: pd.db['cfg']['dhcpinclude'] = svdhcpincl
pd.dbwrite() # Check and Write DB in case DNSUpdateKey was generated
print("% Build completed in tmp directory '{}'".format(pd.tmp))
return True
def doconfigitem(pd, dest, argval, lowerit=False):
# If arg was specified on command line, set it into the configuration and set dbmodified
# if lowerit=True set the value lower-cased
if argval != None:
pd.db['cfg'][dest] = argval if lowerit == False else argval.lower()
pd.dbmodified = True
pd.cmdmodified = True
def cmd_config(pd):
if pd.args.domain != None:
pdom = pd.db['cfg']['domain']
doconfigitem(pd, 'domain', pd.args.domain, lowerit=True)
for cf in ['hostfqdn', 'dnsfqdn', 'mxfqdn']:
fqdn = pd.db['cfg'][cf]
if fqdn.endswith(".{}".format(pdom)):
pd.db['cfg'][cf] = "{}.{}".format(fqdn.split(".{}".format(pdom))[0], pd.db['cfg']['domain'])
if pd.args.subnet != None:
sn = pd.args.subnet.split('/')
if len(sn) > 1:
if sn[1] != "":
if sn[1] != "24":
perrorexit("? Only /24 networks currently supported")
pd.db['cfg']['subnetmask'] = sn[1] # Needs more work
doconfigitem(pd, 'subnet', sn[0])
if pd.args.bindoptions != None:
if pd.args.bindoptions != "" and not os.path.isfile(pd.args.bindoptions):
perrorexit("? --bindoptions file '{}' not found".format(pd.args.bindoptions))
doconfigitem(pd, 'bindoptions', pd.args.bindoptions)
doconfigitem(pd, 'blockdomains', pd.args.blockdomains, lowerit=True)
doconfigitem(pd, 'dhcpglobalopt', pd.args.dhcpglobalopt)
doconfigitem(pd, 'dhcpglobalinclude', pd.args.dhcpglobalinclude)
doconfigitem(pd, 'dhcppoolinclude', pd.args.dhcppoolinclude)
doconfigitem(pd, 'dhcpinclude', pd.args.dhcpinclude)
doconfigitem(pd, 'dhcplease', pd.args.dhcplease)
doconfigitem(pd, 'dhcpsubnet', pd.args.dhcpsubnet)
doconfigitem(pd, 'dnsfqdn', pd.args.dnsfqdn, lowerit=True)
doconfigitem(pd, 'dnsinclude', pd.args.dnsinclude)
doconfigitem(pd, 'dnsip', pd.args.dnsip)
doconfigitem(pd, 'dnslistenport', pd.args.dnslistenport)
doconfigitem(pd, 'externaldns', pd.args.externaldns)
doconfigitem(pd, 'gateway', pd.args.gateway)
doconfigitem(pd, 'hostfqdn', pd.args.hostfqdn, lowerit=True)
doconfigitem(pd, 'hostname', pd.args.hostname, lowerit=True)
doconfigitem(pd, 'internals', pd.args.internals)
doconfigitem(pd, 'mxfqdn', pd.args.mxfqdn, lowerit=True)
doconfigitem(pd, 'myip', pd.args.myip)
doconfigitem(pd, 'netdev', pd.args.netdev)
doconfigitem(pd, 'os', pd.args.os)
doconfigitem(pd, 'timeserver', pd.args.timeserver)
if pd.args.dns != None:
if not pd.args.dns in pd.knowndns:
perrorexit("? Unknown DNS server '{}'".format(pd.args.dns))
doconfigitem(pd, 'dns', pd.args.dns, lowerit=True)
if pd.args.dhcp != None:
if not pd.args.dhcp in pd.knowndhcp:
perrorexit("? Unknown DHCP server '{}'".format(pd.args.dhcp))
doconfigitem(pd, 'dhcp', pd.args.dhcp, lowerit=True)
if pd.args.dns != None or pd.args.dhcp != None:
if pd.db['cfg']['dns'] == "dnsmasq" or pd.db['cfg']['dhcp'] == "dnsmasq":
pd.db['cfg']['dns'] = "dnsmasq"
if pd.args.dhcp != "none":
pd.db['cfg']['dhcp'] = "dnsmasq"
print("% dnsmasq will be used for both DNS and DHCP")
if pd.args.hostfqdn == None and pd.db['cfg']['domain'] != None:
pd.db['cfg']['hostfqdn'] = "{}.{}".format(pd.db['cfg']['hostname'],pd.db['cfg']['domain'])
if pd.db['cfg']['dnsfqdn'] == "": pd.db['cfg']['dnsfqdn'] = pd.db['cfg']['hostfqdn']
if pd.db['cfg']['mxfqdn'] == "": pd.db['cfg']['mxfqdn'] = pd.db['cfg']['hostfqdn']
if pd.args.dhcphostopt != None:
els = pd.args.dhcphostopt.split("=")
if els[1] == "":
del(pd.db['cfg']['dhcphostopt'][els[0]])
else:
pd.db['cfg']['dhcphostopt'][els[0]] = els[1]
pd.dbmodified = pd.cmdmodified = True
# Normalize externaldns and dhcpsubnet strings
if pd.db['cfg']['externaldns'] != "": pd.db['cfg']['externaldns'] = listnormal(pd.db['cfg']['externaldns'], " ")
if pd.db['cfg']['dhcpsubnet'] != "": pd.db['cfg']['dhcpsubnet'] = listnormal(pd.db['cfg']['dhcpsubnet'], " ")
if not pd.cmdmodified and (pd.args.importnet == None): pd.args.list = True
if pd.args.importnet != None:
pd.dbimport()
elif pd.args.list:
for i in sorted(pd.db['cfg']):
if i != 'dhcphostopt': print("{:<15} {}".format(i, pd.db['cfg'][i]))
if len(pd.db['cfg']['dhcphostopt']) > 0:
print("DHCP per-host options")
for i in pd.db['cfg']['dhcphostopt']:
print(" {:<10} {}".format(i, pd.db['cfg']['dhcphostopt'][i]))
pd.dbwrite()
return True
def cmd_delete(pd):
keyname = pd.args.ip
if keyname is None: perrorexit("? No IP address specified")
if keyname in pd.db['hosts']:
if pd.args.hostname != None:
# Delete a single hostname from an IP address
if not pd.args.hostname in pd.db['hosts'][keyname]['hostname']: perrorexit("? Hostname {} not found on IP address {}".format(pd.args.hostname, keyname))
if len(pd.db['hosts'][keyname]['hostname']) > 1:
print("% Deleting host '{}' from IP address '{}'".format(pd.args.hostname, keyname))
del pd.db['hosts'][keyname]['hostname'][pd.args.hostname]
else:
print("% Deleting IP address '{}'".format(keyname))
del pd.db['hosts'][keyname]
else:
print("% Deleting IP address '{}'".format(keyname))
del pd.db['hosts'][keyname]
else:
if keyname in pd.db['cname']:
print("% Deleting CNAME '{}'".format(keyname))
del pd.db['cname'][keyname]
else:
perrorexit("? IP address '{}' is not in the database".format(keyname))
pd.dbwrite(force=True)
return True
def cmd_diff(pd):
cknownos(pd)
pd.dns.diff(dodiff)
pd.dhcp.diff(dodiff)
def cmd_install(pd):
if pd.myuid != 0: perrorexit("? You must be root to Install")
cknownos(pd)
dnsrunning = pd.dns.isrunning() == 0
dhcprunning = pd.dhcp.isrunning() == 0
fnm = pd.dns.preinstall()
fnm = "{} {}".format(fnm, pd.dhcp.preinstall()) # fnm=" " if all is well
if fnm != " ":
print("? Missing configuration files in '{}':".format(pd.tmp))
for fn in fnm.split(" "):
if fn != "": print(" {}".format(fn))
perrorexit("? Please 'sudo {} build' first".format(pd.ndmcmd))
pd.dns.stop()
pd.dhcp.stop()
if pd.args.reset:
# Full Reset: Delete dynamic dns jnl files and dhcp lease files
pd.dns.resetdyndb()
pd.dhcp.resetdyndb()
pd.dns.install()
pd.dhcp.install()
if dnsrunning: pd.dns.start()
if dhcprunning: pd.dhcp.start()
return True
def cmd_list(pd):
if pd.args.dump:
cfg = json.dumps(pd.db['cfg'])
print('CONFIG={{"cfg":{}}}'.format(cfg))
listfmt = "{},{},{},{},{},{}," if pd.args.dump else pd.listformat
pd.dbdoallnodes(printnode, listfmt)
return True
def cmd_modify(pd):
keyname = pd.args.ip
if keyname is None: perrorexit("? --ip must be provided")
if not keyname in pd.db['hosts']: perrorexit("? IP address {} is not in the database".format(keyname))
newname = pd.args.newhostname.lower() if pd.args.newhostname != None else ""
hname = pd.args.hostname.lower() if pd.args.hostname != None else ""
if newname != "":
hname = getmodifyhostname(pd, keyname, hname, "renaming a host")
theip = pd.dbfindhost(newname)
if theip != "": perrorexit("? Hostname '{}' is already assigned to IP '{}'".format(newname, theip))
# Create new hostname (and copy flags, MAC Address, and note), and then delete the old one
pd.db['hosts'][keyname]['hostname'][newname] = {'flags':pd.db['hosts'][keyname]['hostname'][hname]['flags'],\
'macaddr':pd.db['hosts'][keyname]['hostname'][hname]['macaddr'],\
'note':pd.db['hosts'][keyname]['hostname'][hname]['note']}
del(pd.db['hosts'][keyname]['hostname'][hname])
hname = newname
if pd.args.note != None:
hname = getmodifyhostname(pd, keyname, hname, "--note")
pd.db['hosts'][keyname]['hostname'][hname]['note'] = pd.args.note
if pd.args.mac != None:
hname = getmodifyhostname(pd, keyname, hname, "--mac")
pd.db['hosts'][keyname]['hostname'][hname]['macaddr'] = pd.args.mac.lower()
if pd.args.dhcphostopt != None: pd.db['hosts'][keyname]['dhcphostopt'] = pd.args.dhcphostopt
eopts = ""
if pd.args.nodhcp: eopts += "+nodhcp"
if pd.args.dhcponly: eopts += "+dhcponly"
if pd.args.hostsonly: eopts += "+hostsonly"
if pd.args.nodomain: eopts += "+nodomain"
if pd.args.zoneonly: eopts += "+zoneonly"
if eopts != "":
hname = getmodifyhostname(pd, keyname, hname, "modifying DHCP/DNS attributes")
pd.db['hosts'][keyname]['hostname'][hname]['flags'] = eopts
pd.dbwrite(force=True)
return True
def cmd_reip(pd):
keyname = pd.args.ip
if keyname is None: perrorexit("? No IP address specified")
if keyname in pd.db['hosts']:
newip = pd.args.newip
if newip in pd.db['hosts']: perrorexit("? New IP Address '{}' is already in the database".format(newip))
for hn in pd.db['hosts'][keyname]['hostname']:
pd.dbaddhost(eipaddr=newip, emac=pd.db['hosts'][keyname]['hostname'][hn]['macaddr'], edhcphostopt=pd.db['hosts'][keyname]['dhcphostopt'],\
ehost=hn, eopts=pd.db['hosts'][keyname]['hostname'][hn]['flags'], enote=pd.db['hosts'][keyname]['hostname'][hn]['note'])
del pd.db['hosts'][keyname]
else:
perrorexit("? IP address '{}' is not in the database".format(keyname))
pd.dbwrite(force=True)
return True
def cmd_show(pd):
ihost = pd.args.host.lower()
if ihost in pd.db['hosts']: # Print specific IP address if found
printnode(pd, ihost, pd.listformat)
else:
# See if a hostname, MAC address, or hostname abbreviation specified
for keyname in sorted(pd.db['hosts']):
if ihost in pd.db['hosts'][keyname]['hostname']:
printhost(pd, keyname, ihost, pd.listformat)
else:
for hname in sorted(pd.db['hosts'][keyname]['hostname']):
if ihost in pd.db['hosts'][keyname]['hostname'][hname]['macaddr']:
printhost(pd, keyname, hname, pd.listformat)
else:
if ihost in hname:
printhost(pd, keyname, hname, pd.listformat)
# Lastly look at cnames
for keyname in pd.db['cname']:
if ihost in keyname:
printnode(pd, keyname, pd.listformat)
return True
def cmd_addsubnet(pd):
if pd.snetdb is not None:
if pd.args.subnet in pd.snetdb['subnet']: perrorexit("? Subnet '{}' already defined".format(pd.args.subnet))
else:
pd.snetdb = {}
pd.snetdb['subnet'] = {}
pd.snetdb['dbversion'] = pd.snetdbversion
# dns, gateway, timeserver
dhcpsubnet = "" if pd.args.dhcpsubnet is None else listnormal(pd.args.dhcpsubnet, " ")
dns = "" if pd.args.dns is None else pd.args.dns
gateway = "" if pd.args.gateway is None else pd.args.gateway
myip = "" if pd.args.myip is None else pd.args.myip
mask = "" if pd.args.mask is None else pd.args.mask
name = "" if pd.args.name is None else pd.args.name
timeserver = "" if pd.args.timeserver is None else pd.args.timeserver
pd.snetdb['subnet'][pd.args.subnet] = { 'myip':myip, 'dhcpsubnet':dhcpsubnet, 'dns':dns, 'gateway':gateway, 'mask':mask, 'name':name, 'timeserver':timeserver }
pd.snetdbwrite()
def cmd_delsubnet(pd):
if pd.args.subnet in pd.snetdb['subnet']:
del(pd.snetdb['subnet'][pd.args.subnet])
print("% Deleted subnet '{}'".format(pd.args.subnet))
else:
perrorexit("? Subnet '{}' is not a secondary network".format(pd.args.subnet))
pd.snetdbwrite()
return
def cmd_modsubnet(pd):
if pd.args.subnet in pd.snetdb['subnet']:
if not pd.args.dhcpsubnet is None: pd.snetdb['subnet'][pd.args.subnet]['dhcpsubnet'] = listnormal(pd.args.dhcpsubnet, " ")
if not pd.args.myip is None: pd.snetdb['subnet'][pd.args.subnet]['myip'] = pd.args.myip
if not pd.args.dns is None: pd.snetdb['subnet'][pd.args.subnet]['dns'] = pd.args.dns
if not pd.args.gateway is None: pd.snetdb['subnet'][pd.args.subnet]['gateway'] = pd.args.gateway
if not pd.args.mask is None: pd.snetdb['subnet'][pd.args.subnet]['mask'] = pd.args.mask
if not pd.args.name is None: pd.snetdb['subnet'][pd.args.subnet]['name'] = pd.args.name
if not pd.args.timeserver is None: pd.snetdb['subnet'][pd.args.subnet]['timeserver'] = pd.args.timeserver
print("% Modified subnet '{}'".format(pd.args.subnet))
else:
perrorexit("? Subnet '{}' is not a secondary network".format(pd.args.subnet))
pd.snetdbwrite()
return
def cmd_showsubnet(pd):
if pd.snetdb != None and 'subnet' in pd.snetdb:
if not pd.args.subnet is None:
sn = pd.args.subnet
#print("{:11} {:16}{:16} {}".format("Subnet", "Subnet Name", "My Subnet IP", "Subnet DHCP Range"))
if sn in pd.snetdb['subnet']:
print("{:11} {:16}{:16}{:16}{:16}{:16}{}".format(sn, pd.snetdb['subnet'][sn]['name'],\
pd.snetdb['subnet'][sn]['myip'],\
pd.snetdb['subnet'][sn]['dns'],\
pd.snetdb['subnet'][sn]['gateway'],\
pd.snetdb['subnet'][sn]['timeserver'],\
pd.snetdb['subnet'][sn]['dhcpsubnet'])) #???, pd.snetdb['subnet'][sn]['mask']
else:
print("{:11} {:16}{:16}{:16}{:16}{:16} {}".format("Subnet", "Subnet Name", "My Subnet IP", "DNS Server", "Gateway", "Timeserver", "Subnet DHCP Range"))
for sn in sorted(pd.snetdb['subnet']):
print("{:11} {:16}{:16}{:16}{:16}{:16} {}".format(sn, pd.snetdb['subnet'][sn]['name'],\
pd.snetdb['subnet'][sn]['myip'],\
pd.snetdb['subnet'][sn]['dns'],\
pd.snetdb['subnet'][sn]['gateway'],\
pd.snetdb['subnet'][sn]['timeserver'],\
pd.snetdb['subnet'][sn]['dhcpsubnet'])) #???, pd.snetdb['subnet'][sn]['mask']
return
def remove_prefix(s, prefix):
return s[len(prefix):] if s.startswith(prefix) else s
def chghostsubnet(pd, oldsubnet, newsubnet):
delist = []
for ip in pd.db['hosts']:
if oldsubnet in ip:
# Flag this IP to replace
delist.append(ip)
for ip in delist:
# Copy new entry from old, then delete old one
nip = "{}{}".format(newsubnet, remove_prefix(ip, oldsubnet))
pd.db['hosts'][nip] = {}
pd.db['hosts'][nip]['dhcphostopt'] = pd.db['hosts'][ip]['dhcphostopt']
pd.db['hosts'][nip]['hostname'] = pd.db['hosts'][ip]['hostname']
del(pd.db['hosts'][ip])
print("% Renamed host '{}' to '{}'".format(ip, nip))
return
def cmd_resubnet(pd):
if pd.args.oldsubnet == pd.db['cfg']['subnet']:
chghostsubnet(pd, pd.args.oldsubnet, pd.args.newsubnet)
# Update configuration: dhcpsubnet, dnsip, gateway, internals, myip, subnet, timeserver, and dhcphostopts
for c in ['dhcpsubnet', 'dnsip', 'gateway', 'internals', 'myip', 'subnet', 'timeserver' ]:
if pd.args.oldsubnet in pd.db['cfg'][c]:
oldval = pd.db['cfg'][c]
pd.db['cfg'][c] = pd.db['cfg'][c].replace(pd.args.oldsubnet, pd.args.newsubnet)
print("% Updated ndm config '{}' from '{}' to '{}'".format(c, oldval, pd.db['cfg'][c]))
if len(pd.db['cfg']['dhcphostopt']) > 0:
for i in pd.db['cfg']['dhcphostopt']:
if pd.args.oldsubnet in pd.db['cfg']['dhcphostopt'][i]:
oldval = pd.db['cfg']['dhcphostopt'][i]
pd.db['cfg']['dhcphostopt'][i] = pd.db['cfg']['dhcphostopt'][i].replace(pd.args.oldsubnet, pd.args.newsubnet)
print("% Updated dhcphostopt '{}' from '{}' to '{}'".format(i, oldval, pd.db['cfg']['dhcphostopt'][i]))
pd.dbwrite(force=True)
return
else:
if pd.snetdb is not None:
if pd.args.oldsubnet in pd.snetdb['subnet']:
chghostsubnet(pd, pd.args.oldsubnet, pd.args.newsubnet)
pd.snetdb['subnet'][pd.args.newsubnet] = pd.snetdb['subnet'][pd.args.oldsubnet]
pd.snetdb['subnet'][pd.args.newsubnet]['myip'] = pd.snetdb['subnet'][pd.args.newsubnet]['myip'].replace(pd.args.oldsubnet, pd.args.newsubnet)
pd.snetdb['subnet'][pd.args.newsubnet]['dhcpsubnet'] = pd.snetdb['subnet'][pd.args.newsubnet]['dhcpsubnet'].replace(pd.args.oldsubnet, pd.args.newsubnet)
del(pd.snetdb['subnet'][pd.args.oldsubnet])
print("% Renamed secondary subnet '{}' to '{}'".format(pd.args.oldsubnet, pd.args.newsubnet))
if len(pd.db['cfg']['dhcphostopt']) > 0:
for i in pd.db['cfg']['dhcphostopt']:
if pd.args.oldsubnet in pd.db['cfg']['dhcphostopt'][i]:
oldval = pd.db['cfg']['dhcphostopt'][i]
pd.db['cfg']['dhcphostopt'][i] = pd.db['cfg']['dhcphostopt'][i].replace(pd.args.oldsubnet, pd.args.newsubnet)
print("% Updated dhcphostopt '{}' from '{}' to '{}'".format(i, oldval, pd.db['cfg']['dhcphostopt'][i]))
pd.dbwrite(force=True)
pd.snetdbwrite()
return
perrorexit("? Subnet '{}' not found".format(pd.args.oldsubnet))
def cmd_help(pd):
xdm = pd.ndmcmd
xnm = os.path.basename(xdm)
print(f"\n{xnm} builds the DNS and DHCP configuration files from a database\n\
that it maintains in a JSON-formatted configuration file.\n\
\n{xnm} can build configuration files for these services:\n\
DHCP: dnsmasq isc-dhcp-server none\n\
DNS: dnsmasq bind\n\
\n\
Use '{xdm} config --dns whichdns --dhcp whichdhcp' to configure DNS and DHCP services\n\
\n\
Use the '{xdm} build' command to generate the service configuration files\n\
Use the '{xdm} install' command to install the service configuration files\n\
into the system and restart the services\n")
pd.parser.print_help(None)
print(f"\nUse '{xdm} command --help' for help on a specific command\n")
def cmd_version(pd):
xnm = os.path.basename(pd.ndmcmd)
xver = pd.version
print(f"{xnm} {xver}")
#
# Initialize and parse command line
#
pd = pdat()
pd.myuid = os.getuid()
pd.ndmcmd = sys.argv[0]
pd.parser = argparse.ArgumentParser(
description = "Manage, build and install DHCP/DNS config files",
epilog="",
prog=os.path.basename(pd.ndmcmd),
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
pd.parser.set_defaults(func=cmd_help, ckcfg=False, create=False, db=None, loadsrv=False, mktmpdir=False, tmp=None, snetmust=False, usedb=True)
addmodify_parent_parser = argparse.ArgumentParser(add_help=False)
addmodify_parent_parser.add_argument("--dhcponly", help="Only put this host in dhcpd.conf (no hosts or DNS config entries)", action='store_true')
addmodify_parent_parser.add_argument("--hostsonly", help="Only put this host in hosts file (no dhcpd.conf or DNS zone entries)", action='store_true')
addmodify_parent_parser.add_argument("--nodhcp", help="Don't add host to dhcpd.conf", action='store_true')
addmodify_parent_parser.add_argument("--nodomain", help="Don't add the domain name to this entry; It's already fully qualified", action='store_true')
addmodify_parent_parser.add_argument("--zoneonly", help="Only put this host in DNS config files (no dhcpd.conf or hosts file)", action='store_true')
subparsers = pd.parser.add_subparsers(help="sub-command help")
subparser_add = subparsers.add_parser("add", help="Add a new host to the database", parents=[addmodify_parent_parser])
subparser_add.add_argument("ip", help="New host IP address")
subparser_add.add_argument("--cname", help="Put this entry in DNS zone files as a CNAME", action='store_true')
subparser_add.add_argument("--db", help="Specify alternate config file")
subparser_add.add_argument("--dhcphostopt", help="Name of dhcphostopt to add to this host's dhcp entry")
subparser_add.add_argument("--hostname", help="New host name")
subparser_add.add_argument("--mac", help="MAC address for new hostname")
subparser_add.add_argument("--note", help="Note text for new hostname")
subparser_add.set_defaults(func=cmd_add)
subparser_build = subparsers.add_parser("build", help="Build new host databases in tmp staging area")
subparser_build.add_argument("--db", help="Specify alternate config file")
subparser_build.add_argument("--dhcpinclude", help="Specify an additional DHCP include file")
subparser_build.add_argument("--dnsinclude", help="Specify an additional DNS include file")
subparser_build.add_argument("--tmp", help="Directory for the the generated files")
subparser_build.set_defaults(func=cmd_build, ckcfg=True, loadsrv=True, mktmpdir=True)
subparser_config = subparsers.add_parser("config", help="Manage configuration database")
subparser_config.add_argument("--bindoptions", help="Add additional statements to bind 'options' section")
subparser_config.add_argument("--blockdomains", help="Comma-separated list of domains to block")
subparser_config.add_argument("--create", help="Create the Network DB file", action='store_true')
subparser_config.add_argument("--db", help="Specify alternate config file")
subparser_config.add_argument("--dhcpglobalopt", help="Set the global DHCP option string")
subparser_config.add_argument("--dhcpglobalinclude", help="Specify an additional DHCP include file for the global def section")
subparser_config.add_argument("--dhcpinclude", help="Specify an additional DHCP include file")
subparser_config.add_argument("--dhcppoolinclude", help="Specify an additional DHCP include file for the pool def section")
subparser_config.add_argument("--dhcphostopt", help="Set a DHCP per-host option string")
subparser_config.add_argument("--dhcplease", help="DHCP lease length in seconds [86400]")
subparser_config.add_argument("--dhcpsubnet", "--dhcprange", help="Low and High IP addresses for DHCP server pool")
subparser_config.add_argument("--dhcp", help="Specify DHCP server", choices=["dnsmasq", "isc-dhcp-server", "none"])
subparser_config.add_argument("--dns", help="Specify DNS server", choices=["bind", "dnsmasq"])
subparser_config.add_argument("--dnsfqdn", help="DNS server FQDN")
subparser_config.add_argument("--dnsinclude", help="Specify an additional DNS include file")
subparser_config.add_argument("--dnsip", help="DNS server IP address")
subparser_config.add_argument("--dnslistenport", help="Port for DNS Listen")
subparser_config.add_argument("--domain", help="Domain name")
subparser_config.add_argument("--externaldns", help="External DNS Servers")
subparser_config.add_argument("--gateway", help="Network gateway")
subparser_config.add_argument("--hostfqdn", help="My host FQDN")
subparser_config.add_argument("--hostname", help="My host nameN")
subparser_config.add_argument("--internals", help="Additional bind internals subnets from which to allow queries")
config_addil = subparser_config.add_mutually_exclusive_group()
config_addil.add_argument("--importnet", help="Import a file of hosts")
config_addil.add_argument("--list", help="Show the current configuration", action='store_true')
subparser_config.add_argument("--mxfqdn", help="Mail server FQDN")
subparser_config.add_argument("--myip", help="Specify my IP address if I need help")
subparser_config.add_argument("--netdev", help="Specify the network device [eth0]")
subparser_config.add_argument("--os", help="Specify OS type if /etc/os-release fails")
subparser_config.add_argument("--subnet", help="Network subnet")
subparser_config.add_argument("--timeserver", help="Time server IP address")
subparser_config.add_argument("--verbose", help="List each entry imported with --importnet",action='store_true')
subparser_config.set_defaults(func=cmd_config)
subparser_delete = subparsers.add_parser("delete", help="Delete a host from the database")
subparser_delete.add_argument("ip", help="IP address to delete")
subparser_delete.add_argument("--db", help="Specify alternate config file")
subparser_delete.add_argument("--hostname", help="Specify a hostname to delete from the IP address")
subparser_delete.set_defaults(func=cmd_delete)
subparser_diff = subparsers.add_parser("diff", help="Diff current and new config files")
subparser_diff.add_argument("--db", help="Specify alternate config file")
subparser_diff.add_argument("--tmp", help="Directory for the the next version files")
subparser_diff.set_defaults(func=cmd_diff, loadsrv=True, mktmpdir=True)
subparser_install = subparsers.add_parser("install", help="Install new config files into the system")
subparser_install.add_argument("--reset", help="Perform complete build/install; erase dhcp leases and reset dynamic dns", action='store_true')
subparser_install.add_argument("--db", help="Specify alternate config file")
subparser_install.add_argument("--tmp", help="Where to get the previously-generated files")
subparser_install.set_defaults(func=cmd_install, ckcfg=True, loadsrv=True, mktmpdir=True)
subparser_list = subparsers.add_parser("list", help="List the complete database")
subparser_list.add_argument("--db", help="Specify alternate config file name")
subparser_list.add_argument("--dump", help="Print list in import format", action='store_true')
subparser_list.set_defaults(func=cmd_list)
subparser_modify = subparsers.add_parser("modify", help="Modify host attributes in the database", parents=[addmodify_parent_parser])
subparser_modify.add_argument("ip", help="IP address to modify")
subparser_modify.add_argument("--db", help="Specify alternate config file name")
subparser_modify.add_argument("--dhcphostopt", help="Name of dhcphostopt to add to the host's DHCP entry")
subparser_modify.add_argument("--hostname", help="New hostname for IP address")
subparser_modify.add_argument("--mac", help="New MAC address for IP address")
subparser_modify.add_argument("--newhostname", help="New hostname")
subparser_modify.add_argument("--note", help="New note field for the host")
subparser_modify.set_defaults(func=cmd_modify)
subparser_reip = subparsers.add_parser("reip", help="Change a host's IP address")
subparser_reip.add_argument("ip", help="IP address to change")
subparser_reip.add_argument("--newip", help="New IP address")
subparser_reip.add_argument("--db", help="Specify alternate config file")
subparser_reip.set_defaults(func=cmd_reip)
subparser_resubnet = subparsers.add_parser("resubnet", help="Change the subnet's IP Address")
subparser_resubnet.add_argument("oldsubnet", help="Subnet to change")
subparser_resubnet.add_argument("newsubnet", help="New subnet")
subparser_resubnet.set_defaults(func=cmd_resubnet)
subparser_show = subparsers.add_parser("show", help="Show a single host")
subparser_show.add_argument("host", help="Full or partial hostname/IP/MAC host to show")
subparser_show.add_argument("--db", help="Specify alternate config file name")
subparser_show.set_defaults(func=cmd_show)
# addsubnet, delsubnet, showsubnet, modsubnet
subparser_addsubnet = subparsers.add_parser("addsubnet", help="Add secondary subnet")
subparser_addsubnet.add_argument("subnet", help="Secondary subnet to add [su.bn.et]")
subparser_addsubnet.add_argument("--dhcpsubnet", help="Low and high IP addresses for DHCP server range")
subparser_addsubnet.add_argument("--dns", help="DNS Server IP Address")
subparser_addsubnet.add_argument("--gateway", help="Gateway IP Address")
subparser_addsubnet.add_argument("--myip", help="Local IP address in subnet")
subparser_addsubnet.add_argument("--mask", help="Secondary subnet mask")
subparser_addsubnet.add_argument("--name", help="Secondary subnet name")
subparser_addsubnet.add_argument("--timeserver", help="TimeServer IP Address")
subparser_addsubnet.set_defaults(func=cmd_addsubnet)