#!/usr/bin/python2.6
# Copyright 2014 Locaweb.
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License");
#    you may not use this file except in compliance with the License.
#    You may obtain a copy of the License at
#
#        http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS,
#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#    See the License for the specific language governing permissions and
#    limitations under the License.
#
# @author: Juliano Martinez (ncode), Locaweb.
# @author: Luiz Ozaki, Locaweb.
# @author: Eduardo S. Scarpellini, Locaweb.

import pika
import signal
import re
import getopt, sys

from supay import Daemon
from datetime import datetime

import hashlib
import os
import sys
import json
import fcntl
import ConfigParser

from ipaddr import IPv4Network, IPv4Address, IPv6Network, IPv6Address, IPNetwork, IPAddress

from lockfile import FileLock, LockTimeout

import difflib
import logging

from subprocess import *

LOGGING_LEVELS = {'critical': logging.CRITICAL,
                  'error': logging.ERROR,
                  'warning': logging.WARNING,
                  'info': logging.INFO,
                  'debug': logging.DEBUG}


myname = os.uname()[1]
logger = None

class AgentException(Exception):
    pass

def _run_cmd(args):
    logger.warn("Running command: %s" % (" ".join(args)))
    p = Popen(args, stdout=PIPE, stderr=PIPE)
    retval, reterr = p.communicate()
    if p.returncode == -(signal.SIGALRM):
        logger.error("## timeout running command: " + " ".join(args))
    if p.returncode != 0:
        raise AgentException("Failed to run command %s.\nOutput %s" % (args, reterr))

    return retval

def add_vlan(switch, tag, ofport):
    bridge = switch.split(":")[1]
    command = ["ovs-ofctl", "add-flow", bridge, "table=0 priority=1 dl_vlan=%s actions=normal" % tag]
    _run_cmd(command)
    command = ["ovs-ofctl", "add-flow", bridge, "priority=50000,in_port=%s,action=normal" % ofport]
    _run_cmd(command)

def remove_vlan(switch, port):
    bridge = switch.split(":")[1]
    command = ["ovs-ofctl", "del-flows", bridge, "in_port=%s" % port]
    _run_cmd(command)

def get_ofport(port):
    command = ["ovs-vsctl", "get", "Interface", port, "ofport"]
    out = _run_cmd(command)
    if out:
        return out.strip()

def _natural_sort_(l):
    l = [str(i) for i in l]
    convert = lambda s: int(s) if s.isdigit() else s.lower()
    alphanum_key = lambda k: [convert(c) for c in re.split(r"([0-9]+)", k)]
    return sorted(l, key=alphanum_key)


