#!/usr/bin/env python
#
# Copyright (C) 2014 Nippon Telegraph and Telephone Corporation.
#
# 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.

#
# Examples
#
# - get the state of neighbors
# $ gobgpcli show neighbors
# - get the state of a neighbor
# $ gobgpcli show neighbor 10.0.0.2
# - get the local rib of a neighbor
# $ gobgpcli show neighbor 10.0.0.2 local
# - reset
# $ gobgpcli reset neighbor 10.0.0.2
# - softresetin
# $ gobgpcli softresetin neighbor 10.0.0.2
# - softresetout
# $ gobgpcli softresetout neighbor 10.0.0.2
# - shutdown
# $ gobgpcli shutdown neighbor 10.0.0.2

from optparse import OptionParser
import requests
import sys
import inspect
import json
from datetime import timedelta


class Action(object):
    def __init__(self, command, options, args):
        super(Action, self).__init__()
        self.command = command
        self.options = options
        self.args = args
        self.base_url = self.options.url + ":" + str(self.options.port) + "/v1/bgp"

    def __call__(self):
        if len(self.args) != 2:
            return 1
        try:
            r = requests.post(self.base_url + "/neighbor/" + self.args[1] + "/" + self.command)
        except:
            print "Failed to connect to gobgpd. It runs?"
            sys.exit(1)
        else:
            if r.status_code == requests.codes.ok:
                print "Succeed"
            else:
                print "Failed"
            return 0


