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>