mininet-sample/core/topo.py
2020-07-13 09:55:16 +02:00

533 lines
19 KiB
Python

from .parameter import Parameter
import logging
import math
class NetemAt(object):
"""
Class representing a netem command to be run after some time
"""
def __init__(self, at, cmd):
self.at = at
self.cmd = cmd
self.delta = 0
def __str__(self):
return "netem at {} ({}) will be {}".format(self.at, self.delta, self.cmd)
class LinkCharacteristics(object):
"""
Network characteristics associated to a link
Attributes:
id the identifier of the link
delay the one-way delay introduced by the link in ms
queue_size the size of the link buffer, in packets
bandwidth the bandwidth of the link in Mbps
loss the random loss rate in percentage
queuing_delay the maximum time that a packet can stay in the link buffer (computed over queue_size)
netem_at list of NetemAt instances applicable to the link
backup integer indicating if this link is a backup one or not (useful for MPTCP)
"""
def __init__(self, id, delay, queue_size, bandwidth, loss, backup=False):
self.id = id
self.delay = delay
self.queue_size = queue_size
self.bandwidth = bandwidth
self.loss = loss
self.queuing_delay = str(self.extract_queuing_delay(queue_size, bandwidth, delay))
self.netem_at = []
self.backup = backup
def bandwidth_delay_product_divided_by_mtu(self):
"""
Get the bandwidth-delay product in terms of packets (hence, dividing by the MTU)
"""
rtt = 2 * float(self.delay)
""" Since bandwidth is in Mbps and rtt in ms """
bandwidth_delay_product = (float(self.bandwidth) * 125000.0) * (rtt / 1000.0)
return int(math.ceil(bandwidth_delay_product * 1.0 / 1500.0))
def buffer_size(self):
"""
Return the buffer size in bytes
"""
return (1500.0 * self.bandwidth_delay_product_divided_by_mtu()) + \
(float(self.bandwidth) * 1000.0 * float(self.queuing_delay) / 8)
def extract_queuing_delay(self, queue_size, bandwidth, delay, mtu=1500):
queuing_delay = (int(queue_size) * int(mtu) * 8.0 * 1000.0) / \
(float(bandwidth) * 1024 * 1024)
return max(int(queuing_delay), 1)
def add_netem_at(self, n):
if len(self.netem_at) == 0:
n.delta = n.at
self.netem_at.append(n)
else:
if n.at > self.netem_at[-1].at:
n.delta = n.at - self.netem_at[-1].at
self.netem_at.append(n)
else:
logging.error("{}: not taken into account because not specified in order in the topo param file".format(n))
def build_bandwidth_cmd(self, ifname):
return "tc qdisc del {} root ; tc qdisc add dev {} root handle 5:0 tbf rate {}mbit burst 15000 limit {}".format(
ifname, ifname, self.bandwidth, self.buffer_size())
def build_changing_bandwidth_cmd(self, ifname):
return "&&".join(
["sleep {} && {} ".format(
n.delta, self.build_bandwidth_cmd(ifname)) for n in self.netem_at]
+ ["true &"]
)
def build_netem_cmd(self, ifname, cmd):
return "tc qdisc del dev {} root ; tc qdisc add dev {} root handle 10: netem {} delay {}ms limit 50000".format(
ifname, ifname, cmd, self.delay)
def build_changing_netem_cmd(self, ifname):
return "&&".join(
["sleep {} && {} ".format(
n.delta, self.build_netem_cmd(ifname, n.cmd)) for n in self.netem_at]
+ ["true &"]
)
def clean_policing_cmd(self, ifname):
return "tc qdisc del dev {} ingress ".format(ifname)
def build_policing_cmd(self, ifname):
""" For some reason, the delete can break everything """
return "tc qdisc add dev {} handle ffff: ingress ; \
tc filter add dev {} parent ffff: u32 match u32 0 0 police rate {}mbit burst {} drop".format(
ifname, ifname, self.bandwidth, int(self.buffer_size()) * 1.2)
def build_changing_policing_cmd(self, ifname):
return "&&".join(
["sleep {} && {} ".format(
n.delta, self.build_policing_cmd(ifname)) for n in self.netem_at]
+ ["true &"]
)
def as_dict(self):
"""
Notably used by BottleneckLink
"""
return {
"bw": float(self.bandwidth),
"delay": "{}ms".format(self.delay),
"loss": float(self.loss),
"max_queue_size": int(self.queue_size)
}
def __str__(self):
return """
Link id: {}
Delay: {}
Queue Size: {}
Bandwidth: {}
Loss: {}
Backup: {}
""".format(self.id, self.delay, self.queue_size, self.bandwidth, self.loss, self.backup) + \
"".join(["\t {} \n".format(n) for n in self.netem_at])
class TopoParameter(Parameter):
LEFT_SUBNET = "leftSubnet"
RIGHT_SUBNET = "rightSubnet"
NETEM_AT = "netem_at_"
CHANGE_NETEM = "changeNetem"
DEFAULT_PARAMETERS = {
LEFT_SUBNET: "10.1.",
RIGHT_SUBNET: "10.2.",
CHANGE_NETEM: "false",
}
def __init__(self, parameter_filename):
Parameter.__init__(self, parameter_filename)
self.default_parameters.update(TopoParameter.DEFAULT_PARAMETERS)
self.link_characteristics = []
self.load_link_characteristics()
self.load_netem_at()
logging.info(self)
def load_netem_at(self):
if not self.get(TopoParameter.CHANGE_NETEM) == "yes":
return
for k in sorted(self.parameters):
if k.startswith(TopoParameter.NETEM_AT):
link_id = int(k[len(TopoParameter.NETEM_AT):])
val = self.parameters[k]
if not isinstance(val, list):
tmp = val
val = []
val.append(tmp)
self.load_netem_at_list(link_id, val)
def load_netem_at_list(self, link_id, nlist):
for n in nlist:
try:
at, cmd = n.split(",")
na = NetemAt(float(at), cmd)
if link_id < len(self.link_characteristics):
self.link_characteristics[id].add_netem_at(na)
else:
logging.error("Unable to set netem for link {}; only have {} links".format(
link_id, len(self.link_characteristics)))
except ValueError as e:
logging.error("Unable to set netem for link {}: {}".format(link_id, n))
logging.info(self.link_characteristics[link_id].netem_at)
def load_link_characteristics(self):
"""
CAUTION: the path_i in config file is not taken into account. Hence place them in
increasing order in the topo parameter file!
"""
i = 0
for k in sorted(self.parameters):
# TODO FIXME rewrite this function
if k.startswith("path"):
tab = self.parameters[k].split(",")
bup = False
loss = "0.0"
if len(tab) == 5:
loss = tab[3]
bup = tab[4].lower() == 'true'
if len(tab) == 4:
try:
loss = float(tab[3])
loss = tab[3]
except ValueError:
bup = tab[3].lower() == 'true'
if len(tab) == 3 or len(tab) == 4 or len(tab) == 5:
path = LinkCharacteristics(i, tab[0],
tab[1], tab[2], loss, bup)
self.link_characteristics.append(path)
i = i + 1
else:
logging.warning("Ignored path {}".format(self.parameters[k]))
def __str__(self):
s = "{}".format(super(TopoParameter, self).__str__())
s += "".join(["{}".format(lc) for lc in self.link_characteristics])
return s
class BottleneckLink(object):
"""
Representation of a bottleneck link having limited bandwidth, a buffer,
experiencing propagation delay and introducing packet losses.
A bottleneck link has the following actual representation:
bs0 -- bs1 -- bs2 -- bs3
Where bs0 (resp. bs3) is the left (resp. right) side of the link, and having
TC commands for the packet flow s0 -> s3 as follows:
- Policing command to implement buffer on ingress of bs1 from bs0
- Shaping command to implement bandwidth on egress of bs1 to bs2
- Netem command to implement delay and loss on egress of bs2 to bs3
"""
BOTTLENECK_SWITCH_NAME_PREFIX = "bs"
def __init__(self, topo_builder, topo, link_characteristics):
self.link_characteristics = link_characteristics
self.topo = topo
self.bs0 = topo_builder.add_switch("{}_{}_0".format(
BottleneckLink.BOTTLENECK_SWITCH_NAME_PREFIX, self.link_characteristics.id))
self.bs1 = topo_builder.add_switch("{}_{}_1".format(
BottleneckLink.BOTTLENECK_SWITCH_NAME_PREFIX, self.link_characteristics.id))
self.bs2 = topo_builder.add_switch("{}_{}_2".format(
BottleneckLink.BOTTLENECK_SWITCH_NAME_PREFIX, self.link_characteristics.id))
self.bs3 = topo_builder.add_switch("{}_{}_3".format(
BottleneckLink.BOTTLENECK_SWITCH_NAME_PREFIX, self.link_characteristics.id))
topo_builder.add_link(self.bs0, self.bs1)
topo_builder.add_link(self.bs1, self.bs2)
topo_builder.add_link(self.bs2, self.bs3)
def configure_bottleneck(self):
bs1_interface_names = self.topo.get_interface_names(self.bs1)
bs2_interface_names = self.topo.get_interface_names(self.bs2)
# Flow bs0 -> bs3
# Only once
clean_policing_cmd = self.link_characteristics.clean_policing_cmd(bs1_interface_names[0])
logging.info(clean_policing_cmd)
self.topo.command_to(self.bs1, clean_policing_cmd)
policing_cmd = self.link_characteristics.build_policing_cmd(bs1_interface_names[0])
logging.info(policing_cmd)
self.topo.command_to(self.bs1, policing_cmd)
shaping_cmd = self.link_characteristics.build_bandwidth_cmd(bs1_interface_names[-1])
logging.info(shaping_cmd)
self.topo.command_to(self.bs1, shaping_cmd)
netem_cmd = self.link_characteristics.build_netem_cmd(bs2_interface_names[-1],
"loss {}".format(self.link_characteristics.loss))
logging.info(netem_cmd)
self.topo.command_to(self.bs2, netem_cmd)
# Flow bs3 -> bs0
policing_cmd = self.link_characteristics.build_policing_cmd(bs2_interface_names[-1])
logging.info(policing_cmd)
self.topo.command_to(self.bs2, policing_cmd)
shaping_cmd = self.link_characteristics.build_bandwidth_cmd(bs2_interface_names[0])
logging.info(shaping_cmd)
self.topo.command_to(self.bs2, shaping_cmd)
netem_cmd = self.link_characteristics.build_netem_cmd(bs1_interface_names[0],
"loss {}".format(self.link_characteristics.loss))
logging.info(netem_cmd)
self.topo.command_to(self.bs1, netem_cmd)
def configure_changing_bottleneck(self):
bs1_interface_names = self.topo.get_interface_names(self.bs1)
bs2_interface_names = self.topo.get_interface_names(self.bs2)
# Flow bs0 -> bs3
policing_cmd = self.link_characteristics.build_changing_policing_cmd(bs1_interface_names[0])
logging.info(policing_cmd)
self.topo.command_to(self.bs1, policing_cmd)
shaping_cmd = self.link_characteristics.build_changing_bandwidth_cmd(bs1_interface_names[-1])
logging.info(shaping_cmd)
self.topo.command_to(self.bs1, shaping_cmd)
netem_cmd = self.link_characteristics.build_changing_netem_cmd(bs2_interface_names[-1])
logging.info(netem_cmd)
self.topo.command_to(self.bs2, netem_cmd)
# Flow bs3 -> bs0
policing_cmd = self.link_characteristics.build_changing_policing_cmd(bs2_interface_names[-1])
logging.info(policing_cmd)
self.topo.command_to(self.bs2, policing_cmd)
shaping_cmd = self.link_characteristics.build_changing_bandwidth_cmd(bs2_interface_names[0])
logging.info(shaping_cmd)
self.topo.command_to(self.bs2, shaping_cmd)
netem_cmd = self.link_characteristics.build_changing_netem_cmd(bs1_interface_names[0])
logging.info(netem_cmd)
self.topo.command_to(self.bs1, netem_cmd)
def get_left(self):
return self.bs0
def get_right(self):
return self.bs3
class Topo(object):
"""
Base class to instantiate a topology.
The network topology has always the following elements:
- a (set of) client(s)
- a (set of) router(s)
- a (set of) server(s)
- a set of bottleneck links
This class is not instantiable as it. You must define a child class with the
`NAME` attribute.
Attributes:
topo_builder instance of TopoBuilder
topo_parameter instance of TopoParameter
change_netem boolean indicating if netem must be changed
log_file file descriptor logging commands relative to the topo
"""
MININET_BUILDER = "mininet"
TOPO_ATTR = "topoType"
SWITCH_NAME_PREFIX = "s"
ROUTER_NAME_PREFIX = "r"
CLIENT_NAME = "Client"
SERVER_NAME = "Server"
ROUTER_NAME = "Router"
CMD_LOG_FILENAME = "command.log"
def __init__(self, topo_builder, topo_parameter):
self.topo_builder = topo_builder
self.topo_parameter = topo_parameter
self.change_netem = topo_parameter.get(TopoParameter.CHANGE_NETEM).lower() == "yes"
self.log_file = open(Topo.CMD_LOG_FILENAME, 'w')
self.clients = []
self.routers = []
self.servers = []
self.bottleneck_links = []
def get_link_characteristics(self):
return self.topo_parameter.link_characteristics
def command_to(self, who, cmd):
self.log_file.write("{} : {}\n".format(who, cmd))
return self.topo_builder.command_to(who, cmd)
def command_global(self, cmd):
"""
mainly use for not namespace sysctl.
"""
self.log_file.write("Global : {}\n".format(cmd))
return self.topo_builder.command_global(cmd)
def client_count(self):
return len(self.clients)
def get_client(self, index):
return self.clients[index]
def get_router(self, index):
return self.routers[index]
def get_server(self, index):
return self.servers[index]
def router_count(self):
return len(self.routers)
def server_count(self):
return len(self.servers)
def bottleneck_link_count(self):
return len(self.bottleneck_links)
def get_host(self, who):
return self.topo_builder.get_host(who)
def get_interface_names(self, who):
return self.topo_builder.get_interface_names(who)
def add_host(self, host):
return self.topo_builder.add_host(host)
def add_switch(self, switch):
return self.topo_builder.add_switch(switch)
def add_link(self, from_a, to_b, **kwargs):
self.topo_builder.add_link(from_a, to_b, **kwargs)
def add_bottleneck_link(self, from_a, to_b, link_characteristics=None, bottleneck_link=None):
"""
If bottleneck_link is None, create a bottleneck link with parameters kwargs,
otherwise just connect it to from_a and to_b and returns the bottleneck_link
"""
if bottleneck_link is None:
bottleneck_link = BottleneckLink(self.topo_builder, self, link_characteristics)
self.bottleneck_links.append(bottleneck_link)
self.topo_builder.add_link(from_a, bottleneck_link.get_left())
self.topo_builder.add_link(bottleneck_link.get_right(), to_b)
return bottleneck_link
def get_cli(self):
self.topo_builder.get_cli()
def start_network(self):
self.topo_builder.start_network()
for b in self.bottleneck_links:
b.configure_bottleneck()
def close_log_file(self):
self.log_file.close()
def stop_network(self):
self.topo_builder.stop_network()
class TopoConfig(object):
"""
Base class to instantiate a topology.
This class is not instantiable as it. You must define a child class with the
`NAME` attribute.
"""
def __init__(self, topo, param):
self.topo = topo
self.param = param
def configure_network(self):
logging.debug("Configure network in TopoConfig")
self.configure_interfaces()
self.configure_routing()
def disable_tso(self):
"""
Disable TSO on all interfaces
"""
for node in self.topo.topo_builder.net:
for intf in self.topo.get_interface_names(node):
logging.info("Disable TSO on interface {}".format(intf))
cmd = "ethtool -K {} tso off".format(intf)
logging.info(cmd)
self.topo.command_to(node, cmd)
def run_netem_at(self):
"""
Prepare netem commands to be run after some delay
"""
if not self.topo.change_netem:
# Just rely on defaults of TCLink
logging.info("No need to change netem")
return
logging.info("Will change netem config on the fly")
for b in self.topo.bottleneck_links:
b.configure_changing_bottleneck()
def getMidLeftName(self, i):
"get Middle link, left box name"
pass
def getMidRightName(self, i):
pass
def configure_interfaces(self):
"""
Function to override to configure the interfaces of the topology
"""
pass
def configure_routing(self):
"""
Function to override to configure the routing of the topology
"""
pass
def client_interface_count(self):
"""
Return the number of client's interfaces
"""
raise NotImplementedError()
def get_client_interface(self, index):
"""
Return the client's interface with index `index`
"""
raise NotImplementedError()
def get_router_interface_to_switch(self, index):
"""
Return the router's interface to switch with index `index`
"""
raise NotImplementedError()
def interface_backup_command(self, interface_name):
return "ip link set dev {} multipath backup ".format(
interface_name)
def interface_up_command(self, interface_name, ip, subnet):
return "ifconfig {} {} netmask {}".format(interface_name, ip, subnet)
def add_table_route_command(self, from_ip, id):
return "ip rule add from {} table {}".format(from_ip, id + 1)
def add_link_scope_route_command(self, network, interface_name, id):
return "ip route add {} dev {} scope link table {}".format(
network, interface_name, id + 1)
def add_table_default_route_command(self, via, id):
return "ip route add default via {} table {}".format(via, id + 1)
def add_global_default_route_command(self, via, interface_name):
return "ip route add default scope global nexthop via {} dev {}".format(via, interface_name)
def arp_command(self, ip, mac):
return "arp -s {} {}".format(ip, mac)
def add_simple_default_route_command(self, via):
return "ip route add default via {}".format(via)