#!/usr/bin/python3.6

'''
Quick Network Settings
(c) 2022-2023 Jan ONDREJ (SAL) <ondrejj(at)salstar.sk>
Licensed under GPLv2+.

Usage:
	nw show [ip|addr|device|connection|bridge|team] [DEVICE]
	nw [connection|bridge|team] show
	# permanent commands (nmcli):
	nw add ethernet DEVICE
	nw add vlan DEVICE.X [vlanNAME]
	nw add bridge NAME ports DEVICE DEVICE ...
	nw add bridge NAME vlan DEVICE.vlanid # bridge from VLAN
	nw add iface BRIDGE_NAME DEVICE.vlanid # add VLAN to bridge
	nw add team NAME ports DEVICE DEVICE ...
	nw modify DEVICE addr auto|disabled|IP/MASK gw GATEWAY dns IP
	nw modify DEVICE addr6 auto|disabled|IP/mask gw6 GATEWAY dns IP
	nw up|down|restart CONNECTION
	nw delete CONNECTION
	# temporary commands (ip link):
	nw create vlan DEVICE.X [vlanNAME]
	nw create bridge NAME ports DEVICE DEVICE ...
'''

import sys, os, json, re

def nmcli(cmd):
    print("nmcli "+cmd)
    os.system("nmcli "+cmd)

def ip_link(cmd):
    print("ip link "+cmd)
    os.system("ip link "+cmd)

def eprint(*args, **kw):
    print(*args, **kw, file=sys.stderr)

def show_addrs(device=""):
    if device:
        device = " dev "+device
    ipa = json.load(os.popen("ip --json addr show" + device))
    for eth in ipa:
        ifname = eth["ifname"]
        if "altnames" in eth:
            ifname = ifname+"("+",".join(eth.get("altnames", ""))+")"
        print("%s: <%s>" % (ifname, ",".join(eth["flags"])))
        print("%10s: %s" % ("ether", eth["address"]))
        for addr in eth["addr_info"]:
            print("%10s: %s (%s/%s)" % (
                addr["family"],
                addr["local"],
                addr["local"], addr["prefixlen"]
            ))

def show(what="", device=""):
    if what=="ip" or what=="addr" or what=="device":
        show_addrs(device)
    elif what=="bridge":
        bridges = json.load(os.popen("bridge --json link show"))
        masters = [x["master"] for x in bridges if "master" in x]
        for master in set(masters):
            if device and master!=device:
                continue
            print(master+":")
            for slave in bridges:
                if "master" in slave and slave["master"]==master:
                    if "hwmode" in slave:
                        print("\t%-15s\t\t\thwmode=%s" % (
                            slave["ifname"], slave.get("hwmode")
                        ))
                    else:
                        print("\t%-15s\t%s\tprio=%s\tcost=%s" % (
                            slave["ifname"], slave.get("state"),
                            slave.get("priority"), slave.get("cost")
                        ))
    elif what=="team":
        if device:
            teamdevs = [device]
        else:
            teamdevs = [
                x.split(":", 1)[0].strip()
                for x in open("/proc/net/dev").readlines()[2:]
                if x.strip().startswith("team")
                   and "." not in x # ignore vlan devices
            ]
        for teamdev in teamdevs:
            state = json.load(os.popen("teamdctl %s state dump" % teamdev))
            ifinfo = state["team_device"]["ifinfo"]
            print("%s: %s [%s]" % (
                ifinfo["ifname"],
                ifinfo["dev_addr"],
                state["setup"]["runner_name"]
            ))
            for port in state["ports"].values():
                ifinfo = port["ifinfo"]
                link = port["link"]
                print("\t%s: %s" % (
                    ifinfo["ifname"],
                    ifinfo["dev_addr"]
                ))
                updown = "UP" if link["up"] else "DOWN"
                print("\t\t%s, %s Mbit/s, %s duplex" % (
                    "UP" if link["up"] else "DOWN",
                    link["speed"],
                    link["duplex"]
                ))
    elif what=="connection" or what=="":
        nmcli("con")


