Calls arbitrary methods from the network driver instance.
    Please check the readthedocs_ page for the updated list of getters.

    .. _readthedocs: http://napalm.readthedocs.org/en/latest/support/index.html#getters-support-matrix

    method
        Specifies the name of the method to be called.

    *args
        Arguments.

    **kwargs
        More arguments.

    :return: A dictionary with three keys:

        * result (True/False): if the operation succeeded
        * out (object): returns the object as-is from the call
        * comment (string): provides more details in case the call failed
        * traceback (string): complete traceback in case of exception. \
        Please submit an issue including this traceback \
        on the `correct driver repo`_ and make sure to read the FAQ_

    .. _`correct driver repo`: https://github.com/napalm-automation/napalm/issues/new
    .. FAQ_: https://github.com/napalm-automation/napalm#faq

    Example:

    .. code-block:: python

        salt.utils.napalm.call(
            napalm_object,
            'cli',
            [
                'show version',
                'show chassis fan'
            ]
        )

def call(napalm_device, method, *args, **kwargs):
    '''
    Calls arbitrary methods from the network driver instance.
    Please check the readthedocs_ page for the updated list of getters.

    .. _readthedocs: http://napalm.readthedocs.org/en/latest/support/index.html#getters-support-matrix

    method
        Specifies the name of the method to be called.

    *args
        Arguments.

    **kwargs
        More arguments.

    :return: A dictionary with three keys:

        * result (True/False): if the operation succeeded
        * out (object): returns the object as-is from the call
        * comment (string): provides more details in case the call failed
        * traceback (string): complete traceback in case of exception. \
        Please submit an issue including this traceback \
        on the `correct driver repo`_ and make sure to read the FAQ_

    .. _`correct driver repo`: https://github.com/napalm-automation/napalm/issues/new
    .. FAQ_: https://github.com/napalm-automation/napalm#faq

    Example:

    .. code-block:: python

        salt.utils.napalm.call(
            napalm_object,
            'cli',
            [
                'show version',
                'show chassis fan'
            ]
        )
    '''
    result = False
    out = None
    opts = napalm_device.get('__opts__', {})
    retry = kwargs.pop('__retry', True)  # retry executing the task?
    force_reconnect = kwargs.get('force_reconnect', False)
    if force_reconnect:
        log.debug('Forced reconnection initiated')
        log.debug('The current opts (under the proxy key):')
        log.debug(opts['proxy'])
        opts['proxy'].update(**kwargs)
        log.debug('Updated to:')
        log.debug(opts['proxy'])
        napalm_device = get_device(opts)
    try:
        if not napalm_device.get('UP', False):
            raise Exception('not connected')
        # if connected will try to execute desired command
        kwargs_copy = {}
        kwargs_copy.update(kwargs)
        for karg, warg in six.iteritems(kwargs_copy):
            # lets clear None arguments
            # to not be sent to NAPALM methods
            if warg is None:
                kwargs.pop(karg)
        out = getattr(napalm_device.get('DRIVER'), method)(*args, **kwargs)
        # calls the method with the specified parameters
        result = True
    except Exception as error:
        # either not connected
        # either unable to execute the command
        hostname = napalm_device.get('HOSTNAME', '[unspecified hostname]')
        err_tb = traceback.format_exc()  # let's get the full traceback and display for debugging reasons.
        if isinstance(error, NotImplementedError):
            comment = '{method} is not implemented for the NAPALM {driver} driver!'.format(
                method=method,
                driver=napalm_device.get('DRIVER_NAME')
            )
        elif retry and HAS_CONN_CLOSED_EXC_CLASS and isinstance(error, ConnectionClosedException):
            # Received disconection whilst executing the operation.
            # Instructed to retry (default behaviour)
            #   thus trying to re-establish the connection
            #   and re-execute the command
            #   if any of the operations (close, open, call) will rise again ConnectionClosedException
            #   it will fail loudly.
            kwargs['__retry'] = False  # do not attempt re-executing
            comment = 'Disconnected from {device}. Trying to reconnect.'.format(device=hostname)
            log.error(err_tb)
            log.error(comment)
            log.debug('Clearing the connection with %s', hostname)
            call(napalm_device, 'close', __retry=False)  # safely close the connection
            # Make sure we don't leave any TCP connection open behind
            #   if we fail to close properly, we might not be able to access the
            log.debug('Re-opening the connection with %s', hostname)
            call(napalm_device, 'open', __retry=False)
            log.debug('Connection re-opened with %s', hostname)
            log.debug('Re-executing %s', method)
            return call(napalm_device, method, *args, **kwargs)
            # If still not able to reconnect and execute the task,
            #   the proxy keepalive feature (if enabled) will attempt
            #   to reconnect.
            # If the device is using a SSH-based connection, the failure
            #   will also notify the paramiko transport and the `is_alive` flag
            #   is going to be set correctly.
            # More background: the network device may decide to disconnect,
            #   although the SSH session itself is alive and usable, the reason
            #   being the lack of activity on the CLI.
            #   Paramiko's keepalive doesn't help in this case, as the ServerAliveInterval
            #   are targeting the transport layer, whilst the device takes the decision
            #   when there isn't any activity on the CLI, thus at the application layer.
            #   Moreover, the disconnect is silent and paramiko's is_alive flag will
            #   continue to return True, although the connection is already unusable.
            #   For more info, see https://github.com/paramiko/paramiko/issues/813.
            #   But after a command fails, the `is_alive` flag becomes aware of these
            #   changes and will return False from there on. And this is how the
            #   Salt proxy keepalive helps: immediately after the first failure, it
            #   will know the state of the connection and will try reconnecting.
        else:
            comment = 'Cannot execute "{method}" on {device}{port} as {user}. Reason: {error}!'.format(
                device=napalm_device.get('HOSTNAME', '[unspecified hostname]'),
                port=(':{port}'.format(port=napalm_device.get('OPTIONAL_ARGS', {}).get('port'))
                      if napalm_device.get('OPTIONAL_ARGS', {}).get('port') else ''),
                user=napalm_device.get('USERNAME', ''),
                method=method,
                error=error
            )
        log.error(comment)
        log.error(err_tb)
        return {
            'out': {},
            'result': False,
            'comment': comment,
            'traceback': err_tb
        }
    finally:
        if opts and not_always_alive(opts) and napalm_device.get('CLOSE', True):
            # either running in a not-always-alive proxy
            # either running in a regular minion
            # close the connection when the call is over
            # unless the CLOSE is explicitly set as False
            napalm_device['DRIVER'].close()
    return {
        'out': out,
        'result': result,
        'comment': ''
    }