#!/usr/bin/env python

''' generates a YAML file representing a CLOS on a local machine
CLOS is described as a bunch of LEAF clusters each connected via a degree of switches
and then several SPINES, like this

This file generates clos-style fattrees in dot notation according to the
paper: "A Scalable Commodity Data Center Network Architecture" by Al-Fares,
Loukissas and Vahdat.

@todo: update to newest YAML format

@todo: hyperplane CLOS generation
'''

import argparse
import networkx as nx
from netaddr import EUI, mac_unix, IPAddress

def mk_mac(i):
    mac = EUI(i)
    mac.dialect = mac_unix
    return str(mac)

class UdpPortManager(object):
    def __init__(self,base=20000):
        self.base = base
        self.portdict = {}
        pass

    def get(self, srcnode, dstnode, flood=False):
        key = (srcnode,dstnode,flood)
        if self.portdict.has_key(key):
            return self.portdict[key]
        self.base += 1
        self.portdict[key] = self.base
        return self.base
    pass

def mk_ip(i):
    # Generate IPs starting at 111.0.0.0
    ip = IPAddress(111 * 256 * 256 * 256 + i * 256)
    return str(ip)

def agg_id(pod, switch_nr):
    return 100 * pod + switch_nr

def edge_id(pod, switch_nr):
    return 1000 * pod + switch_nr

def core_name(core):
    return 'core_' + str(core)

def agg_name(pod, switch_nr):
    return 'agg_' + str(agg_id(pod, switch_nr))

def edge_name(pod, switch_nr):
    return 'edge_' + str(edge_id(pod, switch_nr))

def print_node(f,nodedict):
    f.write("""
       - name: %(name)s
         level: %(level)s
         systemid: %(systemid)s""" % nodedict)
    if nodedict.has_key("statethriftserviceport"):
        f.write("""
         state_thrift_services_port: %(statethriftserviceport)s
         config_thrift_services_port: %(configthriftserviceport)s""" % nodedict)
        pass
    f.write("""
         interfaces:""" % nodedict)
    pass

def print_interface(f,intfdict):
    f.write("""
            - name: %(if_name)s
              metric: 1
              tx_lie_port: %(src_port)d
              rx_lie_port: %(dst_port)d
              flood_server_port: %(flood_port)d""" % intfdict)
    pass

def print_reverse_interface(f,intfdict):
    f.write("""
            - name: %(rev_if_name)s
              metric: 1
              tx_lie_port: %(dst_port)d
              rx_lie_port: %(src_port)d
              flood_server_port: %(rev_flood_port)d""" % intfdict)
    pass

