source: genesis/tools/gformat.py@ 10311

Last change on this file since 10311 was 10310, checked in by richardvm, 14 years ago

moved interlal route out of the section which is only enable if ileiden is enabled. Reason: in case lvrouted is having troubles

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