Network Emulation Service#

Introduction#

This module exposes different ways of doing network emulation [1] based on tc [2] of the iproute2 tool and use netem [3] as cornerstone.

Different kinds of network topologies can be built and different level of abstractions can be used.

At the network device interface level (the lowest level for now) you can use netem() to set inbound and/or outbound limitation on arbitrary network interfaces of your nodes. Nodes can be seen as the vertices of a star topology where the center is the core of the network. For instance the higher the latency on a node is, the further it is from the core of the network.

The above prevent you to set heterogeneous limitations ie based on the packets destinations. The function netem_htb() enforces Hierarchical Token Bucket [4] on your nodes. This lets you modeling a n-simplex topology where the vertices represent your nodes and the edges the limitations.

Working at the device interface level provides you with great flexibility, however this comes at the cost of being very explicit. For instance, you must know in advance the device names, the IP target… From a higher perspective three Services, working at the role level are provided. The services will infer everything for you (device names, target IPs) based on high level parameters. Of course, those services are using internally the above functions.

  • Netem allows for emulating a star topology: you control the constraints of a node to/from the center of the topology.

  • NetemHTB will enforce heterogeneous network limitations based on the role names (and thus based on the packets’ destination)

  • AccurateNetemHTB inherits from the previous one, but will fix the latency limitations so that the user defined delays match the end-to-end latency of the application.

Note

Requirements:

  • ifb module loaded on the remote hosts for Netem service and netem function.

  • tc tool available (install iproute2 package on debian based distribution)

Netem emulation#

Classes:

Netem()

Set homogeneous network constraints on some hosts

NetemInConstraint(device, options)

A Constraint on the ingress part of a device.

NetemInOutSource(host, constraints)

Model a host and the constraints on its network devices.

NetemOutConstraint(device, options)

A Constraint on the egress part of a device.

Functions:

netem(sources[, chunk_size])

Helper function to enforce in/out limitations on host devices.

class enoslib.service.emul.netem.Netem#

Set homogeneous network constraints on some hosts

Geometrically speaking: nodes are put on a star topology. Limitation are set from a node to/from the center of the Network.

This calls netem() function internally. As a consequence when symmetric is True, it will apply 4 times the constraints for bidirectional communication (out_a, in_b, out_b, in_a) and 2 times if symmetric is False.

Example

 1import logging
 2
 3import enoslib as en
 4
 5en.init_logging(level=logging.INFO)
 6en.check()
 7
 8conf = (
 9    en.G5kConf.from_settings(job_type=[], walltime="01:00:00")
10    .add_machine(
11        roles=["city", "paris"],
12        cluster="paravance",
13        nodes=1,
14    )
15    .add_machine(
16        roles=["city", "berlin"],
17        cluster="paravance",
18        nodes=1,
19    )
20    .add_machine(
21        roles=["city", "londres"],
22        cluster="paravance",
23        nodes=1,
24    )
25)
26provider = en.G5k(conf)
27roles, networks = provider.init()
28roles = en.sync_info(roles, networks)
29
30netem = en.Netem()
31(
32    netem.add_constraints("delay 10ms", roles["paris"], symmetric=True)
33    .add_constraints("delay 20ms", roles["londres"], symmetric=True)
34    .add_constraints("delay 30ms", roles["berlin"], symmetric=True)
35)
36
37netem.deploy()
38netem.validate()
39netem.destroy()
40
41for role, hosts in roles.items():
42    print(role)
43    for host in hosts:
44        print(f"-- {host.alias}")
  • Using a secondary network.

     1import logging
     2from pathlib import Path
     3
     4import enoslib as en
     5
     6en.init_logging(level=logging.INFO)
     7en.check()
     8
     9CLUSTER = "paravance"
    10SITE = en.g5k_api_utils.get_cluster_site(CLUSTER)
    11
    12job_name = Path(__file__).name
    13
    14private = en.G5kNetworkConf(type="kavlan-global", roles=["private"], site=SITE)
    15
    16conf = (
    17    en.G5kConf.from_settings(
    18        job_name=job_name,
    19        walltime="00:30:00",
    20        job_type=["deploy"],
    21        env_name="debian11-nfs",
    22    )
    23    .add_network_conf(private)
    24    .add_machine(
    25        roles=["paris"],
    26        cluster=CLUSTER,
    27        nodes=1,
    28        secondary_networks=[private],
    29    )
    30    .add_machine(
    31        roles=["londres"],
    32        cluster=CLUSTER,
    33        nodes=1,
    34        secondary_networks=[private],
    35    )
    36    .add_machine(
    37        roles=["berlin"],
    38        cluster=CLUSTER,
    39        nodes=1,
    40        secondary_networks=[private],
    41    )
    42)
    43
    44provider = en.G5k(conf)
    45roles, networks = provider.init()
    46roles = en.sync_info(roles, networks)
    47
    48netem = en.Netem()
    49(
    50    netem.add_constraints(
    51        "delay 10ms", roles["paris"], networks=networks["private"], symmetric=True
    52    )
    53    .add_constraints(
    54        "delay 20ms", roles["londres"], networks=networks["private"], symmetric=True
    55    )
    56    .add_constraints(
    57        "delay 30ms", roles["berlin"], networks=networks["private"], symmetric=True
    58    )
    59)
    60
    61netem.deploy()
    62netem.validate()
    63
    64for role, hosts in roles.items():
    65    print(role)
    66    for host in hosts:
    67        print(f"-- {host.alias}")
    
