Adding linux netns client
authorAlina Quereilhac <alina.quereilhac@inria.fr>
Thu, 17 Jul 2014 09:02:22 +0000 (11:02 +0200)
committerAlina Quereilhac <alina.quereilhac@inria.fr>
Thu, 17 Jul 2014 09:02:22 +0000 (11:02 +0200)
setup.py
src/nepi/resources/linux/netns/__init__.py [new file with mode: 0644]
src/nepi/resources/linux/netns/netnsclient.py [new file with mode: 0644]
src/nepi/resources/netns/netnsclient.py [new file with mode: 0644]
src/nepi/resources/netns/netnsserver.py [new file with mode: 0644]
test/resources/linux/netns/netnsclient.py [new file with mode: 0644]
test/resources/netns/netnswrapper.py

index 710c67d..fdb8017 100755 (executable)
--- a/setup.py
+++ b/setup.py
@@ -22,6 +22,7 @@ setup(
             "nepi.resources.linux.ccn",
             "nepi.resources.linux.ns3",
             "nepi.resources.linux.ns3.ccn",
+            "nepi.resources.linux.netns",
             "nepi.resources.netns",
             "nepi.resources.ns3",
             "nepi.resources.ns3.classes",
diff --git a/src/nepi/resources/linux/netns/__init__.py b/src/nepi/resources/linux/netns/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/nepi/resources/linux/netns/netnsclient.py b/src/nepi/resources/linux/netns/netnsclient.py
new file mode 100644 (file)
index 0000000..0233d5f
--- /dev/null
@@ -0,0 +1,108 @@
+#
+#    NEPI, a framework to manage network experiments
+#    Copyright (C) 2014 INRIA
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation, either version 3 of the License, or
+#    (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+# Author: Alina Quereilhac <alina.quereilhac@inria.fr>
+
+import base64
+import cPickle
+import errno
+import os
+import socket
+import time
+import weakref
+
+from optparse import OptionParser, SUPPRESS_HELP
+
+from nepi.resources.netns.netnsclient import NetNSClient
+from nepi.resources.netns.netnsserver import NetNSWrapperMessage
+
+class LinuxNetNSClient(NetNSClient):
+    def __init__(self, emulation):
+        super(LinuxNetNSClient, self).__init__()
+        self._emulation = weakref.ref(emulation)
+
+        self._socat_proc = None
+
+    @property
+    def emulation(self):
+        return self._emulation()
+
+    def send_msg(self, msg_type, *args, **kwargs):
+        msg = [msg_type, args, kwargs]
+
+        def encode(item):
+            item = cPickle.dumps(item)
+            return base64.b64encode(item)
+
+        encoded = "|".join(map(encode, msg))
+
+        if self.emulation.node.get("hostname") in ['localhost', '127.0.0.1']:
+            sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+            sock.connect(self.emulation.remote_socket)
+            sock.send("%s\n" % encoded)
+            reply = sock.recv(1024)
+            sock.close()
+        else:
+            command = ( "python -c 'import socket;"
+                "sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM);"
+                "sock.connect(\"%(socket_addr)s\");"
+                "msg = \"%(encoded_message)s\\n\";"
+                "sock.send(msg);"
+                "reply = sock.recv(1024);"
+                "sock.close();"
+                "print reply'") % {
+                    "encoded_message": encoded,
+                    "socket_addr": self.emulation.remote_socket,
+                    }
+
+            (reply, err), proc = self.emulation.node.execute(command, 
+                    with_lock = True) 
+
+            if (err and proc.poll()) or reply.strip() == "":
+                msg = (" Couldn't connect to remote socket %s - REPLY: %s "
+                      "- ERROR: %s ") % (
+                        self.emulation.remote_socket, reply, err)
+                self.emulation.error(msg, reply, err)
+                raise RuntimeError(msg)
+                   
+        reply = cPickle.loads(base64.b64decode(reply))
+
+        return reply
+
+    def create(self, *args, **kwargs):
+        return self.send_msg(NetNSWrapperMessage.CREATE, *args, **kwargs)
+
+    def invoke(self, *args, **kwargs):
+        return self.send_msg(NetNSWrapperMessage.INVOKE, *args, **kwargs)
+
+    def set(self, *args, **kwargs):
+        return self.send_msg(NetNSWrapperMessage.SET, *args, **kwargs)
+
+    def get(self, *args, **kwargs):
+        return self.send_msg(NetNSWrapperMessage.GET, *args, **kwargs)
+
+    def flush(self, *args, **kwargs):
+        return self.send_msg(NetNSWrapperMessage.FLUSH, *args, **kwargs)
+
+    def shutdown(self, *args, **kwargs):
+        try:
+            return self.send_msg(NetNSWrapperMessage.SHUTDOWN, *args, **kwargs)
+        except:
+            pass
+
+        return None
+
diff --git a/src/nepi/resources/netns/netnsclient.py b/src/nepi/resources/netns/netnsclient.py
new file mode 100644 (file)
index 0000000..b5e285f
--- /dev/null
@@ -0,0 +1,42 @@
+#
+#    NEPI, a framework to manage network experiments
+#    Copyright (C) 2014 INRIA
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation, either version 3 of the License, or
+#    (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+# Author: Alina Quereilhac <alina.quereilhac@inria.fr>
+
+class NetNSClient(object):
+    """ Common Interface for NS3 client classes """
+    def __init__(self):
+        super(NetNSClient, self).__init__()
+
+    def create(self, *args, **kwargs):
+        pass
+
+    def invoke(self, *args, **kwargs):
+        pass
+
+    def set(self, *args, **kwargs):
+        pass
+
+    def get(self, *args, **kwargs):
+        pass
+
+    def flush(self, *args, **kwargs):
+        pass
+
+    def shutdown(self, *args, **kwargs):
+        pass
+
diff --git a/src/nepi/resources/netns/netnsserver.py b/src/nepi/resources/netns/netnsserver.py
new file mode 100644 (file)
index 0000000..826843e
--- /dev/null
@@ -0,0 +1,222 @@
+#
+#    NEPI, a framework to manage network experiments
+#    Copyright (C) 2014 INRIA
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation, either version 3 of the License, or
+#    (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+# Author: Alina Quereilhac <alina.quereilhac@inria.fr>
+
+import base64
+import cPickle
+import errno
+import logging
+import os
+import socket
+import sys
+
+from optparse import OptionParser, SUPPRESS_HELP
+
+from netnswrapper import NetNSWrapper
+
+class NetNSWrapperMessage:
+    CREATE = "CREATE"
+    INVOKE = "INVOKE"
+    SET = "SET"
+    GET = "GET"
+    FLUSH = "FLUSH"
+    SHUTDOWN = "SHUTDOWN"
+
+def handle_message(wrapper, msg_type, args, kwargs):
+    if msg_type == NetNSWrapperMessage.SHUTDOWN:
+        wrapper.shutdown()
+
+        return "BYEBYE"
+    
+    if msg_type == NetNSWrapperMessage.CREATE:
+        clazzname = args.pop(0)
+        
+        return wrapper.create(clazzname, *args)
+        
+    if msg_type == NetNSWrapperMessage.INVOKE:
+        uuid = args.pop(0)
+        operation = args.pop(0)
+   
+        return wrapper.invoke(uuid, operation, *args, **kwargs)
+
+    if msg_type == NetNSWrapperMessage.GET:
+        uuid = args.pop(0)
+        name = args.pop(0)
+
+        return wrapper.get(uuid, name)
+        
+    if msg_type == NetNSWrapperMessage.SET:
+        uuid = args.pop(0)
+        name = args.pop(0)
+        value = args.pop(0)
+
+        return wrapper.set(uuid, name, value)
+
+    if msg_type == NetNSWrapperMessage.FLUSH:
+        # Forces flushing output and error streams.
+        # NS-3 output will stay unflushed until the program exits or 
+        # explicit invocation flush is done
+        sys.stdout.flush()
+        sys.stderr.flush()
+
+        wrapper.logger.debug("FLUSHED") 
+        
+        return "FLUSHED"
+
+def create_socket(socket_name):
+    sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+    sock.bind(socket_name)
+    return sock
+
+def recv_msg(conn):
+    msg = []
+    chunk = ''
+
+    while '\n' not in chunk:
+        try:
+            chunk = conn.recv(1024)
+        except (OSError, socket.error), e:
+            if e[0] != errno.EINTR:
+                raise
+            # Ignore eintr errors
+            continue
+
+        if chunk:
+            msg.append(chunk)
+        else:
+            # empty chunk = EOF
+            break
+    msg = ''.join(msg).strip()
+
+    # The message is formatted as follows:
+    #   MESSAGE_TYPE|args|kwargs
+    #
+    #   where MESSAGE_TYPE, args and kwargs are pickld and enoded in base64
+
+    def decode(item):
+        item = base64.b64decode(item).rstrip()
+        return cPickle.loads(item)
+
+    decoded = map(decode, msg.split("|"))
+
+    # decoded message
+    dmsg_type = decoded.pop(0)
+    dargs = list(decoded.pop(0)) # transforming touple into list
+    dkwargs = decoded.pop(0)
+
+    return (dmsg_type, dargs, dkwargs)
+
+def send_reply(conn, reply):
+    encoded = base64.b64encode(cPickle.dumps(reply))
+    conn.send("%s\n" % encoded)
+
+def get_options():
+    usage = ("usage: %prog -S <socket-name> -L <ns-log>  -D <enable-dump> -v ")
+    
+    parser = OptionParser(usage = usage)
+
+    parser.add_option("-S", "--socket-name", dest="socket_name",
+        help = "Name for the unix socket used to interact with this process", 
+        default = "tap.sock", type="str")
+
+    parser.add_option("-L", "--ns-log", dest="ns_log",
+        help = "NS_LOG environmental variable to be set", 
+        default = "", type="str")
+
+    parser.add_option("-D", "--enable-dump", dest="enable_dump",
+        help = "Enable dumping the remote executed ns-3 commands to a script "
+            "in order to later reproduce and debug the experiment",
+        action = "store_true",
+        default = False)
+
+    parser.add_option("-v", "--verbose",
+        help="Print debug output",
+        action="store_true", 
+        dest="verbose", default=False)
+
+    (options, args) = parser.parse_args()
+    
+    return (options.socket_name, options.verbose, options.ns_log,
+            options.enable_dump)
+
+def run_server(socket_name, level = logging.INFO, ns_log = None, 
+        enable_dump = False):
+
+    # Sets NS_LOG environmental variable for NS debugging
+    if ns_log:
+        os.environ["NS_LOG"] = ns_log
+
+    ###### ns-3 wrapper instantiation
+
+    wrapper = NetNSWrapper(loglevel=level, enable_dump = enable_dump)
+    
+    wrapper.logger.info("STARTING...")
+
+    # create unix socket to receive instructions
+    sock = create_socket(socket_name)
+    sock.listen(0)
+
+    # wait for messages to arrive and process them
+    stop = False
+
+    while not stop:
+        conn, addr = sock.accept()
+        conn.settimeout(5)
+
+        try:
+            (msg_type, args, kwargs) = recv_msg(conn)
+        except socket.timeout, e:
+            # Ingore time-out
+            continue
+
+        if not msg_type:
+            # Ignore - connection lost
+            break
+
+        if msg_type == NetNSWrapperMessage.SHUTDOWN:
+           stop = True
+  
+        try:
+            reply = handle_message(wrapper, msg_type, args, kwargs)  
+        except:
+            import traceback
+            err = traceback.format_exc()
+            wrapper.logger.error(err) 
+            raise
+
+        try:
+            send_reply(conn, reply)
+        except socket.error:
+            break
+        
+    wrapper.logger.info("EXITING...")
+
+if __name__ == '__main__':
+            
+    (socket_name, verbose, ns_log, enable_dump) = get_options()
+
+    ## configure logging
+    FORMAT = "%(asctime)s %(name)s %(levelname)-4s %(message)s"
+    level = logging.DEBUG if verbose else logging.INFO
+
+    logging.basicConfig(format = FORMAT, level = level)
+
+    ## Run the server
+    run_server(socket_name, level, ns_log, enable_dump)
+
diff --git a/test/resources/linux/netns/netnsclient.py b/test/resources/linux/netns/netnsclient.py
new file mode 100644 (file)
index 0000000..447e92c
--- /dev/null
@@ -0,0 +1,171 @@
+#!/usr/bin/env python
+#
+#    NEPI, a framework to manage network experiments
+#    Copyright (C) 2013 INRIA
+#
+#    This program is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation, either version 3 of the License, or
+#    (at your option) any later version.
+#
+#    This program is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+# Author: Alina Quereilhac <alina.quereilhac@inria.fr>
+
+# Test based on netns test/test_core.py file test_run_ping_routing test
+#
+
+from nepi.resources.netns.netnsserver import run_server
+from nepi.resources.linux.netns.netnsclient import LinuxNetNSClient
+
+from test_utils import skipIf
+
+import os
+import threading
+import time
+import unittest
+
+class DummyEmulation(object):
+    def __init__(self, socket_name):
+        self.socket_name = socket_name
+        self.node = dict({'hostname': 'localhost'})
+
+    @property
+    def remote_socket(self):
+        return self.socket_name
+
+class LinuxNetNSClientTest(unittest.TestCase):
+    def setUp(self):
+        self.socket_name = os.path.join("/", "tmp", "NetNSWrapperServer.sock")
+        if os.path.exists(self.socket_name):
+            os.remove(self.socket_name) 
+
+    def tearDown(self):
+        os.remove(self.socket_name) 
+
+    @skipIf(os.getuid() != 0, "Test requires root privileges")
+    def test_run_ping_routing(self):
+        thread = threading.Thread(target = run_server,
+                args = [self.socket_name])
+
+        thread.setDaemon(True)
+        thread.start()
+
+        time.sleep(3)
+
+        # Verify that the communication socket was created
+        self.assertTrue(os.path.exists(self.socket_name))
+
+        # Create a dummy simulation object
+        emulation = DummyEmulation(self.socket_name) 
+
+        # Instantiate the NS3 client
+        client = LinuxNetNSClient(emulation)
+
+        ### create 3 nodes
+        #n1 = netns.Node()
+        #n2 = netns.Node()
+        #n3 = netns.Node()
+        n1 = client.create("Node")
+        n2 = client.create("Node")
+        n3 = client.create("Node")
+
+        ### add interfaces to nodes
+        #i1 = n1.add_if()
+        #i2a = n2.add_if()
+        #i2b = n2.add_if()
+        #i3 = n3.add_if()
+        i1 = client.invoke(n1, "add_if")
+        i2a = client.invoke(n2, "add_if")
+        i2b = client.invoke(n2, "add_if")
+        i3 = client.invoke(n3, "add_if")
+
+        ### set interfaces up
+        # i1.up = i2a.up = i2b.up = i3.up = True
+        client.set(i1, "up", True)
+        client.set(i2a, "up", True)
+        client.set(i2b, "up", True)
+        client.set(i3, "up", True)
+
+        ### create 2 switches
+        #l1 = netns.Switch()
+        #l2 = netns.Switch()
+        l1 = client.create("Switch")
+        l2 = client.create("Switch")
+
+        ### connect interfaces to switches
+        #l1.connect(i1)
+        #l1.connect(i2a)
+        #l2.connect(i2b)
+        #l2.connect(i3)
+        client.invoke(l1, "connect", i1)
+        client.invoke(l1, "connect", i2a)
+        client.invoke(l2, "connect", i2b)
+        client.invoke(l2, "connect", i3)
+
+        ### set switched up
+        # l1.up = l2.up = True
+        client.set(l1, "up", True)
+        client.set(l2, "up", True)
+
+        ## add ip addresses to interfaces
+        #i1.add_v4_address('10.0.0.1', 24)
+        #i2a.add_v4_address('10.0.0.2', 24)
+        #i2b.add_v4_address('10.0.1.1', 24)
+        #i3.add_v4_address('10.0.1.2', 24)
+        client.invoke(i1, "add_v4_address", "10.0.0.1", 24)
+        client.invoke(i2a, "add_v4_address", "10.0.0.2", 24)
+        client.invoke(i2b, "add_v4_address", "10.0.1.1", 24)
+        client.invoke(i3, "add_v4_address", "10.0.1.2", 24)
+
+        ## add routes to nodes
+        #n1.add_route(prefix = '10.0.1.0', prefix_len = 24,
+        #        nexthop = '10.0.0.2')
+        #n3.add_route(prefix = '10.0.0.0', prefix_len = 24,
+        #        nexthop = '10.0.1.1')
+        client.invoke(n1, "add_route", prefix = "10.0.1.0", prefix_len = 24,
+                nexthop = "10.0.0.2")
+        client.invoke(n3, "add_route", prefix = "10.0.0.0", prefix_len = 24,
+                nexthop = "10.0.1.1")
+
+        ## launch pings
+        #a1 = n1.Popen(['ping', '-qc1', '10.0.1.2'], stdout = null)
+        #a2 = n3.Popen(['ping', '-qc1', '10.0.0.1'], stdout = null)
+        path1 = "/tmp/netns_file1"
+        path2 = "/tmp/netns_file2"
+        file1 = client.create("open", path1, "w")
+        file2 = client.create("open", path2, "w")
+        a1 = client.invoke(n1, "Popen", ["ping", "-qc1", "10.0.1.2"], stdout = file1)
+        a2 = client.invoke(n3, "Popen", ["ping", "-qc1", "10.0.0.1"], stdout = file2)
+
+        ## get ping status
+        p1 = None
+        p2 = None
+        while p1 is None or p2 is None:
+            p1 = client.invoke(a1, "poll")
+            p2 = client.invoke(a2, "poll")
+
+        stdout1 = open(path1, "r")
+        stdout2 = open(path2, "r")
+
+        s1 = stdout1.read()
+        s2 = stdout2.read()
+
+        print s1, s2
+
+        expected = "1 packets transmitted, 1 received, 0% packet loss"
+        self.assertTrue(s1.find(expected) > -1)
+        self.assertTrue(s2.find(expected) > -1)
+
+        # wait until emulation is over
+        client.shutdown()
+
+if __name__ == '__main__':
+    unittest.main()
+
index db3bf7e..c2bf982 100755 (executable)
@@ -126,7 +126,12 @@ class NetNSWrapperTest(unittest.TestCase):
         stdout1 = open(path1, "r")
         stdout2 = open(path2, "r")
 
-        print stdout1.read(), stdout2.read()
+        s1 = stdout1.read()
+        s2 = stdout2.read()
+
+        expected = "1 packets transmitted, 1 received, 0% packet loss"
+        self.assertTrue(s1.find(expected) > -1)
+        self.assertTrue(s2.find(expected) > -1)
 
 if __name__ == '__main__':
     unittest.main()