<SYSTEM_TASK:>
Solve the following problem using Python, implementing the functions described below, one line at a time
<END_TASK>
<USER_TASK:>
Description:
 def emulate_network(network_constraints, roles=None, inventory_path=None, extra_vars=None):
 """Emulate network links. Read ``network_constraints`` and apply ``tc`` rules on all the nodes. Constraints are applied between groups of machines. Theses groups are described in the ``network_constraints`` variable and must be found in the inventory file. The newtwork constraints support ``delay``, ``rate`` and ``loss``. Args: network_constraints (dict):
 network constraints to apply roles (dict):
 role->hosts mapping as returned by :py:meth:`enoslib.infra.provider.Provider.init` inventory_path(string):
 path to an inventory extra_vars (dict):
 extra_vars to pass to ansible Examples: * Using defaults The following will apply the network constraints between every groups. For instance the constraints will be applied for the communication between "n1" and "n3" but not between "n1" and "n2". Note that using default leads to symetric constraints. .. code-block:: python roles = { "grp1": ["n1", "n2"], "grp2": ["n3", "n4"], "grp3": ["n3", "n4"], } tc = { "enable": True, "default_delay": "20ms", "default_rate": "1gbit", } emulate_network(roles, tc) If you want to control more precisely which groups need to be taken into account, you can use ``except`` or ``groups`` key .. code-block:: python tc = { "enable": True, "default_delay": "20ms", "default_rate": "1gbit", "except": "grp3" } emulate_network(roles, tc) is equivalent to .. code-block:: python tc = { "enable": True, "default_delay": "20ms", "default_rate": "1gbit", "groups": ["grp1", "grp2"] } emulate_network(roles, inventory, tc) * Using ``src`` and ``dst`` The following will enforce a symetric constraint between ``grp1`` and ``grp2``. .. code-block:: python tc = { "enable": True, "default_delay": "20ms", "default_rate": "1gbit", "constraints": [{ "src": "grp1" "dst": "grp2" "delay": "10ms" "symetric": True }] } emulate_network(roles, inventory, tc) """

    # 1) Retrieve the list of ips for all nodes (Ansible)
    # 2) Build all the constraints (Python)
    #    {source:src, target: ip_dest, device: if, rate:x,  delay:y}
    # 3) Enforce those constraints (Ansible)

    if not network_constraints:
        return

    if roles is None and inventory is None:
        raise ValueError("roles and inventory can't be None")

    if not extra_vars:
        extra_vars = {}

    # 1. getting  ips/devices information
    logger.debug('Getting the ips of all nodes')
    tmpdir = os.path.join(os.getcwd(), TMP_DIRNAME)
    _check_tmpdir(tmpdir)
    utils_playbook = os.path.join(ANSIBLE_DIR, 'utils.yml')
    ips_file = os.path.join(tmpdir, 'ips.txt')
    options = {'enos_action': 'tc_ips',
               'ips_file': ips_file}
    run_ansible([utils_playbook], roles=roles, extra_vars=options)

    # 2.a building the group constraints
    logger.debug('Building all the constraints')
    constraints = _build_grp_constraints(roles, network_constraints)
    # 2.b Building the ip/device level constaints
    with open(ips_file) as f:
        ips = yaml.safe_load(f)
        # will hold every single constraint
        ips_with_constraints = _build_ip_constraints(roles,
                                                     ips,
                                                     constraints)
        # dumping it for debugging purpose
        ips_with_constraints_file = os.path.join(tmpdir,
                                                 'ips_with_constraints.yml')
        with open(ips_with_constraints_file, 'w') as g:
            yaml.dump(ips_with_constraints, g)

    # 3. Enforcing those constraints
    logger.info('Enforcing the constraints')
    # enabling/disabling network constraints
    enable = network_constraints.setdefault('enable', True)
    utils_playbook = os.path.join(ANSIBLE_DIR, 'utils.yml')
    options = {
        'enos_action': 'tc_apply',
        'ips_with_constraints': ips_with_constraints,
        'tc_enable': enable,
    }
    options.update(extra_vars)
    run_ansible([utils_playbook],
                roles=roles,
                inventory_path=inventory_path,
                extra_vars=options)