Merge remote-tracking branch 'upstream/master'
[osm/vim-emu.git] / src / emuvim / dcemulator / net.py
index 7f31a46..15bb6f1 100755 (executable)
@@ -1,26 +1,51 @@
 """
-Distributed Cloud Emulator (dcemulator)
-(c) 2015 by Manuel Peuster <manuel.peuster@upb.de>
+Copyright (c) 2015 SONATA-NFV and Paderborn University
+ALL RIGHTS RESERVED.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+Neither the name of the SONATA-NFV [, ANY ADDITIONAL AFFILIATION]
+nor the names of its contributors may be used to endorse or promote
+products derived from this software without specific prior written
+permission.
+
+This work has been performed in the framework of the SONATA project,
+funded by the European Commission under Grant number 671517 through
+the Horizon 2020 and 5G-PPP programmes. The authors would like to
+acknowledge the contributions of their colleagues of the SONATA
+partner consortium (www.sonata-nfv.eu).
 """
 import logging
 
 import site
 import time
 from subprocess import Popen
-import os
 import re
-import urllib2
-from functools import partial
+import requests
 
 from mininet.net import Containernet
 from mininet.node import Controller, DefaultController, OVSSwitch, OVSKernelSwitch, Docker, RemoteController
 from mininet.cli import CLI
 from mininet.link import TCLink
+from mininet.clean import cleanup
 import networkx as nx
 from emuvim.dcemulator.monitoring import DCNetworkMonitor
 from emuvim.dcemulator.node import Datacenter, EmulatorCompute
 from emuvim.dcemulator.resourcemodel import ResourceModelRegistrar
 
+LOG = logging.getLogger("dcemulator.net")
+LOG.setLevel(logging.DEBUG)
+
 class DCNetwork(Containernet):
     """
     Wraps the original Mininet/Containernet class and provides
@@ -30,6 +55,7 @@ class DCNetwork(Containernet):
     """
 
     def __init__(self, controller=RemoteController, monitor=False,
+                 enable_learning = True,   # in case of RemoteController (Ryu), learning switch behavior can be turned off/on
                  dc_emulation_max_cpu=1.0,  # fraction of overall CPU time for emulation
                  dc_emulation_max_mem=512,  # emulation max mem in MB
                  **kwargs):
@@ -39,17 +65,22 @@ class DCNetwork(Containernet):
         :param kwargs: path through for Mininet parameters
         :return:
         """
+        # members
         self.dcs = {}
+        self.ryu_process = None
+
+        # always cleanup environment before we start the emulator
+        self.killRyu()
+        cleanup()
 
         # call original Docker.__init__ and setup default controller
         Containernet.__init__(
-            self, switch=OVSKernelSwitch, **kwargs)
+            self, switch=OVSKernelSwitch, controller=controller, **kwargs)
 
         # Ryu management
-        self.ryu_process = None
         if controller == RemoteController:
             # start Ryu controller
-            self.startRyu()
+            self.startRyu(learning_switch=enable_learning)
 
         # add the specified controller
         self.addController('c0', controller=controller)
@@ -61,9 +92,10 @@ class DCNetwork(Containernet):
         self.vlans = range(4096)[::-1]
 
         # link to Ryu REST_API
-        ryu_ip = '0.0.0.0'
+        ryu_ip = 'localhost'
         ryu_port = '8080'
         self.ryu_REST_api = 'http://{0}:{1}'.format(ryu_ip, ryu_port)
+        self.RyuSession = requests.Session()
 
         # monitoring agent
         if monitor:
@@ -85,7 +117,7 @@ class DCNetwork(Containernet):
         dc.net = self  # set reference to network
         self.dcs[label] = dc
         dc.create()  # finally create the data center in our Mininet instance
-        logging.info("added data center: %s" % label)
+        LOG.info("added data center: %s" % label)
         return dc
 
     def addLink(self, node1, node2, **params):
@@ -95,7 +127,7 @@ class DCNetwork(Containernet):
         """
         assert node1 is not None
         assert node2 is not None
-        logging.debug("addLink: n1=%s n2=%s" % (str(node1), str(node2)))
+        LOG.debug("addLink: n1=%s n2=%s" % (str(node1), str(node2)))
         # ensure type of node1
         if isinstance( node1, basestring ):
             if node1 in self.dcs:
@@ -222,32 +254,55 @@ class DCNetwork(Containernet):
         Containernet.stop(self)
 
         # stop Ryu controller