def mk_topo(pods, aggr_per_pod, edge_per_pod, cores, portmanager, yaml, prefixes, serviceportmanager):
    # num_hosts         = (pods ** 3)/4

    core_switches = dict ( [ (i,(core_name(i), {'type':'switch','id':i}))
                       for i in range(1,cores + 1)] )

    agg_switches = dict ( [ (agg_id(p,i), (agg_name(p,i), {'type':'switch','id':agg_id(p,i)}))
                        for i in range(1,aggr_per_pod+1)
                        for p in range(1,pods+1)] )

    edge_switches = dict ( [ (edge_id(p,i), (edge_name(p,i), {'type':'switch','id':edge_id(p,i)}))
                        for i in range(1,edge_per_pod+1)
                        for p in range(1,pods+1)] )

    g = nx.DiGraph()
    # g.add_nodes_from(hosts)
    g.add_nodes_from( core_switches.values() )
    g.add_nodes_from(agg_switches.values() )
    g.add_nodes_from(edge_switches.values() )

    # connect core to aggr layer
    for p in range(1,pods+1):
        for c in range(1,cores+1):
            for a in range(1,aggr_per_pod+1):
                sp = portmanager.get(c,agg_id(p,a),False)
                dp = portmanager.get(agg_id(p,a),c,False)
                fp = portmanager.get(c,agg_id(p,a),True)
                rfp = portmanager.get(agg_id(p,a),c,True)

                # print "edge", core_switches[c][0], sp, agg_switches[agg_id(p,a)][0], dp
                g.add_edge( core_switches[c][0],
                            agg_switches[agg_id(p,a)][0],
                    {'if_name': "intf_%d_%d" % (c,agg_id(p,a)),
                     'rev_if_name' : "intf_%d_%d" % (agg_id(p,a),c),
                     'src_port': sp,
                     'dst_port': dp,
                     'flood_port': fp,
                     'rev_flood_port': rfp,
                     'capacity': 1000,
                     'cost' : 1 }
                            )
                pass
            pass
        pass

    # connect aggr layer to edge layer
    for p in range(1,pods+1):
        for a in range(1,aggr_per_pod+1):
            for e in range(1,edge_per_pod+1):
                sp = portmanager.get(agg_id(p,a),edge_id(p,e), False)
                dp = portmanager.get(edge_id(p,e), agg_id(p,a), False)
                fp = portmanager.get(agg_id(p,a),edge_id(p,e), True)
                rfp = portmanager.get(edge_id(p,e), agg_id(p,a), True)

                # print "edge", agg_switches[agg_id(p,a)][0], sp, edge_switches[edge_id(p,e)][0], dp
                g.add_edge( agg_switches[agg_id(p,a)][0],
                            edge_switches[edge_id(p,e)][0],
                    {'if_name': "intf_%d_%d" % (agg_id(p,a),edge_id(p,e)),
                     'rev_if_name': "intf_%d_%d" % (edge_id(p,e),agg_id(p,a)),
                     'src_port': sp,
                     'dst_port': dp,
                     'flood_port': fp,
                     'rev_flood_port': rfp,
                     'capacity': 1000,
                     'cost' : 1 }
                            )
                pass
            pass
        pass

    pfx = 0

    if yaml:
        with open(yaml, mode="w") as f:

            f.write("""
# yaml config
const:
    lie_mcast_address: 224.0.1.150
    lie_mcast_port: 10000 # unused currently
shards:
    id: 0 
    nodes:""")

            for c in core_switches:
                nodedict = { "name": core_name(c),
                "level": 2,
                "systemid": c,
                "statethriftserviceport": None if not serviceportmanager else serviceportmanager.get(c,c,False),
                "configthriftserviceport" : None if not serviceportmanager else serviceportmanager.get(c,c,True),
                }
                print_node(f,nodedict)

                for p in range(1, pods + 1):
                    for a in range(1, aggr_per_pod + 1):
                        intfdict = g.get_edge_data(core_name(c),agg_name(p,a))
                        # print c,agg_id(p,a),intfdict
                        print_interface(f,intfdict)
                        pass
                    pass
                pass

            for a in range(1, aggr_per_pod + 1):
                for p in range(1, pods + 1):

                    access = {"name": agg_name(p,a),
                              "level": 1,
                              "systemid": agg_id(p,a),
                              "statethriftserviceport": None if not serviceportmanager else serviceportmanager.get(p,a,
                                                                                                              False),
                              "configthriftserviceport": None if not serviceportmanager else serviceportmanager.get(p,a,True),
                              }
                    print_node(f,access)

                    for c in core_switches:
                        intfdict = g.get_edge_data(core_name(c), agg_name(p, a))
                        # print c,agg_id(p,a),intfdict
                        print_reverse_interface(f, intfdict)

                        pass

                    for e in range(1, edge_per_pod + 1):
                        intfdict = g.get_edge_data(agg_name(p,a),edge_name(p,e))
                        print_interface(f,intfdict)
                        pass
                    pass
                pass
            pass

            for e in range(1, edge_per_pod + 1):
                for p in range(1, pods + 1):
                    edge = {"name": edge_name(p, e),
                              "level": 0,
                              "systemid": edge_id(p, e),
                              "statethriftserviceport": None if not serviceportmanager else serviceportmanager.get(p,e,
                                                                                                              False),
                              "configthriftserviceport": None if not serviceportmanager else serviceportmanager.get(p,e,True),
                              }
                    print_node(f, edge)

                    for a in range(1, aggr_per_pod + 1):
                        intfdict = g.get_edge_data(agg_name(p,a),edge_name(p,e))
                        print_reverse_interface(f, intfdict)
                        pass

                    if prefixes:

                        f.write("""
         v4prefixes:""")

                        for px in xrange(0,prefixes):
                            pfx += 1
                            a = mk_ip(pfx)
                            f.write("""
            - address: %s
              mask: 24
              metric: 1""" % a)

                        pass
                    pass
                pass
            pass

            f.write("\n")
        pass

    # host_offset = 0
    # for pod in range(pods):
    #     core_offset = 0
    #     for sw in range(pods/2):
    #         switch = agg_switches[(pod*pods) + sw][0]
    #         # Connect to core switches
    #         for port in range(pods/2):
    #             core_switch = core_switches[core_offset][0]
    #             # g.add_edge(switch,core_switch,
    #             #            {'src_port':port,'dst_port':pod,'capacity':bw,'cost':1})
    #             g.add_edge(core_switch,switch,
    #                        {'src_port':pod,'dst_port':port,'capacity':bw,'cost':1})
    #             core_offset += 1
    #
    #         # Connect to aggregate switches in same pod
    #         for port in range(pods/2,pods):
    #             lower_switch = agg_switches[(pod*pods) + port][0]
    #             # g.add_edge(switch,lower_switch,
    #             #            {'src_port':port,'dst_port':sw,'capacity':bw,'cost':1})
    #             g.add_edge(lower_switch,switch,
    #                        {'src_port':sw,'dst_port':port,'capacity':bw,'cost':1})
    #
    #     # for sw in range(pods/2,pods):
    #     #     switch = agg_switches[(pod*pods) + sw][0]
    #     #     # Connect to hosts
    #     #     for port in range(pods/2,pods): # First k/2 pods connect to upper layer
    #     #         host = hosts[host_offset][0]
    #     #         # All hosts connect on port 0
    #     #         g.add_edge(switch,host,
    #     #                    {'src_port':port,'dst_port':0,'capacity':bw,'cost':1})
    #     #         g.add_edge(host,switch,
    #     #                    {'src_port':0,'dst_port':port,'capacity':bw,'cost':1})
    #     #         host_offset += 1

    return g

