#!/usr/bin/env python

from optparse import OptionParser
from subprocess import Popen, PIPE
import glob
import os
import re
import sys
import urllib2

URL_BASE = 'http://mgmt.sie.isc.org/sie-update/'
URL_HOSTS = URL_BASE + 'hosts.txt'
URL_SITES = URL_BASE + 'sites.txt'
URL_CHANNELS = URL_BASE + 'channels.txt'

URL_CHALIAS = URL_BASE + 'chalias.%s.txt'
URL_GRALIAS = URL_BASE + 'gralias.txt'
URL_OPALIAS = URL_BASE + 'opalias.txt'

FNAME_CHALIAS = 'nmsgtool.chalias'
FNAME_GRALIAS = 'nmsg.gralias'
FNAME_OPALIAS = 'nmsg.opalias'

VERBOSE = False

class CommandFailed(Exception):
    pass

def run_cmd(cmd, failok=False):
    if VERBOSE:
        print cmd
    p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE)
    stdout = p.stdout.read()
    stderr = p.stderr.read()
    if VERBOSE:
        sys.stdout.write(stdout)
        sys.stderr.write(stderr)
    rc = p.wait()
    if not failok and rc != 0:
        print >>sys.stderr, 'command "%s" returned non-zero exit code %d' % (cmd, rc)
    if not failok and rc != 0:
        raise CommandFailed
    return (rc, stdout, stderr)

def http_fetch_contents(url):
    if VERBOSE:
        print 'Fetching %s...' % url
    try:
        data = urllib2.urlopen(url).read()
        return data
    except:
        print >>sys.stderr, 'Error: HTTP fetch failed for URL %s' % url
        sys.exit(1)

def update_file(fname, url):
    old_contents = None
    if os.path.isfile(fname):
        old_contents = open(fname).read()
    new_contents = http_fetch_contents(url)
    if old_contents != new_contents:
        open(fname, 'w').write(new_contents)
        print 'Updated %s from %s.' % (fname, url)

def do_update(funcs, iface, etcdir):
    hwaddr = funcs['get_hw_address'](iface)
    hosts = http_fetch_contents(URL_HOSTS)
    vlans = set()
    channels = []

    found_host = False
    try:
        for line in hosts.strip().split('\n'):
            s = line.split()
            if hwaddr == s[0]:
                found_host = True
                site = s[1]
                x = s[2]
                break
    except:
        print >>sys.stderr, 'Error: failed to parse SIE hosts file\n'
        raise

    if not found_host:
        print >>sys.stderr, 'Error: hardware address %s not found in SIE hosts file' % hwaddr
        sys.exit(1)

    found_site = False
    sites = http_fetch_contents(URL_SITES)
    try:
        for line in sites.strip().split('\n'):
            s = line.split()
            s_site = s[0]
            s_net = s[1]
            s_vpnnet = s[2]
            if len(s) >= 4:
                s_vpnmask = int(s[3])
            else:
                s_vpnmask = 24
            if site == s_site:
                net = s_net
                vpnnet = s_vpnnet
                vpnmask = s_vpnmask
                found_site = True
                break
    except:
        print >>sys.stderr, 'Error: failed to parse SIE sites file\n'
        raise

    if not found_site:
        print >>sys.stderr, 'Error: unknown site %s in SIE hosts file' % site
        sys.exit(1)

    funcs['set_link_up'](iface)
    
    try:
        for line in http_fetch_contents(URL_CHANNELS).split():
            line = int(line)
            channels.append(line)
    except:
        print >>sys.stderr, 'Error: failed to parse SIE channels file\n'
        raise

    for ch in channels:
        vlan = ch
        if vlan == 7:
            ip = '%s.%s' % (vpnnet, x)
            gw = '%s.254' % vpnnet
            funcs['set_vlan_up'](iface, vlan, ip, vpnmask)
            funcs['set_vlan_mtu'](iface, vlan, 1280)
            funcs['ip_route_add']('10.255.0.0/16', gw)
        else:
            ip = '%s.%s.%s' % (net, ch, x)
            funcs['set_vlan_up'](iface, vlan, ip)
        vlans.add(vlan)

    cur_vlans = funcs['get_vlans'](iface)
    rm_vlans = cur_vlans.difference(vlans)
    for vlan in rm_vlans:
        funcs['remove_vlan'](iface, vlan)

    update_file(os.path.join(etcdir, FNAME_CHALIAS), URL_CHALIAS % site)
    update_file(os.path.join(etcdir, FNAME_GRALIAS), URL_GRALIAS)
    update_file(os.path.join(etcdir, FNAME_OPALIAS), URL_OPALIAS)

