#! /usr/bin/env python
###############################################################################
#                                                                             #
#   Copyright 2005 University of Cambridge Computer Laboratory.               #
#                                                                             #
#   This file is part of Nprobe.                                              #
#                                                                             #
#   Nprobe is free software; you can redistribute it and/or modify            #
#   it under the terms of the GNU General Public License as published by      #
#   the Free Software Foundation; either version 2 of the License, or         #
#   (at your option) any later version.                                       #
#                                                                             #
#   Nprobe is distributed in the hope that it will be useful,                 #
#   but WITHOUT ANY WARRANTY; without even the implied warranty of            #
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the             #
#   GNU General Public License for more details.                              #
#                                                                             #
#   You should have received a copy of the GNU General Public License         #
#   along with Nprobe; if not, write to the Free Software                     #
#   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA #
#                                                                             #
###############################################################################


import os, sys, re, shutil
from sys import argv
from getopt import getopt
from string import replace
from time import asctime
from types import IntType
	    
##############################################################################
##############################################################################

#
# DEFAULT CONFIGURATION VALUES
#

LOG_DIR_DEF =  '/local/scratch/nprobe/current/'
USE_TCP_QUICKCLOSE_DEF = 1
ACCEPT_TCP_NOSYN_DEF = 1
DUMP_LINKTYPE_DEF = 'nprobe'
DUMP_SPEC_DEF = {}
SERVICES_DEF = {}
HANDLERS_DEF = {'udp': ['udp_other_serv_control_na', 0], 'tcp': ['tcp_other_serv_control_na', 0]}
HISTOGRAMS_DEF = {}
IP_OFFSET_DEF = 0
FILT_PORT_DEF = 0

REP_FSZ_DEF = 8*8*1024*1024
DMP_FSZ_DEF = 8*1024*1024
LOG_FSZS_DEF = {'report': REP_FSZ_DEF, 'dump': DMP_FSZ_DEF}
LARGE_DUMP = 0
LARGE_DUMP_FACT = 64

##############################################################################
##############################################################################

def add_const(str, const):

    if str:
        delim = ' | '
    else:
        delim = ''

    return '%s%s' % (delim, const)
	    
##############################################################################

def do_service(match, stuff, lno):

    serviced = stuff[3]
    cb = match.group('cb')
    trans = match.group('trans')
    match2 = re.match('^(?P<trans>tcp|udp)(_([a-z]|\d)+)+_serv_control$', cb)
    match2 = re.match('^(?P<trans>tcp|udp)(_([a-z]|\d)+)+_serv_control.*$', cb)
    if not  match2:
        print 'Error line: %d \'%s\' is not an expected service control block name' % (lno, cb)
        sys.exit(1)
    if match2.group('trans') != trans:
        print 'Error line: %d control block title \'%s\' does not match transport protocol \'%s\'' % (lno, match2.group(1), trans)
        sys.exit(1)

    port = match.group('port')
    try:
        port = int(port)
    except ValueError:
        if port == 'default':
            pass
    dump1 = match.group('dump1')
    dump2 = match.group('dump2')
    dump3 = match.group('dump3')
    neg1 = match.group('neg1')
    neg2 = match.group('neg2')
    neg3 = match.group('neg3')

    if serviced.has_key((port, trans)):
        print 'Error line %d: %s port number %s already encountered in line %d' % (lno, trans, str(port), serviced[(port, trans)][0])
        sys.exit(1)


    if trans == 'udp' and ((dump1 == 'hdrs' and not neg1) or (dump2 == 'hdrs' and not neg2)):
        print 'Error line %d: Don\'t dump udp headers' % (lno)
        sys.exit(1)


    serviced[(port, trans)] = (lno, (port, trans, cb, [(dump1, neg1), (dump2, neg2), (dump3, neg3)]))
	    
##############################################################################

def do_default_handlers(match, stuff, lno):

    handlerd = stuff[3]
    stuff[2] += 1

    trans = match.group('trans')
    handler = match.group('handler')

    print 'def hand', trans, handler

    if handler.find(trans) != 0:
        print 'Inappropriate %s default handler %s line number %d' % (trans, handler, lno)

    if handlerd[trans][1]:
        print 'Default %s handler %s (line %d) already specified as %s on line %d' % (trans, handler, lno, handlerd[trans][0], handlerd[trans][1])
        sys.exit(1)
    handlerd[trans][0] = handler
    handlerd[trans][1] = lno
	    
