
from __future__ import absolute_import
from __future__ import division

from fnmatch import fnmatch
from glob import iglob
import errno
import itertools
import os

import six

from netaddr.core import AddrFormatError
from netaddr import IPAddress
from netaddr import IPNetwork

from vdsm.common.cmdutils import CommandPath
from vdsm.common.compat import subprocess
from vdsm.common.config import config
from vdsm.network import cmd
from vdsm.network import ethtool
from vdsm.network.link import dpdk
from vdsm.network.netlink import libnl
from vdsm.network.netlink import link

_IP_BINARY = CommandPath('ip', '/sbin/ip')

NET_SYSFS = '/sys/class/net'
DUMMY_BRIDGE = ';vdsmdummy;'
_ROUTE_FLAGS = frozenset((
    'unicast',
    'local',
    'broadcast',
    'anycast',
    'multicast',
    'blackhole',
    'unreachable',
    'prohibit',
    'throw',
    'nat',
    'xresolve',
))


def _isValid(ip, verifier):
    try:
        verifier(ip)
    except (AddrFormatError, ValueError):
        return False

    return True


def equals(cls):
    def __eq__(self, other):
        return type(other) == cls and self.__dict__ == other.__dict__
    cls.__eq__ = __eq__
    return cls


class LinkType(object):
    NIC = 'nic'
    VLAN = 'vlan'
    BOND = 'bond'
    BRIDGE = 'bridge'
    LOOPBACK = 'loopback'
    MACVLAN = 'macvlan'
    DPDK = 'dpdk'
    DUMMY = 'dummy'
    TUN = 'tun'
    OVS = 'openvswitch'
    TEAM = 'team'
    VETH = 'veth'
    VF = 'vf'


