intracom-telecom-sdn/multinet

View on GitHub
net/multinet.py

Summary

Maintainability
D
2 days
Test Coverage
# Copyright (c) 2015 Intracom S.A. Telecom Solutions. All rights reserved.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License v1.0 which accompanies this distribution,
# and is available at http://www.eclipse.org/legal/epl-v10.html

"""
Builds on Mininet to emulate large-scale SDN networks
by creating distributed Mininet topologies
"""

import logging
import time
import mininet
import mininet.util
import mininet.net
import mininet.node
import mininet.link
import mininet.clean
import itertools
import net.topologies
import socket
import struct

logging.basicConfig(level=logging.DEBUG)


class Multinet(mininet.net.Mininet):

    """
    Superclass representing a Mininet topology that is being booted in custom
    way. Switches are being added in groups with certain delay between each
    group.
    """

    """
    name - class correspondence for the topologies
    """
    TOPOS = {
        'disconnected': net.topologies.DisconnectedTopo,
        'linear': net.topologies.LinearTopo,
        'ring': net.topologies.RingTopo,
        'mesh': net.topologies.MeshTopo
    }

    """
    name - class correspondence for the soft switches
    """
    SWITCH_CLASSES = {
        'ovsk': mininet.node.OVSKernelSwitch,
        'user': mininet.node.UserSwitch
    }

    def __init__(self, controller_ip, controller_port, switch_type, topo_type,
                 num_switches, group_size, group_delay_ms, hosts_per_switch,
                 dpid_offset, traffic_generation_duration_ms,
                 interpacket_delay_ms, auto_detect_hosts=False):
        """
        Call the super constructor and initialize any extra properties we want to user

        Args:
            controller_ip (str): The IP address of the RemoteController
            controller_port (int): The OpenFlow port of the RemoteController
            switch_type (str): The type of the soft switch to use for the emulation
            topo_type (str): The type of the topology to build
            num_switches (int): The number of the switches in the topology
            group_size (int): The number of switches in a gorup for gradual bootup
            group_delay_ms (int): The delay between the bootup of each group
            hosts_per_switch (int): The number of hosts connected to each switch
            dpid_offset (int): The dpid offset of this worker
            traffic_generation_duration_ms (int): The interval of time, during
                                                  which transmission of
                                                  Packet_IN's occures
            interpacket_delay_ms (int): The interval of time between 2
                                        Packet_IN transmissions
            auto_detect_hosts (bool): Enable or disable automatic host detection
        """
        self.__network_mask_bits = 16
        self.__base_network = '10.0.0.0'
        self.__network_ip_range = long(2 ** (32 - self.__network_mask_bits))
        self.__available_networks = long(2 ** self.__network_mask_bits -
            (self.ip2long(self.__base_network) / self.__network_ip_range))
        # Initialize the Mininet network of the worker, based on the dpid.
        # Each worker has its own network.
        if dpid_offset <= self.__available_networks:
            self.__mininet_network = self.long2ip(
                self.ip2long(self.__base_network) +
                (dpid_offset * self.__network_ip_range))
        else:
            error('Worker Mininet network is out of range.')
            raise ValueError('Worker Mininet network is out of range.')

        self._topo_type = topo_type
        self._num_switches = num_switches
        self._dpid_offset = dpid_offset
        self._group_size = group_size
        self._group_delay = float(group_delay_ms) / 1000
        self._hosts_per_switch = hosts_per_switch
        self.auto_detect_hosts = auto_detect_hosts
        self._controller_ip = controller_ip
        self._controller_port = controller_port
        self.booted_switches = 0
        self._traffic_generation_duration_ms = traffic_generation_duration_ms
        self._interpacket_delay_ms = interpacket_delay_ms

        super(
            Multinet,
            self).__init__(
            topo=self.TOPOS[
                self._topo_type](
                k=self._num_switches,
                n=self._hosts_per_switch,
                dpid=self._dpid_offset),
            switch=self.SWITCH_CLASSES[switch_type],
            host=mininet.node.Host,
            controller=mininet.node.RemoteController,
            link=mininet.link.Link,
            intf=mininet.link.Intf,
            build=False,
            xterms=False,
            cleanup=False,
            ipBase=self.__mininet_network + '/' + str(self.__network_mask_bits),
            inNamespace=False,
            autoSetMacs=False,
            autoStaticArp=False,
            autoPinCpus=False,
            listenPort=6634,
            waitConnected=False)
        self.ipBaseNum += self._dpid_offset

    def buildFromTopo(self, topo=None):
        """
        Build mininet from a topology object
        At the end of this function, everything should be connected and up.
        Use the dpid offset to distinguise the nodes between Multinet Instances
        """

        info = logging.info
        # Possibly we should clean up here and/or validate
        # the topo
        if self.cleanup:
            pass

        info('*** Creating network\n')

        if not self.controllers and self.controller:
            # Add a default controller
            info('*** Adding controller\n')
            try:
                classes = self.controller
                if not isinstance(classes, list):
                    classes = [classes]
                for i, cls in enumerate(classes):
                    # Allow Controller objects because nobody understands partial()
                    if isinstance(cls, mininet.node.Controller):
                        self.addController(controller=cls,
                                           ip=self._controller_ip,
                                           port=self._controller_port)
                    else:
                        self.addController(name='c{0}'.format(i),
                                           controller=cls,
                                           ip=self._controller_ip,
                                           port=self._controller_port)
            except:
                self.addController(name='c{0}'.format(i), controller=mininet.node.DefaultController)

        info('*** Adding hosts:\n')
        for hostName in topo.hosts():
            kwargs_host = topo.nodeInfo(hostName)
            self.addHost(hostName, **kwargs_host)
            info(hostName + ' ')

        info('\n*** Adding switches:\n')
        for switchName in topo.switches():
            # A bit ugly: add batch parameter if appropriate
            params = topo.nodeInfo(switchName)
            cls = params.get('cls', self.switch)
            params['dpid'] = None
            params['protocols'] = 'OpenFlow13'
            #if hasattr(cls, 'batchStartup'):
            #    params.setdefault('batch', True)
            self.addSwitch(switchName, **params)
            info(switchName + ' ')

        info('\n*** Adding links:\n')
        for srcName, dstName, params in topo.links(
                sort=True, withInfo=True):
            self.addLink(**params)
            info('(%s, %s) ' % (srcName, dstName))

        info('\n')

    def init_topology(self):
        """
        Init the topology
        """

        logging.info("[mininet] Initializing topology.")
        self.build()
        logging.info('[mininet] Topology initialized successfully. '
                     'Booted up {0} switches'.format(self._num_switches))


    def start_topology(self):
        """
        Start controller and switches.
        Do a gradual bootup.
        """
        info = logging.info
        if not self.built:
            self.build()
        info('*** Starting controller\n')
        for controller in self.controllers:
            info(controller.name + ' ')
            controller.start()
        info('\n')
        info('*** Starting %s switches\n' % len(self.switches))

        for ind, switch in enumerate(self.switches):
            if ind % self._group_size == 0:
                time.sleep(self._group_delay)
            logging.debug('[mininet] Starting switch with index {0}'.
                          format(ind + 1))
            info(switch.name + ' ')
            switch.start(self.controllers)
            self.booted_switches += 1

        started = {}
        for swclass, switches in itertools.groupby(
                sorted(self.switches, key=type), type):
            switches = tuple(switches)
            if hasattr(swclass, 'batchStartup'):
                success = swclass.batchStartup(switches)
                started.update({s: s for s in success})
        info('\n')
        if self.waitConn:
            self.waitConnected()

        logging.info('[mininet] Topology started successfully. '
                     'Booted up {0} switches'.format(self._num_switches))
        time.sleep(self._group_delay * 2)

        if self.auto_detect_hosts:
            self.detect_hosts(ping_cnt=50)


    def detect_hosts(self, ping_cnt=50):
        """
        Do a ping from each host to the void to send a PACKET_IN to the controller
        and enable the controller host detector

        Args:
            ping_cnt: Number of pings to send from each host
        """
        for host in self.hosts:
            # ping the void
            host.sendCmd('ping -c{0} {1}'.format(str(ping_cnt),
                                                 str(self.controllers[0].IP())))

        logging.debug('[mininet] Hosts should be visible now')

    def get_switches(self):
        """Returns the total number of switches of the topology

        Returns:
            (int): number of switches in the topology
        """
        return self.booted_switches

    def stop_topology(self):
        """
        Stops the topology
        """

        logging.info('[mininet] Halting topology. Terminating switches.')
        for h in self.hosts:
            h.sendInt()
        mininet.clean.cleanup()
        self.switches = []
        self.hosts = []
        self.links = []
        self.controllers = []
        self.built = False
        self.booted_switches = 0
        logging.info('[mininet] Topology halted successfully')


    def ping_all(self):
        """
        All-to-all host pinging used for testing.
        """
        self.pingAll(timeout=None)


    def get_flows(self):
        """
        Getting flows from switches
        """
        logging.info('[get_flows] Getting flows from switches.')
        flow_number_total = 0
        t_start = time.time()
        for switch in self.switches:
            for stat_item in switch.dpctl('-O OpenFlow13 dump-aggregate').split(' '):
                if stat_item.split('=')[0] == 'flow_count' and len(stat_item.split('=')) == 2:
                    flow_number_total += int(stat_item.split('=')[-1])
        logging.debug('[get_flows] number of flows: {0}'.format(flow_number_total))
        get_flow_latency = time.time() - t_start
        logging.info('[get_flows] Flow latency interval on worker: {0} [sec]]'.
                     format(get_flow_latency))
        return flow_number_total


    def generate_mac_address_pairs(self, current_mac):
        """
        Generated tuple of source/destination mac addresses

        Args:
          current_mac (str): The last generated mac used for traffic
                             generation. It is used as reference to generate
                             the next pair of source and destination mac
                             addresses.
        """

        # We place an extra 11 in front of base_mac and these digits are
        # filtered out, in order to get a full range of mac addresses. We get
        # generated mac addresses as string separated with : for every 2
        # characters.
        base_mac = 0x11000000000000
        generated_mac = hex(base_mac + int(current_mac, 16))

        source_mac = ':'.join(''.join(pair) for pair in zip(*[iter(hex(int(generated_mac, 16) + 1))]*2))[6:]
        dest_mac = ':'.join(''.join(pair) for pair in zip(*[iter(hex(int(generated_mac, 16) + 2))]*2))[6:]
        return source_mac, dest_mac

    def generate_traffic(self):
        """
        Traffic generation from switches to controller
        """

        logging.info('[mininet] Generating traffic from switches.')

        if not self._hosts_per_switch>1:
            raise AssertionError(
                '_hosts_per_switch must be at least 2 or greater.')
        traffic_transmission_delay = self._interpacket_delay_ms / 1000
        traffic_transmission_interval = \
            self._traffic_generation_duration_ms / 1000
        host_index = 0

        transmission_start = time.time()
        last_mac = hex(int(hex(self._dpid_offset) + '00000000', 16) + 0xffffffff)
        current_mac = hex(int(last_mac, 16) - 0x0000ffffffff + 0x000000000001)

        while (time.time() - transmission_start) <= traffic_transmission_interval:
            src_mac, dst_mac = self.generate_mac_address_pairs(current_mac)
            current_mac = hex(int(current_mac, 16) + 2)
            # Flows generation section. In order to work properly we must
            # configure the ODL controller with L2Switch plugin and each switch
            # of the topology must have at least 2 hosts.
            # Step1:
            # From host1 of switch1 we initially send a Gratuitous ARP Reply.
            # We encapsulate this Reply in an ethernet frame with a specific
            # src and dst MAC addresses, generated from the MAC address
            # generator in this class.
            # Step2:
            # We repeat the above steps from host2 of switch1 reversing the src
            # and dst MAC addresses of the ethernet frame.
            # The above sequence has as a result to trigger ODL controller to
            # respond with 2 FlowMod messages in order to establish a datapath
            # between the 2 hosts
            self.hosts[host_index].sendCmd(
                'sudo mz -a {0} -b {1} -t arp'.format(src_mac, dst_mac))
            # We break transmission delay and we place a delay between the
            # transmission of the 2 Gratuitous ARP messages in order to avoid
            # bursts of messages
            time.sleep(traffic_transmission_delay/2)
            self.hosts[host_index + 1].sendCmd(
                'sudo mz -a {0} -b {1} -t arp'.format(dst_mac, src_mac))
            time.sleep(traffic_transmission_delay/2)
            host_index += self._hosts_per_switch

            if host_index >= len(self.hosts):
                for host in self.hosts:
                    host.waitOutput()
                host_index = 0

            if int(current_mac, 16) >= int(last_mac, 16):
                current_mac = \
                    hex(int(last_mac, 16) - 0x0000ffffffff + 0x000000000001)
                # The minimum controller hard_timeout is 1 second.
                # Retransmission using the init_mac must start after the
                # minimum hard_timeout interval
                if (time.time - transmission_start) < 1:
                    time.sleep(1 - (time.time - transmission_start))
        # Cleanup hosts console outputs and write flags after finishing
        # transmission
        for host in self.hosts:
            host.waitOutput()

    def ip2long(self, ip_str):
        """
        Convert an IP string to long number
        Args:
            ip_str (str): IP address as a string
        """
        packedIP = socket.inet_aton(ip_str)
        return struct.unpack("!L", packedIP)[0]

    def long2ip(self, ip_lng):
        """
        Convert long number to IP string
        Args:
            ip_lng (long): IP address as a long number
        """
        return socket.inet_ntoa(struct.pack('!L', ip_lng))