##############################################################################

def do_dump(match, stuff, lno):
    
    dumpd = stuff[3]
    stuff[2] += 1
    for spec in match.group('specs').split():
        if dumpd.has_key(spec):
            print 'duplicate dump spec \'%s\' line %d' % (spec, lno)
            sys.exit(1)
        else:
            dumpd[spec] = 1
	    
##############################################################################

def do_logfszs(match, stuff, lno):

    mults = {None: 1, 'k': 1024, 'm':1024*1024, 'g':1024*1024*1024}

    fszd = stuff[3]
    
    file = match.group('file')
    val = match.group('val')
    unit = match.group('unit')

    mult = mults[unit]

    fszd[file] = int(val)*mult
	    
##############################################################################

def do_logdir(match, stuff, lno):
    
    #dir_form_match = dir_form.match(match.group('dir'))
    dir_form_match = re.match('^(?P<dir>/?([\w.]+/)*([\w.]+/?))$', match.group('dir'))
    if dir_form_match:
        logdir = match.group('dir')
        if logdir[0] != '/':
            print 'Invalid path \'%s\' line %d - must be absolute' % (logdir, lno)
            sys.exit(1)
        if logdir[-1] != '/':
            logdir += '/'
        stuff[2] += 1
        stuff[3] = logdir

    else:
        print 'Invalid path \'%s\' line %d' % (match.group('dir'), lno)
        sys.exit(1)
	    
##############################################################################

def do_hist(match, stuff, lno):
    
    histd = stuff[3]
    for htype in match.group('specs').split():
        if histd.has_key(htype):
            print 'Duplicate histogram spec \'%s\' line %d' % (htype, lno)
            sys.exit(1)
        else:
            histd[htype] = 1
	    
##############################################################################

def do_val(match, stuff, lno):
    
    neg = match.group('neg')
    if neg == 'no':
        val = 0
    elif neg == None:
        val = 1
    elif neg == '':
        val = match.group('val')
    else:
        print 'Don\'t understand line %d' % (lno)
        sys.exit(1)
    stuff[2] = 1
    stuff[3] = val
	    
##############################################################################


def get_config(configf, config_items):
 
    lines = configf.readlines()
    lno = -1
    lined = {}

    for line in lines:

        lno += 1
        #print lno
        l = line.strip().lower()
        if not l:
            continue
        if l[0] == '#':
            continue

        # remove trailing comments
        parts = l.split('#')
        if len(parts) > 1:
            l = parts[0].strip()

        if lined.has_key(l):
            print 'duplicate lines %d and %d \'%s\'' % (lined[l], lno, l)
            sys.exit(1)
        else:
            lined[l] = lno

        got = 0
    
        for key, stuff in config_items.items():
            match = stuff[0].match(l)
            if match:
                got = 1
                stuff[1](match, stuff, lno)
                break
            
        if not got:
            print 'don\'t understand input line %d \'%s\'' % (lno, line.replace('\n', ''))
            sys.exit(1)
            


	    
##############################################################################

