From: Alina Quereilhac Date: Thu, 17 Jul 2014 09:02:22 +0000 (+0200) Subject: Adding linux netns client X-Git-Tag: nepi-3.2.0~35^2~2 X-Git-Url: http://git.onelab.eu/?p=nepi.git;a=commitdiff_plain;h=c705251d1d74e3b5eeeb8f6131e905672ed200b9 Adding linux netns client --- diff --git a/setup.py b/setup.py index 710c67d6..fdb8017d 100755 --- 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 index 00000000..e69de29b diff --git a/src/nepi/resources/linux/netns/netnsclient.py b/src/nepi/resources/linux/netns/netnsclient.py new file mode 100644 index 00000000..0233d5f8 --- /dev/null +++ b/src/nepi/resources/linux/netns/netnsclient.py @@ -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 . +# +# Author: Alina Quereilhac + +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 index 00000000..b5e285f9 --- /dev/null +++ b/src/nepi/resources/netns/netnsclient.py @@ -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 . +# +# Author: Alina Quereilhac + +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 index 00000000..826843eb --- /dev/null +++ b/src/nepi/resources/netns/netnsserver.py @@ -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 . +# +# Author: Alina Quereilhac + +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 -L -D -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 index 00000000..447e92c4 --- /dev/null +++ b/test/resources/linux/netns/netnsclient.py @@ -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 . +# +# Author: Alina Quereilhac + +# 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() + diff --git a/test/resources/netns/netnswrapper.py b/test/resources/netns/netnswrapper.py index db3bf7e8..c2bf9822 100755 --- a/test/resources/netns/netnswrapper.py +++ b/test/resources/netns/netnswrapper.py @@ -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()