source: genesis/tools/gformat.py@ 10567

Last change on this file since 10567 was 10567, checked in by rick, 13 years ago

Ideetje van richard op active machines weer te geven wat beter in de huidige
setup weergeven (help aangepast).

Verder de list <iets> uitgebreid zodat ook andere features weergeven kunnen
worden.

  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 39.9 KB
Line 
1#!/usr/bin/env python
2#
3# vim:ts=2:et:sw=2:ai
4# Wireless Leiden configuration generator, based on yaml files'
5#
6# XXX: This should be rewritten to make use of the ipaddr.py library.
7#
8# Sample apache configuration (mind the AcceptPathInfo!)
9# ScriptAlias /wleiden/config /usr/local/www/genesis/tools/gformat.py
10# <Directory /usr/local/www/genesis>
11# Allow from all
12# AcceptPathInfo On
13# </Directory>
14#
15# Rick van der Zwet <info@rickvanderzwet.nl>
16#
17
18# Hack to make the script directory is also threated as a module search path.
19import sys
20import os
21import re
22sys.path.append(os.path.dirname(__file__))
23
24import cgi
25import cgitb
26import copy
27import glob
28import socket
29import string
30import subprocess
31import time
32import rdnap
33import make_network_kml
34from pprint import pprint
35from collections import defaultdict
36try:
37 import yaml
38except ImportError, e:
39 print e
40 print "[ERROR] Please install the python-yaml or devel/py-yaml package"
41 exit(1)
42
43try:
44 from yaml import CLoader as Loader
45 from yaml import CDumper as Dumper
46except ImportError:
47 from yaml import Loader, Dumper
48
49from jinja2 import Template
50
51import logging
52logging.basicConfig(format='# %(levelname)s: %(message)s' )
53logger = logging.getLogger()
54logger.setLevel(logging.DEBUG)
55
56
57if os.environ.has_key('CONFIGROOT'):
58 NODE_DIR = os.environ['CONFIGROOT']
59else:
60 NODE_DIR = os.path.abspath(os.path.dirname(__file__)) + '/../nodes'
61__version__ = '$Id: gformat.py 10567 2012-04-24 17:42:39Z rick $'
62
63
64files = [
65 'authorized_keys',
66 'dnsmasq.conf',
67 'dhcpd.conf',
68 'rc.conf.local',
69 'resolv.conf',
70 'motd',
71 'wleiden.yaml',
72 ]
73
74# Global variables uses
75OK = 10
76DOWN = 20
77UNKNOWN = 90
78
79def get_yaml(item):
80 """ Get configuration yaml for 'item'"""
81 gfile = os.path.join(NODE_DIR,item,'wleiden.yaml')
82
83 # Use some boring defaults
84 datadump = { 'service_proxy' : False, 'service_ileiden' : False }
85 f = open(gfile, 'r')
86 datadump.update(yaml.load(f,Loader=Loader))
87 f.close()
88
89 # Preformat certain needed variables for formatting and push those into special object
90 datadump['autogen_iface_keys'] = get_interface_keys(datadump)
91
92 wlan_count=0
93 for key in datadump['autogen_iface_keys']:
94 if datadump[key]['type'] in ['11a', '11b', '11g', 'wireless']:
95 datadump[key]['autogen_ifname'] = 'wlan%i' % wlan_count
96 wlan_count += 1
97 else:
98 datadump[key]['autogen_ifname'] = datadump[key]['interface'].split(':')[0]
99
100 dhcp_interfaces = [datadump[key]['autogen_ifname'] for key in datadump['autogen_iface_keys'] if datadump[key]['dhcp']]
101 datadump['autogen_dhcp_interfaces'] = ','.join(dhcp_interfaces)
102 datadump['autogen_item'] = item
103
104 datadump['autogen_realname'] = get_realname(datadump)
105 datadump['autogen_domain'] = datadump['domain'] if datadump.has_key('domain') else 'wleiden.net.'
106 datadump['autogen_fqdn'] = datadump['autogen_realname'] + '.' + datadump['autogen_domain']
107 return datadump
108
109
110def store_yaml(datadump, header=False):
111 """ Store configuration yaml for 'item'"""
112 item = datadump['autogen_item']
113 gfile = os.path.join(NODE_DIR,item,'wleiden.yaml')
114
115 f = open(gfile, 'w')
116 f.write(generate_wleiden_yaml(datadump, header))
117 f.close()
118
119
120
121def make_relations():
122 """ Process _ALL_ yaml files to get connection relations """
123 errors = ""
124 poel = defaultdict(list)
125 for host in get_hostlist():
126 try:
127 datadump = get_yaml(host)
128 for iface_key in datadump['autogen_iface_keys']:
129 l = datadump[iface_key]['ip']
130 addr, mask = l.split('/')
131
132 # Not parsing of these folks please
133 if not valid_addr(addr):
134 continue
135
136 addr = parseaddr(addr)
137 mask = int(mask)
138 network = addr & ~((1 << (32 - mask)) - 1)
139 poel[network] += [(host,datadump[iface_key])]
140 except (KeyError, ValueError), e:
141 errors += "[FOUT] in '%s' interface '%s'" % (host,iface_key)
142 errors += e
143 continue
144 return (poel, errors)
145
146
147def get_proxylist():
148 """Get all available proxies proxyX sorting based on X number"""
149 proxylist = sorted([os.path.basename(x) for x in glob.glob("%s/proxy*" % NODE_DIR)],
150 key=lambda name: int(''.join([c for c in name if c in string.digits])),
151 cmp=lambda x,y: x - y) + sorted([os.path.basename(x) for x in glob.glob("%s/Proxy*" % NODE_DIR)])
152 return proxylist
153
154def get_hybridlist():
155 """Get all available hybrid nodes/proxies"""
156 hybridlist = sorted([os.path.basename(x) for x in glob.glob("%s/Hybrid*" % NODE_DIR)])
157 return hybridlist
158
159
160def valid_addr(addr):
161 """ Show which address is valid in which are not """
162 return str(addr).startswith('172.')
163
164
165def get_nodelist():
166 """ Get all available nodes - sorted """
167 nodelist = sorted([os.path.basename(x) for x in glob.glob("%s/CNode*" % NODE_DIR)])
168 return nodelist
169
170def get_hostlist():
171 """ Combined hosts and proxy list"""
172 return get_nodelist() + get_proxylist() + get_hybridlist()
173
174def angle_between_points(lat1,lat2,long1,long2):
175 """
176 Return Angle in radians between two GPS coordinates
177 See: http://stackoverflow.com/questions/3809179/angle-between-2-gps-coordinates
178 """
179 dy = lat2 - lat1
180 dx = math.cos(math.pi/180*lat1)*(long2 - long1)
181 angle = math.atan2(dy,dx)
182 return angle
183
184def angle_to_cd(angle):
185 """ Return Dutch Cardinal Direction estimation in 'one digit' of radian angle """
186
187 # For easy conversion get positive degree
188 degrees = math.degrees(angle)
189 if degrees < 0:
190 360 - abs(degrees)
191
192 # Numbers can be confusing calculate from the 4 main directions
193 p = 22.5
194 if degrees < p:
195 return "n"
196 elif degrees < (90 - p):
197 return "no"
198 elif degrees < (90 + p):
199 return "o"
200 elif degrees < (180 - p):
201 return "zo"
202 elif degrees < (180 + p):
203 return "z"
204 elif degrees < (270 - p):
205 return "zw"
206 elif degrees < (270 + p):
207 return "w"
208 elif degrees < (360 - p):
209 return "nw"
210 else:
211 return "n"
212
213
214def generate_title(nodelist):
215 """ Main overview page """
216 items = {'root' : "." }
217 output = """
218<html>
219 <head>
220 <title>Wireless leiden Configurator - GFormat</title>
221 <style type="text/css">
222 th {background-color: #999999}
223 tr:nth-child(odd) {background-color: #cccccc}
224 tr:nth-child(even) {background-color: #ffffff}
225 th, td {padding: 0.1em 1em}
226 </style>
227 </head>
228 <body>
229 <center>
230 <form type="GET" action="%(root)s">
231 <input type="hidden" name="action" value="update">
232 <input type="submit" value="Update Configuration Database (SVN)">
233 </form>
234 <table>
235 <caption><h3>Wireless Leiden Configurator</h3></caption>
236 """ % items
237
238 for node in nodelist:
239 items['node'] = node
240 output += '<tr><td><a href="%(root)s/%(node)s">%(node)s</a></td>' % items
241 for config in files:
242 items['config'] = config
243 output += '<td><a href="%(root)s/%(node)s/%(config)s">%(config)s</a></td>' % items
244 output += "</tr>"
245 output += """
246 </table>
247 <hr />
248 <em>%s</em>
249 </center>
250 </body>
251</html>
252 """ % __version__
253
254 return output
255
256
257
258def generate_node(node):
259 """ Print overview of all files available for node """
260 return "\n".join(files)
261
262def generate_node_overview(host):
263 """ Print overview of all files available for node """
264 datadump = get_yaml(host)
265 params = { 'host' : host }
266 output = "<em><a href='..'>Back to overview</a></em><hr />"
267 output += "<h2>Available files:</h2><ul>"
268 for cf in files:
269 params['cf'] = cf
270 output += '<li><a href="%(host)s/%(cf)s">%(cf)s</a></li>\n' % params
271 output += "</ul>"
272
273 # Generate and connection listing
274 output += "<h2>Connected To:</h2><ul>"
275 (poel, errors) = make_relations()
276 for network, hosts in poel.iteritems():
277 if host in [x[0] for x in hosts]:
278 if len(hosts) == 1:
279 # Single not connected interface
280 continue
281 for remote,ifacedump in hosts:
282 if remote == host:
283 # This side of the interface
284 continue
285 params = { 'remote': remote, 'remote_ip' : ifacedump['ip'] }
286 output += '<li><a href="%(remote)s">%(remote)s</a> -- %(remote_ip)s</li>\n' % params
287 output += "</ul>"
288 output += "<h2>MOTD details:</h2><pre>" + generate_motd(datadump) + "</pre>"
289
290 output += "<hr /><em><a href='..'>Back to overview</a></em>"
291 return output
292
293
294def generate_header(ctag="#"):
295 return """\
296%(ctag)s
297%(ctag)s DO NOT EDIT - Automatically generated by 'gformat'
298%(ctag)s Generated at %(date)s by %(host)s
299%(ctag)s
300""" % { 'ctag' : ctag, 'date' : time.ctime(), 'host' : socket.gethostname() }
301
302
303
304def parseaddr(s):
305 """ Process IPv4 CIDR notation addr to a (binary) number """
306 f = s.split('.')
307 return (long(f[0]) << 24L) + \
308 (long(f[1]) << 16L) + \
309 (long(f[2]) << 8L) + \
310 long(f[3])
311
312
313
314def showaddr(a):
315 """ Display IPv4 addr in (dotted) CIDR notation """
316 return "%d.%d.%d.%d" % ((a >> 24) & 0xff, (a >> 16) & 0xff, (a >> 8) & 0xff, a & 0xff)
317
318
319def is_member(ip, mask, canidate):
320 """ Return True if canidate is part of ip/mask block"""
321 ip_addr = gformat.parseaddr(ip)
322 ip_canidate = gformat.parseaddr(canidate)
323 mask = int(mask)
324 ip_addr = ip_addr & ~((1 << (32 - mask)) - 1)
325 ip_canidate = ip_canidate & ~((1 << (32 - mask)) - 1)
326 return ip_addr == ip_canidate
327
328
329
330def cidr2netmask(netmask):
331 """ Given a 'netmask' return corresponding CIDR """
332 return showaddr(0xffffffff & (0xffffffff << (32 - int(netmask))))
333
334def get_network(addr, mask):
335 return showaddr(parseaddr(addr) & ~((1 << (32 - int(mask))) - 1))
336
337
338def generate_dhcpd_conf(datadump):
339 """ Generate config file '/usr/local/etc/dhcpd.conf """
340 output = generate_header()
341 output += Template("""\
342# option definitions common to all supported networks...
343option domain-name "dhcp.{{ autogen_fqdn }}";
344
345default-lease-time 600;
346max-lease-time 7200;
347
348# Use this to enble / disable dynamic dns updates globally.
349#ddns-update-style none;
350
351# If this DHCP server is the official DHCP server for the local
352# network, the authoritative directive should be uncommented.
353authoritative;
354
355# Use this to send dhcp log messages to a different log file (you also
356# have to hack syslog.conf to complete the redirection).
357log-facility local7;
358
359#
360# Interface definitions
361#
362\n""").render(datadump)
363
364 for iface_key in datadump['autogen_iface_keys']:
365 if not datadump[iface_key].has_key('comment'):
366 datadump[iface_key]['comment'] = None
367 output += "## %(interface)s - %(desc)s - %(comment)s\n" % datadump[iface_key]
368
369 (addr, mask) = datadump[iface_key]['ip'].split('/')
370 datadump[iface_key]['addr'] = addr
371 datadump[iface_key]['netmask'] = cidr2netmask(mask)
372 datadump[iface_key]['subnet'] = get_network(addr, mask)
373 try:
374 (dhcp_start, dhcp_stop) = datadump[iface_key]['dhcp'].split('-')
375 except (AttributeError, ValueError):
376 output += "subnet %(subnet)s netmask %(netmask)s {\n ### not autoritive\n}\n\n" % datadump[iface_key]
377 continue
378
379 dhcp_part = ".".join(addr.split('.')[0:3])
380 datadump[iface_key]['dhcp_start'] = dhcp_part + "." + dhcp_start
381 datadump[iface_key]['dhcp_stop'] = dhcp_part + "." + dhcp_stop
382 output += """\
383subnet %(subnet)s netmask %(netmask)s {
384 range %(dhcp_start)s %(dhcp_stop)s;
385 option routers %(addr)s;
386 option domain-name-servers %(addr)s;
387}
388\n""" % datadump[iface_key]
389
390 return output
391
392
393
394def generate_dnsmasq_conf(datadump):
395 """ Generate configuration file '/usr/local/etc/dnsmasq.conf' """
396 output = generate_header()
397 output += Template("""\
398# DHCP server options
399dhcp-authoritative
400dhcp-fqdn
401domain=dhcp.{{ autogen_fqdn }}
402domain-needed
403expand-hosts
404log-async=100
405
406# Low memory footprint
407cache-size=10000
408
409\n""").render(datadump)
410
411 for iface_key in datadump['autogen_iface_keys']:
412 if not datadump[iface_key].has_key('comment'):
413 datadump[iface_key]['comment'] = None
414 output += "## %(interface)s - %(desc)s - %(comment)s\n" % datadump[iface_key]
415
416 try:
417 (dhcp_start, dhcp_stop) = datadump[iface_key]['dhcp'].split('-')
418 (ip, cidr) = datadump[iface_key]['ip'].split('/')
419 datadump[iface_key]['netmask'] = cidr2netmask(cidr)
420 except (AttributeError, ValueError):
421 output += "# not autoritive\n\n"
422 continue
423
424 dhcp_part = ".".join(ip.split('.')[0:3])
425 datadump[iface_key]['dhcp_start'] = dhcp_part + "." + dhcp_start
426 datadump[iface_key]['dhcp_stop'] = dhcp_part + "." + dhcp_stop
427 output += "dhcp-range=%(interface)s,%(dhcp_start)s,%(dhcp_stop)s,%(netmask)s,24h\n\n" % datadump[iface_key]
428
429 return output
430
431
432
433def generate_rc_conf_local(datadump):
434 """ Generate configuration file '/etc/rc.conf.local' """
435 if not datadump.has_key('ileiden'):
436 datadump['autogen_ileiden_enable'] = False
437 else:
438 datadump['autogen_ileiden_enable'] = datadump['ileiden']
439
440 datadump['autogen_ileiden_enable'] = switchFormat(datadump['autogen_ileiden_enable'])
441
442 ileiden_proxies = []
443 normal_proxies = []
444 for proxy in get_proxylist():
445 proxydump = get_yaml(proxy)
446 if proxydump['ileiden']:
447 ileiden_proxies.append(proxydump)
448 else:
449 normal_proxies.append(proxydump)
450 for host in get_hybridlist():
451 hostdump = get_yaml(host)
452 if hostdump['service_ileiden']:
453 ileiden_proxies.append(hostdump)
454 if hostdump['service_proxy']:
455 normal_proxies.append(hostdump)
456
457 datadump['autogen_ileiden_proxies'] = ','.join([x['masterip'] for x in ileiden_proxies])
458 datadump['autogen_ileiden_proxies_names'] = ','.join([x['autogen_item'] for x in ileiden_proxies])
459 datadump['autogen_normal_proxies'] = ','.join([x['masterip'] for x in normal_proxies])
460 datadump['autogen_normal_proxies_names'] = ','.join([x['autogen_item'] for x in normal_proxies])
461
462 output = generate_header("#");
463 output += Template("""\
464hostname='{{ autogen_fqdn }}'
465location='{{ location }}'
466nodetype="{{ nodetype }}"
467
468#
469# Configured listings
470#
471captive_portal_whitelist=""
472# iLeiden Proxies {{ autogen_ileiden_proxies_names }}
473list_ileiden_proxies="{{ autogen_ileiden_proxies }}"
474# normal Proxies {{ autogen_normal_proxies_names }}
475list_normal_proxies="{{ autogen_normal_proxies }}"
476
477{% if nodetype == "Proxy" %}
478#
479# Proxy Configuration
480#
481{% if gateway -%}
482defaultrouter="{{ gateway }}"
483{% else -%}
484#defaultrouter="NOTSET"
485{% endif -%}
486internalif="{{ internalif }}"
487ileiden_enable="{{ autogen_ileiden_enable }}"
488gateway_enable="{{ autogen_ileiden_enable }}"
489pf_enable="yes"
490pf_rules="/etc/pf.conf"
491{% if autogen_ileiden_enable -%}
492pf_flags="-D ext_if={{ externalif }} -D int_if={{ internalif }} -D publicnat={80,443}"
493lvrouted_enable="{{ autogen_ileiden_enable }}"
494lvrouted_flags="-u -s s00p3rs3kr3t -m 28"
495{% else -%}
496pf_flags="-D ext_if={{ externalif }} -D int_if={{ internalif }} -D publicnat={0}"
497{% endif -%}
498{% if internalroute -%}
499static_routes="wleiden"
500route_wleiden="-net 172.16.0.0/12 {{ internalroute }}"
501{% endif -%}
502{% endif -%}
503
504{% if nodetype == "Hybrid" %}
505#
506# Hybrid Configuration
507#
508captive_portal_interfaces="{{ autogen_dhcp_interfaces|default('none', true) }}"
509externalif="{{ externalif|default('vr0', true) }}"
510masterip="{{ masterip }}"
511
512pf_rules="/etc/pf.hybrid.conf"
513pf_flags_hybrid="-D ext_if=$externalif -D ext_if_net=$externalif:network -D captive_portal_interfaces=$captive_portal_interfaces -D masterip=$masterip"
514{% if service_ileiden -%}
515# Service iLeiden
516service_ileiden="yes"
517pf_flags="$pf_flags_hybrid -D publicnat=80,443"
518{% else -%}
519pf_flags="$pf_flags_hybrid -D publicnat=0"
520{% endif -%}
521
522{% if service_proxy %}
523# Service Proxy
524service_proxy="yes"
525tinyproxy_enable="yes"
526{% else -%}
527service_proxy="no"
528pen_wrapper_enable="yes"
529{% endif -%}
530
531{% if board == "ALIX2" %}
532# ''Fat'' configuration, board has 256MB RAM
533dnsmasq_enable="NO"
534named_enable="YES"
535dhcpd_enable="YES"
536{% endif -%}
537
538{% if gateway -%}
539defaultrouter="{{ gateway }}"
540{% endif -%}
541#
542# END Hybrid Configuration
543#
544{% endif -%}
545
546
547{% if nodetype == "CNode" %}
548#
549# NODE iLeiden Configuration
550#
551captive_portal_interfaces="{{ autogen_dhcp_interfaces }}"
552
553{% if tproxy -%}
554tproxy_enable='YES'
555tproxy_range='{{ tproxy }}'
556{% else -%}
557tproxy_enable='NO'
558{% endif -%}
559
560lvrouted_flags="-u -s s00p3rs3kr3t -m 28 -z $list_ileiden_proxies"
561{% endif %}
562\n
563""").render(datadump)
564
565 # lo0 configuration:
566 # - 172.32.255.1/32 is the proxy.wleiden.net deflector
567 # - masterip is special as it needs to be assigned to at
568 # least one interface, so if not used assign to lo0
569 addrs_list = { 'lo0' : [("127.0.0.1/8", "LocalHost"), ("172.31.255.1/32","Proxy IP")] }
570 iface_map = {'lo0' : 'lo0'}
571 dhclient_if = {'lo0' : False}
572
573 masterip_used = False
574 for iface_key in datadump['autogen_iface_keys']:
575 if datadump[iface_key]['ip'].startswith(datadump['masterip']):
576 masterip_used = True
577 break
578 if not masterip_used:
579 addrs_list['lo0'].append((datadump['masterip'] + "/32", 'Master IP Not used in interface'))
580
581 for iface_key in datadump['autogen_iface_keys']:
582 ifacedump = datadump[iface_key]
583 ifname = ifacedump['autogen_ifname']
584
585 # Flag dhclient is possible
586 dhclient_if[ifname] = ifacedump.has_key('dhcpclient') and ifacedump['dhcpclient']
587
588 # Add interface IP to list
589 item = (ifacedump['ip'], ifacedump['desc'])
590 if addrs_list.has_key(ifname):
591 addrs_list[ifname].append(item)
592 else:
593 addrs_list[ifname] = [item]
594
595 # Alias only needs IP assignment for now, this might change if we
596 # are going to use virtual accesspoints
597 if "alias" in iface_key:
598 continue
599
600 # XXX: Might want to deduct type directly from interface name
601 if ifacedump['type'] in ['11a', '11b', '11g', 'wireless']:
602 # Default to station (client) mode
603 ifacedump['wlanmode'] = "sta"
604 if ifacedump['mode'] in ['master', 'master-wds', 'ap', 'ap-wds']:
605 ifacedump['wlanmode'] = "ap"
606 # Default to 802.11b mode
607 ifacedump['mode'] = '11b'
608 if ifacedump['type'] in ['11a', '11b' '11g']:
609 ifacedump['mode'] = ifacedump['type']
610
611 if not ifacedump.has_key('channel'):
612 if ifacedump['type'] == '11a':
613 ifacedump['channel'] = 36
614 else:
615 ifacedump['channel'] = 1
616
617 # Allow special hacks at the back like wds and stuff
618 if not ifacedump.has_key('extra'):
619 ifacedump['extra'] = 'regdomain ETSI country NL'
620
621 output += "wlans_%(interface)s='%(autogen_ifname)s'\n" % ifacedump
622 output += ("create_args_%(autogen_ifname)s='wlanmode %(wlanmode)s mode " +\
623 "%(mode)s ssid %(ssid)s %(extra)s channel %(channel)s'\n") % ifacedump
624
625 elif ifacedump['type'] in ['ethernet', 'eth']:
626 # No special config needed besides IP
627 pass
628 else:
629 assert False, "Unknown type " + ifacedump['type']
630
631 # Print IP address which needs to be assigned over here
632 output += "\n"
633 for iface,addrs in sorted(addrs_list.iteritems()):
634 for addr, comment in sorted(addrs,key=lambda x: parseaddr(x[0].split('/')[0])):
635 output += "# %s || %s || %s\n" % (iface, addr, comment)
636
637 # Write DHCLIENT entry
638 if dhclient_if[iface]:
639 output += "ifconfig_%s='SYNCDHCP'\n\n" % (iface)
640 else:
641 output += "ipv4_addrs_%s='%s'\n\n" % (iface, " ".join([x[0] for x in addrs]))
642
643 return output
644
645
646
647
648def get_all_configs():
649 """ Get dict with key 'host' with all configs present """
650 configs = dict()
651 for host in get_hostlist():
652 datadump = get_yaml(host)
653 configs[host] = datadump
654 return configs
655
656
657def get_interface_keys(config):
658 """ Quick hack to get all interface keys, later stage convert this to a iterator """
659 return sorted([elem for elem in config.keys() if (elem.startswith('iface_') and not "lo0" in elem)])
660
661
662def get_used_ips(configs):
663 """ Return array of all IPs used in config files"""
664 ip_list = []
665 for config in configs:
666 ip_list.append(config['masterip'])
667 for iface_key in get_interface_keys(config):
668 l = config[iface_key]['ip']
669 addr, mask = l.split('/')
670 # Special case do not process
671 if valid_addr(addr):
672 ip_list.append(addr)
673 else:
674 logger.error("## IP '%s' in '%s' not valid" % (addr, config['nodename']))
675 return sorted(ip_list)
676
677
678
679def generate_resolv_conf(datadump):
680 """ Generate configuration file '/etc/resolv.conf' """
681 # XXX: This should properly going to be an datastructure soon
682 datadump['autogen_header'] = generate_header("#")
683 datadump['autogen_edge_nameservers'] = ''
684 for host in get_proxylist():
685 hostdump = get_yaml(host)
686 datadump['autogen_edge_nameservers'] += "nameserver %(masterip)-15s # %(autogen_realname)s\n" % hostdump
687 for host in get_hybridlist():
688 hostdump = get_yaml(host)
689 if hostdump['service_proxy'] or hostdump['service_ileiden']:
690 datadump['autogen_edge_nameservers'] += "nameserver %(masterip)-15s # %(autogen_realname)s\n" % hostdump
691
692 return Template("""\
693{{ autogen_header }}
694search wleiden.net
695
696# Try local (cache) first
697nameserver 127.0.0.1
698
699{% if service_proxy or service_ileiden or nodetype == 'Proxy' -%}
700nameserver 8.8.8.8 # Google Public NameServer
701nameserver 8.8.4.4 # Google Public NameServer
702{% else -%}
703{{ autogen_edge_nameservers }}
704{% endif -%}
705""").render(datadump)
706
707
708
709def generate_motd(datadump):
710 """ Generate configuration file '/etc/motd' """
711 output = """\
712FreeBSD 9.0-RELEASE (kernel.wleiden) #0 r230587: Sun Jan 29 17:09:57 CET 2012
713
714 WWW: %(autogen_fqdn)s - http://www.wirelessleiden.nl
715 Loc: %(location)s
716
717Interlinks:
718""" % datadump
719
720 # XXX: This is a hacky way to get the required data
721 for line in generate_rc_conf_local(datadump).split('\n'):
722 if '||' in line and not line[1:].split()[0] in ['lo0', 'ath0'] :
723 output += " - %s \n" % line[1:]
724 output += """\
725Attached bridges:
726"""
727 for iface_key in datadump['autogen_iface_keys']:
728 ifacedump = datadump[iface_key]
729 if ifacedump.has_key('ns_ip'):
730 output += " - %(interface)s || %(mode)s || %(ns_ip)s\n" % ifacedump
731
732 return output
733
734
735def format_yaml_value(value):
736 """ Get yaml value in right syntax for outputting """
737 if isinstance(value,str):
738 output = '"%s"' % value
739 else:
740 output = value
741 return output
742
743
744
745def format_wleiden_yaml(datadump):
746 """ Special formatting to ensure it is editable"""
747 output = "# Genesis config yaml style\n"
748 output += "# vim:ts=2:et:sw=2:ai\n"
749 output += "#\n"
750 iface_keys = [elem for elem in datadump.keys() if elem.startswith('iface_')]
751 for key in sorted(set(datadump.keys()) - set(iface_keys)):
752 output += "%-10s: %s\n" % (key, format_yaml_value(datadump[key]))
753
754 output += "\n\n"
755
756 key_order = [ 'comment', 'interface', 'ip', 'desc', 'sdesc', 'mode', 'type',
757 'extra_type', 'channel', 'ssid', 'dhcp' ]
758
759 for iface_key in sorted(iface_keys):
760 output += "%s:\n" % iface_key
761 for key in key_order + list(sorted(set(datadump[iface_key].keys()) - set(key_order))):
762 if datadump[iface_key].has_key(key):
763 output += " %-11s: %s\n" % (key, format_yaml_value(datadump[iface_key][key]))
764 output += "\n\n"
765
766 return output
767
768
769
770def generate_wleiden_yaml(datadump, header=True):
771 """ Generate (petty) version of wleiden.yaml"""
772 for key in datadump.keys():
773 if key.startswith('autogen_'):
774 del datadump[key]
775 # Interface autogen cleanups
776 elif type(datadump[key]) == dict:
777 for key2 in datadump[key].keys():
778 if key2.startswith('autogen_'):
779 del datadump[key][key2]
780
781 output = generate_header("#") if header else ''
782 output += format_wleiden_yaml(datadump)
783 return output
784
785
786def generate_yaml(datadump):
787 return generate_config(datadump['nodename'], "wleiden.yaml", datadump)
788
789
790
791def generate_config(node, config, datadump=None):
792 """ Print configuration file 'config' of 'node' """
793 output = ""
794 try:
795 # Load config file
796 if datadump == None:
797 datadump = get_yaml(node)
798
799 if config == 'wleiden.yaml':
800 output += generate_wleiden_yaml(datadump)
801 elif config == 'authorized_keys':
802 f = open(os.path.join(NODE_DIR,"global_keys"), 'r')
803 output += f.read()
804 f.close()
805 elif config == 'dnsmasq.conf':
806 output += generate_dnsmasq_conf(datadump)
807 elif config == 'dhcpd.conf':
808 output += generate_dhcpd_conf(datadump)
809 elif config == 'rc.conf.local':
810 output += generate_rc_conf_local(datadump)
811 elif config == 'resolv.conf':
812 output += generate_resolv_conf(datadump)
813 elif config == 'motd':
814 output += generate_motd(datadump)
815 else:
816 assert False, "Config not found!"
817 except IOError, e:
818 output += "[ERROR] Config file not found"
819 return output
820
821
822
823def process_cgi_request():
824 """ When calling from CGI """
825 # Update repository if requested
826 form = cgi.FieldStorage()
827 if form.getvalue("action") == "update":
828 print "Refresh: 5; url=."
829 print "Content-type:text/plain\r\n\r\n",
830 print "[INFO] Updating subverion, please wait..."
831 print subprocess.Popen(['svn', 'cleanup', "%s/.." % NODE_DIR], stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0],
832 print subprocess.Popen(['svn', 'up', "%s/.." % NODE_DIR], stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0],
833 print "[INFO] All done, redirecting in 5 seconds"
834 sys.exit(0)
835
836
837 base_uri = os.environ['PATH_INFO']
838 uri = base_uri.strip('/').split('/')
839
840 output = ""
841 if base_uri.endswith('/create/network.kml'):
842 output += "Content-type:application/vnd.google-earth.kml+xml\r\n\r\n"
843 output += make_network_kml.make_graph()
844 elif not uri[0]:
845 if is_text_request():
846 output += "Content-type:text/plain\r\n\r\n"
847 output += '\n'.join(get_hostlist())
848 else:
849 output += "Content-type:text/html\r\n\r\n"
850 output += generate_title(get_hostlist())
851 elif len(uri) == 1:
852 if is_text_request():
853 output += "Content-type:text/plain\r\n\r\n"
854 output += generate_node(uri[0])
855 else:
856 output += "Content-type:text/html\r\n\r\n"
857 output += generate_node_overview(uri[0])
858 elif len(uri) == 2:
859 output += "Content-type:text/plain\r\n\r\n"
860 output += generate_config(uri[0], uri[1])
861 else:
862 assert False, "Invalid option"
863 print output
864
865def get_realname(datadump):
866 # Proxy naming convention is special, as the proxy name is also included in
867 # the nodename, when it comes to the numbered proxies.
868 if datadump['nodetype'] == 'Proxy':
869 realname = datadump['nodetype'] + datadump['nodename'].replace('proxy','')
870 else:
871 # By default the full name is listed and also a shortname CNAME for easy use.
872 realname = datadump['nodetype'] + datadump['nodename']
873 return(realname)
874
875
876
877def make_dns(output_dir = 'dns', external = False):
878 items = dict()
879
880 # hostname is key, IP is value
881 wleiden_zone = dict()
882 wleiden_cname = dict()
883
884 pool = dict()
885 for node in get_hostlist():
886 datadump = get_yaml(node)
887
888 # Proxy naming convention is special
889 fqdn = datadump['autogen_realname']
890 if datadump['nodetype'] in ['CNode', 'Hybrid']:
891 wleiden_cname[datadump['nodename']] = fqdn
892
893 wleiden_zone[fqdn] = datadump['masterip']
894
895 # Hacking to get proper DHCP IPs and hostnames
896 for iface_key in get_interface_keys(datadump):
897 iface_name = datadump[iface_key]['interface'].replace(':',"-alias-")
898 (ip, cidr) = datadump[iface_key]['ip'].split('/')
899 try:
900 (dhcp_start, dhcp_stop) = datadump[iface_key]['dhcp'].split('-')
901 datadump[iface_key]['netmask'] = cidr2netmask(cidr)
902 dhcp_part = ".".join(ip.split('.')[0:3])
903 if ip != datadump['masterip']:
904 wleiden_zone["dhcp-gateway-%s.%s" % (iface_name, fqdn)] = ip
905 for i in range(int(dhcp_start), int(dhcp_stop) + 1):
906 wleiden_zone["dhcp-%s-%s.%s" % (i, iface_name, fqdn)] = "%s.%s" % (dhcp_part, i)
907 except (AttributeError, ValueError):
908 # First push it into a pool, to indentify the counter-part later on
909 addr = parseaddr(ip)
910 cidr = int(cidr)
911 addr = addr & ~((1 << (32 - cidr)) - 1)
912 if pool.has_key(addr):
913 pool[addr] += [(iface_name, fqdn, ip)]
914 else:
915 pool[addr] = [(iface_name, fqdn, ip)]
916 continue
917
918
919 def pool_to_name(node, pool_members):
920 """Convert the joined name to a usable pool name"""
921
922 # Get rid of the own entry
923 pool_members = list(set(pool_members) - set([fqdn]))
924
925 target = oldname = ''
926 for node in sorted(pool_members):
927 (name, number) = re.match('^([A-Za-z]+)([0-9]*)$',node).group(1,2)
928 target += "-" + number if name == oldname else "-" + node if target else node
929 oldname = name
930
931 return target
932
933
934 # WL uses an /29 to configure an interface. IP's are ordered like this:
935 # MasterA (.1) -- DeviceA (.2) <<>> DeviceB (.3) --- SlaveB (.4)
936
937 sn = lambda x: re.sub(r'(?i)^cnode','',x)
938
939 # Automatic naming convention of interlinks namely 2 + remote.lower()
940 for (key,value) in pool.iteritems():
941 # Make sure they are sorted from low-ip to high-ip
942 value = sorted(value, key=lambda x: parseaddr(x[2]))
943
944 if len(value) == 1:
945 (iface_name, fqdn, ip) = value[0]
946 wleiden_zone["2unused-%s.%s" % (iface_name, fqdn)] = ip
947
948 # Device DNS names
949 if 'cnode' in fqdn.lower():
950 wleiden_zone["d-at-%s.%s" % (iface_name, fqdn)] = showaddr(parseaddr(ip) + 1)
951 wleiden_cname["d-at-%s.%s" % (iface_name,sn(fqdn))] = "d-at-%s.%s" % (iface_name, fqdn)
952
953 elif len(value) == 2:
954 (a_iface_name, a_fqdn, a_ip) = value[0]
955 (b_iface_name, b_fqdn, b_ip) = value[1]
956 wleiden_zone["2%s.%s" % (b_fqdn,a_fqdn)] = a_ip
957 wleiden_zone["2%s.%s" % (a_fqdn,b_fqdn)] = b_ip
958
959 # Device DNS names
960 if 'cnode' in a_fqdn.lower() and 'cnode' in b_fqdn.lower():
961 wleiden_zone["d-at-%s.%s" % (a_iface_name, a_fqdn)] = showaddr(parseaddr(a_ip) + 1)
962 wleiden_zone["d-at-%s.%s" % (b_iface_name, b_fqdn)] = showaddr(parseaddr(b_ip) - 1)
963 wleiden_cname["d-at-%s.%s" % (a_iface_name,sn(a_fqdn))] = "d-at-%s.%s" % (a_iface_name, a_fqdn)
964 wleiden_cname["d-at-%s.%s" % (b_iface_name,sn(b_fqdn))] = "d-at-%s.%s" % (b_iface_name, b_fqdn)
965 wleiden_cname["d2%s.%s" % (sn(b_fqdn),sn(a_fqdn))] = "d-at-%s.%s" % (a_iface_name, a_fqdn)
966 wleiden_cname["d2%s.%s" % (sn(a_fqdn),sn(b_fqdn))] = "d-at-%s.%s" % (b_iface_name, b_fqdn)
967
968 else:
969 pool_members = [k[1] for k in value]
970 for item in value:
971 (iface_name, fqdn, ip) = item
972 pool_name = "2pool-" + showaddr(key).replace('.','-') + "-" + pool_to_name(fqdn,pool_members)
973 wleiden_zone["%s.%s" % (pool_name, fqdn)] = ip
974
975 # Include static DNS entries
976 # XXX: Should they override the autogenerated results?
977 # XXX: Convert input to yaml more useable.
978 # Format:
979 ##; this is a comment
980 ## roomburgh=CNodeRoomburgh1
981 ## apkerk1.CNodeVosko=172.17.176.8 ;this as well
982 dns = yaml.load(open(os.path.join(NODE_DIR,'../dns/staticDNS.yaml'),'r'))
983
984 # Hack to allow special entries, for development
985 wleiden_raw = dns['raw']
986 del dns['raw']
987
988 for comment, block in dns.iteritems():
989 for k,v in block.iteritems():
990 if valid_addr(v):
991 wleiden_zone[k] = v
992 else:
993 wleiden_cname[k] = v
994
995 details = dict()
996 # 24 updates a day allowed
997 details['serial'] = time.strftime('%Y%m%d%H')
998
999 if external:
1000 dns_masters = ['siteview.wirelessleiden.nl', 'ns1.vanderzwet.net']
1001 else:
1002 dns_masters = ['sunny.wleiden.net']
1003
1004 details['master'] = dns_masters[0]
1005 details['ns_servers'] = '\n'.join(['\tNS\t%s.' % x for x in dns_masters])
1006
1007 dns_header = '''
1008$TTL 3h
1009%(zone)s. SOA %(master)s. beheer.lijst.wirelessleiden.nl. ( %(serial)s 1d 12h 1w 3h )
1010 ; Serial, Refresh, Retry, Expire, Neg. cache TTL
1011
1012%(ns_servers)s
1013 \n'''
1014
1015
1016 if not os.path.isdir(output_dir):
1017 os.makedirs(output_dir)
1018 details['zone'] = 'wleiden.net'
1019 f = open(os.path.join(output_dir,"db." + details['zone']), "w")
1020 f.write(dns_header % details)
1021
1022 for host,ip in wleiden_zone.iteritems():
1023 if valid_addr(ip):
1024 f.write("%s.wleiden.net. IN A %s \n" % (host.lower(), ip))
1025 for source,dest in wleiden_cname.iteritems():
1026 f.write("%s.wleiden.net. IN CNAME %s.wleiden.net.\n" % (source.lower(), dest.lower()))
1027 for source, dest in wleiden_raw.iteritems():
1028 f.write("%s.wleiden.net. %s\n" % (source, dest))
1029 f.close()
1030
1031 # Create whole bunch of specific sub arpa zones. To keep it compliant
1032 for s in range(16,32):
1033 details['zone'] = '%i.172.in-addr.arpa' % s
1034 f = open(os.path.join(output_dir,"db." + details['zone']), "w")
1035 f.write(dns_header % details)
1036
1037 #XXX: Not effient, fix to proper data structure and do checks at other
1038 # stages
1039 for host,ip in wleiden_zone.iteritems():
1040 if valid_addr(ip):
1041 if int(ip.split('.')[1]) == s:
1042 rev_ip = '.'.join(reversed(ip.split('.')))
1043 f.write("%s.in-addr.arpa. IN PTR %s.wleiden.net.\n" % (rev_ip.lower(), host.lower()))
1044 f.close()
1045
1046
1047def usage():
1048 print """Usage: %(prog)s <argument>
1049Argument:
1050\tstandalone [port] = Run configurator webserver [8000]
1051\tdns [outputdir] = Generate BIND compliant zone files in dns [./dns]
1052\tfull-export = Generate yaml export script for heatmap.
1053\tstatic [outputdir] = Generate all config files and store on disk
1054\t with format ./<outputdir>/%%NODE%%/%%FILE%% [./static]
1055\ttest <node> <file> = Receive output of CGI script.
1056\tlist <status> <items> = List systems which have certain status
1057
1058Arguments:
1059\t<node> = NodeName (example: HybridRick)
1060\t<file> = %(files)s
1061\t<status> = all|up|down|planned
1062\t<items> = systems|nodes|proxies
1063
1064NOTE FOR DEVELOPERS; you can test your changes like this:
1065 BEFORE any changes in this code:
1066 $ ./gformat.py static /tmp/pre
1067 AFTER the changes:
1068 $ ./gformat.py static /tmp/post
1069 VIEW differences and VERIFY all are OK:
1070 $ diff -urI 'Generated' -r /tmp/pre /tmp/post
1071""" % { 'prog' : sys.argv[0], 'files' : '|'.join(files) }
1072 exit(0)
1073
1074
1075def is_text_request():
1076 """ Find out whether we are calling from the CLI or any text based CLI utility """
1077 try:
1078 return os.environ['HTTP_USER_AGENT'].split()[0] in ['curl', 'fetch', 'wget']
1079 except KeyError:
1080 return True
1081
1082def switchFormat(setting):
1083 if setting:
1084 return "YES"
1085 else:
1086 return "NO"
1087
1088def main():
1089 """Hard working sub"""
1090 # Allow easy hacking using the CLI
1091 if not os.environ.has_key('PATH_INFO'):
1092 if len(sys.argv) < 2:
1093 usage()
1094
1095 if sys.argv[1] == "standalone":
1096 import SocketServer
1097 import CGIHTTPServer
1098 # Hop to the right working directory.
1099 os.chdir(os.path.dirname(__file__))
1100 try:
1101 PORT = int(sys.argv[2])
1102 except (IndexError,ValueError):
1103 PORT = 8000
1104
1105 class MyCGIHTTPRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
1106 """ Serve this CGI from the root of the webserver """
1107 def is_cgi(self):
1108 if "favicon" in self.path:
1109 return False
1110
1111 self.cgi_info = (os.path.basename(__file__), self.path)
1112 self.path = ''
1113 return True
1114 handler = MyCGIHTTPRequestHandler
1115 SocketServer.TCPServer.allow_reuse_address = True
1116 httpd = SocketServer.TCPServer(("", PORT), handler)
1117 httpd.server_name = 'localhost'
1118 httpd.server_port = PORT
1119
1120 logger.info("serving at port %s", PORT)
1121 try:
1122 httpd.serve_forever()
1123 except KeyboardInterrupt:
1124 httpd.shutdown()
1125 logger.info("All done goodbye")
1126 elif sys.argv[1] == "test":
1127 os.environ['PATH_INFO'] = "/".join(sys.argv[2:])
1128 os.environ['SCRIPT_NAME'] = __file__
1129 process_cgi_request()
1130 elif sys.argv[1] == "static":
1131 items = dict()
1132 items['output_dir'] = sys.argv[2] if len(sys.argv) > 2 else "./static"
1133 for node in get_hostlist():
1134 items['node'] = node
1135 items['wdir'] = "%(output_dir)s/%(node)s" % items
1136 if not os.path.isdir(items['wdir']):
1137 os.makedirs(items['wdir'])
1138 datadump = get_yaml(node)
1139 for config in files:
1140 items['config'] = config
1141 logger.info("## Generating %(node)s %(config)s" % items)
1142 f = open("%(wdir)s/%(config)s" % items, "w")
1143 f.write(generate_config(node, config, datadump))
1144 f.close()
1145 elif sys.argv[1] == "wind-export":
1146 items = dict()
1147 for node in get_hostlist():
1148 datadump = get_yaml(node)
1149 sql = """INSERT IGNORE INTO nodes (name, name_ns, longitude, latitude)
1150 VALUES ('%(nodename)s', '%(nodename)s', %(latitude)s, %(longitude)s);""" % datadump;
1151 sql = """INSERT IGNORE INTO users_nodes (user_id, node_id, owner)
1152 VALUES (
1153 (SELECT id FROM users WHERE username = 'rvdzwet'),
1154 (SELECT id FROM nodes WHERE name = '%(nodename)s'),
1155 'Y');""" % datadump
1156 #for config in files:
1157 # items['config'] = config
1158 # print "## Generating %(node)s %(config)s" % items
1159 # f = open("%(wdir)s/%(config)s" % items, "w")
1160 # f.write(generate_config(node, config, datadump))
1161 # f.close()
1162 for node in get_hostlist():
1163 datadump = get_yaml(node)
1164 for iface_key in sorted([elem for elem in datadump.keys() if elem.startswith('iface_')]):
1165 ifacedump = datadump[iface_key]
1166 if ifacedump.has_key('mode') and ifacedump['mode'] == 'ap-wds':
1167 ifacedump['nodename'] = datadump['nodename']
1168 if not ifacedump.has_key('channel') or not ifacedump['channel']:
1169 ifacedump['channel'] = 0
1170 sql = """INSERT INTO links (node_id, type, ssid, protocol, channel, status)
1171 VALUES ((SELECT id FROM nodes WHERE name = '%(nodename)s'), 'ap',
1172 '%(ssid)s', 'IEEE 802.11b', %(channel)s, 'active');""" % ifacedump
1173 elif sys.argv[1] == "full-export":
1174 hosts = {}
1175 for node in get_hostlist():
1176 datadump = get_yaml(node)
1177 hosts[datadump['nodename']] = datadump
1178 print yaml.dump(hosts)
1179
1180 elif sys.argv[1] == "dns":
1181 make_dns(sys.argv[2] if len(sys.argv) > 2 else 'dns', 'external' in sys.argv)
1182 elif sys.argv[1] == "cleanup":
1183 # First generate all datadumps
1184 datadumps = dict()
1185 for host in get_hostlist():
1186 logger.info("# Processing: %s", host)
1187 # Set some boring default values
1188 datadump = { 'board' : 'UNKNOWN' }
1189 datadump.update(get_yaml(host))
1190 datadumps[datadump['autogen_realname']] = datadump
1191
1192
1193 for host,datadump in datadumps.iteritems():
1194 # Convert all yes and no to boolean values
1195 def fix_boolean(dump):
1196 for key in dump.keys():
1197 if type(dump[key]) == dict:
1198 dump[key] = fix_boolean(dump[key])
1199 elif str(dump[key]).lower() in ["yes", "true"]:
1200 dump[key] = True
1201 elif str(dump[key]).lower() in ["no", "false"]:
1202 # Compass richting no (Noord Oost) is valid input
1203 if key != "compass": dump[key] = False
1204 return dump
1205 datadump = fix_boolean(datadump)
1206
1207 if datadump['rdnap_x'] and datadump['rdnap_y']:
1208 datadump['latitude'], datadump['longitude'] = rdnap.rd2etrs(datadump['rdnap_x'], datadump['rdnap_y'])
1209 elif datadump['latitude'] and datadump['longitude']:
1210 datadump['rdnap_x'], datadump['rdnap_y'] = rdnap.etrs2rd(datadump['latitude'], datadump['longitude'])
1211
1212 if datadump['nodename'].startswith('Proxy'):
1213 datadump['nodename'] = datadump['nodename'].lower()
1214
1215 for iface_key in datadump['autogen_iface_keys']:
1216 # Wireless Leiden SSID have an consistent lowercase/uppercase
1217 if datadump[iface_key].has_key('ssid'):
1218 ssid = datadump[iface_key]['ssid']
1219 prefix = 'ap-WirelessLeiden-'
1220 if ssid.lower().startswith(prefix.lower()):
1221 datadump[iface_key]['ssid'] = prefix + ssid[len(prefix)].upper() + ssid[len(prefix) + 1:]
1222 if datadump[iface_key].has_key('ns_ip') and not datadump[iface_key].has_key('mode'):
1223 datadump[iface_key]['mode'] = 'autogen-FIXME'
1224 if not datadump[iface_key].has_key('desc'):
1225 datadump[iface_key]['desc'] = 'autogen-FIXME'
1226 store_yaml(datadump)
1227 elif sys.argv[1] == "list":
1228 if len(sys.argv) < 4 or not sys.argv[2] in ["up", "down", "planned", "all"]:
1229 usage()
1230 if sys.argv[3] == "nodes":
1231 systems = get_nodelist()
1232 elif sys.argv[3] == "proxies":
1233 systems = get_proxylist()
1234 elif sys.argv[3] == "systems":
1235 systems = get_hostlist()
1236 else:
1237 usage()
1238 for system in systems:
1239 datadump = get_yaml(system)
1240 if sys.argv[2] == "all":
1241 print system
1242 elif datadump['status'] == sys.argv[2]:
1243 print system
1244 elif sys.argv[1] == "create":
1245 if sys.argv[2] == "network.kml":
1246 print make_network_kml.make_graph()
1247 else:
1248 usage()
1249 usage()
1250 else:
1251 # Do not enable debugging for config requests as it highly clutters the output
1252 if not is_text_request():
1253 cgitb.enable()
1254 process_cgi_request()
1255
1256
1257if __name__ == "__main__":
1258 main()
Note: See TracBrowser for help on using the repository browser.