def write_config(outf_c, outf_h, config_items):


    #
    # Map config values to defined C constants
    #
    
    transtypes = {'tcp': 'TRANS_TCP', 'udp': 'TRANS_UDP'}
    dump_linktypes = \
                   {'nprobe': ('LINK_NPROBE', 'dump_pkt'), \
                    'ether': ('LINK_ETH_10MB', 'dump_ethpkt')\
                    }
    serv_pkt_dumptypes = {'pkts': 'DUMP_PKTS',
                          'hdrs': 'DUMP_HDRS | DUMP_OPEN',
                          'payload': 'DUMP_PAYLOAD'}
    dumpspecs = \
              {'all': 'DUMP_ALL_PKTS', \
               'non-ip': 'DUMP_NON_IP_PKTS', \
               'ip': 'DUMP_IP_PKTS', \
               'icmp': 'DUMP_ICMP_PKTS', \
               'udp': 'DUMP_UDP_PKTS', \
               'tcp': 'DUMP_TCP_PKTS', \
               'none': 'DUMP_NO_PKTS' \
               }

    histspecs =  \
              {'gets': 'HIS_GET', \
               'fetches': 'HIS_FETCH', \
               'hostwalk': 'HIS_HOSTWALK', \
               'portwalk': 'HIS_PORTWALK', \
               'pkts': 'HIS_PKTS', \
               'none':'HIS_NONE' \
               }

    # Set flag to configure larger dump files
    large_dump = 0

    # Set flag to denote payload dumping
    dump_payload = 0

    #
    # Write configuration to config *.c/*.h files
    #

    # Specify service control blocks/packet dump/ TCP hdr dump 
    outf_c.write('\n\n\n')
    outf_c.write('void \nservice_reg(void)\n{\n\n')   
    item = config_items['services']
    services = [details[1] for details in item[3].values()]
    if services:
        services.sort()
        for s in services:
            if type(s[0]) is IntType: #port -based handler
                outf_c.write('  extern serv_control_t %s;\n' % (s[2]))
            
        outf_c.write('\n\n')
        
        for s in services:
            sets = ''
            clears = ''
            for dump, neg in s[3]:
                if dump:
                    if neg:
                        clears += add_const(clears, serv_pkt_dumptypes[dump])
                    else:
                        sets += add_const(sets, serv_pkt_dumptypes[dump])
                        if dump == 'pkts':
                            large_dump = 1
                        if dump == 'payload':
                            dump_payload = 1
            if not sets:
                sets = 'NO_DUMP'
            if not clears:
                clears = 'NO_DUMP'
            if  type(s[0]) is IntType: #port -based handler
                outf_c.write('  reg_serv(%d, %s, &%s, %s, %s);\n' % (s[0], transtypes[s[1]], s[2], sets, clears))
            else: # default handler
                outf_c.write('%s.cflags = (%s.cflags | %s) & (~%s);' % (s[2], s[2], sets, clears))
                HANDLERS_DEF[s[1]] = [s[2], 1]

    else:
        print 'No service handlers to register'
        outf_c.write('\n  /* No services to register */\n')

    outf_c.write('\n  return;\n\n}\n\n')

    # Default tcp/udp packet handlers
    item = config_items['default_handlers']
    for trans, (handler, spec) in item[3].items():
        print trans, handler, spec
        outf_c.write('serv_control_t *%s_handler_def = &%s;\n' % (trans, handler))
        if not spec:
            print 'Using default %s handler %s()' % (trans, handler)
    outf_c.write('\n')

    # type of dump output
    item = config_items['dump_linktype']
    if not item[2]:
        print 'No dump_linktype specified, using default DLT_NPROBE'
    stuff = dump_linktypes[item[3]]
    outf_c.write('int dump_linktype = %s;\n\n' % (stuff[0]))
    outf_c.write('void (*pkt_dump)(prec_t *pp, int why, char *data, unsigned int atmdata) =  %s;\n\n\n' % (stuff[1]))


    # Any global packet dumps
    item = config_items['dump_spec']
    dumpd = item[3]
    if item[2]: # modified
        large_dump = 1
        dumpspec = ''
        for spec in dumpd.keys():
            dumpspec += add_const(dumpspec, dumpspecs[spec])
    else:
        dumpspec = dumpspecs['none']
        
    outf_c.write('int dump_pkt_types = (%s);\n' % (dumpspec))

    # Use TCP quick-close?
    item = config_items['tcp_quickclose']
    if not item[2]:
        print 'No TCP quick_close flag specified, using default YES'
    outf_c.write('int tcp_quickclose = %d;\n' % (item[3]))

    # Accept TCP un-syned connections?
    item = config_items['tcp_accept_nosyn']
    if not item[2]:
        print 'No TCP accept_nosyn flag specified, using default YES'
    outf_c.write('int tcp_accept_nosyn = %d;\n\n' % (item[3]))

    # Non-standard log directory?
    item = config_items['logdir']
    if not item[2]:
        print 'No log directory specified - using \'%s\' (input directory for owan)' % (item[3])
    outf_c.write('char log_dir[MAX_LOGDIR_LEN] = \"%s\";\n\n' % (item[3]))
    outf_c.write('int log_dir_specified = %d;\n\n' % (item[2]))

    # Modify start of IP hdr in input buffers
    item = config_items['ip_offset']
    outf_c.write('int ip_hdr_offset = %s;\n\n' % (item[3]))

    # Filter input on a specified port
    item = config_items['filt_port']
    outf_c.write('unsigned short fport = %sU;\n\n' % (item[3]))

    # Control performance histogram production
    item = config_items['histspec']
    histd = item[3]
    if item[2]: # modified
        histspec = ''
        for spec in histd.keys():
            histspec += add_const(histspec, histspecs[spec])
    else:
        histspec = histspecs['none']
    outf_c.write('int histflags = (%s);\n\n' % (histspec))

    # Log file sizes
    item = config_items['logfszs']
    szd = item[3]
    for key in ['report', 'dump']:
        try:
            val = szd[key]
        except KeyError:
            val = LOG_FSZS_DEF[key]
            if key == 'dump' and large_dump:
                val *= LARGE_DUMP_FACT
            print 'No %s file size specified - using default %d' % (key, val)
        outf_c.write('unsigned int %s_file_sz = %dU;\n' % (key, val))

    # Dump HTTP objects, and how much
    item = config_items['dump_http_ob']
    val = int(item[3])
    if not item[2] & val:
        print 'Not dumping HTTP objects'
    outf_c.write('int http_ob_dump_len = %d;\n' % (val))

    # Dumping any payloads?
    outf_c.write('int dumping_payloads = %d;\n' % (dump_payload))

    
	    
