# -*- coding: utf-8 -*-
'''
NET Finder
==========

.. versionadded:: 2017.7.0

A runner to find network details easily and fast.
It's smart enough to know what you are looking for.

Configuration
-------------

- Minion (proxy) config

    To have the complete features, one needs to add the following mine configuration in the minion (proxy) config file:

    .. code-block:: yaml

        mine_functions:
          net.ipaddrs: []
          net.lldp: []
          net.mac: []
          net.arp: []
          net.interfaces: []

    Which instructs Salt to cache the data returned by the NAPALM-functions.
    While they are not mandatory, the less functions configured, the less details will be found by the runner.

    How often the mines are refreshed, can be specified using:

    .. code-block:: yaml

        mine_interval: <X minutes>

- Master config

    By default the following options can be configured on the master.
    They are not necessary, but available in case the user has different requirements.

    target: ``*``
        From what minions will collect the mine data. Default: ``*`` (collect from all minions).

    expr_form: ``glob``
        Minion matching expression form. Default: ``glob``.

    ignore_interfaces
        A list of interfaces name to ignore. By default will consider all interfaces.

    display: ``True``
        Display on the screen or return structured object? Default: ``True`` (return on the CLI).

    outputter: ``table``
        Specify the outputter name when displaying on the CLI. Default: :mod:`table <salt.output.napalm_bgp>`.

    Configuration example:

    .. code-block:: yaml

        runners:
          net.find:
            target: 'edge*'
            expr_form: 'glob'
            ignore_interfaces:
              - lo0
              - em1
              - jsrv
              - fxp0
            outputter: yaml
'''
from __future__ import absolute_import, print_function, unicode_literals

# Import salt lib
import salt.output
import salt.utils.network
from salt.ext import six
from salt.ext.six.moves import map

# Import third party libs
try:
    from netaddr import IPNetwork  # netaddr is already required by napalm-base
    from netaddr.core import AddrFormatError
    from napalm_base import helpers as napalm_helpers
    HAS_NAPALM_BASE = True
except ImportError:
    HAS_NAPALM_BASE = False

# -----------------------------------------------------------------------------
# module properties
# -----------------------------------------------------------------------------

_DEFAULT_TARGET = '*'
_DEFAULT_EXPR_FORM = 'glob'
_DEFAULT_IGNORE_INTF = []
# 'lo0', 'em1', 'em0', 'jsrv', 'fxp0'
_DEFAULT_DISPLAY = True
_DEFAULT_OUTPUTTER = 'table'


# -----------------------------------------------------------------------------
# global variables
# -----------------------------------------------------------------------------

# will cache several details to avoid loading them several times from the mines.
_CACHE = {}

# -----------------------------------------------------------------------------
# helper functions -- will not be exported
# -----------------------------------------------------------------------------

# Define the module's virtual name
__virtualname__ = 'net'


def __virtual__():
    if HAS_NAPALM_BASE:
        return __virtualname__
    return (False, 'The napalm-base module could not be imported')


def _get_net_runner_opts():
    '''
    Return the net.find runner options.
    '''
    runner_opts = __opts__.get('runners', {}).get('net.find', {})
    return {
        'target': runner_opts.get('target', _DEFAULT_TARGET),
        'expr_form': runner_opts.get('expr_form', _DEFAULT_EXPR_FORM),
        'ignore_interfaces': runner_opts.get('ignore_interfaces', _DEFAULT_IGNORE_INTF),
        'display': runner_opts.get('display', _DEFAULT_DISPLAY),
        'outputter': runner_opts.get('outputter', _DEFAULT_OUTPUTTER),
    }


def _get_mine(fun):
    '''
    Return the mine function from all the targeted minions.
    Just a small helper to avoid redundant pieces of code.
    '''
    if fun in _CACHE and _CACHE[fun]:
        return _CACHE[fun]
    net_runner_opts = _get_net_runner_opts()
    _CACHE[fun] = __salt__['mine.get'](net_runner_opts.get('target'),
                                       fun,
                                       tgt_type=net_runner_opts.get('expr_form'))
    return _CACHE[fun]


