Ensure the networks device is configured as specified in the state SLS file.
    Probes not specified will be removed, while probes not confiured as expected will trigger config updates.

    :param probes: Defines the probes as expected to be configured on the
        device.  In order to ease the configuration and avoid repeating the
        same parameters for each probe, the next parameter (defaults) can be
        used, providing common characteristics.

    :param defaults: Specifies common parameters for the probes.

    SLS Example:

    .. code-block:: yaml

        rpmprobes:
            probes.managed:
                - probes:
                    probe_name1:
                        probe1_test1:
                            source: 192.168.0.2
                            target: 192.168.0.1
                        probe1_test2:
                            target: 172.17.17.1
                        probe1_test3:
                            target: 8.8.8.8
                            probe_type: http-ping
                    probe_name2:
                        probe2_test1:
                            test_interval: 100
                - defaults:
                    target: 10.10.10.10
                    probe_count: 15
                    test_interval: 3
                    probe_type: icmp-ping

    In the probes configuration, the only mandatory attribute is *target*
    (specified either in probes configuration, either in the defaults
    dictionary).  All the other parameters will use the operating system
    defaults, if not provided:

    - ``source`` - Specifies the source IP Address to be used during the tests.  If
      not specified will use the IP Address of the logical interface loopback0.

    - ``target`` - Destination IP Address.
    - ``probe_count`` - Total number of probes per test (1..15). System
      defaults: 1 on both JunOS & Cisco.
    - ``probe_interval`` - Delay between tests (0..86400 seconds). System
      defaults: 3 on JunOS, 5 on Cisco.
    - ``probe_type`` - Probe request type. Available options:

      - icmp-ping
      - tcp-ping
      - udp-ping

    Using the example configuration above, after running the state, on the device will be configured 4 probes,
    with the following properties:

    .. code-block:: yaml

        probe_name1:
            probe1_test1:
                source: 192.168.0.2
                target: 192.168.0.1
                probe_count: 15
                test_interval: 3
                probe_type: icmp-ping
            probe1_test2:
                target: 172.17.17.1
                probe_count: 15
                test_interval: 3
                probe_type: icmp-ping
            probe1_test3:
                target: 8.8.8.8
                probe_count: 15
                test_interval: 3
                probe_type: http-ping
        probe_name2:
            probe2_test1:
                target: 10.10.10.10
                probe_count: 15
                test_interval: 3
                probe_type: icmp-ping

