Merge pull request #27 from ehlers/netem

Packer scripts for NETem
This commit is contained in:
Julien Duponchelle 2015-10-12 11:09:17 +02:00
commit d043363b69
7 changed files with 668 additions and 0 deletions

60
packer/NETem/NETem.json Normal file
View File

@ -0,0 +1,60 @@
{
"variables": {
"tc_iso_url": "http://distro.ibiblio.org/tinycorelinux/6.x/x86/release/Core-6.4.iso",
"tc_iso_checksum": "c8e04e26de234e5528e6eac8ecb1bdda",
"vm_name": "NETem.qcow2",
"setup_script": "NETem.sh",
"upload_dir": "uploads",
"disk_size": "32"
},
"builders": [
{
"type": "qemu",
"iso_url": "{{user `tc_iso_url`}}",
"iso_checksum": "{{user `tc_iso_checksum`}}",
"iso_checksum_type": "md5",
"shutdown_command": "sudo poweroff",
"format": "qcow2",
"headless": false,
"ssh_username": "gns3",
"ssh_password": "gns3",
"accelerator": "none",
"vm_name": "{{user `vm_name`}}",
"disk_interface": "ide",
"disk_size": "{{user `disk_size`}}",
"net_device": "e1000",
"http_directory": "http",
"boot_wait": "5s",
"boot_command": [
"mc user=gns3<enter><wait10><wait10><wait10><wait10><wait10><wait10>",
"sudo passwd gns3<enter>gns3<enter>gns3<enter>",
"tce-load -wi openssh<enter><wait10>",
"cd /usr/local/etc/ssh; [ -f sshd_config.example ] && sudo cp -a sshd_config.example sshd_config; cd<enter>",
"sudo /usr/local/etc/init.d/openssh start<enter>"
]
}
],
"provisioners": [
{
"type": "shell",
"script": "scripts/hd-install.sh"
},
{
"type": "shell",
"script": "scripts/serial.sh"
},
{
"type": "file",
"source": "{{user `upload_dir`}}/",
"destination": "/tmp"
},
{
"type": "shell",
"script": "scripts/{{user `setup_script`}}"
},
{
"type": "shell",
"script": "scripts/post_setup.sh"
}
]
}

View File