def _display_runner(rows, labels, title, display=_DEFAULT_DISPLAY):
    '''
    Display or return the rows.
    '''
    if display:
        net_runner_opts = _get_net_runner_opts()
        if net_runner_opts.get('outputter') == 'table':
            ret = salt.output.out_format({'rows': rows, 'labels': labels},
                                         'table',
                                         __opts__,
                                         title=title,
                                         rows_key='rows',
                                         labels_key='labels')
        else:
            ret = salt.output.out_format(rows,
                                         net_runner_opts.get('outputter'),
                                         __opts__)
        print(ret)
    else:
        return rows


def _get_network_obj(addr):
    '''
    Try to convert a string into a valid IP Network object.
    '''
    ip_netw = None
    try:
        ip_netw = IPNetwork(addr)
    except AddrFormatError:
        return ip_netw
    return ip_netw


def _find_interfaces_ip(mac):
    '''
    Helper to search the interfaces IPs using the MAC address.
    '''
    try:
        mac = napalm_helpers.convert(napalm_helpers.mac, mac)
    except AddrFormatError:
        return ('', '', [])

    all_interfaces = _get_mine('net.interfaces')
    all_ipaddrs = _get_mine('net.ipaddrs')

    for device, device_interfaces in six.iteritems(all_interfaces):
        if not device_interfaces.get('result', False):
            continue
        for interface, interface_details in six.iteritems(device_interfaces.get('out', {})):
            try:
                interface_mac = napalm_helpers.convert(napalm_helpers.mac, interface_details.get('mac_address'))
            except AddrFormatError:
                continue
            if mac != interface_mac:
                continue
            interface_ipaddrs = all_ipaddrs.get(device, {}).get('out', {}).get(interface, {})
            ip_addresses = interface_ipaddrs.get('ipv4', {})
            ip_addresses.update(interface_ipaddrs.get('ipv6', {}))
            interface_ips = ['{0}/{1}'.format(ip_addr,
                                              addr_details.get('prefix_length', '32'))
                             for ip_addr, addr_details in six.iteritems(ip_addresses)]
            return device, interface, interface_ips

    return ('', '', [])


def _find_interfaces_mac(ip):  # pylint: disable=invalid-name
    '''
    Helper to get the interfaces hardware address using the IP Address.
    '''
    all_interfaces = _get_mine('net.interfaces')
    all_ipaddrs = _get_mine('net.ipaddrs')

    for device, device_ipaddrs in six.iteritems(all_ipaddrs):
        if not device_ipaddrs.get('result', False):
            continue
        for interface, interface_ipaddrs in six.iteritems(device_ipaddrs.get('out', {})):
            ip_addresses = interface_ipaddrs.get('ipv4', {}).keys()
            ip_addresses.extend(interface_ipaddrs.get('ipv6', {}).keys())
            for ipaddr in ip_addresses:
                if ip != ipaddr:
                    continue
                interface_mac = all_interfaces.get(device, {}).get('out', {}).get(interface, {}).get('mac_address', '')
                return device, interface, interface_mac

    return ('', '', '')


# -----------------------------------------------------------------------------
# callable functions
# -----------------------------------------------------------------------------


