source: genesis/tools/gformat.py@ 10365

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

Ugly hack needed to fix the odd behaviour around proxy naming.

This is fixed the moment only names will be used.

Related-To beheer#174

  • Property svn:executable set to *
  • Property svn:keywords set to Id
File size: 34.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
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 10365 2012-04-07 10:01:02Z 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) + sorted([os.path.basename(x) for x in glob.glob("%s/Proxy*" % NODE_DIR)])
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, as the proxy name is also included in
742 # the nodename, when it comes to the numbered proxies.
743 if datadump['nodetype'] == 'Proxy':
744 fqdn = datadump['nodetype'] + datadump['nodename'].replace('proxy','')
745 else:
746 # By default the full name is listed and also a shortname CNAME for easy use.
747 fqdn = datadump['nodetype'] + datadump['nodename']
748 return(fqdn)
749
750
751
752def make_dns(output_dir = 'dns', external = False):
753 items = dict()
754
755 # hostname is key, IP is value
756 wleiden_zone = dict()
757 wleiden_cname = dict()
758
759 pool = dict()
760 for node in get_hostlist():
761 logger.info("Processing host %s", node)
762 datadump = get_yaml(node)
763
764 # Proxy naming convention is special
765 fqdn = get_fqdn(datadump)
766 if datadump['nodetype'] == 'CNode':
767 wleiden_cname[datadump['nodename']] = fqdn
768
769 wleiden_zone[fqdn] = datadump['masterip']
770
771 # Hacking to get proper DHCP IPs and hostnames
772 for iface_key in get_interface_keys(datadump):
773 iface_name = datadump[iface_key]['interface'].replace(':',"-alias-")
774 (ip, netmask) = datadump[iface_key]['ip'].split('/')
775 try:
776 (dhcp_start, dhcp_stop) = datadump[iface_key]['dhcp'].split('-')
777 datadump[iface_key]['subnet'] = netmask2subnet(netmask)
778 dhcp_part = ".".join(ip.split('.')[0:3])
779 if ip != datadump['masterip']:
780 wleiden_zone["dhcp-gateway-%s.%s" % (iface_name, fqdn)] = ip
781 for i in range(int(dhcp_start), int(dhcp_stop) + 1):
782 wleiden_zone["dhcp-%s-%s.%s" % (i, iface_name, fqdn)] = "%s.%s" % (dhcp_part, i)
783 except (AttributeError, ValueError):
784 # First push it into a pool, to indentify the counter-part later on
785 addr = parseaddr(ip)
786 netmask = int(netmask)
787 addr = addr & ~((1 << (32 - netmask)) - 1)
788 if pool.has_key(addr):
789 pool[addr] += [(iface_name, fqdn, ip)]
790 else:
791 pool[addr] = [(iface_name, fqdn, ip)]
792 continue
793
794
795 def pool_to_name(node, pool_members):
796 """Convert the joined name to a usable pool name"""
797
798 # Get rid of the own entry
799 pool_members = list(set(pool_members) - set([fqdn]))
800
801 target = oldname = ''
802 for node in sorted(pool_members):
803 (name, number) = re.match('^([A-Za-z]+)([0-9]*)$',node).group(1,2)
804 target += "-" + number if name == oldname else "-" + node if target else node
805 oldname = name
806
807 return target
808
809
810 # WL uses an /29 to configure an interface. IP's are ordered like this:
811 # MasterA (.1) -- DeviceA (.2) <<>> DeviceB (.3) --- SlaveB (.4)
812
813 sn = lambda x: re.sub(r'(?i)^cnode','',x)
814
815 # Automatic naming convention of interlinks namely 2 + remote.lower()
816 for (key,value) in pool.iteritems():
817 # Make sure they are sorted from low-ip to high-ip
818 value = sorted(value, key=lambda x: parseaddr(x[2]))
819
820 if len(value) == 1:
821 (iface_name, fqdn, ip) = value[0]
822 wleiden_zone["2unused-%s.%s" % (iface_name, fqdn)] = ip
823
824 # Device DNS names
825 if 'cnode' in fqdn.lower():
826 wleiden_zone["d-at-%s.%s" % (iface_name, fqdn)] = showaddr(parseaddr(ip) + 1)
827 wleiden_cname["d-at-%s.%s" % (iface_name,sn(fqdn))] = "d-at-%s.%s" % (iface_name, fqdn)
828
829 elif len(value) == 2:
830 (a_iface_name, a_fqdn, a_ip) = value[0]
831 (b_iface_name, b_fqdn, b_ip) = value[1]
832 wleiden_zone["2%s.%s" % (b_fqdn,a_fqdn)] = a_ip
833 wleiden_zone["2%s.%s" % (a_fqdn,b_fqdn)] = b_ip
834
835 # Device DNS names
836 if 'cnode' in a_fqdn.lower() and 'cnode' in b_fqdn.lower():
837 wleiden_zone["d-at-%s.%s" % (a_iface_name, a_fqdn)] = showaddr(parseaddr(a_ip) + 1)
838 wleiden_zone["d-at-%s.%s" % (b_iface_name, b_fqdn)] = showaddr(parseaddr(b_ip) - 1)
839 wleiden_cname["d-at-%s.%s" % (a_iface_name,sn(a_fqdn))] = "d-at-%s.%s" % (a_iface_name, a_fqdn)
840 wleiden_cname["d-at-%s.%s" % (b_iface_name,sn(b_fqdn))] = "d-at-%s.%s" % (b_iface_name, b_fqdn)
841 wleiden_cname["d2%s.%s" % (sn(b_fqdn),sn(a_fqdn))] = "d-at-%s.%s" % (a_iface_name, a_fqdn)
842 wleiden_cname["d2%s.%s" % (sn(a_fqdn),sn(b_fqdn))] = "d-at-%s.%s" % (b_iface_name, b_fqdn)
843
844 else:
845 pool_members = [k[1] for k in value]
846 for item in value:
847 (iface_name, fqdn, ip) = item
848 pool_name = "2pool-" + showaddr(key).replace('.','-') + "-" + pool_to_name(fqdn,pool_members)
849 wleiden_zone["%s.%s" % (pool_name, fqdn)] = ip
850
851 # Include static DNS entries
852 # XXX: Should they override the autogenerated results?
853 # XXX: Convert input to yaml more useable.
854 # Format:
855 ##; this is a comment
856 ## roomburgh=CNodeRoomburgh1
857 ## apkerk1.CNodeVosko=172.17.176.8 ;this as well
858 dns = yaml.load(open(os.path.join(NODE_DIR,'../dns/staticDNS.yaml'),'r'))
859
860 # Hack to allow special entries, for development
861 wleiden_raw = dns['raw']
862 del dns['raw']
863
864 for comment, block in dns.iteritems():
865 for k,v in block.iteritems():
866 if valid_addr(v):
867 wleiden_zone[k] = v
868 else:
869 wleiden_cname[k] = v
870
871 details = dict()
872 # 24 updates a day allowed
873 details['serial'] = time.strftime('%Y%m%d%H')
874
875 if external:
876 dns_masters = ['siteview.wirelessleiden.nl', 'ns1.vanderzwet.net']
877 else:
878 dns_masters = ['sunny.wleiden.net']
879
880 details['master'] = dns_masters[0]
881 details['ns_servers'] = '\n'.join(['\tNS\t%s.' % x for x in dns_masters])
882
883 dns_header = '''
884$TTL 3h
885%(zone)s. SOA %(master)s. beheer.lijst.wirelessleiden.nl. ( %(serial)s 1d 12h 1w 3h )
886 ; Serial, Refresh, Retry, Expire, Neg. cache TTL
887
888%(ns_servers)s
889 \n'''
890
891
892 if not os.path.isdir(output_dir):
893 os.makedirs(output_dir)
894 details['zone'] = 'wleiden.net'
895 f = open(os.path.join(output_dir,"db." + details['zone']), "w")
896 f.write(dns_header % details)
897
898 for host,ip in wleiden_zone.iteritems():
899 if valid_addr(ip):
900 f.write("%s.wleiden.net. IN A %s \n" % (host.lower(), ip))
901 for source,dest in wleiden_cname.iteritems():
902 f.write("%s.wleiden.net. IN CNAME %s.wleiden.net.\n" % (source.lower(), dest.lower()))
903 for source, dest in wleiden_raw.iteritems():
904 f.write("%s.wleiden.net. %s\n" % (source, dest))
905 f.close()
906
907 # Create whole bunch of specific sub arpa zones. To keep it compliant
908 for s in range(16,32):
909 details['zone'] = '%i.172.in-addr.arpa' % s
910 f = open(os.path.join(output_dir,"db." + details['zone']), "w")
911 f.write(dns_header % details)
912
913 #XXX: Not effient, fix to proper data structure and do checks at other
914 # stages
915 for host,ip in wleiden_zone.iteritems():
916 if valid_addr(ip):
917 if int(ip.split('.')[1]) == s:
918 rev_ip = '.'.join(reversed(ip.split('.')))
919 f.write("%s.in-addr.arpa. IN PTR %s.wleiden.net.\n" % (rev_ip.lower(), host.lower()))
920 f.close()
921
922
923def usage():
924 print """Usage: %s <standalone [port] |test [test arguments]|static|dns>
925Examples:
926\tdns [outputdir] = Generate BIND compliant zone files in dns.
927\tstandalone = Run configurator webserver [default port=8000]
928\twind-export = Generate SQL import scripts for WIND database
929\tfull-export = Generate yaml export script for heatmap.
930\tstatic = Generate all config files and store on disk
931\t with format ./static/%%NODE%%/%%FILE%%
932\ttest CNodeRick dnsmasq.conf = Receive output of CGI script
933\t for arguments CNodeRick/dnsmasq.conf
934\tlist <all|nodes|proxies> = List systems which marked up.
935"""
936 exit(0)
937
938
939def is_text_request():
940 """ Find out whether we are calling from the CLI or any text based CLI utility """
941 try:
942 return os.environ['HTTP_USER_AGENT'].split()[0] in ['curl', 'fetch', 'wget']
943 except KeyError:
944 return True
945
946def main():
947 """Hard working sub"""
948 # Allow easy hacking using the CLI
949 if not os.environ.has_key('PATH_INFO'):
950 if len(sys.argv) < 2:
951 usage()
952
953 if sys.argv[1] == "standalone":
954 import SocketServer
955 import CGIHTTPServer
956 # Hop to the right working directory.
957 os.chdir(os.path.dirname(__file__))
958 try:
959 PORT = int(sys.argv[2])
960 except (IndexError,ValueError):
961 PORT = 8000
962
963 class MyCGIHTTPRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler):
964 """ Serve this CGI from the root of the webserver """
965 def is_cgi(self):
966 if "favicon" in self.path:
967 return False
968
969 self.cgi_info = (os.path.basename(__file__), self.path)
970 self.path = ''
971 return True
972 handler = MyCGIHTTPRequestHandler
973 SocketServer.TCPServer.allow_reuse_address = True
974 httpd = SocketServer.TCPServer(("", PORT), handler)
975 httpd.server_name = 'localhost'
976 httpd.server_port = PORT
977
978 logger.info("serving at port %s", PORT)
979 try:
980 httpd.serve_forever()
981 except KeyboardInterrupt:
982 httpd.shutdown()
983 logger.info("All done goodbye")
984 elif sys.argv[1] == "test":
985 os.environ['PATH_INFO'] = "/".join(sys.argv[2:])
986 os.environ['SCRIPT_NAME'] = __file__
987 process_cgi_request()
988 elif sys.argv[1] == "unit-test":
989 os.environ['SCRIPT_NAME'] = __file__
990 for host in get_hostlist():
991 for outfile in files:
992 os.environ['PATH_INFO'] = "/".join([host,outfile])
993 try:
994 process_cgi_request()
995 except Exception:
996 print "# ERROR: %s" % os.environ['PATH_INFO']
997 raise
998
999
1000 elif sys.argv[1] == "static":
1001 items = dict()
1002 for node in get_hostlist():
1003 items['node'] = node
1004 items['wdir'] = "./static/%(node)s" % items
1005 if not os.path.isdir(items['wdir']):
1006 os.makedirs(items['wdir'])
1007 datadump = get_yaml(node)
1008 for config in files:
1009 items['config'] = config
1010 logger.info("## Generating %(node)s %(config)s" % items)
1011 f = open("%(wdir)s/%(config)s" % items, "w")
1012 f.write(generate_config(node, config, datadump))
1013 f.close()
1014 elif sys.argv[1] == "wind-export":
1015 items = dict()
1016 for node in get_hostlist():
1017 datadump = get_yaml(node)
1018 sql = """INSERT IGNORE INTO nodes (name, name_ns, longitude, latitude)
1019 VALUES ('%(nodename)s', '%(nodename)s', %(latitude)s, %(longitude)s);""" % datadump;
1020 sql = """INSERT IGNORE INTO users_nodes (user_id, node_id, owner)
1021 VALUES (
1022 (SELECT id FROM users WHERE username = 'rvdzwet'),
1023 (SELECT id FROM nodes WHERE name = '%(nodename)s'),
1024 'Y');""" % datadump
1025 #for config in files:
1026 # items['config'] = config
1027 # print "## Generating %(node)s %(config)s" % items
1028 # f = open("%(wdir)s/%(config)s" % items, "w")
1029 # f.write(generate_config(node, config, datadump))
1030 # f.close()
1031 for node in get_hostlist():
1032 datadump = get_yaml(node)
1033 for iface_key in sorted([elem for elem in datadump.keys() if elem.startswith('iface_')]):
1034 ifacedump = datadump[iface_key]
1035 if ifacedump.has_key('mode') and ifacedump['mode'] == 'ap-wds':
1036 ifacedump['nodename'] = datadump['nodename']
1037 if not ifacedump.has_key('channel') or not ifacedump['channel']:
1038 ifacedump['channel'] = 0
1039 sql = """INSERT INTO links (node_id, type, ssid, protocol, channel, status)
1040 VALUES ((SELECT id FROM nodes WHERE name = '%(nodename)s'), 'ap',
1041 '%(ssid)s', 'IEEE 802.11b', %(channel)s, 'active');""" % ifacedump
1042 elif sys.argv[1] == "full-export":
1043 hosts = {}
1044 for node in get_hostlist():
1045 datadump = get_yaml(node)
1046 hosts[datadump['nodename']] = datadump
1047 print yaml.dump(hosts)
1048
1049 elif sys.argv[1] == "dns":
1050 make_dns(sys.argv[2] if len(sys.argv) > 2 else 'dns', 'external' in sys.argv)
1051 elif sys.argv[1] == "cleanup":
1052 # First generate all datadumps
1053 datadumps = dict()
1054 for host in get_hostlist():
1055 logger.info("# Processing: %s", host)
1056 datadump = get_yaml(host)
1057 datadumps[get_fqdn(datadump)] = datadump
1058
1059 for host,datadump in datadumps.iteritems():
1060 datadump['latitude'], datadump['longitude'] = rdnap.rd2etrs(datadump['rdnap_x'], datadump['rdnap_y'])
1061 if datadump['nodename'].startswith('Proxy'):
1062 datadump['nodename'] = datadump['nodename'].lower()
1063
1064 for iface_key in datadump['autogen_iface_keys']:
1065 # Wireless Leiden SSID have an consistent lowercase/uppercase
1066 if datadump[iface_key].has_key('ssid'):
1067 ssid = datadump[iface_key]['ssid']
1068 prefix = 'ap-WirelessLeiden-'
1069 if ssid.lower().startswith(prefix.lower()):
1070 datadump[iface_key]['ssid'] = prefix + ssid[len(prefix)].upper() + ssid[len(prefix) + 1:]
1071 if datadump[iface_key].has_key('ns_ip') and not datadump[iface_key].has_key('mode'):
1072 datadump[iface_key]['mode'] = 'autogen-FIXME'
1073 if not datadump[iface_key].has_key('desc'):
1074 datadump[iface_key]['desc'] = 'autogen-FIXME'
1075 store_yaml(datadump)
1076 elif sys.argv[1] == "list":
1077 if sys.argv[2] == "nodes":
1078 systems = get_nodelist()
1079 elif sys.argv[2] == "proxies":
1080 systems = get_proxylist()
1081 elif sys.argv[2] == "all":
1082 systems = get_hostlist()
1083 else:
1084 usage()
1085 for system in systems:
1086 datadump = get_yaml(system)
1087 if datadump['status'] == "up":
1088 print system
1089 else:
1090 usage()
1091 else:
1092 # Do not enable debugging for config requests as it highly clutters the output
1093 if not is_text_request():
1094 cgitb.enable()
1095 process_cgi_request()
1096
1097
1098if __name__ == "__main__":
1099 main()
Note: See TracBrowser for help on using the repository browser.