-        self.stopRyu()
+        self.killRyu()
 
 
     def CLI(self):
         CLI(self)
 
-    # to remove chain do setChain( src, dst, cmd='del-flows')
     def setChain(self, vnf_src_name, vnf_dst_name, vnf_src_interface=None, vnf_dst_interface=None, **kwargs):
+        """
+        Chain 2 vnf interfaces together by installing the flowrules in the switches along their path.
+        Currently the path is found using the default networkx shortest path function.
+        Each chain gets a unique vlan id , so different chains wil not interfere.
+
+        :param vnf_src_name: vnf name (string)
+        :param vnf_dst_name: vnf name (string)
+        :param vnf_src_interface: source interface name  (string)
+        :param vnf_dst_interface: destination interface name  (string)
+        :param cmd: 'add-flow' (default) to add a chain, 'del-flows' to remove a chain
+        :param cookie: cookie for the installed flowrules (can be used later as identifier for a set of installed chains)
+        :param match: custom match entry to be added to the flowrules (default: only in_port and vlan tag)
+        :param priority: custom flowrule priority
+        :return: output log string
+        """
         cmd = kwargs.get('cmd')
         if cmd == 'add-flow':
             ret = self._chainAddFlow(vnf_src_name, vnf_dst_name, vnf_src_interface, vnf_dst_interface, **kwargs)
             if kwargs.get('bidirectional'):
-                return ret +'\n' + self._chainAddFlow(vnf_dst_name, vnf_src_name, vnf_dst_interface, vnf_src_interface, **kwargs)
+                ret = ret +'\n' + self._chainAddFlow(vnf_dst_name, vnf_src_name, vnf_dst_interface, vnf_src_interface, **kwargs)
 
-        elif cmd == 'del-flows':  # TODO: del-flow to be implemented
+        elif cmd == 'del-flows':
             ret = self._chainAddFlow(vnf_src_name, vnf_dst_name, vnf_src_interface, vnf_dst_interface, **kwargs)
             if kwargs.get('bidirectional'):
-                return ret + '\n' + self._chainAddFlow(vnf_dst_name, vnf_src_name, vnf_dst_interface, vnf_src_interface, **kwargs)
+                ret = ret + '\n' + self._chainAddFlow(vnf_dst_name, vnf_src_name, vnf_dst_interface, vnf_src_interface, **kwargs)
 
         else:
-            return "Command unknown"
+            ret = "Command unknown"
+
+        return ret
 
 
     def _chainAddFlow(self, vnf_src_name, vnf_dst_name, vnf_src_interface=None, vnf_dst_interface=None, **kwargs):
 
-        # TODO: this needs to be cleaned up
+        src_sw = None
+        dst_sw = None
+        src_sw_inport_nr = 0
+        dst_sw_outport_nr = 0
+
+        LOG.debug("call chainAddFlow vnf_src_name=%r, vnf_src_interface=%r, vnf_dst_name=%r, vnf_dst_interface=%r",
+                  vnf_src_name, vnf_src_interface, vnf_dst_name, vnf_dst_interface)
+
         #check if port is specified (vnf:port)
         if vnf_src_interface is None:
             # take first interface by default
@@ -258,10 +313,10 @@ class DCNetwork(Containernet):
         for connected_sw in self.DCNetwork_graph.neighbors(vnf_src_name):
             link_dict = self.DCNetwork_graph[vnf_src_name][connected_sw]
             for link in link_dict:
-                if link_dict[link]['src_port_id'] == vnf_src_interface:
+                if (link_dict[link]['src_port_id'] == vnf_src_interface or
+                        link_dict[link]['src_port_name'] == vnf_src_interface):  # Fix: we might also get interface names, e.g, from a son-emu-cli call
                     # found the right link and connected switch
                     src_sw = connected_sw
-
                     src_sw_inport_nr = link_dict[link]['dst_port_nr']
                     break
 
@@ -275,7 +330,8 @@ class DCNetwork(Containernet):
         for connected_sw in self.DCNetwork_graph.neighbors(vnf_dst_name):
             link_dict = self.DCNetwork_graph[connected_sw][vnf_dst_name]
             for link in link_dict:
-                if link_dict[link]['dst_port_id'] == vnf_dst_interface:
+                if link_dict[link]['dst_port_id'] == vnf_dst_interface or \
+                        link_dict[link]['dst_port_name'] == vnf_dst_interface:  # Fix: we might also get interface names, e.g, from a son-emu-cli call
                     # found the right link and connected switch
                     dst_sw = connected_sw
                     dst_sw_outport_nr = link_dict[link]['src_port_nr']
