#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
gnng - Fetches network devices interfaces and displays them in a table view.

Fetches interface information from routing and firewall devices. This includes
network and IP information along with the inbound and outbound filters that 
may be applied to the interface. Works on Juniper, Netscreen, Foundry, and Cisco
devices.
"""
__author__ = 'Jathan McCollum, Mark Ellzey Thomas'
__maintainer__ = 'Jathan McCollum'
__email__ = 'jathan.mccollum@teamaol.com'
__copyright__ = 'Copyright 2003-2011, AOL Inc.'
__version__ = '1.15'

import re
import math
import os
import sys
import pprint
import cStringIO
import operator
from IPy import IP
from optparse import OptionParser
from twisted.python import log
from xml.etree.cElementTree import ElementTree, Element, SubElement, dump

from trigger.netdevices import NetDevices
from trigger.cmds import NetACLInfo

#log.startLogging(sys.stdout, setStdout=False)

DEBUG = False
max_connections = 30 
current_connections = 0

def parse_args(argv):
    parser = OptionParser(usage='%prog [options] [routers]', description='''GetNets-NG

Fetches interface information from routing and firewall devices. This includes
network and IP information along with the inbound and outbound filters that 
may be applied to the interface. Works on Juniper, Netscreen, Foundry, and Cisco
devices.''')
    parser.add_option('-a', '--all', action='store_true', 
                      help='run on all devices')
    parser.add_option('-j', '--jobs', type='int', default=10,
                      help='maximum simultaneous connections to maintain.')
    parser.add_option('-c', '--csv', action='store_true',
                      help='output the data in CSV format instead.')
    parser.add_option('-s', '--sqldb', type='str', 
                      help='output to SQLite DB')
    parser.add_option('', '--dotty', action='store_true',
                      help='output connect-to information in dotty format.')
    parser.add_option('', '--filter-on-group', action='append', 
                      help='Run on all devices owned by this group')
    parser.add_option('', '--filter-on-type', action='append', 
                      help='Run on all devices with this device type')
    parser.add_option('-N', '--nonprod', action='store_false', default=True,
                      help='Look for production and non-production devices.')

    opts, args = parser.parse_args(argv)

    if len(args) == 1 and not opts.all:
        parser.print_help()
        sys.exit(1)

    return opts, args
    
def fetch_router_list(args):
    """Turns a list of device names into device objects, skipping unsupported,
    invalid, or filtered devices."""
    nd = NetDevices(production_only=opts.nonprod)
    ret = []
    blocked_groups = [] 
    if args:
        for arg in args:
            if not pass_filters(nd.find(arg)):
                continue
            ret.append(nd.find(arg))

    else:
        for entry in nd.itervalues():
            if entry.owningTeam in blocked_groups:
                continue
            if entry.vendor in ('cisco', 'foundry', 'juniper'):
                if 'oob' in entry.shortName:
                    continue

                if not pass_filters(entry):
                    continue
                ret.append(entry)

    return sorted(ret, reverse=True)

def pass_filters(device):
    """Used by fetch_router_list() to filter a device based on command-line arguments."""
    if opts.filter_on_group:
        if device.owningTeam not in opts.filter_on_group:
            return False
    if opts.filter_on_type:
        if device.deviceType not in opts.filter_on_type:
            return False

    return True 
    
def indent(rows, hasHeader=False, headerChar='-', delim=' | ', justify='left',
           separateRows=False, prefix='', postfix='', wrapfunc=lambda x:x, wraplast=True):
    """Indents a table by column.
       - rows: A sequence of sequences of items, one sequence per row.
       - hasHeader: True if the first row consists of the columns' names.
       - headerChar: Character to be used for the row separator line
         (if hasHeader==True or separateRows==True).
       - delim: The column delimiter.
       - justify: Determines how are data justified in their column. 
         Valid values are 'left','right' and 'center'.
       - separateRows: True if rows are to be separated by a line
         of 'headerChar's.
       - prefix: A string prepended to each printed row.
       - postfix: A string appended to each printed row.
       - wrapfunc: A function f(text) for wrapping text; each element in
         the table is first wrapped by this function."""
    # closure for breaking logical rows to physical, using wrapfunc
    def rowWrapper(row):
        if not wraplast:
            lastcolumn = row[-1]
            newRows = [wrapfunc(item).split('\n') for item in row[0:-1]]
            newRows.append([lastcolumn])
        else:
            newRows = [wrapfunc(item).split('\n') for item in row]

        return [[substr or '' for substr in item] for item in map(None,*newRows)]
    # break each logical row into one or more physical ones
    logicalRows = [rowWrapper(row) for row in rows]
    # columns of physical rows
    columns = map(None,*reduce(operator.add,logicalRows))
    # get the maximum of each column by the string length of its items
    maxWidths = [max([len(str(item)) for item in column]) for column in columns]
    rowSeparator = headerChar * (len(prefix) + len(postfix) + sum(maxWidths) + \
                                 len(delim)*(len(maxWidths)-1))
    # select the appropriate justify method
    justify = {'center':str.center, 'right':str.rjust, 'left':str.ljust}[justify.lower()]
    output=cStringIO.StringIO()
    if separateRows: print >> output, rowSeparator
    for physicalRows in logicalRows:
        for row in physicalRows:
            print >> output, \
                prefix \
                + delim.join([justify(str(item),width) for (item,width) in zip(row,maxWidths)]) \
                + postfix
        if separateRows or hasHeader: print >> output, rowSeparator; hasHeader=False
    return output.getvalue()

# written by Mike Brown
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/148061
def wrap_onspace(text, width):
    """
    A word-wrap function that preserves existing line breaks
    and most spaces in the text. Expects that existing line
    breaks are posix newlines (\n).
    """
    return reduce(lambda line, word, width=width: '%s%s%s' %
                  (line,
                   ' \n'[(len(line[line.rfind('\n')+1:])
                         + len(word.split('\n',1)[0]
                              ) >= width)],
                   word),
                  text.split(' ')
                 )

def wrap_onspace_strict(text, width):
    """Similar to wrap_onspace, but enforces the width constraint:
       words longer than width are split."""
    wordRegex = re.compile(r'\S{'+str(width)+r',}')
    return wrap_onspace(wordRegex.sub(lambda m: wrap_always(m.group(),width),text),width)

def wrap_always(text, width):
    """A simple word-wrap function that wraps text on exactly width characters.
       It doesn't split the text in words."""
    return '\n'.join([ text[width*i:width*(i+1)] \
                       for i in xrange(int(math.ceil(1.*len(text)/width))) ])
    
