source: genesis/tools/gformat.py@ 10274

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

Make sure more usefull page overviews for nodes and other items. Including an
dynamic listing on the units they are connected to.

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