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