@ -0,0 +1,112 @@
set -e
set -x
# get TinyCore mirror
. /etc/init.d/tc-functions
getMirror
# TCE directory back to ramdisk
mv /etc/sysconfig/tcedir /etc/sysconfig/tcedir.hd
ln -s /tmp/tce /etc/sysconfig/tcedir
mkdir build
cd build
tce-load -wi squashfs-tools
# create utf8-locale
tce-load -wi getlocale
sudo mkdir -p /usr/lib/locale
sudo localedef -i en_US -c -f UTF-8 en_US.UTF-8
sudo localedef -i en_US -c -f UTF-8 C.UTF-8
sudo mkdir -p /tmp/utf8-locale/usr/lib/locale
sudo cp -p /usr/lib/locale/* /tmp/utf8-locale/usr/lib/locale/
mksquashfs /tmp/utf8-locale utf8-locale.tcz
md5sum utf8-locale.tcz > utf8-locale.tcz.md5.txt
# create python3dialog
tce-load -wi python3-dev
wget https://bootstrap.pypa.io/get-pip.py
sudo python3 get-pip.py
rm get-pip.py
tce-load -wi dialog
sudo LANG=C.UTF-8 pip3 install pythondialog
sudo mkdir -p /tmp/python3dialog/usr/local/lib/python3.4/site-packages
sudo cp -a /usr/local/lib/python3.4/site-packages/dialog* /tmp/python3dialog/usr/local/lib/python3.4/site-packages/
sudo cp -a /usr/local/lib/python3.4/site-packages/pythondialog* /tmp/python3dialog/usr/local/lib/python3.4/site-packages/
mksquashfs /tmp/python3dialog python3dialog.tcz
md5sum python3dialog.tcz > python3dialog.tcz.md5.txt
echo -e 'python3.tcz\ndialog.tcz' > python3dialog.tcz.dep
# TCEDIR back to harddisk
rm -f /etc/sysconfig/tcedir; mv /etc/sysconfig/tcedir.hd /etc/sysconfig/tcedir
mkdir -p /etc/sysconfig/tcedir/optional
chmod 775 /etc/sysconfig/tcedir/optional
rm -f /usr/local/tce.installed/*
# install utf8-locale
cp -p utf8-locale.tcz* /etc/sysconfig/tcedir/optional/
echo 'utf8-locale.tcz' >> /etc/sysconfig/tcedir/onboot.lst
# install python3 without TK
cp -p /tmp/tce/optional/python3.tcz /etc/sysconfig/tcedir/optional/
cp -p /tmp/tce/optional/python3.tcz.md5.txt /etc/sysconfig/tcedir/optional/
sed -e '/^tk/ d' /tmp/tce/optional/python3.tcz.dep > /etc/sysconfig/tcedir/optional/python3.tcz.dep
echo 'python3.tcz' >> /etc/sysconfig/tcedir/onboot.lst
for pkg in `cat /etc/sysconfig/tcedir/optional/python3.tcz.dep`; do tce-load -w $pkg; done
# install python3dialog
cp -p python3dialog.tcz* /etc/sysconfig/tcedir/optional/
echo 'python3dialog.tcz' >> /etc/sysconfig/tcedir/onboot.lst
tce-load -w dialog
# additional linux networking modules
KERNEL=`uname -r`
tce-load -w net-bridging-$KERNEL
echo "net-bridging-$KERNEL.tcz" >> /etc/sysconfig/tcedir/onboot.lst
tce-load -w net-sched-$KERNEL
echo "net-sched-$KERNEL.tcz" >> /etc/sysconfig/tcedir/onboot.lst
# iproute2 without db library
# Bug in TinyCore 6.x, which makes arpd non-working:
# There is a mismatch of the library version between arpd and the db library.
# Therefore loading the db library has no advantage, it uses only disk space.
wget $MIRROR/iproute2.tcz
wget $MIRROR/iproute2.tcz.md5.txt
cp -p iproute2.tcz* /etc/sysconfig/tcedir/optional/
echo 'iproute2.tcz' >> /etc/sysconfig/tcedir/onboot.lst
# clean up build environment
cd ..
rm -r build
# NETem menu system
mv /tmp/netem-conf.py .
chmod +x netem-conf.py
# autologin on serial console
sudo sed -i -e '/^tty1:/ s/^.*/tty1::respawn:\/sbin\/getty 38400 tty1/' -e '/^ttyS0:/ s/^.*/ttyS0::askfirst:\/sbin\/getty -nl \/sbin\/autologin 38400 ttyS0 xterm/' /etc/inittab
sudo sed -i -e 's/tty1/`\/usr\/bin\/tty`/' /sbin/autologin
echo 'sbin/autologin' >> /opt/.filetool.lst
# autostart netem-conf
sed -i -e '/^TERMTYPE/,$ d' .profile
cat >> .profile << 'EOF'
# autostart netem-conf only on local terminals
TERMTYPE=`/usr/bin/tty`
if [ "${TERMTYPE:5:3}" = "tty" ]; then
./netem-conf.py
rm -f /var/log/autologin
fi
EOF
# disable automatic interface configuration with dhcp
sudo sed -i -e '/label microcore/,/append / s/\(append .*\)/\1 nodhcp/' /mnt/sda1/boot/extlinux/extlinux.conf
# set locale and configure network at startup
sed -n -e '1,/^\/opt\/bootlocal/ p' /opt/bootsync.sh | head -n -1 > /tmp/bootsync.head
sed -n -e '/^\/opt\/bootlocal/,$ p' /opt/bootsync.sh > /tmp/bootsync.tail
cat /tmp/bootsync.head > /opt/bootsync.sh
cat /tmp/boot_script >> /opt/bootsync.sh; echo >> /opt/bootsync.sh
cat /tmp/bootsync.tail >> /opt/bootsync.sh
# Done

