docs: 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)