Tutorial: Getting Started - containernet/containernet GitHub Wiki

Getting Started



Using Containernet is very similar to using Mininet with custom topologies.

Create a custom topology

To start, a Python-based network topology description has to be created as shown in the following example:

#!/usr/bin/python
"""
This is the most simple example to showcase Containernet.
"""
from mininet.net import Containernet
from mininet.node import Controller
from mininet.cli import CLI
from mininet.link import TCLink
from mininet.log import info, setLogLevel
setLogLevel('info')

net = Containernet(controller=Controller)
info('*** Adding controller\n')
net.addController('c0')
info('*** Adding docker containers\n')
d1 = net.addDocker('d1', ip='10.0.0.251', dimage="ubuntu:trusty")
d2 = net.addDocker('d2', ip='10.0.0.252', dimage="ubuntu:trusty")
info('*** Adding switches\n')
s1 = net.addSwitch('s1')
s2 = net.addSwitch('s2')
info('*** Creating links\n')
net.addLink(d1, s1)
net.addLink(s1, s2, cls=TCLink, delay='100ms', bw=1)
net.addLink(s2, d2)
info('*** Starting network\n')
net.start()
info('*** Testing connectivity\n')
net.ping([d1, d2])
info('*** Running CLI\n')
CLI(net)
info('*** Stopping network')
net.stop()

You can find this topology in containernet/examples/containernet_example.py. As you can see the example creates a topology with two Hosts represented by Docker containers and two switches.

First a Containernet object is instantiated and a Controller is added:

net = Containernet(controller=Controller)
info('*** Adding controller\n')
net.addController('c0')
info('*** Adding docker containers\n')

Next the containers are added, with host ips '10.0.0.251' and '10.0.0.252' and the standard image "ubuntu:trusty".

d1 = net.addDocker('d1', ip='10.0.0.251', dimage="ubuntu:trusty")
d2 = net.addDocker('d2', ip='10.0.0.252', dimage="ubuntu:trusty")

Next we add switches...

s1 = net.addSwitch('s1')
s2 = net.addSwitch('s2')

... and Links.

net.addLink(d1, s1)
net.addLink(s1, s2, cls=TCLink, delay='100ms', bw=1)
net.addLink(s2, d2)

The network is now set up. It only needs to be started:

net.start()

After that connectivity between hosts can be tested by ping:

net.ping([d1, d2])

At last the CLI can be started:

CLI(net)

Run this example python containernet_example.py. After the pings the CLI will be available. You can run a command on every host. The syntax is: <hostname> <command>. For example:

containernet> d1 ping 10.0.0.252
PING 10.0.0.252 (10.0.0.252) 56(84) bytes of data.
64 bytes from 10.0.0.252: icmp_seq=1 ttl=64 time=0.049 ms
64 bytes from 10.0.0.252: icmp_seq=2 ttl=64 time=0.086 ms
64 bytes from 10.0.0.252: icmp_seq=3 ttl=64 time=0.092 ms
^C
--- 10.0.0.252 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2045ms
rtt min/avg/max/mdev = 0.049/0.075/0.092/0.021 ms

You can also use the hostnames defined in script: d1 ping d2

Type help for more.

Three member functions of Containernet/Mininet class used here - addDocker, addSwitch, addLink are the main tools to create topologies. Their documentation is available in chapter APIs of this wiki.

Interaction with Docker

If you open another terminal and run docker ps and docker container ls while the script is running you will see the created containers and processes.

$ sudo docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
199a7c75f0ae        ubuntu:trusty       "/bin/bash"         7 minutes ago       Up 7 minutes                            mn.d2
43249fb41ed1        ubuntu:trusty       "/bin/bash"         7 minutes ago       Up 7 minutes                            mn.d1

$ sudo docker container ls
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
199a7c75f0ae        ubuntu:trusty       "/bin/bash"         29 seconds ago      Up 26 seconds                           mn.d2
43249fb41ed1        ubuntu:trusty       "/bin/bash"         32 seconds ago      Up 29 seconds                           mn.d1

"mn" stands for Mininet. Use command exit to close the CLI and clean up all containers:

containernet> exit
*** Stopping network*** Stopping 1 controllers
c0 
*** Stopping 3 links
...
*** Stopping 2 switches
s1 s2 
*** Stopping 2 hosts
d1 d2 
*** Done
*** Removing NAT rules of 0 SAPs

