Experiment Model Reference
In Merge, the user defines experiment models in the Python language, making use
mergexp package. At a high level, an experiment model begins by
creating a network topology object, creating experiment nodes, defining how the
nodes are connected by network links, and finally, executing the model.
For example, consider a simple experiment with two nodes,
B, which are
directly connected via a network link. The Python model could be written as:
from mergexp import * # create the network topology object net = Network('example network') # Create nodes and add them to the network a = net.node('A') b = net.node('B') # Connect A and B link = net.connect([a, b]) # Set IP addresses for A and B link[a].socket.addrs = ip4('10.0.0.1/24') link[b].socket.addrs = ip4('10.0.0.2/24') # Execute the topology experiment(net)
In real-world scenarios, the user will want to create more complex topologies, and control some of the properties of the experiment nodes and links. These are called constraints in Merge. The available constraints for each object type are detailed below.
The network object represents the experiment model’s topology.
A network topology is created with the
Network() constructor. The constructor
takes a string, which defines the name of the topology, and an optional set of
def Network(name, *constraints):
|name||string||user description of the model|
Network objects can use the following constraints:
|routing||static||automatically configure static routing on the experiment nodes|
|addressing||ipv4||automatically assign IPv4 addresses to experiment nodes|
|experimentnetresolution||True,False||whether Merge will resolve hostnames to experiment network addresses. If set to False, node names will resolve to infranet IP addresses instead (default: True)|
TipWhen using automatic routing or addressing, specific node interfaces can be excluded by setting a constraint of
Linkobject. The automatic modes will only consider links of layer 3 or higher.
# Define a network with automatic assignment of static routes and # ipv4 addresses to nodes net = Network('my topology', routing==static, addressing==ipv4)
# Define a network in which node names do *not* resolve to experiment # network addresses, instead resolving to infranet addresses net = Network('my topology', experimentnetresolution==False) n = net.node("gw-router", proc.cores>=4, metal==True, memory.capacity>=gb(8))
Typically, the short name
gw-router would resolve to the first experiment IP address allocated for the node.
If set as above, the short name
gw-router will not resolve to any experiment IP addresses, and
will instead resolve to the infranet IP address of the node.
A new node is created with the
def Node(name, *constraints):
|name||string||hostname of the experiment node|
If a node in a model needs to meet some criteria, the
Node() constructor can
be supplied with optional constraints that will be used to control which
physical machine in the facility will be selected. Constraints are specified as
the constraint name, an operator (<=, ==, >=, etc), and a value.
|image||string||specify the OS image to boot on the node (default: |
|metal||bool||node should be not be virtualized (True) (default: False)|
|proc.cores||int||has the specified number of processor cores (default: 1)|
|memory.capacity||int||bytes||require the memory size (default: 512 MB)|
net = Network("example topology") n = net.node("gw-router", proc.cores>=4, metal==True, memory.capacity>=gb(8))
Creates a node with the hostname
gw-router which has at least 4 processor cores, runs on bare metal (not virtualized), and has at least 8GB of main memory.
Supported Operating Systems
image constraint can specify one of the following values:
net = Network("example topology") n = net.node("gw-router", image == "2004")
Creates a node with the hostname
gw-router which runs the Ubuntu 20.04 operating system.
Nodes can host services that are accessible from outside the experiment. When a node ingress is created, the testbed configures a network path from the experiment node to the site’s gateway, allowing external connections to route through the testbed network to the experiment.
Ingresses are specified in the model via the
ingress function on a node. See the
Ingresses documentation for details.
Nodes can be categorized into groups, a technique which can be useful when automating experiments
using Ansible (see Experiment Automation). To put
nodes into groups, use a special
properties field of the
Node object as follows:
from mergexp import * net = Network('example topology with groups') a = [net.node('a%d' % i) for i in range(2)] b = [net.node('b%d' % i) for i in range(3)] r = net.node('router') net.connect(a + [r]) net.connect(b + [r]) for n in a: n.properties['group'] = ['group_a'] for n in b: n.properties['group'] = ['group_b'] experiment(net)
Once this model has been successfully realized, you can use the CLI to generate an Ansible inventory
for the model. Assuming a materialization named
mrg generate inventory r0.groups.murphy [all] a0 a1 b0 b1 b2 router [group_a] a0 a1 [group_b] b0 b1 b2
Network links are represented by the
Link object, and are created via the
.connect() method on a
Creating the link adds it to the network topology. A
Link is used to model both point-to-point links (2 nodes) and LANs (3 or more nodes).
def connect(node_list, *constraint):
|node_list||sequence of Node||two or more nodes to be interconnected|
The following constraints are defined for
|capacity||int||bits/sec||>0||set the max bandwidth|
|latency||int||ns||>=0||set the one-way packet delay|
|loss||float||percentage||0.0-1.0||set the packet loss rate|
Avoid setting constraints on links which don’t require emulation, because it adds overhead to running the experiment.
The logical operation for the
loss constraints is always ignored. The constraint value is always used
as if the logical operation were equality (
net = Network('example topology') a = net.node('a') b = net.node('b') net.connect([a, b])
IP addresses on experiment nodes are configured via the
Link object. The
Link object can be dereferenced using a
Node object, and setting the
.socket.addrs value, which sets the IP address for that node’s interface on
the link. For convenience, the
mergexp package provides the following utility
# The ip4 function takes one or more strings specifying IPv4 addresses in `address/network` # format (see https://docs.python.org/3/library/ipaddress.html#ipaddress.IPv4Network) def ip4(*addrs):
net = Network('example topology') a = net.node('a') b = net.node('b') link = net.connect([a, b]) link[a].socket.addrs = ip4('10.0.0.1/24')
Constraints are used by Merge to select physical resources that meet criteria required by the user. Constraints are specified as a boolean relationship between the constraint type and the constraint value.
metal == True memory.capacity >= gb(8) proc.cores > 1
TipWhen specifying a constraint with equality, always use
mergexp package defines many units which can be used as constraint values.
The size constraints are used to specify disk and memory requirements. The base unit for size constraints is bytes. The following convenience constraint values are defined:
kb() mb() gb() tb() pb() eb()
The capacity constraints are used to specify link bandwidth. The base unit for capacity constraints is bytes/sec. The following convenience constraint values are defined:
bps() kbps() mbps() gbps() tbps() pbps() epps()
The time constraint is used for setting the one-way link latency. The base unit is nanoseconds. The following convenience constraint values are defined:
ns() us() ms() s()