def interfaces(device=None,
               interface=None,
               title=None,
               pattern=None,
               ipnet=None,
               best=True,
               display=_DEFAULT_DISPLAY):
    '''
    Search for interfaces details in the following mine functions:

    - net.interfaces
    - net.ipaddrs

    Optional arguments:

    device
        Return interface data from a certain device only.

    interface
        Return data selecting by interface name.

    pattern
        Return interfaces that contain a certain pattern in their description.

    ipnet
        Return interfaces whose IP networks associated include this IP network.

    best: ``True``
        When ``ipnet`` is specified, this argument says if the runner should return only the best match
        (the output will contain at most one row). Default: ``True`` (return only the best match).

    display: True
        Display on the screen or return structured object? Default: ``True`` (return on the CLI).

    title
        Display a custom title for the table.

    CLI Example:

    .. code-block:: bash

        $ sudo salt-run net.interfaces interface=vt-0/0/10

    Output Example:

    .. code-block:: text

        Details for interface xe-0/0/0
        _________________________________________________________________________________________________________________
        |    Device    | Interface | Interface Description |  UP  | Enabled | Speed [Mbps] | MAC Address | IP Addresses |
        _________________________________________________________________________________________________________________
        | edge01.bjm01 | vt-0/0/10 |                       | True |   True  |     1000     |             |              |
        _________________________________________________________________________________________________________________
        | edge01.flw01 | vt-0/0/10 |                       | True |   True  |     1000     |             |              |
        _________________________________________________________________________________________________________________
        | edge01.pos01 | vt-0/0/10 |                       | True |   True  |     1000     |             |              |
        _________________________________________________________________________________________________________________
        | edge01.oua01 | vt-0/0/10 |                       | True |   True  |     1000     |             |              |
        _________________________________________________________________________________________________________________
    '''

    def _ipnet_belongs(net):
        '''
        Helper to tell if a IP address or network belong to a certain network.
        '''
        if net == '0.0.0.0/0':
            return False
        net_obj = _get_network_obj(net)
        if not net_obj:
            return False
        return ipnet in net_obj or net_obj in ipnet

    labels = {
        'device': 'Device',
        'interface': 'Interface',
        'interface_description': 'Interface Description',
        'is_up': 'UP',
        'is_enabled': 'Enabled',
        'speed': 'Speed [Mbps]',
        'mac': 'MAC Address',
        'ips': 'IP Addresses'
    }
    rows = []

    net_runner_opts = _get_net_runner_opts()

    if pattern:
        title = 'Pattern "{0}" found in the description of the following interfaces'.format(pattern)
    if not title:
        title = 'Details'
        if interface:
            title += ' for interface {0}'.format(interface)
        else:
            title += ' for all interfaces'
        if device:
            title += ' on device {0}'.format(device)
        if ipnet:
            title += ' that include network {net}'.format(net=six.text_type(ipnet))
            if best:
                title += ' - only best match returned'

    all_interfaces = _get_mine('net.interfaces')
    all_ipaddrs = _get_mine('net.ipaddrs')

    if device:
        all_interfaces = {device: all_interfaces.get(device, {})}

    if ipnet and not isinstance(ipnet, IPNetwork):
        ipnet = _get_network_obj(ipnet)

    best_row = {}
    best_net_match = None
    for device, net_interfaces_out in six.iteritems(all_interfaces):  # pylint: disable=too-many-nested-blocks
        if not net_interfaces_out:
            continue
        if not net_interfaces_out.get('result', False):
            continue
        selected_device_interfaces = net_interfaces_out.get('out', {})
        if interface:
            selected_device_interfaces = {interface: selected_device_interfaces.get(interface, {})}
        for interface_name, interface_details in six.iteritems(selected_device_interfaces):
            if not interface_details:
                continue
            if ipnet and interface_name in net_runner_opts.get('ignore_interfaces'):
                continue
            interface_description = (interface_details.get('description', '') or '')
            if pattern:
                if pattern.lower() not in interface_description.lower():
                    continue
            if not all_ipaddrs.get(device, {}).get('result', False):
                continue
            ips = []
            device_entry = {
                'device': device,
                'interface': interface_name,
                'interface_description': interface_description,
                'is_up': (interface_details.get('is_up', '') or ''),
                'is_enabled': (interface_details.get('is_enabled', '') or ''),
                'speed': (interface_details.get('speed', '') or ''),
                'mac': napalm_helpers.convert(napalm_helpers.mac, (interface_details.get('mac_address', '') or '')),
                'ips': []
            }
            intf_entry_found = False
            for intrf, interface_ips in six.iteritems(all_ipaddrs.get(device, {}).get('out', {})):
                if intrf.split('.')[0] == interface_name:
                    ip_addresses = interface_ips.get('ipv4', {})  # all IPv4 addresses
                    ip_addresses.update(interface_ips.get('ipv6', {}))  # and all IPv6 addresses
                    ips = [
                        '{0}/{1}'.format(
                            ip_addr,
                            addr_details.get('prefix_length', '32')
                        ) for ip_addr, addr_details in six.iteritems(ip_addresses)
                    ]
                    interf_entry = {}
                    interf_entry.update(device_entry)
                    interf_entry['ips'] = ips
                    if display:
                        interf_entry['ips'] = '\n'.join(interf_entry['ips'])
                    if ipnet:
                        inet_ips = [
                            six.text_type(ip) for ip in ips if _ipnet_belongs(ip)
                        ]  # filter and get only IP include ipnet
                        if inet_ips:  # if any
                            if best:
                                # determine the global best match
                                compare = [best_net_match]
                                compare.extend(list(map(_get_network_obj, inet_ips)))
                                new_best_net_match = max(compare)
                                if new_best_net_match != best_net_match:
                                    best_net_match = new_best_net_match
                                    best_row = interf_entry
                            else:
                                # or include all
                                intf_entry_found = True
                                rows.append(interf_entry)
                    else:
                        intf_entry_found = True
                        rows.append(interf_entry)
            if not intf_entry_found and not ipnet:
                interf_entry = {}
                interf_entry.update(device_entry)
                if display:
                    interf_entry['ips'] = ''
                rows.append(interf_entry)

    if ipnet and best and best_row:
        rows = [best_row]

    return _display_runner(rows, labels, title, display=display)