By enter docker ps -a and docker container ls -a again you will see that, all containers and docker processes are removed. If you don't terminate Containernet properly or an error occurs during execution, there would be remains, that could bug the execution of scripts in future. To clean them up enter sudo mn -c.

Case of error during execution

Let's modify our script add an iperf function call after the ping, to test the bandwidth between d1 and d2:

net.ping([d1, d2])
net.iperf([d1, d2])

As function mininet.net.Mininet.ping uses the bash command ping, function mininet.net.Mininet.iperf uses iperf. Executing the script gives an error:

*** Adding controller
*** Adding docker containers
d1: kwargs {'ip': '10.0.0.251'}
d1: update resources {'cpu_quota': -1}
d2: kwargs {'ip': '10.0.0.252'}
d2: update resources {'cpu_quota': -1}
*** Adding switches
*** Creating links
(1.00Mbit 100ms delay) (1.00Mbit 100ms delay) (1.00Mbit 100ms delay) (1.00Mbit 100ms delay) *** Starting network
*** Configuring hosts
d1 d2 
*** Starting controller
c0 
*** Starting 2 switches
s1 (1.00Mbit 100ms delay) s2 (1.00Mbit 100ms delay) ...(1.00Mbit 100ms delay) (1.00Mbit 100ms delay) 
*** Testing connectivity
d1 -> d2 
d2 -> d1 
*** Results: 0% dropped (2/2 received)
*** Iperf: testing TCP bandwidth between d1 and d2 
Traceback (most recent call last):
  File "containernet_example.py", line 29, in <module>
    net.iperf([d1, d2])
  File "build/bdist.linux-x86_64/egg/mininet/net.py", line 928, in iperf
  File "build/bdist.linux-x86_64/egg/mininet/util.py", line 655, in waitListening
Exception: Could not find telnet

The containers must have iperf, telnet and telnetd installed, which is not included in the used Docker image. As mentioned above, in such case first we have to clean up with sudo mn -c.

Custom image

Let's create our own Docker image which will contain what is needed. If you are not familiar with Docker, maybe first you want to checkout the according Get Started Article. Short summary of what to do: first we create a "Dockerfile", which is some kind of descriptor of the image to create. Then from this Dockerfile we create the image and modify our script to use it. Docker containers used in Containernet must fullfill certain requirements. It has to have following packages installed:

  • net-tools
  • iputils-ping
  • iproute2

Our Dockerfile:

# parent image
FROM ubuntu:trusty

# install needed packages
RUN apt-get update && apt-get install -y \
    net-tools \
    iputils-ping \
    iproute2 \
    telnet telnetd \
    iperf

# run bash interpreter
CMD /bin/bash

Save this in a file named "Dockerfile.iperf" (in a folder named example-containers/) and create image:

sudo docker build --tag=iperf_test -f example-containers/Dockerfile.iperf example-containers/

In the script modify the addDocker function call by changing the dimage argument.

d1 = net.addDocker('d1', ip='10.0.0.251', dimage="iperf_test:latest")
d2 = net.addDocker('d2', ip='10.0.0.252', dimage="iperf_test:latest")

And run the script again:

$ sudo python containernet_example.py 
[...]
*** Iperf: testing TCP bandwidth between d1 and d2 
*** Results: ['60.8 Gbits/sec', '60.9 Gbits/sec']
*** Running CLI
*** Starting CLI:
containernet> 

It works. Let's modify one of the links, by changing its bandwidth:

net.addLink(s1, s2, cls=TCLink, delay='100ms', bw=0.5)

Which will lead to the following results:

$ sudo python containernet_example.py
[...]
*** Iperf: testing TCP bandwidth between d1 and d2 
*** Results: ['57.6 Gbits/sec', '57.7 Gbits/sec']
*** Running CLI
*** Starting CLI:
containernet> 

Note - Error - iperf - Telnet not found

Symptom: When running net.iperf((d1,d2)) an error is encountered of the form: Telnet not found. The first is that the containers must have iperf, telnet and telnetd installed. The second is that using other ubuntu docker images may also have other requirements. It is possible that the inetd service must be started manually on each container.

An example of this for ubuntu:xenial is below:

Dockerfile:

FROM ubuntu:xenial