class NetworkWorker(object):

    def __init__(self, body):
        self.switch_name    = None
        if (body.get("switch_id")):
            self.switch_name    = body.get("switch_id").get("name")
        self.vlan_id        = body.get("vlan_id")
        self.vlan_type      = self.vlan_id.get("type") if self.vlan_id is not None else None
        self.vlan_num       = self.vlan_id.get("vlan_num", {}) if self.vlan_id is not None else None
        self.vif            = body.get("name")
        self.mac            = body.get("id")
        self.ofport         = body.get("ofport")
        self.bridgeport     = body.get("bridgeport")

        if body.get("routers"):
            self.router_macs  = [x['mac'] for x in body.get("routers") if x.get("mac")]
        else:
            self.router_macs = []

    def add_flow(self, flow):
        # TODO: configure ovs remote access using
        # run ovs-vsctl set-controller xapi<number> pssl:port
        addr = self.switch_name.split(":")[1]
        command = ["ovs-ofctl", "add-flow", addr, flow]
        _run_cmd(command)

    def del_flow(self, flow):
        # TODO: configure ovs remote access using
        # run ovs-vsctl set-controller xapi<number> pssl:port
        addr = self.switch_name.split(":")[1]
        command = ["ovs-ofctl", "del-flows", addr, flow]
        _run_cmd(command)

    def add(self, data):
        if self.vlan_id is not None and self.vlan_type == "dedicated_vlan":
            add_vlan(self.switch_name, self.vlan_num, self.ofport)
        else:
            for ip in data['ips']:
                if ip['subnet']['vlan']['type'] == "private_vlan":
                    self.add_to_private_vlan(ip, data)

    def remove(self, data):
        if self.vlan_id is not None and self.vlan_type == "dedicated_vlan":
            remove_vlan(self.switch_name, self.ofport)
        else:
            for ip in data['ips']:
                if ip['subnet']['vlan']['type'] == "private_vlan":
                    self.remove_from_private_vlan(ip['ip'])
                else:
                    logger.info("Unknown type. Ignoring...")

    def add_to_private_vlan(self, ipdata, data):
        ip = ipdata['ip']
        subnet_vlan_num = ipdata['subnet']['vlan']['vlan_num']
        ##### TABLE 0 #####
        rule = "table=0 dl_src=01:00:00:00:00:00/01:00:00:00:00:00 actions=drop" # multicast
        self.add_flow(rule)

        rule = "table=0 dl_dst=01:80:c2:00:00:00/ff:ff:ff:ff:ff:f0 actions=drop" # spanning tree
        self.add_flow(rule)

        rule = "table=0 priority=0 actions=resubmit(,1)" # default
        self.add_flow(rule)

        ##### TABLE 1 #####
        rule = "table=1 priority=0 actions=drop"
        self.add_flow(rule)

        rule = "table=1 priority=100 dl_type=0x0806 actions=resubmit(,22)"
        self.add_flow(rule)

        for mac in self.router_macs:
            rule = "table=1 priority=99 in_port=1 dl_src=%s actions=resubmit(,2)" % mac
            self.add_flow(rule)
            rule = "table=1 priority=99 in_port=2 dl_src=%s actions=resubmit(,2)" % mac
            self.add_flow(rule)
            rule = "table=1 priority=99 dl_type=0x0800 dl_dst=%s actions=resubmit(,3)" % mac
            ##### TABLE 22 #####
            self.add_flow(rule)
            rule = "table=22 priority=100 dl_type=0x0806 arp_sha=%s nw_dst=%s actions=strip_vlan,%s" % (
                mac, ip, self.ofport
            )
            self.add_flow(rule)
            rule = "table=22 priority=99 vlan_tci=0 in_port=%s dl_type=0x0806 dl_src=%s dl_dst=%s actions=mod_vlan_vid:%s,bundle(eth_src,0,hrw,ofport,slaves:1,2)" % (
               self.ofport, self.mac, mac, subnet_vlan_num
            )
            self.add_flow(rule)

        ##### TABLE 2 #####
        rule = "table=2 priority=99 dl_type=0x0800 dl_dst=%s nw_dst=%s actions=strip_vlan,%s" % (
            self.mac, ip, self.ofport
        )
        self.add_flow(rule)

        ##### TABLE 3 #####
        rule = "table=3 priority=99 in_port=%s dl_type=0x0800 dl_src=%s nw_src=%s actions=mod_vlan_vid:%s,bundle(eth_src,0,hrw,ofport,slaves:1,2)" % (
            self.ofport, self.mac, ip, subnet_vlan_num
        )
        self.add_flow(rule)

        ##### TABLE 22 #####
        rule = "table=22 priority=99 vlan_tci=0 in_port=%s dl_type=0x0806 dl_src=%s dl_dst=ff:ff:ff:ff:ff:ff actions=mod_vlan_vid:%s,bundle(eth_src,0,hrw,ofport,slaves:1,2)" % (
            self.ofport, self.mac, subnet_vlan_num
        )
        self.add_flow(rule)

        rule = "table=22 priority=0 actions=drop"
        self.add_flow(rule)

        dhcp_request = "priority=41000,udp,in_port=%s,tp_src=68,tp_dst=67 "
        dhcp_request += "actions=mod_dl_dst:%s,%s"
        self.add_flow(dhcp_request % (self.ofport, data['switch_id']['mac'], self.bridgeport))

        dhcp_src = "priority=29000,arp,dl_vlan=%s,dl_src=%s actions=normal"
        self.add_flow(dhcp_src % (subnet_vlan_num, data['switch_id']['mac']))

        dhcp_dst = "priority=41000,arp,dl_vlan=%s,dl_dst=%s actions=%s"
        self.add_flow(dhcp_dst % (subnet_vlan_num, data['switch_id']['mac'], self.bridgeport))

        dhcp_reply = "priority=41000,udp,dl_src=%s,tp_src=67,"
        dhcp_reply += "tp_dst=68 actions=normal"
        self.add_flow(dhcp_reply % (data['switch_id']['mac']))

    def remove_from_private_vlan(self, ip):
        self.del_flow("dl_src=%s" % self.mac)
        self.del_flow("dl_dst=%s" % self.mac)
        self.del_flow("dl_type=0x0806 arp_tpa=%s" % ip)

    def remove_ip_from_private_vlan(self, ip):
        self.del_flow("ip,dl_src=%s,nw_src=%s" % (self.mac, ip))
        self.del_flow("ip,dl_dst=%s,nw_dst=%s" % (self.mac, ip))
        self.del_flow("arp,arp_tpa=%s" % ip)

