source: genesis/tools/gformat.py@ 10368

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

Make sure to use FQDN as fullname for DHCP to avoid problems with equally named
devices (like proxyFoo en nodeFoo).

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