Tutorial: Getting Started - containernet/containernet GitHub Wiki
- Create a custom topology
- Custom image
- Blocked command line and multiple terminals
- Client-Server Example
- More docker options
Using Containernet is very similar to using Mininet with custom topologies.
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.
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
.
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
.
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>
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.
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
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>
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>
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/
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.
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
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)