View File

@ -0,0 +1,43 @@
# Install tinycore on harddisk
set -x
# format harddisk
echo -e 'n\np\n1\n\n\na\n1\nw' | sudo fdisk -H16 -S32 /dev/sda
sudo mkfs.ext2 /dev/sda1
# copy system to harddisk
sudo mkdir /mnt/sda1
sudo mount /dev/sda1 /mnt/sda1
sudo mount /mnt/sr0
sudo cp -a /mnt/sr0/boot /mnt/sda1/
sudo umount /mnt/sr0
# modify bootloader config
sudo mv /mnt/sda1/boot/isolinux /mnt/sda1/boot/extlinux
cd /mnt/sda1/boot/extlinux
sudo rm boot.cat isolinux.bin
sudo mv isolinux.cfg extlinux.conf
sudo sed -i -e '/append / s/$/ user=gns3/' -e 's/timeout .*/timeout 1/' extlinux.conf
cd
# make disk bootable
tce-load -wi syslinux
sudo sh -c 'cat /usr/local/share/syslinux/mbr.bin > /dev/sda'
sudo /usr/local/sbin/extlinux --install /mnt/sda1/boot/extlinux
# create extensions directory
sudo mkdir /mnt/sda1/tce
sudo chgrp staff /mnt/sda1/tce
sudo chmod 775 /mnt/sda1/tce
# change tcedir to harddisk
mv /etc/sysconfig/tcedir /etc/sysconfig/tcedir.bak
ln -s /mnt/sda1/tce /etc/sysconfig/tcedir
rm -rf /usr/local/tce.installed/*
# base system modifications
sudo sed -i -e '/^\/opt\/bootlocal/ i' /opt/bootsync.sh
echo -e "\nusername 'gns3', password 'gns3'\n" >> /etc/issue
echo 'etc/issue' >> /opt/.filetool.lst
echo 'etc/shadow' >> /opt/.filetool.lst

View File

@ -0,0 +1,9 @@
# post-installation script
set -x
# save changes
rm -f .ash_history
filetool.sh -b sda1
# write 0, not really necessary
#sudo dd if=/dev/zero of=/mnt/sda1/zero ; sudo rm -f /mnt/sda1/zero

View File

@ -0,0 +1,21 @@
# Add serial console support
set -x
# Boot configuration
# Serial interface is secondary console, the vga console remains main console
# To change that, swap the two 'console=' boot parameter
sudo sed -i -e '1 i serial 0 38400' -e '/label microcore/,/append / s/\(append .*\)/\1 console=ttyS0,38400 console=tty0/' /mnt/sda1/boot/extlinux/extlinux.conf
# /etc/inittab
sudo sed -i -e '/tty6/ a ttyS0::respawn:/sbin/getty 38400 ttyS0 xterm' /etc/inittab
# /etc/securetty
sudo sed -i -e 's/^# *ttyS0/ttyS0/' /etc/securetty
# reload inittab on startup
sudo sed -i -e '/^\/opt\/bootlocal/ i # reload inittab' -e '/^\/opt\/bootlocal/ i kill -HUP 1' -e '/^\/opt\/bootlocal/ i' /opt/bootsync.sh
# add modified files to backup list
echo 'etc/inittab' >> /opt/.filetool.lst
echo 'etc/securetty' >> /opt/.filetool.lst

View File

@ -0,0 +1,26 @@
. /etc/init.d/tc-functions
# default LANG=C.UTF-8
[ ! -f /etc/sysconfig/language ] || [ "`cat /etc/sysconfig/language`" = "LANG=C" ] && \
echo "LANG=C.UTF-8" > /etc/sysconfig/language
# Configure network interfaces only when boot parameter "nodhcp" is used
if grep -q -w nodhcp /proc/cmdline; then
echo -en "${BLUE}Configuring network interfaces... ${NORMAL}"
# This waits until all devices have registered
/sbin/udevadm settle --timeout=10
ip link add name br0 type bridge
ip link set dev eth0 promisc on
ip link set dev eth0 mtu 2000
ip link set dev eth0 up
ip link set dev eth0 master br0
ip link set dev eth1 promisc on
ip link set dev eth1 mtu 2000
ip link set dev eth1 up
ip link set dev eth1 master br0
ip link set dev br0 up
echo -e "${GREEN}Done.${NORMAL}"
fi

View File

@ -0,0 +1,397 @@
#!/usr/bin/env python3
#
# netem-conf - configure NETem parameter
#
import copy
import os
import subprocess
import json
from dialog import Dialog
# minimal config
config = { 'eth0_to_eth1': {}, 'symmetric': True }
# open dialog system
d = Dialog(dialog="dialog", autowidgetsize=True)
d.add_persistent_args(["--no-collapse"])
# configure NETem parameter in linux
def conf_netem(link, dev):
# remove current config
subprocess.call(['sudo', '-S', 'tc', 'qdisc', 'del', 'dev', dev, 'root'],
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
# base NETem command line
netem_cmd = ['sudo', '-S', 'tc', 'qdisc', 'add', 'dev', dev]
# configure bandwidth with htb
if config[link].get('bandwidth') is not None:
buffer = max(int(0.3*config[link]['bandwidth']+0.5), 1600)
bw_cmd = ['sudo', '-S', 'tc', 'qdisc', 'add', 'dev', dev,
'root', 'handle', '1:',
'tbf', 'rate', str(config[link]['bandwidth'])+"kbit",
'buffer', str(buffer), 'latency', '20ms']
proc = subprocess.Popen(bw_cmd, stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
out, err = proc.communicate()
if err:
err = err.decode('ascii').strip()
if err == "Password:":
err = "sudo needs password"
d.msgbox("Can't configure bandwidth !!!\n\n" + \
" ".join(bw_cmd) + "\n\n" + str(err))
return False
netem_cmd += ['parent', '1:1', 'handle', '10']
else:
netem_cmd += ['root', 'handle', '1']
netem_cmd.append('netem')
# add delay to command line
if config[link].get('delay') is not None:
netem_cmd.append("delay")
netem_cmd.append(str(config[link]['delay']) + "ms")
if config[link].get('jitter') is not None:
netem_cmd.append(str(config[link]['jitter']) + "ms")
# add loss to command line
# see http://netgroup.uniroma2.it/TR/TR-loss-netem.pdf
if config[link].get('loss') is not None:
if config[link].get('loss_burst') is None:
p13 = config[link]['loss']
p31 = 100 - config[link]['loss']
else:
p13 = config[link]['loss'] / \
(config[link]['loss_burst'] * (1 - config[link]['loss'] / 100))
p31 = 100 / config[link]['loss_burst']
netem_cmd.append("loss")
netem_cmd.append("gemodel")
netem_cmd.append(str(p13) + "%")
netem_cmd.append(str(p31) + "%")
netem_cmd.append("0")
netem_cmd.append("0")
# configure NETem parameter
proc = subprocess.Popen(netem_cmd, stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
out, err = proc.communicate()
if err:
err = err.decode('ascii').strip()
if err == "Password:":
err = "sudo needs password"
d.msgbox("Can't configure NETem !!!\n\n" + \
" ".join(netem_cmd) + "\n\n" + str(err))
return False
return True
# bandwidth configuration of a link
def string_bandwidth(link):
if config[link].get('bandwidth') is None:
bw_text = "<No Limit>"
else:
bw_text = str(config[link]['bandwidth']) + " kBit/s"
return bw_text
# delay configuration of a link
def string_delay(link):
if config[link].get('delay') is None:
delay_text = "<None>"
else:
delay_text = str(config[link]['delay']) + " ms"
if config[link].get('jitter') is not None:
delay_text += ", Jitter: " + str(config[link]['jitter']) + " ms"
return delay_text
# loss configuration of a link
def string_loss(link):
if config[link].get('loss') is None:
loss_text = "<None>"
else:
loss_text = str(config[link]['loss']) + " %"
if config[link].get('loss_burst') is not None:
loss_text += ", Burst: " + str(config[link]['loss_burst'])
return loss_text
# convert string to number
def conv_num(string):
string = string.strip()
if string == "":
x = None
else:
try:
x = float(string)
if abs(x) < 1e9 and x == int(x):
x = int(x)
except ValueError:
raise ValueError("Invalid number: " + string)
return x
# convert string to postitive number (or zero)
def conv_num_positive(string):
x = conv_num(string)
if x is not None and x < 0:
raise ValueError("Negative number: " + string)
return x
# convert string to number greater or equal one
def conv_num_ge_one(string):
x = conv_num(string)
if x is not None and x < 1:
raise ValueError("Must be at least 1: " + string)
return x
# convert string to percentage
def conv_num_percent(string):
x = conv_num(string)
if x is not None and (x < 0 or x > 100):
raise ValueError("Percentage must be 0..100: " + string)
return x
# link parameter for parsing
# ( variable, label, unit, conversion_function, input_width )
link_param_bandwidth = [
( "bandwidth", "Bandwidth", "kBit/s", conv_num_positive, 10 ) ]
link_param_delay = [
( "delay", "Delay", "ms", conv_num_positive, 10 ),
( "jitter", "Jitter", "ms", conv_num_positive, 10 ) ]
link_param_loss = [
( "loss", "Packet Loss", "%", conv_num_percent, 10 ),
( "loss_burst", "Loss Burst", "Pkts", conv_num_ge_one, 10 ) ]
link_param_all = link_param_bandwidth + link_param_delay + link_param_loss
# get link configuration
def get_link(link, link_params):
global config
title = link.replace('_to_', ' -> ')
# convert link parameter to strings
fields = []
for param in link_params:
val = config[link].get(param[0])
if val is None:
val = ""
else:
val = str(val)
fields.append(val)
# get parameter, until no errors left
ok = False
while not ok:
# create elements array for dialog.form
elements = []
i = 0
for param in link_params:
label = param[1]
if param[2] is not None:
label += " [" + param[2] + "]"
elements.append((label, i+1, 2, fields[i], i+1, 22, param[4], 0))
i += 1
# get parameter
code, fields = d.form("Link configuration " + title, elements,
title=" "+title+" ")
if code != Dialog.OK:
break
# convert string fields to data
data = {}
ok = True
i = 0
for param in link_params:
try:
data[param[0]] = param[3](fields[i])
except ValueError as err:
ok = False
d.msgbox("Input error !!!\n\n" + param[1] + ":\n" + str(err))
break
i += 1
# additinal checks
if ok and data.get('delay') is not None and \
data.get('jitter') is not None and \
data['jitter'] > data['delay']:
ok = False
d.msgbox("Input error !!!\n\nJitter must be less than delay.")
# all fine, handle some special values and copy data to link config
if ok:
if data.get('delay') == 0:
data['delay'] = None
if data.get('delay') is None or data.get('jitter') == 0:
data['jitter'] = None
if data.get('loss') == 0:
data['loss'] = None
if data.get('loss') is None or data.get('loss_burst') == 1:
data['loss_burst'] = None
for param in data:
config[link][param] = data[param]
return
# menu functions
def menu_0to1():
get_link('eth0_to_eth1', link_param_all)
def menu_0to1_bandwidth():
get_link('eth0_to_eth1', link_param_bandwidth)
def menu_0to1_delay():
get_link('eth0_to_eth1', link_param_delay)
def menu_0to1_loss():
get_link('eth0_to_eth1', link_param_loss)
def menu_asymmetric():
global config
code = d.yesno("Do you want to change to symmetric mode?")
if code == Dialog.OK:
config['symmetric'] = True
del config['eth1_to_eth0']
def menu_symmetric():
global config
code = d.yesno("Do you want to change to asymmetric mode?")
if code == Dialog.OK:
config['symmetric'] = False
config['eth1_to_eth0'] = copy.deepcopy(config['eth0_to_eth1'])
def menu_1to0():
if config['symmetric']:
menu_symmetric()
else:
get_link('eth1_to_eth0', link_param_all)
def menu_1to0_bandwidth():
get_link('eth1_to_eth0', link_param_bandwidth)
def menu_1to0_delay():
get_link('eth1_to_eth0', link_param_delay)
def menu_1to0_loss():
get_link('eth1_to_eth0', link_param_loss)
def menu_load():
global config
title = " Load Configuration "
code, path = d.fselect("configs/", 10, 60, title=title)
if code == Dialog.OK:
try:
with open(path, "r") as f:
config = json.load(f)
except (ValueError, IOError, OSError) as err:
d.msgbox("Error !!!\n\n" + str(err), title=title)
def menu_save():
title = " Save Configuration "
code, path = d.fselect("configs/", 10, 60, title=title)
if code == Dialog.OK:
try:
with open(path, "w") as f:
json.dump(config, f, sort_keys=True, indent=4,
separators=(',', ': '))
f.write("\n")
except (ValueError, IOError, OSError) as err:
d.msgbox("Error !!!\n\n" + str(err), title=title)
# backup to persistent disk
subprocess.call(['filetool.sh', '-b'],
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
def menu_shell():
d.clear()
print('Starting sub-shell, return with "exit"...')
subprocess.call('/bin/sh')
def menu_shutdown():
d.clear()
subprocess.call(['sudo', 'poweroff'])
menu_functions = {
'eth0->eth1': menu_0to1,
' Bandwidth': menu_0to1_bandwidth,
' Delay': menu_0to1_delay,
' Loss': menu_0to1_loss,
'eth1->eth0': menu_1to0,
' Asymmetric': menu_asymmetric,
' Symmetric': menu_symmetric,
' Bandwidth ': menu_1to0_bandwidth,
' Delay ': menu_1to0_delay,
' Loss ': menu_1to0_loss,
'Load': menu_load,
'Save': menu_save,
'Shell': menu_shell,
'Shutdown': menu_shutdown
}
# Main starts here
try:
# create config subdirectory
os.makedirs("configs", exist_ok=True)
# try to load initial configuration
try:
with open("configs/init", "r") as f:
config = json.load(f)
except (ValueError, IOError, OSError):
pass
# input loop
while True:
# set parameter in linux
if conf_netem('eth0_to_eth1', 'eth1'):
if config['symmetric']:
conf_netem('eth0_to_eth1', 'eth0')
else:
conf_netem('eth1_to_eth0', 'eth0')
# main menue
choices = [ ('eth0->eth1', "Configure link eth0 -> eth1"),
(' Bandwidth', string_bandwidth('eth0_to_eth1')),
(' Delay', string_delay('eth0_to_eth1')),
(' Loss', string_loss('eth0_to_eth1')),
('eth1->eth0', "Configure link eth1 -> eth0") ]
if config['symmetric']:
choices += [ (' Symmetric', "Same config as eth0 -> eth1") ]
else:
choices += [ (' Asymmetric', "Use specific configuration"),
(' Bandwidth ', string_bandwidth('eth1_to_eth0')),
(' Delay ', string_delay('eth1_to_eth0')),
(' Loss ', string_loss('eth1_to_eth0')) ]
choices += [ ("Load", "Load configuration from file"),
("Save", "Save configuration to file"),
("Shell", "Open a console"),
("Shutdown", "Shutdown the VM") ]
code, tag = d.menu("NETem Configuration", choices=choices,
title=" NETem Configuration ", no_cancel=True)
if code == Dialog.OK and tag in menu_functions:
menu_functions[tag]()
# intercept Ctrl-C
except KeyboardInterrupt:
d.clear()
exit(0)