##############################################################################

        

def gen_config(configf, outf_c, outf_h):
    
    service_form = re.compile('^(?P<neg>)(?P<val>(?P<port>\d+)\s+(?P<trans>tcp|udp)\s+(?P<cb>(?:[a-z]|_|\d)+)(?:\s+(?P<neg1>no\s)?(?P<dump1>(?:pkts)|(?:hdrs)))?(?:\s+(?P<neg2>no\s)?(?P<dump2>(?:pkts)|(?:hdrs))))?$')

    
    service_form = re.compile('^(?P<neg>)(?P<val>)(?P<port>\d+)\s+(?P<trans>tcp|udp)\s+(?P<cb>(?:[a-z]|_|\d)+)(?:\s+(?P<neg1>no\s)?(?P<dump1>(?:pkts)|(?:hdrs)))?(?:\s+(?P<neg2>no\s)?(?P<dump2>(?:pkts)|(?:hdrs)))?$')

    
    service_form = re.compile('^(?P<neg>)(?P<val>)(?P<port>\d+|default)\s+(?P<trans>tcp|udp)\s+(?P<cb>(?:[a-z]|_|\d)+)(?:\s+(?P<neg1>no\s)?(?P<dump1>(?:pkts)|(?:hdrs)|(?:payload)))?(?:\s+(?P<neg2>no\s)?(?P<dump2>(?:pkts)|(?:hdrs)|(?:payload)))?(?:\s+(?P<neg3>no\s)?(?P<dump3>(?:pkts)|(?:hdrs)|(?:payload)))?$')

    default_handler_form = re.compile('^(?P<trans>tcp|udp)(\s*)default\s*handler\s*=\s*(?P<handler>.+)\Z')
    
    dump_linktype_form = re.compile('^dump_linktype(\s*)=(\s*)(?P<neg>)(?P<val>(nprobe|ether))\Z')
    dump_form = re.compile('^dump(?P<specs>(?:\s+(all|ip|non-ip|tcp|udp|icmp))+)$')

    tcp_quick_close_form = re.compile('^((?P<neg>no)\s+)?tcp\s+quick_close$')

    tcp_accept_nosyn_form = re.compile('^((?P<neg>no)\s+)?tcp\s+accept_nosyn$')

    logdir_form = re.compile('^log\s+dir\s+(?P<dir>[\w./]+)$')
    #dir_form = re.compile('^(?P<dir>/?([\w.]+/)*([\w.]+/?))$')
    hist_form = re.compile('^histograms?(?P<specs>(?:\s+(gets|fetches|pkts|hostwalk|portwalk))+)$')
    ip_off_form = re.compile('^ip\s+hdr\s+offset\s+(?P<neg>)(?P<val>-?\d+)$')
    port_form = re.compile('^port\s+(?P<neg>)(?P<val>\d{1,5})$')

    log_fszs_form = re.compile('^(?P<neg>)(?P<file>report|dump)\s+file\s+size\s+(?P<val>\d+)\s*(?P<unit>(k|m|g))?(b(ytes)?)?$')
    dump_obs_form = re.compile('^(?P<neg>)http\s+objects\s+dump\s+(?P<val>-?\d+)$')


    #
    # Directory fields :-
    # 'key': [matching re, handler, modified, default value]
    #
    config_items = \
                {'services': [service_form, do_service, 0, SERVICES_DEF], \
                 'default_handlers': \
                 [default_handler_form, do_default_handlers, 0, HANDLERS_DEF],\
                 'dump_linktype': \
                 [dump_linktype_form, do_val, 0, DUMP_LINKTYPE_DEF], \
                 'dump_spec': [dump_form, do_dump, 0, DUMP_SPEC_DEF], \
                 'histspec': [hist_form, do_hist, 0, HISTOGRAMS_DEF], \
                 'tcp_quickclose': [tcp_quick_close_form, do_val, 0, USE_TCP_QUICKCLOSE_DEF], \
                 'tcp_accept_nosyn': [tcp_accept_nosyn_form, do_val, 0, ACCEPT_TCP_NOSYN_DEF], \
                 'logdir': [logdir_form, do_logdir, 0, LOG_DIR_DEF], \
                 'ip_offset': [ip_off_form, do_val, 0, IP_OFFSET_DEF], \
                 'filt_port': [port_form, do_val, 0, FILT_PORT_DEF], \
                 'logfszs': [log_fszs_form, do_logfszs, 0, {}], \
                 'dump_http_ob': [dump_obs_form, do_val, 0, 0] \
                 }

    get_config(configf, config_items)
    write_config(outf_c, outf_h, config_items)

    
	    
