Adding NetNSApplication and test
[nepi.git] / src / nepi / resources / linux / netns / netnsemulation.py
diff --git a/src/nepi/resources/linux/netns/netnsemulation.py b/src/nepi/resources/linux/netns/netnsemulation.py
new file mode 100644 (file)
index 0000000..c2d65d3
--- /dev/null
@@ -0,0 +1,363 @@
+#
+#    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>
+
+from nepi.execution.attribute import Attribute, Flags, Types
+from nepi.execution.trace import Trace, TraceAttr
+from nepi.execution.resource import ResourceManager, clsinit_copy, \
+        ResourceState, ResourceFactory, reschedule_delay
+from nepi.resources.linux.application import LinuxApplication
+from nepi.util.timefuncs import tnow, tdiffsec
+from nepi.resources.netns.netnsemulation import NetNSEmulation
+from nepi.resources.linux.netns.netnsclient import LinuxNetNSClient
+
+import os
+import time
+import threading
+
+@clsinit_copy
+class LinuxNetNSEmulation(LinuxApplication, NetNSEmulation):
+    _rtype = "LinuxNetNSEmulation"
+
+    @classmethod
+    def _register_attributes(cls):
+        verbose = Attribute("verbose",
+            "True to output debugging info for the client-server communication",
+            type = Types.Bool,
+            flags = Flags.Design)
+
+        enable_dump = Attribute("enableDump",
+            "Enable dumping the remote executed commands to a script "
+            "in order to later reproduce and debug the experiment",
+            type = Types.Bool,
+            default = False,
+            flags = Flags.Design)
+
+        version = Attribute("version",
+            "Version of netns to install from nsam repo",
+            default = "netns-dev", 
+            flags = Flags.Design)
+
+        cls._register_attribute(enable_dump)
+        cls._register_attribute(verbose)
+        cls._register_attribute(version)
+
+    def __init__(self, ec, guid):
+        LinuxApplication.__init__(self, ec, guid)
+        NetNSEmulation.__init__(self)
+
+        self._client = None
+        self._home = "netns-emu-%s" % self.guid
+        self._socket_name = "netns-%s.sock" % os.urandom(4).encode('hex')
+
+    @property
+    def socket_name(self):
+        return self._socket_name
+
+    @property
+    def remote_socket(self):
+        return os.path.join(self.run_home, self.socket_name)
+
+    def upload_sources(self):
+        self.node.mkdir(os.path.join(self.node.src_dir, "netnswrapper"))
+
+        # upload wrapper python script
+        wrapper = os.path.join(os.path.dirname(__file__), "..", "..", "netns", 
+                "netnswrapper.py")
+
+        self.node.upload(wrapper,
+                os.path.join(self.node.src_dir, "netnswrapper", "netnswrapper.py"),
+                overwrite = False)
+
+        # upload wrapper debug python script
+        wrapper_debug = os.path.join(os.path.dirname(__file__), "..", "..", "netns", 
+                "netnswrapper_debug.py")
+
+        self.node.upload(wrapper_debug,
+                os.path.join(self.node.src_dir, "netnswrapper", "netnswrapper_debug.py"),
+                overwrite = False)
+
+        # upload server python script
+        server = os.path.join(os.path.dirname(__file__), "..", "..", "netns",
+                "netnsserver.py")
+
+        self.node.upload(server,
+                os.path.join(self.node.src_dir, "netnswrapper", "netnsserver.py"),
+                overwrite = False)
+
+        # Upload user defined sources
+        self.node.mkdir(os.path.join(self.node.src_dir, "netns"))
+        src_dir = os.path.join(self.node.src_dir, "netns")
+
+        super(LinuxNetNSEmulation, self).upload_sources(src_dir = src_dir)
+    
+    def upload_extra_sources(self, sources = None, src_dir = None):
+        return super(LinuxNetNSEmulation, self).upload_sources(
+                sources = sources, 
+                src_dir = src_dir)
+
+    def upload_start_command(self):
+        command = self.get("command")
+        env = self.get("env")
+
+        # We want to make sure the emulator is running
+        # before the experiment starts.
+        # Run the command as a bash script in background,
+        # in the host ( but wait until the command has
+        # finished to continue )
+        env = self.replace_paths(env)
+        command = self.replace_paths(command)
+
+        shfile = os.path.join(self.app_home, "start.sh")
+        self.node.upload_command(command, 
+                    shfile = shfile,
+                    env = env,
+                    overwrite = True)
+
+        # Run the wrapper 
+        self._run_in_background()
+
+        # Wait until the remote socket is created
+        self.wait_remote_socket()
+
+    def do_deploy(self):
+        if not self.node or self.node.state < ResourceState.READY:
+            self.debug("---- RESCHEDULING DEPLOY ---- node state %s " % self.node.state )
+            
+            # ccnd needs to wait until node is deployed and running
+            self.ec.schedule(reschedule_delay, self.deploy)
+        else:
+            if not self.get("command"):
+                self.set("command", self._start_command)
+            
+            if not self.get("depends"):
+                self.set("depends", self._dependencies)
+
+            if self.get("sources"):
+                sources = self.get("sources")
+                source = sources.split(" ")[0]
+                basename = os.path.basename(source)
+                version = ( basename.strip().replace(".tar.gz", "")
+                    .replace(".tar","")
+                    .replace(".gz","")
+                    .replace(".zip","") )
+
+                self.set("version", version)
+                self.set("sources", source)
+
+            if not self.get("build"):
+                self.set("build", self._build)
+
+            if not self.get("env"):
+                self.set("env", self._environment)
+
+            self.do_discover()
+            self.do_provision()
+
+            # Create client
+            self._client = LinuxNetNSClient(self)
+
+            self.set_ready()
+
+    def do_release(self):
+        self.info("Releasing resource")
+
+        tear_down = self.get("tearDown")
+        if tear_down:
+            self.node.execute(tear_down)
+
+        self.do_stop()
+        self._client.shutdown()
+        LinuxApplication.do_stop(self)
+        
+        super(LinuxApplication, self).do_release()
+
+    @property
+    def _start_command(self):
+        command = [] 
+
+        #command.append("sudo")
+        command.append("PYTHONPATH=$PYTHONPATH:${SRC}/netnswrapper/")
+        command.append("python ${SRC}/netnswrapper/netnsserver.py -S %s" % \
+                os.path.basename(self.remote_socket) )
+
+        if self.get("enableDump"):
+            command.append("-D")
+
+        if self.get("verbose"):
+            command.append("-v")
+
+        command = " ".join(command)
+        return command
+
+    @property
+    def _dependencies(self):
+        if self.node.use_rpm:
+            return ( " python python-devel mercurial unzip bridge-utils iproute")
+        elif self.node.use_deb:
+            return ( " python python-dev mercurial unzip bridge-utils iproute")
+        return ""
+
+    @property
+    def netns_repo(self):
+        return "http://nepi.inria.fr/code/netns"
+
+    @property
+    def netns_version(self):
+        version = self.get("version")
+        return version or "dev"
+
+    @property
+    def python_unshare_repo(self):
+        return "http://nepi.inria.fr/code/python-unshare"
+
+    @property
+    def python_unshare_version(self):
+        return "dev"
+
+    @property
+    def python_passfd_repo(self):
+        return "http://nepi.inria.fr/code/python-passfd"
+
+    @property
+    def python_passfd_version(self):
+        return "dev"
+
+    @property
+    def netns_src(self):
+        location = "${SRC}/netns/%(version)s" \
+                    % {
+                        "version": self.netns_version,
+                      }
+
+        return location
+
+    @property
+    def python_unshare_src(self):
+        location = "${SRC}/python_unshare/%(version)s" \
+                    % {
+                        "version": self.python_unshare_version,
+                      }
+
+        return location
+
+    @property
+    def python_passfd_src(self):
+        location = "${SRC}/python_passfd/%(version)s" \
+                    % {
+                        "version": self.python_passfd_version,
+                      }
+
+        return location
+
+    def clone_command(self, name, repo, src):
+        clone_cmd = (
+                # Test if alredy cloned
+                " ( "
+                "  ( "
+                "    ( test -d %(src)s ) "
+                "   && echo '%(name)s binaries found, nothing to do'"
+                "  ) "
+                " ) "
+                "  || " 
+                # clone source code
+                " ( "
+                "   mkdir -p %(src)s && "
+                "   hg clone %(repo)s %(src)s"
+                " ) "
+             ) % {
+                    "repo": repo,
+                    "src": src,
+                    "name": name,
+                 }
+
+        return clone_cmd
+
+    @property
+    def _build(self):
+        netns_clone = self.clone_command("netns", self.netns_repo, 
+                self.netns_src)
+        python_unshare_clone = self.clone_command("python_unshare", 
+                self.python_unshare_repo, self.python_unshare_src)
+        python_passfd_clone = self.clone_command("python_passfd", 
+                self.python_passfd_repo, self.python_passfd_src)
+
+        build_cmd = (
+                # Netns installation
+                "( %(netns_clone)s )"
+                "  && "
+                "( %(python_unshare_clone)s )"
+                "  && "
+                "( %(python_passfd_clone)s )"
+             ) % { 
+                    "netns_clone": netns_clone,
+                    "python_unshare_clone": python_unshare_clone,  
+                    "python_passfd_clone": python_passfd_clone,  
+                 }
+
+        return build_cmd
+
+    @property
+    def _environment(self):
+        env = []
+        env.append("PYTHONPATH=$PYTHONPAH:%(netns_src)s/src/:%(python_unshare_src)s/src:%(python_passfd_src)s/src}" % { 
+                    "netns_src": self.netns_src,
+                    "python_unshare_src": self.python_unshare_src,
+                    "python_passfd_src": self.python_passfd_src,
+                 })
+
+        return " ".join(env) 
+
+    def replace_paths(self, command):
+        """
+        Replace all special path tags with shell-escaped actual paths.
+        """
+        return ( command
+            .replace("${USR}", self.node.usr_dir)
+            .replace("${LIB}", self.node.lib_dir)
+            .replace("${BIN}", self.node.bin_dir)
+            .replace("${SRC}", self.node.src_dir)
+            .replace("${SHARE}", self.node.share_dir)
+            .replace("${EXP}", self.node.exp_dir)
+            .replace("${EXP_HOME}", self.node.exp_home)
+            .replace("${APP_HOME}", self.app_home)
+            .replace("${RUN_HOME}", self.run_home)
+            .replace("${NODE_HOME}", self.node.node_home)
+            .replace("${HOME}", self.node.home_dir)
+            )
+
+    def valid_connection(self, guid):
+        # TODO: Validate!
+        return True
+
+    def wait_remote_socket(self):
+        """ Waits until the remote socket is created
+        """
+        command = " [ -e %s ] && echo 'DONE' " % self.remote_socket
+
+        for i in xrange(200):
+            (out, err), proc = self.node.execute(command, retry = 1, 
+                    with_lock = True)
+
+            if out.find("DONE") > -1:
+                break
+        else:
+            raise RuntimeError("Remote socket not found at %s" % \
+                    self.remote_socket)
+    
+