def findarp(device=None,
            interface=None,
            mac=None,
            ip=None,
            display=_DEFAULT_DISPLAY):  # pylint: disable=invalid-name
    '''
    Search for entries in the ARP tables using the following mine functions:

    - net.arp

    Optional arguments:

    device
        Return interface data from a certain device only.

    interface
        Return data selecting by interface name.

    mac
        Search using a specific MAC Address.

    ip
        Search using a specific IP Address.

    display: ``True``
        Display on the screen or return structured object? Default: ``True``, will return on the CLI.

    CLI Example:

    .. code-block:: bash

        $ sudo salt-run net.findarp mac=8C:60:0F:78:EC:41

    Output Example:

    .. code-block:: text

        ARP Entries for MAC 8C:60:0F:78:EC:41
        ________________________________________________________________________________
        |    Device    |     Interface     |        MAC        |       IP      |  Age  |
        ________________________________________________________________________________
        | edge01.bjm01 | irb.171 [ae0.171] | 8C:60:0F:78:EC:41 | 172.172.17.19 | 956.0 |
        ________________________________________________________________________________
    '''
    labels = {
        'device': 'Device',
        'interface': 'Interface',
        'mac': 'MAC',
        'ip': 'IP',
        'age': 'Age'
    }
    rows = []

    all_arp = _get_mine('net.arp')

    title = "ARP Entries"
    if device:
        title += ' on device {device}'.format(device=device)
    if interface:
        title += ' on interface {interf}'.format(interf=interface)
    if ip:
        title += ' for IP {ip}'.format(ip=ip)
    if mac:
        title += ' for MAC {mac}'.format(mac=mac)

    if device:
        all_arp = {device: all_arp.get(device)}

    for device, device_arp in six.iteritems(all_arp):
        if not device_arp:
            continue
        if not device_arp.get('result', False):
            continue
        arp_table = device_arp.get('out', [])
        for arp_entry in arp_table:
            if ((mac and arp_entry.get('mac', '').lower() == mac.lower()) or  # pylint: disable=too-many-boolean-expressions
                (interface and interface in arp_entry.get('interface', '')) or
                (ip and napalm_helpers.convert(napalm_helpers.ip, arp_entry.get('ip', '')) ==
                 napalm_helpers.convert(napalm_helpers.ip, ip))):
                rows.append({
                    'device': device,
                    'interface': arp_entry.get('interface'),
                    'mac': napalm_helpers.convert(napalm_helpers.mac, arp_entry.get('mac')),
                    'ip': napalm_helpers.convert(napalm_helpers.ip, arp_entry.get('ip')),
                    'age': arp_entry.get('age')
                })

    return _display_runner(rows, labels, title, display=display)