class Show(object):
    def __init__(self, _command, options, args):
        super(Show, self).__init__()
        self.options = options
        self.args = args
        self.base_url = self.options.url + ":" + str(self.options.port) + "/v1/bgp"

    def __call__(self):
        if len(self.args) == 0:
            return 1

        for f in inspect.getmembers(self, inspect.ismethod):
            func_name = f[0]
            if func_name == "do_" + self.args[0]:
                return f[1]()
        return 1

    def do_global(self):
        if len(self.args) != 1 and len(self.args) != 2:
            return 1

        url = self.base_url + "/global/rib"
        if len(self.args) == 2:
            url += "/" + self.args[1]
        else:
            url += "/ipv4"

        try:
            r = requests.get(url)
        except:
            print "Failed to connect to gobgpd. It runs?"
            sys.exit(1)

        f = "{:2s} {:18s} {:15s} {:10s} {:10s} {:s}"
        print(f.format("", "Network", "Next Hop", "AS_PATH", "Age", "Attrs"))

        for d in r.json()["Destinations"]:
            d["Paths"][d["BestPathIdx"]]["Best"] = True
            self.show_routes(f, d["Paths"], True, True)

        return 0

    def _neighbor(self, neighbor=None):
        capdict = {1: "MULTIPROTOCOL",
                   2: "ROUTE_REFRESH",
                   4: "CARRYING_LABEL_INFO",
                   64: "GRACEFUL_RESTART",
                   65: "FOUR_OCTET_AS_NUMBER",
                   70: "ENHANCED_ROUTE_REFRESH",
                   128: "ROUTE_REFRESH_CISCO"}
        try:
            r = requests.get(self.base_url + "/neighbors")
        except:
            print "Failed to connect to gobgpd. It runs?"
            sys.exit(1)

        neighbors = r.json()
        if self.options.debug:
            print neighbors
            return 0
        for n in sorted(neighbors, key=lambda n: n["conf"]["remote_ip"]):
            if neighbor is not None and neighbor != n["conf"]["remote_ip"]:
                continue
            print("BGP neighbor is {:s}, remote AS {:d}".format(n["conf"]["remote_ip"], n["conf"]["remote_as"]))
            print("  BGP version 4, remote router ID {:s}".format(n["conf"]["id"]))
            print("  BGP state = {:s}, up for {:s}".format(n["info"]["bgp_state"], str(timedelta(seconds=n["info"]["uptime"]))))
            print("  BGP OutQ = {:d}, Flops = {:d}".format(n["info"]["OutQ"], n["info"]["Flops"]))
            print("  Neighbor capabilities:")
            allcap = set(n["conf"]["RemoteCap"]) | set(n["conf"]["LocalCap"])
            for i in sorted(allcap):
                if i in capdict:
                    k = capdict[i]
                else:
                    k = "UNKNOWN (" + str(i) + ")"
                r = ""
                if i in n["conf"]["LocalCap"]:
                    r += "advertised"
                if i in n["conf"]["RemoteCap"]:
                    if len(r) != 0:
                        r += " and "
                    r += "received"
                print("    {:s}: {:s}".format(k, r))
            print("  Message statistics:")
            print("                         Sent       Rcvd")
            print("    Opens:         {:>10d} {:>10d}".format(n["info"]["open_message_out"], n["info"]["open_message_in"]))
            print("    Notifications: {:>10d} {:>10d}".format(n["info"]["notification_out"], n["info"]["notification_in"]))
            print("    Updates:       {:>10d} {:>10d}".format(n["info"]["update_message_out"], n["info"]["update_message_in"]))
            print("    Keepalives:    {:>10d} {:>10d}".format(n["info"]["keepalive_message_out"], n["info"]["keepalive_message_in"]))
            print("    Route Refesh:  {:>10d} {:>10d}".format(n["info"]["refresh_message_out"], n["info"]["refresh_message_in"]))
            print("    Discarded:     {:>10d} {:>10d}".format(n["info"]["DiscardedOut"], n["info"]["DiscardedIn"]))
            print("    Total:         {:>10d} {:>10d}".format(n["info"]["total_message_out"], n["info"]["total_message_in"]))
            print("")
        return 0

    @staticmethod
    def format_timedelta(sec):
        days = timedelta(seconds=sec).days
        t = timedelta(seconds=timedelta(seconds=sec).seconds)
        if days == 0:
            up = t
        else:
            up = str(days) + "d" + " " + str(t)
        return up

    def do_neighbors(self):
        if len(self.args) != 1:
            return 1
        try:
            r = requests.get(self.base_url + "/neighbors")
        except:
            print "Failed to connect to gobgpd. It runs?"
            sys.exit(1)

        neighbors = r.json()
        if self.options.debug:
            print neighbors
            return 0
        sorted_neighbors = []
        for n in sorted(neighbors, key=lambda n: n["conf"]["remote_ip"]):
            sorted_neighbors.append(n)
        if self.options.quiet:
            sorted_neighbors_ip = []
            for n in sorted_neighbors:
                print str(n["conf"]["remote_ip"])
            return 0
        maxaddrlen = 0
        maxaslen = 0
        maxtimelen = len("Up/Down")
        for n in sorted_neighbors:
            if len(n["conf"]["remote_ip"]) > maxaddrlen:
                maxaddrlen = len(n["conf"]["remote_ip"])
            if len(str(n["conf"]["remote_as"])) > maxaslen:
                maxaslen = len(str(n["conf"]["remote_as"]))

            s = n["info"]["bgp_state"]
            if n["info"]["uptime"] == 0:
                t = "never"
            elif s == "BGP_FSM_ESTABLISHED":
                t = self.format_timedelta(n["info"]["uptime"])
            else:
                t = self.format_timedelta(n["info"]["downtime"])
            if len(str(t)) > maxtimelen:
                maxtimelen = len(str(t))
            n["info"]["time"] = t

        h = "{:%ds} {:>%ds} {:%ds} {:11s} |" % (maxaddrlen, maxaslen, maxtimelen)
        f1 = h + "#{:8s} {:8s} {:8s}"
        print(f1.format("Peer", "AS", "Up/Down", "State", "Advertised", "Received", "Accepted"))
        for n in sorted_neighbors:
            s = n["info"]["bgp_state"]
            if s == "BGP_FSM_IDLE":
                state = "Idle"
            elif s == "BGP_FSM_CONNECT":
                state = "Connect"
            elif s == "BGP_FSM_ACTIVE":
                state = "Active"
            elif s == "BGP_FSM_OPENSENT":
                state = "Sent"
            elif s == "BGP_FSM_OPENCONFIRM":
                state = "Confirm"
            elif s == "BGP_FSM_ESTABLISHED":
                state = "Establ"

            if n["info"]["AdminState"] == "ADMIN_STATE_DOWN":
                state = "Idle(Admin)"

            f2 = h + "   {:>8d} {:>8d} {:>8d}"
            print f2.format(n["conf"]["remote_ip"], str(n["conf"]["remote_as"]), n["info"]["time"], state, n["info"]["Advertized"], n["info"]["Received"], n["info"]["Accepted"])
        return 0

    def _format_attrs(self, attrlist):
        attrs = []
        for a in attrlist:
            if a["Type"] == "BGP_ATTR_TYPE_NEXT_HOP":
                pass
            elif a["Type"] == "BGP_ATTR_TYPE_AS_PATH":
                pass
            elif a["Type"] == "BGP_ATTR_TYPE_ORIGIN":
                attrs.append({"Origin": a["Value"]})
            elif a["Type"] == "BGP_ATTR_TYPE_MULTI_EXIT_DISC":
                attrs.append({"Med": a["Metric"]})
            elif a["Type"] == "BGP_ATTR_TYPE_LOCAL_PREF":
                attrs.append({"LocalPref": a["Pref"]})
            elif a["Type"] == "BGP_ATTR_TYPE_ATOMIC_AGGREGATE":
                attrs.append("AtomicAggregate")
            elif a["Type"] == "BGP_ATTR_TYPE_AGGREGATE":
                attrs.append({"Aggregate": {"AS": a["AS"], "Address": a["Address"]}})
            elif a["Type"] == "BGP_ATTR_TYPE_COMMUNITIES":
                wellknown = {
                    0xffff0000: "planned-shut",
                    0xffff0001: "accept-own",
                    0xffff0002: "ROUTE_FILTER_TRANSLATED_v4",
                    0xffff0003: "ROUTE_FILTER_v4",
                    0xffff0004: "ROUTE_FILTER_TRANSLATED_v6",
                    0xffff0005: "ROUTE_FILTER_v6",
                    0xffff0006: "LLGR_STALE",
                    0xffff0007: "NO_LLGR",
                    0xFFFFFF01: "NO_EXPORT",
                    0xFFFFFF02: "NO_ADVERTISE",
                    0xFFFFFF03: "NO_EXPORT_SUBCONFED",
                    0xFFFFFF04: "NOPEER"}

                l = []
                for v in a["Value"]:
                    if v in wellknown:
                        l.append(wellknown[v])
                    else:
                        l.append(str((0xffff0000 & v) >> 16) + ":" + str(0xffff & v))
                attrs.append({"Community": l})
            elif a["Type"] == "BGP_ATTR_TYPE_ORIGINATOR_ID":
                attrs.append({"Originator": a["Address"]})
            elif a["Type"] == "BGP_ATTR_TYPE_CLUSTER_LIST":
                attrs.append({"Cluster": a["Address"]})
            elif a["Type"] == "BGP_ATTR_TYPE_MP_REACH_NLRI":
                pass
            elif a["Type"] == "BGP_ATTR_TYPE_MP_UNREACH_NLRI":
                pass
            elif a["Type"] == "BGP_ATTR_TYPE_AS4_PATH":
                pass
            else:
                attrs.append({a["Type"]: a["Value"]})
        return attrs

    def do_neighbor(self):
        if len(self.args) != 2 and len(self.args) != 3 and len(self.args) != 4:
            return 1
        if len(self.args) == 2:
            return self._neighbor(neighbor=self.args[1])
        if self.args[2] in ("local", "local-rib"):
            self.args[2] = "local-rib"
        elif self.args[2] in ("received-routes", "adj-rib-in", "adj-in"):
            self.args[2] = "adj-rib-in"
        elif self.args[2] in ("advertised-routes", "adj-rib-out", "adj-out"):
            self.args[2] = "adj-rib-out"
        else:
            print self.args[2], ": No such command"
            return 1

        try:
            r = requests.get(self.base_url + "/neighbor/" + self.args[1] + "/" + self.args[2])
            url = self.base_url + "/neighbor/" + self.args[1] + "/" + self.args[2]
            if len(self.args) == 3:
                if self.args[2].find(':') == -1:
                    url += "/ipv4"
                else:
                    url += "/ipv6"
            else:
                url += "/" + self.args[3]
            r = requests.get(url)
        except:
            print "Failed to connect to gobgpd. It runs?"
            sys.exit(1)

        if self.options.debug:
            print r.json()
            return 0

        timestamp = True
        if self.args[2] == "adj-rib-out":
            timestamp = False

        if timestamp:
            f = "{:2s} {:18s} {:15s} {:10s} {:10s} {:s}"
            print(f.format("", "Network", "Next Hop", "AS_PATH", "Age", "Attrs"))
        else:
            f = "{:2s} {:18s} {:15s} {:10s} {:s}"
            print(f.format("", "Network", "Next Hop", "AS_PATH", "Attrs"))

        if self.args[2] == "local-rib":
            for d in r.json()["Destinations"]:
                d["Paths"][d["BestPathIdx"]]["Best"] = True
                self.show_routes(f, d["Paths"], True, True)

        elif self.args[2] == "adj-rib-in" or self.args[2] == "adj-rib-out":
            rfs = ["RF_IPv4_UC", "RF_IPv6_UC"]
            for rf in rfs:
                if rf in r.json():
                    paths = r.json()[rf]
                    self.show_routes(f, paths, False, timestamp)
        return 0

    def show_routes(self, f, paths, showBest=False, timestamp=False):
        for p in paths:
            nexthop = ""
            AS = ""
            for a in p["Attrs"]:
                if a["Type"] == "BGP_ATTR_TYPE_AS_PATH":
                    AS = a["AsPath"]
            if showBest:
                if "Best" in p:
                    header = "*>"
                else:
                    header = "*"
            else:
                header = ""
            if timestamp:
                print(f.format(header, p["Network"], p["Nexthop"], AS, self.format_timedelta(p["Age"]), self._format_attrs(p["Attrs"])))
            else:
                print(f.format(header, p["Network"], p["Nexthop"], AS, self._format_attrs(p["Attrs"])))


def main():
    usage = "gobpgcli [options] <command> <args>"
    parser = OptionParser(usage)

    parser.add_option("-u", "--url", dest="url", default="http://localhost",
                      help="specifying an url (http://localhost by default)")
    parser.add_option("-p", "--port", dest="port", default=8080,
                      help="specifying a port (8080 by default)")
    parser.add_option("-d", "--debug", dest="debug", action="store_true",
                      help="dump raw json")
    parser.add_option("-q", "--quiet", dest="quiet", action="store_true",
                      help="for shell completion")

    (options, args) = parser.parse_args()

    commands = {"show": Show,
                "reset": Action,
                "shutdown": Action,
                "softreset": Action,
                "softresetin": Action,
                "softresetout": Action,
                "enable": Action,
                "disable": Action}

    if len(args) == 0:
        parser.print_help()
        sys.exit(1)

    if args[0] not in commands:
        parser.print_help()
        sys.exit(1)

    ret = commands[args[0]](args[0], options, args[1:])()
    if ret != 0:
        parser.print_help()
        sys.exit(1)

if __name__ == "__main__":
    main()