# Copyright (c) 2016-2017 VMware, Inc.  All rights reserved. -- VMware Confidential

'''The file contains network utilities for configuring
and quering network configuiration.'''

import sys
import time
from subprocess import Popen, PIPE

# Cloudvm commands
VAMI_GET_NETWORK_CMD = '/opt/vmware/share/vami/vami_get_network'
VAMI_DEFAULT_INTERFACE_CMD = '/opt/vmware/share/vami/vami_default_interface'
IPV6_NEIGH_UPDATE_CMD = '/usr/lib/applmgmt/networking/bin/ndsend'


class NetworkConfigError(Exception):
    '''Exception class for network configuration errors'''
    pass


def _get_cmd_output(cmd, logger):
    ''' Execute command amd returns output'''
    proc = Popen(cmd, stdout=PIPE, stderr=PIPE)
    (out, err) = proc.communicate()
    default_encoding = sys.getfilesystemencoding()
    out = out.decode(default_encoding)
    err = err.decode(default_encoding)
    if proc.returncode != 0:
        error = ('Command %s failed stdout: %s, stderr: %s, exit-code: %s.' %
                 (cmd, out, err, proc.returncode))
        logger.error(error)
        raise NetworkConfigError(error)

    return out.strip()


class NetworkConfig():
    def __init__(self, interface, logger):
        output = _get_cmd_output([VAMI_GET_NETWORK_CMD, interface], logger)
        if not output:
            error = ("Failed to get network configuration for "
                     "interface %s: missing output" % self.interface)
            logger.error(error)
            raise NetworkConfigError(error)

        networkConfig = {}
        for line in output.splitlines():
            if line and ":" in line:
                key, val = line.split(":", 1)
                networkConfig[key] = val.strip()

        self.subnet_mask = networkConfig.get("active_netmask", "")
        self.subnet_prefix = networkConfig.get("active_prefix", "")
        self.ipv4_addr = networkConfig.get("active_ipv4addr")
        self.ipv6_addr = networkConfig.get("active_ipv6addr")
        self.config_present = (networkConfig.get("config_present") == "true")


def wait_for_interface_to_be_available(interface, logger):
    '''Wait for interface IP address to be available

    @param interface: The name of the interface.
    @type interface: str

    @param logger: Logger handle
    @type logger: Logger
    '''
    cmd = ('ip addr show %s scope global | grep "inet6\|inet.*%s$"' %
           (interface, interface))
    for count in range(60, 0, -1):
        proc = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True)
        (out, err) = proc.communicate()
        if proc.returncode == 0:
            logger.info("Interface is up %s" % out)
            return
        else:
            logger.info("Waiting for IP for %s, %s seconds", interface, count)
            time.sleep(1)

    error = "Failed to acquire IP for link %s." % interface
    logger.error(error)
    raise NetworkConfigError(error)


def toggle_interface(interface, logger):
    '''Toggle interface

    @param interface: The name of the interface.
    @type interface: str

    @param logger: Logger handle
    @type logger: Logger
    '''
    command = ['/sbin/ip', 'link', 'set', 'dev', interface, 'down']
    _get_cmd_output(command, logger)

    command = ['/sbin/ip', 'link', 'set', 'dev', interface, 'up']
    _get_cmd_output(command, logger)

    wait_for_interface_to_be_available(interface, logger)

    logger.info("Successfully toggled interface %s" % interface)


def _find_free_alias_interface(interface, logger):
    '''Find alias interface which is still not allocated. Iterate first 10
    alias interfaces and return first one which is free.

    @param interface: The name of the interface.
    @type interface: str

    @param logger: Logger handle
    @type logger: Logger

    @return: The alias interface if a free alias interface is found,
      None otherwise.
    @rtype: str
    '''
    for idx in range(10):
        alias_interface = "%s:%s" % (interface, idx)
        if not NetworkConfig(alias_interface, logger).config_present:
            return alias_interface

    return None


def _add_temp_ipv4(interface, ip, netmask, signal_arp, logger):
    '''Add temporary ipv4 address to given interface.

    @param interface: The name of the interface.
    @type interface: str

    @param ip: Ip address which will be set on the alias interface
    @type ip: str

    @param netmask: Net mask which will be set on the alias interface
    @type netmask: str

    @param logger: Logger handle
    @type logger: Logger

    @return: The alias interface where the temporary address has been allocated
      to or None if the operation failed.
    @rtype: str
    '''

    if not interface:
        raise ValueError("Interface is not specified")
    if ":" in interface:
        raise ValueError("Cannot add temporary address to alias interface")
    if not ip:
        raise ValueError("Ip address is not specified")
    if not netmask:
        raise ValueError("Netmask is not specified")

    alias_interface = _find_free_alias_interface(interface, logger)
    if not alias_interface:
        error = "Cannot find spare alias interfaces on %s" % interface
        logger.error(error)
        raise NetworkConfigError(error)

    command = ['/sbin/ifconfig', alias_interface, ip, "netmask", netmask, "up"]

    output = _get_cmd_output(command, logger)
    logger.info("Command %s has result %s", " ".join(command), output)

    if signal_arp:
        # After the temporary ip is added, update all the devices in the network
        # that the temporary ip address is assigned to a new mac address
        update_neighbors_arp_tables(interface, ip, logger)

    logger.info("Successfully added temporary address on %s: %s netmask %s",
                alias_interface, ip, netmask)
    return alias_interface