def _linux_get_hw_address(iface):
    try:
        rc, stdout, stderr = run_cmd('ip link show %s' % iface)
        hwaddr = re.findall('link/ether ([0-9a-f:]+)', stdout)[0]
        return hwaddr
    except:
        print >>sys.stderr, 'Error: unable to determine hardware address for interface %s' % iface
        sys.exit(1)

def _freebsd_get_hw_address(iface):
    try:
        rc, stdout, stderr = run_cmd('ifconfig %s' % iface)
        hwaddr = re.findall('ether ([0-9a-f:]+)', stdout)[0]
        return hwaddr
    except:
        print >>sys.stderr, 'Error: unable to determine hardware address for interface %s' % iface
        sys.exit(1)

def _linux_get_vlans(iface):
    vlans = set()
    for x in glob.glob('/proc/net/vlan/%s.*' % iface):
        vlan = os.path.basename(x).split('%s.' % iface, 1)[1]
        vlans.add(int(vlan))
    return vlans

def _freebsd_get_vlans(iface):
    vlans = set()
    try:
        rc, stdout, stderr = run_cmd('ifconfig | grep "^vlan"', failok=True)
        if rc == 0:
            for line in stdout.strip().split('\n'):
                s = line.split(':', 1)[0].split('vlan', 1)[1]
                vlans.add(int(s))
    except:
        print >>sys.stderr, 'Error: unable to enumerate VLAN IDs for interface %s' % iface
        sys.exit(1)
    return vlans

def _linux_ip_addr_add(ip, iface, netmask=24):
    run_cmd('ip addr add %s/%s dev %s' % (ip, netmask, iface))

def _freebsd_ip_addr_add(ip, iface, netmask=24):
    netmask = hex((1 << 32) - (1 << (32 - netmask))) # bah
    run_cmd('ifconfig %s alias %s netmask %s' % (iface, ip, netmask))

def _linux_ip_route_add(net, gw):
    rc, stdout, stderr = run_cmd('ip route show %s' % net)
    if rc == 0 and stdout:
        if not ('via %s' % gw in stdout):
            run_cmd('ip route del %s' % net, failok=True)
            run_cmd('ip route add %s via %s' % (net, gw))
    else:
        run_cmd('ip route add %s via %s' % (net, gw))

def _freebsd_ip_route_add(net, gw):
    rc, stdout, stderr = run_cmd('route get %s' % net, failok=True)
    if rc == 0:
        if not ('gateway: %s' % gw in stdout):
            run_cmd('route del %s' % net, failok=True)
            run_cmd('route add %s %s' % (net, gw))
    else:
        run_cmd('route add %s %s' % (net, gw))

def _linux_set_link_up(iface):
    rc, stdout, stderr = run_cmd('ip link show %s' % iface, failok=True)
    if rc != 0:
        print >>sys.stderr, 'Error: unable to bring up SIE network interface %s' % iface
        sys.exit(1)
    if len(stdout) > 0 and not ('mtu 9000' in stdout and 'state UP' in stdout):
        run_cmd('ip link set up %s mtu 9000' % iface)

def _freebsd_set_link_up(iface):
    rc, stdout, stderr = run_cmd('ifconfig %s' % iface, failok=True)
    if rc != 0:
        print >>sys.stderr, 'Error: unable to bring up SIE network interface %s' % iface
        sys.exit(1)

    if len(stdout) > 0 and not ('mtu 9000' in stdout and 'UP' in stdout):
        run_cmd('ifconfig %s mtu 9000 up' % iface)

def _linux_set_vlan_mtu(iface, vlan, mtu):
    run_cmd('ip link set mtu %s dev %s.%s' % (mtu, iface, vlan))

def _freebsd_set_vlan_mtu(iface, vlan, mtu):
    run_cmd('ifconfig vlan%s mtu %s' % (vlan, mtu))

