#!/usr/bin/env python # # vim:ts=2:et:sw=2:ai # Wireless Leiden configuration generator, based on yaml files' # Rick van der Zwet # Hack to make the script directory is also threated as a module search path. import sys import os import re sys.path.append(os.path.dirname(__file__)) import cgi import cgitb import copy import glob import socket import string import subprocess import time import rdnap from pprint import pprint try: import yaml except ImportError, e: print e print "[ERROR] Please install the python-yaml or devel/py-yaml package" exit(1) try: from yaml import CLoader as Loader from yaml import CDumper as Dumper except ImportError: from yaml import Loader, Dumper if os.environ.has_key('CONFIGROOT'): NODE_DIR = os.environ['CONFIGROOT'] else: NODE_DIR = os.path.abspath(os.path.dirname(__file__)) + '/../nodes' __version__ = '$Id: gformat.py 9589 2011-08-28 11:33:05Z rick $' files = [ 'authorized_keys', 'dnsmasq.conf', 'rc.conf.local', 'resolv.conf', 'wleiden.yaml' ] # Global variables uses OK = 10 DOWN = 20 UNKNOWN = 90 def get_proxylist(): """Get all available proxies proxyX sorting based on X number""" os.chdir(NODE_DIR) proxylist = sorted(glob.glob("proxy*"), key=lambda name: int(''.join([c for c in name if c in string.digits])), cmp=lambda x,y: x - y) return proxylist def valid_addr(addr): """ Show which address is valid in which are not """ return str(addr).startswith('172.') def get_nodelist(): """ Get all available nodes - sorted """ os.chdir(NODE_DIR) nodelist = sorted(glob.glob("CNode*")) return nodelist def get_hostlist(): """ Combined hosts and proxy list""" return get_nodelist() + get_proxylist() def angle_between_points(lat1,lat2,long1,long2): """ Return Angle in radians between two GPS coordinates See: http://stackoverflow.com/questions/3809179/angle-between-2-gps-coordinates """ dy = lat2 - lat1 dx = math.cos(math.pi/180*lat1)*(long2 - long1) angle = math.atan2(dy,dx) return angle def angle_to_cd(angle): """ Return Dutch Cardinal Direction estimation in 'one digit' of radian angle """ # For easy conversion get positive degree degrees = math.degrees(angle) if degrees < 0: 360 - abs(degrees) # Numbers can be confusing calculate from the 4 main directions p = 22.5 if degrees < p: return "n" elif degrees < (90 - p): return "no" elif degrees < (90 + p): return "o" elif degrees < (180 - p): return "zo" elif degrees < (180 + p): return "z" elif degrees < (270 - p): return "zw" elif degrees < (270 + p): return "w" elif degrees < (360 - p): return "nw" else: return "n" def generate_title(nodelist): """ Main overview page """ items = {'root' : "." } output = """ Wireless leiden Configurator - GFormat
""" % items for node in nodelist: items['node'] = node output += '' % items for config in files: items['config'] = config output += '' % items output += "" output += """

Wireless Leiden Configurator

%(node)s%(config)s

