#!/usr/bin/env python3
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Parts of this are based on the example python code that ships with
# NetworkManager
# http://cgit.freedesktop.org/NetworkManager/NetworkManager/tree/examples/python
#
# Copyright (C) 2012 Canonical, Ltd.

import os
import re
import sys
import dbus
import sys
from checkbox.parsers.modinfo import ModinfoParser
from subprocess import check_output, CalledProcessError, STDOUT

# This example lists basic information about network interfaces known to NM
devtypes = {1: "Ethernet",
            2: "WiFi",
            5: "Bluetooth",
            6: "OLPC",
            7: "WiMAX",
            8: "Modem"}

states = {0: "Unknown",
          10: "Unmanaged",
          20: "Unavailable",
          30: "Disconnected",
          40: "Prepare",
          50: "Config",
          60: "Need Auth",
          70: "IP Config",
          80: "IP Check",
          90: "Secondaries",
          100: "Activated",
          110: "Deactivating",
          120: "Failed"}


class NetworkingDevice():
    def __init__(self, devtype, props, dev_proxy, bus):
        self._devtype = devtype
        try:
            self._interface = props['Interface']
        except KeyError:
            self._interface = "Unknown"

        try:
            self._ip = self._int_to_ip(props['Ip4Address'])
        except KeyError:
            self._ip = "Unknown"

        try:
            self._driver = props['Driver']
        except KeyError:
            self._driver = "Unknown"

        if self._driver != "Unknown":
            self._modinfo = self._modinfo_parser(props['Driver'])
            if self._modinfo:
                self._driver_ver = self._find_driver_ver()
            else:
                self._driver_ver = "Unknown"

        try:
            self._firmware_missing = props['FirmwareMissing']
        except KeyError:
            self._firmware_missing = False

        try:
            self._state = states[props['State']]
        except KeyError:
            self._state = "Unknown"

    def __str__(self):
        ret = "Type: %s\n" % self._devtype
        ret += "Interface: %s\n" % self._interface
        ret += "IP: %s\n" % self._ip
        ret += "Driver: %s (ver: %s)\n" % (self._driver, self._driver_ver)
        if self._firmware_missing:
            ret += "Warning: Required Firmware Missing for device\n"
        ret += "State: %s\n" % self._state
        return ret

    def getstate(self):
        return self._state

    def gettype(self):
        return self._devtype

    def _bitrate_to_mbps(self, bitrate):
        try:
            intbr = int(bitrate)
            return str(intbr / 1000)
        except Exception:
            return "NaN"

    def _modinfo_parser(self, driver):
        cmd = ['/sbin/modinfo', driver]
        try:
            stream = check_output(cmd, stderr=STDOUT, universal_newlines=True)
        except CalledProcessError as err:
            print("Error running %s:" % ' '.join(cmd), file=sys.stderr)
            print(err.output, file=sys.stderr)
            return None

        if not stream:
            print ("Error: modinfo returned nothing", file=sys.stderr)
            return None
        else:
            parser = ModinfoParser(stream)
            modinfo = parser.get_all()
        
        return modinfo

    def _find_driver_ver(self):
        # try the version field first, then vermagic second, some audio
        # drivers don't report version if the driver is in-tree
        if self._modinfo['version'] and self._modinfo['version'] != 'in-tree:':
            return self._modinfo['version']
        else:
            # vermagic will look like this (below) and we only care about the
            # first part:
            # "3.2.0-29-generic SMP mod_unload modversions"
            return self._modinfo['vermagic'].split()[0]

    def _int_to_ip(self, int_ip):
        ip = [0, 0, 0, 0]
        ip[0] = int_ip & 0xff
        ip[1] = (int_ip >> 8) & 0xff
        ip[2] = (int_ip >> 16) & 0xff
        ip[3] = (int_ip >> 24) & 0xff
        return "%d.%d.%d.%d" % (ip[0], ip[1], ip[2], ip[3])


def get_nm_devices():
    devices = []
    bus = dbus.SystemBus()

    # Get a proxy for the base NetworkManager object
    proxy = bus.get_object("org.freedesktop.NetworkManager",
        "/org/freedesktop/NetworkManager")
    manager = dbus.Interface(proxy, "org.freedesktop.NetworkManager")

    # Get all devices known to NM and print their properties
    nm_devices = manager.GetDevices()
    for d in nm_devices:
        dev_proxy = bus.get_object("org.freedesktop.NetworkManager", d)
        prop_iface = dbus.Interface(dev_proxy,"org.freedesktop.DBus.Properties")
        props = prop_iface.GetAll("org.freedesktop.NetworkManager.Device")
        try:
            devtype = devtypes[props['DeviceType']]
        except KeyError:
            devtype = "Unknown"

        # only return WiFi and LAN devices
        if devtype == "WiFi" or devtype == "Ethernet":
            devices.append(NetworkingDevice(devtype, props, dev_proxy, bus))
    return devices


def match_counts(nm_devices, pci_devices, devtype):
    """
    Ensures that the count of devices matching devtype is the same for the
    two passed in lists, devices from Network Manager and devices from lspci.
    """
    # now check that the count (by type) matches - WiFi
    nm_type_devices = list(filter(lambda dev: dev.gettype() == devtype,
        nm_devices))
    pci_type_devices = list(filter(lambda dev: dev['type'] == devtype,
        pci_devices))
    if len(nm_type_devices) != len(pci_type_devices):
        print("ERROR: %s devices missing: lspci showed %d devices, but "
              "NetworkManager saw %d devices" % (devtype,
              len(pci_type_devices), len(nm_type_devices)))
        return False
    else:
        return True


def main(args):
    pci_devices = []
    command = "lspci"
    for line in os.popen(command).readlines():
        if "Ethernet controller: " in line:
            device = {}
            device['name'] = line.split(": ")[1].strip()
            device['type'] = "Ethernet"
            pci_devices.append(device)
        elif "Network controller: " in line:
            device = {}
            device['name'] = line.split(": ")[1].strip()
            device['type'] = "WiFi"
            pci_devices.append(device)

    if pci_devices:
        print("Devices found by lspci------------------------------")
        for device in pci_devices:
            print("%s: %s" % (device['type'], device['name']))
    else:
        print("ERROR: No Networking devices found.", file=sys.stderr)
        return 1

    nm_devices = []
    try:
        nm_devices = get_nm_devices()
    except dbus.exceptions.DBusException as e:
        # server's don't have network manager installed
        print("Warning: Exception while talking to Network Manager over dbus."
              " Skipping the remainder of this test. If this is a server, this"
              " is expected.", file=sys.stderr)
        print("The Error Generated was:\n %s" % e, file=sys.stderr)
        return 0

    print("Devices found by Network Manager-------------------------")
    for nm_dev in nm_devices:
        print(nm_dev)

    if not match_counts(nm_devices, pci_devices, "WiFi"):
        return 1
    elif not match_counts(nm_devices, pci_devices, "Ethernet"):
        return 1
    else:
        return 0

if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