backup()#

(abstract) Backup the service.

deploy(chunk_size=100, **kwargs)#

Apply the constraints on all the hosts.

destroy(**kwargs)#

(abstract) Destroy the service.

class enoslib.service.emul.netem.NetemInConstraint(device: str, options: str)#

A Constraint on the ingress part of a device.

Inbound limitations works differently. see https://wiki.linuxfoundation.org/networking/netem

We’ll create an ifb, redirect incoming traffic to it and apply some queuing discipline using netem.

Parameters:

ifb – the ifb name (e.g. ifb0) that will be used. That means that the various ifbs must be provisioned out of band.

add_commands(ifb: str) List[str]#

Return the commands that adds the ifb.

commands(ifb: str) List[str]#

Return the commands that redirect and apply netem constraints on the ifb.

remove_commands(ifb: str) List[str]#

Return the commands that remove the qdisc from the ifb and the net device.

class enoslib.service.emul.netem.NetemInOutSource(host: ~enoslib.objects.Host, constraints: ~typing.Set[~enoslib.service.emul.netem.NetemConstraint] = <factory>)#

Model a host and the constraints on its network devices.

Parameters:
  • inbound – The constraints to set on the ingress part of the host devices

  • outbound – The constraints to set on the egress part of the host devices

add_constraints(constraints: Iterable[NetemConstraint])#

Merge constraints to existing ones.

In this context if two constraints are set on the same device we overwrite the original one. At the end we ensure that there’s only one constraint per device

Parameters:

constraints – Iterable of NetemIn[Out]Constraint

equal(c1: NetemConstraint, c2: NetemConstraint) bool#

Encode the equality between two constraints in this context.

class enoslib.service.emul.netem.NetemOutConstraint(device: str, options: str)#

A Constraint on the egress part of a device.

Parameters:
  • device – the device name where the qdisc will be added

  • options – the options string the pass down to netem (e.g. delay 10ms)

commands(_: str) List[str]#

Nothing to do.

enoslib.service.emul.netem.netem(sources: List[NetemInOutSource], chunk_size: int = 100, **kwargs)#

Helper function to enforce in/out limitations on host devices.

Nodes can be seen as the vertices of a star topology where the center is the core of the network. For instance the higher the latency on a node is, the further it is from the core of the network.

This method is optimized toward execution time: enforcing thousands of atomic constraints (= tc commands) shouldn’t be a problem. Commands are sent by batch and chunk_size controls the size of the batch.

Idempotency note: the existing qdiscs are removed before applying new ones. This must be safe in most of the cases to consider that this is a form of idempotency.

Parameters:
  • sources – list of constraints to apply as a list of Source

  • chunk_size – size of the chunk to use

  • kwargs – keyword argument to pass to enoslib.api.run_ansible().

Example

 1import logging
 2from pathlib import Path
 3
 4import enoslib as en
 5
 6en.init_logging(level=logging.INFO)
 7en.check()
 8
 9job_name = Path(__file__).name
