.. versionadded:: 2018.3.0

    Returns the differences between two containers' networks. When a network is
    only present one of the two containers, that network's diff will simply be
    represented with ``True`` for the side of the diff in which the network is
    present) and ``False`` for the side of the diff in which the network is
    absent.

    This function works by comparing the contents of both containers'
    ``Networks`` keys (under ``NetworkSettings``) in the return data from
    :py:func:`docker.inspect_container
    <salt.modules.dockermod.inspect_container>`. Because each network contains
    some items that either A) only set at runtime, B) naturally varying from
    container to container, or both, by default the following keys in each
    network are examined:

    - **Aliases**
    - **Links**
    - **IPAMConfig**

    The exception to this is if ``IPAMConfig`` is unset (i.e. null) in one
    container but not the other. This happens when no static IP configuration
    is set, and automatic IP configuration is in effect. So, in order to report
    on changes between automatic IP configuration in one container and static
    IP configuration in another container (as we need to do for the
    :py:func:`docker_container.running <salt.states.docker_container.running>`
    state), automatic IP configuration will also be checked in these cases.

    This function uses the :conf_minion:`docker.compare_container_networks`
    minion config option to determine which keys to examine. This provides
    flexibility in the event that features added in a future Docker release
    necessitate changes to how Salt compares networks. In these cases, rather
    than waiting for a new Salt release one can just set
    :conf_minion:`docker.compare_container_networks`.

    .. note::
        The checks for automatic IP configuration described above only apply if
        ``IPAMConfig`` is among the keys set for static IP checks in
        :conf_minion:`docker.compare_container_networks`.

    first
        Name or ID of first container (old)

    second
        Name or ID of second container (new)


    CLI Example:

    .. code-block:: bash

        salt myminion docker.compare_container_networks foo bar