#ip_disabled = " ipv4.method disabled ipv6.method disabled"
IP_DISABLED = ["addr4", "disabled", "addr6", "disabled"]

def set_ip(args):
    ipaddr, ipaddr6 = [], []
    gateway = gateway6 = None
    dns, dns6 = [], []
    while args:
        if args[0]=="addr" or args[0]=="addr4" or args[0]=="address":
            args.pop(0)
            if args[0]=="auto" or args[0]=="disabled":
                ipaddr = args.pop(0)
            else:
                ipaddr.append(args.pop(0))
        elif args[0]=="addr6":
            args.pop(0)
            if args[0]=="auto" or args[0]=="disabled":
                ipaddr6 = args.pop(0)
            else:
                ipaddr6.append(args.pop(0))
        elif args[0]=="gw" or args[0]=="gw4":
            args.pop(0)
            gateway = args.pop(0)
        elif args[0]=="gw6":
            args.pop(0)
            gateway6 = args.pop(0)
        elif args[0]=="dns" or args[0]=="dns4":
            args.pop(0)
            dns.append(args.pop(0))
        elif args[0]=="dns6":
            args.pop(0)
            dns6.append(args.pop(0))
    cmd = ""
    # IPv4
    if ipaddr=="auto" or ipaddr=="disabled":
        cmd += " ipv4.method " + ipaddr
    elif ipaddr:
        cmd += " ipv4.method manual ipv4.addresses " + ",".join(ipaddr)
        if gateway:
            cmd += " ipv4.gateway " + gateway
        else:
            cmd += " ipv4.never-default yes"
        # ignore auto routes and DNS for manual settings
        cmd += " ipv4.ignore-auto-routes yes"
        cmd += " ipv4.ignore-auto-dns yes"
        cmd += " ipv4.may-fail no"
    if dns:
        cmd += " ipv4.dns " + ",".join(dns)
    # IPv6
    if ipaddr6=="auto" or ipaddr6=="disabled":
        cmd += " ipv6.method " + ipaddr6
    elif ipaddr6:
        cmd += " ipv6.method manual ipv6.addresses " + ",".join(ipaddr6)
        if gateway6:
            cmd += " ipv6.gateway " + gateway6
        else:
            cmd += " ipv6.never-default yes"
        # ignore auto routes and DNS for manual settings
        cmd += " ipv6.ignore-auto-routes yes"
        cmd += " ipv6.ignore-auto-dns yes"
        cmd += " ipv6.may-fail no"
    if dns6:
        cmd += " ipv6.dns " + ",".join(dns6)
    return cmd

def add_ethernet(ifname, ip_args=[]):
    cmd = "con add con-name %s type ethernet ifname %s" % (ifname, ifname)
    cmd += set_ip(ip_args)
    nmcli(cmd)

def add_vlan(vlan_dev, vlan_name="", bridge="", ip_args=[]):
    device, vlan_id = vlan_dev.split(".", 1)
    name = vlan_name or "%s.%s" % (device, vlan_id)
    cmd = "con add con-name %s type vlan ifname %s" % (name, name)
    cmd += " dev %s id %s" % (device, vlan_id)
    if bridge:
        cmd += " master %s" % bridge
    cmd += set_ip(ip_args)
    nmcli(cmd)

def add_bridge(master, type_verb, *slaves, ip_args=[]):
    if type_verb not in ["ports", "vlan"]:
        print("Wrong parameter: %s. Must be ports or vlan!" % type_verb)
        return
    if not ip_args:
        ip_args = IP_DISABLED
    nmcli(
        "con add con-name %s type bridge ifname %s %s"
        % (master, master, set_ip(ip_args))
    )
    for slave in slaves:
        if type_verb=="vlan":
            add_vlan(slave, bridge=master, ip_args=IP_DISABLED)
        elif type_verb=="ports":
            nmcli(
                "con add type bridge-slave ifname %s con-name %s-%s master %s"
                " ipv4.method disabled ipv6.method disabled"
                % (slave, master, slave, master)
            )