@@ -288,10 +344,15 @@ class DCNetwork(Containernet):
             # if all shortest paths are wanted, use: all_shortest_paths
             path = nx.shortest_path(self.DCNetwork_graph, src_sw, dst_sw, weight=kwargs.get('weight'))
         except:
-            logging.info("No path could be found between {0} and {1}".format(vnf_src_name, vnf_dst_name))
+            LOG.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
+                vnf_src_name, vnf_dst_name, src_sw, dst_sw))
+            LOG.debug("Graph nodes: %r" % self.DCNetwork_graph.nodes())
+            LOG.debug("Graph edges: %r" % self.DCNetwork_graph.edges())
+            for e, v in self.DCNetwork_graph.edges():
+                LOG.debug("%r" % self.DCNetwork_graph[e][v])
             return "No path could be found between {0} and {1}".format(vnf_src_name, vnf_dst_name)
 
-        logging.info("Path between {0} and {1}: {2}".format(vnf_src_name, vnf_dst_name, path))
+        LOG.info("Path between {0} and {1}: {2}".format(vnf_src_name, vnf_dst_name, path))
 
         current_hop = src_sw
         switch_inport_nr = src_sw_inport_nr
@@ -316,9 +377,9 @@ class DCNetwork(Containernet):
 
             if next_hop == vnf_dst_name:
                 switch_outport_nr = dst_sw_outport_nr
-                logging.info("end node reached: {0}".format(vnf_dst_name))
+                LOG.info("end node reached: {0}".format(vnf_dst_name))
             elif not isinstance( next_node, OVSSwitch ):
-                logging.info("Next node: {0} is not a switch".format(next_hop))
+                LOG.info("Next node: {0} is not a switch".format(next_hop))
                 return "Next node: {0} is not a switch".format(next_hop)
             else:
                 # take first link between switches by default
@@ -331,10 +392,15 @@ class DCNetwork(Containernet):
                 kwargs['vlan'] = vlan
                 kwargs['path'] = path
                 kwargs['current_hop'] = current_hop
-                ## set flow entry via ovs-ofctl
-                #self._set_flow_entry_dpctl(current_node, switch_inport_nr, switch_outport_nr, **kwargs)
-                ## set flow entry via ryu rest api
-                self._set_flow_entry_ryu_rest(current_node, switch_inport_nr, switch_outport_nr, **kwargs)
+
+                if self.controller == RemoteController:
+                    ## set flow entry via ryu rest api
+                    self._set_flow_entry_ryu_rest(current_node, switch_inport_nr, switch_outport_nr, **kwargs)
+                else:
+                    ## set flow entry via ovs-ofctl
+                    self._set_flow_entry_dpctl(current_node, switch_inport_nr, switch_outport_nr, **kwargs)
+
+
 
             # take first link between switches by default
             if isinstance( next_node, OVSSwitch ):
@@ -352,6 +418,7 @@ class DCNetwork(Containernet):
         path = kwargs.get('path')
         current_hop = kwargs.get('current_hop')
         vlan = kwargs.get('vlan')
+        priority = kwargs.get('priority')
 
         s = ','
         if match_input:
@@ -359,9 +426,11 @@ class DCNetwork(Containernet):
 
         flow = {}
         flow['dpid'] = int(node.dpid, 16)
+
         if cookie:
             flow['cookie'] = int(cookie)
-
+        if priority:
+            flow['priority'] = int(priority)
 
         flow['actions'] = []
 
@@ -369,15 +438,13 @@ class DCNetwork(Containernet):
         # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#add-a-flow-entry
         if cmd == 'add-flow':
             prefix = 'stats/flowentry/add'
-            action = {}
-            action['type'] = 'OUTPUT'
-            action['port'] = switch_outport_nr
-            flow['actions'].append(action)
             if vlan != None:
                 if path.index(current_hop) == 0:  # first node
                     action = {}
                     action['type'] = 'PUSH_VLAN'  # Push a new VLAN tag if a input frame is non-VLAN-tagged
                     action['ethertype'] = 33024   # Ethertype 0x8100(=33024): IEEE 802.1Q VLAN-tagged frame
+                    flow['actions'].append(action)
+                    action = {}
                     action['type'] = 'SET_FIELD'
                     action['field'] = 'vlan_vid'
                     action['value'] = vlan