10
11conf = (
12    en.G5kConf.from_settings(job_name=job_name, job_type=[])
13    .add_machine(
14        roles=["city", "paris"],
15        cluster="paravance",
16        nodes=1,
17    )
18    .add_machine(
19        roles=["city", "berlin"],
20        cluster="paravance",
21        nodes=1,
22    )
23    .add_machine(
24        roles=["city", "londres"],
25        cluster="paravance",
26        nodes=1,
27    )
28)
29provider = en.G5k(conf)
30roles, networks = provider.init()
31
32sources = []
33for idx, host in enumerate(roles["city"]):
34    delay = 5 * idx
35    print(f"{host.alias} <-> {delay}")
36    inbound = en.NetemOutConstraint(device="br0", options=f"delay {delay}ms")
37    outbound = en.NetemInConstraint(device="br0", options=f"delay {delay}ms")
38    sources.append(en.NetemInOutSource(host, constraints={inbound, outbound}))
39
40en.netem(sources)

HTB (Hierarchical Token Bucket) emulation#

HTB based emulation.

Classes:

AccurateNetemHTB()

NetemHTB but for expressing end-to-end latency.

HTBConstraint(device, delay, target[, rate, ...])

An HTB constraint.

HTBSource(host, constraints)

Model a host and all the htb constraints.

NetemHTB()

Add extra delay to outgoing packets based on their destination.

Functions:

netem_htb(htb_hosts[, chunk_size])

Helper function to enforce heterogeneous limitations on hosts.

class enoslib.service.emul.htb.AccurateNetemHTB#

NetemHTB but for expressing end-to-end latency.

End-to-End (one way )latency between two distributed processes is composed of:

  • the latency for a packet to go out of the first machine (e.g. IP stack latency + network card latency).

  • the latency for the packet to flight to the second machine (e.g. network, middle-boxes latency)

  • the latency for a packet to be transferred to the application on the second machine.

This service tries the compensated the extra latency added to outgoing packets by TC so that the End-to-End latency stick to the one specified by the user.

Internals hints:

At deploy time AccurateNetemHTB will estimate the delay introduced by the infrastructure and correct the wanted netem by this estimation. For each source and target in the wanted constraints it will send N ICMP probes and aggregate their RTT time. The current estimation is based on the mean RTT.

The AccurateNetemHTB is useful when the delays introduced by the infrastructure aren’t negligible in comparison to the wanted ones (e.g few ms in a LAN environment). Since correction is applied on each pair (source, destination), the service can cope with the heterogeneity of delays in the infrastructure.

Of course this service can’t do any miracle like trying to set delays less than the one existing on the infrastructure. Always double check, the observed network condition before drawing any conclusion.

Add extra delay to outgoing packets based on their destination.

It allows to setup complex network topology. For a much simpler way of applying constraints see Netem

The topology can be built by add constraints iteratively with add_constraints() or by passing a description as a dictionary using the from_dict() class method.

deploy(chunk_size: int = 100, **kwargs)#

Deploy the network emulation.

This is where the hard work is done:

  • estimate the current status of the network

  • correct the user defined constraints

  • enforce them

Parameters:
class enoslib.service.emul.htb.HTBConstraint(device: str, delay: str, target: str, rate: str = '10gbit', loss: str | None = None)#

An HTB constraint.

An HTBconstraint will be enforced by creating a filter and a class. The filtering stage will send the packet to the class (delay, rate and loss will be applied) based on the target ip.

The purpose of this class is to give you the commands to create the slice (class and filter) that will be added to the root qdisc.

Parameters:
  • target – the target ip Can be an ipv4 or an ipv6.

  • device – the device name where the qdisc will be added The one you get when listing the interfaces using iproute2.

  • delay – the delay (e.g 10ms) One way delay.

  • rate – the rate (e.g 10gbit) Bandwitdth of the link

  • loss – the loss (e.g “0.05” == “5%”) Chance for a packet to be lost

add_commands() List[str]#

Add the class-full qdisc at the root of the device.

commands(idx: int)#

Get the command for the current slice.

remove_commands() List[str]#

Remove everything.

class enoslib.service.emul.htb.HTBSource(host: ~enoslib.objects.Host, constraints: ~typing.Set[~enoslib.service.emul.htb.HTBConstraint] = <factory>)#

Model a host and all the htb constraints.

Args

host: Host where the constraint will be applied constraints: list of enoslib.service.netem.netem.HTBConstraint