def add_iface(master, *slaves):
    for slave in slaves:
        nmcli(
            "con add type bridge-slave ifname %s con-name %s-%s master %s"
            % (slave, master, slave, master)
        )

def add_team(master, ports_verb, *slaves, ip_args=[]):
    json_config = '{"runner": {"name": "lacp"}}'
    if not ip_args:
        ip_args = IP_DISABLED
    nmcli(
        "con add con-name %s type team ifname %s team.config '%s' %s"
        % (master, master, json_config, set_ip(ip_args))
    )
    for slave in slaves:
        nmcli(
            "con add type ethernet con-name %s-%s ifname %s slave-type team master %s"
            % (master, slave, slave, master)
        )

def create_vlan(vlan_dev, vlan_name="", ip_args=[]):
    '''Create temporary VLAN'''
    device, vlan_id = vlan_dev.split(".", 1)
    name = vlan_name or "%s.%s" % (device, vlan_id)
    ip_link("add link %s name %s type vlan id %s" % (
      device, vlan_dev, vlan_id
    ))

def create_bridge(master, ports_verb, *slaves, ip_args=[]):
    '''Create temporary bridge'''
    ip_link("add name %s type bridge" % master)
    ip_link("set dev %s up" % master)
    for slave in slaves:
        ip_link("set dev %s master %s" % (slave, master))

def modify_ip(device, ip_args):
    nmcli("con modify %s%s" % (device, set_ip(ip_args)))
    nmcli("con up %s" % device) # apply

def connection_up(devices):
    for device in devices:
        nmcli("con up %s" % device)

def connection_down(devices):
    for device in devices:
        nmcli("con down %s" % device)

def connection_restart(devices):
    for device in devices:
        nmcli("con down %s" % device)
    for device in devices[::-1]: # start in reverse order
        nmcli("con up %s" % device)

def delete_connections(devices):
    for device in devices:
        nmcli("con delete %s" % device)