def managed(name, probes, defaults=None):

    '''
    Ensure the networks device is configured as specified in the state SLS file.
    Probes not specified will be removed, while probes not confiured as expected will trigger config updates.

    :param probes: Defines the probes as expected to be configured on the
        device.  In order to ease the configuration and avoid repeating the
        same parameters for each probe, the next parameter (defaults) can be
        used, providing common characteristics.

    :param defaults: Specifies common parameters for the probes.

    SLS Example:

    .. code-block:: yaml

        rpmprobes:
            probes.managed:
                - probes:
                    probe_name1:
                        probe1_test1:
                            source: 192.168.0.2
                            target: 192.168.0.1
                        probe1_test2:
                            target: 172.17.17.1
                        probe1_test3:
                            target: 8.8.8.8
                            probe_type: http-ping
                    probe_name2:
                        probe2_test1:
                            test_interval: 100
                - defaults:
                    target: 10.10.10.10
                    probe_count: 15
                    test_interval: 3
                    probe_type: icmp-ping

    In the probes configuration, the only mandatory attribute is *target*
    (specified either in probes configuration, either in the defaults
    dictionary).  All the other parameters will use the operating system
    defaults, if not provided:

    - ``source`` - Specifies the source IP Address to be used during the tests.  If
      not specified will use the IP Address of the logical interface loopback0.

    - ``target`` - Destination IP Address.
    - ``probe_count`` - Total number of probes per test (1..15). System
      defaults: 1 on both JunOS & Cisco.
    - ``probe_interval`` - Delay between tests (0..86400 seconds). System
      defaults: 3 on JunOS, 5 on Cisco.
    - ``probe_type`` - Probe request type. Available options:

      - icmp-ping
      - tcp-ping
      - udp-ping

    Using the example configuration above, after running the state, on the device will be configured 4 probes,
    with the following properties:

    .. code-block:: yaml

        probe_name1:
            probe1_test1:
                source: 192.168.0.2
                target: 192.168.0.1
                probe_count: 15
                test_interval: 3
                probe_type: icmp-ping
            probe1_test2:
                target: 172.17.17.1
                probe_count: 15
                test_interval: 3
                probe_type: icmp-ping
            probe1_test3:
                target: 8.8.8.8
                probe_count: 15
                test_interval: 3
                probe_type: http-ping
        probe_name2:
            probe2_test1:
                target: 10.10.10.10
                probe_count: 15
                test_interval: 3
                probe_type: icmp-ping
    '''

    ret = _default_ret(name)

    result = True
    comment = ''

    rpm_probes_config = _retrieve_rpm_probes()  # retrieves the RPM config from the device
    if not rpm_probes_config.get('result'):
        ret.update({
            'result': False,
            'comment': 'Cannot retrieve configurtion of the probes from the device: {reason}'.format(
                reason=rpm_probes_config.get('comment')
            )
        })
        return ret

    # build expect probes config dictionary
    # using default values
    configured_probes = rpm_probes_config.get('out', {})
    if not isinstance(defaults, dict):
        defaults = {}
    expected_probes = _expand_probes(probes, defaults)

    _clean_probes(configured_probes)  # let's remove the unnecessary data from the configured probes
    _clean_probes(expected_probes)  # also from the expected data

    # ----- Compare expected config with the existing config ---------------------------------------------------------->

    diff = _compare_probes(configured_probes, expected_probes)  # compute the diff

    # <---- Compare expected config with the existing config -----------------------------------------------------------

    # ----- Call set_probes and delete_probes as needed --------------------------------------------------------------->

    add_probes = diff.get('add')
    update_probes = diff.get('update')
    remove_probes = diff.get('remove')

    changes = {
        'added': _ordered_dict_to_dict(add_probes),
        'updated': _ordered_dict_to_dict(update_probes),
        'removed': _ordered_dict_to_dict(remove_probes)
    }

    ret.update({
        'changes': changes
    })

    if __opts__['test'] is True:
        ret.update({
            'comment': 'Testing mode: configuration was not changed!',
            'result': None
        })
        return ret

    config_change_expected = False  # to check if something changed and a commit would be needed

    if add_probes:
        added = _set_rpm_probes(add_probes)
        if added.get('result'):
            config_change_expected = True
        else:
            result = False
            comment += 'Cannot define new probes: {reason}\n'.format(
                reason=added.get('comment')
            )

    if update_probes:
        updated = _set_rpm_probes(update_probes)
        if updated.get('result'):
            config_change_expected = True
        else:
            result = False
            comment += 'Cannot update probes: {reason}\n'.format(
                reason=updated.get('comment')
            )

    if remove_probes:
        removed = _delete_rpm_probes(remove_probes)
        if removed.get('result'):
            config_change_expected = True
        else:
            result = False
            comment += 'Cannot remove probes! {reason}\n'.format(
                reason=removed.get('comment')
            )

    # <---- Call set_probes and delete_probes as needed ----------------------------------------------------------------

    # ----- Try to save changes --------------------------------------------------------------------------------------->

    if config_change_expected:
        # if any changes expected, try to commit
        result, comment = __salt__['net.config_control']()

    # <---- Try to save changes ----------------------------------------------------------------------------------------

    # ----- Try to schedule the probes -------------------------------------------------------------------------------->

    add_scheduled = _schedule_probes(add_probes)
    if add_scheduled.get('result'):
        # if able to load the template to schedule the probes, try to commit the scheduling data
        # (yes, a second commit is needed)
        # on devices such as Juniper, RPM probes do not need to be scheduled
        # therefore the template is empty and won't try to commit empty changes
        result, comment = __salt__['net.config_control']()

    if config_change_expected:
        if result and comment == '':  # if any changes and was able to apply them
            comment = 'Probes updated successfully!'

    ret.update({
        'result': result,
        'comment': comment
    })

    return ret