def _add_ipv6(interface, ip, prefix, logger):
    '''Add ipv6 address to given interface. This will allow cloudvm
    clients to still be connected to that ip address and execute
    commands against it.

    @param interface: The name of the interface.
    @type interface: str

    @param ip: IPv6 address which will be set on the interface
    @type ip: str

    @param prefix: Net mask which will be set on the alias interface
    @type prefix: str

    @param logger: Logger handle
    @type logger: Logger
    '''
    if not interface:
        raise ValueError("Interface is not specified")
    if not ip:
        raise ValueError("Ip address is not specified")
    if not prefix:
        raise ValueError("Prefix is not specified")

    addr = "%s/%s" % (ip, prefix)
    command = ['/sbin/ifconfig', interface, "inet6", "add", addr]

    output = _get_cmd_output(command, logger)
    logger.info("Command %s has result %s", " ".join(command), output)

    # After the temporary ip is added, update all the devices in the network
    # that the temporary ip address is assigned to a new mac address
    update_neighbors_arp_tables(interface, ip, logger)

    logger.info("Successfully added temporary address on %s: %s netmask %s",
                interface, ip, prefix)


def update_neighbors_arp_tables(interface, ip, logger, msg_counts=2):
    '''Sends broadcast arp reply message that the ip on that MAC address has
    been changed.

    @param interface: The name of the interface.
    @type interface: str

    @param ip: IP source address which will make ARP request
    @param ip: str

    @param msg_counts: Number of ARP request are going to be made
    @param msg_counts: int

    @param logger: Logger handle
    @type logger: Logger
    '''
    update_arp_tables = None
    if ":" in ip:
        # Ipv6 has to automatically updates its ARP cache by doing multicasting
        # add the time when a new ipv6 is introduced or released. In addition,
        # we execute ndsend command to update the neighbors ARP cache.
        update_arp_tables = [IPV6_NEIGH_UPDATE_CMD, ip, interface]
    else:
        # For IPv4, broadcast ARP message to all nodes in ethernet
        update_arp_tables = ['/sbin/arping', "-A", "-c",
                             str(msg_counts), "-I", interface, ip]
    output = _get_cmd_output(update_arp_tables, logger)
    logger.info("Command %s output %s", " ".join(update_arp_tables), output)


def apply_temporary_networking(interface,
                               tmp_ipv4_addr, tmp_subnet_mask,
                               tmp_ipv6_addr, tmp_subnet_prefix,
                               logger):
    '''Sets temporary address aliases.
    The primary purpose of temporary addresses is to help UI interface to stay
    connected using the same IP addresses, when IP addresses get reconfigured
    during upgrade or restore.

    @param interface: Interface where the alias to be created
    @type tmp_ipv4_addr: str

    @param tmp_ipv4_addr: IPv4 address e.g. "10.10.10.10"
    @type tmp_ipv4_addr: str

    @param tmp_subnet_mask: Subnet mask e.g. "255.255.255.0"
    @type tmp_subnet_mask: str

    @param tmp_ipv6_addr: IPv6 address e.g. "2001:0db8:0:f101::1"
    @type tmp_ipv6_addr: str

    @param tmp_subnet_prefix: Subnet prefix e.g. "64"
    @type tmp_subnet_prefix: str

    @param logger: Logger handle
    @type logger: Logger

    @return: Ipv4 alias interface, if Ipv4 is configured, None otherwise
    @rtype: str
    '''

    alias_ipv4 = None

    # Add older ip address as temporary ip address
    if tmp_ipv4_addr and tmp_subnet_mask:
        alias_ipv4 = _add_temp_ipv4(interface, tmp_ipv4_addr,
                                    tmp_subnet_mask, True, logger)
        # In IPv4 environment, the interface needs to be toggled,
        # after adding temporary addresses. Otherwise, the new IPv4
        # addresses are not accessible.
        toggle_interface(interface, logger)
        _add_temp_ipv4(interface, tmp_ipv4_addr,
                                    tmp_subnet_mask, False, logger)
    else:
        logger.info("Temporary ipv4 address is not presented")

    if tmp_ipv6_addr and tmp_subnet_prefix:
        _add_ipv6(interface, tmp_ipv6_addr, tmp_subnet_prefix, logger)
    else:
        logger.info("Temporary ipv6 address is not presented")

    return alias_ipv4


def release_temp_networking(interface, alias_ipv4,
                            tmp_ipv6_addr, tmp_subnet_prefix,
                            logger):
    '''Clears temporary address aliases.

    @param interface: Interface where the alias is created
    @type tmp_ipv4_addr: str

    @param alias_ipv4: IPv4 alias inteface
    @type tmp_ipv4_addr: str

    @param tmp_ipv6_addr: IPv6 address e.g. "2001:0db8:0:f101::1"
    @type tmp_ipv6_addr: str

    @param tmp_subnet_prefix: Subnet prefix e.g. "64"
    @type tmp_subnet_prefix: str

    @param logger: Logger handle
    @type logger: Logger
    '''
    if alias_ipv4:
        command = ['/sbin/ifconfig', alias_ipv4, 'down']
        _get_cmd_output(command, logger)

    if tmp_ipv6_addr and tmp_subnet_prefix:
        command = ['/sbin/ifconfig', interface, 'inet6', 'del',
                   '%s/%s' % (tmp_ipv6_addr, tmp_subnet_prefix)]
        _get_cmd_output(command, logger)

    logger.info("Successfully released temporary addresses")
    return


def get_default_interface(logger):
    '''Get  /opt/vmware/share/vami/vami_default_interface.'''

    return _get_cmd_output(VAMI_DEFAULT_INTERFACE_CMD, logger)