def findmac(device=None, mac=None, interface=None, vlan=None, display=_DEFAULT_DISPLAY):
    '''
    Search in the MAC Address tables, using the following mine functions:

    - net.mac

    Optional arguments:

    device
        Return interface data from a certain device only.

    interface
        Return data selecting by interface name.

    mac
        Search using a specific MAC Address.

    vlan
        Search using a VLAN ID.

    display: ``True``
        Display on the screen or return structured object? Default: ``True``, will return on the CLI.

    CLI Example:

    .. code-block:: bash

        $ sudo salt-run net.findmac mac=8C:60:0F:78:EC:41

    Output Example:

    .. code-block:: text

        MAC Address(es)
        _____________________________________________________________________________________________
        |    Device    | Interface |        MAC        | VLAN | Static | Active | Moves | Last move |
        _____________________________________________________________________________________________
        | edge01.bjm01 |  ae0.171  | 8C:60:0F:78:EC:41 | 171  | False  |  True  |   0   |    0.0    |
        _____________________________________________________________________________________________
    '''
    labels = {
        'device': 'Device',
        'interface': 'Interface',
        'mac': 'MAC',
        'vlan': 'VLAN',
        'static': 'Static',
        'active': 'Active',
        'moves': 'Moves',
        'last_move': 'Last Move'
    }
    rows = []

    all_mac = _get_mine('net.mac')

    title = "MAC Address(es)"
    if device:
        title += ' on device {device}'.format(device=device)
    if interface:
        title += ' on interface {interf}'.format(interf=interface)
    if vlan:
        title += ' on VLAN {vlan}'.format(vlan=vlan)

    if device:
        all_mac = {device: all_mac.get(device)}

    for device, device_mac in six.iteritems(all_mac):
        if not device_mac:
            continue
        if not device_mac.get('result', False):
            continue
        mac_table = device_mac.get('out', [])
        for mac_entry in mac_table:
            if ((mac and    # pylint: disable=too-many-boolean-expressions
                napalm_helpers.convert(napalm_helpers.mac, mac_entry.get('mac', '')) ==
                napalm_helpers.convert(napalm_helpers.mac, mac)) or
                (interface and interface in mac_entry.get('interface', '')) or
               (vlan and six.text_type(mac_entry.get('vlan', '')) == six.text_type(vlan))):
                rows.append({
                    'device': device,
                    'interface': mac_entry.get('interface'),
                    'mac': napalm_helpers.convert(napalm_helpers.mac, mac_entry.get('mac')),
                    'vlan': mac_entry.get('vlan'),
                    'static': mac_entry.get('static'),
                    'active': mac_entry.get('active'),
                    'moves': mac_entry.get('moves'),
                    'last_move': mac_entry.get('last_move')
                })

    return _display_runner(rows, labels, title, display=display)