def on_message(channel, method_frame, header_frame, body):
    logger.debug("Processing message with body: %s" % (body))
    try:
        body = json.loads(body)
        nw = NetworkWorker(body)
        if body.get("action"):
            if body["action"] == "plug" or body["action"] == "replug":
                nw.add(body)
            elif body["action"] == "unplug":
                nw.remove(body)
            elif body["action"] == "removeip":
                nw.remove_ip_from_private_vlan(body["ip"])
        else:
            logger.warn("Action not implemented: " + str(body))
        channel.basic_ack(delivery_tag=method_frame.delivery_tag)
    except AgentException as inst:
        channel.basic_ack(delivery_tag=method_frame.delivery_tag)
        logger.error(inst)

def help():
    print '%s::' % sys.argv[0]
    print '   -a action <stop|start|status|foreground>'
    print '   -u user <simplenet>'
    print '   -c config </etc/simplenet/agents.cfg>'


def main(user, action, config_file):
    global logger
    config = ConfigParser.ConfigParser()
    config.read(config_file)

    if config.has_option('ovs','hostname'):
        global myname
        myname = config.get('ovs','hostname')

    logger = create_logger(config)
    if action == 'foreground':
        logger.addHandler(logging.StreamHandler())

    cred = pika.PlainCredentials(config.get('ovs','broker_user'),config.get('ovs','broker_pass'))

    connection = pika.BlockingConnection(pika.ConnectionParameters(
        config.get('ovs','broker_host'),
        config.getint('ovs','broker_port'),
        '/',
        cred))

    channel = connection.channel()

    channel.queue_declare(queue=myname, durable=True, exclusive=False, auto_delete=False)
    channel.basic_consume(on_message, myname)

    try:
        channel.start_consuming()
    finally:
        connection.close()

def create_logger(config):
    # create logger
    logger = logging.getLogger('sn-ovs-agent')
    logger.setLevel(LOGGING_LEVELS.get(config.get('ovs', 'logging')))

    formatter = logging.Formatter('%(asctime)s [%(name)s/%(levelname)s] %(message)s')

    fileHandler = logging.FileHandler('/var/log/simplenet-ovs.log')
    fileHandler.setLevel(LOGGING_LEVELS.get(config.get('ovs', 'logging')))
    fileHandler.setFormatter(formatter)

    logger.addHandler(fileHandler)

    return logger

if __name__ == '__main__':
    try:
        opts, args = getopt.getopt(sys.argv[1:], 'a:u:c:', ['action=', 'user=', 'config='])
    except getopt.GetoptError, err:
        help()

    user = 'simplenet'
    action = 'start'
    config = '/etc/simplenet/agents.cfg'
    for o, a in opts:
        if o in ('-a', '--action'):
            action = a
        elif o in ('-u', '--user'):
            user = a
        elif o in ('-c', '--config'):
            config = a
        else:
            help()

    daemon = Daemon(name='simplenet', catch_all_log='/var/log/simplenet-ovs.log')
    if action == 'start':
        daemon.start()
        main(user, action, config)
    elif action == 'status':
        daemon.status()
    elif action == 'stop':
        daemon.stop()
    elif action == 'foreground':
        main(user, action, config)
