'''
Created on Feb 28, 2019

@summary: The network module is intended to provide networking information (IP MAC addresses, DNS, status etc) on network interfaces
@author: gderoire
'''

import logging
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())

import ipaddress
import netifaces
import socket
import lib_system
import dbus
from multiprocessing import Pool
import functools
import itertools

LAN_INTERFACE_NAME = 'fec0'
LAN2_INTERFACE_NAME = 'fec1'

def get_connman_ready_interfaces():
	''' Return a list of ready interfaces by their system interface name (e.g.: ['fec0','wlan0','ppp0'])
	'''
	bus = dbus.SystemBus()
	manager = dbus.Interface(bus.get_object("net.connman", "/"), "net.connman.Manager")
	services = manager.GetServices()
	ready_interfaces = [ properties['Ethernet']['Interface'] for path, properties in services if properties['State'] == 'ready']
	return ready_interfaces

def get_connman_ifproperties(interface_name):
	''' Return interface properties (from connman DBus)

		print properties['Name']					- connman service name (e.g.: 'RDSoft_2G4@unifi')
		print properties['IPv4']['Address']			- IPv4 address (e.g.: '192.168.1.10')
		print properties['IPv4']['Netmask']			- IPv4 network mask (e.g.: '255.255.255.0')
		print properties['IPv4']['Gateway']			- IPv4 gateway address (e.g.: '192.168.1.0')
		print properties['IPv6']					- same as IPv4
		print properties['Nameservers']				- list of DNS IP addresses
		print properties['Ethernet']['Interface'] 	- system interface name (e.g.: 'fec0')
		print properties['Ethernet']['Address'] 	- interface MAC address (e.g. : '34:C9:F0:87:B5:81')
	'''
	bus = dbus.SystemBus()
	manager = dbus.Interface(bus.get_object("net.connman", "/"), "net.connman.Manager")
	services = manager.GetServices()
	interface_properties = next((properties for path, properties in services if properties['Ethernet']['Interface'] == interface_name), None)
	return interface_properties

def get_system_nameservers():
	"""
	Get the system wide DNS server (ones listed in /etc/resolv.conf)
	"""
	bus = dbus.SystemBus()
	resolved_manager = dbus.Interface(bus.get_object("org.freedesktop.resolve1", "/org/freedesktop/resolve1"), "org.freedesktop.DBus.Properties")
	dbus_nameservers = resolved_manager.Get("org.freedesktop.resolve1.Manager", "DNS")

	nameservers = []
	for dbus_addr in dbus_nameservers:
		# DBus answers of resolve1 contain a nameserver's IP address in the third element of a dbus.Struct object
		# as a dbus.Array of four dbus.Byte.
		_, _, addr_bytearray = dbus_addr

		# @octet is first converted to an integer before being converted to a string
		# because otherwise it is converted to a char, as it's the type dbus.Byte is mapped to.
		addr = ".".join([str(int(octet)) for octet in addr_bytearray])
		try:
			# ipaddress.IPv4Address() is used to check the IP address format here.
			# If its format is invalid, it will throw an exception and directly move on the next IP address
			# As a result, invalid IP addresses won't be added to the list @nameservers
			ipaddress.IPv4Address(addr)
			nameservers.append(addr)
		except:
			logger.warning('Retrieved DNS server address "{}" is not a valid IP, ignoring it'.format(addr))

	return nameservers

def get_interface_dns(interface_name):
	''' Return first interface DNS
	@param interface_name: name of the interface selected
	'''

	# For network interfaces that are managed by connman (wifi and cellular), DNS servers are
	# retrieved through the connman DBus interface.
	# Otherwise, system wide configured DNS servers are used (listed in /etc/resolv.conf).
	connman_ifproperties = get_connman_ifproperties(interface_name)
	if connman_ifproperties != None:
		dns = connman_ifproperties['Nameservers']
	else:
		dns = get_system_nameservers()
	if len(dns) == 0:
		logger.warning('Fail to get nameserver for {}'.format(interface_name))
		return None
	logger.debug('Nameserver retrieve for {} is {}'.format(interface_name, dns[0]))
	return dns[0]

def get_interface_MAC(interface_name):
	""" Return a list of MAC linked to an interface or an empty list if interface is unknown or has no MAC
	@param interface_name: name of the network interface (e.g.: lo, eth0)
		e.g. : [{'broadcast': 'ff:ff:ff:ff:ff:ff', 'addr': '10:12:25:1b:bf:14'}]
	"""
	if interface_name not in netifaces.interfaces():
		logger.debug('Fail to retrieve interface {}'.format(interface_name))
		return []

	try:
		macs = netifaces.ifaddresses(interface_name)[netifaces.AF_LINK]
		logger.debug('MAC address for {} is {}'.format(interface_name, macs[0]['addr']))
	except:
		logger.debug('Fail to retrieve MAC address for {}'.format(interface_name))
		return []
	return macs