%s
""" % __version__ return output def generate_node(node): """ Print overview of all files available for node """ return "\n".join(files) def generate_header(ctag="#"): return """\ %(ctag)s %(ctag)s DO NOT EDIT - Automatically generated by 'gformat' %(ctag)s Generated at %(date)s by %(host)s %(ctag)s """ % { 'ctag' : ctag, 'date' : time.ctime(), 'host' : socket.gethostname() } def parseaddr(s): """ Process IPv4 CIDR notation addr to a (binary) number """ f = s.split('.') return (long(f[0]) << 24L) + \ (long(f[1]) << 16L) + \ (long(f[2]) << 8L) + \ long(f[3]) def showaddr(a): """ Display IPv4 addr in (dotted) CIDR notation """ return "%d.%d.%d.%d" % ((a >> 24) & 0xff, (a >> 16) & 0xff, (a >> 8) & 0xff, a & 0xff) def is_member(ip, mask, canidate): """ Return True if canidate is part of ip/mask block""" ip_addr = gformat.parseaddr(ip) ip_canidate = gformat.parseaddr(canidate) mask = int(mask) ip_addr = ip_addr & ~((1 << (32 - mask)) - 1) ip_canidate = ip_canidate & ~((1 << (32 - mask)) - 1) return ip_addr == ip_canidate def netmask2subnet(netmask): """ Given a 'netmask' return corresponding CIDR """ return showaddr(0xffffffff & (0xffffffff << (32 - int(netmask)))) def generate_dnsmasq_conf(datadump): """ Generate configuration file '/usr/local/etc/dnsmasq.conf' """ output = generate_header() output += """\ # DHCP server options dhcp-authoritative dhcp-fqdn domain=dhcp.%(nodename_lower)s.%(domain)s domain-needed expand-hosts # Low memory footprint cache-size=10000 \n""" % datadump for iface_key in datadump['iface_keys']: if not datadump[iface_key].has_key('comment'): datadump[iface_key]['comment'] = None output += "## %(interface)s - %(desc)s - %(comment)s\n" % datadump[iface_key] try: (dhcp_start, dhcp_stop) = datadump[iface_key]['dhcp'].split('-') (ip, netmask) = datadump[iface_key]['ip'].split('/') datadump[iface_key]['subnet'] = netmask2subnet(netmask) except (AttributeError, ValueError): output += "# not autoritive\n\n" continue dhcp_part = ".".join(ip.split('.')[0:3]) datadump[iface_key]['dhcp_start'] = dhcp_part + "." + dhcp_start datadump[iface_key]['dhcp_stop'] = dhcp_part + "." + dhcp_stop output += "dhcp-range=%(interface)s,%(dhcp_start)s,%(dhcp_stop)s,%(subnet)s,24h\n\n" % datadump[iface_key] return output def generate_rc_conf_local(datadump): """ Generate configuration file '/etc/rc.conf.local' """ output = generate_header("#"); output += """\ hostname='%(nodetype)s%(nodename)s.%(domain)s' location='%(location)s' """ % datadump # TProxy configuration output += "\n" try: if datadump['tproxy']: output += """\ tproxy_enable='YES' tproxy_range='%(tproxy)s' """ % datadump except KeyError: output += "tproxy_enable='NO'\n" output += '\n' # lo0 configuration: # - 172.32.255.1/32 is the proxy.wleiden.net deflector # - masterip is special as it needs to be assigned to at # least one interface, so if not used assign to lo0 addrs_list = { 'lo0' : ["127.0.0.1/8", "172.31.255.1/32"] } iface_map = {'lo0' : 'lo0'} masterip_used = False for iface_key in datadump['iface_keys']: if datadump[iface_key]['ip'].startswith(datadump['masterip']): masterip_used = True break if not masterip_used: addrs_list['lo0'].append(datadump['masterip'] + "/32") wlan_count = 0 for iface_key in datadump['iface_keys']: ifacedump = datadump[iface_key] interface = ifacedump['interface'] # By default no special interface mapping iface_map[interface] = interface # Add interface IP to list if addrs_list.has_key(interface): addrs_list[interface].append(ifacedump['ip']) else: addrs_list[interface] = [ifacedump['ip']] # Alias only needs IP assignment for now, this might change if we # are going to use virtual accesspoints if "alias" in iface_key: continue # XXX: Might want to deduct type directly from interface name if ifacedump['type'] in ['11a', '11b', '11g', 'wireless']: # Create wlanX interface ifacedump['wlanif'] ="wlan%i" % wlan_count iface_map[interface] = ifacedump['wlanif'] wlan_count += 1 # Default to station (client) mode ifacedump['wlanmode'] = "sta" if ifacedump['mode'] in ['master', 'master-wds']: ifacedump['wlanmode'] = "ap" # Default to 802.11b mode ifacedump['mode'] = '11b' if ifacedump['type'] in ['11a', '11b' '11g']: ifacedump['mode'] = ifacedump['type'] if not ifacedump.has_key('channel'): if ifacedump['type'] == '11a': ifacedump['channel'] = 36 else: ifacedump['channel'] = 1 # Allow special hacks at the back like wds and stuff if not ifacedump.has_key('extra'): ifacedump['extra'] = 'regdomain ETSI country NL' output += "wlans_%(interface)s='%(wlanif)s'\n" % ifacedump output += ("create_args_%(wlanif)s='wlanmode %(wlanmode)s mode " +\ "%(mode)s ssid %(ssid)s %(extra)s channel %(channel)s'\n") % ifacedump elif ifacedump['type'] in ['ethernet', 'eth']: # No special config needed besides IP pass else: assert False, "Unknown type " + ifacedump['type'] # Print IP address which needs to be assigned over here output += "\n" for iface,addrs in sorted(addrs_list.iteritems()): output += "ipv4_addrs_%s='%s'\n" % (iface_map[iface], " ".join(addrs)) return output def get_yaml(item): """ Get configuration yaml for 'item'""" gfile = os.path.join(NODE_DIR,item,'wleiden.yaml') f = open(gfile, 'r') datadump = yaml.load(f,Loader=Loader) f.close() return datadump def store_yaml(datadump): """ Store configuration yaml for 'item'""" gfile = os.path.join(NODE_DIR,item,'wleiden.yaml') f = open(gfile, 'w') f.write(generate_wleiden_yaml(datadump)) f.close() def get_all_configs(): """ Get dict with key 'host' with all configs present """ configs = dict() for host in get_hostlist(): datadump = get_yaml(host) configs[host] = datadump return configs def get_interface_keys(config): """ Quick hack to get all interface keys, later stage convert this to a iterator """ return [elem for elem in config.keys() if (elem.startswith('iface_') and not "lo0" in elem)] def get_used_ips(configs): """ Return array of all IPs used in config files""" ip_list = [] for config in configs: ip_list.append(config['masterip']) for iface_key in get_interface_keys(config): l = config[iface_key]['ip'] addr, mask = l.split('/') # Special case do not process if valid_addr(addr): ip_list.append(addr) else: print "## IP '%s' in '%s' not valid" % (addr, config['nodename']) return sorted(ip_list) def write_yaml(item, datadump): """ Write configuration yaml for 'item'""" gfile = os.path.join(NODE_DIR,item,'wleiden.yaml') f = open(gfile, 'w') f.write(format_wleiden_yaml(datadump)) f.close() def generate_resolv_conf(datadump): """ Generate configuration file '/etc/resolv.conf' """ output = generate_header("#"); output += """\ search wleiden.net # Try local (cache) first nameserver 127.0.0.1 # Proxies are recursive nameservers # needs to be in resolv.conf for dnsmasq as well """ % datadump for proxy in get_proxylist(): proxy_ip = get_yaml(proxy)['masterip'] output += "nameserver %-15s # %s\n" % (proxy_ip, proxy) return output def format_yaml_value(value): """ Get yaml value in right syntax for outputting """ if isinstance(value,str): output = "'%s'" % value else: output = value return output def format_wleiden_yaml(datadump): """ Special formatting to ensure it is editable""" output = "# Genesis config yaml style\n" output += "# vim:ts=2:et:sw=2:ai\n" output += "#\n" iface_keys = [elem for elem in datadump.keys() if elem.startswith('iface_')] for key in sorted(set(datadump.keys()) - set(iface_keys)): output += "%-10s: %s\n" % (key, format_yaml_value(datadump[key])) output += "\n\n" key_order = [ 'comment', 'interface', 'ip', 'desc', 'sdesc', 'mode', 'type', 'extra_type', 'channel', 'ssid', 'dhcp' ] for iface_key in sorted(iface_keys): output += "%s:\n" % iface_key for key in key_order + list(sorted(set(datadump[iface_key].keys()) - set(key_order))): if datadump[iface_key].has_key(key): output += " %-11s: %s\n" % (key, format_yaml_value(datadump[iface_key][key])) output += "\n\n" return output def generate_wleiden_yaml(datadump): """ Generate (petty) version of wleiden.yaml""" output = generate_header("#") output += format_wleiden_yaml(datadump) return output def generate_yaml(datadump): return generate_config(datadump['nodename'], "wleiden.yaml", datadump) def generate_config(node, config, datadump=None): """ Print configuration file 'config' of 'node' """ output = "" try: # Load config file if datadump == None: datadump = get_yaml(node) # Preformat certain needed variables for formatting and push those into special object datadump_extra = copy.deepcopy(datadump) if not datadump_extra.has_key('domain'): datadump_extra['domain'] = 'wleiden.net' datadump_extra['nodename_lower'] = datadump_extra['nodename'].lower() datadump_extra['iface_keys'] = sorted([elem for elem in datadump.keys() if elem.startswith('iface_')]) if config == 'wleiden.yaml': output += generate_wleiden_yaml(datadump) elif config == 'authorized_keys': f = open("global_keys", 'r') output += f.read() f.close() elif config == 'dnsmasq.conf': output += generate_dnsmasq_conf(datadump_extra) elif config == 'rc.conf.local': output += generate_rc_conf_local(datadump_extra) elif config == 'resolv.conf': output += generate_resolv_conf(datadump_extra) else: assert False, "Config not found!" except IOError, e: output += "[ERROR] Config file not found" return output def process_cgi_request(): """ When calling from CGI """ # Update repository if requested form = cgi.FieldStorage() if form.getvalue("action") == "update": print "Refresh: 5; url=." print "Content-type:text/plain\r\n\r\n", print "[INFO] Updating subverion, please wait..." print subprocess.Popen(['svn', 'up', NODE_DIR], stderr=subprocess.STDOUT, stdout=subprocess.PIPE).communicate()[0], print "[INFO] All done, redirecting in 5 seconds" sys.exit(0) uri = os.environ['PATH_INFO'].strip('/').split('/') output = "" if not uri[0]: output += "Content-type:text/html\r\n\r\n" output += generate_title(get_hostlist()) elif len(uri) == 1: output += "Content-type:text/plain\r\n\r\n" output += generate_node(uri[0]) elif len(uri) == 2: output += "Content-type:text/plain\r\n\r\n" output += generate_config(uri[0], uri[1]) else: assert False, "Invalid option" print output def get_fqdn(datadump): # Proxy naming convention is special if datadump['nodetype'] == 'Proxy': fqdn = datadump['nodename'] else: # By default the full name is listed and also a shortname CNAME for easy use. fqdn = datadump['nodetype'] + datadump['nodename'] return(fqdn) def make_dns(output_dir = 'dns'): items = dict() # hostname is key, IP is value wleiden_zone = dict() wleiden_cname = dict() pool = dict() for node in get_hostlist(): datadump = get_yaml(node) # Proxy naming convention is special fqdn = get_fqdn(datadump) if datadump['nodetype'] == 'CNode': wleiden_cname[datadump['nodename']] = fqdn wleiden_zone[fqdn] = datadump['masterip'] # Hacking to get proper DHCP IPs and hostnames for iface_key in get_interface_keys(datadump): iface_name = datadump[iface_key]['interface'].replace(':',"-alias-") (ip, netmask) = datadump[iface_key]['ip'].split('/') try: (dhcp_start, dhcp_stop) = datadump[iface_key]['dhcp'].split('-') datadump[iface_key]['subnet'] = netmask2subnet(netmask) dhcp_part = ".".join(ip.split('.')[0:3]) if ip != datadump['masterip']: wleiden_zone["dhcp-gateway-%s.%s" % (iface_name, fqdn)] = ip for i in range(int(dhcp_start), int(dhcp_stop) + 1): wleiden_zone["dhcp-%s-%s.%s" % (i, iface_name, fqdn)] = "%s.%s" % (dhcp_part, i) except (AttributeError, ValueError): # First push it into a pool, to indentify the counter-part later on addr = parseaddr(ip) netmask = int(netmask) addr = addr & ~((1 << (32 - netmask)) - 1) if pool.has_key(addr): pool[addr] += [(iface_name, fqdn, ip)] else: pool[addr] = [(iface_name, fqdn, ip)] continue def pool_to_name(node, pool_members): """Convert the joined name to a usable pool name""" # Get rid of the own entry pool_members = list(set(pool_members) - set([fqdn])) target = oldname = '' for node in sorted(pool_members): (name, number) = re.match('^([A-Za-z]+)([0-9]*)$',node).group(1,2) target += "-" + number if name == oldname else "-" + node if target else node oldname = name return target # Automatic naming convention of interlinks namely 2 + remote.lower() for (key,value) in pool.iteritems(): if len(value) == 1: (iface_name, fqdn, ip) = value[0] wleiden_zone["2unused-%s.%s" % (iface_name, fqdn)] = ip elif len(value) == 2: (a_iface_name, a_fqdn, a_ip) = value[0] (b_iface_name, b_fqdn, b_ip) = value[1] wleiden_zone["2%s.%s" % (b_fqdn,a_fqdn)] = a_ip wleiden_zone["2%s.%s" % (a_fqdn,b_fqdn)] = b_ip else: pool_members = [k[1] for k in value] for item in value: (iface_name, fqdn, ip) = item pool_name = "2pool-" + showaddr(key).replace('.','-') + "-" + pool_to_name(fqdn,pool_members) wleiden_zone["%s.%s" % (pool_name, fqdn)] = ip # Include static DNS entries # XXX: Should they override the autogenerated results? # XXX: Convert input to yaml more useable. # Format: ##; this is a comment ## roomburgh=CNodeRoomburgh1 ## apkerk1.CNodeVosko=172.17.176.8 ;this as well dns = yaml.load(open(os.path.join(NODE_DIR,'../dns/staticDNS.yaml'),'r')) for comment, block in dns.iteritems(): for k,v in block.iteritems(): if valid_addr(v): wleiden_zone[k] = v else: wleiden_cname[k] = v details = dict() # 24 updates a day allowed details['serial'] = time.strftime('%Y%m%d%H') dns_header = ''' $TTL 3h %(zone)s. SOA sunny.wleiden.net. beheer.lijst.wirelessleiden.nl. ( %(serial)s 1d 12h 1w 3h ) ; Serial, Refresh, Retry, Expire, Neg. cache TTL NS sunny.wleiden.net. \n''' if not os.path.isdir('dns'): os.makedirs('dns') details['zone'] = 'wleiden.net' f = open(os.path.join(output_dir,"db." + details['zone']), "w") f.write(dns_header % details) for host,ip in wleiden_zone.iteritems(): if valid_addr(ip): f.write("%s.wleiden.net. IN A %s \n" % (host.lower(), ip)) for source,dest in wleiden_cname.iteritems(): f.write("%s.wleiden.net. IN CNAME %s.wleiden.net.\n" % (source.lower(), dest.lower())) f.close() # Create whole bunch of specific sub arpa zones. To keep it compliant for s in range(16,32): details['zone'] = '%i.172.in-addr.arpa' % s f = open(os.path.join(output_dir,"db." + details['zone']), "w") f.write(dns_header % details) #XXX: Not effient, fix to proper data structure and do checks at other # stages for host,ip in wleiden_zone.iteritems(): if valid_addr(ip): if int(ip.split('.')[1]) == s: rev_ip = '.'.join(reversed(ip.split('.'))) f.write("%s.in-addr.arpa. IN PTR %s.wleiden.net.\n" % (rev_ip.lower(), host.lower())) f.close() def usage(): print """Usage: %s Examples: \tdns [outputdir] = Generate BIND compliant zone files in dns. \tstandalone = Run configurator webserver [default port=8000] \twind-export = Generate SQL import scripts for WIND database \tfull-export = Generate yaml export script for heatmap. \tstatic = Generate all config files and store on disk \t with format ./static/%%NODE%%/%%FILE%% \ttest CNodeRick dnsmasq.conf = Receive output of CGI script \t for arguments CNodeRick/dnsmasq.conf """ exit(0) def main(): """Hard working sub""" # Allow easy hacking using the CLI if not os.environ.has_key('PATH_INFO'): if len(sys.argv) < 2: usage() if sys.argv[1] == "standalone": import SocketServer import CGIHTTPServer # CGI does not go backward, little hack to get ourself in the right working directory. os.chdir(os.path.dirname(__file__) + '/..') try: PORT = int(sys.argv[2]) except (IndexError,ValueError): PORT = 8000 class MyCGIHTTPRequestHandler(CGIHTTPServer.CGIHTTPRequestHandler): """ Serve this CGI from the root of the webserver """ def is_cgi(self): if "favicon" in self.path: return False self.cgi_info = (__file__, self.path) self.path = '' return True handler = MyCGIHTTPRequestHandler httpd = SocketServer.TCPServer(("", PORT), handler) httpd.server_name = 'localhost' httpd.server_port = PORT print "serving at port", PORT try: httpd.serve_forever() except KeyboardInterrupt: httpd.shutdown() elif sys.argv[1] == "test": os.environ['PATH_INFO'] = "/".join(sys.argv[2:]) os.environ['SCRIPT_NAME'] = __file__ process_cgi_request() elif sys.argv[1] == "static": items = dict() for node in get_hostlist(): items['node'] = node items['wdir'] = "./static/%(node)s" % items if not os.path.isdir(items['wdir']): os.makedirs(items['wdir']) datadump = get_yaml(node) for config in files: items['config'] = config print "## Generating %(node)s %(config)s" % items f = open("%(wdir)s/%(config)s" % items, "w") f.write(generate_config(node, config, datadump)) f.close() elif sys.argv[1] == "wind-export": items = dict() for node in get_hostlist(): datadump = get_yaml(node) sql = """INSERT IGNORE INTO nodes (name, name_ns, longitude, latitude) VALUES ('%(nodename)s', '%(nodename)s', %(latitude)s, %(longitude)s);""" % datadump; print sql sql = """INSERT IGNORE INTO users_nodes (user_id, node_id, owner) VALUES ( (SELECT id FROM users WHERE username = 'rvdzwet'), (SELECT id FROM nodes WHERE name = '%(nodename)s'), 'Y');""" % datadump print sql #for config in files: # items['config'] = config # print "## Generating %(node)s %(config)s" % items # f = open("%(wdir)s/%(config)s" % items, "w") # f.write(generate_config(node, config, datadump)) # f.close() for node in get_hostlist(): datadump = get_yaml(node) for iface_key in sorted([elem for elem in datadump.keys() if elem.startswith('iface_')]): ifacedump = datadump[iface_key] if ifacedump.has_key('mode') and ifacedump['mode'] == 'ap-wds': ifacedump['nodename'] = datadump['nodename'] if not ifacedump.has_key('channel') or not ifacedump['channel']: ifacedump['channel'] = 0 sql = """INSERT INTO links (node_id, type, ssid, protocol, channel, status) VALUES ((SELECT id FROM nodes WHERE name = '%(nodename)s'), 'ap', '%(ssid)s', 'IEEE 802.11b', %(channel)s, 'active');""" % ifacedump print sql elif sys.argv[1] == "full-export": hosts = {} for node in get_hostlist(): datadump = get_yaml(node) hosts[datadump['nodename']] = datadump print yaml.dump(hosts) elif sys.argv[1] == "dns": make_dns(sys.argv[2] if len(sys.argv) > 2 else 'dns') elif sys.argv[1] == "cleanup": # First generate all datadumps datadumps = dict() for host in get_hostlist(): datadump = get_yaml(host) datadumps[get_fqdn(datadump)] = datadump datadump['latitude'], datadump['longitude'] = rdnap.rd2etrs(datadump['rdnap_x'], datadump['rdnap_y']) write_yaml(host, datadump) else: usage() else: cgitb.enable() process_cgi_request() if __name__ == "__main__": main()