Note

Consistency check (such as device names) between the host and the constraints are left to the application.

add_constraint(*args, **kwargs)#

Add a constraint.

*args and **kwargs are those from enoslib.service.netem.netem.HTBConstraint

add_constraints(constraints: Iterable[HTBConstraint]) HTBSource#

Merge constraints to existing ones.

In this context if two constraints are set on the same device with the same target will overwrite the original one. At the end we ensure that there’s only one constraint per device and target

Parameters:

constraints – Iterable of HTBConstraints

static equal(c1: HTBConstraint, c2: HTBConstraint) bool#

Encode the equality of two constraints in this context.

class enoslib.service.emul.htb.NetemHTB#

Add extra delay to outgoing packets based on their destination.

It allows to setup complex network topology. For a much simpler way of applying constraints see Netem

The topology can be built by add constraints iteratively with add_constraints() or by passing a description as a dictionary using the from_dict() class method.

add_constraints(src: Iterable[Host], dest: Iterable[Host], delay: str, rate: str, loss: float | None = None, networks: Iterable[Network] | None = None, symmetric: bool = False, *, symetric: bool | None = None) NetemHTB#

Add some constraints.

Parameters:
  • src – list of hosts on which the constraint will be applied

  • dest – list of hosts to which traffic will be limited

  • delay – the delay to apply as a string (e.g. 10ms)

  • rate – the rate to apply as a string (e.g. 1gbit)

  • loss – the percentage of loss (between 0 and 1)

  • networks – only consider these networks when applying the resources (default to all networks)

  • symmetric – True iff the symmetric rules should be also added.

Returns:

The current service with updated constraints. (This allows to chain the addition of constraints)

Examples

 1import logging
 2from pathlib import Path
 3
 4import enoslib as en
 5
 6en.init_logging(level=logging.INFO)
 7en.check()
 8
 9job_name = Path(__file__).name
10
11conf = (
12    en.G5kConf.from_settings(job_name=job_name, job_type=[])
13    .add_machine(roles=["paris"], cluster="paravance", nodes=1)
14    .add_machine(roles=["berlin"], cluster="paravance", nodes=1)
15    .add_machine(roles=["londres"], cluster="paravance", nodes=1)
16)
17provider = en.G5k(conf)
18roles, networks = provider.init()
19roles = en.sync_info(roles, networks)
20
21
22netem = en.NetemHTB()
23(
24    netem.add_constraints(
25        src=roles["paris"],
26        dest=roles["londres"],
27        delay="10ms",
28        rate="1gbit",
29        symmetric=True,
30    )
31    .add_constraints(
32        src=roles["paris"],
33        dest=roles["berlin"],
34        delay="20ms",
35        rate="1gbit",
36        symmetric=True,
37    )
38    .add_constraints(
39        src=roles["londres"],
40        dest=roles["berlin"],
41        delay="20ms",
42        rate="1gbit",
43        symmetric=True,
44    )
45)
46netem.deploy()
47netem.validate()
48# netem.destroy()
  • Using a secondary network from a list of constraints

 1import logging
 2from pathlib import Path
 3
 4import enoslib as en
 5
 6en.init_logging(level=logging.INFO)
 7en.check()
 8
 9CLUSTER = "paravance"
10SITE = en.g5k_api_utils.get_cluster_site(CLUSTER)
11
12job_name = Path(__file__).name
13
14private = en.G5kNetworkConf(type="kavlan", roles=["private"], site=SITE)
15
16conf = (
17    en.G5kConf.from_settings(
18        job_name=job_name, job_type=["deploy"], env_name="debian11-nfs"
19    )
20    .add_network_conf(private)
21    .add_machine(
22        roles=["paris"],
23        cluster=CLUSTER,
24        nodes=1,
25        secondary_networks=[private],
26    )
27    .add_machine(
28        roles=["londres"],
29        cluster=CLUSTER,
30        nodes=1,
31        secondary_networks=[private],
32    )
33    .add_machine(
34        roles=["berlin"],
35        cluster=CLUSTER,
36        nodes=1,
37        secondary_networks=[private],
38    )
39)
40
41provider = en.G5k(conf)
42roles, networks = provider.init()
43roles = en.sync_info(roles, networks)
44
45
46netem = en.NetemHTB()
47(
48    netem.add_constraints(
49        src=roles["paris"],
50        dest=roles["londres"],
51        delay="10ms",
52        rate="1gbit",
53        symmetric=True,
54        networks=networks["private"],
55    )
56    .add_constraints(
57        src=roles["paris"],
58        dest=roles["berlin"],
59        delay="20ms",
60        rate="1gbit",
61        symmetric=True,
62        networks=networks["private"],
63    )
64    .add_constraints(
65        src=roles["londres"],
66        dest=roles["berlin"],
67        delay="20ms",
68        rate="1gbit",
69        symmetric=True,
70        networks=networks["private"],
71    )
72)
73netem.deploy()
74netem.validate()
75# netem.destroy()
backup()#

