From 1762316cf70b2dcc495ec5cc4c4c1ac220d92d2c Mon Sep 17 00:00:00 2001 From: Alina Quereilhac Date: Thu, 10 Jul 2014 14:39:26 +0200 Subject: [PATCH] Adding netns wrapper --- src/nepi/resources/netns/netnswrapper.py | 186 +++++++++++++++++ .../resources/netns/netnswrapper_debug.py | 191 ++++++++++++++++++ src/nepi/resources/ns3/ns3wrapper_debug.py | 4 +- test/lib/test_utils.py | 15 ++ test/resources/netns/netnswrapper.py | 133 ++++++++++++ 5 files changed, 527 insertions(+), 2 deletions(-) create mode 100644 src/nepi/resources/netns/netnswrapper.py create mode 100644 src/nepi/resources/netns/netnswrapper_debug.py create mode 100755 test/resources/netns/netnswrapper.py diff --git a/src/nepi/resources/netns/netnswrapper.py b/src/nepi/resources/netns/netnswrapper.py new file mode 100644 index 00000000..f2e1e7a9 --- /dev/null +++ b/src/nepi/resources/netns/netnswrapper.py @@ -0,0 +1,186 @@ +# +# 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 + +import logging +import netns +import time +import os +import sys +import uuid + +class NetNSWrapper(object): + def __init__(self, loglevel = logging.INFO, enable_dump = False): + super(NetNSWrapper, self).__init__() + # holds reference to all C++ objects and variables in the simulation + self._objects = dict() + + # Logging + self._logger = logging.getLogger("netnswrapper") + self._logger.setLevel(loglevel) + + # Object to dump instructions to reproduce and debug experiment + from netnswrapper_debug import NetNSWrapperDebuger + self._debuger = NetNSWrapperDebuger(enabled = enable_dump) + + @property + def debuger(self): + return self._debuger + + @property + def logger(self): + return self._logger + + def make_uuid(self): + return "uuid%s" % uuid.uuid4() + + def get_object(self, uuid): + return self._objects.get(uuid) + + def create(self, clazzname, *args): + """ This method should be used to construct netns objects """ + + if clazzname not in ['open'] and not hasattr(netns, clazzname): + msg = "Type %s not supported" % (clazzname) + self.logger.error(msg) + + uuid = self.make_uuid() + + ### DEBUG + self.logger.debug("CREATE %s( %s )" % (clazzname, str(args))) + + self.debuger.dump_create(uuid, clazzname, args) + ######## + + if clazzname == "open": + path = args[0] + mode = args[1] + obj = open(path, mode) + else: + clazz = getattr(netns, clazzname) + + # arguments starting with 'uuid' identify ns-3 C++ + # objects and must be replaced by the actual object + realargs = self.replace_args(args) + + obj = clazz(*realargs) + + self._objects[uuid] = obj + + ### DEBUG + self.logger.debug("RET CREATE ( uuid %s ) %s = %s( %s )" % (str(uuid), + str(obj), clazzname, str(args))) + ######## + + return uuid + + def invoke(self, uuid, operation, *args, **kwargs): + newuuid = self.make_uuid() + + ### DEBUG + self.logger.debug("INVOKE %s -> %s( %s, %s ) " % ( + uuid, operation, str(args), str(kwargs))) + + self.debuger.dump_invoke(newuuid, uuid, operation, args, kwargs) + ######## + + obj = self.get_object(uuid) + + method = getattr(obj, operation) + + # arguments starting with 'uuid' identify netns + # objects and must be replaced by the actual object + realargs = self.replace_args(args) + realkwargs = self.replace_kwargs(kwargs) + + result = method(*realargs, **realkwargs) + + # If the result is an object (not a base value), + # then keep track of the object a return the object + # reference (newuuid) + if not (result is None or type(result) in [ + bool, float, long, str, int]): + self._objects[newuuid] = result + result = newuuid + + ### DEBUG + self.logger.debug("RET INVOKE %s%s = %s -> %s(%s, %s) " % ( + "(uuid %s) " % str(newuuid) if newuuid else "", str(result), uuid, + operation, str(args), str(kwargs))) + ######## + + return result + + def set(self, uuid, name, value): + ### DEBUG + self.logger.debug("SET %s %s %s" % (uuid, name, str(value))) + + self.debuger.dump_set(uuid, name, value) + ######## + + obj = self.get_object(uuid) + setattr(obj, name, value) + + ### DEBUG + self.logger.debug("RET SET %s = %s -> set(%s, %s)" % (str(value), uuid, name, + str(value))) + ######## + + return value + + def get(self, uuid, name): + ### DEBUG + self.logger.debug("GET %s %s" % (uuid, name)) + + self.debuger.dump_get(uuid, name) + ######## + + obj = self.get_object(uuid) + result = getattr(obj, name) + + ### DEBUG + self.logger.debug("RET GET %s = %s -> get(%s)" % (str(result), uuid, name)) + ######## + + return result + + def shutdown(self): + ### DEBUG + self.debuger.dump_shutdown() + ######## + + sys.stdout.flush() + sys.stderr.flush() + + ### DEBUG + self.logger.debug("SHUTDOWN") + ######## + + def replace_args(self, args): + realargs = [self.get_object(arg) if \ + str(arg).startswith("uuid") else arg for arg in args] + + return realargs + + def replace_kwargs(self, kwargs): + realkwargs = dict([(k, self.get_object(v) \ + if str(v).startswith("uuid") else v) \ + for k,v in kwargs.iteritems()]) + + return realkwargs + diff --git a/src/nepi/resources/netns/netnswrapper_debug.py b/src/nepi/resources/netns/netnswrapper_debug.py new file mode 100644 index 00000000..b2735631 --- /dev/null +++ b/src/nepi/resources/netns/netnswrapper_debug.py @@ -0,0 +1,191 @@ +# +# 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 + + +############ METHODS DEBUG NETNSWRAPPER EXECUTION +## +## The netnswrapper works in ditributed mode, receiving instructions from +## a remote client. This makes it very hard to debug scripting errors +## in the client side. To simplify error debugging, when set to debug mode, +## the ns3wrapper dumps every executed line to a script that can be then +## executed locally to reproduce and debug the experiment. +## +########################################################################### + +import logging + +class NetNSWrapperDebuger(object): + def __init__(self, enabled): + super(NetNSWrapperDebuger, self).__init__() + self._enabled = enabled + self._script_path = "debug.py" + + self.dump_header() + + @property + def enabled(self): + return self._enabled + + @property + def script_path(self): + return self._script_path + + def dump_to_script(self, command): + f = open(self.script_path, "a") + f.write("%s" % command) + f.close() + + def dump_header(self): + if not self.enabled: + return + + header = """ +from netnswrapper import NetNSWrapper + +wrapper = NS3Wrapper() + +""" + self.dump_to_script(header) + + def dump_factory(self, uuid, type_name, kwargs): + if not self.enabled: + return + + command = ("kwargs = %(kwargs)s\n" + "%(uuid)s = wrapper.factory(%(type_name)s, **kwargs)\n\n" + ) % dict({ + "uuid": self.format_value(uuid), + "type_name": self.format_value(type_name), + "kwargs": self.format_kwargs(kwargs) + }) + + self.dump_to_script(command) + + def dump_create(self, uuid, clazzname, args): + if not self.enabled: + return + + command = ("args = %(args)s\n" + "%(uuid)s = wrapper.create(%(clazzname)s, *args)\n\n" + ) % dict({ + "uuid": self.format_value(uuid), + "clazzname": self.format_value(clazzname), + "args": self.format_args(args), + }) + + self.dump_to_script(command) + + def dump_invoke(self, newuuid, uuid, operation, args, kwargs): + if not self.enabled: + return + + command = ("args = %(args)s\n" + "kwargs = %(kwargs)s\n" + "%(newuuid)s = wrapper.invoke(%(uuid)s, %(operation)s, *args, **kwargs)\n\n" + ) % dict({ + "newuuid": self.format_value(newuuid) if newuuid else "nothing", + "uuid": self.format_value(uuid), + "operation": self.format_value(operation), + "args": self.format_args(args), + "kwargs": self.format_kwargs(kwargs), + }) + + self.dump_to_script(command) + + def dump_set(self, uuid, name, value): + if not self.enabled: + return + + command = ("wrapper.set(%(uuid)s, %(name)s, %(value)s)\n\n" + ) % dict({ + "uuid": self.format_value(uuid), + "name": self.format_value(name), + "value": self.format_value(value), + }) + + self.dump_to_script(command) + + def dump_get(self, uuid, name): + if not self.enabled: + return + + command = ("wrapper.get(%(uuid)s, %(name)s)\n\n" + ) % dict({ + "uuid": self.format_value(uuid), + "name": self.format_value(name), + }) + + self.dump_to_script(command) + + def dump_start(self): + if not self.enabled: + return + + command = "wrapper.start()\n\n" + self.dump_to_script(command) + + def dump_stop(self, time = None): + if not self.enabled: + return + + command = ("wrapper.stop(time=%(time)s)\n\n" + ) % dict({ + "time": self.format_value(time) if time else "None", + }) + + self.dump_to_script(command) + + def dump_shutdown(self): + if not self.enabled: + return + + command = "wrapper.shutdown()\n\n" + self.dump_to_script(command) + + def dump_add_static_route(self, uuid, args): + if not self.enabled: + return + + command = ("args = %(args)s\n" + "wrapper._add_static_route(%(uuid)s, *args)\n\n" + ) % dict({ + "uuid": self.format_value(uuid), + "args": self.format_args(args), + }) + + self.dump_to_script(command) + + def format_value(self, value): + if isinstance(value, str) and value.startswith("uuid"): + return value.replace("-", "") + + import pprint + return pprint.pformat(value) + + def format_args(self, args): + fargs = map(self.format_value, args) + return "[%s]" % ",".join(fargs) + + def format_kwargs(self, kwargs): + fkwargs = map(lambda (k,w): + "%s: %s" % (self.format_value(k), self.format_value(w)), + kwargs.iteritems()) + + return "dict({%s})" % ",".join(fkwargs) + diff --git a/src/nepi/resources/ns3/ns3wrapper_debug.py b/src/nepi/resources/ns3/ns3wrapper_debug.py index 51eae837..99a6e5bb 100644 --- a/src/nepi/resources/ns3/ns3wrapper_debug.py +++ b/src/nepi/resources/ns3/ns3wrapper_debug.py @@ -20,8 +20,8 @@ ############ METHODS DEBUG NS3WRAPPER EXECUTION ## -## The ns3wrapper works in an interactive mode, receiving instructions in -## a distributed manner. This makes it very hard to debug scripting errors +## The ns3wrapper works in ditributed mode, receiving instructions from +## a remote client. This makes it very hard to debug scripting errors ## in the client side. To simplify error debugging, when set to debug mode, ## the ns3wrapper dumps every executed line to a script that can be then ## executed locally to reproduce and debug the experiment. diff --git a/test/lib/test_utils.py b/test/lib/test_utils.py index 9422114f..958cfc0a 100644 --- a/test/lib/test_utils.py +++ b/test/lib/test_utils.py @@ -149,3 +149,18 @@ def skipIfNotSfi(func): return func(*args, **kwargs) return wrapped + +def skipIf(cond, text): + def wrapped(func, text): + name = func.__name__ + + def banner(*args, **kwargs): + sys.stderr.write("*** WARNING: Skipping test %s: `%s'\n" % + (name, text)) + return None + return banner + + return (lambda func: wrapped(func, text)) if cond else lambda func: func + + + diff --git a/test/resources/netns/netnswrapper.py b/test/resources/netns/netnswrapper.py new file mode 100755 index 00000000..db3bf7e8 --- /dev/null +++ b/test/resources/netns/netnswrapper.py @@ -0,0 +1,133 @@ +#!/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.netnswrapper import NetNSWrapper + +from test_utils import skipIf + +import os +import subprocess +import sys +import time +import unittest + +class NetNSWrapperTest(unittest.TestCase): + def setUp(self): + pass + + @skipIf(os.getuid() != 0, "Test requires root privileges") + def test_run_ping_routing(self): + wrapper = NetNSWrapper() + + ### create 3 nodes + #n1 = netns.Node() + #n2 = netns.Node() + #n3 = netns.Node() + n1 = wrapper.create("Node") + n2 = wrapper.create("Node") + n3 = wrapper.create("Node") + + ### add interfaces to nodes + #i1 = n1.add_if() + #i2a = n2.add_if() + #i2b = n2.add_if() + #i3 = n3.add_if() + i1 = wrapper.invoke(n1, "add_if") + i2a = wrapper.invoke(n2, "add_if") + i2b = wrapper.invoke(n2, "add_if") + i3 = wrapper.invoke(n3, "add_if") + + ### set interfaces up + # i1.up = i2a.up = i2b.up = i3.up = True + wrapper.set(i1, "up", True) + wrapper.set(i2a, "up", True) + wrapper.set(i2b, "up", True) + wrapper.set(i3, "up", True) + + ### create 2 switches + #l1 = netns.Switch() + #l2 = netns.Switch() + l1 = wrapper.create("Switch") + l2 = wrapper.create("Switch") + + ### connect interfaces to switches + #l1.connect(i1) + #l1.connect(i2a) + #l2.connect(i2b) + #l2.connect(i3) + wrapper.invoke(l1, "connect", i1) + wrapper.invoke(l1, "connect", i2a) + wrapper.invoke(l2, "connect", i2b) + wrapper.invoke(l2, "connect", i3) + + ### set switched up + # l1.up = l2.up = True + wrapper.set(l1, "up", True) + wrapper.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) + wrapper.invoke(i1, "add_v4_address", "10.0.0.1", 24) + wrapper.invoke(i2a, "add_v4_address", "10.0.0.2", 24) + wrapper.invoke(i2b, "add_v4_address", "10.0.1.1", 24) + wrapper.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') + wrapper.invoke(n1, "add_route", prefix = "10.0.1.0", prefix_len = 24, + nexthop = "10.0.0.2") + wrapper.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 = wrapper.create("open", path1, "w") + file2 = wrapper.create("open", path2, "w") + a1 = wrapper.invoke(n1, "Popen", ["ping", "-qc1", "10.0.1.2"], stdout = file1) + a2 = wrapper.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 = wrapper.invoke(a1, "poll") + p2 = wrapper.invoke(a2, "poll") + + stdout1 = open(path1, "r") + stdout2 = open(path2, "r") + + print stdout1.read(), stdout2.read() + +if __name__ == '__main__': + unittest.main() + -- 2.43.0