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 forNetem
service and netem function.
tc
tool available (installiproute2
package on debian based distribution)
Netem emulation#
Classes:
|
Set homogeneous network constraints on some hosts |
|
A Constraint on the ingress part of a device. |
|
Model a host and the constraints on its network devices. |
|
A Constraint on the egress part of a device. |
Functions:
|
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:
NetemHTB but for expressing end-to-end latency. |
|
|
An HTB constraint. |
|
Model a host and all the htb constraints. |
|
Add extra delay to outgoing packets based on their destination. |
Functions:
|
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 thefrom_dict()
class method.
- 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 thefrom_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.
- 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 variousenoslib.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 toenoslib.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>