##############################################################################

def copy_template(tmplf, outf_c, outf_h):

    c_h_delim = re.compile('\s*/\*+\s*DO NOT DELETE THIS LINE - IT SEPARATES THE SRC AND HEADER FILE PARTS OF THE TEMPLATE\s*\*+/\s*')

    lines = tmplf.readlines()
    outf = outf_c

    for l in lines:
        if c_h_delim.match(l):
            outf = outf_h
            continue
        else:
            outf.write(l) 

    
	    
##############################################################################

def gen_outf(args, scriptname):

    try:
        tmplf = open(args[0], 'r')
    except IOError, s:
        print '%s ERROR: %s' % (scriptname, s)
        sys.exit(1)

    try:
        configf = open(args[1], 'r')
    except IOError, s:
        print '%s ERROR: %s' % (scriptname, s)
        sys.exit(1)

    try:
        outf_c = open('probe_config.c', 'w')
    except IOError, s:
        print '%s ERROR: %s' % (scriptname, s)
        sys.exit(1)

    try:
        outf_h = open('probe_config.h', 'w')
    except IOError, s:
        print '%s ERROR: %s' % (scriptname, s)
        sys.exit(1)

    for f, suff in [[outf_c, 'c'], [outf_h, 'h']]:
        f.write('\n\n/*\n * probe_config.%s\n * Atomatically generated from %s and %s \n * by %s@%s 0n %s\n */\n\n' % (suff, args[1], args[0], os.environ['LOGNAME'], os.environ['HOSTNAME'], asctime()))

    copy_template(tmplf, outf_c, outf_h)

    gen_config(configf, outf_c, outf_h)

    outf_h.write('#endif /* ifndef _PROBE_CONFIG_H_ */')

    for f, suff in [[outf_c, 'c'], [outf_h, 'h']]:
        f.write('\n\n/*\n * End probe_config.%s\n */\n\n' % (suff))
	    
##############################################################################

def backup():

    def copy(s, d):
        try:
            shutil.copy2(s, d)
        except IOError, e:
            print 'ERROR backing up %s: %s' % (s, e)
            sys.exit(1)

    if os.path.isfile('probe_config.c'):
        print 'Backing up probe_config.c'
        copy('probe_config.c', 'probe_config.c.bak')

    if os.path.isfile('probe_config.h'):
        print 'Backing up probe_config.h'
        copy('probe_config.h', 'probe_config.h.bak')
	    
##############################################################################
	
def main():

    scriptname = os.path.basename(argv[0])

    try:
        optlist, args = getopt(argv[1:], '')

    except getopt.error, s:
        print '%s: Unrecognised option' % (scriptname)
        sys.exit(1)

    if len(args) != 2:
        print '%s ERROR: Takes two arguments - %d given' % (scriptname, len(args))
        sys.exit(1)

    backup()
    gen_outf(args, scriptname)
        
	    
##############################################################################


# Call main when run as script
if __name__ == '__main__':
        main()
