diff --git a/packer/NETem/NETem.json b/packer/NETem/NETem.json new file mode 100644 index 0000000..09ea0b0 --- /dev/null +++ b/packer/NETem/NETem.json @@ -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", + "sudo passwd gns3gns3gns3", + "tce-load -wi openssh", + "cd /usr/local/etc/ssh; [ -f sshd_config.example ] && sudo cp -a sshd_config.example sshd_config; cd", + "sudo /usr/local/etc/init.d/openssh start" + ] + } + ], + "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" + } + ] +} diff --git a/packer/NETem/scripts/NETem.sh b/packer/NETem/scripts/NETem.sh new file mode 100644 index 0000000..38271bc --- /dev/null +++ b/packer/NETem/scripts/NETem.sh @@ -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 diff --git a/packer/NETem/scripts/hd-install.sh b/packer/NETem/scripts/hd-install.sh new file mode 100644 index 0000000..652e9cb --- /dev/null +++ b/packer/NETem/scripts/hd-install.sh @@ -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 diff --git a/packer/NETem/scripts/post_setup.sh b/packer/NETem/scripts/post_setup.sh new file mode 100644 index 0000000..f19e0d0 --- /dev/null +++ b/packer/NETem/scripts/post_setup.sh @@ -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 diff --git a/packer/NETem/scripts/serial.sh b/packer/NETem/scripts/serial.sh new file mode 100644 index 0000000..8284fd9 --- /dev/null +++ b/packer/NETem/scripts/serial.sh @@ -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 diff --git a/packer/NETem/uploads/boot_script b/packer/NETem/uploads/boot_script new file mode 100644 index 0000000..93a9378 --- /dev/null +++ b/packer/NETem/uploads/boot_script @@ -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 diff --git a/packer/NETem/uploads/netem-conf.py b/packer/NETem/uploads/netem-conf.py new file mode 100644 index 0000000..1193e75 --- /dev/null +++ b/packer/NETem/uploads/netem-conf.py @@ -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 = "" + 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 = "" + 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 = "" + 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)