@equals
class Link(object):
    _fakeNics = config.get('vars', 'fake_nics').split(',')
    _hiddenBonds = config.get('vars', 'hidden_bonds').split(',')
    _hiddenNics = config.get('vars', 'hidden_nics').split(',')
    _hiddenVlans = config.get('vars', 'hidden_vlans').split(',')

    def __init__(self, address, index, linkType, mtu, name, qdisc, state,
                 vlanid=None, vlanprotocol=None, master=None, device=None,
                 **kwargs):
        self.address = address
        self.index = index
        self.type = linkType
        self.mtu = mtu
        self.name = name
        self.qdisc = qdisc
        self.state = state
        self.master = master
        if vlanid is not None:
            self.vlanid = vlanid
        if vlanprotocol is not None:
            self.vlanprotocol = vlanprotocol
        if device is not None:
            self.device = device
        for key, value in kwargs.items():
            setattr(self, key, value)

    def __repr__(self):
        return '%s: %s(%s) %s' % (self.index, self.name, self.type,
                                  self.address)

    @classmethod
    def fromDict(cls, data):
        data['linkType'] = (data['type'] if 'type' in data else
                            cls._detectType(data['name']))
        return cls(**data)

    @staticmethod
    def _detectType(name):
        detectedType = None
        try:
            driver = ethtool.driver_name(name)
        except IOError as ioe:
            if ioe.errno == errno.EOPNOTSUPP:
                if name == 'lo':
                    detectedType = LinkType.LOOPBACK
                else:
                    detectedType = LinkType.DUMMY
                return detectedType
            else:
        if driver in (LinkType.BRIDGE, LinkType.MACVLAN, LinkType.TUN,
                      LinkType.OVS, LinkType.TEAM, LinkType.VETH):
            detectedType = driver
        elif driver == 'bonding':
            detectedType = LinkType.BOND
        elif 'VLAN' in driver or 'vlan' in driver:
            detectedType = LinkType.VLAN
        elif os.path.exists('/sys/class/net/%s/device/physfn/' % name):
            detectedType = LinkType.VF
        else:
            detectedType = LinkType.NIC
        return detectedType

    def isBOND(self):
        return self.type == LinkType.BOND

    def isBRIDGE(self):
        return self.type == LinkType.BRIDGE

    def isDUMMY(self):
        return self.type == LinkType.DUMMY

    def isDPDK(self):
        return self.type == LinkType.DPDK

    def isNIC(self):
        return self.type == LinkType.NIC

    def isVETH(self):
        return self.type == LinkType.VETH

    def isVF(self):
        return self.type == LinkType.VF

    def isVLAN(self):
        return self.type == LinkType.VLAN

    def isMACVLAN(self):
        return self.type == LinkType.MACVLAN

    def isFakeNIC(self):
        if self.isDUMMY() or self.isVETH() or self.isMACVLAN():
            return _any_fnmatch(self.name, self._fakeNics)
        return False

    def isNICLike(self):
        return self.isNIC() or self.isVF() or self.isFakeNIC() or self.isDPDK()

    def isHidden(self):
        if self.isVLAN():
            return _any_fnmatch(self.name, self._hiddenVlans)
        elif self.isNICLike():
            return (_any_fnmatch(self.name, self._hiddenNics) or
                    (self.master and _bondExists(self.master) and
                     _any_fnmatch(self.master, self._hiddenBonds)) or
                    (self.isVF() and self._isVFhidden()))
        elif self.isBOND():
            return _any_fnmatch(self.name, self._hiddenBonds)
        elif self.isBRIDGE():
            return self.name == DUMMY_BRIDGE
        return False

    def _isVFhidden(self):
        if self.address == '00:00:00:00:00:00':
            if self._is_vmfex():
                return True
        for path in iglob('/sys/class/net/*/address'):
            dev = os.path.basename(os.path.dirname(path))
            if (dev != self.name and _read_stripped(path) == self.address and
                    self._detectType(dev) == LinkType.MACVLAN):
                return True
        return False

    def _is_vmfex(self):
        return ethtool.driver_name(self.name) == 'enic'

    @property
    def oper_up(self):
        if dpdk.is_dpdk(self.name):
            return dpdk.is_oper_up(self.name)
        return bool(
            link.get_link(self.name)['flags'] & libnl.IfaceStatus.IFF_RUNNING)

    def get_promisc(self):
        return bool(
            link.get_link(self.name)['flags'] & libnl.IfaceStatus.IFF_PROMISC)

    def set_promisc(self, value):
        promisc = 'on' if value else 'off'
        linkSet(self.name, ['promisc', promisc])

    promisc = property(get_promisc, set_promisc, None, 'Link promiscuity flag')


def _read_stripped(path):
    with open(path) as f:
        return f.read().strip()


def _bondExists(bondName):
    return os.path.exists('/sys/class/net/%s/bonding' % bondName)


def getLinks():
    dpdk_links = (dpdk.link_info(dev_name, dev_info['pci_addr'])
                  for dev_name, dev_info
                  in six.viewitems(dpdk.get_dpdk_devices()))
    for data in itertools.chain(link.iter_links(), dpdk_links):
        try:
            yield Link.fromDict(data)
            continue


def getLink(dev):
    if dpdk.is_dpdk(dev):
        return dpdk.link_info(dev)
    return Link.fromDict(link.get_link(dev))