def lldp(device=None,
         interface=None,
         title=None,
         pattern=None,
         chassis=None,
         display=_DEFAULT_DISPLAY):
    '''
    Search in the LLDP neighbors, using the following mine functions:

    - net.lldp

    Optional arguments:

    device
        Return interface data from a certain device only.

    interface
        Return data selecting by interface name.

    pattern
        Return LLDP neighbors that have contain this pattern in one of the following fields:

        - Remote Port ID
        - Remote Port Description
        - Remote System Name
        - Remote System Description

    chassis
        Search using a specific Chassis ID.

    display: ``True``
        Display on the screen or return structured object? Default: ``True`` (return on the CLI).

    display: ``True``
        Display on the screen or return structured object? Default: ``True`` (return on the CLI).

    title
        Display a custom title for the table.

    CLI Example:

    .. code-block:: bash

        $ sudo salt-run net.lldp pattern=Ethernet1/48

    Output Example:

    .. code-block:: text

        Pattern "Ethernet1/48" found in one of the following LLDP details
        _________________________________________________________________________________________________________________________________________________________________________________________
        |    Device    | Interface | Parent Interface | Remote Chassis ID | Remote Port ID | Remote Port Description |   Remote System Name   |            Remote System Description            |
        _________________________________________________________________________________________________________________________________________________________________________________________
        | edge01.bjm01 |  xe-2/3/4 |       ae0        | 8C:60:4F:3B:52:19 |                |       Ethernet1/48      | edge05.bjm01.dummy.net |   Cisco NX-OS(tm) n6000, Software (n6000-uk9),  |
        |              |           |                  |                   |                |                         |                        | Version 7.3(0)N7(5), RELEASE SOFTWARE Copyright |
        |              |           |                  |                   |                |                         |                        |  (c) 2002-2012 by Cisco Systems, Inc. Compiled  |
        |              |           |                  |                   |                |                         |                        |                2/17/2016 22:00:00               |
        _________________________________________________________________________________________________________________________________________________________________________________________
        | edge01.flw01 |  xe-1/2/3 |       ae0        | 8C:60:4F:1A:B4:22 |                |       Ethernet1/48      | edge05.flw01.dummy.net |   Cisco NX-OS(tm) n6000, Software (n6000-uk9),  |
        |              |           |                  |                   |                |                         |                        | Version 7.3(0)N7(5), RELEASE SOFTWARE Copyright |
        |              |           |                  |                   |                |                         |                        |  (c) 2002-2012 by Cisco Systems, Inc. Compiled  |
        |              |           |                  |                   |                |                         |                        |                2/17/2016 22:00:00               |
        _________________________________________________________________________________________________________________________________________________________________________________________
        | edge01.oua01 |  xe-0/1/2 |       ae1        | 8C:60:4F:51:A4:22 |                |       Ethernet1/48      | edge05.oua01.dummy.net |   Cisco NX-OS(tm) n6000, Software (n6000-uk9),  |
        |              |           |                  |                   |                |                         |                        | Version 7.3(0)N7(5), RELEASE SOFTWARE Copyright |
        |              |           |                  |                   |                |                         |                        |  (c) 2002-2012 by Cisco Systems, Inc. Compiled  |
        |              |           |                  |                   |                |                         |                        |                2/17/2016 22:00:00               |
        _________________________________________________________________________________________________________________________________________________________________________________________
    '''
    all_lldp = _get_mine('net.lldp')

    labels = {
        'device': 'Device',
        'interface': 'Interface',
        'parent_interface': 'Parent Interface',
        'remote_chassis_id': 'Remote Chassis ID',
        'remote_port_id': 'Remote Port ID',
        'remote_port_desc': 'Remote Port Description',
        'remote_system_name': 'Remote System Name',
        'remote_system_desc': 'Remote System Description'
    }
    rows = []

    if pattern:
        title = 'Pattern "{0}" found in one of the following LLDP details'.format(pattern)
    if not title:
        title = 'LLDP Neighbors'
        if interface:
            title += ' for interface {0}'.format(interface)
        else:
            title += ' for all interfaces'
        if device:
            title += ' on device {0}'.format(device)
        if chassis:
            title += ' having Chassis ID {0}'.format(chassis)

    if device:
        all_lldp = {device: all_lldp.get(device)}

    for device, device_lldp in six.iteritems(all_lldp):
        if not device_lldp:
            continue
        if not device_lldp.get('result', False):
            continue
        lldp_interfaces = device_lldp.get('out', {})
        if interface:
            lldp_interfaces = {interface: lldp_interfaces.get(interface, [])}
        for intrf, interface_lldp in six.iteritems(lldp_interfaces):
            if not interface_lldp:
                continue
            for lldp_row in interface_lldp:
                rsn = (lldp_row.get('remote_system_name', '') or '')
                rpi = (lldp_row.get('remote_port_id', '') or '')
                rsd = (lldp_row.get('remote_system_description', '') or '')
                rpd = (lldp_row.get('remote_port_description', '') or '')
                rci = (lldp_row.get('remote_chassis_id', '') or '')
                if pattern:
                    ptl = pattern.lower()
                    if not((ptl in rsn.lower()) or (ptl in rsd.lower()) or
                           (ptl in rpd.lower()) or (ptl in rci.lower())):
                        # nothing matched, let's move on
                        continue
                if chassis:
                    if (napalm_helpers.convert(napalm_helpers.mac, rci) !=
                       napalm_helpers.convert(napalm_helpers.mac, chassis)):
                        continue
                rows.append({
                    'device': device,
                    'interface': intrf,
                    'parent_interface': (lldp_row.get('parent_interface', '') or ''),
                    'remote_chassis_id': napalm_helpers.convert(napalm_helpers.mac, rci),
                    'remote_port_id': rpi,
                    'remote_port_descr': rpd,
                    'remote_system_name': rsn,
                    'remote_system_descr': rsd
                })

    return _display_runner(rows, labels, title, display=display)


