aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--rumba/elements/experimentation.py16
-rw-r--r--rumba/elements/topology.py98
-rw-r--r--rumba/irm_backend.py8
-rw-r--r--rumba/prototypes/ouroboros.py35
-rw-r--r--rumba/testbeds/local.py4
-rw-r--r--rumba/testbeds/localnet.py5
-rw-r--r--rumba/utils.py14
7 files changed, 134 insertions, 46 deletions
diff --git a/rumba/elements/experimentation.py b/rumba/elements/experimentation.py
index 67fb2e3..0b53f11 100644
--- a/rumba/elements/experimentation.py
+++ b/rumba/elements/experimentation.py
@@ -84,6 +84,22 @@ class Testbed(object):
# testbed) have no interfaces, so applying a LinkQuality is a no-op.
supports_link_quality = True
+ # Whether rumba must build/install the prototype on the nodes. Testbeds
+ # that run on the host against already-installed binaries (local, localnet,
+ # nstb) set this False; remote testbeds (emulab, jfed, ...) leave it True.
+ needs_install = True
+
+ # Whether the prototype runs as a single instance in one namespace/host:
+ # one IRMd, driven as local subprocesses / the in-process IRM backend.
+ # Testbeds that give each node its own namespace (nstb) or its own
+ # container/host (docker, remote) set this False.
+ single_irmd = False
+
+ # Whether swap_out destroys the execution context (e.g. network + mount
+ # namespaces) the prototype runs in, so the prototype must be terminated
+ # *before* swap_out while it is still reachable. nstb sets this True.
+ terminate_before_swapout = False
+
def __init__(self,
exp_name,
username,
diff --git a/rumba/elements/topology.py b/rumba/elements/topology.py
index f6ad32a..f69ae29 100644
--- a/rumba/elements/topology.py
+++ b/rumba/elements/topology.py
@@ -745,44 +745,76 @@ class LinkQuality(object):
def corrupt(self):
return self._corrupt
+ @staticmethod
+ def _netem_target(ipcp):
+ """Return (netem_dev, on_host).
+
+ netem goes on the bridge-side interface (host_ifname) when there is
+ one, else on the IPCP interface itself. *on_host* is True when that
+ interface lives on the host rather than in the IPCP's namespace, in
+ which case the command must run through the node's host_executor.
+ """
+ host_ifname = getattr(ipcp, 'host_ifname', None)
+ if host_ifname and host_ifname != ipcp.ifname:
+ return host_ifname, True
+ return ipcp.ifname, False
+
+ @staticmethod
+ def _run_host(ipcp, cmds, check=True):
+ """Run host-side (bridge/netem) commands.
+
+ Uses the node's host_executor when set (namespaced testbeds, where
+ the bridge-side interface is on the host); otherwise falls back to
+ the node executor, which for the non-namespaced testbeds already runs
+ on the host -- so behaviour is unchanged there.
+ """
+ host_executor = getattr(ipcp.node, 'host_executor', None)
+ for cmd in cmds:
+ if host_executor is not None:
+ host_executor.execute_command(ipcp.node, cmd, True,
+ check=check)
+ else:
+ ipcp.node.execute_command(cmd, as_root=True, check=check)
+
def build_commands(self, ipcp):
- netem_cmd = []
- cmds = []
-
- # Rate limiting (HTB) goes on the IPCP interface so a blocking
- # send back-pressures the source. All netem impairments go on
- # the bridge-side interface (host_ifname), where frames are
- # already orphaned -- netem's skb_orphan() there no longer
- # defeats that back-pressure. Testbeds without a bridge (e.g.
- # UDP veths) have no host_ifname and put both on the IPCP
- # interface. apply_to_ipcp() clears any prior qdiscs first, so
- # these are plain adds.
- netem_dev = getattr(ipcp, 'host_ifname', None) or ipcp.ifname
+ """Split tc commands into (node_cmds, host_cmds).
+
+ Rate limiting (HTB) goes on the IPCP interface so a blocking send
+ back-pressures the source, and always runs in the IPCP's namespace.
+ netem impairments go on the bridge-side interface (host_ifname) where
+ frames are already orphaned -- netem's skb_orphan() there no longer
+ defeats that back-pressure -- and run on the host for namespaced
+ testbeds. Testbeds without a bridge (e.g. UDP veths) have no
+ host_ifname and chain netem under HTB on the IPCP interface.
+ apply_to_ipcp() clears any prior qdiscs first, so these are plain adds.
+ """
+ node_cmds = []
+ host_cmds = []
+ netem_dev, on_host = self._netem_target(ipcp)
if self.rate:
- cmds.append(
+ node_cmds.append(
"tc qdisc add dev %s root handle 1: htb default 1"
% ipcp.ifname)
- cmds.append(
+ node_cmds.append(
"tc class add dev %s parent 1: classid 1:1 "
"htb rate %imbit" % (ipcp.ifname, self.rate))
netem_parts = [self.delay, self.loss,
self.duplicate, self.reorder, self.corrupt]
if any(netem_parts):
- if self.rate and netem_dev == ipcp.ifname:
+ if self.rate and not on_host:
qref = "parent 1:1" # no bridge: chain under HTB
else:
qref = "root"
- netem_cmd.append(
- "tc qdisc add dev %s %s netem limit 65536"
- % (netem_dev, qref))
+ netem_cmd = ["tc qdisc add dev %s %s netem limit 65536"
+ % (netem_dev, qref)]
for part in netem_parts:
if part:
netem_cmd.append(part.build_command())
- cmds.append(" ".join(netem_cmd))
+ (host_cmds if on_host else node_cmds).append(" ".join(netem_cmd))
- return cmds
+ return node_cmds, host_cmds
def apply_to_ipcp(self, ipcp):
"""Apply this link quality configuration to a single *ipcp*."""
@@ -803,8 +835,11 @@ class LinkQuality(object):
# Clear any prior qdiscs first (best-effort, both devices) so
# the adds in build_commands never hit an existing root qdisc.
self.deactivate_ipcp(ipcp)
- ipcp.node.execute_commands(
- self.build_commands(ipcp), as_root=True)
+ node_cmds, host_cmds = self.build_commands(ipcp)
+ if node_cmds:
+ ipcp.node.execute_commands(node_cmds, as_root=True)
+ if host_cmds:
+ self._run_host(ipcp, host_cmds)
ipcp._netem_active = True
def deactivate_ipcp(self, ipcp):
@@ -825,16 +860,16 @@ class LinkQuality(object):
"Could not remove LinkQuality from IPCP because "
"the interface name is None")
return
- # Best-effort teardown: HTB lives on the IPCP interface, netem on
- # the bridge-side interface; either may be absent, so do not check.
+ # Best-effort teardown: HTB lives on the IPCP interface (node ns),
+ # netem on the bridge-side interface (host for namespaced testbeds);
+ # either may be absent, so do not check.
ipcp.node.execute_command(
"tc qdisc del dev %s root" % ipcp.ifname,
as_root=True, check=False)
- host_ifname = getattr(ipcp, 'host_ifname', None)
- if host_ifname and host_ifname != ipcp.ifname:
- ipcp.node.execute_command(
- "tc qdisc del dev %s root" % host_ifname,
- as_root=True, check=False)
+ netem_dev, on_host = self._netem_target(ipcp)
+ if on_host:
+ self._run_host(
+ ipcp, ["tc qdisc del dev %s root" % netem_dev], check=False)
def apply(self, layer):
"""Apply this link quality to all IPCPs in *layer*."""
@@ -916,6 +951,11 @@ class Node(object):
self.policies[layer] = policies.get(layer, Policy(layer, self))
self.executor = None # set by testbed on swap_in
+ # Optional executor for host-side link shaping. When a testbed runs
+ # nodes in their own namespace (nstb), the bridge-side (host_ifname)
+ # interface lives on the host, not in the node's namespace, so netem
+ # must be applied there. None means "same namespace as executor".
+ self.host_executor = None
self.startup_command = None # set by prototype
self._validate()
diff --git a/rumba/irm_backend.py b/rumba/irm_backend.py
index 2550884..07a51a7 100644
--- a/rumba/irm_backend.py
+++ b/rumba/irm_backend.py
@@ -184,13 +184,21 @@ class IrmCLI(IrmBackend):
node.execute_command(cmd, time_out=None)
+ def _ensure_name(self, node, name):
+ # Allocating a flow to a name requires that name to exist in the
+ # local IRMd registry; with one IRMd per node a remote destination
+ # is unknown until created. No-op when it already exists.
+ node.execute_command("irm n c %s || true" % name, time_out=None)
+
def enroll_ipcp(self, node, name, dst_name, autobind=True):
+ self._ensure_name(node, dst_name)
cmd = "irm i e n %s dst %s" % (name, dst_name)
if autobind:
cmd += " autobind"
node.execute_command(cmd, time_out=None)
def connect_ipcp(self, node, name, dst_name):
+ self._ensure_name(node, dst_name)
cmd = "irm i conn n %s dst %s" % (name, dst_name)
node.execute_command(cmd, time_out=None)
diff --git a/rumba/prototypes/ouroboros.py b/rumba/prototypes/ouroboros.py
index 1c2e007..931b2e0 100644
--- a/rumba/prototypes/ouroboros.py
+++ b/rumba/prototypes/ouroboros.py
@@ -146,9 +146,10 @@ class Experiment(mod.Experiment):
self.set_startup_command("irmd")
self.metrics_python_version = "python3.9"
- # Create IRM backend
- if isinstance(testbed, (local.Testbed,
- localnet.LocalNetworkTestbed)):
+ # Create IRM backend. IrmPython drives a single in-process IRMd, so
+ # it only makes sense for single-IRMd testbeds; everything else (per
+ # node/namespace/host IRMds) is driven through the CLI backend.
+ if testbed.single_irmd:
try:
self.irm = IrmPython()
logger.info("Using pyouroboros IRM backend")
@@ -192,7 +193,7 @@ class Experiment(mod.Experiment):
except ImportError:
pass
- if self._is_local:
+ if self.testbed.single_irmd:
subprocess.check_call('sudo -v'.split())
popen = subprocess.Popen(["sudo", "irmd"])
self.irmd = Process(None, popen.pid, "irmd", popen=popen)
@@ -201,8 +202,16 @@ class Experiment(mod.Experiment):
time.sleep(2)
else:
for node in self.nodes:
- node.execute_command("sudo nohup irmd > /dev/null &",
- time_out=None)
+ # Redirect BOTH stdout and stderr: irmd does not daemonize, so
+ # a leaked stderr fd would keep an output-capturing executor
+ # (e.g. the nsenter executor) blocked forever waiting for EOF.
+ # --stdout: without it irmd logs to syslog and the file
+ # stays empty; with it every message is fflush()ed, so the
+ # log (in the node's private tmpfs) can be read live.
+ node.execute_command(
+ "sudo nohup irmd --stdout "
+ "> /var/run/ouroboros/irmd.log 2>&1 &",
+ time_out=None)
time.sleep(1)
def _install_packages_and_execute_cmds(self, packages, cmds, nodes=None):
@@ -226,14 +235,8 @@ class Experiment(mod.Experiment):
m_processing.call_in_parallel(names, args, executors)
- @property
- def _is_local(self):
- """True if the testbed runs locally (single IRMd)."""
- return isinstance(self.testbed,
- (local.Testbed, localnet.LocalNetworkTestbed))
-
def install_ouroboros(self):
- if self._is_local:
+ if not self.testbed.needs_install:
return
packages = ["cmake", "protobuf-c-compiler", "git", "libfuse-dev",
@@ -250,7 +253,7 @@ class Experiment(mod.Experiment):
self._install_packages_and_execute_cmds(packages, cmds)
def update_ouroboros(self, branch):
- if self._is_local:
+ if not self.testbed.needs_install:
return
if branch is not None:
@@ -284,7 +287,7 @@ class Experiment(mod.Experiment):
self._install_packages_and_execute_cmds(packages, [])
def install_ouroboros_python_exporter(self):
- if self._is_local:
+ if not self.testbed.needs_install:
return
if self.influxdb is None:
@@ -510,7 +513,7 @@ class Experiment(mod.Experiment):
cmds.append('killall -15 irmd')
logger.info("Killing Ouroboros...")
- if self._is_local:
+ if self.testbed.single_irmd:
cmds = list(map(lambda c: "sudo %s" % (c,), cmds))
for cmd in cmds:
subprocess.call(cmd.split())
diff --git a/rumba/testbeds/local.py b/rumba/testbeds/local.py
index ad0db9f..968c365 100644
--- a/rumba/testbeds/local.py
+++ b/rumba/testbeds/local.py
@@ -43,6 +43,10 @@ class Testbed(mod.Testbed):
# (rate/delay/loss) cannot be applied -- treat apply as a no-op.
supports_link_quality = False
+ # Runs against host-installed binaries as a single local IRMd.
+ needs_install = False
+ single_irmd = True
+
def __init__(self, exp_name='foo', username='bar',
proj_name="rumba", password=""):
"""
diff --git a/rumba/testbeds/localnet.py b/rumba/testbeds/localnet.py
index 4278367..28ca080 100644
--- a/rumba/testbeds/localnet.py
+++ b/rumba/testbeds/localnet.py
@@ -52,6 +52,11 @@ class LocalNetworkTestbed(mod.Testbed):
netem support on the veth interfaces.
"""
+ # Runs against host-installed binaries as a single local IRMd, in the
+ # default namespace (all veths/bridges live on the host).
+ needs_install = False
+ single_irmd = True
+
def __init__(self, exp_name='localnet', username='',
proj_name='rumba', password=''):
"""
diff --git a/rumba/utils.py b/rumba/utils.py
index 324d3bf..6742f4d 100644
--- a/rumba/utils.py
+++ b/rumba/utils.py
@@ -211,10 +211,22 @@ class ExperimentManager(object):
# Swap out
if do_swap_out:
+ testbed = self.experiment.testbed
# Kill prototype in case of local testbed
- if isinstance(self.experiment.testbed, local.Testbed):
+ if isinstance(testbed, local.Testbed):
self.experiment.terminate_prototype()
else:
+ # Testbeds whose swap_out destroys the execution context
+ # (e.g. nstb's per-node net+mount namespaces) must reap the
+ # prototype first, while it is still reachable.
+ if testbed.terminate_before_swapout:
+ try:
+ self.experiment.terminate_prototype()
+ except Exception as e:
+ logger.warning(
+ 'Could not terminate prototype before swap '
+ 'out. %s%s', type(e).__name__,
+ ": " + str(e) if str(e) != "" else "")
self.experiment.swap_out()
if exc_val is not None:
logger.error('Something went wrong. '