def parse_args():
    parser = argparse.ArgumentParser()
    parser.add_argument('-p','--pods',type=int,action='store',dest='pods',
                        default=2,
                        help='number of pods (parameter k in the paper)')
    parser.add_argument('-c','--cores',type=int,action='store',dest='cores',
                        default=2,
                        help='number of cores ')
    parser.add_argument('-a','--aggregates',type=int,action='store',dest='aggregates',
                        default=3,
                        help='number of aggregate switches per pod')

    parser.add_argument('-e','--edges',type=int,action='store',dest='edges',
                        default=4,
                        help='number of edge switches per pod')

    parser.add_argument('-P', '--prefix-per-edge', type=int, action='store', dest='prefixes',
                        default=10,
                        help='number of prefixes per edge switch')

    parser.add_argument('-b','--base',type=int,action='store',dest='base',
                        default=20000,
                        help='port base for udp ports used')

    parser.add_argument('-s', '--thrift-service-base', type=int, action='store', dest='servicebase',
                        default=None,
                        help='port base for thrift service ports used, if set, each node exposes RIFT thrift services')

    parser.add_argument('-o', '--out', action='store',dest='output',
                        default=None,
                        help='dot file to write to')

    parser.add_argument('-y', '--yaml', action='store',dest='yaml',
                        default=None,
                        help='yaml file to write config to')

    return parser.parse_args()

if __name__ == '__main__':
    args = parse_args()
    topo = mk_topo(args.pods,args.aggregates,args.edges,args.cores,
                   UdpPortManager(args.base),
                   args.yaml,
                   args.prefixes,
                   None if not args.servicebase else UdpPortManager(args.servicebase))

    if args.output:
        nx.drawing.nx_agraph.write_dot(topo, args.output)
    else:
        print nx.drawing.nx_agraph.to_agraph(topo)