RUN apt update && apt install -y \
        net-tools \
        iputils-ping \
        iproute2 \
        telnet telnetd \
        iperf

Example test code:

from mininet.net import Containernet
from mininet.node import Controller
from mininet.cli import CLI
from mininet.link import TCLink
from mininet.log import info, setLogLevel
setLogLevel('info')

net = Containernet(controller=Controller)
info('*** Adding controller\n')
net.addController('c0')
info('*** Adding docker containers\n')
d1 = net.addDocker('d1', ip='10.0.0.251', dimage="cjen1/iperf")
d2 = net.addDocker('d2', ip='10.0.0.252', dimage="cjen1/iperf")

d1.cmd("service openbsd-inetd start")
d2.cmd("service openbsd-inetd start")

info('*** Adding switches\n')
s1 = net.addSwitch('s1')
s2 = net.addSwitch('s2')
info('*** Creating links\n')
net.addLink(d1, s1)
net.addLink(s1, s2, cls=TCLink, delay='100ms', bw=1)
net.addLink(s2, d2)
info('*** Starting network\n')
net.start()
info('*** Testing connectivity\n')
net.ping([d1, d2])

net.iperf((d1,d2), port=23)

info('*** Stopping network')
net.stop()

The other thing to note is that by default the test uses port 5001, which is not open by default, however port 23 does work.

Build and rebuild images automatically

Function addDocker has parameter build_params, which allows building docker images directly from your Python-based topology. Example:

d3 = net.addDocker("d3", ip='10.0.0.253',
                   dimage="webserver_curl",
                   build_params={"dockerfile": "Dockerfile.server",
                                 "path": "containernet/examples/example-containers/webserver_curl"})

Output:

Docker image built. Output:
Step 1/6 : FROM python:2.7-slim
 ---> 772727e1e204
Step 2/6 : WORKDIR /app
 ---> Using cache
 ---> 4768324d62ee
Step 3/6 : COPY . /app
 ---> Using cache
 ---> a19add0368fa
Step 4/6 : RUN apt-get update && apt-get install -y     net-tools     iputils-ping     iproute2     curl
 ---> Using cache
 ---> a56b6cfc769c
Step 5/6 : RUN pip install -r requirements.txt
 ---> Using cache
 ---> 1fe0677ba6b9
Step 6/6 : CMD ["python", "app.py"]
 ---> Using cache
 ---> 17b76fb9f565
Successfully built 17b76fb9f565
Successfully tagged webserver_curl:latest
d3: kwargs {'ip': '10.0.0.253'}
d3: update resources {'cpu_quota': -1}

Default value of build_params is {}. The build process starts if the dict contains "path", otherwise it will be ignored. The image will be rebuilt every time you start the script. The dict is passed to build function of the Docker-Low-level-API. Any parameters of that function can be set in this dict. In current version it works only with tags, not with image IDs, so either dimage or build_params["tag"] must be set, otherwise an error will be raised. See its documentation: https://docker-py.readthedocs.io/en/stable/api.html#module-docker.api.build. See also the API doc

Blocked command line and multiple terminals

If you try to use iperf from command line, you will notice, that activating the iperf server blocks the terminal:

containernet> d1 iperf -s
------------------------------------------------------------
Server listening on TCP port 5001
TCP window size: 85.3 KByte (default)
------------------------------------------------------------

So you are not able to start iperf on the client. This can happen with many other command line tools. But it is possible to access the docker container directly from a second terminal:

$ sudo docker exec -it mn.d2 /bin/bash
root@d2:/# iperf -c 10.0.0.251
------------------------------------------------------------
Client connecting to 10.0.0.251, TCP port 5001
TCP window size: 85.3 KByte (default)
------------------------------------------------------------
[  3] local 10.0.0.252 port 42452 connected with 10.0.0.251 port 5001
[ ID] Interval       Transfer     Bandwidth
[  3]  0.0-10.0 sec  71.3 GBytes  61.3 Gbits/sec

Or directly with $ sudo docker exec -it mn.d2 iperf -c 10.0.0.251.
In case of iperf you can use the -D argument, to run iperf in daemon mode:

containernet> d1 iperf -s -D
Running Iperf Server as a daemon
The Iperf daemon process ID : 41
containernet> d2 iperf -c d1
------------------------------------------------------------
Client connecting to 10.0.0.251, TCP port 5001
TCP window size: 85.3 KByte (default)
------------------------------------------------------------
[  3] local 10.0.0.252 port 42468 connected with 10.0.0.251 port 5001
[ ID] Interval       Transfer     Bandwidth
[  3]  0.0-10.0 sec  71.2 GBytes  61.1 Gbits/sec
containernet>

Client-Server Example

We create two containers, a client and a server. The server contains a python script using Flask. The client uses curl two call the server.

Server:

All stored in a separate file

webserver_curl/
β”œβ”€β”€ app.py
β”œβ”€β”€ Dockerfile.server
└── requirements.txt

Dockerfile:

# Use an official Python runtime as a parent image
FROM python:2.7-slim

# Set the working directory to /app
WORKDIR /app

# Copy the current directory contents into the container at /app
COPY . /app

# Install any needed packages
RUN apt-get update && apt-get install -y \
    net-tools \
    iputils-ping \
    iproute2 \
    curl
# Install needed python packages specified in requirements.txt
RUN pip install -r requirements.txt

CMD ["python", "app.py"]

app.py

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello1():
    # returns 0
    return "0\n"

@app.route("/<arg>")
def hello(arg=0):
    # returns arg+1
    try:
        arg = int(arg)
        arg += 1
    except ValueError:
        pass
    return str(arg) + "\n"

if __name__ == "__main__":
    app.run(host='0.0.0.0', port=80)

And the requirements.txt with only one requirement - Flask.

Client:

Dockerfile

FROM ubuntu:trusty

RUN apt-get update && apt-get install -y \
    net-tools \
    iputils-ping \
    iproute2 \
    curl

CMD /bin/bash

Build images:

$ docker build --tag=server_example -f webserver_curl/Dockerfile.server webserver_curl/
[...]
$ docker build --tag=curl_example -f example_containers/Dockerfile.curl example_containers/

The python script is very similar to the first example:

#!/usr/bin/python
from mininet.net import Containernet
from mininet.node import Controller
from mininet.cli import CLI
from mininet.link import TCLink
from mininet.log import info, setLogLevel
setLogLevel('info')

net = Containernet(controller=Controller)
info('*** Adding controller\n')
net.addController('c0')
info('*** Adding docker containers\n')
server = net.addDocker('server', ip='10.0.0.251',
                       dcmd="python app.py",
                       dimage="server_example:latest")
client = net.addDocker('client', ip='10.0.0.252', dimage="curl_example:latest")
info('*** Adding switches\n')
s1 = net.addSwitch('s1')
s2 = net.addSwitch('s2')
info('*** Creating links\n')
net.addLink(server, s1)
net.addLink(s1, s2, cls=TCLink, delay='100ms', bw=1)
net.addLink(s2, client)
info('*** Starting network\n')
net.start()
info('\nclient.cmd("time curl 10.0.0.251:80/9999"):\n')
info(client.cmd("time curl 10.0.0.251:80/9999") + "\n")
info('*** Running CLI\n')
CLI(net)
info('*** Stopping network')
net.stop()

Note the following changes:

  • of course we change the dimage argument to use the created images
  • argument dcmd="python app.py" make the server start after setting up the container, default is /bin/bash, without this argument you will have to start the server manually Also the curl request is done before the cli is started:
info('*** Starting network\n')
net.start()
info('\nclient.cmd("time curl 10.0.0.251/9999"):\n')
info(client.cmd("time curl 10.0.0.251/9999") + "\n")
info('*** Running CLI\n')

This leads to the following output:

$ sudo python containernet_example_server_curl.py
[...]
client.cmd("time curl 10.0.0.251/9999"):
10000

real    0m0.694s
user    0m0.000s
sys     0m0.005s

*** Running CLI
*** Starting CLI:
containernet> 

More docker options

Port bindings

As you can see containers do not need published and binded ports to communicate to each other. But it will be needed to have access to them from outside. It is done by first listing ports and second binding ports. Example:

server = net.addDocker('server', ip='10.0.0.251',
                       ports=[80], port_bindings={80: 80},
                       dcmd="python app.py",
                       dimage="server_example:latest")

Modifying the above example like this allows for example make the curl-request from another terminal or the browser. Terminal 1:

$ sudo python containernet_example_server_curl.py 
[...]
containernet>

Terminal 2:

$ docker ps
CONTAINER ID        IMAGE                   COMMAND             CREATED             STATUS              PORTS                NAMES
0bf9816acacd        curl_example:latest     "/bin/bash"         10 seconds ago      Up 7 seconds                             mn.client
a4bd0414399e        server_example:latest   "python app.py"     14 seconds ago      Up 11 seconds       0.0.0.0:80->80/tcp   mn.server
$ curl localhost:80/9999
10000

Note that we do not use the ip 10.0.0.251, because we access the docker directly. 10.0.0.251 is not reachable. You also can specify another protocol (default is tcp):

ports=[(1111, 'udp'), 2222]
port_bindings={'1111/udp': 4567, 2222: None}

One more note: There seems to be a small bug in docker-py (at least in the used version) that can cause the ordering of the port mapping to be mixed up. So you might need to try:

 port_bindings={80:8888}

and

 port_bindings={8888:80}

see also: https://docker-py.readthedocs.io/en/1.7.0/port-bindings/

Volumes

If you are not familiar with docker volumes, see here Volumes. However, a volume is memory which can be shared between multiple containers and the host and after stopping the containers the the volume and its content remains in the host filesystem.

Use keyword argument volumes for addDocker function call. It must have the format
<docker_volume>:<container_directory>:<options> or
<host_directory>:<container_directory>:<options>.
options are optional and can be ignored. Let's try format 1:

d1 = net.addDocker('d1', volumes=["vol1:/home/vol1"], ip='10.0.0.251', dimage="ubuntu:trusty")
d2 = net.addDocker('d2', volumes=["vol1:/home/vol1"], ip='10.0.0.252', dimage="ubuntu:trusty")

This uses a docker volume, or creates one, if it does not exist:

$ docker volume ls
DRIVER              VOLUME NAME
local               vol1

Format 2 creates a directory on the host machine (or uses an existing one), which is not registered by docker:

net.addDocker('d2', volumes=["/home/:/home/vol1"], ip='10.0.0.252', dimage="ubuntu:trusty")

Terminal:

$ docker volume ls
DRIVER              VOLUME NAME
$ ls /home/user/
vol1

All directories must be absolute. Possible options are rw - read/write (default) and ro - read-only, see here for more. Note that when running containernet inside a docker, the path shall be taken from the host filesystem and not from the filesystem inside the containernet docker.

Set an environment variable

d1 = net.addDocker('d1', 
        ip='10.0.0.251’,
        dimage="ubuntu:trusty”,
        environment={"var1": "value1", "var2": 5})

CN - CLI:

containernet> d1 echo $var1
value1
containernet> d1 echo $var2
5

Set a mac address

At first it is possible to set a mac address to the default interface host-eth0, which Containernet creates together with the container. That can be done by adding the keyword argument mac to the function call:

d1 = net.addDocker('d2', ip='10.0.0.251', mac="00:00:00:00:00:02", dimage="ubuntu:trusty")

CN-CLI

containernet> d1 ifconfig
d1-eth0   Link encap:Ethernet  HWaddr 00:00:00:00:00:02  
          inet addr:10.0.0.251  Bcast:0.0.0.0  Mask:255.0.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:4 errors:0 dropped:0 overruns:0 frame:0
          TX packets:4 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:280 (280.0 B)  TX bytes:280 (280.0 B)

If an additional interface must be created, the function call of addLink must be modified. Here the keyword arguments are addr1 and Γ ddr2. In this example addr2 would be the mac on the side of s2.

net.addLink(d1, s2, params1={"ip": "192.168.1.1/24"}, addr1="00:00:00:00:00:01")

But note, that the ip of the interface has to be passed in an separate dictionary unter the keyword argument params1. The same on the side of s2 under params2.
CN-CLI:

d1-eth0   Link encap:Ethernet  HWaddr 00:00:00:00:00:02 
          inet addr:10.0.0.251  Bcast:0.0.0.0  Mask:255.0.0.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:20 errors:0 dropped:0 overruns:0 frame:0
          TX packets:3 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:2652 (2.6 KB)  TX bytes:126 (126.0 B)

d1-eth1   Link encap:Ethernet  HWaddr 00:00:00:00:00:01  
          inet addr:192.168.1.1  Bcast:0.0.0.0  Mask:255.255.255.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:23 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:2778 (2.7 KB)  TX bytes:0 (0.0 B)
⚠️ **GitHub.com Fallback** ⚠️