def write_sqldb(sqlfile, dev, rows):
    """Write device fields to sqlite db"""
    from sqlite3 import dbapi2 as sqlite
    create_table = False
    
    if not os.path.isfile(sqlfile):
        create_table = True

    connection = sqlite.connect(sqlfile)
    cursor = connection.cursor()

    if create_table:
        # if the db doesn't exist we want to create the table.
        cursor.execute('''
        CREATE TABLE dev_nets (
            id            INTEGER PRIMARY KEY,
            insert_date   DATE,
            device_name   VARCHAR(128),
            iface_name    VARCHAR(32),
            iface_addrs   VARCHAR(1024),
            iface_subnets VARCHAR(1024),
            iface_inacl   VARCHAR(32),
            iface_outacl  VARCHAR(32),
            iface_descr   VARCHAR(1024) 
        );
        ''')
        cursor.execute('''
        CREATE TRIGGER auto_date AFTER INSERT ON dev_nets
        BEGIN
            UPDATE dev_nets SET insert_date = DATETIME('NOW')
                WHERE rowid = new.rowid;
        END;
        ''')

    for row in rows:
        iface, addrs, snets, inacl, outacl, desc = row
        
        cursor.execute('''
            INSERT INTO dev_nets (
                device_name, 
                iface_name,
                iface_addrs,
                iface_subnets,
                iface_inacl,
                iface_outacl,
                iface_descr ) 
            VALUES (
                '%s', '%s', '%s', 
                '%s', '%s', '%s', '%s' 
            );''' % (
                dev, iface, addrs, 
                snets, inacl, outacl, desc )
        )

    connection.commit()
    cursor.close()
    connection.close()

    