def find(addr, best=True, display=_DEFAULT_DISPLAY):
    '''
    Search in all possible entities (Interfaces, MAC tables, ARP tables, LLDP neighbors),
    using the following mine functions:

    - net.mac
    - net.arp
    - net.lldp
    - net.ipaddrs
    - net.interfaces

    This function has the advantage that it knows where to look, but the output might
    become quite long as returns all possible matches.

    Optional arguments:

    best: ``True``
        Return only the best match with the interfaces IP networks
        when the saerching pattern is a valid IP Address or Network.

    display: ``True``
        Display on the screen or return structured object? Default: ``True`` (return on the CLI).

    CLI Example:

    .. code-block:: bash

        $ sudo salt-run net.find 10.10.10.7

    Output Example:

    .. code-block:: text

        Details for all interfaces that include network 10.10.10.7/32 - only best match returned
        ________________________________________________________________________________________________________________________
        |    Device    | Interface | Interface Description |  UP  | Enabled | Speed [Mbps] |    MAC Address    |  IP Addresses |
        ________________________________________________________________________________________________________________________
        | edge01.flw01 |    irb    |                       | True |   True  |      -1      | 5C:5E:AB:AC:52:B4 | 10.10.10.1/22 |
        ________________________________________________________________________________________________________________________

        ARP Entries for IP 10.10.10.7
        _____________________________________________________________________________
        |    Device    |     Interface     |        MAC        |     IP     |  Age  |
        _____________________________________________________________________________
        | edge01.flw01 | irb.349 [ae0.349] | 2C:60:0C:2A:4C:0A | 10.10.10.7 | 832.0 |
        _____________________________________________________________________________
    '''
    if not addr:
        if display:
            print('Please type a valid MAC/IP Address / Device / Interface / VLAN')
        return {}

    device = ''
    interface = ''
    mac = ''
    ip = ''  # pylint: disable=invalid-name
    ipnet = None

    results = {
        'int_net': [],
        'int_descr': [],
        'int_name': [],
        'int_ip': [],
        'int_mac': [],
        'int_device': [],
        'lldp_descr': [],
        'lldp_int': [],
        'lldp_device': [],
        'lldp_mac': [],
        'lldp_device_int': [],
        'mac_device': [],
        'mac_int': [],
        'arp_device': [],
        'arp_int': [],
        'arp_mac': [],
        'arp_ip': []
    }

    if isinstance(addr, int):
        results['mac'] = findmac(vlan=addr, display=display)
        if not display:
            return results
        else:
            return None

    try:
        mac = napalm_helpers.convert(napalm_helpers.mac, addr)
    except IndexError:
        # no problem, let's keep searching
        pass
    if salt.utils.network.is_ipv6(addr):
        mac = False
    if not mac:
        try:
            ip = napalm_helpers.convert(napalm_helpers.ip, addr)  # pylint: disable=invalid-name
        except ValueError:
            pass
        ipnet = _get_network_obj(addr)
        if ipnet:
            results['int_net'] = interfaces(ipnet=ipnet, best=best, display=display)
        if not (ipnet or ip):
            # search in all possible places
            # display all interfaces details
            results['int_descr'] = interfaces(pattern=addr, display=display)
            results['int_name'] = interfaces(interface=addr, display=display)
            results['int_device'] = interfaces(device=addr, display=display)
            # search in LLDP details
            results['lldp_descr'] = lldp(pattern=addr, display=display)
            results['lldp_int'] = lldp(interface=addr, display=display)
            results['lldp_device'] = lldp(device=addr, display=display)
            # search in MAC Address tables
            results['mac_device'] = findmac(device=addr, display=display)
            results['mac_int'] = findmac(interface=addr, display=display)
            # search in ARP tables
            results['arp_device'] = findarp(device=addr, display=display)
            results['arp_int'] = findarp(interface=addr, display=display)
            if not display:
                return results
    if mac:
        results['int_descr'] = findmac(mac=mac, display=display)
        results['arp_mac'] = findarp(mac=mac, display=display)
        results['lldp_mac'] = lldp(chassis=mac, display=display)
    if ip:
        results['arp_ip'] = findarp(ip=ip, display=display)

    # let's search in Interfaces

    if mac:
        device, interface, ips = _find_interfaces_ip(mac)
        ip = ', '.join(ips)  # pylint: disable=invalid-name
        if device and interface:
            title = 'Interface {interface} on {device} has the physical address ({mac})'.format(
                interface=interface,
                device=device,
                mac=mac
            )
            results['int_mac'] = interfaces(device=device, interface=interface, title=title, display=display)

    elif ip:
        device, interface, mac = _find_interfaces_mac(ip)
        if device and interface:
            title = 'IP Address {ip} is set for interface {interface}, on {device}'.format(
                interface=interface,
                device=device,
                ip=ip
            )
            results['int_ip'] = interfaces(device=device, interface=interface, title=title, display=display)

    if device and interface:
        results['lldp_device_int'] = lldp(device, interface, display=display)

    if not display:
        return results


