chiark / gitweb /
make-secnet-sites: prefix names when writing sites file
[secnet.git] / make-secnet-sites
1 #! /usr/bin/env python3
2 #
3 # This file is part of secnet.
4 # See README for full list of copyright holders.
5 #
6 # secnet is free software; you can redistribute it and/or modify it
7 # under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
10
11 # secnet is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15
16 # You should have received a copy of the GNU General Public License
17 # version 3 along with secnet; if not, see
18 # https://www.gnu.org/licenses/gpl.html.
19
20 """VPN sites file manipulation.
21
22 This program enables VPN site descriptions to be submitted for
23 inclusion in a central database, and allows the resulting database to
24 be turned into a secnet configuration file.
25
26 A database file can be turned into a secnet configuration file simply:
27 make-secnet-sites.py [infile [outfile]]
28
29 It would be wise to run secnet with the "--just-check-config" option
30 before installing the output on a live system.
31
32 The program expects to be invoked via userv to manage the database; it
33 relies on the USERV_USER and USERV_GROUP environment variables. The
34 command line arguments for this invocation are:
35
36 make-secnet-sites.py -u header-filename groupfiles-directory output-file \
37   group
38
39 All but the last argument are expected to be set by userv; the 'group'
40 argument is provided by the user. A suitable userv configuration file
41 fragment is:
42
43 reset
44 no-disconnect-hup
45 no-suppress-args
46 cd ~/secnet/sites-test/
47 execute ~/secnet/make-secnet-sites.py -u vpnheader groupfiles sites
48
49 This program is part of secnet.
50
51 """
52
53 from __future__ import print_function
54 from __future__ import unicode_literals
55 from builtins import int
56
57 import string
58 import time
59 import sys
60 import os
61 import getopt
62 import re
63 import argparse
64 import math
65
66 import ipaddress
67
68 # entry 0 is "near the executable", or maybe from PYTHONPATH=.,
69 # which we don't want to preempt
70 sys.path.insert(1,"/usr/local/share/secnet")
71 sys.path.insert(1,"/usr/share/secnet")
72 import ipaddrset
73
74 from argparseactionnoyes import ActionNoYes
75
76 VERSION="0.1.18"
77
78 from sys import version_info
79 if version_info.major == 2:  # for python2
80     import codecs
81     sys.stdin = codecs.getreader('utf-8')(sys.stdin)
82     sys.stdout = codecs.getwriter('utf-8')(sys.stdout)
83     import io
84     open=lambda f,m='r': io.open(f,m,encoding='utf-8')
85
86 max={'rsa_bits':8200,'name':33,'dh_bits':8200}
87
88 class Tainted:
89         def __init__(self,s,tline=None,tfile=None):
90                 self._s=s
91                 self._ok=None
92                 self._line=line if tline is None else tline
93                 self._file=file if tfile is None else tfile
94         def __eq__(self,e):
95                 return self._s==e
96         def __ne__(self,e):
97                 # for Python2
98                 return not self.__eq__(e)
99         def __str__(self):
100                 raise RuntimeError('direct use of Tainted value')
101         def __repr__(self):
102                 return 'Tainted(%s)' % repr(self._s)
103
104         def _bad(self,what,why):
105                 assert(self._ok is not True)
106                 self._ok=False
107                 complain('bad parameter: %s: %s' % (what, why))
108                 return self
109
110         def _max_ok(self,what,maxlen):
111                 if len(self._s) > maxlen:
112                         self._bad(what,'too long (max %d)' % maxlen)
113                 return self
114
115         def _re_ok(self,bad,what,maxlen=None):
116                 if maxlen is None: maxlen=max[what]
117                 self._max_ok(what,maxlen)
118                 if self._ok is False: return self
119                 if bad.search(self._s): return self._bad(what,'bad syntax')
120                 return self
121
122         def _rtnval(self, is_ok, ifgood, ifbad=''):
123                 if is_ok:
124                         assert(self._ok is not False)
125                         self._ok=True
126                         return ifgood
127                 else:
128                         assert(self._ok is not True)
129                         self._ok=False
130                         return ifbad
131
132         def _rtn(self, is_ok, ifbad=''):
133                 return self._rtnval(is_ok, self._s, ifbad)
134
135         def raw(self):
136                 return self._s
137         def raw_mark_ok(self):
138                 # caller promises to throw if syntax was dangeorus
139                 return self._rtn(True)
140
141         def output(self):
142                 if self._ok is False: return ''
143                 if self._ok is True: return self._s
144                 print('%s:%d: unchecked/unknown additional data "%s"' %
145                       (self._file,self._line,self._s),
146                       file=sys.stderr)
147                 sys.exit(1)
148
149         bad_name=re.compile(r'^[^a-zA-Z]|[^-_0-9a-zA-Z]')
150         # secnet accepts _ at start of names, but we reserve that
151         bad_name_counter=0
152         def name(self):
153                 ok=self._re_ok(Tainted.bad_name,'name')
154                 return self._rtn(ok,
155                                  '_line%d_%s' % (self._line, id(self)))
156
157         def keyword(self):
158                 ok=self._s in keywords or self._s in levels
159                 if not ok:
160                         complain('unknown keyword %s' % self._s)
161                 return self._rtn(ok)
162
163         bad_hex=re.compile(r'[^0-9a-fA-F]')
164         def bignum_16(self,kind,what):
165                 maxlen=(max[kind+'_bits']+3)/4
166                 ok=self._re_ok(Tainted.bad_hex,what,maxlen)
167                 return self._rtn(ok)
168
169         bad_num=re.compile(r'[^0-9]')
170         def bignum_10(self,kind,what):
171                 maxlen=math.ceil(max[kind+'_bits'] / math.log10(2))
172                 ok=self._re_ok(Tainted.bad_num,what,maxlen)
173                 return self._rtn(ok)
174
175         def number(self,minn,maxx,what='number'):
176                 # not for bignums
177                 ok=self._re_ok(Tainted.bad_num,what,10)
178                 if ok:
179                         v=int(self._s)
180                         if v<minn or v>maxx:
181                                 ok=self._bad(what,'out of range %d..%d'
182                                              % (minn,maxx))
183                 return self._rtnval(ok,v,minn)
184
185         bad_host=re.compile(r'[^-\][_.:0-9a-zA-Z]')
186         # We permit _ so we can refer to special non-host domains
187         # which have A and AAAA RRs.  This is a crude check and we may
188         # still produce config files with syntactically invalid
189         # domains or addresses, but that is OK.
190         def host(self):
191                 ok=self._re_ok(Tainted.bad_host,'host/address',255)
192                 return self._rtn(ok)
193
194         bad_email=re.compile(r'[^-._0-9a-z@!$%^&*=+~/]')
195         # ^ This does not accept all valid email addresses.  That's
196         # not really possible with this input syntax.  It accepts
197         # all ones that don't require quoting anywhere in email
198         # protocols (and also accepts some invalid ones).
199         def email(self):
200                 ok=self._re_ok(Tainted.bad_email,'email address',1023)
201                 return self._rtn(ok)
202
203         bad_groupname=re.compile(r'^[^_A-Za-z]|[^-+_0-9A-Za-z]')
204         def groupname(self):
205                 ok=self._re_ok(Tainted.bad_groupname,'group name',64)
206                 return self._rtn(ok)
207
208 def parse_args():
209         global service
210         global inputfile
211         global header
212         global groupfiledir
213         global sitesfile
214         global group
215         global user
216         global of
217         global key_prefix
218
219         ap = argparse.ArgumentParser(description='process secnet sites files')
220         ap.add_argument('--userv', '-u', action='store_true',
221                         help='userv service fragment update mode')
222         ap.add_argument('--conf-key-prefix', action=ActionNoYes,
223                         default=True,
224                  help='prefix conf file key names derived from sites data')
225         ap.add_argument('--prefix', '-P', nargs=1,
226                         help='set prefix')
227         ap.add_argument('arg',nargs=argparse.REMAINDER)
228         av = ap.parse_args()
229         #print(repr(av), file=sys.stderr)
230         service = 1 if av.userv else 0
231         key_prefix = av.conf_key_prefix
232         if service:
233                 if len(av.arg)!=4:
234                         print("Wrong number of arguments")
235                         sys.exit(1)
236                 (header, groupfiledir, sitesfile, group) = av.arg
237                 group = Tainted(group,0,'command line')
238                 # untrusted argument from caller
239                 if "USERV_USER" not in os.environ:
240                         print("Environment variable USERV_USER not found")
241                         sys.exit(1)
242                 user=os.environ["USERV_USER"]
243                 # Check that group is in USERV_GROUP
244                 if "USERV_GROUP" not in os.environ:
245                         print("Environment variable USERV_GROUP not found")
246                         sys.exit(1)
247                 ugs=os.environ["USERV_GROUP"]
248                 ok=0
249                 for i in ugs.split():
250                         if group==i: ok=1
251                 if not ok:
252                         print("caller not in group %s"%group)
253                         sys.exit(1)
254         else:
255                 if len(av.arg)>3:
256                         print("Too many arguments")
257                         sys.exit(1)
258                 (inputfile, outputfile) = (av.arg + [None]*2)[0:2]
259                 if outputfile is None: of=sys.stdout
260                 else: of=open(outputfile,'w')
261
262 parse_args()
263
264 # Classes describing possible datatypes in the configuration file
265
266 class basetype:
267         "Common protocol for configuration types."
268         def add(self,obj,w):
269                 complain("%s %s already has property %s defined"%
270                         (obj.type,obj.name,w[0].raw()))
271
272 class conflist:
273         "A list of some kind of configuration type."
274         def __init__(self,subtype,w):
275                 self.subtype=subtype
276                 self.list=[subtype(w)]
277         def add(self,obj,w):
278                 self.list.append(self.subtype(w))
279         def __str__(self):
280                 return ', '.join(map(str, self.list))
281 def listof(subtype):
282         return lambda w: conflist(subtype, w)
283
284 class single_ipaddr (basetype):
285         "An IP address"
286         def __init__(self,w):
287                 self.addr=ipaddress.ip_address(w[1].raw_mark_ok())
288         def __str__(self):
289                 return '"%s"'%self.addr
290
291 class networks (basetype):
292         "A set of IP addresses specified as a list of networks"
293         def __init__(self,w):
294                 self.set=ipaddrset.IPAddressSet()
295                 for i in w[1:]:
296                         x=ipaddress.ip_network(i.raw_mark_ok(),strict=True)
297                         self.set.append([x])
298         def __str__(self):
299                 return ",".join(map((lambda n: '"%s"'%n), self.set.networks()))
300
301 class dhgroup (basetype):
302         "A Diffie-Hellman group"
303         def __init__(self,w):
304                 self.mod=w[1].bignum_16('dh','dh mod')
305                 self.gen=w[2].bignum_16('dh','dh gen')
306         def __str__(self):
307                 return 'diffie-hellman("%s","%s")'%(self.mod,self.gen)
308
309 class hash (basetype):
310         "A choice of hash function"
311         def __init__(self,w):
312                 hname=w[1]
313                 self.ht=hname.raw()
314                 if (self.ht!='md5' and self.ht!='sha1'):
315                         complain("unknown hash type %s"%(self.ht))
316                         self.ht=None
317                 else:
318                         hname.raw_mark_ok()
319         def __str__(self):
320                 return '%s'%(self.ht)
321
322 class email (basetype):
323         "An email address"
324         def __init__(self,w):
325                 self.addr=w[1].email()
326         def __str__(self):
327                 return '<%s>'%(self.addr)
328
329 class boolean (basetype):
330         "A boolean"
331         def __init__(self,w):
332                 v=w[1]
333                 if re.match('[TtYy1]',v.raw()):
334                         self.b=True
335                         v.raw_mark_ok()
336                 elif re.match('[FfNn0]',v.raw()):
337                         self.b=False
338                         v.raw_mark_ok()
339                 else:
340                         complain("invalid boolean value");
341         def __str__(self):
342                 return ['False','True'][self.b]
343
344 class num (basetype):
345         "A decimal number"
346         def __init__(self,w):
347                 self.n=w[1].number(0,0x7fffffff)
348         def __str__(self):
349                 return '%d'%(self.n)
350
351 class address (basetype):
352         "A DNS name and UDP port number"
353         def __init__(self,w):
354                 self.adr=w[1].host()
355                 self.port=w[2].number(1,65536,'port')
356         def __str__(self):
357                 return '"%s"; port %d'%(self.adr,self.port)
358
359 class rsakey (basetype):
360         "An RSA public key"
361         def __init__(self,w):
362                 self.l=w[1].number(0,max['rsa_bits'],'rsa len')
363                 self.e=w[2].bignum_10('rsa','rsa e')
364                 self.n=w[3].bignum_10('rsa','rsa n')
365                 if len(w) >= 5: w[4].email()
366         def __str__(self):
367                 return 'rsa-public("%s","%s")'%(self.e,self.n)
368
369 # Possible properties of configuration nodes
370 keywords={
371  'contact':(email,"Contact address"),
372  'dh':(dhgroup,"Diffie-Hellman group"),
373  'hash':(hash,"Hash function"),
374  'key-lifetime':(num,"Maximum key lifetime (ms)"),
375  'setup-timeout':(num,"Key setup timeout (ms)"),
376  'setup-retries':(num,"Maximum key setup packet retries"),
377  'wait-time':(num,"Time to wait after unsuccessful key setup (ms)"),
378  'renegotiate-time':(num,"Time after key setup to begin renegotiation (ms)"),
379  'restrict-nets':(networks,"Allowable networks"),
380  'networks':(networks,"Claimed networks"),
381  'pubkey':(rsakey,"RSA public site key"),
382  'peer':(single_ipaddr,"Tunnel peer IP address"),
383  'address':(address,"External contact address and port"),
384  'mobile':(boolean,"Site is mobile"),
385 }
386
387 def sp(name,value):
388         "Simply output a property - the default case"
389         return "%s %s;\n"%(name,value)
390
391 # All levels support these properties
392 global_properties={
393         'contact':(lambda name,value:"# Contact email address: %s\n"%(value)),
394         'dh':sp,
395         'hash':sp,
396         'key-lifetime':sp,
397         'setup-timeout':sp,
398         'setup-retries':sp,
399         'wait-time':sp,
400         'renegotiate-time':sp,
401         'restrict-nets':(lambda name,value:"# restrict-nets %s\n"%value),
402 }
403
404 class level:
405         "A level in the configuration hierarchy"
406         depth=0
407         leaf=0
408         allow_properties={}
409         require_properties={}
410         def __init__(self,w):
411                 self.type=w[0].keyword()
412                 self.name=w[1].name()
413                 self.properties={}
414                 self.children={}
415         def indent(self,w,t):
416                 w.write("                 "[:t])
417         def prop_out(self,n):
418                 return self.allow_properties[n](n,str(self.properties[n]))
419         def output_props(self,w,ind):
420                 for i in self.properties.keys():
421                         if self.allow_properties[i]:
422                                 self.indent(w,ind)
423                                 w.write("%s"%self.prop_out(i))
424         def kname(self):
425                 return ((self.type[0].upper() if key_prefix else '')
426                         + self.name)
427         def output_data(self,w,path):
428                 ind = 2*len(path)
429                 self.indent(w,ind)
430                 w.write("%s {\n"%(self.kname()))
431                 self.output_props(w,ind+2)
432                 if self.depth==1: w.write("\n");
433                 for c in self.children.values():
434                         c.output_data(w,path+(c,))
435                 self.indent(w,ind)
436                 w.write("};\n")
437
438 class vpnlevel(level):
439         "VPN level in the configuration hierarchy"
440         depth=1
441         leaf=0
442         type="vpn"
443         allow_properties=global_properties.copy()
444         require_properties={
445          'contact':"VPN admin contact address"
446         }
447         def __init__(self,w):
448                 level.__init__(self,w)
449         def output_vpnflat(self,w,path):
450                 "Output flattened list of site names for this VPN"
451                 ind=2*(len(path)+1)
452                 self.indent(w,ind)
453                 w.write("%s {\n"%(self.kname()))
454                 for i in self.children.keys():
455                         self.children[i].output_vpnflat(w,path+(self,))
456                 w.write("\n")
457                 self.indent(w,ind+2)
458                 w.write("all-sites %s;\n"%
459                         ','.join(map(lambda i: i.kname(),
460                                      self.children.values())))
461                 self.indent(w,ind)
462                 w.write("};\n")
463
464 class locationlevel(level):
465         "Location level in the configuration hierarchy"
466         depth=2
467         leaf=0
468         type="location"
469         allow_properties=global_properties.copy()
470         require_properties={
471          'contact':"Location admin contact address",
472         }
473         def __init__(self,w):
474                 level.__init__(self,w)
475                 self.group=w[2].groupname()
476         def output_vpnflat(self,w,path):
477                 ind=2*(len(path)+1)
478                 self.indent(w,ind)
479                 # The "path=path,self=self" abomination below exists because
480                 # Python didn't support nested_scopes until version 2.1
481                 #
482                 #"/"+self.name+"/"+i
483                 w.write("%s %s;\n"%(self.kname(),','.join(
484                         map(lambda x,path=path,self=self:
485                             '/'.join([prefix+"vpn-data"] + list(map(
486                                     lambda i: i.kname(),
487                                     path+(self,x)))),
488                             self.children.values()))))
489
490 class sitelevel(level):
491         "Site level (i.e. a leafnode) in the configuration hierarchy"
492         depth=3
493         leaf=1
494         type="site"
495         allow_properties=global_properties.copy()
496         allow_properties.update({
497          'address':sp,
498          'networks':None,
499          'peer':None,
500          'pubkey':(lambda n,v:"key %s;\n"%v),
501          'mobile':sp,
502         })
503         require_properties={
504          'dh':"Diffie-Hellman group",
505          'contact':"Site admin contact address",
506          'networks':"Networks claimed by the site",
507          'hash':"hash function",
508          'peer':"Gateway address of the site",
509          'pubkey':"RSA public key of the site",
510         }
511         def __init__(self,w):
512                 level.__init__(self,w)
513         def output_data(self,w,path):
514                 ind=2*len(path)
515                 np='/'.join(map(lambda i: i.name, path))
516                 self.indent(w,ind)
517                 w.write("%s {\n"%(self.kname()))
518                 self.indent(w,ind+2)
519                 w.write("name \"%s\";\n"%(np,))
520                 self.output_props(w,ind+2)
521                 self.indent(w,ind+2)
522                 w.write("link netlink {\n");
523                 self.indent(w,ind+4)
524                 w.write("routes %s;\n"%str(self.properties["networks"]))
525                 self.indent(w,ind+4)
526                 w.write("ptp-address %s;\n"%str(self.properties["peer"]))
527                 self.indent(w,ind+2)
528                 w.write("};\n")
529                 self.indent(w,ind)
530                 w.write("};\n")
531
532 # Levels in the configuration file
533 # (depth,properties)
534 levels={'vpn':vpnlevel, 'location':locationlevel, 'site':sitelevel}
535
536 # Reserved vpn/location/site names
537 reserved={'all-sites':None}
538 reserved.update(keywords)
539 reserved.update(levels)
540
541 def complain(msg):
542         "Complain about a particular input line"
543         global complaints
544         print(("%s line %d: "%(file,line))+msg)
545         complaints=complaints+1
546 def moan(msg):
547         "Complain about something in general"
548         global complaints
549         print(msg);
550         complaints=complaints+1
551
552 class UntaintedRoot():
553         def __init__(self,s): self._s=s
554         def name(self): return self._s
555         def keyword(self): return self._s
556
557 root=level([UntaintedRoot(x) for x in ['root','root']])
558 # All vpns are children of this node
559 obstack=[root]
560 allow_defs=0   # Level above which new definitions are permitted
561 prefix=''
562
563 def set_property(obj,w):
564         "Set a property on a configuration node"
565         prop=w[0]
566         if prop.raw() in obj.properties:
567                 obj.properties[prop.raw_mark_ok()].add(obj,w)
568         else:
569                 obj.properties[prop.raw()]=keywords[prop.raw_mark_ok()][0](w)
570
571
572 def pline(il,allow_include=False):
573         "Process a configuration file line"
574         global allow_defs, obstack, root
575         w=il.rstrip('\n').split()
576         if len(w)==0: return ['']
577         w=list([Tainted(x) for x in w])
578         keyword=w[0]
579         current=obstack[len(obstack)-1]
580         copyout=lambda: ['    '*len(obstack) +
581                         ' '.join([ww.output() for ww in w]) +
582                         '\n']
583         if keyword=='end-definitions':
584                 keyword.raw_mark_ok()
585                 allow_defs=sitelevel.depth
586                 obstack=[root]
587                 return copyout()
588         if keyword=='include':
589                 if not allow_include:
590                         complain("include not permitted here")
591                         return []
592                 if len(w) != 2:
593                         complain("include requires one argument")
594                         return []
595                 newfile=os.path.join(os.path.dirname(file),w[1].raw_mark_ok())
596                 # ^ user of "include" is trusted so raw_mark_ok is good
597                 return pfilepath(newfile,allow_include=allow_include)
598         if keyword.raw() in levels:
599                 # We may go up any number of levels, but only down by one
600                 newdepth=levels[keyword.raw_mark_ok()].depth
601                 currentdepth=len(obstack) # actually +1...
602                 if newdepth<=currentdepth:
603                         obstack=obstack[:newdepth]
604                 if newdepth>currentdepth:
605                         complain("May not go from level %d to level %d"%
606                                 (currentdepth-1,newdepth))
607                 # See if it's a new one (and whether that's permitted)
608                 # or an existing one
609                 current=obstack[len(obstack)-1]
610                 tname=w[1].name()
611                 if tname in current.children:
612                         # Not new
613                         current=current.children[tname]
614                         if service and group and current.depth==2:
615                                 if group!=current.group:
616                                         complain("Incorrect group!")
617                                 w[2].groupname()
618                 else:
619                         # New
620                         # Ignore depth check for now
621                         nl=levels[keyword.raw()](w)
622                         if nl.depth<allow_defs:
623                                 complain("New definitions not allowed at "
624                                         "level %d"%nl.depth)
625                                 # we risk crashing if we continue
626                                 sys.exit(1)
627                         current.children[tname]=nl
628                         current=nl
629                 obstack.append(current)
630                 return copyout()
631         if keyword.raw() not in current.allow_properties:
632                 complain("Property %s not allowed at %s level"%
633                         (keyword.raw(),current.type))
634                 return []
635         elif current.depth == vpnlevel.depth < allow_defs:
636                 complain("Not allowed to set VPN properties here")
637                 return []
638         else:
639                 set_property(current,w)
640                 return copyout()
641
642         complain("unknown keyword '%s'"%(keyword.raw()))
643
644 def pfilepath(pathname,allow_include=False):
645         f=open(pathname)
646         outlines=pfile(pathname,f.readlines(),allow_include=allow_include)
647         f.close()
648         return outlines
649
650 def pfile(name,lines,allow_include=False):
651         "Process a file"
652         global file,line
653         file=name
654         line=0
655         outlines=[]
656         for i in lines:
657                 line=line+1
658                 if (i[0]=='#'): continue
659                 outlines += pline(i,allow_include=allow_include)
660         return outlines
661
662 def outputsites(w):
663         "Output include file for secnet configuration"
664         w.write("# secnet sites file autogenerated by make-secnet-sites "
665                 +"version %s\n"%VERSION)
666         w.write("# %s\n"%time.asctime(time.localtime(time.time())))
667         w.write("# Command line: %s\n\n"%' '.join(sys.argv))
668
669         # Raw VPN data section of file
670         w.write(prefix+"vpn-data {\n")
671         for i in root.children.values():
672                 i.output_data(w,(i,))
673         w.write("};\n")
674
675         # Per-VPN flattened lists
676         w.write(prefix+"vpn {\n")
677         for i in root.children.values():
678                 i.output_vpnflat(w,())
679         w.write("};\n")
680
681         # Flattened list of sites
682         w.write(prefix+"all-sites %s;\n"%",".join(
683                 map(lambda x:"%svpn/%s/all-sites"%(prefix,x.kname()),
684                         root.children.values())))
685
686 line=0
687 file=None
688 complaints=0
689
690 # Sanity check section
691 # Delete nodes where leaf=0 that have no children
692
693 def live(n):
694         "Number of leafnodes below node n"
695         if n.leaf: return 1
696         for i in n.children.keys():
697                 if live(n.children[i]): return 1
698         return 0
699 def delempty(n):
700         "Delete nodes that have no leafnode children"
701         for i in list(n.children.keys()):
702                 delempty(n.children[i])
703                 if not live(n.children[i]):
704                         del n.children[i]
705
706 # Check that all constraints are met (as far as I can tell
707 # restrict-nets/networks/peer are the only special cases)
708
709 def checkconstraints(n,p,ra):
710         new_p=p.copy()
711         new_p.update(n.properties)
712         for i in n.require_properties.keys():
713                 if i not in new_p:
714                         moan("%s %s is missing property %s"%
715                                 (n.type,n.name,i))
716         for i in new_p.keys():
717                 if i not in n.allow_properties:
718                         moan("%s %s has forbidden property %s"%
719                                 (n.type,n.name,i))
720         # Check address range restrictions
721         if "restrict-nets" in n.properties:
722                 new_ra=ra.intersection(n.properties["restrict-nets"].set)
723         else:
724                 new_ra=ra
725         if "networks" in n.properties:
726                 if not n.properties["networks"].set <= new_ra:
727                         moan("%s %s networks out of bounds"%(n.type,n.name))
728                 if "peer" in n.properties:
729                         if not n.properties["networks"].set.contains(
730                                 n.properties["peer"].addr):
731                                 moan("%s %s peer not in networks"%(n.type,n.name))
732         for i in n.children.keys():
733                 checkconstraints(n.children[i],new_p,new_ra)
734
735 if service:
736         headerinput=pfilepath(header,allow_include=True)
737         userinput=sys.stdin.readlines()
738         pfile("user input",userinput)
739 else:
740         if inputfile is None:
741                 pfile("stdin",sys.stdin.readlines())
742         else:
743                 pfilepath(inputfile)
744
745 delempty(root)
746 checkconstraints(root,{},ipaddrset.complete_set())
747
748 if complaints>0:
749         if complaints==1: print("There was 1 problem.")
750         else: print("There were %d problems."%(complaints))
751         sys.exit(1)
752 complaints=None # arranges to crash if we complain later
753
754 if service:
755         # Put the user's input into their group file, and rebuild the main
756         # sites file
757         f=open(groupfiledir+"/T"+group.groupname(),'w')
758         f.write("# Section submitted by user %s, %s\n"%
759                 (user,time.asctime(time.localtime(time.time()))))
760         f.write("# Checked by make-secnet-sites version %s\n\n"%VERSION)
761         for i in userinput: f.write(i)
762         f.write("\n")
763         f.close()
764         os.rename(groupfiledir+"/T"+group.groupname(),
765                   groupfiledir+"/R"+group.groupname())
766         f=open(sitesfile+"-tmp",'w')
767         f.write("# sites file autogenerated by make-secnet-sites\n")
768         f.write("# generated %s, invoked by %s\n"%
769                 (time.asctime(time.localtime(time.time())),user))
770         f.write("# use make-secnet-sites to turn this file into a\n")
771         f.write("# valid /etc/secnet/sites.conf file\n\n")
772         for i in headerinput: f.write(i)
773         files=os.listdir(groupfiledir)
774         for i in files:
775                 if i[0]=='R':
776                         j=open(groupfiledir+"/"+i)
777                         f.write(j.read())
778                         j.close()
779         f.write("# end of sites file\n")
780         f.close()
781         os.rename(sitesfile+"-tmp",sitesfile)
782 else:
783         outputsites(of)