(Not Implemented) Backup.

Feel free to share your ideas.

deploy(chunk_size: int = 100, **kwargs) List[HTBSource]#

(abstract) Deploy the service.

destroy(**kwargs)#

Reset the network constraints(latency, bandwidth …)

Remove any filter that have been applied to shape the traffic ever.

Careful: This remove every rule, including those not managed by this service.

classmethod from_dict(network_constraints: Dict, roles: Roles, networks: Networks) NetemHTB#

Build the service from a dictionary describing the network topology.

Parameters:

network_constraints – Dictionary of constraints. This must conform with SCHEMA.

Examples

 1import logging
 2from pathlib import Path
 3
 4import enoslib as en
 5
 6en.init_logging(level=logging.INFO)
 7en.check()
 8
 9job_name = Path(__file__).name
10
11conf = (
12    en.G5kConf.from_settings(job_name=job_name, job_type=[])
13    .add_machine(
14        roles=["paris", "vm"],
15        cluster="paravance",
16        nodes=1,
17    )
18    .add_machine(
19        roles=["berlin", "vm"],
20        cluster="paravance",
21        nodes=1,
22    )
23    .add_machine(
24        roles=["londres", "vm"],
25        cluster="paravance",
26        nodes=1,
27    )
28)
29provider = en.G5k(conf)
30roles, networks = provider.init()
31roles = en.sync_info(roles, networks)
32
33# Building the network constraints
34emulation_conf = {
35    "default_delay": "20ms",
36    "default_rate": "1gbit",
37    "except": [],
38    "constraints": [
39        {"src": "paris", "dst": "londres", "symmetric": True, "delay": "10ms"}
40    ],
41}
42
43logging.info(emulation_conf)
44
45netem = en.NetemHTB.from_dict(emulation_conf, roles, networks)
46netem.deploy()
47netem.validate()
48# netem.destroy()

Using a secondary network:

 1import logging
 2from pathlib import Path
 3
 4import enoslib as en
 5
 6en.init_logging(level=logging.INFO)
 7en.check()
 8
 9CLUSTER = "paravance"
10SITE = en.g5k_api_utils.get_cluster_site(CLUSTER)
11
12job_name = Path(__file__).name
13
14prod = en.G5kNetworkConf(type="prod", roles=["my_network"], site=SITE)
15private = en.G5kNetworkConf(type="kavlan", roles=["private"], site=SITE)
16
17conf = (
18    en.G5kConf.from_settings(
19        job_name=job_name, job_type=["deploy"], env_name="debian11-nfs"
20    )
21    .add_network_conf(prod)
22    .add_network_conf(private)
23    .add_machine(
24        roles=["paris"],
25        cluster=CLUSTER,
26        nodes=1,
27        primary_network=prod,
28        secondary_networks=[private],
29    )
30    .add_machine(
31        roles=["londres"],
32        cluster=CLUSTER,
33        nodes=1,
34        primary_network=prod,
35        secondary_networks=[private],
36    )
37    .add_machine(
38        roles=["berlin"],
39        cluster=CLUSTER,
40        nodes=1,
41        primary_network=prod,
42        secondary_networks=[private],
43    )
44)
45
46provider = en.G5k(conf)
47roles, networks = provider.init()
48roles = en.sync_info(roles, networks)
49
50# Building the network constraints
51emulation_conf = {
52    "default_delay": "20ms",
53    "default_rate": "1gbit",
54    "except": [],
55    "default_network": "private",
56    "constraints": [
57        {"src": "paris", "dst": "londres", "symmetric": True, "delay": "10ms"}
58    ],
59}
60
61logging.info(emulation_conf)
62
63netem = en.NetemHTB.from_dict(emulation_conf, roles, networks)
64netem.destroy()
65netem.deploy()
66netem.validate()
validate(*, networks: Iterable[Network] | None = None, output_dir: Path | str | None = None, **kwargs) Results#

