Skip to main content

Experiment Model Reference

Overview​

In Merge, the user defines experiment models in the Python language, making use of the 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, A and 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.

Network​

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 constraints.

def Network(name, *constraints):
ParameterTypeDescription
namestringuser description of the model

Network objects can use the following constraints:

ConstraintPossible ValuesDescription
routingstaticautomatically configure static routing on the experiment nodes
addressingipv4automatically assign IPv4 addresses to experiment nodes
experimentnetresolutionTrue,Falsewhether Merge will resolve hostnames to experiment network addresses. If set to False, node names will resolve to infranet IP addresses instead (default: True)
Tip

When using automatic routing or addressing, specific node interfaces can be excluded by setting a constraint of layer==2 on the Link object. The automatic modes will only consider links of layer 3 or higher.

Example
# Define a network with automatic assignment of static routes and
# ipv4 addresses to nodes
net = Network('my topology', routing==static, addressing==ipv4)
Example
# 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.

Nodes​

A new node is created with the Node() constructor.

def Node(name, *constraints):
ParameterTypeDescription
namestringhostname 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.

NameTypeUnitsDescription
imagestringspecify the OS image to boot on the node (default: bullseye)
metalboolnode should be not be virtualized (True) (default: False)
proc.coresinthas the specified number of processor cores (default: 1)
memory.capacityintbytesrequire the memory size (default: 512 MB)
disk.capacityintbytesrequire the specified disk size
Example
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​

The image constraint must be one of the available image names at realization time. How to find the list of available images is here. For the examples below, assume an image named ubuntu2204 exists and is available for use.

Example
net = Network("example topology")
n = net.node("gw-router", image == "ubuntu2204")

Creates a node with the hostname gw-router which runs the image named ubuntu2204 which is presumably an Ubuntu 22.04 operating system. Check the image listing page to confirm the base OS and image description.

Additional Disk Storage​

Experimental VM nodes are allocated with a 32G root file system. If additional storage is required, disk.capacity constraint can be used:

n = net.node("store", disk.capacity==gb(40))

Creates a node with the hostname store which has an extra virtual drive /dev/vdb.

This virtual drive can be formatted and mounted for storing data:

# become root
sudo bash

# format the drive and label the file system "data":
mkfs.ext4 -L data /dev/vdb

# create a directory and mount the file system:
mkdir /mnt/data
mount -L data /mnt/data

# if you want to mount it automatically after a reboot, you can add it to /etc/fstab:
echo "LABEL=data /mnt/data ext4 defaults 0 0" >>/etc/fstab
systemctl daemon-reload
Tip

All files stored on the virtual drive will disappear after the experiment is dematerialized. If you need to keep them, please remember to copy them off the experimental node before ending your realization.

Ingresses​

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.

Groups​

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 r0.groups.murphy:

mrg nodes 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 Network object. 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):
ParameterTypeDescription
node_listsequence of Nodetwo or more nodes to be interconnected

The following constraints are defined for Link objects:

NameTypeUnitsRangeDescription
layerintnumber1-3network layer
capacityintbits/sec>0set the max bandwidth
latencyintns>=0set the one-way packet delay
lossfloatpercentage0.0-1.0set the packet loss rate
Tip

Avoid setting constraints on links which don't require emulation, because it adds overhead to running the experiment.

The logical operation for the capacity, latency and loss constraints is always ignored. The constraint value is always used as if the logical operation were equality (==).

Example
net = Network('example topology')
a = net.node('a')
b = net.node('b')
net.connect([a, b])

IP Addresses​

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 function:

# 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):
Example
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​

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.

Example
metal == True
memory.capacity >= gb(8)
proc.cores > 1
Tip

When specifying a constraint with equality, always use == and not =.

The mergexp package defines many units which can be used as constraint values.

Size​

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()

Capacity​

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()

Time​

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()