def get_interface_IPv4(interface_name):
	""" Return a list of IP information linked to an interface or an empty list if interface is unknown or has no IP
	@param interface_name: name of the network interface (e.g.: lo, eth0)
		e.g. : [{'broadcast': '10.130.1.255', 'netmask': '255.255.255.128', 'addr': '10.130.1.166'}]
	"""
	if interface_name not in netifaces.interfaces():
		logger.debug('Fail to retrieve interface {}'.format(interface_name))
		return []

	try:
		ips = netifaces.ifaddresses(interface_name)[netifaces.AF_INET]
		logger.debug('IPv4 address for {} is {}'.format(interface_name, ips[0]['addr']))
	except:
		logger.debug('Fail to retrieve IPv4 address for {}'.format(interface_name))
		return []
	return ips


def is_interface_up(interface_name):
	""" Check the link status of a network interface
	@param interface_name: name of the network interface (e.g.: fec0, fec1, eth0)
	@return: True if link is up
	"""
	# If interface is up, carrier gives the link state (0: down, 1$: up)
	# If interface is up, carrier file is missing
	try:
		with open("/sys/class/net/{}/carrier".format(interface_name)) as file:
			if file.read(1) == '1':
				return True
	except IOError:
		return False
	return False


def ping_dns_server(interface):
    """
    Checks DNS server reachability. Gets the name server Ip of network interface and Ping.
    @input : interface , the network interface
    @return the command execution status
    """
    interface_dns = None
    interface_dns = str(get_interface_dns(interface))
    if interface_dns is None:
        logger.debug("Unable to fetch name server from the DNS")
        return interface_dns
  
    (status,output) = lib_system.call_shell(["ping","-c","1", interface_dns], False)
    return status
 
     
def check_dns_resoultion(server_url_or_ip='pool.ntp.org',from_server_ip=None):
    """
    Checks DNS server hostname to IP and vice versa resoultion using 'nslookup'.
    @input : server_url_or_ip , hostname or IP of the server
    @input : from_server_ip , Ip used to check dns resolution from a given IP 
    @return the command execution status
    """
    command = "nslookup " + server_url_or_ip
    if from_server_ip:
        command = command + " " + from_server_ip
    (status,output) = lib_system.call_shell(["/bin/sh","-c", command], False)
    
    return status


def contact_single_server(interface=None, server_url='pool.ntp.org'):
	"""try to establish a connection to a server (port 80)
	@param server_url: server to test
	@param interface: (interface name, interface DNS IP) tuple of the network interface to use
	@return: True if server is reachable, False otherwise
	"""
	if interface:
		logger.debug('Trying to contact {} from {} (with DNS {})'.format(server_url, interface[0], interface[1]))
	else:
		logger.debug('Trying to contact {}'.format(server_url))
	command = ['timeout', '-t', '10', '-s', 'KILL', 'curl', '--silent', '--show-error', '--head']
	if interface:
		command.extend( [ '--dns-servers', interface[1] , '--dns-interface', interface[0], '--interface', interface[0] ] )
	command.append(server_url)
	(code, output) = lib_system.call_shell(command, check_error_code=False)
	if code == 0:
		logger.debug('Succeed to contact server')
		return True
	else:
		logger.info('Fail to contact server')
	return False

def contact_server(interface_name=None, server_urls=[ 'storefsngeneral.blob.core.windows.net', 'pool.ntp.org' ]):
	"""try to establish a connection to a server (port 80)
	@param server_urls: list of servers to test
	@param interface_name: name of the network interface to use
	@return: True if a least one server is reachable, False otherwise
	"""
	interface = None
	if interface_name:
		interface_dns = get_interface_dns(interface_name)
		if not interface_dns:
			return False
		interface = (interface_name, interface_dns)

	parallelized = False
	if parallelized:
		logger.debug('Initialize pool')
		pool = Pool(processes=len(server_urls))
		it = pool.imap_unordered( functools.partial(contact_single_server, interface) , server_urls)
	else:
		it = itertools.imap( functools.partial(contact_single_server, interface) , server_urls)

	has_contacted_server = False
	for result in it:
		if result:
			has_contacted_server = True
			break

	if parallelized:
		logger.debug('Clean pool')
		pool.close()
		pool.terminate()

	if has_contacted_server:
		logger.info('Success to contact server')
	else:
		logger.warning('Fail to contact server')
	return has_contacted_server