def multi_find(*patterns, **kwargs):
    '''
    Execute multiple search tasks.
    This function is based on the `find` function.
    Depending on the search items, some information might overlap.

    Optional arguments:

    best: ``True``
        Return only the best match with the interfaces IP networks
        when the saerching pattern is a valid IP Address or Network.

    display: ``True``
        Display on the screen or return structured object? Default: `True` (return on the CLI).


    CLI Example:

    .. code-block:: bash

        $ sudo salt-run net.multi_find Ethernet1/49 xe-0/1/2

    Output Example:

    .. code-block:: text

        Pattern "Ethernet1/49" found in one of the following LLDP details

            -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
            |    Device    | Interface | Parent Interface | Remote Chassis ID | Remote Port Description | Remote Port ID |          Remote System Description          |   Remote System Name   |
            -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
            | edge01.oua04 |  xe-0/1/2 |       ae1        | DE:AD:BE:EF:DE:AD |       Ethernet1/49      |                | Cisco NX-OS(tm) n6000, Software (n6000-uk9) | edge07.oua04.dummy.net |
            -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

        Details for interface xe-0/1/2

            -----------------------------------------------------------------------------------------------------------------------
            |    Device    | Interface | Interface Description | IP Addresses | Enabled |  UP  |    MAC Address    | Speed [Mbps] |
            -----------------------------------------------------------------------------------------------------------------------
            | edge01.oua04 |  xe-0/1/2 |     ae1 sw01.oua04    |              |   True  | True | BE:EF:DE:AD:BE:EF |    10000     |
            -----------------------------------------------------------------------------------------------------------------------

        LLDP Neighbors for interface xe-0/1/2

            -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
            |    Device    | Interface | Parent Interface | Remote Chassis ID | Remote Port Description | Remote Port ID |          Remote System Description          |   Remote System Name   |
            -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
            | edge01.oua04 |  xe-0/1/2 |       ae1        | DE:AD:BE:EF:DE:AD |       Ethernet1/49      |                | Cisco NX-OS(tm) n6000, Software (n6000-uk9) | edge07.oua04.dummy.net |
            -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
    '''
    out = {}
    for pattern in set(patterns):
        search_result = find(pattern,
                             best=kwargs.get('best', True),
                             display=kwargs.get('display', _DEFAULT_DISPLAY))
        out[pattern] = search_result
    if not kwargs.get('display', _DEFAULT_DISPLAY):
        return out
