source: genesis/tools/gformat.py@ 10375

Last change on this file since 10375 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
RevLine 
[8242]1#!/usr/bin/env python
2#
3# vim:ts=2:et:sw=2:ai
4# Wireless Leiden configuration generator, based on yaml files'
[9957]5#
6# XXX: This should be rewritten to make use of the ipaddr.py library.
7#
[10058]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#
[8242]15# Rick van der Zwet <info@rickvanderzwet.nl>
[9957]16#
[8622]17
18# Hack to make the script directory is also threated as a module search path.
19import sys
20import os
[9286]21import re
[8622]22sys.path.append(os.path.dirname(__file__))
23
[8242]24import cgi
[8267]25import cgitb
26import copy
[8242]27import glob
28import socket
29import string
30import subprocess
31import time
[8622]32import rdnap
[8584]33from pprint import pprint
[10281]34from collections import defaultdict
[8575]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)
[8588]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
[10110]48from jinja2 import Template
49
[9697]50import logging
51logging.basicConfig(format='# %(levelname)s: %(message)s' )
52logger = logging.getLogger()
53logger.setLevel(logging.DEBUG)
[8242]54
[9283]55
[8948]56if os.environ.has_key('CONFIGROOT'):
57 NODE_DIR = os.environ['CONFIGROOT']
58else:
[9283]59 NODE_DIR = os.path.abspath(os.path.dirname(__file__)) + '/../nodes'
[8242]60__version__ = '$Id: gformat.py 10368 2012-04-07 10:22:52Z rick $'
61
[8267]62
[9283]63files = [
[8242]64 'authorized_keys',
65 'dnsmasq.conf',
66 'rc.conf.local',
67 'resolv.conf',
[10069]68 'motd',
[10054]69 'wleiden.yaml',
[8242]70 ]
71
[8319]72# Global variables uses
[8323]73OK = 10
74DOWN = 20
75UNKNOWN = 90
[8257]76
77
[10281]78def make_relations():
[10270]79 """ Process _ALL_ yaml files to get connection relations """
80 errors = ""
[10281]81 poel = defaultdict(list)
[10270]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)
[10281]95 network = addr & ~((1 << (32 - mask)) - 1)
96 poel[network] += [(host,datadump[iface_key])]
[10270]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
[8267]104def get_proxylist():
105 """Get all available proxies proxyX sorting based on X number"""
[10041]106 proxylist = sorted([os.path.basename(x) for x in glob.glob("%s/proxy*" % NODE_DIR)],
[8267]107 key=lambda name: int(''.join([c for c in name if c in string.digits])),
[10364]108 cmp=lambda x,y: x - y) + sorted([os.path.basename(x) for x in glob.glob("%s/Proxy*" % NODE_DIR)])
[8267]109 return proxylist
110
[10192]111def get_hybridlist():
112 """Get all available proxies hybridX sorting based on X number"""
[10193]113 hybridlist = sorted([os.path.basename(x) for x in glob.glob("%s/hybrid*" % NODE_DIR)],
[10192]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
[8267]117
118
[8321]119def valid_addr(addr):
120 """ Show which address is valid in which are not """
121 return str(addr).startswith('172.')
122
123
[8267]124def get_nodelist():
125 """ Get all available nodes - sorted """
[10041]126 nodelist = sorted([os.path.basename(x) for x in glob.glob("%s/CNode*" % NODE_DIR)])
[8267]127 return nodelist
128
[8296]129def get_hostlist():
130 """ Combined hosts and proxy list"""
[10192]131 return get_nodelist() + get_proxylist() + get_hybridlist()
[8267]132
[8588]133def angle_between_points(lat1,lat2,long1,long2):
[9283]134 """
[8588]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
[8267]142
[8588]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"
[9283]155 elif degrees < (90 - p):
[8588]156 return "no"
[9283]157 elif degrees < (90 + p):
[8588]158 return "o"
[9283]159 elif degrees < (180 - p):
[8588]160 return "zo"
[9283]161 elif degrees < (180 + p):
[8588]162 return "z"
[9283]163 elif degrees < (270 - p):
[8588]164 return "zw"
[9283]165 elif degrees < (270 + p):
[8588]166 return "w"
[9283]167 elif degrees < (360 - p):
[8588]168 return "nw"
169 else:
170 return "n"
171
172
[8267]173def generate_title(nodelist):
[8257]174 """ Main overview page """
[9283]175 items = {'root' : "." }
[8267]176 output = """
[8257]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>
[8259]189 <form type="GET" action="%(root)s">
[8257]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
[8242]196
[8296]197 for node in nodelist:
[8257]198 items['node'] = node
[8267]199 output += '<tr><td><a href="%(root)s/%(node)s">%(node)s</a></td>' % items
[8257]200 for config in files:
201 items['config'] = config
[8267]202 output += '<td><a href="%(root)s/%(node)s/%(config)s">%(config)s</a></td>' % items
203 output += "</tr>"
204 output += """
[8257]205 </table>
206 <hr />
207 <em>%s</em>
208 </center>
209 </body>
210</html>
211 """ % __version__
[8242]212
[8267]213 return output
[8257]214
215
[8267]216
217def generate_node(node):
[8257]218 """ Print overview of all files available for node """
[8267]219 return "\n".join(files)
[8242]220
[10270]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>"
[8257]231
[10270]232 # Generate and connection listing
233 output += "<h2>Connected To:</h2><ul>"
[10281]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
[10270]246 output += "</ul>"
[10281]247 output += "<h2>MOTD details:</h2><pre>" + generate_motd(datadump) + "</pre>"
[8257]248
[10270]249 output += "<hr /><em><a href='..'>Back to overview</a></em>"
250 return output
251
252
[8242]253def generate_header(ctag="#"):
254 return """\
[9283]255%(ctag)s
[8242]256%(ctag)s DO NOT EDIT - Automatically generated by 'gformat'
257%(ctag)s Generated at %(date)s by %(host)s
[9283]258%(ctag)s
[8242]259""" % { 'ctag' : ctag, 'date' : time.ctime(), 'host' : socket.gethostname() }
260
[8257]261
262
[8242]263def parseaddr(s):
[8257]264 """ Process IPv4 CIDR notation addr to a (binary) number """
[8242]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
[8257]271
272
[8242]273def showaddr(a):
[8257]274 """ Display IPv4 addr in (dotted) CIDR notation """
[8242]275 return "%d.%d.%d.%d" % ((a >> 24) & 0xff, (a >> 16) & 0xff, (a >> 8) & 0xff, a & 0xff)
276
[8257]277
[8584]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
[8257]286
[8584]287
288
[9283]289
[8242]290def netmask2subnet(netmask):
[8257]291 """ Given a 'netmask' return corresponding CIDR """
[8242]292 return showaddr(0xffffffff & (0xffffffff << (32 - int(netmask))))
293
[8257]294
295
[8242]296def generate_dnsmasq_conf(datadump):
[8257]297 """ Generate configuration file '/usr/local/etc/dnsmasq.conf' """
[8242]298 output = generate_header()
[10368]299 output += Template("""\
[9283]300# DHCP server options
[8242]301dhcp-authoritative
302dhcp-fqdn
[10368]303domain=dhcp.{{ autogen_fqdn }}.{{ autogen_domain }}
[8242]304domain-needed
305expand-hosts
[10120]306log-async=100
[8242]307
308# Low memory footprint
309cache-size=10000
310
[10368]311\n""").render(datadump)
312
[10281]313 for iface_key in datadump['autogen_iface_keys']:
[8262]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]
[8242]317
318 try:
[8257]319 (dhcp_start, dhcp_stop) = datadump[iface_key]['dhcp'].split('-')
[8242]320 (ip, netmask) = datadump[iface_key]['ip'].split('/')
321 datadump[iface_key]['subnet'] = netmask2subnet(netmask)
[8262]322 except (AttributeError, ValueError):
[8242]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]
[9283]330
[8242]331 return output
332
[8257]333
334
[8242]335def generate_rc_conf_local(datadump):
[8257]336 """ Generate configuration file '/etc/rc.conf.local' """
[10112]337 datadump['autogen_ileiden_enable'] = 'yes' if datadump['ileiden'] else 'no'
[10110]338
[10112]339 ileiden_proxies = []
[10367]340 normal_proxies = []
[10112]341 for proxy in get_proxylist():
342 proxydump = get_yaml(proxy)
343 if proxydump['ileiden']:
344 ileiden_proxies.append(proxydump)
[10367]345 else:
346 normal_proxies.append(proxydump)
[10112]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])
[10367]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])
[10112]351
[8242]352 output = generate_header("#");
[10110]353 output += Template("""\
[10286]354hostname='{{ autogen_fqdn }}.{{ autogen_domain }}'
[10110]355location='{{ location }}'
356nodetype="{{ nodetype }}"
[9283]357
[10110]358{% if tproxy -%}
[8242]359tproxy_enable='YES'
[10110]360tproxy_range='{{ tproxy }}'
361{% else -%}
362tproxy_enable='NO'
363{% endif -%}
[9283]364
[10244]365{% if nodetype == "Proxy" or nodetype == "Hybrid" %}
[10054]366#
[10244]367# Edge Configuration
[10054]368#
[10188]369
[10244]370
371# Firewall and Routing Configuration
372
[10110]373{% if gateway -%}
374defaultrouter="{{ gateway }}"
375{% else -%}
376#defaultrouter="NOTSET"
377{% endif -%}
378internalif="{{ internalif }}"
[10112]379ileiden_enable="{{ autogen_ileiden_enable }}"
380gateway_enable="{{ autogen_ileiden_enable }}"
[10238]381pf_enable="yes"
[10302]382pf_rules="/etc/pf.conf"
[10238]383{% if autogen_ileiden_enable == "yes" -%}
[10234]384pf_flags="-D ext_if={{ externalif }} -D int_if={{ internalif }} -D publicnat={80,443}"
[10238]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}"
[10310]389{% endif -%}
[10238]390{% if internalroute -%}
391static_routes="wleiden"
392route_wleiden="-net 172.16.0.0/12 {{ internalroute }}"
[10110]393{% endif -%}
[10238]394{% endif -%}
[10054]395
[10110]396{% if nodetype == "CNode" %}
[10112]397#
[10054]398# NODE iLeiden Configuration
[10112]399#
400# iLeiden Proxies {{ autogen_ileiden_proxies_names }}
[10367]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"
[10110]406{% endif %}
[10190]407{% if vpnif -%}
[10244]408 vpnif="{{ vpnif }}"
[10190]409{% endif -%}
410
[10183]411captive_portal_whitelist=""
412captive_portal_interfaces="{{ autogen_dhcp_interfaces }}"
[10318]413\n
[10110]414""").render(datadump)
415
[8242]416 # lo0 configuration:
417 # - 172.32.255.1/32 is the proxy.wleiden.net deflector
[9283]418 # - masterip is special as it needs to be assigned to at
[8242]419 # least one interface, so if not used assign to lo0
[9808]420 addrs_list = { 'lo0' : [("127.0.0.1/8", "LocalHost"), ("172.31.255.1/32","Proxy IP")] }
[9283]421 iface_map = {'lo0' : 'lo0'}
[10366]422 dhclient_if = {'lo0' : False}
[8242]423
[8297]424 masterip_used = False
[10281]425 for iface_key in datadump['autogen_iface_keys']:
[8297]426 if datadump[iface_key]['ip'].startswith(datadump['masterip']):
427 masterip_used = True
428 break
[9283]429 if not masterip_used:
[10108]430 addrs_list['lo0'].append((datadump['masterip'] + "/32", 'Master IP Not used in interface'))
[8297]431
[10281]432 for iface_key in datadump['autogen_iface_keys']:
[8242]433 ifacedump = datadump[iface_key]
[10162]434 ifname = ifacedump['autogen_ifname']
[8242]435
[10366]436 # Flag dhclient is possible
437 dhclient_if[ifname] = ifacedump.has_key('dhcpclient') and ifacedump['dhcpclient']
[10318]438
[8242]439 # Add interface IP to list
[9808]440 item = (ifacedump['ip'], ifacedump['desc'])
[10162]441 if addrs_list.has_key(ifname):
442 addrs_list[ifname].append(item)
[8242]443 else:
[10162]444 addrs_list[ifname] = [item]
[8242]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"
[10166]455 if ifacedump['mode'] in ['master', 'master-wds', 'ap', 'ap-wds']:
[8242]456 ifacedump['wlanmode'] = "ap"
457 # Default to 802.11b mode
458 ifacedump['mode'] = '11b'
459 if ifacedump['type'] in ['11a', '11b' '11g']:
[9283]460 ifacedump['mode'] = ifacedump['type']
[8242]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
[10054]472 output += "wlans_%(interface)s='%(autogen_ifname)s'\n" % ifacedump
473 output += ("create_args_%(autogen_ifname)s='wlanmode %(wlanmode)s mode " +\
[8274]474 "%(mode)s ssid %(ssid)s %(extra)s channel %(channel)s'\n") % ifacedump
[9283]475
[8242]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
[9283]482 # Print IP address which needs to be assigned over here
[8242]483 output += "\n"
484 for iface,addrs in sorted(addrs_list.iteritems()):
[10079]485 for addr, comment in sorted(addrs,key=lambda x: parseaddr(x[0].split('/')[0])):
[9808]486 output += "# %s || %s || %s\n" % (iface, addr, comment)
[8242]487
[10366]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
[8242]494 return output
495
[8257]496
497
[8242]498def get_yaml(item):
[8257]499 """ Get configuration yaml for 'item'"""
[9284]500 gfile = os.path.join(NODE_DIR,item,'wleiden.yaml')
[8242]501
502 f = open(gfile, 'r')
[8588]503 datadump = yaml.load(f,Loader=Loader)
[10281]504 f.close()
505
506 # Preformat certain needed variables for formatting and push those into special object
[10052]507 datadump['autogen_iface_keys'] = get_interface_keys(datadump)
[10054]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:
[10162]515 datadump[key]['autogen_ifname'] = datadump[key]['interface'].split(':')[0]
[10054]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)
[10053]519 datadump['autogen_item'] = item
[10054]520 datadump['autogen_fqdn'] = get_fqdn(datadump)
[8242]521
[10281]522 datadump['autogen_domain'] = datadump['domain'] if datadump.has_key('domain') else 'wleiden.net'
[8242]523 return datadump
524
[10281]525
[10074]526def store_yaml(datadump, header=False):
[8588]527 """ Store configuration yaml for 'item'"""
[10053]528 item = datadump['autogen_item']
[9284]529 gfile = os.path.join(NODE_DIR,item,'wleiden.yaml')
[8257]530
[8588]531 f = open(gfile, 'w')
[10067]532 f.write(generate_wleiden_yaml(datadump, header))
[8588]533 f.close()
[8257]534
[8588]535
536
[8317]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
[8319]546def get_interface_keys(config):
547 """ Quick hack to get all interface keys, later stage convert this to a iterator """
[10054]548 return sorted([elem for elem in config.keys() if (elem.startswith('iface_') and not "lo0" in elem)])
[8317]549
[8319]550
[8317]551def get_used_ips(configs):
552 """ Return array of all IPs used in config files"""
553 ip_list = []
[8319]554 for config in configs:
[8317]555 ip_list.append(config['masterip'])
[8319]556 for iface_key in get_interface_keys(config):
[8317]557 l = config[iface_key]['ip']
558 addr, mask = l.split('/')
559 # Special case do not process
[8332]560 if valid_addr(addr):
561 ip_list.append(addr)
562 else:
[9728]563 logger.error("## IP '%s' in '%s' not valid" % (addr, config['nodename']))
[8317]564 return sorted(ip_list)
565
566
567
[8242]568def generate_resolv_conf(datadump):
[8257]569 """ Generate configuration file '/etc/resolv.conf' """
[8242]570 output = generate_header("#");
571 output += """\
572search wleiden.net
[10053]573"""
[10197]574 if datadump['nodetype'] == 'Proxy':
[10053]575 output += """\
[10209]576# Try local (cache) first
577nameserver 127.0.0.1
[10053]578nameserver 8.8.8.8 # Google Public NameServer
579nameserver 8.8.4.4 # Google Public NameServer
580"""
[10197]581 elif datadump['nodetype'] == 'Hybrid':
[10209]582 for proxy in get_proxylist():
583 proxy_ip = get_yaml(proxy)['masterip']
584 output += "nameserver %-15s # %s\n" % (proxy_ip, proxy)
[10197]585 output += """\
586nameserver 8.8.8.8 # Google Public NameServer
587nameserver 8.8.4.4 # Google Public NameServer
588"""
[10053]589 else:
590 output += """\
[10209]591# Try local (cache) first
592nameserver 127.0.0.1
593
[9283]594# Proxies are recursive nameservers
[8242]595# needs to be in resolv.conf for dnsmasq as well
596""" % datadump
[10053]597 for proxy in get_proxylist():
598 proxy_ip = get_yaml(proxy)['masterip']
599 output += "nameserver %-15s # %s\n" % (proxy_ip, proxy)
[9283]600
[8242]601 return output
602
[10069]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
[8242]607
[10069]608 WWW: %(autogen_fqdn)s.wleiden.net - http://www.wirelessleiden.nl
[10113]609 Loc: %(location)s
[8257]610
[10069]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
[8267]629def format_yaml_value(value):
630 """ Get yaml value in right syntax for outputting """
631 if isinstance(value,str):
[10049]632 output = '"%s"' % value
[8267]633 else:
634 output = value
[9283]635 return output
[8267]636
637
638
639def format_wleiden_yaml(datadump):
[8242]640 """ Special formatting to ensure it is editable"""
[9283]641 output = "# Genesis config yaml style\n"
[8262]642 output += "# vim:ts=2:et:sw=2:ai\n"
[8242]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)):
[8267]646 output += "%-10s: %s\n" % (key, format_yaml_value(datadump[key]))
[9283]647
[8242]648 output += "\n\n"
[9283]649
[8272]650 key_order = [ 'comment', 'interface', 'ip', 'desc', 'sdesc', 'mode', 'type',
651 'extra_type', 'channel', 'ssid', 'dhcp' ]
652
[8242]653 for iface_key in sorted(iface_keys):
654 output += "%s:\n" % iface_key
[8272]655 for key in key_order + list(sorted(set(datadump[iface_key].keys()) - set(key_order))):
656 if datadump[iface_key].has_key(key):
[9283]657 output += " %-11s: %s\n" % (key, format_yaml_value(datadump[iface_key][key]))
[8242]658 output += "\n\n"
659
660 return output
661
662
[8257]663
[10067]664def generate_wleiden_yaml(datadump, header=True):
[8267]665 """ Generate (petty) version of wleiden.yaml"""
[10053]666 for key in datadump.keys():
667 if key.startswith('autogen_'):
668 del datadump[key]
[10054]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
[10067]675 output = generate_header("#") if header else ''
[8267]676 output += format_wleiden_yaml(datadump)
677 return output
678
679
[8588]680def generate_yaml(datadump):
681 return generate_config(datadump['nodename'], "wleiden.yaml", datadump)
[8267]682
[8588]683
[9283]684
[8298]685def generate_config(node, config, datadump=None):
[8257]686 """ Print configuration file 'config' of 'node' """
[8267]687 output = ""
[8242]688 try:
689 # Load config file
[8298]690 if datadump == None:
691 datadump = get_yaml(node)
[9283]692
[8242]693 if config == 'wleiden.yaml':
[8267]694 output += generate_wleiden_yaml(datadump)
695 elif config == 'authorized_keys':
[10051]696 f = open(os.path.join(NODE_DIR,"global_keys"), 'r')
[8267]697 output += f.read()
[8242]698 f.close()
699 elif config == 'dnsmasq.conf':
[10281]700 output += generate_dnsmasq_conf(datadump)
[8242]701 elif config == 'rc.conf.local':
[10281]702 output += generate_rc_conf_local(datadump)
[8242]703 elif config == 'resolv.conf':
[10281]704 output += generate_resolv_conf(datadump)
[10069]705 elif config == 'motd':
[10281]706 output += generate_motd(datadump)
[8242]707 else:
[9283]708 assert False, "Config not found!"
[8242]709 except IOError, e:
[8267]710 output += "[ERROR] Config file not found"
711 return output
[8242]712
713
[8257]714
[8258]715def process_cgi_request():
716 """ When calling from CGI """
717 # Update repository if requested
718 form = cgi.FieldStorage()
719 if form.getvalue("action") == "update":
[8259]720 print "Refresh: 5; url=."
[8258]721 print "Content-type:text/plain\r\n\r\n",
722 print "[INFO] Updating subverion, please wait..."
[10143]723 print subprocess.Popen(['svn', 'cleanup', "%s/.." % NODE_DIR], stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0],
[10071]724 print subprocess.Popen(['svn', 'up', "%s/.." % NODE_DIR], stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0],
[8258]725 print "[INFO] All done, redirecting in 5 seconds"
726 sys.exit(0)
[9283]727
728
[10270]729 base_uri = os.environ['PATH_INFO']
730 uri = base_uri.strip('/').split('/')
731
[8267]732 output = ""
[8258]733 if not uri[0]:
[10070]734 if is_text_request():
[10060]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())
[8258]740 elif len(uri) == 1:
[10270]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])
[8258]747 elif len(uri) == 2:
[8267]748 output += "Content-type:text/plain\r\n\r\n"
749 output += generate_config(uri[0], uri[1])
[8258]750 else:
751 assert False, "Invalid option"
[8267]752 print output
[8242]753
[8588]754def get_fqdn(datadump):
[10365]755 # Proxy naming convention is special, as the proxy name is also included in
756 # the nodename, when it comes to the numbered proxies.
[8588]757 if datadump['nodetype'] == 'Proxy':
[10365]758 fqdn = datadump['nodetype'] + datadump['nodename'].replace('proxy','')
[8588]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)
[8259]763
[9283]764
765
[10264]766def make_dns(output_dir = 'dns', external = False):
[8588]767 items = dict()
[8598]768
[8588]769 # hostname is key, IP is value
770 wleiden_zone = dict()
771 wleiden_cname = dict()
[8598]772
[8588]773 pool = dict()
774 for node in get_hostlist():
[9697]775 logger.info("Processing host %s", node)
[8588]776 datadump = get_yaml(node)
[9283]777
[8588]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
[8598]785 # Hacking to get proper DHCP IPs and hostnames
[8588]786 for iface_key in get_interface_keys(datadump):
[8598]787 iface_name = datadump[iface_key]['interface'].replace(':',"-alias-")
[8588]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)
[9283]802 if pool.has_key(addr):
[8588]803 pool[addr] += [(iface_name, fqdn, ip)]
[9283]804 else:
[8588]805 pool[addr] = [(iface_name, fqdn, ip)]
806 continue
807
[9286]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
[9957]824 # WL uses an /29 to configure an interface. IP's are ordered like this:
[9958]825 # MasterA (.1) -- DeviceA (.2) <<>> DeviceB (.3) --- SlaveB (.4)
[9957]826
827 sn = lambda x: re.sub(r'(?i)^cnode','',x)
828
[8598]829 # Automatic naming convention of interlinks namely 2 + remote.lower()
[8588]830 for (key,value) in pool.iteritems():
[9958]831 # Make sure they are sorted from low-ip to high-ip
832 value = sorted(value, key=lambda x: parseaddr(x[2]))
833
[8588]834 if len(value) == 1:
835 (iface_name, fqdn, ip) = value[0]
836 wleiden_zone["2unused-%s.%s" % (iface_name, fqdn)] = ip
[9957]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
[8588]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
[9957]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)
[9958]852 wleiden_zone["d-at-%s.%s" % (b_iface_name, b_fqdn)] = showaddr(parseaddr(b_ip) - 1)
[9957]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
[8588]858 else:
859 pool_members = [k[1] for k in value]
860 for item in value:
[9283]861 (iface_name, fqdn, ip) = item
[9286]862 pool_name = "2pool-" + showaddr(key).replace('.','-') + "-" + pool_to_name(fqdn,pool_members)
[8588]863 wleiden_zone["%s.%s" % (pool_name, fqdn)] = ip
[8598]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
[9284]872 dns = yaml.load(open(os.path.join(NODE_DIR,'../dns/staticDNS.yaml'),'r'))
[9938]873
874 # Hack to allow special entries, for development
875 wleiden_raw = dns['raw']
876 del dns['raw']
877
[8622]878 for comment, block in dns.iteritems():
879 for k,v in block.iteritems():
[8598]880 if valid_addr(v):
881 wleiden_zone[k] = v
882 else:
883 wleiden_cname[k] = v
[9283]884
[8598]885 details = dict()
886 # 24 updates a day allowed
887 details['serial'] = time.strftime('%Y%m%d%H')
888
[10264]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
[8598]897 dns_header = '''
898$TTL 3h
[10264]899%(zone)s. SOA %(master)s. beheer.lijst.wirelessleiden.nl. ( %(serial)s 1d 12h 1w 3h )
[8598]900 ; Serial, Refresh, Retry, Expire, Neg. cache TTL
901
[10264]902%(ns_servers)s
[8598]903 \n'''
904
[9283]905
[10264]906 if not os.path.isdir(output_dir):
907 os.makedirs(output_dir)
[8598]908 details['zone'] = 'wleiden.net'
[9284]909 f = open(os.path.join(output_dir,"db." + details['zone']), "w")
[8598]910 f.write(dns_header % details)
911
[8588]912 for host,ip in wleiden_zone.iteritems():
[8598]913 if valid_addr(ip):
[9283]914 f.write("%s.wleiden.net. IN A %s \n" % (host.lower(), ip))
[8588]915 for source,dest in wleiden_cname.iteritems():
[8636]916 f.write("%s.wleiden.net. IN CNAME %s.wleiden.net.\n" % (source.lower(), dest.lower()))
[9938]917 for source, dest in wleiden_raw.iteritems():
918 f.write("%s.wleiden.net. %s\n" % (source, dest))
[8588]919 f.close()
[9283]920
[8598]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
[9284]924 f = open(os.path.join(output_dir,"db." + details['zone']), "w")
[8598]925 f.write(dns_header % details)
[8588]926
[8598]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('.')))
[9283]933 f.write("%s.in-addr.arpa. IN PTR %s.wleiden.net.\n" % (rev_ip.lower(), host.lower()))
[8598]934 f.close()
[8588]935
[8598]936
[8259]937def usage():
[8598]938 print """Usage: %s <standalone [port] |test [test arguments]|static|dns>
[8259]939Examples:
[9284]940\tdns [outputdir] = Generate BIND compliant zone files in dns.
[8259]941\tstandalone = Run configurator webserver [default port=8000]
[9589]942\twind-export = Generate SQL import scripts for WIND database
943\tfull-export = Generate yaml export script for heatmap.
[8296]944\tstatic = Generate all config files and store on disk
945\t with format ./static/%%NODE%%/%%FILE%%
[9283]946\ttest CNodeRick dnsmasq.conf = Receive output of CGI script
[8259]947\t for arguments CNodeRick/dnsmasq.conf
[10270]948\tlist <all|nodes|proxies> = List systems which marked up.
[8259]949"""
950 exit(0)
951
952
[10070]953def is_text_request():
[10107]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
[8259]959
[8267]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()
[9283]966
[8267]967 if sys.argv[1] == "standalone":
968 import SocketServer
969 import CGIHTTPServer
[10105]970 # Hop to the right working directory.
971 os.chdir(os.path.dirname(__file__))
[8267]972 try:
973 PORT = int(sys.argv[2])
974 except (IndexError,ValueError):
975 PORT = 8000
[9283]976
[8267]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
[9283]982
[10364]983 self.cgi_info = (os.path.basename(__file__), self.path)
[8267]984 self.path = ''
985 return True
986 handler = MyCGIHTTPRequestHandler
[9807]987 SocketServer.TCPServer.allow_reuse_address = True
[8267]988 httpd = SocketServer.TCPServer(("", PORT), handler)
989 httpd.server_name = 'localhost'
990 httpd.server_port = PORT
[9283]991
[9728]992 logger.info("serving at port %s", PORT)
[8860]993 try:
994 httpd.serve_forever()
995 except KeyboardInterrupt:
996 httpd.shutdown()
[9728]997 logger.info("All done goodbye")
[8267]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()
[10107]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
[8296]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'])
[8298]1021 datadump = get_yaml(node)
[8296]1022 for config in files:
1023 items['config'] = config
[9728]1024 logger.info("## Generating %(node)s %(config)s" % items)
[8296]1025 f = open("%(wdir)s/%(config)s" % items, "w")
[8298]1026 f.write(generate_config(node, config, datadump))
[8296]1027 f.close()
[9514]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
[9589]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
[8584]1063 elif sys.argv[1] == "dns":
[10264]1064 make_dns(sys.argv[2] if len(sys.argv) > 2 else 'dns', 'external' in sys.argv)
[9283]1065 elif sys.argv[1] == "cleanup":
[8588]1066 # First generate all datadumps
1067 datadumps = dict()
1068 for host in get_hostlist():
[9728]1069 logger.info("# Processing: %s", host)
[8588]1070 datadump = get_yaml(host)
1071 datadumps[get_fqdn(datadump)] = datadump
[9283]1072
[10156]1073 for host,datadump in datadumps.iteritems():
[8622]1074 datadump['latitude'], datadump['longitude'] = rdnap.rd2etrs(datadump['rdnap_x'], datadump['rdnap_y'])
[10319]1075 if datadump['nodename'].startswith('Proxy'):
1076 datadump['nodename'] = datadump['nodename'].lower()
1077
[10156]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:]
[10162]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'
[10074]1089 store_yaml(datadump)
[9971]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()
[10270]1095 elif sys.argv[2] == "all":
1096 systems = get_hostlist()
[9971]1097 else:
1098 usage()
1099 for system in systems:
1100 datadump = get_yaml(system)
1101 if datadump['status'] == "up":
1102 print system
[9283]1103 else:
1104 usage()
1105 else:
[10070]1106 # Do not enable debugging for config requests as it highly clutters the output
1107 if not is_text_request():
1108 cgitb.enable()
[9283]1109 process_cgi_request()
1110
1111
1112if __name__ == "__main__":
1113 main()
Note: See TracBrowser for help on using the repository browser.