@equals
class Route(object):
    def __init__(self, network, via=None, src=None, device=None, table=None):
        if network != 'local' and not _isValid(network, IPNetwork):
            raise ValueError('network %s is not properly defined' % network)

        if via and not _isValid(via, IPAddress):
            raise ValueError('via %s is not a proper IP address' % via)

        if src and not _isValid(src, IPAddress):
            raise ValueError('src %s is not a proper IP address' % src)

        self.network = network
        self.via = via
        self.src = src
        self.device = device
        self.table = table

    @classmethod
    def parse(cls, text):
        route = text.split()
        try:
            route = route[:route.index('\\')]
        except ValueError:
            pass

        flags = {}
        while route[0] in _ROUTE_FLAGS:
            flags[route[0]] = True
            route = route[1:]

        network = route[0]

        data = dict(route[i:i + 2] for i in range(1, len(route), 2))
        data['network'] = '0.0.0.0/0' if network == 'default' else network
        data.update(flags)
        return data

    @classmethod
    def fromText(cls, text):
        try:
            data = cls.parse(text)
        except Exception:
            raise ValueError('Route %s: Failed to parse route.' % text)

        via = data.get('via')
        src = data.get('src')
        try:
            device = data['dev']
        except KeyError:
            raise ValueError('Route %s: Routes require a device.' % text)
        table = data.get('table')

        return cls(data['network'], via=via, src=src, device=device,
                   table=table)

    def __str__(self):
        output = str(self.network)
        if self.network == 'local':
            output += ' %s' % self.src
        if self.via:
            output += ' via %s' % self.via

        output += ' dev %s' % self.device

        if self.src:
            output += ' src %s' % self.src

        if self.table:
            output += ' table %s' % self.table

        return output

    def __iter__(self):
        for word in str(self).split():
            yield word


@equals
class Rule(object):
    def __init__(self, table, source=None, destination=None, srcDevice=None,
                 detached=False, prio=None):
        if source:
            if not (_isValid(source, IPAddress) or
                    _isValid(source, IPNetwork)):
                raise ValueError('Source %s invalid: Not an ip address '
                                 'or network.' % source)

        if destination:
            if not (_isValid(destination, IPAddress) or
                    _isValid(destination, IPNetwork)):
                raise ValueError('Destination %s invalid: Not an ip address '
                                 'or network.' % destination)

        self.table = table
        self.source = source
        self.destination = destination
        self.srcDevice = srcDevice
        self.detached = detached
        self.prio = prio

    @classmethod
    def parse(cls, text):
        isDetached = '[detached]' in text
        rule = [entry for entry in text.split() if entry != '[detached]']
        prio = int(rule[0][:-1])
        parameters = rule[1:]

        if len(rule) % 2 == 0:
            raise ValueError('Rule %s: The length of a textual representation '
                             'of a rule must be odd. ' % text)

        values = \
            dict(parameters[i:i + 2] for i in range(0, len(parameters), 2))
        values['detached'] = isDetached
        values['prio'] = prio
        return values

    @classmethod
    def fromText(cls, text):
        data = cls.parse(text)
        try:
            table = data['lookup']
        except KeyError:
            raise ValueError('Rule %s: Rules require "lookup" information. ' %
                             text)
        try:
            source = data['from']
        except KeyError:
            raise ValueError('Rule %s: Rules require "from" information. ' %
                             text)

        destination = data.get('to')
        if source == 'all':
            source = None
        if destination == 'all':
            destination = None
        srcDevice = data.get('dev') or data.get('iif')
        detached = data['detached']
        prio = data['prio']

        return cls(table, source=source, destination=destination,
                   srcDevice=srcDevice, detached=detached, prio=prio)

    def __str__(self):
        output = 'from '
        if self.source:
            output += self.source
        else:
            output += 'all'
        if self.destination:
            output += ' to %s' % self.destination
        if self.srcDevice:
            output += ' dev %s' % self.srcDevice
        if self.prio:
            output += ' prio %s' % self.prio
        if self.table:
            output += ' table %s' % self.table

        return output

    def __iter__(self):
        for word in str(self).split():
            yield word


class IPRoute2Error(Exception):
    pass


class IPRoute2NoDeviceError(IPRoute2Error):
    pass


class IPRoute2AlreadyExistsError(IPRoute2Error):
    pass


def _exec_cmd(command):
    returnCode, output, error = cmd.exec_sync(command)

    if returnCode:
        if 'Cannot find device' in error:
            exc = IPRoute2NoDeviceError
        elif 'File exists' in error:
            exc = IPRoute2AlreadyExistsError
        else:
            exc = IPRoute2Error
        raise exc(returnCode, error.splitlines())
    return output.splitlines()