@@ -389,14 +456,18 @@ class DCNetwork(Containernet):
                     flow['actions'].append(action)
                 else:  # middle nodes
                     match += ',dl_vlan=%s' % vlan
-            #flow['match'] = self._parse_match(match)
+            # output action must come last
+            action = {}
+            action['type'] = 'OUTPUT'
+            action['port'] = switch_outport_nr
+            flow['actions'].append(action)
+
         elif cmd == 'del-flows':
-            #del(flow['actions'])
             prefix = 'stats/flowentry/delete'
+
             if cookie:
-                flow['cookie_mask'] = cookie
-            #if cookie is None:
-            #    flow['match'] = self._parse_match(match)
+                # TODO: add cookie_mask as argument
+                flow['cookie_mask'] = int('0xffffffffffffffff', 16)  # need full mask to match complete cookie
 
             action = {}
             action['type'] = 'OUTPUT'
@@ -440,11 +511,11 @@ class DCNetwork(Containernet):
             ofcmd = ''
 
         node.dpctl(cmd, ofcmd)
-        logging.info("{3} in switch: {0} in_port: {1} out_port: {2}".format(node.name, switch_inport_nr,
+        LOG.info("{3} in switch: {0} in_port: {1} out_port: {2}".format(node.name, switch_inport_nr,
                                                                                  switch_outport_nr, cmd))
 
     # start Ryu Openflow controller as Remote Controller for the DCNetwork
-    def startRyu(self):
+    def startRyu(self, learning_switch=True):
         # start Ryu controller with rest-API
         python_install_path = site.getsitepackages()[0]
         ryu_path = python_install_path + '/ryu/app/simple_switch_13.py'
@@ -455,30 +526,56 @@ class DCNetwork(Containernet):
         ryu_of_port = '6653'
         ryu_cmd = 'ryu-manager'
         FNULL = open("/tmp/ryu.log", 'w')
-        self.ryu_process = Popen([ryu_cmd, ryu_path, ryu_path2, ryu_option, ryu_of_port], stdout=FNULL, stderr=FNULL)
-        # no learning switch
-        #self.ryu_process = Popen([ryu_cmd, ryu_path2, ryu_option, ryu_of_port], stdout=FNULL, stderr=FNULL)
+        if learning_switch:
+            self.ryu_process = Popen([ryu_cmd, ryu_path, ryu_path2, ryu_option, ryu_of_port], stdout=FNULL, stderr=FNULL)
+        else:
+            # no learning switch, but with rest api
+            self.ryu_process = Popen([ryu_cmd, ryu_path2, ryu_option, ryu_of_port], stdout=FNULL, stderr=FNULL)
         time.sleep(1)
 
-    def stopRyu(self):
+    def killRyu(self):
+        """
+        Stop the Ryu controller that might be started by son-emu.
+        :return:
+        """
+        # try it nicely
         if self.ryu_process is not None:
             self.ryu_process.terminate()
             self.ryu_process.kill()
+        # ensure its death ;-)
+        Popen(['pkill', '-f', 'ryu-manager'])
 
     def ryu_REST(self, prefix, dpid=None, data=None):
+
         if dpid:
             url = self.ryu_REST_api + '/' + str(prefix) + '/' + str(dpid)
         else:
             url = self.ryu_REST_api + '/' + str(prefix)
         if data:
-            #logging.info('POST: {0}'.format(str(data)))
-            req = urllib2.Request(url, str(data))
+            req = self.RyuSession.post(url, json=data)
         else:
-            req = urllib2.Request(url)
+            req = self.RyuSession.get(url)
+
 
-        ret = urllib2.urlopen(req).read()
+        # do extra logging if status code is not 200 (OK)
+        if req.status_code is not requests.codes.ok:
+            logging.info(
+                'type {0}  encoding: {1} text: {2} headers: {3} history: {4}'.format(req.headers['content-type'],
+                                                                                     req.encoding, req.text,
+                                                                                     req.headers, req.history))
+            LOG.info('url: {0}'.format(str(url)))
+            if data: LOG.info('POST: {0}'.format(str(data)))
+            LOG.info('status: {0} reason: {1}'.format(req.status_code, req.reason))
+
+
+        if 'json' in req.headers['content-type']:
+            ret = req.json()
+            return ret
+
+        ret = req.text.rstrip()
         return ret
 
+
     # need to respect that some match fields must be integers
     # http://ryu.readthedocs.io/en/latest/app/ofctl_rest.html#description-of-match-and-actions
     def _parse_match(self, match):