Validate the network parameters(latency, bandwidth …)

Performs ping tests to validate the constraints set by deploy(). Reports are available in the putput directory. used by enos.

Parameters:
  • roles (dict) – role -> hosts mapping as returned by enoslib.infra.provider.Provider.init()

  • inventory_path (str) – path to an inventory

  • output_dir (str) – directory where validation files will be stored. Default to enoslib.constants.TMP_DIRNAME.

enoslib.service.emul.htb.netem_htb(htb_hosts: List[HTBSource], chunk_size: int = 100, **kwargs)#

Helper function to enforce heterogeneous limitations on hosts.

This function do the heavy lifting of building the qdisc tree for each node and add filters based on the ip destination of the packet. Ref: https://tldp.org/HOWTO/Traffic-Control-HOWTO/classful-qdiscs.html

This function is used internally by the enoslib.service.netem.netem.Netem service. Here, you must ensure that the various enoslib.service.netem.netem.HTBSource are consistent (e.g. device names.) with the host. Symmetric limitations can be achieved by adding both end of the communication to the list. The same host can appear multiple times in the list, all the constraints will be merged according to enoslib.service.emul.htb.HTBSource.add_constraints()

This method is optimized toward execution time: enforcing thousands of atomic constraints (= tc commands) shouldn’t be a problem. Commands are sent by batch and chunk_size controls the size of the batch.

Idempotency note: the existing qdiscs are removed before applying new ones. This must be safe in most of the cases to consider that this is a form of idempotency.

Parameters:
  • htb_hosts – list of constraints to apply.

  • chunk_size – size of the chunk to use

  • kwargs – keyword arguments passed to enoslib.api.run_ansible()

Examples

 1import logging
 2from itertools import islice, product
 3from pathlib import Path
 4
 5import enoslib as en
 6
 7en.init_logging(level=logging.INFO)
 8en.check()
 9
10job_name = Path(__file__).name
11
12
13conf = (
14    en.G5kConf.from_settings(job_name=job_name, job_type=[])
15    .add_network(
16        id="not_linked_to_any_machine",
17        type="slash_22",
18        roles=["my_subnet"],
19        site="rennes",
20    )
21    .add_machine(roles=["control"], cluster="paravance", nodes=10)
22)
23
24provider = en.G5k(conf)
25
26# Get actual resources
27roles, networks = provider.init()
28
29# distribute some virtual ips :)
30N = 100
31ips = networks["my_subnet"][0].free_ips
32for host in roles["control"]:
33    host.extra.update(ips=[str(ip) for ip in islice(ips, N)])
34with en.play_on(roles=roles, gather_facts=False) as p:
35    p.shell(
36        "(ip a | grep {{ item }}) || ip addr add {{ item }}/22 dev br0",
37        loop="{{ ips }}",
38    )
39
40# All virtual ips being set on the hosts
41# let's build the list of constraints
42htb_hosts = []
43humans = []
44for h_idx, (h1, h2) in enumerate(product(roles["control"], roles["control"])):
45    # need to account for h1 = h2 to set constraint on loopback device
46    htb = en.HTBSource(host=h1)
47    ips2 = h2.extra["ips"]
48    for ip_idx, ip2 in enumerate(ips2):
49        # this is the delay between one machine h1 and any of the virtual ip of h2
50        # since h1 and h2 will be swapped in another iteration, we'll also set
51        # the "symmetrical" at some point.
52        delay = 5 * ip_idx
53        humans.append(f"({h1.alias}) -->{delay}--> {ip2}({h2.alias}) ")
54        if h1 == h2:
55            # loopback
56            htb.add_constraint(delay=f"{delay}ms", device="lo", target=ip2)
57        else:
58            htb.add_constraint(delay=f"{delay}ms", device="br0", target=ip2)
59    htb_hosts.append(htb)
60
61en.netem_htb(htb_hosts)
62
63
64Path("htb.list").write_text("\n".join(humans))
65# you can check the constraint be issuing some:
66# ping -I <ip_source> <ip_dest>