if __name__ == '__main__':
    routers = []
    global opts

    opts, args = parse_args(sys.argv)

    if opts.all:
        routers = fetch_router_list(None)
    else:
        routers = fetch_router_list(args[1:])

    if not routers:
        sys.exit(1)

    main_data = {}

    ninfo = NetACLInfo(devices=routers, production_only=opts.nonprod)
    ninfo.run()
    if DEBUG: 
        print 'NetACLInfo done!'

    main_data = ninfo.config
    
    subnet_table = {}
    labels = ('Interface', 'Addresses', 'Subnets', 'ACLS IN', 'ACLS OUT', 'Description')
    for dev, data in main_data.iteritems():
        rows = []
        if not opts.csv and not opts.dotty: 
            print "DEVICE: %s" % dev

        interfaces = sorted(data.keys())
        for interface in interfaces:
            iface = data[interface]

            # Skip down interfaces
            if 'addr' not in iface:
                continue

            if DEBUG: 
                print '>>> ', interface

            def make_ipy(nets):
                return [IP(x[0]) for x in nets]
            def make_cidrs(nets):
                print 'GOT nets:', nets
                return [IP(x[0]).make_net(x[1]) for x in nets]

            #addrs   = make_ipy(iface['addr'])
            #if DEBUG:
            #    print 'GOT ADDRS:', addrs
            #subns   = make_cidrs(iface.get('subnets', []) or iface['addr'])
            #acls_in  = iface.get('acl_in', [])
            #acls_out = iface.get('acl_out', [])
            #desctext = ' '.join(iface.get('description', [])).replace(' : ', ':')
            addrs   = iface['addr']
            subns   = iface['subnets']
            acls_in  = iface['acl_in']
            acls_out = iface['acl_out']
            desctext = ' '.join(iface.get('description')).replace(' : ', ':')

            if not opts.csv:
                desctext = desctext[0:50]
            if not addrs:
                continue
            
            addresses = [] 
            subnets   = [] 

            for x in addrs:
                addresses.append(x.strNormal())
            
            for x in subns:
                subnets.append(x.strNormal())

                if x in subnet_table:
                    subnet_table[x].append((dev, interface, addrs))
                else:
                    subnet_table[x] = [(dev, interface, addrs)]

            if DEBUG:
                print '\t in:', acls_in
                print '\t ou:', acls_out
            rows.append([interface, ' '.join(addresses), 
             ' '.join(subnets), '\n'.join(acls_in), '\n'.join(acls_out), desctext])

        if opts.csv:
            import csv
            writer = csv.writer(sys.stdout)
            for row in rows:
                writer.writerow([dev]+row)
        elif opts.dotty:
            continue
        elif opts.sqldb:
            write_sqldb(opts.sqldb, dev, rows)
        else: 
            print indent([labels]+rows, hasHeader=True, separateRows=False, 
              wrapfunc=lambda x: wrap_onspace(x,20), delim=' | ',wraplast=False)
        
    links = {}
            
    for ip,devs in subnet_table.iteritems():
        if len(devs) > 1: #and IP(ip).prefixlen() >= 30:
            router1 = devs[0][0]
            router2 = devs[1][0]

            kf1 = links.has_key(router1)
            kf2 = links.has_key(router2)

            if kf1:
                if router2 not in links[router1]:
                    links[router1].append(router2)
            
            elif kf2:
                if router1 not in links[router2]:
                    links[router2].append(router1)

            else:
                links[router1] = [router2] 

    if opts.dotty:
        print '''graph network {
    overlap=scale; center=true; orientation=land; 
    resolution=0.10; rankdir=LR; ratio=fill;
    node [fontname=Courier, fontsize=10]'''
        nd = NetDevices()
        for leaf,subleaves in links.iteritems():
            for subleaf in subleaves:
                print '"%s"--"%s"' % (nd[leaf].shortName, nd[subleaf].shortName)
            #print >>sys.stderr, leaf,"connects to: ",','.join(subleaves)
        print '}'
