├── Examples ├── LoadBalancer │ ├── MiniNAM.py │ ├── README.md │ ├── install.sh │ └── paping ├── NAT │ ├── MiniNAM.py │ ├── README.md │ ├── badNAT.py │ ├── conf.config │ └── goodNAT.py └── Routing │ ├── MiniNAM.py │ ├── README.md │ ├── simple_switch_13.py │ ├── simple_switch_stp_13.py │ └── spanning_tree.py ├── Install_MiniNam_On_Windows.pdf ├── LICENSE ├── MiniNAM.py ├── README.md └── conf.config /Examples/LoadBalancer/README.md: -------------------------------------------------------------------------------- 1 | # LoadBalancer 2 | 3 | This example uses Ryu SDN controller to implement Server Load Balancing. 4 | 5 | This example is a part of [Open-State SDN Project](https://github.com/OpenState-SDN/ryu/wiki/Server-Load-Balancing). 6 | 7 | ### Installation 8 | 9 | * Install OpenFlowState from LoadBalancer directory 10 | `bash -C install.sh` 11 | or 12 | `bash -c "$(wget -O - http://openstate-sdn.org/install.sh)` 13 | 14 | This will modify your ryu controller so make sure to keep a backup 15 | of your ryu controller code, if you have any 16 | 17 | * Install paping that allows to ping specific ports. The paping binary is 18 | included in LoadBalancer directory. Just set permissions for it: 19 | 20 | `sudo chmod +x paping` 21 | 22 | * Use the MiniNAM file provided in this directory. The `getQueue()` function 23 | in this file has been modified to add all the packets in same queue, as 24 | all the packets in this example belong to same flow. 25 | 26 | ### Running 27 | 28 | * Launch the server 29 | `ryu-manager ~/ryu/ryu/app/openstate/forwarding_consistency_1_to_many.py` 30 | 31 | * Start MiniNAM and the network: 32 | `sudo python MiniNAM.py --topo single,4 --mac --switch user --controller remote` 33 | 34 | * Start three servers by opening terminals on h2, h3 and h4 and: 35 | `h2# python /home/mininam/ryu/ryu/app/openstate/echo_server.py 200` 36 | `h3# python /home/mininam/ryu/ryu/app/openstate/echo_server.py 300` 37 | `h4# python /home/mininam/ryu/ryu/app/openstate/echo_server.py 400` 38 | 39 | * Send pings from client: 40 | `h1# ./paping 10.0.0.2 -p 80 -c 20` 41 | -------------------------------------------------------------------------------- /Examples/LoadBalancer/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # OpenState install script for Mininet 2.2.1 on Ubuntu 14.04 4 | # (https://github.com/mininet/mininet/wiki/Mininet-VM-Images) 5 | # This script is based on "Mininet install script" by Brandon Heller 6 | # (brandonh@stanford.edu) 7 | # 8 | # Authors: Davide Sanvito, Luca Pollini, Carmelo Cascone 9 | 10 | # Exit immediately if a command exits with a non-zero status. 11 | set -e 12 | 13 | # Exit immediately if a command tries to use an unset variable 14 | set -o nounset 15 | 16 | function of13 { 17 | echo "Installing OpenState switch implementation based on ofsoftswitch13..." 18 | 19 | cd ~/ 20 | 21 | if [ -d "ofsoftswitch13" ]; then 22 | read -p "A directory named ofsoftswitch13 already exists, by proceeding \ 23 | it will be deleted. Are you sure? (y/n) " -n 1 -r 24 | echo # (optional) move to a new line 25 | if [[ $REPLY =~ ^[Yy]$ ]]; then 26 | sudo rm -rf ~/ofsoftswitch13 27 | else 28 | echo "User abort!" 29 | return -1 30 | fi 31 | fi 32 | git clone https://github.com/OpenState-SDN/ofsoftswitch13.git 33 | 34 | # Resume the install: 35 | cd ~/ofsoftswitch13 36 | ./boot.sh 37 | ./configure 38 | make 39 | sudo make install 40 | cd ~/ 41 | 42 | sudo chown -R mininam ~/ofsoftswitch13 43 | } 44 | 45 | # Install RYU 46 | function ryu { 47 | echo "Installing RYU controller with OpenState support..." 48 | 49 | # install Ryu dependencies" 50 | sudo apt-get -y install autoconf automake g++ libtool python make libxml2 \ 51 | libxslt-dev python-pip python-dev 52 | sudo pip install gevent 53 | 54 | # install libraries for SPIDER 55 | sudo apt-get -y install python-matplotlib 56 | sudo pip install pbr pulp networkx fnss 57 | 58 | # fetch RYU 59 | cd ~/ 60 | if [ -d "ryu" ]; then 61 | read -p "A directory named ryu already exists, by proceeding it will be \ 62 | deleted. Are you sure? (y/n) " -n 1 -r 63 | echo # (optional) move to a new line 64 | if [[ $REPLY =~ ^[Yy]$ ]]; then 65 | sudo rm -rf ~/ryu 66 | else 67 | echo "User abort!" 68 | return -1 69 | fi 70 | fi 71 | git clone https://github.com/OpenState-SDN/ryu.git ryu 72 | cd ryu 73 | 74 | # install ryu 75 | sudo pip install -r tools/pip-requires 76 | sudo pip install -I six==1.9.0 77 | sudo python ./setup.py install 78 | 79 | sudo chown -R mininam ~/ryu 80 | } 81 | 82 | sudo apt-get update 83 | ~/mininet/util/install.sh -nt 84 | ryu 85 | of13 86 | 87 | echo "All set! To start using OpenState please refer to \ 88 | http://openstate-sdn.org for some example applications." 89 | -------------------------------------------------------------------------------- /Examples/LoadBalancer/paping: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uccmisl/MiniNAM/f8642753363295137d16d9d6621e0b2388dc1eee/Examples/LoadBalancer/paping -------------------------------------------------------------------------------- /Examples/NAT/README.md: -------------------------------------------------------------------------------- 1 | # NAT 2 | 3 | This example shows packets flowing through a router that uses NATing for hosts. 4 | 5 | ### Installation 6 | 7 | No installation needed. Just cd to the NAT directory. 8 | 9 | Use the MiniNAM.py file provided in this directory as it has been modified to 10 | detect the NAT rule being installed on the router. 11 | 12 | ### Running 13 | 14 | * Start MiniNAM and the network: 15 | `sudo python MiniNAM.py --config conf.config --custom goodNAT.py --topo mytopo` 16 | 17 | We do not need controller as the router used here is linuxrouter not OpenVSwitch, 18 | and for switches the default OVSBridge will be used by Mininet. 19 | 20 | * Adjust preferences from `Edit->Preferences`. The config file loads default preferences. 21 | 22 | * Send ping from h1 (192.168.1.100) to h3 (10.0.0.100) by opening a terminal for h1. 23 | 24 | On the packets, you can see part of IP Address (first and last octect). 25 | You can notice the change of IP Address once the packet crosses router. 26 | To see how MiniNAM makes debugging easier, let's try to have a bad rule installed in router. 27 | 28 | * Use badNAT.py or change the goodNAT.py as follows: 29 | `self.cmd( 'sysctl net.ipv4.ip_forward=0' )` 30 | 31 | Now when you create your network, you will see the packets arriving at router but not 32 | leaving (because ip_forward is not set to 1). 33 | 34 | The visualization in MiniNAM makes it very easy to identify where the problem is in network. 35 | 36 | The logical error in this example is very simple and basic but it shows how MiniNAM can 37 | simplify debugging when you are building complex protocols. 38 | 39 | 40 | -------------------------------------------------------------------------------- /Examples/NAT/badNAT.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | linuxrouter.py: Example network with Linux IP router 5 | 6 | This example converts a Node into a router using IP forwarding 7 | already built into Linux. 8 | 9 | The example topology creates a router and three IP subnets: 10 | 11 | - 192.168.1.0/24 (r0-eth1, IP: 192.168.1.1) 12 | - 172.16.0.0/12 (r0-eth2, IP: 172.16.0.1) 13 | - 10.0.0.0/8 (r0-eth3, IP: 10.0.0.1) 14 | 15 | Each subnet consists of a single host connected to 16 | a single switch: 17 | 18 | r0-eth1 - s1-eth1 - h1-eth0 (IP: 192.168.1.100) 19 | r0-eth2 - s2-eth1 - h2-eth0 (IP: 172.16.0.100) 20 | r0-eth3 - s3-eth1 - h3-eth0 (IP: 10.0.0.100) 21 | 22 | The example relies on default routing entries that are 23 | automatically created for each router interface, as well 24 | as 'defaultRoute' parameters for the host interfaces. 25 | 26 | Additional routes may be added to the router or hosts by 27 | executing 'ip route' or 'route' commands on the router or hosts. 28 | """ 29 | 30 | from mininet.topo import Topo 31 | from mininet.node import Node 32 | 33 | class LinuxRouter( Node ): 34 | "A Node with IP forwarding enabled." 35 | 36 | def config( self, **params ): 37 | super( LinuxRouter, self).config( **params ) 38 | # Enable forwarding on the router 39 | self.cmd( 'sysctl net.ipv4.ip_forward=0' ) 40 | self.cmdPrint('iptables --table nat --append POSTROUTING --out-interface r0-eth3 -j MASQUERADE') 41 | 42 | def terminate( self ): 43 | self.cmd( 'sysctl net.ipv4.ip_forward=0' ) 44 | super( LinuxRouter, self ).terminate() 45 | 46 | 47 | class NetworkTopo( Topo ): 48 | "A LinuxRouter connecting three IP subnets" 49 | 50 | def build( self, **_opts ): 51 | 52 | defaultIP = '192.168.1.1/24' # IP address for r0-eth1 53 | router = self.addNode( 'r0', cls=LinuxRouter, ip=defaultIP ) 54 | 55 | s1, s2, s3 = [ self.addSwitch( s ) for s in 's1', 's2', 's3' ] 56 | 57 | self.addLink( s1, router, intfName2='r0-eth1', 58 | params2={ 'ip' : defaultIP } ) # for clarity 59 | self.addLink( s2, router, intfName2='r0-eth2', 60 | params2={ 'ip' : '172.16.0.1/12' } ) 61 | self.addLink( s3, router, intfName2='r0-eth3', 62 | params2={ 'ip' : '10.0.0.1/8' } ) 63 | 64 | h1 = self.addHost( 'h1', ip='192.168.1.100/24', 65 | defaultRoute='via 192.168.1.1' ) 66 | h2 = self.addHost( 'h2', ip='172.16.0.100/12', 67 | defaultRoute='via 172.16.0.1' ) 68 | h3 = self.addHost( 'h3', ip='10.0.0.100/8', 69 | defaultRoute='via 10.0.0.1' ) 70 | 71 | for h, s in [ (h1, s1), (h2, s2), (h3, s3) ]: 72 | self.addLink( h, s ) 73 | 74 | locations = {'c0':(50,50), 'r0':(450,100), 's1':(200,300), 's2':(450,300), 's3':(700,300),'h1':(200,450),'h2':(450,450),'h3':(700,450)} 75 | 76 | 77 | topos = { 'mytopo': ( lambda: NetworkTopo() ) } 78 | 79 | -------------------------------------------------------------------------------- /Examples/NAT/conf.config: -------------------------------------------------------------------------------- 1 | { 2 | "filters": { 3 | "hideFromIPMAC": [ 4 | "0.0.0.0", 5 | "255.255.255.255" 6 | ], 7 | "hidePackets": [ 8 | "IPv6" 9 | ], 10 | "hideToIPMAC": [ 11 | "0.0.0.0", 12 | "255.255.255.255" 13 | ], 14 | "showPackets": [ 15 | "TCP", 16 | "UDP", 17 | "ICMP" 18 | ] 19 | }, 20 | "preferences": { 21 | "displayFlows": 1, 22 | "displayHosts": 1, 23 | "flowTime": 20000, 24 | "identifyFlows": 1, 25 | "nodeColors": "Source", 26 | "showAddr": "Source", 27 | "showNodeStats": 0, 28 | "startCLI": 0, 29 | "terminalType": "xterm", 30 | "typeColors": { 31 | "ARP": "Red", 32 | "ICMP": "Blue", 33 | "TCP": "Green", 34 | "UDP": "Green" 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /Examples/NAT/goodNAT.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | linuxrouter.py: Example network with Linux IP router 5 | 6 | This example converts a Node into a router using IP forwarding 7 | already built into Linux. 8 | 9 | The example topology creates a router and three IP subnets: 10 | 11 | - 192.168.1.0/24 (r0-eth1, IP: 192.168.1.1) 12 | - 172.16.0.0/12 (r0-eth2, IP: 172.16.0.1) 13 | - 10.0.0.0/8 (r0-eth3, IP: 10.0.0.1) 14 | 15 | Each subnet consists of a single host connected to 16 | a single switch: 17 | 18 | r0-eth1 - s1-eth1 - h1-eth0 (IP: 192.168.1.100) 19 | r0-eth2 - s2-eth1 - h2-eth0 (IP: 172.16.0.100) 20 | r0-eth3 - s3-eth1 - h3-eth0 (IP: 10.0.0.100) 21 | 22 | The example relies on default routing entries that are 23 | automatically created for each router interface, as well 24 | as 'defaultRoute' parameters for the host interfaces. 25 | 26 | Additional routes may be added to the router or hosts by 27 | executing 'ip route' or 'route' commands on the router or hosts. 28 | """ 29 | 30 | from mininet.topo import Topo 31 | from mininet.node import Node 32 | 33 | class LinuxRouter( Node ): 34 | "A Node with IP forwarding enabled." 35 | 36 | def config( self, **params ): 37 | super( LinuxRouter, self).config( **params ) 38 | # Enable forwarding on the router 39 | self.cmd( 'sysctl net.ipv4.ip_forward=1' ) 40 | self.cmdPrint('iptables --table nat --append POSTROUTING --out-interface r0-eth3 -j MASQUERADE') 41 | 42 | def terminate( self ): 43 | self.cmd( 'sysctl net.ipv4.ip_forward=0' ) 44 | super( LinuxRouter, self ).terminate() 45 | 46 | 47 | class NetworkTopo( Topo ): 48 | "A LinuxRouter connecting three IP subnets" 49 | 50 | def build( self, **_opts ): 51 | 52 | defaultIP = '192.168.1.1/24' # IP address for r0-eth1 53 | router = self.addNode( 'r0', cls=LinuxRouter, ip=defaultIP ) 54 | 55 | s1, s2, s3 = [ self.addSwitch( s ) for s in 's1', 's2', 's3' ] 56 | 57 | self.addLink( s1, router, intfName2='r0-eth1', 58 | params2={ 'ip' : defaultIP } ) # for clarity 59 | self.addLink( s2, router, intfName2='r0-eth2', 60 | params2={ 'ip' : '172.16.0.1/12' } ) 61 | self.addLink( s3, router, intfName2='r0-eth3', 62 | params2={ 'ip' : '10.0.0.1/8' } ) 63 | 64 | h1 = self.addHost( 'h1', ip='192.168.1.100/24', 65 | defaultRoute='via 192.168.1.1' ) 66 | h2 = self.addHost( 'h2', ip='172.16.0.100/12', 67 | defaultRoute='via 172.16.0.1' ) 68 | h3 = self.addHost( 'h3', ip='10.0.0.100/8', 69 | defaultRoute='via 10.0.0.1' ) 70 | 71 | for h, s in [ (h1, s1), (h2, s2), (h3, s3) ]: 72 | self.addLink( h, s ) 73 | 74 | locations = {'c0':(50,50), 'r0':(450,100), 's1':(200,300), 's2':(450,300), 's3':(700,300),'h1':(200,450),'h2':(450,450),'h3':(700,450)} 75 | 76 | 77 | topos = { 'mytopo': ( lambda: NetworkTopo() ) } 78 | 79 | -------------------------------------------------------------------------------- /Examples/Routing/README.md: -------------------------------------------------------------------------------- 1 | # Routing 2 | 3 | Taken from Spanning Tree in Ryu controller, this example creates a network with multiple paths 4 | between hosts. If a path is broken, the controller tries and updates the path, if possible. 5 | 6 | ### Installation 7 | 8 | Place the simple_switch_stp_13.py file in /ryu/ryu/app/ 9 | 10 | ### Running 11 | 12 | * Start controller: 13 | `ryu-manager ~/ryu/ryu/app/simple_switch_stp_13.py` 14 | 15 | * To see the behavior where routing will not work due to loop, you can run a simple switch 16 | `ryu-manager ~/ryu/ryu/app/simple_switch_13.py` 17 | 18 | * Start MiniNAM: 19 | `sudo python MiniNAM.py --custom spanning_tree.py --topo mytopo --controller remote` 20 | 21 | * Set the switches to OF13 by running following as sudo: 22 | `ovs-vsctl set Bridge s1 protocols=OpenFlow13` 23 | `ovs-vsctl set Bridge s2 protocols=OpenFlow13` 24 | `ovs-vsctl set Bridge s3 protocols=OpenFlow13` 25 | 26 | Wait for controller to configure ports (FORWARD and DISABLE) 27 | 28 | * Send ping from h1 (10.0.0.1) to h2 (10.0.0.2). Packets flow through shortest path (s1-s2) 29 | 30 | * Set the link (s1-s2) down from Right-Menu of the link and choosing Link Down 31 | 32 | * Ping again. 33 | 34 | Once the switches are configured, packets can be seen flowing through the other path (s1-s3-s2) 35 | 36 | -------------------------------------------------------------------------------- /Examples/Routing/simple_switch_13.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2011 Nippon Telegraph and Telephone Corporation. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 12 | # implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from ryu.base import app_manager 17 | from ryu.controller import ofp_event 18 | from ryu.controller.handler import CONFIG_DISPATCHER, MAIN_DISPATCHER 19 | from ryu.controller.handler import set_ev_cls 20 | from ryu.ofproto import ofproto_v1_3 21 | from ryu.lib.packet import packet 22 | from ryu.lib.packet import ethernet 23 | from ryu.lib.packet import ether_types 24 | 25 | 26 | class SimpleSwitch13(app_manager.RyuApp): 27 | OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION] 28 | 29 | def __init__(self, *args, **kwargs): 30 | super(SimpleSwitch13, self).__init__(*args, **kwargs) 31 | self.mac_to_port = {} 32 | 33 | @set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER) 34 | def switch_features_handler(self, ev): 35 | datapath = ev.msg.datapath 36 | ofproto = datapath.ofproto 37 | parser = datapath.ofproto_parser 38 | 39 | # install table-miss flow entry 40 | # 41 | # We specify NO BUFFER to max_len of the output action due to 42 | # OVS bug. At this moment, if we specify a lesser number, e.g., 43 | # 128, OVS will send Packet-In with invalid buffer_id and 44 | # truncated packet data. In that case, we cannot output packets 45 | # correctly. The bug has been fixed in OVS v2.1.0. 46 | match = parser.OFPMatch() 47 | actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER, 48 | ofproto.OFPCML_NO_BUFFER)] 49 | self.add_flow(datapath, 0, match, actions) 50 | 51 | def add_flow(self, datapath, priority, match, actions, buffer_id=None): 52 | ofproto = datapath.ofproto 53 | parser = datapath.ofproto_parser 54 | 55 | inst = [parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS, 56 | actions)] 57 | if buffer_id: 58 | mod = parser.OFPFlowMod(datapath=datapath, buffer_id=buffer_id, 59 | priority=priority, match=match, 60 | instructions=inst) 61 | else: 62 | mod = parser.OFPFlowMod(datapath=datapath, priority=priority, 63 | match=match, instructions=inst) 64 | datapath.send_msg(mod) 65 | 66 | @set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER) 67 | def _packet_in_handler(self, ev): 68 | # If you hit this you might want to increase 69 | # the "miss_send_length" of your switch 70 | if ev.msg.msg_len < ev.msg.total_len: 71 | self.logger.debug("packet truncated: only %s of %s bytes", 72 | ev.msg.msg_len, ev.msg.total_len) 73 | msg = ev.msg 74 | datapath = msg.datapath 75 | ofproto = datapath.ofproto 76 | parser = datapath.ofproto_parser 77 | in_port = msg.match['in_port'] 78 | 79 | pkt = packet.Packet(msg.data) 80 | eth = pkt.get_protocols(ethernet.ethernet)[0] 81 | 82 | if eth.ethertype == ether_types.ETH_TYPE_LLDP: 83 | # ignore lldp packet 84 | return 85 | dst = eth.dst 86 | src = eth.src 87 | 88 | dpid = datapath.id 89 | self.mac_to_port.setdefault(dpid, {}) 90 | 91 | self.logger.info("packet in %s %s %s %s", dpid, src, dst, in_port) 92 | 93 | # learn a mac address to avoid FLOOD next time. 94 | self.mac_to_port[dpid][src] = in_port 95 | 96 | if dst in self.mac_to_port[dpid]: 97 | out_port = self.mac_to_port[dpid][dst] 98 | else: 99 | out_port = ofproto.OFPP_FLOOD 100 | 101 | actions = [parser.OFPActionOutput(out_port)] 102 | 103 | # install a flow to avoid packet_in next time 104 | if out_port != ofproto.OFPP_FLOOD: 105 | match = parser.OFPMatch(in_port=in_port, eth_dst=dst) 106 | # verify if we have a valid buffer_id, if yes avoid to send both 107 | # flow_mod & packet_out 108 | if msg.buffer_id != ofproto.OFP_NO_BUFFER: 109 | self.add_flow(datapath, 1, match, actions, msg.buffer_id) 110 | return 111 | else: 112 | self.add_flow(datapath, 1, match, actions) 113 | data = None 114 | if msg.buffer_id == ofproto.OFP_NO_BUFFER: 115 | data = msg.data 116 | 117 | out = parser.OFPPacketOut(datapath=datapath, buffer_id=msg.buffer_id, 118 | in_port=in_port, actions=actions, data=data) 119 | datapath.send_msg(out) 120 | -------------------------------------------------------------------------------- /Examples/Routing/simple_switch_stp_13.py: -------------------------------------------------------------------------------- 1 | from ryu.base import app_manager 2 | from ryu.controller import ofp_event 3 | from ryu.controller.handler import CONFIG_DISPATCHER, MAIN_DISPATCHER 4 | from ryu.controller.handler import set_ev_cls 5 | from ryu.ofproto import ofproto_v1_3 6 | from ryu.lib import dpid as dpid_lib 7 | from ryu.lib import stplib 8 | from ryu.lib.packet import packet 9 | from ryu.lib.packet import ethernet 10 | from ryu.app import simple_switch_13 11 | 12 | 13 | class SimpleSwitch13(simple_switch_13.SimpleSwitch13): 14 | OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION] 15 | _CONTEXTS = {'stplib': stplib.Stp} 16 | 17 | def __init__(self, *args, **kwargs): 18 | super(SimpleSwitch13, self).__init__(*args, **kwargs) 19 | self.mac_to_port = {} 20 | self.stp = kwargs['stplib'] 21 | 22 | # Sample of stplib config. 23 | # please refer to stplib.Stp.set_config() for details. 24 | config = {dpid_lib.str_to_dpid('0000000000000001'): 25 | {'bridge': {'priority': 0x8000}}, 26 | dpid_lib.str_to_dpid('0000000000000002'): 27 | {'bridge': {'priority': 0x9000}}, 28 | dpid_lib.str_to_dpid('0000000000000003'): 29 | {'bridge': {'priority': 0xa000}}} 30 | self.stp.set_config(config) 31 | 32 | def delete_flow(self, datapath): 33 | ofproto = datapath.ofproto 34 | parser = datapath.ofproto_parser 35 | 36 | for dst in self.mac_to_port[datapath.id].keys(): 37 | match = parser.OFPMatch(eth_dst=dst) 38 | mod = parser.OFPFlowMod( 39 | datapath, command=ofproto.OFPFC_DELETE, 40 | out_port=ofproto.OFPP_ANY, out_group=ofproto.OFPG_ANY, 41 | priority=1, match=match) 42 | datapath.send_msg(mod) 43 | 44 | @set_ev_cls(stplib.EventPacketIn, MAIN_DISPATCHER) 45 | def _packet_in_handler(self, ev): 46 | msg = ev.msg 47 | datapath = msg.datapath 48 | ofproto = datapath.ofproto 49 | parser = datapath.ofproto_parser 50 | in_port = msg.match['in_port'] 51 | 52 | pkt = packet.Packet(msg.data) 53 | eth = pkt.get_protocols(ethernet.ethernet)[0] 54 | 55 | dst = eth.dst 56 | src = eth.src 57 | 58 | dpid = datapath.id 59 | self.mac_to_port.setdefault(dpid, {}) 60 | 61 | self.logger.info("packet in %s %s %s %s", dpid, src, dst, in_port) 62 | 63 | # learn a mac address to avoid FLOOD next time. 64 | self.mac_to_port[dpid][src] = in_port 65 | 66 | if dst in self.mac_to_port[dpid]: 67 | out_port = self.mac_to_port[dpid][dst] 68 | else: 69 | out_port = ofproto.OFPP_FLOOD 70 | 71 | actions = [parser.OFPActionOutput(out_port)] 72 | 73 | # install a flow to avoid packet_in next time 74 | if out_port != ofproto.OFPP_FLOOD: 75 | match = parser.OFPMatch(in_port=in_port, eth_dst=dst) 76 | self.add_flow(datapath, 1, match, actions) 77 | 78 | data = None 79 | if msg.buffer_id == ofproto.OFP_NO_BUFFER: 80 | data = msg.data 81 | 82 | out = parser.OFPPacketOut(datapath=datapath, buffer_id=msg.buffer_id, 83 | in_port=in_port, actions=actions, data=data) 84 | datapath.send_msg(out) 85 | 86 | @set_ev_cls(stplib.EventTopologyChange, MAIN_DISPATCHER) 87 | def _topology_change_handler(self, ev): 88 | dp = ev.dp 89 | dpid_str = dpid_lib.dpid_to_str(dp.id) 90 | msg = 'Receive topology change event. Flush MAC table.' 91 | self.logger.debug("[dpid=%s] %s", dpid_str, msg) 92 | 93 | if dp.id in self.mac_to_port: 94 | self.delete_flow(dp) 95 | del self.mac_to_port[dp.id] 96 | 97 | @set_ev_cls(stplib.EventPortStateChange, MAIN_DISPATCHER) 98 | def _port_state_change_handler(self, ev): 99 | dpid_str = dpid_lib.dpid_to_str(ev.dp.id) 100 | of_state = {stplib.PORT_STATE_DISABLE: 'DISABLE', 101 | stplib.PORT_STATE_BLOCK: 'BLOCK', 102 | stplib.PORT_STATE_LISTEN: 'LISTEN', 103 | stplib.PORT_STATE_LEARN: 'LEARN', 104 | stplib.PORT_STATE_FORWARD: 'FORWARD'} 105 | self.logger.debug("[dpid=%s][port=%d] state=%s", 106 | dpid_str, ev.port_no, of_state[ev.port_state]) 107 | -------------------------------------------------------------------------------- /Examples/Routing/spanning_tree.py: -------------------------------------------------------------------------------- 1 | """ 2 | spanning_tree.py 3 | Custom opology creation for routing example. 4 | """ 5 | from mininet.topo import Topo 6 | 7 | class MyTopo( Topo ): 8 | 9 | def __init__( self ): 10 | 11 | "Create custom topo." 12 | 13 | #Initialize topology 14 | Topo.__init__( self ) 15 | 16 | # Add hosts and switches 17 | s1 = self.addSwitch('s1') 18 | s2 = self.addSwitch('s2') 19 | s3 = self.addSwitch('s3') 20 | 21 | h1 = self.addHost('h1') 22 | h2 = self.addHost('h2') 23 | 24 | #Add links 25 | self.addLink(s1, h1) 26 | self.addLink(s2, h2) 27 | 28 | self.addLink(s1, s2) 29 | self.addLink(s2, s3) 30 | self.addLink(s3, s1) 31 | 32 | topos = { 'mytopo': ( lambda: MyTopo() ) } 33 | 34 | 35 | locations = {'c0':(50,50), 's1':(200,300), 's2':(600,300), 's3':(400,100),'h1':(200,450),'h2':(600,450)} 36 | 37 | -------------------------------------------------------------------------------- /Install_MiniNam_On_Windows.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uccmisl/MiniNAM/f8642753363295137d16d9d6621e0b2388dc1eee/Install_MiniNam_On_Windows.pdf -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /MiniNAM.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or modify 2 | # it under the terms of the GNU General Public License version 2 as 3 | # published by the Free Software Foundation; 4 | # 5 | # This program is distributed in the hope that it will be useful, 6 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 7 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 8 | # GNU General Public License for more details. 9 | # 10 | # You should have received a copy of the GNU General Public License 11 | # along with this program; if not, write to the Free Software 12 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 13 | # 14 | # 15 | # Created by Ahmed Khalid a.khalid@cs.ucc.ie and Jason Quinlan j.quinlan@cs.ucc.ie 16 | # 03 November 2017 - version number 1.0.1 17 | 18 | import socket 19 | from struct import * 20 | from subprocess import * 21 | import threading 22 | 23 | from mininet.clean import cleanup 24 | from mininet.cli import CLI 25 | from mininet.log import lg, LEVELS, info, debug, warn, error 26 | from mininet.net import MininetWithControlNet 27 | from mininet.node import ( Host, Node, CPULimitedHost, Controller, OVSController, 28 | Ryu, NOX, RemoteController, findController, 29 | DefaultController, NullController, 30 | UserSwitch, OVSSwitch, OVSBridge, 31 | IVSSwitch ) 32 | from mininet.nodelib import LinuxBridge 33 | from mininet.link import Link, TCLink, OVSLink, TCULink 34 | from mininet.topo import ( SingleSwitchTopo, LinearTopo, 35 | SingleSwitchReversedTopo, MinimalTopo ) 36 | from mininet.topolib import TreeTopo, TorusTopo 37 | from mininet.util import customClass, specialClass, splitArgs 38 | from mininet.util import buildTopo 39 | from functools import partial 40 | from mininet.examples.cluster import ( MininetCluster, RemoteHost, 41 | RemoteOVSSwitch, RemoteLink, 42 | SwitchBinPlacer, RandomPlacer, 43 | ClusterCleanup ) 44 | from mininet.examples.clustercli import ClusterCLI 45 | 46 | 47 | from optparse import OptionParser 48 | import os 49 | from tkMessageBox import showerror 50 | import tkFont 51 | import tkFileDialog 52 | import tkSimpleDialog 53 | import json 54 | from distutils.version import StrictVersion 55 | from mininet.term import makeTerm, cleanUpScreens 56 | from mininet.net import Mininet, VERSION 57 | from mininet.util import quietRun 58 | import random 59 | from threading import Thread 60 | from Tkinter import * 61 | import time 62 | from cmath import pi 63 | from math import atan2, sin, cos 64 | from PIL import Image, ImageDraw 65 | from PIL import ImageTk as itk 66 | import Queue 67 | from collections import OrderedDict 68 | 69 | MININET_VERSION = re.sub(r'[^\d\.]', '', VERSION) 70 | if StrictVersion(MININET_VERSION) > StrictVersion('2.0'): 71 | from mininet.node import IVSSwitch 72 | MININAM_VERSION = "1.0.1" 73 | 74 | # Fix setuptools' evil madness, and open up (more?) security holes 75 | if 'PYTHONPATH' in os.environ: 76 | sys.path = os.environ[ 'PYTHONPATH' ].split( ':' ) + sys.path 77 | 78 | Eth_Protocols = {'8':'IP', '1544':'ARP', '56710':'IPv6'} 79 | IP_Protocols = {'1':'ICMP', '6':'TCP', '17':'UDP'} 80 | 81 | TOPODEF = 'minimal' 82 | TOPOS = {'minimal': MinimalTopo, 83 | 'linear': LinearTopo, 84 | 'reversed': SingleSwitchReversedTopo, 85 | 'single': SingleSwitchTopo, 86 | 'tree': TreeTopo, 87 | 'torus': TorusTopo} 88 | 89 | SWITCHDEF = 'default' 90 | SWITCHES = {'user': UserSwitch, 91 | 'ovs': OVSSwitch, 92 | 'ovsbr': OVSBridge, 93 | # Keep ovsk for compatibility with 2.0 94 | 'ovsk': OVSSwitch, 95 | 'ivs': IVSSwitch, 96 | 'lxbr': LinuxBridge, 97 | 'default': OVSSwitch} 98 | SWITCHES_TYPES = [switch.__name__ for switch in SWITCHES.values()] 99 | 100 | HOSTDEF = 'proc' 101 | HOSTS = {'proc': Host, 102 | 'rt': specialClass(CPULimitedHost, defaults=dict(sched='rt')), 103 | 'cfs': specialClass(CPULimitedHost, defaults=dict(sched='cfs'))} 104 | HOSTS_TYPES = ['Host', 'CPULimitedHost'] 105 | 106 | CONTROLLERDEF = 'default' 107 | CONTROLLERS = {'ref': Controller, 108 | 'ovsc': OVSController, 109 | 'nox': NOX, 110 | 'remote': RemoteController, 111 | 'ryu': Ryu, 112 | 'default': DefaultController, # Note: replaced below 113 | 'none': NullController} 114 | CONTROLLERS_TYPES = [ctrlr.__name__ for ctrlr in CONTROLLERS.values()] 115 | 116 | LINKDEF = 'default' 117 | LINKS = {'default': Link, 118 | 'tc': TCLink, 119 | 'tcu': TCULink, 120 | 'ovs': OVSLink} 121 | LINKS_TYPES = ['Link', 'TCLink', 'OVSLink', 'TCULink'] 122 | 123 | LEGACY_TYPES = ['LegacyRouter', 'LinuxRouter', 'LegacySwitch'] 124 | 125 | FLOWTIMEDEF = 'Fast' 126 | FLOWTIME = OrderedDict([('Very Slow', 40000),('Slow',20000),('Fast', 5000), ('Very Fast', 1000), ('Real Time', 1)]) 127 | 128 | LinkTime = 0.1 129 | 130 | def version( *_args ): 131 | "Print Mininet and MiniNAM version and exit" 132 | print "Mininet: %s" % MININET_VERSION 133 | print "MiniNAM: %s" % MININAM_VERSION 134 | sys.exit() 135 | 136 | def packetParser(packet): 137 | 138 | PacketInfo = {} 139 | PacketInfo['eth_protocol'] = None 140 | PacketInfo['srcMAC'] = None 141 | PacketInfo['dstMAC'] = None 142 | PacketInfo['s_addr'] = None 143 | PacketInfo['d_addr'] = None 144 | PacketInfo['ip_protocol'] = None 145 | PacketInfo['ttl'] = None 146 | PacketInfo['source_port'] = None 147 | PacketInfo['dest_port'] = None 148 | PacketInfo['sequence'] = None 149 | PacketInfo['data'] = None 150 | PacketInfo['icmp_type'] = None 151 | PacketInfo['code'] = None 152 | PacketInfo['checksum'] = None 153 | PacketInfo['length'] = None 154 | PacketInfo['protocol_type'] = None 155 | 156 | try: 157 | # parse ethernet header 158 | eth_length = 14 159 | eth_header = packet[:eth_length] 160 | 161 | eth = unpack('!6s6sH', eth_header) 162 | 163 | eth_protocol = socket.ntohs(eth[2]) 164 | 165 | dstMAC = ':'.join('%02x' % ord(b) for b in packet[0:6]) 166 | srcMAC = ':'.join('%02x' % ord(b) for b in packet[6:12]) 167 | 168 | PacketInfo['srcMAC'] = str(srcMAC) 169 | PacketInfo['dstMAC'] = str(dstMAC) 170 | PacketInfo['eth_protocol'] = str(eth_protocol) 171 | 172 | # Parse IP packets, IP Protocol number = 8 173 | if eth_protocol == 8: 174 | # Parse IP header 175 | # take first 20 characters for the ip header 176 | ip_header = packet[eth_length:20 + eth_length] 177 | 178 | # now unpack them 179 | iph = unpack('!BBHHHBBH4s4s', ip_header) 180 | 181 | version_ihl = iph[0] 182 | ihl = version_ihl & 0xF 183 | 184 | iph_length = ihl * 4 185 | 186 | ttl = iph[5] 187 | protocol = iph[6] 188 | s_addr = socket.inet_ntoa(iph[8]); 189 | d_addr = socket.inet_ntoa(iph[9]); 190 | 191 | PacketInfo['s_addr'] = s_addr 192 | PacketInfo['d_addr'] = d_addr 193 | PacketInfo['ip_protocol'] = str(protocol) 194 | PacketInfo['ttl'] = str(ttl) 195 | 196 | # TCP protocol 197 | if protocol == 6: 198 | t = iph_length + eth_length 199 | tcp_header = packet[t:t + 32] 200 | 201 | # now unpack them 202 | tcph = unpack('!HHLLBBHHHBBBBLL', tcp_header) 203 | 204 | source_port = tcph[0] 205 | dest_port = tcph[1] 206 | sequence = tcph[2] 207 | acknowledgement = tcph[3] 208 | doff_reserved = tcph[4] 209 | tcph_length = doff_reserved >> 4 210 | TSVal = tcph[13] 211 | 212 | 213 | h_size = eth_length + iph_length + tcph_length * 4 214 | # get data from the packet 215 | data = packet[h_size:] 216 | PacketInfo['source_port'] = source_port 217 | PacketInfo['dest_port'] = dest_port 218 | PacketInfo['sequence'] = sequence 219 | PacketInfo['acknowledgement'] = acknowledgement 220 | PacketInfo['TSVal'] = TSVal 221 | PacketInfo['data'] = data 222 | 223 | # ICMP Packets 224 | elif protocol == 1: 225 | u = iph_length + eth_length 226 | icmph_length = 4 227 | icmp_header = packet[u:u + 4] 228 | 229 | # now unpack them 230 | icmph = unpack('!BBH', icmp_header) 231 | 232 | icmp_type = icmph[0] 233 | code = icmph[1] 234 | checksum = icmph[2] 235 | 236 | h_size = eth_length + iph_length + icmph_length 237 | data_size = len(packet) - h_size 238 | 239 | # get data from the packet 240 | data = packet[h_size:] 241 | PacketInfo['icmp_type'] = str(icmp_type) 242 | PacketInfo['code'] = str(code) 243 | PacketInfo['checksum'] = str(checksum) 244 | PacketInfo['data'] = data 245 | 246 | # UDP packets 247 | elif protocol == 17: 248 | u = iph_length + eth_length 249 | udph_length = 8 250 | udp_header = packet[u:u + 8] 251 | 252 | # now unpack them 253 | udph = unpack('!HHHH', udp_header) 254 | 255 | source_port = udph[0] 256 | dest_port = udph[1] 257 | length = udph[2] 258 | checksum = udph[3] 259 | 260 | h_size = eth_length + iph_length + udph_length 261 | data_size = len(packet) - h_size 262 | 263 | # get data from the packet 264 | data = packet[h_size:] 265 | PacketInfo['source_port'] = source_port 266 | PacketInfo['dest_port'] = dest_port 267 | PacketInfo['length'] = length 268 | PacketInfo['checksum'] = checksum 269 | PacketInfo['data'] = data 270 | # Other IP packet like IGMP can be parsed here. 271 | else: 272 | pass 273 | 274 | #Parse ARP packets 275 | elif eth_protocol == 1544: 276 | arp_header = packet[14:42] 277 | arph = unpack("!2sH1s1s2s6s4s6s4s", arp_header) 278 | s_addr = socket.inet_ntoa(arph[6]) 279 | d_addr = socket.inet_ntoa(arph[8]) 280 | protocol_type = arph[1] 281 | PacketInfo['s_addr'] = str(s_addr) 282 | PacketInfo['d_addr'] = str(d_addr) 283 | PacketInfo['protocol_type'] = protocol_type 284 | #Other EthPackets like IPv6 can be parsed here. 285 | else: 286 | pass 287 | 288 | return PacketInfo 289 | 290 | except: 291 | return PacketInfo 292 | 293 | class PrefsDialog(tkSimpleDialog.Dialog): 294 | "Preferences dialog" 295 | 296 | def __init__(self, parent, title, prefDefaults): 297 | 298 | self.prefValues = prefDefaults 299 | 300 | tkSimpleDialog.Dialog.__init__(self, parent, title) 301 | 302 | def body(self, master): 303 | "Create dialog body" 304 | self.rootFrame = master 305 | 306 | # Field for displaying traffic flows 307 | Label(self.rootFrame, text="Display traffic flows in the network").grid(row=0, sticky=W) 308 | self.displayFlows = IntVar() 309 | self.cdisplayFlows = Checkbutton(self.rootFrame, variable=self.displayFlows) 310 | self.cdisplayFlows.grid(row=0, column=1, sticky=W) 311 | if self.prefValues['displayFlows'] == 0: 312 | self.cdisplayFlows.deselect() 313 | else: 314 | self.cdisplayFlows.select() 315 | 316 | # Field for displaying hosts along with network topology 317 | Label(self.rootFrame, text="Display hosts in the network:").grid(row=1, sticky=W) 318 | self.displayHosts = IntVar() 319 | self.cdisplayHosts = Checkbutton(self.rootFrame, variable=self.displayHosts) 320 | self.cdisplayHosts.grid(row=1, column=1, sticky=W) 321 | if self.prefValues['displayHosts'] == 0: 322 | self.cdisplayHosts.deselect() 323 | else: 324 | self.cdisplayHosts.select() 325 | 326 | # Field for Packet Flow Speed 327 | Label(self.rootFrame, text="Speed of Packet Flow").grid(row=2, sticky=W) 328 | self.flowTime = StringVar(self.rootFrame) 329 | self.flowTime.set(FLOWTIME.keys()[FLOWTIME.values().index(self.prefValues['flowTime'])]) 330 | self.flowTimeMenu = OptionMenu(self.rootFrame, self.flowTime, *FLOWTIME.keys()) 331 | self.flowTimeMenu.grid(row=2, column=1, sticky=W) 332 | 333 | # Field for Node Colors 334 | Label(self.rootFrame, text="Color Code Packets By:").grid(row=3, sticky=W) 335 | self.nodeColorsVar = StringVar(self.rootFrame) 336 | self.nodeColorsOption = OptionMenu(self.rootFrame, self.nodeColorsVar, "Source", "Destination", "None") 337 | self.nodeColorsOption.grid(row=3, column=1, sticky=W) 338 | self.nodeColorsVar.set(self.prefValues['nodeColors']) 339 | 340 | # Selection for color of packet type 341 | self.typeColorsFrame= LabelFrame(self.rootFrame, text='Colors for Packet Types', padx=5, pady=5) 342 | self.typeColorsFrame.grid(row=4, column=0, columnspan=2, sticky=EW) 343 | for i in range(3): 344 | self.typeColorsFrame.columnconfigure(i, weight=1) 345 | self.typeColors = self.prefValues['typeColors'] 346 | 347 | # Selection of color for ARP 348 | Label(self.typeColorsFrame, text="ARP").grid(row=0, column=0, sticky=W) 349 | self.ARPColor = StringVar(self.typeColorsFrame) 350 | self.ARPColor.set(self.typeColors["ARP"]) 351 | self.ARPColorMenu = OptionMenu(self.typeColorsFrame, self.ARPColor, "None", "Red", "Green", "Blue", "Purple") 352 | self.ARPColorMenu.grid(row=1, column=0, sticky=W) 353 | 354 | # Selection of color for TCP 355 | Label(self.typeColorsFrame, text="TCP").grid(row=0, column=1, sticky=W) 356 | self.TCPColor = StringVar(self.typeColorsFrame) 357 | self.TCPColor.set(self.typeColors["TCP"]) 358 | self.TCPColorMenu = OptionMenu(self.typeColorsFrame, self.TCPColor, "None", "Red", "Green", "Blue", "Purple") 359 | self.TCPColorMenu.grid(row=1, column=1, sticky=W) 360 | # Selection of color for ICMP 361 | Label(self.typeColorsFrame, text="ICMP").grid(row=0, column=2, sticky=W) 362 | self.ICMPColor = StringVar(self.typeColorsFrame) 363 | self.ICMPColor.set(self.typeColors["ICMP"]) 364 | self.ICMPColorMenu = OptionMenu(self.typeColorsFrame, self.ICMPColor, "None", "Red", "Green", "Blue", "Purple") 365 | self.ICMPColorMenu.grid(row=1, column=2, sticky=W) 366 | # Selection of color for UDP 367 | Label(self.typeColorsFrame, text="UDP").grid(row=0, column=3, sticky=W) 368 | self.UDPColor = StringVar(self.typeColorsFrame) 369 | self.UDPColor.set(self.typeColors["UDP"]) 370 | self.UDPColorMenu = OptionMenu(self.typeColorsFrame, self.UDPColor, "None", "Red", "Green", "Blue", "Purple") 371 | self.UDPColorMenu.grid(row=1, column=3, sticky=W) 372 | 373 | 374 | # Selection of terminal type 375 | Label(self.rootFrame, text="Default Terminal:").grid(row=5, sticky=W) 376 | self.terminalVar = StringVar(self.rootFrame) 377 | self.terminalOption = OptionMenu(self.rootFrame, self.terminalVar, "xterm", "gterm") 378 | self.terminalOption.grid(row=5, column=1, sticky=W) 379 | terminalType = self.prefValues['terminalType'] 380 | self.terminalVar.set(terminalType) 381 | 382 | # Field for CLI 383 | Label(self.rootFrame, text="Start CLI:").grid(row=6, sticky=W) 384 | self.cliStart = IntVar() 385 | self.cliButton = Checkbutton(self.rootFrame, variable=self.cliStart) 386 | self.cliButton.grid(row=6, column=1, sticky=W) 387 | if self.prefValues['startCLI'] == 0: 388 | self.cliButton.deselect() 389 | else: 390 | self.cliButton.select() 391 | 392 | # Field for showing IP Packets 393 | Label(self.rootFrame, text="Show IP address on packets:").grid(row=7, sticky=W) 394 | self.showAddrVar = StringVar(self.rootFrame) 395 | self.showaddrOption = OptionMenu(self.rootFrame, self.showAddrVar, "Source", "Destination", "None") 396 | self.showaddrOption.grid(row=7, column=1, sticky=W) 397 | self.showAddrVar.set(self.prefValues['showAddr']) 398 | 399 | # Field for showing nodeStats 400 | Label(self.rootFrame, text="Show Node Statistics Box:").grid(row=8, sticky=W) 401 | self.showNodeStats = IntVar() 402 | self.cshowNodeStats = Checkbutton(self.rootFrame, variable=self.showNodeStats) 403 | self.cshowNodeStats.grid(row=8, column=1, sticky=W) 404 | if self.prefValues['showNodeStats'] == 0: 405 | self.cshowNodeStats.deselect() 406 | else: 407 | self.cshowNodeStats.select() 408 | 409 | # Field for identifying packets belonging to same flow 410 | Label(self.rootFrame, text="Identify packets in the same flow and display in order:").grid(row=9, sticky=W) 411 | self.identifyFlows = IntVar() 412 | self.cidentifyFlows = Checkbutton(self.rootFrame, variable=self.identifyFlows) 413 | self.cidentifyFlows.grid(row=9, column=1, sticky=W) 414 | if self.prefValues['identifyFlows'] == 0: 415 | self.cidentifyFlows.deselect() 416 | else: 417 | self.cidentifyFlows.select() 418 | 419 | def apply(self): 420 | 421 | flowTime = FLOWTIME[self.flowTime.get()] 422 | self.typeColors['ARP'] = str(self.ARPColor.get()) 423 | self.typeColors['TCP'] = str(self.TCPColor.get()) 424 | self.typeColors['ICMP'] = str(self.ICMPColor.get()) 425 | self.typeColors['UDP'] = str(self.UDPColor.get()) 426 | typeColors = self.typeColors 427 | 428 | self.result = {'displayFlows': self.displayFlows.get(), 429 | 'displayHosts': self.displayHosts.get(), 430 | 'flowTime': flowTime, 431 | 'nodeColors': self.nodeColorsVar.get(), 432 | 'typeColors': typeColors, 433 | 'terminalType': self.terminalVar.get(), 434 | 'startCLI': self.cliStart.get(), 435 | 'showAddr': self.showAddrVar.get(), 436 | 'showNodeStats': self.showNodeStats.get(), 437 | 'identifyFlows': self.identifyFlows.get() 438 | } 439 | 440 | class FiltersDialog(tkSimpleDialog.Dialog): 441 | "Filters dialog" 442 | 443 | def __init__(self, parent, title, filterDefaults): 444 | 445 | self.filterValues = filterDefaults 446 | 447 | tkSimpleDialog.Dialog.__init__(self, parent, title) 448 | 449 | def body(self, master): 450 | "Create dialog body" 451 | self.rootFrame = master 452 | 453 | 454 | # Field for Show Packet Types 455 | Label(self.rootFrame, text="Show Packet Types:").grid(row=0, sticky=E) 456 | self.showPackets = Text(self.rootFrame, height = 1) 457 | self.showPackets.grid(row=0, column=1) 458 | showPackets = self.filterValues['showPackets'] 459 | for item in showPackets: 460 | self.showPackets.insert(END, item + ', ') 461 | 462 | # Field for Hide Packet Types 463 | Label(self.rootFrame, text="Hide Packet Types:").grid(row=2, sticky=E) 464 | self.hidePackets = Text(self.rootFrame, height = 1) 465 | self.hidePackets.grid(row=2, column=1) 466 | hidePackets = self.filterValues['hidePackets'] 467 | for item in hidePackets: 468 | self.hidePackets.insert(END, item + ', ') 469 | 470 | # Field for Hide Packets From IP or MAC 471 | Label(self.rootFrame, text="Hide Packets from IP or MAC:").grid(row=3, sticky=E) 472 | self.hideFromIPMAC = Text(self.rootFrame, height = 1) 473 | self.hideFromIPMAC.grid(row=3, column=1) 474 | hideFromIPMAC = self.filterValues['hideFromIPMAC'] 475 | for item in hideFromIPMAC: 476 | self.hideFromIPMAC.insert(END, item + ', ') 477 | 478 | # Field for Hide Packet To IP or MAC 479 | Label(self.rootFrame, text="Hide Packets To IP or MAC:").grid(row=4, sticky=E) 480 | self.hideToIPMAC = Text(self.rootFrame, height = 1) 481 | self.hideToIPMAC.grid(row=4, column=1) 482 | hideToIPMAC = self.filterValues['hideToIPMAC'] 483 | for item in hideToIPMAC: 484 | self.hideToIPMAC.insert(END, item + ', ') 485 | 486 | 487 | # initial focus 488 | return self.showPackets 489 | 490 | def apply(self): 491 | showPackets = str(self.showPackets.get("1.0",'end-1c')).replace(' ', '').replace('\n', '').replace('\r', '').split(',') 492 | hidePackets = str(self.hidePackets.get("1.0",'end-1c')).replace(' ', '').replace('\n', '').replace('\r', '').split(',') 493 | hideFromIPMAC = str(self.hideFromIPMAC.get("1.0",'end-1c')).replace(' ', '').replace('\n', '').replace('\r', '').split(',') 494 | hideToIPMAC = str(self.hideToIPMAC.get("1.0",'end-1c')).replace(' ', '').replace('\n', '').replace('\r', '').split(',') 495 | # Removing empty items from lists 496 | showPackets = filter(None, showPackets) 497 | hidePackets = filter(None, hidePackets) 498 | hideFromIPMAC = filter(None, hideFromIPMAC) 499 | hideToIPMAC = filter(None, hideToIPMAC) 500 | self.result= { 501 | 'showPackets': showPackets, 502 | 'hidePackets': hidePackets, 503 | 'hideFromIPMAC': hideFromIPMAC, 504 | 'hideToIPMAC': hideToIPMAC 505 | } 506 | 507 | 508 | @staticmethod 509 | def getOvsVersion(): 510 | "Return OVS version" 511 | outp = quietRun("ovs-vsctl show") 512 | r = r'ovs_version: "(.*)"' 513 | m = re.search(r, outp) 514 | if m is None: 515 | print 'Version check failed' 516 | return None 517 | else: 518 | print 'Open vSwitch version is '+m.group(1) 519 | return m.group(1) 520 | 521 | class NodeStats(object): 522 | 523 | def __init__(self, widget): 524 | self.widget = widget 525 | self.tipwindow = None 526 | self.id = None 527 | self.x = self.y = 0 528 | 529 | def showtip(self, text): 530 | "Display text in nodeStats window" 531 | self.text = text 532 | if self.tipwindow or not self.text: 533 | return 534 | x, y, _cx, cy = self.widget.bbox("insert") 535 | x = x + self.widget.winfo_rootx() + 27 536 | y = y + cy + self.widget.winfo_rooty() +27 537 | self.tipwindow = tw = Toplevel(self.widget) 538 | tw.wm_overrideredirect(1) 539 | tw.wm_geometry("+%d+%d" % (x, y)) 540 | try: 541 | tw.tk.call("::tk::unsupported::MacWindowStyle", 542 | "style", tw._w, 543 | "help", "noActivates") 544 | except TclError: 545 | pass 546 | label = Label(tw, text=self.text, justify=LEFT, 547 | background="#ffffe0", relief=SOLID, borderwidth=1, 548 | font=("tahoma", "8", "normal")) 549 | label.pack(ipadx=1) 550 | 551 | def hidetip(self): 552 | tw = self.tipwindow 553 | self.tipwindow = None 554 | if tw: 555 | tw.destroy() 556 | 557 | class MiniNAM( Frame ): 558 | 559 | "A realtime network animator for Mininet." 560 | 561 | def __init__( self, parent=None, cheight=600, cwidth=1000 , net= None, locations={}): 562 | 563 | Frame.__init__( self, parent ) 564 | self.action = None 565 | 566 | #Defaults for preferences and filters 567 | self.appPrefs={ 568 | 'displayFlows': 1, 569 | 'displayHosts': 1, 570 | 'flowTime': FLOWTIME[FLOWTIMEDEF], 571 | 'nodeColors': 'Source', 572 | 'typeColors': {'ARP': 'Red', 'TCP': 'Green', 'ICMP': 'Blue', 'UDP': 'Green'}, 573 | 'startCLI': 1, 574 | 'terminalType': 'xterm', 575 | 'showAddr': 'None', 576 | 'showNodeStats': 0, 577 | 'identifyFlows': 1 578 | } 579 | self.appFilters={ 580 | 'showPackets': ['TCP', 'UDP', 'ICMP'], #ARP? 581 | 'hidePackets': ['IPv6'], 582 | 'hideFromIPMAC': ['0.0.0.0', '255.255.255.255'], 583 | 'hideToIPMAC': ['0.0.0.0', '255.255.255.255'] 584 | } 585 | 586 | # Style 587 | self.fixedFont = tkFont.Font ( family="DejaVu Sans Mono", size="14" ) 588 | self.font = ( 'Geneva', 9 ) 589 | self.smallFont = ( 'Geneva', 7 ) 590 | self.bg = 'white' 591 | #If more hosts than this list then random colors are assigned to remaining hosts 592 | self.HOST_COLORS = ['#E57300', '#FF66B2', '#fa0004', '#b1b106', '#957aff', '#FF00FF', '#2f90d0', '#818c8d', 593 | '#A93226', '#1b2ef8', '#3ef979', '#7c2ff9'] 594 | self.Controller_Color = '#9D9D9D' 595 | self.images = miniImages() 596 | 597 | # Title 598 | self.appName = 'MiniNAM' 599 | self.top = self.winfo_toplevel() 600 | self.top.title( self.appName ) 601 | 602 | # Menu bar 603 | self.createMenubar() 604 | 605 | # Editing canvas 606 | self.cheight, self.cwidth = cheight, cwidth 607 | self.cframe, self.canvas = self.createCanvas() 608 | 609 | # Layout 610 | self.cframe.grid( column=1, row=0 ) 611 | self.columnconfigure( 1, weight=1 ) 612 | self.rowconfigure( 0, weight=1 ) 613 | self.pack( expand=True, fill='both' ) 614 | 615 | # Info boxes 616 | self.aboutBox = None 617 | self.infoBox = None 618 | 619 | # Initialize node data 620 | self.nodeBindings = self.createNodeBindings() 621 | self.nodePrefixes = { 'LegacyRouter': 'r', 'LegacySwitch': 's', 'Switch': 's', 'Host': 'h' , 'Controller': 'c'} 622 | self.widgetToItem = {} 623 | self.itemToWidget = {} 624 | self.Nodes = [] 625 | # intfdata is [{"node":"-1", 'color': None, "type":"-1", "interface": "-1", "mac":"-1", "ip": "-1", "dgw":"-1", "link": "-1", "TXP":0, "RXP":0, "TXB":0, "RXB":0}] 626 | self.intfData = [] 627 | 628 | # Initialize link tool 629 | self.link = self.linkWidget = None 630 | 631 | # Selection support 632 | self.selection = None 633 | 634 | # Keyboard and Popup bindings 635 | self.bind( '', lambda event: self.quit() ) 636 | self.focus() 637 | self.canvas.bind('', self.setFocus) 638 | self.hostPopup = Menu(self.top, tearoff=0, takefocus=1) 639 | self.hostPopup.add_command(label='Host Options', font=self.font) 640 | self.hostPopup.add_separator() 641 | self.hostPopup.add_command(label='Terminal', font=self.font, command=self.xterm ) 642 | self.hostPopup.bind("", self.popupFocusOut) 643 | 644 | self.legacyRouterPopup = Menu(self.top, tearoff=0, takefocus=1) 645 | self.legacyRouterPopup.add_command(label='Router Options', font=self.font) 646 | self.legacyRouterPopup.add_separator() 647 | self.legacyRouterPopup.add_command(label='Terminal', font=self.font, command=self.xterm ) 648 | self.legacyRouterPopup.bind("", self.popupFocusOut) 649 | 650 | self.switchPopup = Menu(self.top, tearoff=0, takefocus=1) 651 | self.switchPopup.add_command(label='Switch Options', font=self.font) 652 | self.switchPopup.add_separator() 653 | self.switchPopup.add_command(label='List bridge details', font=self.font, command=self.listBridge ) 654 | self.switchPopup.bind("", self.popupFocusOut) 655 | 656 | self.linkPopup = Menu(self.top, tearoff=0, takefocus=1) 657 | self.linkPopup.add_command(label='Link Options', font=self.font) 658 | self.linkPopup.add_separator() 659 | self.linkPopup.add_command(label='Link Up', font=self.font, command=self.linkUp ) 660 | self.linkPopup.add_command(label='Link Down', font=self.font, command=self.linkDown ) 661 | self.linkPopup.bind("", self.popupFocusOut) 662 | 663 | 664 | # Event handling initalization 665 | self.linkx = self.linky = self.linkItem = None 666 | self.lastSelection = None 667 | 668 | # Model initialization 669 | self.packetImage = [] 670 | self.flowQueues = {} 671 | self.PLACEMENT = {'block': SwitchBinPlacer, 'random': RandomPlacer} 672 | self.links = {} 673 | self.cli = None 674 | 675 | #Setting up values when MiniNAM class is called from a script 676 | self.nodelocations = locations 677 | self.options = None 678 | self.args = None 679 | self.validate = None 680 | self.net = net 681 | self.active = True 682 | 683 | #Setup network if MiniNAM is called from CLI 684 | if self.net is None: 685 | self.parseArgs() 686 | self.setup() 687 | self.begin() 688 | if self.options.test == 'cli': 689 | self.startCLI() 690 | 691 | #Exit if network wasn't created properly 692 | if self.net is None: 693 | error('Network does not exist. Do not use net(stop) in your script if you want GUI to load.') 694 | sys.exit() 695 | 696 | #Start siniffing packets on Mininet interfaces 697 | self.sniff = Thread( target=self.sniff ) 698 | self.sniff.daemon = True 699 | self.sniff.start() 700 | 701 | #Start CLI thread if requested 702 | if self.appPrefs['startCLI'] == 1: 703 | self.startCLI() 704 | 705 | #Gather topology info and create nodes 706 | self.TopoInfo() 707 | self.createNodes() 708 | 709 | # Place window at bottom 710 | self.top.geometry("%dx%d%+d%+d" % (self.cwidth, self.cheight, 1, 1000)) 711 | 712 | # Close window gracefully 713 | Wm.wm_protocol( self.top, name='WM_DELETE_WINDOW', func=self.quit ) 714 | 715 | #Set the logo for MiniNAM 716 | logo = self.images['Logo'] 717 | self.top.tk.call('wm', 'iconphoto', self.top._w, logo) 718 | 719 | # Arguments and Network 720 | 721 | def custom( self, _option, _opt_str, value, _parser ): 722 | "Parse custom file and add params." 723 | files = [] 724 | if os.path.isfile( value ): 725 | # Accept any single file (including those with commas) 726 | files.append( value ) 727 | else: 728 | # Accept a comma-separated list of filenames 729 | files += value.split(',') 730 | 731 | for fileName in files: 732 | customs = {} 733 | if os.path.isfile( fileName ): 734 | execfile( fileName, customs, customs ) 735 | for name, val in customs.iteritems(): 736 | self.setCustom( name, val ) 737 | else: 738 | raise Exception( 'could not find custom file: %s' % fileName ) 739 | 740 | def setCustom( self, name, value ): 741 | "Set custom parameters for Mininet." 742 | if name.upper() == 'NET': 743 | info('*** Loading network from custom file ***\n') 744 | self.net = value 745 | elif name in ( 'topos', 'switches', 'hosts', 'controllers' ): 746 | # Update dictionaries 747 | param = name.upper() 748 | try: 749 | globals()[ param ].update( value ) 750 | globals()[str(param + '_TYPES')].append( value.keys()[0]) 751 | except: 752 | pass 753 | elif name == 'validate': 754 | # Add custom validate function 755 | self.validate = value 756 | 757 | elif name == 'locations': 758 | self.nodelocations = value 759 | 760 | else: 761 | # Add or modify global variable or class 762 | globals()[ name ] = value 763 | 764 | def configs( self, _option, _opt_str, value, _parser ): 765 | 766 | "Load custom configs." 767 | fileName = value 768 | 769 | if not os.path.isfile(fileName): 770 | print 'Could not find config file: %s. Loading default preferences and filters.' % fileName 771 | return 772 | f = open(fileName, 'r') 773 | loadedPrefs = self.convertJsonUnicode(json.load(f)) 774 | # Load application preferences 775 | if 'preferences' in loadedPrefs: 776 | self.appPrefs = dict(self.appPrefs.items() + loadedPrefs['preferences'].items()) 777 | # Load application filters 778 | if 'filters' in loadedPrefs: 779 | self.appFilters = dict(self.appFilters.items() + loadedPrefs['filters'].items()) 780 | f.close() 781 | 782 | def setNat( self, _option, opt_str, value, parser ): 783 | "Set NAT option(s)" 784 | assert self # satisfy pylint 785 | parser.values.nat = True 786 | # first arg, first char != '-' 787 | if parser.rargs and parser.rargs[ 0 ][ 0 ] != '-': 788 | value = parser.rargs.pop( 0 ) 789 | _, args, kwargs = splitArgs( opt_str + ',' + value ) 790 | parser.values.nat_args = args 791 | parser.values.nat_kwargs = kwargs 792 | else: 793 | parser.values.nat_args = [] 794 | parser.values.nat_kwargs = {} 795 | 796 | def addDictOption(self, opts, choicesDict, default, name, **kwargs): 797 | """Convenience function to add choices dicts to OptionParser. 798 | opts: OptionParser instance 799 | choicesDict: dictionary of valid choices, must include default 800 | default: default choice key 801 | name: long option name 802 | kwargs: additional arguments to add_option""" 803 | helpStr = ('|'.join(sorted(choicesDict.keys())) + 804 | '[,param=value...]') 805 | helpList = ['%s=%s' % (k, v.__name__) 806 | for k, v in choicesDict.items()] 807 | helpStr += ' ' + (' '.join(helpList)) 808 | params = dict(type='string', default=default, help=helpStr) 809 | params.update(**kwargs) 810 | opts.add_option('--' + name, **params) 811 | 812 | def parseArgs( self ): 813 | """Parse command-line args and return options object. 814 | returns: opts parse options dict""" 815 | 816 | desc = ( "The %prog utility creates Mininet network from the\n" 817 | "command line, loads a GUI with the created topology and\n" 818 | "displays any network traffic generated." ) 819 | 820 | usage = ( '%prog [options]\n' 821 | '(type %prog -h for details)' ) 822 | 823 | opts = OptionParser( description=desc, usage=usage ) 824 | opts.add_option('--config', action='callback', 825 | callback=self.configs, 826 | type='string', 827 | help='load custom preferences from .config file' 828 | ) 829 | opts.add_option('--custom', action='callback', 830 | callback=self.custom, 831 | type='string', 832 | help='read custom classes, params or network (with net as name) from .py file(s)' 833 | ) 834 | 835 | self.addDictOption( opts, SWITCHES, SWITCHDEF, 'switch' ) 836 | self.addDictOption( opts, HOSTS, HOSTDEF, 'host' ) 837 | self.addDictOption( opts, CONTROLLERS, [], 'controller', action='append' ) 838 | self.addDictOption( opts, LINKS, LINKDEF, 'link' ) 839 | self.addDictOption( opts, TOPOS, TOPODEF, 'topo' ) 840 | 841 | opts.add_option( '--clean', '-c', action='store_true', 842 | default=False, help='clean and exit' ) 843 | 844 | # optional tests to run 845 | TESTS = ['cli', 'build', 'pingall', 'pingpair', 'iperf', 'all', 'iperfudp', 'none'] 846 | 847 | opts.add_option( '--test', type='choice', choices=TESTS, 848 | default=TESTS[ -1 ], 849 | help='|'.join( TESTS ) ) 850 | opts.add_option( '--xterms', '-x', action='store_true', 851 | default=False, help='spawn xterms for each node' ) 852 | opts.add_option( '--ipbase', '-i', type='string', default='10.0.0.0/8', 853 | help='base IP address for hosts' ) 854 | opts.add_option( '--mac', action='store_true', 855 | default=False, help='automatically set host MACs' ) 856 | opts.add_option( '--arp', action='store_true', 857 | default=False, help='set all-pairs ARP entries' ) 858 | opts.add_option( '--verbosity', '-v', type='choice', 859 | choices=LEVELS.keys(), default = 'info', 860 | help = '|'.join( LEVELS.keys() ) ) 861 | opts.add_option( '--innamespace', action='store_true', 862 | default=False, help='sw and ctrl in namespace?' ) 863 | opts.add_option( '--listenport', type='int', default=6634, 864 | help='base port for passive switch listening' ) 865 | opts.add_option( '--nolistenport', action='store_true', 866 | default=False, help="don't use passive listening " + 867 | "port") 868 | opts.add_option( '--pre', type='string', default=None, 869 | help='CLI script to run before tests' ) 870 | opts.add_option( '--post', type='string', default=None, 871 | help='CLI script to run after tests' ) 872 | opts.add_option( '--pin', action='store_true', 873 | default=False, help="pin hosts to CPU cores " 874 | "(requires --host cfs or --host rt)" ) 875 | opts.add_option( '--nat', action='callback', callback=self.setNat, 876 | help="adds a NAT to the topology that" 877 | " connects Mininet hosts to the physical network." 878 | " Warning: This may route any traffic on the machine" 879 | " that uses Mininet's" 880 | " IP subnet into the Mininet network." 881 | " If you need to change" 882 | " Mininet's IP subnet, see the --ipbase option." ) 883 | opts.add_option( '--version', action='callback', callback=version, 884 | help='prints the version and exits' ) 885 | opts.add_option( '--cluster', type='string', default=None, 886 | metavar='server1,server2...', 887 | help=( 'run on multiple servers (experimental!)' ) ) 888 | opts.add_option( '--placement', type='choice', 889 | choices=self.PLACEMENT.keys(), default='block', 890 | metavar='block|random', 891 | help=( 'node placement for --cluster ' 892 | '(experimental!) ' ) ) 893 | 894 | self.options, self.args = opts.parse_args() 895 | 896 | # We don't accept extra arguments after the options 897 | if self.args: 898 | opts.print_help() 899 | sys.exit() 900 | 901 | def setup( self ): 902 | "Setup and validate environment." 903 | lg.setLogLevel( self.options.verbosity ) 904 | 905 | def begin( self ): 906 | "Create and run mininet." 907 | 908 | if self.options.cluster: 909 | servers = self.options.cluster.split( ',' ) 910 | for server in servers: 911 | ClusterCleanup.add( server ) 912 | 913 | if self.options.clean: 914 | cleanup() 915 | sys.exit() 916 | 917 | if not self.options.controller: 918 | # Update default based on available controllers 919 | CONTROLLERS[ 'default' ] = findController() 920 | self.options.controller = [ 'default' ] 921 | if not CONTROLLERS[ 'default' ]: 922 | self.options.controller = [ 'none' ] 923 | if self.options.switch == 'default': 924 | info( '*** No default OpenFlow controller found ' 925 | 'for default switch!\n' ) 926 | info( '*** Falling back to OVS Bridge\n' ) 927 | self.options.switch = 'ovsbr' 928 | elif self.options.switch not in ( 'ovsbr', 'lxbr' ): 929 | raise Exception( "Could not find a default controller " 930 | "for switch %s" % 931 | self.options.switch ) 932 | 933 | topo = buildTopo( TOPOS, self.options.topo ) 934 | switch = customClass( SWITCHES, self.options.switch ) 935 | host = customClass( HOSTS, self.options.host ) 936 | controller = [ customClass( CONTROLLERS, c ) 937 | for c in self.options.controller ] 938 | 939 | if self.options.switch == 'user' and self.options.link == 'default': 940 | # Using TCULink with UserSwitch 941 | # Use link configured correctly for UserSwitch 942 | self.options.link = 'tcu' 943 | 944 | link = customClass( LINKS, self.options.link ) 945 | 946 | if self.validate: 947 | self.validate( self.options ) 948 | 949 | ipBase = self.options.ipbase 950 | xterms = self.options.xterms 951 | mac = self.options.mac 952 | arp = self.options.arp 953 | pin = self.options.pin 954 | listenPort = None 955 | if not self.options.nolistenport: 956 | listenPort = self.options.listenport 957 | 958 | # Handle inNamespace, cluster options 959 | inNamespace = self.options.innamespace 960 | cluster = self.options.cluster 961 | if inNamespace and cluster: 962 | print "Please specify --innamespace OR --cluster" 963 | sys.exit() 964 | Net = MininetWithControlNet if inNamespace else Mininet 965 | if cluster: 966 | warn( '*** WARNING: Experimental cluster mode!\n' 967 | '*** Using RemoteHost, RemoteOVSSwitch, RemoteLink\n' ) 968 | host, switch, link = RemoteHost, RemoteOVSSwitch, RemoteLink 969 | Net = partial( MininetCluster, servers=servers, 970 | placement=self.PLACEMENT[ self.options.placement ] ) 971 | 972 | 973 | self.net = Net( topo=topo, 974 | switch=switch, host=host, controller=controller, 975 | link=link, 976 | ipBase=ipBase, 977 | inNamespace=inNamespace, 978 | xterms=xterms, autoSetMacs=mac, 979 | autoStaticArp=arp, autoPinCpus=pin, 980 | listenPort=listenPort ) 981 | 982 | if self.options.ensure_value( 'nat', False ): 983 | nat = self.net.addNAT( *self.options.nat_args, 984 | **self.options.nat_kwargs ) 985 | nat.configDefault() 986 | 987 | self.net.start() 988 | 989 | def runTest( self ): 990 | 991 | cluster = self.options.cluster 992 | cli = ClusterCLI if cluster else CLI 993 | if self.options.pre: 994 | cli(self.net, script=self.options.pre) 995 | test = self.options.test 996 | ALTSPELLING = {'pingall': 'pingAll', 997 | 'pingpair': 'pingPair', 998 | 'iperfudp': 'iperfUdp', 999 | 'iperfUDP': 'iperfUdp'} 1000 | 1001 | test = ALTSPELLING.get(test, test) 1002 | 1003 | if test == 'none': 1004 | pass 1005 | elif test == 'all': 1006 | self.net.waitConnected() 1007 | self.net.start() 1008 | self.net.ping() 1009 | self.net.iperf() 1010 | elif test == 'cli': 1011 | cli( self.net ) 1012 | pass 1013 | elif test != 'build': 1014 | self.net.waitConnected() 1015 | getattr(self.net, test)() 1016 | 1017 | if self.options.post: 1018 | cli(self.net, script=self.options.post) 1019 | 1020 | # Topology and Sniffing 1021 | 1022 | def TopoInfo(self): 1023 | 1024 | #Gather node info for all nodes in the network 1025 | for item , value in self.net.items(): 1026 | if value.__class__.__name__ in CONTROLLERS_TYPES: 1027 | self.Nodes.append({'name': item, 'widget':None, 'type':value.__class__.__name__, 'ip':value.ip, 'port':value.port, 'color':self.Controller_Color}) 1028 | elif value.__class__.__name__ in SWITCHES_TYPES: 1029 | self.Nodes.append({'name': item, 'widget': None, 'type': value.__class__.__name__, 'dpid':value.dpid, 'color': None, 'controllers':[]}) 1030 | elif value.__class__.__name__ in HOSTS_TYPES: 1031 | if self.appPrefs['displayHosts'] == 1: 1032 | self.Nodes.append({'name': item, 'widget': None, 'type': value.__class__.__name__, 'ip':value.IP(), 'color': None}) 1033 | else: 1034 | continue 1035 | else: 1036 | self.Nodes.append( 1037 | {'name': item, 'widget': None, 'type': value.__class__.__name__, 'color': None}) 1038 | 1039 | #Gather interface info for all interfaces of a node 1040 | for intf in value.intfList(): 1041 | intf2 = str(intf.link).replace(intf.name,'').replace('<->','') 1042 | if intf2 != 'None': 1043 | self.intfData.append({'node': item, 'type': value.__class__.__name__, 'interface': intf.name, 'mac': intf.mac, 'ip':intf.ip, 1044 | 'link': intf2, 'TXP': 0, 'RXP': 0, 'TXB': 0, 'RXB': 0}) 1045 | 1046 | #To find and save the controller that each switch is connected to. Needed because there can be more than one controller. 1047 | for switch in self.Nodes: 1048 | if switch['type'] in SWITCHES_TYPES: 1049 | try: 1050 | switch_info = check_output(["ovs-vsctl", "get-controller", switch['name']]) 1051 | except: 1052 | switch_info = '-1' 1053 | first_controller = None 1054 | for controller in self.Nodes: 1055 | if controller['type'] in CONTROLLERS_TYPES: 1056 | controller_info = str(controller['ip']) + ':' + str(controller['port']) 1057 | if controller_info in switch_info: 1058 | switch['controllers'].append(controller['name']) 1059 | if first_controller == None: 1060 | first_controller = controller['name'] 1061 | # TODO: Assign user switch properly to the correct controller 1062 | # Currently just assigning the first controller to the switch. Will work if there is only one controller. 1063 | if switch['controllers'] == []: 1064 | switch['controllers'].append(first_controller) 1065 | 1066 | 1067 | def intfExists(self, interface): 1068 | for data in self.intfData: 1069 | if data["interface"] == interface: 1070 | return data 1071 | return None 1072 | 1073 | def sniff( self ): 1074 | 1075 | #Create raw socket to receive everything 1076 | try: 1077 | s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.ntohs(0x0003)) 1078 | except socket.error as msg: 1079 | print('Socket could not be created. Error Code : ' + str(msg[0]) + ' Message ' + msg[1]) 1080 | sys.exit() 1081 | # receive a packet 1082 | while True: 1083 | if self.appPrefs['displayFlows'] == 0: 1084 | continue 1085 | 1086 | packet = s.recvfrom(65565) 1087 | 1088 | interface = packet[1][0] 1089 | direction = "incoming" 1090 | if packet[1][2] == socket.PACKET_OUTGOING: 1091 | direction = "outgoing" 1092 | 1093 | # packet string from tuple 1094 | packet = packet[0] 1095 | 1096 | #Parse the packet and get info in headers 1097 | PacketInfo = packetParser(packet) 1098 | 1099 | eth_protocol = PacketInfo['eth_protocol'] 1100 | srcMAC, dstMAC = PacketInfo['srcMAC'], PacketInfo['dstMAC'] 1101 | ip_protocol = PacketInfo['ip_protocol'] 1102 | s_addr, d_addr = PacketInfo['s_addr'], PacketInfo['d_addr'] 1103 | data = PacketInfo['data'] 1104 | 1105 | # TODO: Sniff the controller packets 1106 | 1107 | try: 1108 | #Skip packet if it is supposed to be filtered 1109 | if self.filterPacket(srcMAC, dstMAC, s_addr, d_addr, eth_protocol, ip_protocol): 1110 | continue 1111 | except: 1112 | continue 1113 | 1114 | try: 1115 | #Make sure that the interface info exists in our topology 1116 | intf = self.intfExists(interface) 1117 | if not intf: 1118 | continue 1119 | except Exception: 1120 | continue 1121 | 1122 | PacketInfo['interface'] = interface 1123 | PacketInfo['direction'] = direction 1124 | 1125 | try: 1126 | #MiniNAM sniffs packets as they are received, except for the packets going to the host. 1127 | #This is because hosts are separate processes in Mininet and their interfaces are not 1128 | #actual interfaces on the machine. So to sniff packets reaching hosts, we look at the 1129 | #packets leaving the last-hop switch. 1130 | if direction == "outgoing": 1131 | intf["TXB"] += len(packet) 1132 | intf["TXP"] += 1 1133 | link = self.intfExists(intf["link"]) 1134 | if link['type'] in HOSTS_TYPES or link['type'] in ['LinuxRouter', 'LinuxSwitch'] : 1135 | link["RXB"] += len(packet) 1136 | link["RXP"] += 1 1137 | #Check if the packet should be color coded by IP 1138 | if self.appPrefs['nodeColors'] == 'Source': 1139 | sender = next((node for node in self.Nodes if 'ip' in node if node['ip'] == s_addr), None) 1140 | PacketInfo['node_color'] = sender['color'] if sender else 'black' 1141 | if self.appPrefs['nodeColors'] == 'Destination': 1142 | receiver = next((node for node in self.Nodes if 'ip' in node if node['ip'] == d_addr), None) 1143 | PacketInfo['node_color'] = receiver['color'] if receiver else 'black' 1144 | src, dst = intf["node"], intf["link"].split('-')[0] 1145 | #To view the effect of link delays LinkTime value can be replaced by actual link delays set in Mininet 1146 | PacketInfo['time'] = LinkTime 1147 | #Create a packet object to be displayed in the GUI 1148 | self.createPacket(src, dst, PacketInfo) 1149 | 1150 | #Sniff packets reaching at any interface 1151 | if direction == "incoming": 1152 | intf["RXB"] += len(packet) 1153 | intf["RXP"] += 1 1154 | link = self.intfExists(intf["link"]) 1155 | if link['type'] in HOSTS_TYPES or link['type'] in ['LinuxRouter', 'LinuxSwitch']: 1156 | link = self.intfExists(intf["link"]) 1157 | link["TXB"] += len(packet) 1158 | link["TXP"] += 1 1159 | # Check if the packet should be color coded by IP 1160 | if self.appPrefs['nodeColors'] == 'Source': 1161 | sender = next((node for node in self.Nodes if 'ip' in node if node['ip'] == s_addr), None) 1162 | PacketInfo['node_color'] = sender['color'] if sender else 'black' 1163 | if self.appPrefs['nodeColors'] == 'Destination': 1164 | receiver = next((node for node in self.Nodes if 'ip' in node if node['ip'] == d_addr), None) 1165 | PacketInfo['node_color'] = receiver['color'] if receiver else 'black' 1166 | src, dst = intf["link"].split('-')[0], intf["node"] 1167 | # To view the effect of link delays LinkTime value can be replaced by actual link delays set in Mininet 1168 | PacketInfo['time'] = LinkTime 1169 | # Create a packet object to be displayed in the GUI 1170 | self.createPacket(src, dst, PacketInfo) 1171 | 1172 | except Exception: 1173 | pass 1174 | 1175 | def createNodes(self): 1176 | #Drawing node Widgets 1177 | for node in self.Nodes: 1178 | if not self.findWidgetByName(node['name']): 1179 | location = self.nodelocations[node['name']] if node['name'] in self.nodelocations else (random.randrange(70,self.cwidth-100), random.randrange(70,self.cheight-100)) 1180 | self.newNamedNode(node, location[0], location[1]) 1181 | 1182 | #Drawing data links 1183 | for data in self.intfData: 1184 | try: 1185 | self.drawLink(data["interface"].split('-')[0], data["link"].split('-')[0]) 1186 | except: 1187 | pass 1188 | 1189 | #Drawing control links 1190 | for switch in self.Nodes: 1191 | try: 1192 | for ctrlr in switch['controllers']: 1193 | self.drawLink(ctrlr, switch['name']) 1194 | except: 1195 | pass 1196 | 1197 | def filterPacket(self, srcMAC, dstMAC, s_addr, d_addr, eth_protocol, ip_protocol): 1198 | try: 1199 | if s_addr is not None: 1200 | if s_addr in self.appFilters['hideFromIPMAC']: 1201 | return True 1202 | if d_addr is not None: 1203 | if d_addr in self.appFilters['hideToIPMAC']: 1204 | return True 1205 | if srcMAC is not None: 1206 | if srcMAC in self.appFilters['hideFromIPMAC']: 1207 | return True 1208 | if dstMAC is not None: 1209 | if dstMAC in self.appFilters['hideToIPMAC']: 1210 | return True 1211 | if ip_protocol is not None: 1212 | if IP_Protocols[str(ip_protocol)] in self.appFilters['hidePackets']: 1213 | return True 1214 | if eth_protocol is not None: 1215 | if Eth_Protocols[str(eth_protocol)] in self.appFilters['hidePackets']: 1216 | return True 1217 | if 'ALL' in [t.upper() for t in self.appFilters['showPackets']]: 1218 | return False 1219 | if ip_protocol is not None: 1220 | if IP_Protocols[str(ip_protocol)] in self.appFilters['showPackets']: 1221 | return False 1222 | if eth_protocol is not None: 1223 | if Eth_Protocols[str(eth_protocol)] in self.appFilters['showPackets']: 1224 | return False 1225 | return True 1226 | except: 1227 | return True 1228 | 1229 | def createPacket(self, src, dst, PacketInfo): 1230 | try: 1231 | #Get the queue in which the packet should be added 1232 | q = self.getQueue(PacketInfo) 1233 | if q is not None: 1234 | #Refresh a queue if it grows above 100 packets 1235 | if q.qsize() > 100: 1236 | self.clearQueue(q) 1237 | #Create a thread to display packet and add it to the queue 1238 | thr = Thread(target= self.displayPacket, args=(src, dst, PacketInfo)) 1239 | thr.daemon = True 1240 | q.put(thr) 1241 | except Exception as e: 1242 | print e 1243 | 1244 | def displayPacket(self, src, dst, PacketInfo): 1245 | 1246 | try: 1247 | c = self.canvas 1248 | s = self.findWidgetByName(src) 1249 | d = self.findWidgetByName(dst) 1250 | srcx, srcy = c.coords(self.widgetToItem[s]) 1251 | dstx, dsty = c.coords(self.widgetToItem[d]) 1252 | 1253 | #Draw a rectangle shape for the packet 1254 | image1 = Image.new("RGBA", (30, 15)) 1255 | draw = ImageDraw.Draw(image1) 1256 | draw.polygon([(0, 0), (0, 15), (30, 15), (30, 0)], "black") 1257 | 1258 | #Color code packet based on IP if needed 1259 | try: 1260 | node_color = PacketInfo["node_color"] 1261 | except: 1262 | node_color = 'black' 1263 | draw.polygon([(10, 0), (10, 15), (30, 15), (30, 0)], node_color) 1264 | 1265 | # Color code packet by type 1266 | try: 1267 | eth_color = self.appPrefs['typeColors'][Eth_Protocols[PacketInfo['eth_protocol']]] 1268 | if eth_color == 'None': eth_color = 'black' 1269 | except: 1270 | eth_color = None 1271 | try: 1272 | ip_color = self.appPrefs['typeColors'][IP_Protocols[PacketInfo["ip_protocol"]]] 1273 | if ip_color == 'None': ip_color = 'black' 1274 | except: 1275 | ip_color = None 1276 | 1277 | if eth_color is not None: 1278 | draw.polygon([(0, 0), (0, 15), (10, 15), (10, 0)], eth_color) 1279 | if ip_color is not None: 1280 | draw.polygon([(0, 0), (0, 15), (10, 15), (10, 0)], ip_color) 1281 | 1282 | #If IP address is not displayed then rotate the packet along the link 1283 | if self.appPrefs['showAddr'] == 'None': 1284 | angle = -1 * atan2(dsty-srcy,dstx-srcx) 1285 | dx = 7 * sin(angle) 1286 | dy = 7 * cos(angle) 1287 | angle = 180*angle/pi 1288 | packetImage = itk.PhotoImage(image1.rotate(angle, expand=True)) 1289 | 1290 | else: 1291 | if self.appPrefs['showAddr'] == 'Source': 1292 | addr = PacketInfo['s_addr'] 1293 | elif self.appPrefs['showAddr'] == 'Destination': 1294 | addr = PacketInfo['d_addr'] 1295 | 1296 | address = addr.split('.')[0] + '.' + addr.split('.')[-1] 1297 | draw.text((0, 0), address) 1298 | packetImage = itk.PhotoImage(image1) 1299 | dx, dy = 0, 0 1300 | 1301 | self.packetImage.append(packetImage) 1302 | packet = c.create_image(srcx+dx, srcy+dy, image=packetImage) 1303 | deltax = (dstx - srcx) / 50 1304 | deltay = (dsty - srcy) / 50 1305 | delta = deltax, deltay 1306 | 1307 | t = float(self.appPrefs['flowTime']) * float(PacketInfo['time']) / 50000 # 1000 for ms and 50 for steps 1308 | self.movePacket(packet, packetImage, delta, t) 1309 | 1310 | except Exception: 1311 | pass 1312 | 1313 | def movePacket(self, packet, image, delta, t): 1314 | c = self.canvas 1315 | i = 0 1316 | #Move the packet in 50 steps then remove the image 1317 | while i < 50: 1318 | if self.active: 1319 | i+=1 1320 | c.move(packet, delta[0], delta[1]) 1321 | c.update() 1322 | time.sleep(t) 1323 | c.delete(packet) 1324 | self.packetImage.remove(image) 1325 | 1326 | def getQueue(self, PacketInfo): 1327 | eth_protocol, ip_protocol = PacketInfo['eth_protocol'], PacketInfo['ip_protocol'] 1328 | s_addr, d_addr = PacketInfo['s_addr'], PacketInfo['d_addr'] 1329 | interface = PacketInfo['interface'] 1330 | addr1, addr2 = s_addr, d_addr 1331 | 1332 | t = float(self.appPrefs['flowTime']) * float(PacketInfo['time']) / 50000 #sec to ms 1000 and 50 steps 1333 | 1334 | #Separate queue for each interface for real-time or if not trying to identify flows 1335 | if self.appPrefs['flowTime'] == 1 or self.appPrefs['identifyFlows'] == 0: 1336 | q_name = ip_protocol + addr1 + addr2 + interface 1337 | if q_name in self.flowQueues: 1338 | return self.flowQueues[q_name] 1339 | else: 1340 | self.flowQueues[q_name] = Queue.Queue() 1341 | qt = Thread(target=self.startQueue, args=(self.flowQueues[q_name], t)) 1342 | qt.daemon = True 1343 | qt.start() 1344 | return self.flowQueues[q_name] 1345 | 1346 | #Queues based on packet type, s_addr and d_addr 1347 | else: 1348 | q_name1 = ip_protocol+addr1+addr2 1349 | q_name2 = ip_protocol+addr2+addr1 1350 | if q_name1 in self.flowQueues: 1351 | return self.flowQueues[q_name1] 1352 | elif q_name2 in self.flowQueues: 1353 | return self.flowQueues[q_name2] 1354 | else: 1355 | self.flowQueues[q_name1] = Queue.Queue() 1356 | qt = Thread(target=self.startQueue, args=(self.flowQueues[q_name1],t)) 1357 | qt.daemon = True 1358 | qt.start() 1359 | return self.flowQueues[q_name1] 1360 | 1361 | def startQueue(self, q, t): 1362 | while True: 1363 | thr = q.get() 1364 | # For realtime display, empty queue more frequently to display up-to-date packets 1365 | if self.appPrefs['flowTime'] == 1: 1366 | while not q.empty(): 1367 | q.task_done() 1368 | thr = q.get() 1369 | thr.start() 1370 | 1371 | while thr.isAlive(): 1372 | time.sleep(t) 1373 | pass 1374 | try: 1375 | q.task_done() 1376 | except: 1377 | pass 1378 | 1379 | def clearQueue( self, qu=None): 1380 | "To clear queue and remove all enqueued packets" 1381 | if qu is None: 1382 | for q in self.flowQueues: 1383 | while not self.flowQueues[q].empty(): 1384 | self.flowQueues[q].task_done() 1385 | self.flowQueues[q].get() 1386 | else: 1387 | while not qu.empty(): 1388 | qu.task_done() 1389 | qu.get() 1390 | 1391 | # Canvas 1392 | 1393 | def createCanvas( self ): 1394 | "Create and return our scrolling canvas frame." 1395 | f = Frame( self ) 1396 | 1397 | canvas = Canvas( f, width=self.cwidth, height=self.cheight, 1398 | bg=self.bg ) 1399 | 1400 | # Scroll bars 1401 | xbar = Scrollbar( f, orient='horizontal', command=canvas.xview ) 1402 | ybar = Scrollbar( f, orient='vertical', command=canvas.yview ) 1403 | canvas.configure( xscrollcommand=xbar.set, yscrollcommand=ybar.set ) 1404 | 1405 | # Resize box 1406 | resize = Label( f, bg='white' ) 1407 | 1408 | # Layout 1409 | canvas.grid( row=0, column=1, sticky='nsew') 1410 | ybar.grid( row=0, column=2, sticky='ns') 1411 | xbar.grid( row=1, column=1, sticky='ew' ) 1412 | resize.grid( row=1, column=2, sticky='nsew' ) 1413 | 1414 | 1415 | # Resize behavior 1416 | f.rowconfigure( 0, weight=1 ) 1417 | f.columnconfigure( 1, weight=1 ) 1418 | f.grid( row=0, column=0, sticky='nsew' ) 1419 | f.bind( '', lambda event: self.updateScrollRegion() ) 1420 | 1421 | return f, canvas 1422 | 1423 | def updateScrollRegion( self ): 1424 | "Update canvas scroll region to hold everything." 1425 | bbox = self.canvas.bbox( 'all' ) 1426 | if bbox is not None: 1427 | self.canvas.configure( scrollregion=( 0, 0, bbox[ 2 ], 1428 | bbox[ 3 ] ) ) 1429 | 1430 | def canvasx( self, x_root ): 1431 | "Convert root x coordinate to canvas coordinate." 1432 | c = self.canvas 1433 | return c.canvasx( x_root ) - c.winfo_rootx() 1434 | 1435 | def canvasy( self, y_root ): 1436 | "Convert root y coordinate to canvas coordinate." 1437 | c = self.canvas 1438 | return c.canvasy( y_root ) - c.winfo_rooty() 1439 | 1440 | def popupFocusOut(self, event=None): 1441 | event.widget.unpost() 1442 | 1443 | def setFocus(self, event=None): 1444 | event.widget.focus_set() 1445 | 1446 | # Generic node handlers 1447 | 1448 | def findWidgetByName( self, name ): 1449 | for widget in self.widgetToItem: 1450 | if name == widget[ 'text' ]: 1451 | return widget 1452 | 1453 | def findItem( self, x, y ): 1454 | "Find items at a location in our canvas." 1455 | items = self.canvas.find_overlapping( x, y, x, y ) 1456 | if len( items ) == 0: 1457 | return None 1458 | else: 1459 | return items[ 0 ] 1460 | 1461 | def nodeIcon( self, node, name , color): 1462 | "Create a new node icon." 1463 | icon = Button( self.canvas, image=self.images[ node ], 1464 | text=name, compound='top' ) 1465 | icon.config(highlightbackground=color, highlightcolor=color, highlightthickness=3) 1466 | # Unfortunately bindtags wants a tuple 1467 | bindtags = [ str( self.nodeBindings ) ] 1468 | bindtags += list( icon.bindtags() ) 1469 | icon.bindtags( tuple( bindtags ) ) 1470 | return icon 1471 | 1472 | def randColor(self): 1473 | 1474 | i =0; 1475 | color = self.HOST_COLORS[i] 1476 | try: 1477 | while color in [node['color'] for node in self.Nodes]: 1478 | i+=1 1479 | color = self.HOST_COLORS[i] 1480 | except: 1481 | color = "#" + ("%06x" % random.randint(0, 16777215)) 1482 | while any(color in sublist for sublist in self.Nodes): 1483 | color = "#" + ("%06x" % random.randint(0, 16777215)) 1484 | 1485 | return color 1486 | 1487 | def newNamedNode(self, Node, x, y): 1488 | name = Node['name'] 1489 | type = Node['type'] 1490 | color = None 1491 | #Add a new node to our canvas. 1492 | c = self.canvas 1493 | 1494 | if type in LEGACY_TYPES: 1495 | if 'ROUTER' in type.upper(): 1496 | node = 'LegacyRouter' 1497 | else: 1498 | node = 'LegacySwitch' 1499 | 1500 | elif type in SWITCHES_TYPES: 1501 | node = 'Switch' 1502 | 1503 | elif type in HOSTS_TYPES: 1504 | node = 'Host' 1505 | color = self.randColor() 1506 | elif type in CONTROLLERS_TYPES: 1507 | node = 'Controller' 1508 | color = self.Controller_Color 1509 | else: 1510 | print "Specify node type for ", Node['name'] 1511 | 1512 | 1513 | icon = self.nodeIcon(node, name, color) 1514 | item = self.canvas.create_window(x, y, anchor='c', window=icon, 1515 | tags=node) 1516 | self.widgetToItem[icon] = item 1517 | self.itemToWidget[item] = icon 1518 | self.selectItem(item) 1519 | icon.links = {} 1520 | 1521 | Node['color'] , Node['widget'] = color, item 1522 | 1523 | self.showNodeStats(icon) 1524 | icon.bind('', self.setFocus) 1525 | if 'Switch' == node: 1526 | icon.bind('', self.do_switchPopup) 1527 | if 'LegacyRouter' == node: 1528 | icon.bind('', self.do_legacyRouterPopup) 1529 | if 'LegacySwitch' == node: 1530 | icon.bind('', self.do_legacySwitchPopup) 1531 | if 'Host' == node: 1532 | icon.bind('', self.do_hostPopup) 1533 | if 'Controller' == node: 1534 | icon.bind('', self.do_controllerPopup) 1535 | self.updateScrollRegion() 1536 | 1537 | def createNodeBindings( self ): 1538 | "Create a set of bindings for nodes." 1539 | bindings = { 1540 | '': self.selectNode, 1541 | '': self.dragNodeAround, 1542 | '': self.enterNode, 1543 | '': self.leaveNode 1544 | } 1545 | l = Label() # lightweight-ish owner for bindings 1546 | for event, binding in bindings.items(): 1547 | l.bind( event, binding ) 1548 | return l 1549 | 1550 | def selectItem( self, item ): 1551 | "Select an item and remember old selection." 1552 | self.lastSelection = self.selection 1553 | self.selection = item 1554 | 1555 | def enterNode( self, event ): 1556 | "Select node on entry." 1557 | self.selectNode( event ) 1558 | 1559 | def leaveNode( self, _event ): 1560 | "Restore old selection on exit." 1561 | self.selectItem( self.lastSelection ) 1562 | 1563 | def showNodeStats(self, widget): 1564 | nodeStats = NodeStats(widget) 1565 | def enter(_event): 1566 | if self.appPrefs['showNodeStats'] == 0: 1567 | return 1568 | 1569 | TXP = 0; TXB = 0; RXP = 0; RXB = 0 1570 | node = widget[ 'text' ] 1571 | for data in self.intfData: 1572 | if data['node'] == node: 1573 | TXP += data['TXP'];TXB+=data['TXB']; RXP+=data['RXP']; RXB+=data['RXB']; 1574 | text = "TXP: " + str(TXP) + "\n" + "RXP: " + str(RXP) + "\n" + "TXB: " + str(TXB) + "\n" + "RXB: " + str(RXB) 1575 | nodeStats.showtip(text) 1576 | def leave(_event): 1577 | nodeStats.hidetip() 1578 | widget.bind('', enter) 1579 | widget.bind('', leave) 1580 | 1581 | # Specific node handlers 1582 | 1583 | def selectNode( self, event ): 1584 | "Select the node that was clicked on." 1585 | item = self.widgetToItem.get( event.widget, None ) 1586 | self.selectItem( item ) 1587 | 1588 | def dragNodeAround( self, event ): 1589 | "Drag a node around on the canvas." 1590 | c = self.canvas 1591 | # Convert global to local coordinates; 1592 | # Necessary since x, y are widget-relative 1593 | x = self.canvasx( event.x_root ) 1594 | y = self.canvasy( event.y_root ) 1595 | w = event.widget 1596 | # Adjust node position 1597 | item = self.widgetToItem[ w ] 1598 | c.coords( item, x, y ) 1599 | # Adjust link positions 1600 | for dest in w.links: 1601 | link = w.links[ dest ] 1602 | item = self.widgetToItem[ dest ] 1603 | x1, y1 = c.coords( item ) 1604 | c.coords( link, x, y, x1, y1 ) 1605 | self.updateScrollRegion() 1606 | 1607 | def createControlLinkBindings( self ): 1608 | "Create a set of bindings for nodes." 1609 | # Link bindings 1610 | # Selection still needs a bit of work overall 1611 | # Callbacks ignore event 1612 | 1613 | def select( _event, link=self.link ): 1614 | "Select item on mouse entry." 1615 | self.selectItem( link ) 1616 | 1617 | def highlight( _event, link=self.link ): 1618 | "Highlight item on mouse entry." 1619 | self.selectItem( link ) 1620 | self.canvas.itemconfig( link, fill='green' ) 1621 | 1622 | def unhighlight( _event, link=self.link ): 1623 | "Unhighlight item on mouse exit." 1624 | self.canvas.itemconfig( link, fill='red' ) 1625 | 1626 | self.canvas.tag_bind( self.link, '', highlight ) 1627 | self.canvas.tag_bind( self.link, '', unhighlight ) 1628 | self.canvas.tag_bind( self.link, '', select ) 1629 | 1630 | def createDataLinkBindings( self ): 1631 | "Create a set of bindings for nodes." 1632 | # Link bindings 1633 | # Selection still needs a bit of work overall 1634 | # Callbacks ignore event 1635 | 1636 | def select( _event, link=self.link ): 1637 | "Select item on mouse entry." 1638 | self.selectItem( link ) 1639 | self.canvas.focus_set() 1640 | 1641 | 1642 | def highlight( _event, link=self.link ): 1643 | "Highlight item on mouse entry." 1644 | self.selectItem( link ) 1645 | self.canvas.itemconfig( link, fill='green' ) 1646 | 1647 | def unhighlight( _event, link=self.link ): 1648 | "Unhighlight item on mouse exit." 1649 | self.canvas.itemconfig( link, fill='blue' ) 1650 | #self.selectItem( None ) 1651 | 1652 | self.canvas.tag_bind( self.link, '', highlight ) 1653 | self.canvas.tag_bind( self.link, '', unhighlight ) 1654 | self.canvas.tag_bind( self.link, '', select ) 1655 | self.canvas.tag_bind( self.link, '', self.do_linkPopup ) 1656 | 1657 | def drawLink( self, src, dst ): 1658 | 1659 | "Finish creating a link" 1660 | w = self.findWidgetByName(src) 1661 | item = self.widgetToItem[ w ] 1662 | 1663 | x, y = self.canvas.coords( item ) 1664 | self.link = self.canvas.create_line( x, y, x, y, width=4, 1665 | fill='blue', tag='link' ) 1666 | self.linkx, self.linky = x, y 1667 | self.linkWidget = w 1668 | self.linkItem = item 1669 | 1670 | source = self.linkWidget 1671 | c = self.canvas 1672 | 1673 | dest = self.findWidgetByName(dst) 1674 | x, y = c.coords(self.widgetToItem[dest]) 1675 | 1676 | if ( source is None or dest is None or source == dest 1677 | or dest in source.links or source in dest.links ): 1678 | return 1679 | # For now, don't allow hosts to be directly linked 1680 | stags = self.canvas.gettags( self.widgetToItem[ source ] ) 1681 | dtags = self.canvas.gettags( self.widgetToItem[ dest] ) 1682 | if (('Host' in stags and 'Host' in dtags) or 1683 | ('Controller' in dtags and 'LegacyRouter' in stags) or 1684 | ('Controller' in stags and 'LegacyRouter' in dtags) or 1685 | ('Controller' in dtags and 'LegacySwitch' in stags) or 1686 | ('Controller' in stags and 'LegacySwitch' in dtags) or 1687 | ('Controller' in dtags and 'Host' in stags) or 1688 | ('Controller' in stags and 'Host' in dtags) or 1689 | ('Controller' in stags and 'Controller' in dtags)): 1690 | return 1691 | 1692 | if 'Controller' in stags or 'Controller' in dtags: 1693 | linkType='control' 1694 | c.itemconfig(self.link, dash=(6, 4, 2, 4), fill='red') 1695 | self.createControlLinkBindings() 1696 | else: 1697 | linkType='data' 1698 | self.createDataLinkBindings() 1699 | c.itemconfig(self.link, tags=c.gettags(self.link)+(linkType,)) 1700 | 1701 | x, y = c.coords( self.widgetToItem[dest] ) 1702 | c.coords( self.link, self.linkx, self.linky, x, y ) 1703 | self.addLink( source, dest, linktype=linkType ) 1704 | 1705 | # We're done 1706 | self.link = self.linkWidget = None 1707 | 1708 | def addLink(self, source, dest, linktype='data', linkopts=None): 1709 | "Add link to model." 1710 | if linkopts is None: 1711 | linkopts = {} 1712 | source.links[dest] = self.link 1713 | dest.links[source] = self.link 1714 | self.links[self.link] = {'type': linktype, 1715 | 'src': source, 1716 | 'dest': dest, 1717 | 'linkOpts': linkopts} 1718 | 1719 | # Menu handlers 1720 | 1721 | def createMenubar( self ): 1722 | "Create our menu bar." 1723 | 1724 | font = self.font 1725 | 1726 | mbar = Menu( self.top, font=font ) 1727 | self.top.configure( menu=mbar ) 1728 | 1729 | fileMenu = Menu( mbar, tearoff=False ) 1730 | mbar.add_cascade( label="File", font=font, menu=fileMenu ) 1731 | fileMenu.add_command( label="Load Prefs.", font=font, command=self.loadPrefs ) 1732 | fileMenu.add_command( label="Save Prefs.", font=font, command=self.savePrefs ) 1733 | fileMenu.add_separator() 1734 | fileMenu.add_command( label='Quit', command=self.quit, font=font ) 1735 | 1736 | editMenu = Menu( mbar, tearoff=False ) 1737 | mbar.add_cascade( label="Edit", font=font, menu=editMenu ) 1738 | editMenu.add_command( label="Preferences", font=font, command=self.prefDetails) 1739 | editMenu.add_command( label="Filters", font=font, command=self.filterDetails) 1740 | 1741 | runMenu = Menu( mbar, tearoff=False ) 1742 | mbar.add_cascade( label="Run", font=font, menu=runMenu ) 1743 | runMenu.add_command( label="Pause", font=font, command=lambda: self.doRun(runMenu) ) 1744 | runMenu.add_command( label="Clear", font=font, command=self.clearQueue ) 1745 | fileMenu.add_separator() 1746 | runMenu.add_command( label='Show Interfaces Summary', font=font, command=self.intfInfo ) 1747 | runMenu.add_command( label='Show OVS Summary', font=font, command=self.ovsShow ) 1748 | runMenu.add_command( label='Root Terminal', font=font, command=self.rootTerminal ) 1749 | 1750 | # Application menu 1751 | appMenu = Menu( mbar, tearoff=False ) 1752 | mbar.add_cascade( label="Help", font=font, menu=appMenu ) 1753 | appMenu.add_command( label='About MiniNAM', command=self.about, 1754 | font=font) 1755 | 1756 | def convertJsonUnicode(self, text): 1757 | "Some part of Mininet doesn't like Unicode" 1758 | if isinstance(text, dict): 1759 | return {self.convertJsonUnicode(key): self.convertJsonUnicode(value) for key, value in text.iteritems()} 1760 | elif isinstance(text, list): 1761 | return [self.convertJsonUnicode(element) for element in text] 1762 | elif isinstance(text, unicode): 1763 | return text.encode('utf-8') 1764 | else: 1765 | return text 1766 | 1767 | def savePrefs( self ): 1768 | "Save preferences and filters." 1769 | myFormats = [ 1770 | ('Config File','*.config'), 1771 | ('All Files','*'), 1772 | ] 1773 | savingDictionary = {} 1774 | fileName = tkFileDialog.asksaveasfilename(filetypes=myFormats ,title="Save preferences and filters as...") 1775 | if len(fileName ) > 0: 1776 | # Save Application preferences 1777 | savingDictionary['preferences'] = self.appPrefs 1778 | # Save Application filters 1779 | savingDictionary['filters'] = self.appFilters 1780 | 1781 | try: 1782 | f = open(fileName, 'wb') 1783 | f.write(json.dumps(savingDictionary, sort_keys=True, indent=4, separators=(',', ': '))) 1784 | # pylint: disable=broad-except 1785 | except Exception as er: 1786 | print er 1787 | # pylint: enable=broad-except 1788 | finally: 1789 | f.close() 1790 | 1791 | def loadPrefs( self ): 1792 | "Load command." 1793 | c = self.canvas 1794 | 1795 | myFormats = [ 1796 | ('Config File','*.config'), 1797 | ('All Files','*'), 1798 | ] 1799 | f = tkFileDialog.askopenfile(filetypes=myFormats, mode='rb') 1800 | if f == None: 1801 | return 1802 | 1803 | loadedPrefs = self.convertJsonUnicode(json.load(f)) 1804 | # Load application preferences 1805 | if 'preferences' in loadedPrefs: 1806 | self.appPrefs = dict(self.appPrefs.items() + loadedPrefs['preferences'].items()) 1807 | # Load application filters 1808 | if 'filters' in loadedPrefs: 1809 | self.appFilters = dict(self.appFilters.items() + loadedPrefs['filters'].items()) 1810 | f.close() 1811 | 1812 | def printdata( self ): 1813 | #Convienience function to print interface data while developing 1814 | keys = ["node", "type", "interface", "ip", "dgw", "link", "TXP", "RXP", "TXB", "RXB", "mac"] 1815 | row_format = "{:>15}" * (len(keys) + 1) 1816 | print row_format.format("", *keys) 1817 | for data in self.intfData: 1818 | list = [] 1819 | for key in keys: 1820 | try: 1821 | list.append(data[key]) 1822 | except: 1823 | list.append("") 1824 | print row_format.format("", *list) 1825 | 1826 | def doRun( self , menu): 1827 | "Run command." 1828 | if self.active: 1829 | self.active = False 1830 | menu.entryconfigure(0, label="Resume") 1831 | else: 1832 | self.active = True 1833 | menu.entryconfigure(0, label="Pause") 1834 | 1835 | def about( self ): 1836 | "Display about box." 1837 | about = self.aboutBox 1838 | if about is None: 1839 | bg = 'white' 1840 | about = Toplevel( bg='white' ) 1841 | about.title( 'About' ) 1842 | desc = self.appName + ': a real-time network animator for Mininet' 1843 | version = 'MiniNAM ' + MININAM_VERSION 1844 | author = 'Originally by: Ahmed Khalid , July 2016' 1845 | enhancements = 'Enhancements by: Ahmed Khalid, Nov 2017' 1846 | www = 'https://www.ucc.ie/en/misl/research/software/mininam/' 1847 | line1 = Label( about, text=desc, font='Helvetica 10 bold', bg=bg ) 1848 | line2 = Label( about, text=version, font='Helvetica 9', bg=bg ) 1849 | line3 = Label( about, text=author, font='Helvetica 9', bg=bg ) 1850 | line4 = Label( about, text=enhancements, font='Helvetica 9', bg=bg ) 1851 | line5 = Entry( about, font='Helvetica 9', bg=bg, width=len(www), justify=CENTER ) 1852 | line5.insert(0, www) 1853 | line5.configure(state='readonly') 1854 | line1.pack( padx=20, pady=10 ) 1855 | line2.pack(pady=10 ) 1856 | line3.pack(pady=10 ) 1857 | line4.pack(pady=10 ) 1858 | line5.pack(pady=10 ) 1859 | hide = ( lambda about=about: about.withdraw() ) 1860 | self.aboutBox = about 1861 | # Hide on close rather than destroying window 1862 | Wm.wm_protocol( about, name='WM_DELETE_WINDOW', func=hide ) 1863 | # Show (existing) window 1864 | about.deiconify() 1865 | 1866 | def linkUp( self ): 1867 | if ( self.selection is None or 1868 | self.net is None): 1869 | return 1870 | link = self.selection 1871 | linkDetail = self.links[link] 1872 | src = linkDetail['src'] 1873 | dst = linkDetail['dest'] 1874 | srcName, dstName = src[ 'text' ], dst[ 'text' ] 1875 | self.net.configLinkStatus(srcName, dstName, 'up') 1876 | self.canvas.itemconfig(link, dash=()) 1877 | 1878 | def linkDown( self ): 1879 | if ( self.selection is None or 1880 | self.net is None): 1881 | return 1882 | link = self.selection 1883 | linkDetail = self.links[link] 1884 | src = linkDetail['src'] 1885 | dst = linkDetail['dest'] 1886 | srcName, dstName = src[ 'text' ], dst[ 'text' ] 1887 | self.net.configLinkStatus(srcName, dstName, 'down') 1888 | self.canvas.itemconfig(link, dash=(4, 4)) 1889 | 1890 | def prefDetails( self ): 1891 | prefDefaults = self.appPrefs 1892 | prefBox = PrefsDialog(self, title='Preferences', prefDefaults=prefDefaults) 1893 | if prefBox.result: 1894 | self.appPrefs = prefBox.result 1895 | if self.appPrefs['startCLI']== 1: 1896 | self.startCLI() 1897 | 1898 | def filterDetails( self ): 1899 | filterDefaults = self.appFilters 1900 | filterBox = FiltersDialog(self, title='Filters', filterDefaults=filterDefaults) 1901 | if filterBox.result: 1902 | self.appFilters = filterBox.result 1903 | 1904 | def listBridge( self, _ignore=None ): 1905 | if ( self.selection is None or 1906 | self.net is None or 1907 | self.selection not in self.itemToWidget ): 1908 | return 1909 | name = self.itemToWidget[ self.selection ][ 'text' ] 1910 | tags = self.canvas.gettags( self.selection ) 1911 | 1912 | if name not in self.net.nameToNode: 1913 | return 1914 | if 'Switch' in tags or 'LegacySwitch' in tags: 1915 | call(["xterm -T 'Bridge Details' -sb -sl 2000 -e 'ovs-vsctl list bridge " + name + "; read -p \"Press Enter to close\"' &"], shell=True) 1916 | 1917 | def intfInfo( self ): 1918 | 1919 | info = self.infoBox 1920 | if info is None: 1921 | info = Toplevel( bg='white') 1922 | info.title( 'Interfaces' ) 1923 | 1924 | columns = 9 1925 | font = 'Helvetica 10 bold' 1926 | widgets = [] 1927 | Label(info, text='Interface', borderwidth=0, font=font, padx=10).grid(row=0, column=0, sticky="nsew", padx=1, pady=1) 1928 | Label(info, text='Linked To', borderwidth=0, font=font, padx=10).grid(row=0, column=1, sticky="nsew", padx=1, pady=1) 1929 | Label(info, text='Node Type', borderwidth=0, font=font, padx=10).grid(row=0, column=2, sticky="nsew", padx=1, pady=1) 1930 | Label(info, text='IP Address', borderwidth=0, font=font, padx=10).grid(row=0, column=3, sticky="nsew", padx=1, pady=1) 1931 | Label(info, text='MAC Address', borderwidth=0, font=font, padx=10).grid(row=0, column=4, sticky="nsew", padx=1, pady=1) 1932 | Label(info, text='TXP', borderwidth=0, font=font, padx=10).grid(row=0, column=5, sticky="nsew", padx=1, pady=1) 1933 | Label(info, text='RXP', borderwidth=0, font=font, padx=10).grid(row=0, column=6, sticky="nsew", padx=1, pady=1) 1934 | Label(info, text='TXB', borderwidth=0, font=font, padx=10).grid(row=0, column=7, sticky="nsew", padx=1, pady=1) 1935 | Label(info, text='RXB', borderwidth=0, font=font, padx=10).grid(row=0, column=8, sticky="nsew", padx=1, pady=1) 1936 | row = 0 1937 | font = 'Helvetica 9' 1938 | infoOrder = ['interface', 'link', 'type', 'ip', 'mac', 'TXP', 'RXP', 'TXB', 'RXB'] 1939 | for data in self.intfData: 1940 | row += 1 1941 | current_row = [] 1942 | for column in range(columns): 1943 | label = Label(info, text=data[infoOrder[column]], borderwidth=0, font=font, padx=5) 1944 | label_name = 'label_'+ infoOrder[column] 1945 | data[label_name] = label 1946 | label.grid(row=row, column=column, sticky="nsew", padx=1, pady=1) 1947 | current_row.append(label) 1948 | widgets.append(current_row) 1949 | 1950 | for column in range(columns): 1951 | info.columnconfigure(column, weight=1) 1952 | # Scroll bars 1953 | ybar = Scrollbar(info, orient='vertical') 1954 | ybar.grid(row=0, rowspan=row+1, column=9, sticky='ns') 1955 | 1956 | hide = (lambda info=info: info.withdraw()) 1957 | self.infoBox = info 1958 | # Hide on close rather than destroying window 1959 | Wm.wm_protocol(info, name='WM_DELETE_WINDOW', func=hide) 1960 | # Show (existing) window 1961 | self.updateIntfInfo() 1962 | info.deiconify() 1963 | 1964 | def updateIntfInfo (self): 1965 | self.printdata() 1966 | items = ['interface', 'link', 'type', 'ip', 'mac', 1967 | 'TXP', 'RXP', 'TXB', 'RXB', ] 1968 | for data in self.intfData: 1969 | for item in items: 1970 | data[str('label_' + item)].config(text=str(data[item])) 1971 | 1972 | def startCLI(self): 1973 | # Don't start a second CLI thread if it already exists 1974 | if self.cli is None: 1975 | self.cli = Thread(target=CLI, args=(self.net,)) 1976 | self.cli.daemon = True 1977 | self.cli.start() 1978 | elif not self.cli.isAlive(): 1979 | self.cli = Thread(target=CLI, args=(self.net,)) 1980 | self.cli.daemon = True 1981 | self.cli.start() 1982 | 1983 | @staticmethod 1984 | def ovsShow( _ignore=None ): 1985 | call(["xterm -T 'OVS Summary' -sb -sl 2000 -e 'ovs-vsctl show; read -p \"Press Enter to close\"' &"], shell=True) 1986 | 1987 | @staticmethod 1988 | def rootTerminal( _ignore=None ): 1989 | call(["xterm -T 'Root Terminal' -sb -sl 2000 &"], shell=True) 1990 | 1991 | def do_linkPopup(self, event): 1992 | 1993 | # display the popup menu 1994 | if self.net: 1995 | try: 1996 | self.linkPopup.post(event.x_root, event.y_root) 1997 | self.linkPopup.focus_set() 1998 | finally: 1999 | # make sure to release the grab (Tk 8.0a1 only) 2000 | self.linkPopup.grab_release() 2001 | 2002 | def do_controllerPopup(self, event): 2003 | # do nothing 2004 | return 2005 | 2006 | def do_legacyRouterPopup(self, event): 2007 | # display the popup menu 2008 | if self.net: 2009 | try: 2010 | self.legacyRouterPopup.post(event.x_root, event.y_root) 2011 | self.legacyRouterPopup.focus_set() 2012 | finally: 2013 | # make sure to release the grab (Tk 8.0a1 only) 2014 | self.legacyRouterPopup.grab_release() 2015 | 2016 | def do_hostPopup(self, event): 2017 | 2018 | # display the popup menu 2019 | if self.net: 2020 | try: 2021 | self.hostPopup.post(event.x_root, event.y_root) 2022 | self.hostPopup.focus_set() 2023 | finally: 2024 | # make sure to release the grab (Tk 8.0a1 only) 2025 | self.hostPopup.grab_release() 2026 | 2027 | def do_legacySwitchPopup(self, event): 2028 | # display the popup menu 2029 | if self.net: 2030 | try: 2031 | self.switchPopup.post(event.x_root, event.y_root) 2032 | self.switchPopup.focus_set() 2033 | 2034 | finally: 2035 | # make sure to release the grab (Tk 8.0a1 only) 2036 | self.switchPopup.grab_release() 2037 | 2038 | def do_switchPopup(self, event): 2039 | # display the popup menu 2040 | if self.net: 2041 | try: 2042 | self.switchPopup.post(event.x_root, event.y_root) 2043 | self.switchPopup.focus_set() 2044 | finally: 2045 | # make sure to release the grab (Tk 8.0a1 only) 2046 | self.switchPopup.grab_release() 2047 | 2048 | # Model interface 2049 | 2050 | def xterm( self, _ignore=None ): 2051 | "Make an xterm when a button is pressed." 2052 | if ( self.selection is None or 2053 | self.net is None or 2054 | self.selection not in self.itemToWidget ): 2055 | return 2056 | name = self.itemToWidget[ self.selection ][ 'text' ] 2057 | if name not in self.net.nameToNode: 2058 | return 2059 | term = makeTerm( self.net.nameToNode[ name ], 'Host', term=self.appPrefs['terminalType'] ) 2060 | if StrictVersion(MININET_VERSION) > StrictVersion('2.0'): 2061 | self.net.terms += term 2062 | else: 2063 | self.net.terms.append(term) 2064 | 2065 | def iperf( self, _ignore=None ): 2066 | "Make an xterm when a button is pressed." 2067 | if ( self.selection is None or 2068 | self.net is None or 2069 | self.selection not in self.itemToWidget ): 2070 | return 2071 | name = self.itemToWidget[ self.selection ][ 'text' ] 2072 | if name not in self.net.nameToNode: 2073 | return 2074 | self.net.nameToNode[ name ].cmd( 'iperf -s -p 5001 &' ) 2075 | 2076 | 2077 | @staticmethod 2078 | def pathCheck( *args, **kwargs ): 2079 | "Make sure each program in *args can be found in $PATH." 2080 | moduleName = kwargs.get( 'moduleName', 'it' ) 2081 | for arg in args: 2082 | if not quietRun( 'which ' + arg ): 2083 | showerror(title="Error", 2084 | message= 'Cannot find required executable %s.\n' % arg + 2085 | 'Please make sure that %s is installed ' % moduleName + 2086 | 'and available in your $PATH.' ) 2087 | 2088 | def stop( self ): 2089 | 2090 | #Reset the terminal even if CLI wasn't exited properly 2091 | os.system('stty sane') 2092 | #Clear the packet queues 2093 | for q in self.flowQueues: 2094 | self.flowQueues[q].queue.clear() 2095 | 2096 | #Stop network. 2097 | if self.net: 2098 | self.net.stop() 2099 | 2100 | cleanUpScreens() 2101 | 2102 | self.net = None 2103 | 2104 | def quit( self ): 2105 | "Stop our network, if any, then quit." 2106 | self.stop() 2107 | 2108 | Frame.quit( self ) 2109 | 2110 | def miniImages(): 2111 | "Create and return images for MiniNAM." 2112 | 2113 | # Image data. Git will be unhappy. However, the alternative 2114 | # is to keep track of separate binary files, which is also 2115 | # unappealing. 2116 | 2117 | return { 2118 | 'Select': BitmapImage( 2119 | file='/usr/include/X11/bitmaps/left_ptr' ), 2120 | 2121 | 'Switch': PhotoImage( data=r""" 2122 | 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 2153 | """), 2154 | 2155 | 'LegacySwitch': PhotoImage( data=r""" 2156 | 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 2187 | 4BE2eIRYeHAEIBwBP0Y4Qn41YWRSCQgAOw== 2188 | """), 2189 | 2190 | 'LegacyRouter': PhotoImage( data=r""" 2191 | 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 2220 | gGPLHwLwcMIo12Qxu0ABAQA7 2221 | """), 2222 | 2223 | 'Controller': PhotoImage( data=r""" 2224 | R0lGODlhMAAwAPcAAAEBAWfNAYWFhcfHx+3t6/f390lJUaWlpfPz8/Hx72lpaZGRke/v77m5uc0B 2225 | AeHh4e/v7WNjY3t7e5eXlyMjI4mJidPT0+3t7f///09PT7Ozs/X19fHx8ZWTk8HBwX9/fwAAAAAA 2226 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 2227 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 2228 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 2229 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 2230 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 2231 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 2232 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 2233 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 2234 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 2235 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 2236 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 2237 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAwADAA 2238 | Bwj/AAEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGAEIeMCxo8ePHwVkBGABg8mTKFOmtDByAIYN 2239 | MGPCRCCzQIENNzEMGOkBAwIKQIMKpYCgKAIHCDB4GNkAA4OnUJ9++CDhQ1QGFzA0GKkBA4GvYMOK 2240 | BYtBA1cNaNOqXcuWq8q3b81m7Cqzbk2bMMu6/Tl0qFEEAZLKxdj1KlSqVA3rnet1rOOwiwmznUzZ 2241 | LdzLJgdfpIv3pmebN2Pm1GyRbocNp1PLNMDaAM3Im1/alQk4gO28pCt2RdCBt+/eRg8IP1AUdmmf 2242 | f5MrL56bYlcOvaP7Xo6Ag3HdGDho3869u/YE1507t+3AgLz58ujPMwg/sTBUCAzgy49PH0LW5u0x 2243 | XFiwvz////5dcJ9bjxVIAHsSdUXAAgs2yOCDDn6FYEQaFGDgYxNCpEFfHHKIX4IDhCjiiCSS+CGF 2244 | FlCmogYpcnVABTDGKGOMAlRQYwUHnKjhAjX2aOOPN8LImgAL6PiQBhLMqCSNAThQgQRGOqRBBD1W 2245 | aaOVAggnQARRNqRBBxmEKeaYZIrZQZcMKbDiigqM5OabcMYp55x01ilnQAA7 2246 | """), 2247 | 2248 | 'Host': PhotoImage( data=r""" 2249 | R0lGODlhIAAYAPcAMf//////zP//mf//Zv//M///AP/M///MzP/M 2250 | mf/MZv/MM//MAP+Z//+ZzP+Zmf+ZZv+ZM/+ZAP9m//9mzP9mmf9m 2251 | Zv9mM/9mAP8z//8zzP8zmf8zZv8zM/8zAP8A//8AzP8Amf8AZv8A 2252 | M/8AAMz//8z/zMz/mcz/Zsz/M8z/AMzM/8zMzMzMmczMZszMM8zM 2253 | AMyZ/8yZzMyZmcyZZsyZM8yZAMxm/8xmzMxmmcxmZsxmM8xmAMwz 2254 | /8wzzMwzmcwzZswzM8wzAMwA/8wAzMwAmcwAZswAM8wAAJn//5n/ 2255 | zJn/mZn/Zpn/M5n/AJnM/5nMzJnMmZnMZpnMM5nMAJmZ/5mZzJmZ 2256 | mZmZZpmZM5mZAJlm/5lmzJlmmZlmZplmM5lmAJkz/5kzzJkzmZkz 2257 | ZpkzM5kzAJkA/5kAzJkAmZkAZpkAM5kAAGb//2b/zGb/mWb/Zmb/ 2258 | M2b/AGbM/2bMzGbMmWbMZmbMM2bMAGaZ/2aZzGaZmWaZZmaZM2aZ 2259 | AGZm/2ZmzGZmmWZmZmZmM2ZmAGYz/2YzzGYzmWYzZmYzM2YzAGYA 2260 | /2YAzGYAmWYAZmYAM2YAADP//zP/zDP/mTP/ZjP/MzP/ADPM/zPM 2261 | zDPMmTPMZjPMMzPMADOZ/zOZzDOZmTOZZjOZMzOZADNm/zNmzDNm 2262 | mTNmZjNmMzNmADMz/zMzzDMzmTMzZjMzMzMzADMA/zMAzDMAmTMA 2263 | ZjMAMzMAAAD//wD/zAD/mQD/ZgD/MwD/AADM/wDMzADMmQDMZgDM 2264 | MwDMAACZ/wCZzACZmQCZZgCZMwCZAABm/wBmzABmmQBmZgBmMwBm 2265 | AAAz/wAzzAAzmQAzZgAzMwAzAAAA/wAAzAAAmQAAZgAAM+4AAN0A 2266 | ALsAAKoAAIgAAHcAAFUAAEQAACIAABEAAADuAADdAAC7AACqAACI 2267 | AAB3AABVAABEAAAiAAARAAAA7gAA3QAAuwAAqgAAiAAAdwAAVQAA 2268 | RAAAIgAAEe7u7t3d3bu7u6qqqoiIiHd3d1VVVURERCIiIhEREQAA 2269 | ACH5BAEAAAAALAAAAAAgABgAAAiNAAH8G0iwoMGDCAcKTMiw4UBw 2270 | BPXVm0ixosWLFvVBHFjPoUeC9Tb+6/jRY0iQ/8iVbHiS40CVKxG2 2271 | HEkQZsyCM0mmvGkw50uePUV2tEnOZkyfQA8iTYpTKNOgKJ+C3AhO 2272 | p9SWVaVOfWj1KdauTL9q5UgVbFKsEjGqXVtP40NwcBnCjXtw7tx/ 2273 | C8cSBBAQADs= 2274 | """ ), 2275 | 2276 | 'OldSwitch': PhotoImage( data=r""" 2277 | R0lGODlhIAAYAPcAMf//////zP//mf//Zv//M///AP/M///MzP/M 2278 | mf/MZv/MM//MAP+Z//+ZzP+Zmf+ZZv+ZM/+ZAP9m//9mzP9mmf9m 2279 | Zv9mM/9mAP8z//8zzP8zmf8zZv8zM/8zAP8A//8AzP8Amf8AZv8A 2280 | M/8AAMz//8z/zMz/mcz/Zsz/M8z/AMzM/8zMzMzMmczMZszMM8zM 2281 | AMyZ/8yZzMyZmcyZZsyZM8yZAMxm/8xmzMxmmcxmZsxmM8xmAMwz 2282 | /8wzzMwzmcwzZswzM8wzAMwA/8wAzMwAmcwAZswAM8wAAJn//5n/ 2283 | zJn/mZn/Zpn/M5n/AJnM/5nMzJnMmZnMZpnMM5nMAJmZ/5mZzJmZ 2284 | mZmZZpmZM5mZAJlm/5lmzJlmmZlmZplmM5lmAJkz/5kzzJkzmZkz 2285 | ZpkzM5kzAJkA/5kAzJkAmZkAZpkAM5kAAGb//2b/zGb/mWb/Zmb/ 2286 | M2b/AGbM/2bMzGbMmWbMZmbMM2bMAGaZ/2aZzGaZmWaZZmaZM2aZ 2287 | AGZm/2ZmzGZmmWZmZmZmM2ZmAGYz/2YzzGYzmWYzZmYzM2YzAGYA 2288 | /2YAzGYAmWYAZmYAM2YAADP//zP/zDP/mTP/ZjP/MzP/ADPM/zPM 2289 | zDPMmTPMZjPMMzPMADOZ/zOZzDOZmTOZZjOZMzOZADNm/zNmzDNm 2290 | mTNmZjNmMzNmADMz/zMzzDMzmTMzZjMzMzMzADMA/zMAzDMAmTMA 2291 | ZjMAMzMAAAD//wD/zAD/mQD/ZgD/MwD/AADM/wDMzADMmQDMZgDM 2292 | MwDMAACZ/wCZzACZmQCZZgCZMwCZAABm/wBmzABmmQBmZgBmMwBm 2293 | AAAz/wAzzAAzmQAzZgAzMwAzAAAA/wAAzAAAmQAAZgAAM+4AAN0A 2294 | ALsAAKoAAIgAAHcAAFUAAEQAACIAABEAAADuAADdAAC7AACqAACI 2295 | AAB3AABVAABEAAAiAAARAAAA7gAA3QAAuwAAqgAAiAAAdwAAVQAA 2296 | RAAAIgAAEe7u7t3d3bu7u6qqqoiIiHd3d1VVVURERCIiIhEREQAA 2297 | ACH5BAEAAAAALAAAAAAgABgAAAhwAAEIHEiwoMGDCBMqXMiwocOH 2298 | ECNKnEixosWB3zJq3Mixo0eNAL7xG0mypMmTKPl9Cznyn8uWL/m5 2299 | /AeTpsyYI1eKlBnO5r+eLYHy9Ck0J8ubPmPOrMmUpM6UUKMa/Ui1 2300 | 6saLWLNq3cq1q9evYB0GBAA7 2301 | """ ), 2302 | 2303 | 'NetLink': PhotoImage( data=r""" 2304 | R0lGODlhFgAWAPcAMf//////zP//mf//Zv//M///AP/M///MzP/M 2305 | mf/MZv/MM//MAP+Z//+ZzP+Zmf+ZZv+ZM/+ZAP9m//9mzP9mmf9m 2306 | Zv9mM/9mAP8z//8zzP8zmf8zZv8zM/8zAP8A//8AzP8Amf8AZv8A 2307 | M/8AAMz//8z/zMz/mcz/Zsz/M8z/AMzM/8zMzMzMmczMZszMM8zM 2308 | AMyZ/8yZzMyZmcyZZsyZM8yZAMxm/8xmzMxmmcxmZsxmM8xmAMwz 2309 | /8wzzMwzmcwzZswzM8wzAMwA/8wAzMwAmcwAZswAM8wAAJn//5n/ 2310 | zJn/mZn/Zpn/M5n/AJnM/5nMzJnMmZnMZpnMM5nMAJmZ/5mZzJmZ 2311 | mZmZZpmZM5mZAJlm/5lmzJlmmZlmZplmM5lmAJkz/5kzzJkzmZkz 2312 | ZpkzM5kzAJkA/5kAzJkAmZkAZpkAM5kAAGb//2b/zGb/mWb/Zmb/ 2313 | M2b/AGbM/2bMzGbMmWbMZmbMM2bMAGaZ/2aZzGaZmWaZZmaZM2aZ 2314 | AGZm/2ZmzGZmmWZmZmZmM2ZmAGYz/2YzzGYzmWYzZmYzM2YzAGYA 2315 | /2YAzGYAmWYAZmYAM2YAADP//zP/zDP/mTP/ZjP/MzP/ADPM/zPM 2316 | zDPMmTPMZjPMMzPMADOZ/zOZzDOZmTOZZjOZMzOZADNm/zNmzDNm 2317 | mTNmZjNmMzNmADMz/zMzzDMzmTMzZjMzMzMzADMA/zMAzDMAmTMA 2318 | ZjMAMzMAAAD//wD/zAD/mQD/ZgD/MwD/AADM/wDMzADMmQDMZgDM 2319 | MwDMAACZ/wCZzACZmQCZZgCZMwCZAABm/wBmzABmmQBmZgBmMwBm 2320 | AAAz/wAzzAAzmQAzZgAzMwAzAAAA/wAAzAAAmQAAZgAAM+4AAN0A 2321 | ALsAAKoAAIgAAHcAAFUAAEQAACIAABEAAADuAADdAAC7AACqAACI 2322 | AAB3AABVAABEAAAiAAARAAAA7gAA3QAAuwAAqgAAiAAAdwAAVQAA 2323 | RAAAIgAAEe7u7t3d3bu7u6qqqoiIiHd3d1VVVURERCIiIhEREQAA 2324 | ACH5BAEAAAAALAAAAAAWABYAAAhIAAEIHEiwoEGBrhIeXEgwoUKG 2325 | Cx0+hGhQoiuKBy1irChxY0GNHgeCDAlgZEiTHlFuVImRJUWXEGEy 2326 | lBmxI8mSNknm1Dnx5sCAADs= 2327 | """ ), 2328 | 2329 | 'Logo': PhotoImage( 2330 | data="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") 2331 | 2332 | } 2333 | 2334 | if __name__ == "__main__": 2335 | try: 2336 | 2337 | app = MiniNAM() 2338 | 2339 | app.mainloop() 2340 | 2341 | except KeyboardInterrupt: 2342 | info( "\n\nKeyboard Interrupt. Shutting down and cleaning up...\n\n") 2343 | app.stop() 2344 | 2345 | except Exception: 2346 | # Print exception 2347 | type_, val_, trace_ = sys.exc_info() 2348 | line = sys.exc_info()[-1].tb_lineno 2349 | errorMsg = ("-" * 80 + "\n" + 2350 | "Caught exception on line %d." % (line) + 2351 | " Cleaning up...\n\n" + "%s: %s\n" % (type_.__name__, val_) + 2352 | "-" * 80 + "\n") 2353 | error(errorMsg) 2354 | # Print stack trace to debug log 2355 | import traceback 2356 | 2357 | stackTrace = traceback.format_exc() 2358 | debug(stackTrace + "\n") 2359 | app.stop() 2360 | 2361 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MiniNAM: A Network Animator for Mininet 2 | 3 | A Network Animator for Visualizing Real-Time Packet Flows in Mininet 4 | 5 | MiniNAM 1.0.1 6 | 7 | ### What is MiniNAM? 8 | 9 | MiniNAM is a GUI based tool written in Python Tkinter. It provides real-time animation 10 | of any network created by the Mininet emulator. It includes all the components required 11 | to initiate, visualize and modify Mininet network flows in real-time. 12 | 13 | MiniNAM allows dynamic modification of preferences and packet filters: a user can view 14 | selective flows with options to color code packets based on the source/destination node 15 | and/or packet type. This establishes MiniNAM as a very powerful tool for debugging network 16 | protocols or teaching, learning and understanding network concepts. 17 | 18 | ### How to start MiniNAM? 19 | 20 | There are three ways to start MiniNAM: 21 | 22 | * Start MiniNAM from CLI with a single command. E.g. 23 | 24 | `sudo python MiniNAM.py --topo tree,depth=2,fanout=2 --controller remote` 25 | 26 | To make MiniNAM easier for Mininet users, arguments of MiniNAM have been kept the same 27 | as Mininet. This means that all the arguments that can be passed to `mn` utility can be 28 | passed to MiniNAM too. A list of these arguments can be seen in help: 29 | 30 | `sudo python MiniNAM.py --help` 31 | 32 | * Start MiniNAM from CLI and pass custom files. E.g. 33 | 34 | `sudo python MiniNAM.py --custom ` 35 | `sudo python MiniNAM.py --custom --topo ` 36 | 37 | In addition to custom instances that Mininet takes, MiniNAM can also take a network 38 | instance. The network instance in custom script must be named net (upper or lower case) 39 | 40 | `sudo python MiniNAM.py --custom net` 41 | 42 | * Import MiniNAM in your code and create an instance. The *init* function of MiniNAM takes 43 | the following arguments: 44 | `__init__( self, parent=None, cheight=600, cwidth=1000 , net= None, locations={})` 45 | 46 | Threading might be needed for this because MiniNAM will start a Tkinter GUI which should 47 | run as a main thread. If you have code that you want to run in parallel, use threading. 48 | 49 | ### How does MiniNAM work? 50 | 51 | When MiniNAM is launched, it starts or loads the Mininet network instance. It then starts 52 | two threads. One to sniff packets on all the network interfaces created by Mininet and the 53 | second for the Tkinter GUI. The GUI displays network nodes and links. If a packet is sniffed 54 | at any interface, it is displayed over the relevant link in GUI after applying user-specific 55 | preferences and filters. 56 | 57 | The speed of packet flow can be decreased, if needed, for better visibility. As there can be 58 | more than one flow in the network at a particular time, MiniNAM tries to identify packets 59 | that belong to the same flow and adds those packets to a separate FIFO queue for each flow. 60 | In this way those packets are displayed one after another, providing a more representable 61 | view. By default, MiniNAM uses packet type, source and destination address to identify flows. 62 | This should work for most legacy protocols such as pings, iperf etc. However if flows in your 63 | protocol or network do not just rely on IP addresses, you can modify the flow identification 64 | process of MiniNAM by modifying the `getQueue()` function, to suit your needs. 65 | 66 | ### Features 67 | 68 | * Run programs to generate and monitor traffic in real-time using host terminals. 69 | 70 | * Set preferences via `Edit->Preferences` to customize the packet flows: 71 | 72 | * Adjust the speed of packet flows. 73 | 74 | * Hide hosts in the topology if there are too many nodes (only at start-up). 75 | 76 | * Color code packets based on the source or destination nodes. 77 | 78 | * Color code packets based on the packet type. 79 | 80 | * Show IP address (first and last octet) on packets. 81 | 82 | * Show a live statistics box with each node on mouse hover. 83 | 84 | * Filter out certain packets based on packet type, IP and/or MAC address via `Edit->Filters`. 85 | 86 | * Easily save and load preferences and filters using `File` menu. 87 | 88 | * Pause the flow display at any time using `Run->Pause`. 89 | 90 | * Set a link down and back up again in run-time by right-clicking a link and choosing options. 91 | 92 | * Check statistics of every interface in the network via `Run->Show Interfaces Summary`. 93 | 94 | ### New features in this release 95 | 96 | The first release of MiniNAM was just a binary file. This is the first source code release. 97 | Apart from that this is a performance improvement and a bug fix release. 98 | 99 | * Issue of creating networks with custom switches when passing a custom file has been fixed. 100 | 101 | * Pop-up menus have been fixed to not disappear on focus out. 102 | 103 | * Fixed the issue with typing feedback gone in terminal, if CLI wasn't exited properly. 104 | 105 | * The link-delay detection mechanism has been removed. You can modify the source code to 106 | load link-delay values from your Mininet script if you want. 107 | 108 | * Additional options have been added to the preference menu. 109 | 110 | * Added option to import MiniNAM in custom code/script and create its instance. 111 | 112 | ### Installation 113 | 114 | MiniNAM is a GUI tool written in Python2.7 with Tkinter and Mininet's Python API. This means 115 | that it requires a DISPLAY environment to run. If you are using Mininet VM via SSH then make 116 | sure to have X forwarding enabled. Also make sure to have Tkinter imaging installed: 117 | 118 | `sudo apt-get install python-imaging` 119 | 120 | Depending on your version of linux, you may need to install Tkinter using this call instead: 121 | 122 | `sudo apt-get install python-imaging-tk` 123 | 124 | MiniNAM has no additional dependencies and if you have Mininet installed in your machine 125 | then MiniNAM should work fine too. To install Mininet, you can go through a very helpful 126 | [getting started documentation](http://mininet.org/download/) provided by Mininet. 127 | 128 | ### Documentation 129 | 130 | You can find a tutorial to set up and use MiniNAM at: 131 | 132 | 133 | 134 | The tutorial also includes three distinct network examples that use MiniNAM to create a 135 | network and display traffic flows. These examples are a good starting point to learn how to 136 | use MiniNAM. 137 | 138 | ### Support 139 | 140 | We kindly ask that should you mention MiniNAM, or use our code, in your publication, that 141 | you would reference the following [paper](http://ieeexplore.ieee.org/document/7899417/): 142 | 143 | Ahmed Khalid, Jason J. Quinlan, Cormac J. Sreenan, "MiniNAM: A network animator for 144 | visualizing real-time packet flows in Mininet". In 20th Conference on Innovations in Clouds, 145 | Internet and Networks (ICIN), March 2017 . 146 | 147 | You can send any queries, comments or suggestions to: 148 | 149 | `a.khalid@cs.ucc.ie or mislgit@cs.ucc.ie` 150 | 151 | Whilst we will attempt to provide the best support where possible, we do not guarantee that 152 | any particular support query can, or will be answered to the extent, or within a time frame 153 | that the inquirer is completely satisfied. 154 | 155 | Best wishes, 156 | 157 | Ahmed Khalid 158 | 159 | -------------------------------------------------------------------------------- /conf.config: -------------------------------------------------------------------------------- 1 | { 2 | "filters": { 3 | "hideFromIPMAC": [ 4 | "0.0.0.0", 5 | "255.255.255.255" 6 | ], 7 | "hidePackets": [ 8 | "IPv6" 9 | ], 10 | "hideToIPMAC": [ 11 | "0.0.0.0", 12 | "255.255.255.255" 13 | ], 14 | "showPackets": [ 15 | "TCP", 16 | "UDP", 17 | "ICMP" 18 | ] 19 | }, 20 | "preferences": { 21 | "displayFlows": 1, 22 | "displayHosts": 1, 23 | "flowTime": 5000, 24 | "identifyFlows": 1, 25 | "nodeColors": "Source", 26 | "showAddr": "None", 27 | "showNodeStats": 0, 28 | "startCLI": 1, 29 | "terminalType": "xterm", 30 | "typeColors": { 31 | "ARP": "Red", 32 | "ICMP": "Blue", 33 | "TCP": "Green", 34 | "UDP": "Green" 35 | } 36 | } 37 | } --------------------------------------------------------------------------------