source: genesis/tools/gformat.py@ 10467

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

Exposing the fact that we are an certain service whould be usefull in scripts for line-checking and such.

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