Network Emulation#

This tutorial illustrates how network constraints can be enforced using EnOSlib. Another resources can be found in the Netem emulation.

Setting up homogeneous constraints#

When all your nodes share the same network limitations you can use the Netem service.

 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}")

Setting up heterogeneous constraints#

You can use the HTBNetem service for this purpose. The example is based on the G5K provider, but can be adapted to another one if desired.

  • Build from a dictionary:

     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()
    
  • Build a list of constraints iteratively

     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

     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()
    
  • 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()
    

Working at the network device level#

If you know the device on which limitations will be applied you can use the functions netem or netem_htb.

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