def routeShowGateways(table):
    command = [_IP_BINARY.cmd, 'route', 'show', 'to', '0.0.0.0/0', 'table',
               table]
    return _exec_cmd(command)


def route6_show_gateways(table):
    command = [_IP_BINARY.cmd, '-6', 'route', 'show', 'to', '::/0', 'table',
               table]
    return _exec_cmd(command)


def routeShowTable(table):
    command = [_IP_BINARY.cmd, '-oneline', 'route', 'show', 'table', table]
    return _exec_cmd(command)


def routeAdd(route, family=4, dev=None):
    command = [_IP_BINARY.cmd, '-%s' % family, 'route', 'add']
    command += route
    if dev is not None:
        command += ['dev', dev]
    _exec_cmd(command)


def routeDel(route, family):
    command = [_IP_BINARY.cmd, '-%s' % family, 'route', 'del']
    command += route
    _exec_cmd(command)


def routeGet(ipAddress):
    command = [_IP_BINARY.cmd, 'route', 'get']
    command += ipAddress
    return _exec_cmd(command)


def _getValidEntries(constructor, iterable):
    for entry in iterable:
        try:
            yield constructor(entry)
        except ValueError:
            pass


def routeExists(route):
    return route in _getValidEntries(constructor=Route.fromText,
                                     iterable=routeShowTable('all'))


def ruleList():
    command = [_IP_BINARY.cmd, 'rule']
    return _exec_cmd(command)


def ruleAdd(rule):
    command = [_IP_BINARY.cmd, 'rule', 'add']
    command += rule
    _exec_cmd(command)


def ruleDel(rule):
    command = [_IP_BINARY.cmd, 'rule', 'del']
    command += rule
    _exec_cmd(command)


def ruleExists(rule):
    return rule in _getValidEntries(constructor=Rule.fromText,
                                    iterable=ruleList())


def addrAdd(dev, ipaddr, netmask, family=4):
    command = [_IP_BINARY.cmd, '-%s' % family, 'addr', 'add', 'dev', dev,
               '%s/%s' % (ipaddr, netmask)]
    _exec_cmd(command)


def addrDel(dev, ipaddr, netmask, family):
    command = [_IP_BINARY.cmd, '-%s' % family, 'addr', 'del', 'dev', dev,
               '%s/%s' % (ipaddr, netmask)]
    _exec_cmd(command)


def addrFlush(dev, family='both'):
    family_param = ['-%s' % family] if family in (4, 6) else []
    command = [_IP_BINARY.cmd] + family_param + ['addr', 'flush', 'dev', dev,
                                                 'scope', 'global']
    _exec_cmd(command)


def linkAdd(name, linkType, link=None, args=()):
    command = [_IP_BINARY.cmd, 'link', 'add']
    if link:
        command += ['link', link]

    command += ['name', name, 'type', linkType]

    command.extend(args)

    _exec_cmd(command)


def linkSet(dev, linkArgs):
    command = [_IP_BINARY.cmd, 'link', 'set', 'dev', dev]
    command += linkArgs
    _exec_cmd(command)


def linkDel(dev):
    command = [_IP_BINARY.cmd, 'link', 'del', 'dev', dev]
    _exec_cmd(command)


def netns_add(name):
    _exec_cmd([_IP_BINARY.cmd, 'netns', 'add', name])


def netns_delete(name):
    _exec_cmd([_IP_BINARY.cmd, 'netns', 'delete', name])


def netns_exec(netns_name, command):
    cmds = [_IP_BINARY.cmd, 'netns', 'exec', netns_name] + command
    return subprocess.Popen(cmds)


def link_set_netns(device, netns_name):
    return linkSet(device, ['netns', netns_name])


def _any_fnmatch(name, patterns):
    return any(fnmatch(name, pattern) for pattern in patterns)