def _linux_set_vlan_up(iface, vlan, ip, netmask=24):
    vlan_iface = '%s.%s' % (iface, vlan)

    rc, stdout, stderr = run_cmd('ip addr show %s' % vlan_iface, failok=True)
    if rc != 0:
        run_cmd('ip link add link %s name %s type vlan id %s' % (iface, vlan_iface, vlan))
        _linux_ip_addr_add(ip, vlan_iface)
        print 'Added new VLAN %s.' % vlan
    else:
        current_ips = set(re.findall('inet ([0-9./]+)', stdout))
        ipnm = '%s/%s' % (ip, netmask)
        if ipnm in current_ips:
            rm_ips = set(current_ips)
            rm_ips.remove(ipnm)
            if rm_ips:
                for x in rm_ips:
                    print 'Removing obsolete IP address %s from interface %s' % (x, vlan_iface)
                    run_cmd('ip addr del %s dev %s' % (x, vlan_iface))
        else:
            run_cmd('ip addr flush dev %s' % vlan_iface)
            _linux_ip_addr_add(ip, vlan_iface, netmask)
    run_cmd('ip link set up dev %s' % vlan_iface)

def _freebsd_set_vlan_up(iface, vlan, ip, netmask=24):
    vlan_iface = 'vlan%s' % vlan

    rc, stdout, stderr = run_cmd('ifconfig %s' % vlan_iface, failok=True)
    if rc != 0:
        run_cmd('ifconfig %s create vlan %s vlandev %s' % (vlan_iface, vlan, iface))
        _freebsd_ip_addr_add(ip, vlan_iface)
        print 'Added new VLAN %s.' % vlan
    else:
        current_ips = set(re.findall('inet ([0-9.]+)', stdout))
        rm_ips = set(current_ips)
        if ip in current_ips:
            rm_ips.remove(ip)
        else:
            _freebsd_ip_addr_add(ip, vlan_iface, netmask)
        if rm_ips:
            for x in rm_ips:
                run_cmd('ifconfig %s -alias %s' % (vlan_iface, x))

def _linux_remove_vlan(iface, vlan):
    run_cmd('ip link del dev %s.%s' % (iface, vlan))
    print 'Removed old VLAN %s.' % vlan

def _freebsd_remove_vlan(iface, vlan):
    run_cmd('ifconfig vlan%s destroy' % vlan)
    print 'Removed old VLAN %s.' % vlan

def main():
    global URL_BASE
    global VERBOSE

    linux_net_funcs = {
        'get_hw_address':   _linux_get_hw_address,
        'get_vlans':        _linux_get_vlans,
        'ip_addr_add':      _linux_ip_addr_add,
        'ip_route_add':     _linux_ip_route_add,
        'set_link_up':      _linux_set_link_up,
        'set_vlan_up':      _linux_set_vlan_up,
        'set_vlan_mtu':     _linux_set_vlan_mtu,
        'remove_vlan':      _linux_remove_vlan
    }

    freebsd_net_funcs = {
        'get_hw_address':   _freebsd_get_hw_address,
        'get_vlans':        _freebsd_get_vlans,
        'ip_addr_add':      _freebsd_ip_addr_add,
        'ip_route_add':     _freebsd_ip_route_add,
        'set_link_up':      _freebsd_set_link_up,
        'set_vlan_up':      _freebsd_set_vlan_up,
        'set_vlan_mtu':     _freebsd_set_vlan_mtu,
        'remove_vlan':      _freebsd_remove_vlan
    }

    parser = OptionParser('Usage: %prog -i INTERFACE [OPTION]...')
    parser.add_option('-i', '--interface', dest='interface', help='SIE network interface')
    parser.add_option('-e', '--etcdir', dest='etcdir', default='/etc',
        help='system configuration directory')
    parser.add_option('-v', '--verbose', dest='verbose', default=VERBOSE,
        action='store_true', help='verbose mode')
    
    opt, arg = parser.parse_args()

    VERBOSE = opt.verbose

    if not os.path.isdir(opt.etcdir):
        print >>sys.stderr, 'Error: path does not exist: %s' % opt.etcdir
        sys.exit(1)

    if not opt.interface:
        print >>sys.stderr, 'Error: SIE network interface (-i) required'
        parser.print_help()
        sys.exit(1)

    kernel = os.uname()[0]
    if kernel == 'Linux':
        do_update(linux_net_funcs, opt.interface, opt.etcdir)
    elif kernel == 'FreeBSD':
        do_update(freebsd_net_funcs, opt.interface, opt.etcdir)
    else:
        print >>sys.stderr, 'Error: unsupported system: %s' % kernel
        sys.exit(1)

if __name__ == '__main__':
    main()
