source: genesis/tools/gformat.py@ 10319

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

Make sure proxy reflects the (lowercase) directory name.

  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 34.7 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 10319 2012-04-01 20:10:30Z rick $'
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\n
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 # Write DHCLIENT entry
426 if ifacedump.has_key('dhcpclient') and ifacedump['dhcpclient']:
427 output += "ifconfig_%s='SYNCDHCP'" % ifacedump['autogen_ifname']
428
429 # Add interface IP to list
430 item = (ifacedump['ip'], ifacedump['desc'])
431 if addrs_list.has_key(ifname):
432 addrs_list[ifname].append(item)
433 else:
434 addrs_list[ifname] = [item]
435
436 # Alias only needs IP assignment for now, this might change if we
437 # are going to use virtual accesspoints
438 if "alias" in iface_key:
439 continue
440
441 # XXX: Might want to deduct type directly from interface name
442 if ifacedump['type'] in ['11a', '11b', '11g', 'wireless']:
443 # Default to station (client) mode
444 ifacedump['wlanmode'] = "sta"
445 if ifacedump['mode'] in ['master', 'master-wds', 'ap', 'ap-wds']:
446 ifacedump['wlanmode'] = "ap"
447 # Default to 802.11b mode
448 ifacedump['mode'] = '11b'
449 if ifacedump['type'] in ['11a', '11b' '11g']:
450 ifacedump['mode'] = ifacedump['type']
451
452 if not ifacedump.has_key('channel'):
453 if ifacedump['type'] == '11a':
454 ifacedump['channel'] = 36
455 else:
456 ifacedump['channel'] = 1
457
458 # Allow special hacks at the back like wds and stuff
459 if not ifacedump.has_key('extra'):
460 ifacedump['extra'] = 'regdomain ETSI country NL'
461
462 output += "wlans_%(interface)s='%(autogen_ifname)s'\n" % ifacedump
463 output += ("create_args_%(autogen_ifname)s='wlanmode %(wlanmode)s mode " +\
464 "%(mode)s ssid %(ssid)s %(extra)s channel %(channel)s'\n") % ifacedump
465
466 elif ifacedump['type'] in ['ethernet', 'eth']:
467 # No special config needed besides IP
468 pass
469 else:
470 assert False, "Unknown type " + ifacedump['type']
471
472 # Print IP address which needs to be assigned over here
473 output += "\n"
474 for iface,addrs in sorted(addrs_list.iteritems()):
475 for addr, comment in sorted(addrs,key=lambda x: parseaddr(x[0].split('/')[0])):
476 output += "# %s || %s || %s\n" % (iface, addr, comment)
477 output += "ipv4_addrs_%s='%s'\n\n" % (iface, " ".join([x[0] for x in addrs]))
478
479 return output
480
481
482
483def get_yaml(item):
484 """ Get configuration yaml for 'item'"""
485 gfile = os.path.join(NODE_DIR,item,'wleiden.yaml')
486
487 f = open(gfile, 'r')
488 datadump = yaml.load(f,Loader=Loader)
489 f.close()
490
491 # Preformat certain needed variables for formatting and push those into special object
492 datadump['autogen_iface_keys'] = get_interface_keys(datadump)
493
494 wlan_count=0
495 for key in datadump['autogen_iface_keys']:
496 if datadump[key]['type'] in ['11a', '11b', '11g', 'wireless']:
497 datadump[key]['autogen_ifname'] = 'wlan%i' % wlan_count
498 wlan_count += 1
499 else:
500 datadump[key]['autogen_ifname'] = datadump[key]['interface'].split(':')[0]
501
502 dhcp_interfaces = [datadump[key]['autogen_ifname'] for key in datadump['autogen_iface_keys'] if datadump[key]['dhcp'] != 'no']
503 datadump['autogen_dhcp_interfaces'] = ' '.join(dhcp_interfaces)
504 datadump['autogen_item'] = item
505 datadump['autogen_fqdn'] = get_fqdn(datadump)
506
507 datadump['autogen_domain'] = datadump['domain'] if datadump.has_key('domain') else 'wleiden.net'
508 datadump['autogen_nodename_lower'] = datadump['nodename'].lower()
509 return datadump
510
511
512def store_yaml(datadump, header=False):
513 """ Store configuration yaml for 'item'"""
514 item = datadump['autogen_item']
515 gfile = os.path.join(NODE_DIR,item,'wleiden.yaml')
516
517 f = open(gfile, 'w')
518 f.write(generate_wleiden_yaml(datadump, header))
519 f.close()
520
521
522
523def get_all_configs():
524 """ Get dict with key 'host' with all configs present """
525 configs = dict()
526 for host in get_hostlist():
527 datadump = get_yaml(host)
528 configs[host] = datadump
529 return configs
530
531
532def get_interface_keys(config):
533 """ Quick hack to get all interface keys, later stage convert this to a iterator """
534 return sorted([elem for elem in config.keys() if (elem.startswith('iface_') and not "lo0" in elem)])
535
536
537def get_used_ips(configs):
538 """ Return array of all IPs used in config files"""
539 ip_list = []
540 for config in configs:
541 ip_list.append(config['masterip'])
542 for iface_key in get_interface_keys(config):
543 l = config[iface_key]['ip']
544 addr, mask = l.split('/')
545 # Special case do not process
546 if valid_addr(addr):
547 ip_list.append(addr)
548 else:
549 logger.error("## IP '%s' in '%s' not valid" % (addr, config['nodename']))
550 return sorted(ip_list)
551
552
553
554def generate_resolv_conf(datadump):
555 """ Generate configuration file '/etc/resolv.conf' """
556 output = generate_header("#");
557 output += """\
558search wleiden.net
559"""
560 if datadump['nodetype'] == 'Proxy':
561 output += """\
562# Try local (cache) first
563nameserver 127.0.0.1
564nameserver 8.8.8.8 # Google Public NameServer
565nameserver 8.8.4.4 # Google Public NameServer
566"""
567 elif datadump['nodetype'] == 'Hybrid':
568 for proxy in get_proxylist():
569 proxy_ip = get_yaml(proxy)['masterip']
570 output += "nameserver %-15s # %s\n" % (proxy_ip, proxy)
571 output += """\
572nameserver 8.8.8.8 # Google Public NameServer
573nameserver 8.8.4.4 # Google Public NameServer
574"""
575 else:
576 output += """\
577# Try local (cache) first
578nameserver 127.0.0.1
579
580# Proxies are recursive nameservers
581# needs to be in resolv.conf for dnsmasq as well
582""" % datadump
583 for proxy in get_proxylist():
584 proxy_ip = get_yaml(proxy)['masterip']
585 output += "nameserver %-15s # %s\n" % (proxy_ip, proxy)
586
587 return output
588
589def generate_motd(datadump):
590 """ Generate configuration file '/etc/motd' """
591 output = """\
592FreeBSD 9.0-RELEASE (kernel.wleiden) #0 r230587: Sun Jan 29 17:09:57 CET 2012
593
594 WWW: %(autogen_fqdn)s.wleiden.net - http://www.wirelessleiden.nl
595 Loc: %(location)s
596
597Interlinks:
598""" % datadump
599
600 # XXX: This is a hacky way to get the required data
601 for line in generate_rc_conf_local(datadump).split('\n'):
602 if '||' in line and not line[1:].split()[0] in ['lo0', 'ath0'] :
603 output += " - %s \n" % line[1:]
604 output += """\
605Attached bridges:
606"""
607 for iface_key in datadump['autogen_iface_keys']:
608 ifacedump = datadump[iface_key]
609 if ifacedump.has_key('ns_ip'):
610 output += " - %(interface)s || %(mode)s || %(ns_ip)s\n" % ifacedump
611
612 return output
613
614
615def format_yaml_value(value):
616 """ Get yaml value in right syntax for outputting """
617 if isinstance(value,str):
618 output = '"%s"' % value
619 else:
620 output = value
621 return output
622
623
624
625def format_wleiden_yaml(datadump):
626 """ Special formatting to ensure it is editable"""
627 output = "# Genesis config yaml style\n"
628 output += "# vim:ts=2:et:sw=2:ai\n"
629 output += "#\n"
630 iface_keys = [elem for elem in datadump.keys() if elem.startswith('iface_')]
631 for key in sorted(set(datadump.keys()) - set(iface_keys)):
632 output += "%-10s: %s\n" % (key, format_yaml_value(datadump[key]))
633
634 output += "\n\n"
635
636 key_order = [ 'comment', 'interface', 'ip', 'desc', 'sdesc', 'mode', 'type',
637 'extra_type', 'channel', 'ssid', 'dhcp' ]
638
639 for iface_key in sorted(iface_keys):
640 output += "%s:\n" % iface_key
641 for key in key_order + list(sorted(set(datadump[iface_key].keys()) - set(key_order))):
642 if datadump[iface_key].has_key(key):
643 output += " %-11s: %s\n" % (key, format_yaml_value(datadump[iface_key][key]))
644 output += "\n\n"
645
646 return output
647
648
649
650def generate_wleiden_yaml(datadump, header=True):
651 """ Generate (petty) version of wleiden.yaml"""
652 for key in datadump.keys():
653 if key.startswith('autogen_'):
654 del datadump[key]
655 # Interface autogen cleanups
656 elif type(datadump[key]) == dict:
657 for key2 in datadump[key].keys():
658 if key2.startswith('autogen_'):
659 del datadump[key][key2]
660
661 output = generate_header("#") if header else ''
662 output += format_wleiden_yaml(datadump)
663 return output
664
665
666def generate_yaml(datadump):
667 return generate_config(datadump['nodename'], "wleiden.yaml", datadump)
668
669
670
671def generate_config(node, config, datadump=None):
672 """ Print configuration file 'config' of 'node' """
673 output = ""
674 try:
675 # Load config file
676 if datadump == None:
677 datadump = get_yaml(node)
678
679 if config == 'wleiden.yaml':
680 output += generate_wleiden_yaml(datadump)
681 elif config == 'authorized_keys':
682 f = open(os.path.join(NODE_DIR,"global_keys"), 'r')
683 output += f.read()
684 f.close()
685 elif config == 'dnsmasq.conf':
686 output += generate_dnsmasq_conf(datadump)
687 elif config == 'rc.conf.local':
688 output += generate_rc_conf_local(datadump)
689 elif config == 'resolv.conf':
690 output += generate_resolv_conf(datadump)
691 elif config == 'motd':
692 output += generate_motd(datadump)
693 else:
694 assert False, "Config not found!"
695 except IOError, e:
696 output += "[ERROR] Config file not found"
697 return output
698
699
700
701def process_cgi_request():
702 """ When calling from CGI """
703 # Update repository if requested
704 form = cgi.FieldStorage()
705 if form.getvalue("action") == "update":
706 print "Refresh: 5; url=."
707 print "Content-type:text/plain\r\n\r\n",
708 print "[INFO] Updating subverion, please wait..."
709 print subprocess.Popen(['svn', 'cleanup', "%s/.." % NODE_DIR], stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0],
710 print subprocess.Popen(['svn', 'up', "%s/.." % NODE_DIR], stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0],
711 print "[INFO] All done, redirecting in 5 seconds"
712 sys.exit(0)
713
714
715 base_uri = os.environ['PATH_INFO']
716 uri = base_uri.strip('/').split('/')
717
718 output = ""
719 if not uri[0]:
720 if is_text_request():
721 output += "Content-type:text/plain\r\n\r\n"
722 output += '\n'.join(get_hostlist())
723 else:
724 output += "Content-type:text/html\r\n\r\n"
725 output += generate_title(get_hostlist())
726 elif len(uri) == 1:
727 if is_text_request():
728 output += "Content-type:text/plain\r\n\r\n"
729 output += generate_node(uri[0])
730 else:
731 output += "Content-type:text/html\r\n\r\n"
732 output += generate_node_overview(uri[0])
733 elif len(uri) == 2:
734 output += "Content-type:text/plain\r\n\r\n"
735 output += generate_config(uri[0], uri[1])
736 else:
737 assert False, "Invalid option"
738 print output
739
740def get_fqdn(datadump):
741 # Proxy naming convention is special
742 if datadump['nodetype'] == 'Proxy':
743 fqdn = datadump['nodename']
744 else:
745 # By default the full name is listed and also a shortname CNAME for easy use.
746 fqdn = datadump['nodetype'] + datadump['nodename']
747 return(fqdn)
748
749
750
751def make_dns(output_dir = 'dns', external = False):
752 items = dict()
753
754 # hostname is key, IP is value
755 wleiden_zone = dict()
756 wleiden_cname = dict()
757
758 pool = dict()
759 for node in get_hostlist():
760 logger.info("Processing host %s", node)
761 datadump = get_yaml(node)
762
763 # Proxy naming convention is special
764 fqdn = get_fqdn(datadump)
765 if datadump['nodetype'] == 'CNode':
766 wleiden_cname[datadump['nodename']] = fqdn
767
768 wleiden_zone[fqdn] = datadump['masterip']
769
770 # Hacking to get proper DHCP IPs and hostnames
771 for iface_key in get_interface_keys(datadump):
772 iface_name = datadump[iface_key]['interface'].replace(':',"-alias-")
773 (ip, netmask) = datadump[iface_key]['ip'].split('/')
774 try:
775 (dhcp_start, dhcp_stop) = datadump[iface_key]['dhcp'].split('-')
776 datadump[iface_key]['subnet'] = netmask2subnet(netmask)
777 dhcp_part = ".".join(ip.split('.')[0:3])
778 if ip != datadump['masterip']:
779 wleiden_zone["dhcp-gateway-%s.%s" % (iface_name, fqdn)] = ip
780 for i in range(int(dhcp_start), int(dhcp_stop) + 1):
781 wleiden_zone["dhcp-%s-%s.%s" % (i, iface_name, fqdn)] = "%s.%s" % (dhcp_part, i)
782 except (AttributeError, ValueError):
783 # First push it into a pool, to indentify the counter-part later on
784 addr = parseaddr(ip)
785 netmask = int(netmask)
786 addr = addr & ~((1 << (32 - netmask)) - 1)
787 if pool.has_key(addr):
788 pool[addr] += [(iface_name, fqdn, ip)]
789 else:
790 pool[addr] = [(iface_name, fqdn, ip)]
791 continue
792
793
794 def pool_to_name(node, pool_members):
795 """Convert the joined name to a usable pool name"""
796
797 # Get rid of the own entry
798 pool_members = list(set(pool_members) - set([fqdn]))
799
800 target = oldname = ''
801 for node in sorted(pool_members):
802 (name, number) = re.match('^([A-Za-z]+)([0-9]*)$',node).group(1,2)
803 target += "-" + number if name == oldname else "-" + node if target else node
804 oldname = name
805
806 return target
807
808
809 # WL uses an /29 to configure an interface. IP's are ordered like this:
810 # MasterA (.1) -- DeviceA (.2) <<>> DeviceB (.3) --- SlaveB (.4)
811
812 sn = lambda x: re.sub(r'(?i)^cnode','',x)
813
814 # Automatic naming convention of interlinks namely 2 + remote.lower()
815 for (key,value) in pool.iteritems():
816 # Make sure they are sorted from low-ip to high-ip
817 value = sorted(value, key=lambda x: parseaddr(x[2]))
818
819 if len(value) == 1:
820 (iface_name, fqdn, ip) = value[0]
821 wleiden_zone["2unused-%s.%s" % (iface_name, fqdn)] = ip
822
823 # Device DNS names
824 if 'cnode' in fqdn.lower():
825 wleiden_zone["d-at-%s.%s" % (iface_name, fqdn)] = showaddr(parseaddr(ip) + 1)
826 wleiden_cname["d-at-%s.%s" % (iface_name,sn(fqdn))] = "d-at-%s.%s" % (iface_name, fqdn)
827
828 elif len(value) == 2:
829 (a_iface_name, a_fqdn, a_ip) = value[0]
830 (b_iface_name, b_fqdn, b_ip) = value[1]
831 wleiden_zone["2%s.%s" % (b_fqdn,a_fqdn)] = a_ip
832 wleiden_zone["2%s.%s" % (a_fqdn,b_fqdn)] = b_ip
833
834 # Device DNS names
835 if 'cnode' in a_fqdn.lower() and 'cnode' in b_fqdn.lower():
836 wleiden_zone["d-at-%s.%s" % (a_iface_name, a_fqdn)] = showaddr(parseaddr(a_ip) + 1)
837 wleiden_zone["d-at-%s.%s" % (b_iface_name, b_fqdn)] = showaddr(parseaddr(b_ip) - 1)
838 wleiden_cname["d-at-%s.%s" % (a_iface_name,sn(a_fqdn))] = "d-at-%s.%s" % (a_iface_name, a_fqdn)
839 wleiden_cname["d-at-%s.%s" % (b_iface_name,sn(b_fqdn))] = "d-at-%s.%s" % (b_iface_name, b_fqdn)
840 wleiden_cname["d2%s.%s" % (sn(b_fqdn),sn(a_fqdn))] = "d-at-%s.%s" % (a_iface_name, a_fqdn)
841 wleiden_cname["d2%s.%s" % (sn(a_fqdn),sn(b_fqdn))] = "d-at-%s.%s" % (b_iface_name, b_fqdn)
842
843 else:
844 pool_members = [k[1] for k in value]
845 for item in value:
846 (iface_name, fqdn, ip) = item
847 pool_name = "2pool-" + showaddr(key).replace('.','-') + "-" + pool_to_name(fqdn,pool_members)
848 wleiden_zone["%s.%s" % (pool_name, fqdn)] = ip
849
850 # Include static DNS entries
851 # XXX: Should they override the autogenerated results?
852 # XXX: Convert input to yaml more useable.
853 # Format:
854 ##; this is a comment
855 ## roomburgh=CNodeRoomburgh1
856 ## apkerk1.CNodeVosko=172.17.176.8 ;this as well
857 dns = yaml.load(open(os.path.join(NODE_DIR,'../dns/staticDNS.yaml'),'r'))
858
859 # Hack to allow special entries, for development
860 wleiden_raw = dns['raw']
861 del dns['raw']
862
863 for comment, block in dns.iteritems():
864 for k,v in block.iteritems():
865 if valid_addr(v):
866 wleiden_zone[k] = v
867 else:
868 wleiden_cname[k] = v
869
870 details = dict()
871 # 24 updates a day allowed
872 details['serial'] = time.strftime('%Y%m%d%H')
873
874 if external:
875 dns_masters = ['siteview.wirelessleiden.nl', 'ns1.vanderzwet.net']
876 else:
877 dns_masters = ['sunny.wleiden.net']
878
879 details['master'] = dns_masters[0]
880 details['ns_servers'] = '\n'.join(['\tNS\t%s.' % x for x in dns_masters])
881
882 dns_header = '''
883$TTL 3h
884%(zone)s. SOA %(master)s. beheer.lijst.wirelessleiden.nl. ( %(serial)s 1d 12h 1w 3h )
885 ; Serial, Refresh, Retry, Expire, Neg. cache TTL
886
887%(ns_servers)s
888 \n'''
889
890
891 if not os.path.isdir(output_dir):
892 os.makedirs(output_dir)
893 details['zone'] = 'wleiden.net'
894 f = open(os.path.join(output_dir,"db." + details['zone']), "w")
895 f.write(dns_header % details)
896
897 for host,ip in wleiden_zone.iteritems():
898 if valid_addr(ip):
899 f.write("%s.wleiden.net. IN A %s \n" % (host.lower(), ip))
900 for source,dest in wleiden_cname.iteritems():
901 f.write("%s.wleiden.net. IN CNAME %s.wleiden.net.\n" % (source.lower(), dest.lower()))
902 for source, dest in wleiden_raw.iteritems():
903 f.write("%s.wleiden.net. %s\n" % (source, dest))
904 f.close()
905
906 # Create whole bunch of specific sub arpa zones. To keep it compliant
907 for s in range(16,32):
908 details['zone'] = '%i.172.in-addr.arpa' % s
909 f = open(os.path.join(output_dir,"db." + details['zone']), "w")
910 f.write(dns_header % details)
911
912 #XXX: Not effient, fix to proper data structure and do checks at other
913 # stages
914 for host,ip in wleiden_zone.iteritems():
915 if valid_addr(ip):
916 if int(ip.split('.')[1]) == s:
917 rev_ip = '.'.join(reversed(ip.split('.')))
918 f.write("%s.in-addr.arpa. IN PTR %s.wleiden.net.\n" % (rev_ip.lower(), host.lower()))
919 f.close()
920
921
922def usage():
923 print """Usage: %s <standalone [port] |test [test arguments]|static|dns>
924Examples:
925\tdns [outputdir] = Generate BIND compliant zone files in dns.
926\tstandalone = Run configurator webserver [default port=8000]
927\twind-export = Generate SQL import scripts for WIND database
928\tfull-export = Generate yaml export script for heatmap.
929\tstatic = Generate all config files and store on disk
930\t with format ./static/%%NODE%%/%%FILE%%
931\ttest CNodeRick dnsmasq.conf = Receive output of CGI script
932\t for arguments CNodeRick/dnsmasq.conf
933\tlist <all|nodes|proxies> = List systems which marked up.
934"""
935 exit(0)
936
937
938def is_text_request():
939 """ Find out whether we are calling from the CLI or any text based CLI utility """
940 try:
941 return os.environ['HTTP_USER_AGENT'].split()[0] in ['curl', 'fetch', 'wget']
942 except KeyError:
943 return True
944
945def main():
946 """Hard working sub"""
947 # Allow easy hacking using the CLI
948 if not os.environ.has_key('PATH_INFO'):
949 if len(sys.argv) < 2:
950 usage()
951
952 if sys.argv[1] == "standalone":
953 import SocketServer
954 import CGIHTTPServer
955 # Hop to the right working directory.
956 os.chdir(os.path.dirname(__file__))
957 try:
958 PORT = int(sys.argv[2])
959 except (IndexError,ValueError):
960 PORT = 8000
961
962 class MyCGIHTTPRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
963 """ Serve this CGI from the root of the webserver """
964 def is_cgi(self):
965 if "favicon" in self.path:
966 return False
967
968 self.cgi_info = (__file__, self.path)
969 self.path = ''
970 return True
971 handler = MyCGIHTTPRequestHandler
972 SocketServer.TCPServer.allow_reuse_address = True
973 httpd = SocketServer.TCPServer(("", PORT), handler)
974 httpd.server_name = 'localhost'
975 httpd.server_port = PORT
976
977 logger.info("serving at port %s", PORT)
978 try:
979 httpd.serve_forever()
980 except KeyboardInterrupt:
981 httpd.shutdown()
982 logger.info("All done goodbye")
983 elif sys.argv[1] == "test":
984 os.environ['PATH_INFO'] = "/".join(sys.argv[2:])
985 os.environ['SCRIPT_NAME'] = __file__
986 process_cgi_request()
987 elif sys.argv[1] == "unit-test":
988 os.environ['SCRIPT_NAME'] = __file__
989 for host in get_hostlist():
990 for outfile in files:
991 os.environ['PATH_INFO'] = "/".join([host,outfile])
992 try:
993 process_cgi_request()
994 except Exception:
995 print "# ERROR: %s" % os.environ['PATH_INFO']
996 raise
997
998
999 elif sys.argv[1] == "static":
1000 items = dict()
1001 for node in get_hostlist():
1002 items['node'] = node
1003 items['wdir'] = "./static/%(node)s" % items
1004 if not os.path.isdir(items['wdir']):
1005 os.makedirs(items['wdir'])
1006 datadump = get_yaml(node)
1007 for config in files:
1008 items['config'] = config
1009 logger.info("## Generating %(node)s %(config)s" % items)
1010 f = open("%(wdir)s/%(config)s" % items, "w")
1011 f.write(generate_config(node, config, datadump))
1012 f.close()
1013 elif sys.argv[1] == "wind-export":
1014 items = dict()
1015 for node in get_hostlist():
1016 datadump = get_yaml(node)
1017 sql = """INSERT IGNORE INTO nodes (name, name_ns, longitude, latitude)
1018 VALUES ('%(nodename)s', '%(nodename)s', %(latitude)s, %(longitude)s);""" % datadump;
1019 sql = """INSERT IGNORE INTO users_nodes (user_id, node_id, owner)
1020 VALUES (
1021 (SELECT id FROM users WHERE username = 'rvdzwet'),
1022 (SELECT id FROM nodes WHERE name = '%(nodename)s'),
1023 'Y');""" % datadump
1024 #for config in files:
1025 # items['config'] = config
1026 # print "## Generating %(node)s %(config)s" % items
1027 # f = open("%(wdir)s/%(config)s" % items, "w")
1028 # f.write(generate_config(node, config, datadump))
1029 # f.close()
1030 for node in get_hostlist():
1031 datadump = get_yaml(node)
1032 for iface_key in sorted([elem for elem in datadump.keys() if elem.startswith('iface_')]):
1033 ifacedump = datadump[iface_key]
1034 if ifacedump.has_key('mode') and ifacedump['mode'] == 'ap-wds':
1035 ifacedump['nodename'] = datadump['nodename']
1036 if not ifacedump.has_key('channel') or not ifacedump['channel']:
1037 ifacedump['channel'] = 0
1038 sql = """INSERT INTO links (node_id, type, ssid, protocol, channel, status)
1039 VALUES ((SELECT id FROM nodes WHERE name = '%(nodename)s'), 'ap',
1040 '%(ssid)s', 'IEEE 802.11b', %(channel)s, 'active');""" % ifacedump
1041 elif sys.argv[1] == "full-export":
1042 hosts = {}
1043 for node in get_hostlist():
1044 datadump = get_yaml(node)
1045 hosts[datadump['nodename']] = datadump
1046 print yaml.dump(hosts)
1047
1048 elif sys.argv[1] == "dns":
1049 make_dns(sys.argv[2] if len(sys.argv) > 2 else 'dns', 'external' in sys.argv)
1050 elif sys.argv[1] == "cleanup":
1051 # First generate all datadumps
1052 datadumps = dict()
1053 for host in get_hostlist():
1054 logger.info("# Processing: %s", host)
1055 datadump = get_yaml(host)
1056 datadumps[get_fqdn(datadump)] = datadump
1057
1058 for host,datadump in datadumps.iteritems():
1059 datadump['latitude'], datadump['longitude'] = rdnap.rd2etrs(datadump['rdnap_x'], datadump['rdnap_y'])
1060 if datadump['nodename'].startswith('Proxy'):
1061 datadump['nodename'] = datadump['nodename'].lower()
1062
1063 for iface_key in datadump['autogen_iface_keys']:
1064 # Wireless Leiden SSID have an consistent lowercase/uppercase
1065 if datadump[iface_key].has_key('ssid'):
1066 ssid = datadump[iface_key]['ssid']
1067 prefix = 'ap-WirelessLeiden-'
1068 if ssid.lower().startswith(prefix.lower()):
1069 datadump[iface_key]['ssid'] = prefix + ssid[len(prefix)].upper() + ssid[len(prefix) + 1:]
1070 if datadump[iface_key].has_key('ns_ip') and not datadump[iface_key].has_key('mode'):
1071 datadump[iface_key]['mode'] = 'autogen-FIXME'
1072 if not datadump[iface_key].has_key('desc'):
1073 datadump[iface_key]['desc'] = 'autogen-FIXME'
1074 store_yaml(datadump)
1075 elif sys.argv[1] == "list":
1076 if sys.argv[2] == "nodes":
1077 systems = get_nodelist()
1078 elif sys.argv[2] == "proxies":
1079 systems = get_proxylist()
1080 elif sys.argv[2] == "all":
1081 systems = get_hostlist()
1082 else:
1083 usage()
1084 for system in systems:
1085 datadump = get_yaml(system)
1086 if datadump['status'] == "up":
1087 print system
1088 else:
1089 usage()
1090 else:
1091 # Do not enable debugging for config requests as it highly clutters the output
1092 if not is_text_request():
1093 cgitb.enable()
1094 process_cgi_request()
1095
1096
1097if __name__ == "__main__":
1098 main()
Note: See TracBrowser for help on using the repository browser.