def compare_container_networks(first, second):
    '''
    .. versionadded:: 2018.3.0

    Returns the differences between two containers' networks. When a network is
    only present one of the two containers, that network's diff will simply be
    represented with ``True`` for the side of the diff in which the network is
    present) and ``False`` for the side of the diff in which the network is
    absent.

    This function works by comparing the contents of both containers'
    ``Networks`` keys (under ``NetworkSettings``) in the return data from
    :py:func:`docker.inspect_container
    <salt.modules.dockermod.inspect_container>`. Because each network contains
    some items that either A) only set at runtime, B) naturally varying from
    container to container, or both, by default the following keys in each
    network are examined:

    - **Aliases**
    - **Links**
    - **IPAMConfig**

    The exception to this is if ``IPAMConfig`` is unset (i.e. null) in one
    container but not the other. This happens when no static IP configuration
    is set, and automatic IP configuration is in effect. So, in order to report
    on changes between automatic IP configuration in one container and static
    IP configuration in another container (as we need to do for the
    :py:func:`docker_container.running <salt.states.docker_container.running>`
    state), automatic IP configuration will also be checked in these cases.

    This function uses the :conf_minion:`docker.compare_container_networks`
    minion config option to determine which keys to examine. This provides
    flexibility in the event that features added in a future Docker release
    necessitate changes to how Salt compares networks. In these cases, rather
    than waiting for a new Salt release one can just set
    :conf_minion:`docker.compare_container_networks`.

    .. note::
        The checks for automatic IP configuration described above only apply if
        ``IPAMConfig`` is among the keys set for static IP checks in
        :conf_minion:`docker.compare_container_networks`.

    first
        Name or ID of first container (old)

    second
        Name or ID of second container (new)


    CLI Example:

    .. code-block:: bash

        salt myminion docker.compare_container_networks foo bar
    '''
    def _get_nets(data):
        return data.get('NetworkSettings', {}).get('Networks', {})

    compare_keys = __opts__['docker.compare_container_networks']
    result1 = inspect_container(first) \
        if not isinstance(first, dict) \
        else first
    result2 = inspect_container(second) \
        if not isinstance(second, dict) \
        else second
    nets1 = _get_nets(result1)
    nets2 = _get_nets(result2)
    state1 = state(first)
    state2 = state(second)

    # When you attempt and fail to set a static IP (for instance, because the
    # IP is not in the network's subnet), Docker will raise an exception but
    # will (incorrectly) leave the record for that network in the inspect
    # results for the container. Work around this behavior (bug?) by checking
    # which containers are actually connected.
    all_nets = set(nets1)
    all_nets.update(nets2)
    for net_name in all_nets:
        try:
            connected_containers = inspect_network(
                net_name).get('Containers', {})
        except Exception as exc:
            # Shouldn't happen unless a network was removed outside of Salt
            # between the time that a docker_container.running state started
            # and when this comparison took place.
            log.warning(
                'Failed to inspect Docker network %s: %s', net_name, exc)
            continue
        else:
            if state1 == 'running' \
                    and net_name in nets1 \
                    and result1['Id'] not in connected_containers:
                del nets1[net_name]
            if state2 == 'running' \
                    and net_name in nets2 \
                    and result2['Id'] not in connected_containers:
                del nets2[net_name]

    ret = {}

    def _check_ipconfig(ret, net_name, **kwargs):
        # Make some variables to make the logic below easier to understand
        nets1_missing = 'old' not in kwargs
        if nets1_missing:
            nets1_static = False
        else:
            nets1_static = bool(kwargs['old'])
        nets1_autoip = not nets1_static and not nets1_missing
        nets2_missing = 'new' not in kwargs
        if nets2_missing:
            nets2_static = False
        else:
            nets2_static = bool(kwargs['new'])
        nets2_autoip = not nets2_static and not nets2_missing

        autoip_keys = compare_keys.get('automatic', [])

        if nets1_autoip and (nets2_static or nets2_missing):
            for autoip_key in autoip_keys:
                autoip_val = nets1[net_name].get(autoip_key)
                if autoip_val:
                    ret.setdefault(net_name, {})[autoip_key] = {
                        'old': autoip_val, 'new': None
                    }
            if nets2_static:
                ret.setdefault(net_name, {})['IPAMConfig'] = {
                    'old': None, 'new': kwargs['new']
                }
            if not any(x in ret.get(net_name, {}) for x in autoip_keys):
                ret.setdefault(net_name, {})['IPConfiguration'] = {
                    'old': 'automatic',
                    'new': 'static' if nets2_static else 'not connected'
                }
        elif nets2_autoip and (nets1_static or nets1_missing):
            for autoip_key in autoip_keys:
                autoip_val = nets2[net_name].get(autoip_key)
                if autoip_val:
                    ret.setdefault(net_name, {})[autoip_key] = {
                        'old': None, 'new': autoip_val
                    }
            if not any(x in ret.get(net_name, {}) for x in autoip_keys):
                ret.setdefault(net_name, {})['IPConfiguration'] = {
                    'old': 'static' if nets1_static else 'not connected',
                    'new': 'automatic'
                }
            if nets1_static:
                ret.setdefault(net_name, {})['IPAMConfig'] = {
                    'old': kwargs['old'], 'new': None
                }
        else:
            old_val = kwargs.get('old')
            new_val = kwargs.get('new')
            if old_val != new_val:
                # Static IP configuration present in both containers and there
                # are differences, so report them
                ret.setdefault(net_name, {})['IPAMConfig'] = {
                    'old': old_val, 'new': new_val
                }

    for net_name in (x for x in nets1 if x not in nets2):
        # Network is not in the network_settings, but the container is attached
        # to the network
        for key in compare_keys.get('static', []):
            val = nets1[net_name].get(key)
            if key == 'IPAMConfig':
                _check_ipconfig(ret, net_name, old=val)
            if val:
                if key == 'Aliases':
                    try:
                        val.remove(result1['Config']['Hostname'])
                    except (ValueError, AttributeError):
                        pass
                    else:
                        if not val:
                            # The only alias was the default one for the
                            # hostname
                            continue
                ret.setdefault(net_name, {})[key] = {'old': val, 'new': None}

    for net_name in nets2:
        if net_name not in nets1:
            # Container is not attached to the network, but network is present
            # in the network_settings
            for key in compare_keys.get('static', []):
                val = nets2[net_name].get(key)
                if key == 'IPAMConfig':
                    _check_ipconfig(ret, net_name, new=val)
                    continue
                elif val:
                    if key == 'Aliases':
                        try:
                            val.remove(result2['Config']['Hostname'])
                        except (ValueError, AttributeError):
                            pass
                        else:
                            if not val:
                                # The only alias was the default one for the
                                # hostname
                                continue
                    ret.setdefault(net_name, {})[key] = {
                        'old': None, 'new': val}
        else:
            for key in compare_keys.get('static', []):
                old_val = nets1[net_name][key]
                new_val = nets2[net_name][key]
                for item in (old_val, new_val):
                    # Normalize for list order
                    try:
                        item.sort()
                    except AttributeError:
                        pass
                if key == 'Aliases':
                    # Normalize for hostname alias
                    try:
                        old_val.remove(result1['Config']['Hostname'])
                    except (AttributeError, ValueError):
                        pass
                    if not old_val:
                        old_val = None
                    try:
                        new_val.remove(result2['Config']['Hostname'])
                    except (AttributeError, ValueError):
                        pass
                    if not new_val:
                        new_val = None
                elif key == 'IPAMConfig':
                    _check_ipconfig(ret, net_name, old=old_val, new=new_val)
                    # We don't need the final check since it's included in the
                    # _check_ipconfig helper
                    continue
                if bool(old_val) is bool(new_val) is False:
                    continue
                elif old_val != new_val:
                    ret.setdefault(net_name, {})[key] = {
                        'old': old_val, 'new': new_val
                    }
    return ret