def complete(cword, words):
    if len(words)>cword-1:
        cur = words[cword-1]
    else:
        cur = ""

    class connections(list):
      def __init__(self, **kw):
          super().__init__(self.values(**kw))
          self.used = set()
      def values(self):
          return os.popen(
                     "nmcli --get NAME --color no con show"
                 ).read().split("\n")
      def use(self, word):
          if word in self:
              #eprint("USE:", word)
              self.remove(word)

    class ifaces(connections):
      def values(self, ext="", exclude="^$"):
          return [
              x.split(":", 1)[0].strip()+ext
              for x in open("/proc/net/dev").readlines()[2:]
              if x.split(":", 1)[0].strip() not in ["", "lo"]
                 and not re.search(exclude, x.strip())
          ]

    def cprint(words):
        if type(words)==str:
            words = words.split(" ")
        elif type(words)==arg_dict:
            words = words.keys()
        #eprint("CPRINT:", words, type(words), cur)
        for word in words:
            if word.startswith(cur):
                print(word)

    def next_device(prefix):
        cons = connections()
        for i in range(999):
            if "%s%d" % (prefix, i) not in cons:
                return "%s%d" % (prefix, i)
        return prefix+"X"

    class arg_dict(dict):
        def __init__(self, **kw):
            super().__init__(**kw)
        def __call__(self, key):
            return self.keys(), self.get(key)

    class arg_list(list):
        def __init__(self, *args, repeat=None):
            super().__init__(args)
            self.repeat = repeat
        def __call__(self, key):
            #eprint(len(self), self, self.repeat)
            if not self:
                if self.repeat:
                    return self.repeat[0], arg_list(*self.repeat[1:], repeat=self.repeat)
                else:
                    return [], arg_list()
            return self[0], arg_list(*self[1:], repeat=self.repeat)

    arg_ip_set = [
        ["addr", "addr4", "addr6"],
            ["auto", "disabled",
             "192.168.0.1/24", "172.16.0.1/16", "10.0.0.1/8"],
        ["gw", "gw4", "gw6"],
            ["192.168.0.1", "172.16.0.1", "10.0.0.1"],
        ["dns", "dns4", "dns6"],
            ["192.168.0.1", "172.16.0.1", "10.0.0.1"]
    ]

    arg_values = arg_dict(
        show = arg_dict(
            ip = arg_list(ifaces()),
            addr = arg_list(ifaces()),
            device = arg_list(ifaces()),
            bridge = arg_list(ifaces()),
            team = arg_list(ifaces()),
            connections = arg_list()
        ),
        connection = arg_list(["show"]),
        bridge = arg_list(["show"]),
        team = arg_list(["show"]),
        add = arg_dict(
            ethernet = arg_list(ifaces(exclude=r"\.")),
            vlan = arg_list(ifaces(ext=".", exclude="^vlan")),
            bridge = arg_list(next_device("br"), ["ports", "vlan"],
                              repeat=[ifaces(exclude="^br")]),
            iface = arg_list(ifaces(), repeat=[ifaces()]),
            team = arg_list(next_device("team"), "ports",
                            repeat=[ifaces(exclude="^team")])
        ),
        create = arg_dict(
            vlan = arg_list(ifaces(ext=".", exclude="^vlan")),
            bridge = arg_list(next_device("br"), "ports",
                              repeat=[ifaces(exclude="^br")])
        ),
        modify = arg_list(ifaces(), repeat=arg_ip_set),
        up = arg_list(repeat=[connections()]),
        down = arg_list(repeat=[connections()]),
        restart = arg_list(repeat=[connections()]),
        delete = arg_list(repeat=[connections()])
    )

    #eprint(words)
    arg_opts = arg_values
    for i in range(cword):
        if i>=len(words):
            arg_opts, arg_values = arg_values("")
            break
        arg_opts, arg_values = arg_values(words[i])
        #eprint(i, type(arg_opts), arg_opts)
        #eprint(" ", type(arg_values), arg_values)
        if isinstance(arg_opts, connections):
            arg_opts.use(words[i])
    cprint(arg_opts)

if __name__=="__main__":
    if not sys.argv[1:] or sys.argv[1]=="help":
        print(__doc__.strip())
    elif sys.argv[1]=="show":
        show(*sys.argv[2:])
    elif sys.argv[1] in ("connection", "bridge", "team") and sys.argv[2]=="show":
        show(sys.argv[1])
    elif sys.argv[1]=="add":
        if sys.argv[2]=="ethernet":
            add_ethernet(sys.argv[3], ip_args=sys.argv[4:])
        elif sys.argv[2]=="vlan":
            add_vlan(*sys.argv[3:])
        elif sys.argv[2]=="bridge":
            add_bridge(*sys.argv[3:])
        elif sys.argv[2]=="iface":
            add_iface(*sys.argv[3:])
        elif sys.argv[2]=="team":
            add_team(*sys.argv[3:])
    elif sys.argv[1]=="create":
        if sys.argv[2]=="vlan":
            create_vlan(*sys.argv[3:])
        elif sys.argv[2]=="bridge":
            create_bridge(*sys.argv[3:])
        elif sys.argv[2]=="team":
            create_team(*sys.argv[3:])
    elif sys.argv[1]=="modify":
        modify_ip(sys.argv[2], sys.argv[3:])
    elif sys.argv[1]=="up":
        connection_up(sys.argv[2:])
    elif sys.argv[1]=="down":
        connection_down(sys.argv[2:])
    elif sys.argv[1]=="restart":
        connection_restart(sys.argv[2:])
    elif sys.argv[1]=="delete":
        delete_connections(sys.argv[2:])
    elif sys.argv[1]=="complete":
        #eprint(sys.argv)
        complete(int(sys.argv[2]), sys.argv[4:])
    else:
        print("Parse error:", " ".join(sys.argv[1:]))
