nemu-0.2/0000755000175000017500000000000012235530226013111 5ustar tinchotincho00000000000000nemu-0.2/COPYING0000644000175000017500000004325412225521027014152 0ustar tinchotincho00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 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 2 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, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. nemu-0.2/setup.cfg0000644000175000017500000000002012225521027014720 0ustar tinchotincho00000000000000[clean] all = 1 nemu-0.2/DEPENDENCIES0000644000175000017500000000045612225521027014665 0ustar tinchotincho00000000000000Required packages for using nemu ================================ python-unshare http://pypi.python.org/pypi/python-unshare python-passfd http://pypi.python.org/pypi/python-passfd linux-kernel >= 2.6.35 http://kernel.org/ bridge-utils iproute procps xauth Needed only for X11 forwarding support. nemu-0.2/src/0000755000175000017500000000000012235530226013700 5ustar tinchotincho00000000000000nemu-0.2/src/nemu/0000755000175000017500000000000012235530226014644 5ustar tinchotincho00000000000000nemu-0.2/src/nemu/environ.py0000644000175000017500000001523412225521027016701 0ustar tinchotincho00000000000000# vim:ts=4:sw=4:et:ai:sts=4 # -*- coding: utf-8 -*- # Copyright 2010, 2011 INRIA # Copyright 2011 Martín Ferrari # # This file is part of Nemu. # # Nemu is free software: you can redistribute it and/or modify it under the # terms of the GNU General Public License version 2, as published by the Free # Software Foundation. # # Nemu 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 # Nemu. If not, see . import errno, os, os.path, socket, subprocess, sys, syslog from syslog import LOG_ERR, LOG_WARNING, LOG_NOTICE, LOG_INFO, LOG_DEBUG __all__ = ["IP_PATH", "TC_PATH", "BRCTL_PATH", "SYSCTL_PATH", "HZ"] __all__ += ["TCPDUMP_PATH", "NETPERF_PATH", "XAUTH_PATH", "XDPYINFO_PATH"] __all__ += ["execute", "backticks", "eintr_wrapper"] __all__ += ["find_listen_port"] __all__ += ["LOG_ERR", "LOG_WARNING", "LOG_NOTICE", "LOG_INFO", "LOG_DEBUG"] __all__ += ["set_log_level", "logger"] __all__ += ["error", "warning", "notice", "info", "debug"] def find_bin(name, extra_path = None): """Try hard to find the location of needed programs.""" search = [] if "PATH" in os.environ: search += os.environ["PATH"].split(":") search.extend(os.path.join(x, y) for x in ("/", "/usr/", "/usr/local/") for y in ("bin", "sbin")) if extra_path: search += extra_path for dirr in search: path = os.path.join(dirr, name) if os.path.exists(path): return path return None def find_bin_or_die(name, extra_path = None): """Try hard to find the location of needed programs; raise on failure.""" res = find_bin(name, extra_path) if not res: raise RuntimeError("Cannot find `%s', impossible to continue." % name) return res IP_PATH = find_bin_or_die("ip") TC_PATH = find_bin_or_die("tc") BRCTL_PATH = find_bin_or_die("brctl") SYSCTL_PATH = find_bin_or_die("sysctl") # Optional tools TCPDUMP_PATH = find_bin("tcpdump") NETPERF_PATH = find_bin("netperf") XAUTH_PATH = find_bin("xauth") XDPYINFO_PATH = find_bin("xdpyinfo") # Seems this is completely bogus. At least, we can assume that the internal HZ # is bigger than this. HZ = os.sysconf("SC_CLK_TCK") try: os.stat("/sys/class/net") except: raise RuntimeError("Sysfs does not seem to be mounted, impossible to " + "continue.") def execute(cmd): """Execute a command, if the return value is non-zero, raise an exception. Raises: RuntimeError: the command was unsuccessful (return code != 0). """ debug("execute(%s)" % cmd) null = open("/dev/null", "r+") proc = subprocess.Popen(cmd, stdout = null, stderr = subprocess.PIPE) _, err = proc.communicate() if proc.returncode != 0: raise RuntimeError("Error executing `%s': %s" % (" ".join(cmd), err)) def backticks(cmd): """Execute a command and capture its output. If the return value is non-zero, raise an exception. Returns: (stdout, stderr): tuple containing the captured output. Raises: RuntimeError: the command was unsuccessful (return code != 0). """ debug("backticks(%s)" % cmd) proc = subprocess.Popen(cmd, stdout = subprocess.PIPE, stderr = subprocess.PIPE) out, err = proc.communicate() if proc.returncode != 0: raise RuntimeError("Error executing `%s': %s" % (" ".join(cmd), err)) return out def eintr_wrapper(func, *args): "Wraps some callable with a loop that retries on EINTR." while True: try: return func(*args) except OSError, ex: # pragma: no cover if ex.errno == errno.EINTR: continue raise except IOError, ex: # pragma: no cover if ex.errno == errno.EINTR: continue raise def find_listen_port(family = socket.AF_INET, type = socket.SOCK_STREAM, proto = 0, addr = "127.0.0.1", min_port = 1, max_port = 65535): sock = socket.socket(family, type, proto) for port in range(min_port, max_port + 1): try: sock.bind((addr, port)) return sock, port except socket.error: pass raise RuntimeError("Cannot find an usable port in the range specified") # Logging _log_level = LOG_WARNING _log_use_syslog = False _log_stream = sys.stderr _log_syslog_opts = () _log_pid = os.getpid() def set_log_level(level): "Sets the log level for console messages, does not affect syslog logging." global _log_level assert level > LOG_ERR and level <= LOG_DEBUG _log_level = level def set_log_output(stream): "Redirect console messages to the provided stream." global _log_stream assert hasattr(stream, "write") and hasattr(stream, "flush") _log_stream = stream def log_use_syslog(use = True, ident = None, logopt = 0, facility = syslog.LOG_USER): "Enable or disable the use of syslog for logging messages." global _log_use_syslog, _log_syslog_opts _log_syslog_opts = (ident, logopt, facility) _log_use_syslog = use _init_log() def _init_log(): if not _log_use_syslog: syslog.closelog() return (ident, logopt, facility) = _log_syslog_opts if not ident: #ident = os.path.basename(sys.argv[0]) ident = "nemu" syslog.openlog("%s[%d]" % (ident, os.getpid()), logopt, facility) info("Syslog logging started") def logger(priority, message): "Print a log message in syslog, console or both." if _log_use_syslog: if os.getpid() != _log_pid: _init_log() # Need to tell syslog the new PID. syslog.syslog(priority, message) return if priority > _log_level: return eintr_wrapper(_log_stream.write, "[%d] %s\n" % (os.getpid(), message.rstrip())) _log_stream.flush() def error(message): logger(LOG_ERR, message) def warning(message): logger(LOG_WARNING, message) def notice(message): logger(LOG_NOTICE, message) def info(message): logger(LOG_INFO, message) def debug(message): logger(LOG_DEBUG, message) def _custom_hook(tipe, value, traceback): # pragma: no cover """Custom exception hook, to print nested exceptions information.""" if hasattr(value, "child_traceback"): sys.stderr.write("Nested exception, original traceback " + "(most recent call last):\n") sys.stderr.write(value.child_traceback + ("-" * 70) + "\n") sys.__excepthook__(tipe, value, traceback) sys.excepthook = _custom_hook nemu-0.2/src/nemu/interface.py0000644000175000017500000004515612225521027017167 0ustar tinchotincho00000000000000# vim:ts=4:sw=4:et:ai:sts=4 # -*- coding: utf-8 -*- # Copyright 2010, 2011 INRIA # Copyright 2011 Martín Ferrari # # This file is part of Nemu. # # Nemu is free software: you can redistribute it and/or modify it under the # terms of the GNU General Public License version 2, as published by the Free # Software Foundation. # # Nemu 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 # Nemu. If not, see . import os, weakref import nemu.iproute from nemu.environ import * __all__ = ['NodeInterface', 'P2PInterface', 'ImportedInterface', 'ImportedNodeInterface', 'Switch'] class Interface(object): """Just a base class for the *Interface classes: assign names and handle destruction.""" _nextid = 0 @staticmethod def _gen_next_id(): n = Interface._nextid Interface._nextid += 1 return n @staticmethod def _gen_if_name(): n = Interface._gen_next_id() # Max 15 chars return "NETNSif-%.4x%.3x" % (os.getpid(), n) def __init__(self, index): self._idx = index debug("%s(0x%x).__init__(), index = %d" % (self.__class__.__name__, id(self), index)) def __del__(self): debug("%s(0x%x).__del__()" % (self.__class__.__name__, id(self))) self.destroy() def destroy(self): raise NotImplementedError @property def index(self): """Interface index as seen by the kernel.""" return self._idx @property def control(self): """Associated interface in the main name space (if it exists). Only control interfaces can be put into a Switch, for example.""" return None class NSInterface(Interface): """Add user-facing methods for interfaces that go into a netns.""" def __init__(self, node, index): super(NSInterface, self).__init__(index) self._slave = node._slave # Disable auto-configuration # you wish: need to take into account the nonetns mode; plus not # touching some pre-existing ifaces #node.system([SYSCTL_PATH, '-w', 'net.ipv6.conf.%s.autoconf=0' % #self.name]) node._add_interface(self) # some black magic to automatically get/set interface attributes def __getattr__(self, name): # If name starts with _, it must be a normal attr if name[0] == '_': return super(Interface, self).__getattribute__(name) try: slave = super(Interface, self).__getattribute__("_slave") except: # Not initialised yet return super(Interface, self).__getattribute__(name) iface = slave.get_if_data(self.index) return getattr(iface, name) def __setattr__(self, name, value): if name[0] == '_': # forbid anything that doesn't start with a _ super(Interface, self).__setattr__(name, value) return iface = nemu.iproute.interface(index = self.index) setattr(iface, name, value) return self._slave.set_if(iface) def add_v4_address(self, address, prefix_len, broadcast = None): addr = nemu.iproute.ipv4address(address, prefix_len, broadcast) self._slave.add_addr(self.index, addr) def add_v6_address(self, address, prefix_len): addr = nemu.iproute.ipv6address(address, prefix_len) self._slave.add_addr(self.index, addr) def del_v4_address(self, address, prefix_len, broadcast = None): addr = nemu.iproute.ipv4address(address, prefix_len, broadcast) self._slave.del_addr(self.index, addr) def del_v6_address(self, address, prefix_len): addr = nemu.iproute.ipv6address(address, prefix_len) self._slave.del_addr(self.index, addr) def get_addresses(self): addresses = self._slave.get_addr_data(self.index) ret = [] for a in addresses: if hasattr(a, 'broadcast'): ret.append(dict( address = a.address, prefix_len = a.prefix_len, broadcast = a.broadcast, family = 'inet')) else: ret.append(dict( address = a.address, prefix_len = a.prefix_len, family = 'inet6')) return ret class NodeInterface(NSInterface): """Class to create and handle a virtual interface inside a name space, it can be connected to a Switch object with emulation of link characteristics.""" def __init__(self, node): """Create a new interface. `node' is the name space in which this interface should be put.""" self._slave = None if1 = nemu.iproute.interface(name = self._gen_if_name()) if2 = nemu.iproute.interface(name = self._gen_if_name()) ctl, ns = nemu.iproute.create_if_pair(if1, if2) try: nemu.iproute.change_netns(ns, node.pid) except: nemu.iproute.del_if(ctl) # the other interface should go away automatically raise self._control = SlaveInterface(ctl.index) super(NodeInterface, self).__init__(node, ns.index) @property def control(self): return self._control def destroy(self): if not self._slave: return debug("NodeInterface(0x%x).destroy()" % id(self)) if self.index in self._slave.get_if_data(): self._slave.del_if(self.index) self._slave = None class P2PInterface(NSInterface): """Class to create and handle point-to-point interfaces between name spaces, without using Switch objects. Those do not allow any kind of traffic shaping. As two interfaces need to be created, instead of using the class constructor, use the P2PInterface.create_pair() static method.""" @staticmethod def create_pair(node1, node2): """Create and return a pair of connected P2PInterface objects, assigned to name spaces represented by `node1' and `node2'.""" if1 = nemu.iproute.interface(name = P2PInterface._gen_if_name()) if2 = nemu.iproute.interface(name = P2PInterface._gen_if_name()) pair = nemu.iproute.create_if_pair(if1, if2) try: nemu.iproute.change_netns(pair[0], node1.pid) nemu.iproute.change_netns(pair[1], node2.pid) except: nemu.iproute.del_if(pair[0]) # the other interface should go away automatically raise o1 = P2PInterface.__new__(P2PInterface) super(P2PInterface, o1).__init__(node1, pair[0].index) o2 = P2PInterface.__new__(P2PInterface) super(P2PInterface, o2).__init__(node2, pair[1].index) return o1, o2 def __init__(self): "Not to be called directly. Use P2PInterface.create_pair()" raise RuntimeError(P2PInterface.__init__.__doc__) def destroy(self): if not self._slave: return debug("P2PInterface(0x%x).destroy()" % id(self)) if self.index in self._slave.get_if_data(): self._slave.del_if(self.index) self._slave = None class ImportedNodeInterface(NSInterface): """Class to handle already existing interfaces inside a name space: real devices, tun devices, etc. The flag 'migrate' in the constructor indicates that the interface needs to be moved inside the name space. On destruction, the interface will be restored to the original name space and will try to restore the original state.""" def __init__(self, node, iface, migrate = True): self._slave = None self._migrate = migrate if self._migrate: iface = nemu.iproute.get_if(iface) self._original_state = iface.copy() # Change the name to avoid clashes iface.name = self._gen_if_name() nemu.iproute.set_if(iface) # Migrate it nemu.iproute.change_netns(iface, node.pid) else: iface = node._slave.get_if_data(iface) self._original_state = iface.copy() super(ImportedNodeInterface, self).__init__(node, iface.index) def destroy(self): # override: restore as much as possible if not self._slave: return debug("ImportedNodeInterface(0x%x).destroy()" % id(self)) if self.index in self._slave.get_if_data(): if self._migrate: self._slave.change_netns(self.index, os.getpid()) else: self._slave.set_if(self._original_state) if self._migrate: # else, assume it is already in the main name space nemu.iproute.set_if(self._original_state) self._slave = None class TapNodeInterface(NSInterface): """Class to create a tap interface inside a name space, it can be connected to a Switch object with emulation of link characteristics.""" def __init__(self, node, use_pi = False): """Create a new tap interface. 'node' is the name space in which this interface should be put.""" self._fd = None self._slave = None iface = nemu.iproute.interface(name = self._gen_if_name()) iface, self._fd = nemu.iproute.create_tap(iface, use_pi = use_pi) nemu.iproute.change_netns(iface.name, node.pid) super(TapNodeInterface, self).__init__(node, iface.index) @property def fd(self): return self._fd def destroy(self): if not self._fd: return debug("TapNodeInterface(0x%x).destroy()" % id(self)) try: os.close(self._fd) except: pass class TunNodeInterface(NSInterface): """Class to create a tun interface inside a name space, it can be connected to a Switch object with emulation of link characteristics.""" def __init__(self, node, use_pi = False): """Create a new tap interface. 'node' is the name space in which this interface should be put.""" self._fd = None self._slave = None iface = nemu.iproute.interface(name = self._gen_if_name()) iface, self._fd = nemu.iproute.create_tap(iface, use_pi = use_pi, tun = True) nemu.iproute.change_netns(iface.name, node.pid) super(TunNodeInterface, self).__init__(node, iface.index) @property def fd(self): return self._fd def destroy(self): if not self._fd: return debug("TunNodeInterface(0x%x).destroy()" % id(self)) try: os.close(self._fd) except: pass class ExternalInterface(Interface): """Add user-facing methods for interfaces that run in the main namespace.""" @property def control(self): # This is *the* control interface return self # some black magic to automatically get/set interface attributes def __getattr__(self, name): iface = nemu.iproute.get_if(self.index) return getattr(iface, name) def __setattr__(self, name, value): if name[0] == '_': # forbid anything that doesn't start with a _ super(ExternalInterface, self).__setattr__(name, value) return iface = nemu.iproute.interface(index = self.index) setattr(iface, name, value) return nemu.iproute.set_if(iface) def add_v4_address(self, address, prefix_len, broadcast = None): addr = nemu.iproute.ipv4address(address, prefix_len, broadcast) nemu.iproute.add_addr(self.index, addr) def add_v6_address(self, address, prefix_len): addr = nemu.iproute.ipv6address(address, prefix_len) nemu.iproute.add_addr(self.index, addr) def del_v4_address(self, address, prefix_len, broadcast = None): addr = nemu.iproute.ipv4address(address, prefix_len, broadcast) nemu.iproute.del_addr(self.index, addr) def del_v6_address(self, address, prefix_len): addr = nemu.iproute.ipv6address(address, prefix_len) nemu.iproute.del_addr(self.index, addr) def get_addresses(self): addresses = nemu.iproute.get_addr_data(self.index) ret = [] for a in addresses: if hasattr(a, 'broadcast'): ret.append(dict( address = a.address, prefix_len = a.prefix_len, broadcast = a.broadcast, family = 'inet')) else: ret.append(dict( address = a.address, prefix_len = a.prefix_len, family = 'inet6')) return ret class SlaveInterface(ExternalInterface): """Class to handle the main-name-space-facing half of NodeInterface. Does nothing, just avoids any destroy code.""" def destroy(self): pass class ImportedInterface(ExternalInterface): """Class to handle already existing interfaces. Analogous to ImportedNodeInterface, this class only differs in that the interface is not migrated inside the name space. This kind of interfaces can only be connected to Switch objects and not assigned to a name space. On destruction, the code will try to restore the interface to the state it was in before being imported into nemu.""" def __init__(self, iface): self._original_state = None iface = nemu.iproute.get_if(iface) self._original_state = iface.copy() super(ImportedInterface, self).__init__(iface.index) # FIXME: register somewhere for destruction! def destroy(self): # override: restore as much as possible if self._original_state: debug("ImportedInterface(0x%x).destroy()" % id(self)) nemu.iproute.set_if(self._original_state) self._original_state = None # Switch is just another interface type class Switch(ExternalInterface): @staticmethod def _gen_br_name(): n = Switch._gen_next_id() # Max 15 chars return "NETNSbr-%.4x%.3x" % (os.getpid(), n) def __init__(self, **args): """Creates a new Switch object, which models a linux bridge device. Parameters are passed to the set_parameters() method after creation.""" # attributes init self._idx = None self._parameters = {} self._ports = weakref.WeakValueDictionary() iface = nemu.iproute.create_bridge(self._gen_br_name()) super(Switch, self).__init__(iface.index) # FIXME: is this correct/desirable/etc? self.stp = False self.forward_delay = 0 # FIXME: register somewhere if args: self.set_parameters(**args) def __getattr__(self, name): iface = nemu.iproute.get_bridge(self.index) return getattr(iface, name) def __setattr__(self, name, value): if name[0] == '_': # forbid anything that doesn't start with a _ super(Switch, self).__setattr__(name, value) return # Set ports if name in ('up', 'mtu'): for i in self._ports.values(): if self._check_port(i.index): setattr(i, name, value) # Set bridge iface = nemu.iproute.bridge(index = self.index) setattr(iface, name, value) nemu.iproute.set_bridge(iface) def destroy(self): if not self.index: return debug("Switch(0x%x).destroy()" % id(self)) # Verify they are still there for p in self._ports.keys(): self._check_port(p) self.up = False for p in self._ports.values(): self.disconnect(p) self._ports.clear() nemu.iproute.del_bridge(self.index) self._idx = None def connect(self, iface): assert iface.control.index not in self._ports try: self._apply_parameters(self._parameters, iface.control) nemu.iproute.add_bridge_port(self.index, iface.control.index) except: self._apply_parameters({}, iface.control) raise iface.control.up = self.up iface.control.mtu = self.mtu self._ports[iface.control.index] = iface.control def _check_port(self, port_index): ports = nemu.iproute.get_bridge_data()[2] if self.index in ports and port_index in ports[self.index]: return True # else warning("Switch(0x%x): Port (index = %d) went away." % (id(self), port_index)) del self._ports[port_index] return False def disconnect(self, iface): assert iface.control.index in self._ports if not self._check_port(iface.control.index): return nemu.iproute.del_bridge_port(self.index, iface.control.index) self._apply_parameters({}, iface.control) del self._ports[iface.control.index] def set_parameters(self, bandwidth = None, delay = None, delay_jitter = None, delay_correlation = None, delay_distribution = None, loss = None, loss_correlation = None, dup = None, dup_correlation = None, corrupt = None, corrupt_correlation = None): """Set the parameters that control the link characteristics. For the description of each, refer to netem documentation: http://www.linuxfoundation.org/collaborate/workgroups/networking/netem Arguments: - `bandwidth' should be specified in bits per second. - `delay' and `delay_jitter' are specified in seconds. - `delay_distribution' is the name of a distribution description file; `iproute' comes by default with `normal', `pareto', and `paretonormal'. - `delay_correlation', `loss', `loss_correlation', `dup', `dup_correlation', `corrupt', and `corrupt_correlation' take a percentage value in the form of a number between 0 and 1. (50% is passed as 0.5).""" parameters = dict(bandwidth = bandwidth, delay = delay, delay_jitter = delay_jitter, delay_correlation = delay_correlation, delay_distribution = delay_distribution, loss = loss, loss_correlation = loss_correlation, dup = dup, dup_correlation = dup_correlation, corrupt = corrupt, corrupt_correlation = corrupt_correlation) try: self._apply_parameters(parameters) except: self._apply_parameters(self._parameters) raise self._parameters = parameters def _apply_parameters(self, parameters, port = None): for i in [port] if port else self._ports.values(): nemu.iproute.set_tc(i.index, **parameters) nemu-0.2/src/nemu/__init__.py0000644000175000017500000000437712225521027016766 0ustar tinchotincho00000000000000# vim:ts=4:sw=4:et:ai:sts=4 # -*- coding: utf-8 -*- # Copyright 2010, 2011 INRIA # Copyright 2011 Martín Ferrari # # This file is part of Nemu. # # Nemu is free software: you can redistribute it and/or modify it under the # terms of the GNU General Public License version 2, as published by the Free # Software Foundation. # # Nemu 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 # Nemu. If not, see . """Nemu package. Nemu (Netwok EMUlator) is a small Python library to create emulated networks and run and test programs in them. """ # pylint: disable=W0401,R0903 import os, pwd from nemu.node import * from nemu.interface import * class _Config(object): """Global configuration singleton for Nemu.""" def __init__(self): self._run_as = 65534 try: pwd.getpwnam('nobody') self._run_as = 'nobody' except KeyError: pass # User not found. def _set_run_as(self, user): """Setter for `run_as'.""" if str(user).isdigit(): uid = int(user) try: _user = pwd.getpwuid(uid)[0] except: raise AttributeError("UID %d does not exist" % int(user)) run_as = int(user) else: try: uid = pwd.getpwnam(str(user))[2] except: raise AttributeError("User %s does not exist" % str(user)) run_as = str(user) if uid == 0: raise AttributeError("Cannot run as root by default") self._run_as = run_as return run_as def _get_run_as(self): """Setter for `run_as'.""" return self._run_as run_as = property(_get_run_as, _set_run_as, None, "Default user to run applications as") config = _Config() # pylint: disable=C0103 # FIXME: set atfork hooks # http://code.google.com/p/python-atfork/source/browse/atfork/__init__.py #def set_cleanup_hooks(on_exit = False, on_signals = []): # pass nemu-0.2/src/nemu/iproute.py0000644000175000017500000010102112235526042016701 0ustar tinchotincho00000000000000# vim:ts=4:sw=4:et:ai:sts=4 # -*- coding: utf-8 -*- # Copyright 2010, 2011 INRIA # Copyright 2011 Martín Ferrari # # This file is part of Nemu. # # Nemu is free software: you can redistribute it and/or modify it under the # terms of the GNU General Public License version 2, as published by the Free # Software Foundation. # # Nemu 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 # Nemu. If not, see . import copy, fcntl, os, re, socket, struct, subprocess, sys from nemu.environ import * # helpers def _any_to_bool(any): if isinstance(any, bool): return any if isinstance(any, int): return any != 0 if isinstance(any, str): if any.isdigit(): return int(any) != 0 if any.lower() == "true": return True if any.lower() == "false": return False return any != "" return bool(any) def _positive(val): v = int(val) if v <= 0: raise ValueError("Invalid value: %d" % v) return v def _non_empty_str(val): if val == "": return None else: return str(val) def _fix_lladdr(addr): foo = addr.lower() if ":" in addr: # Verify sanity and split m = re.search("^" + ":".join(["([0-9a-f]{1,2})"] * 6) + "$", foo) if m is None: raise ValueError("Invalid address: `%s'." % addr) # Fill missing zeros and glue again return ":".join(("0" * (2 - len(x)) + x for x in m.groups())) # Fill missing zeros foo = "0" * (12 - len(foo)) + foo # Verify sanity and split m = re.search("^" + "([0-9a-f]{2})" * 6 + "$", foo) if m is None: raise ValueError("Invalid address: `%s'." % addr) # Glue return ":".join(m.groups()) def _make_getter(attr, conv = lambda x: x): def getter(self): return conv(getattr(self, attr)) return getter def _make_setter(attr, conv = lambda x: x): def setter(self, value): if value == None: setattr(self, attr, None) else: setattr(self, attr, conv(value)) return setter # classes for internal use class interface(object): """Class for internal use. It is mostly a data container used to easily pass information around; with some convenience methods.""" # information for other parts of the code changeable_attributes = ["name", "mtu", "lladdr", "broadcast", "up", "multicast", "arp"] # Index should be read-only index = property(_make_getter("_index")) up = property(_make_getter("_up"), _make_setter("_up", _any_to_bool)) mtu = property(_make_getter("_mtu"), _make_setter("_mtu", _positive)) lladdr = property(_make_getter("_lladdr"), _make_setter("_lladdr", _fix_lladdr)) arp = property(_make_getter("_arp"), _make_setter("_arp", _any_to_bool)) multicast = property(_make_getter("_mc"), _make_setter("_mc", _any_to_bool)) def __init__(self, index = None, name = None, up = None, mtu = None, lladdr = None, broadcast = None, multicast = None, arp = None): self._index = _positive(index) if index is not None else None self.name = name self.up = up self.mtu = mtu self.lladdr = lladdr self.broadcast = broadcast self.multicast = multicast self.arp = arp def __repr__(self): s = "%s.%s(index = %s, name = %s, up = %s, mtu = %s, lladdr = %s, " s += "broadcast = %s, multicast = %s, arp = %s)" return s % (self.__module__, self.__class__.__name__, self.index.__repr__(), self.name.__repr__(), self.up.__repr__(), self.mtu.__repr__(), self.lladdr.__repr__(), self.broadcast.__repr__(), self.multicast.__repr__(), self.arp.__repr__()) def __sub__(self, o): """Compare attributes and return a new object with just the attributes that differ set (with the value they have in the first operand). The index remains equal to the first operand.""" name = None if self.name == o.name else self.name up = None if self.up == o.up else self.up mtu = None if self.mtu == o.mtu else self.mtu lladdr = None if self.lladdr == o.lladdr else self.lladdr broadcast = None if self.broadcast == o.broadcast else self.broadcast multicast = None if self.multicast == o.multicast else self.multicast arp = None if self.arp == o.arp else self.arp return self.__class__(self.index, name, up, mtu, lladdr, broadcast, multicast, arp) def copy(self): return copy.copy(self) class bridge(interface): changeable_attributes = interface.changeable_attributes + ["stp", "forward_delay", "hello_time", "ageing_time", "max_age"] # Index should be read-only stp = property(_make_getter("_stp"), _make_setter("_stp", _any_to_bool)) forward_delay = property(_make_getter("_forward_delay"), _make_setter("_forward_delay", float)) hello_time = property(_make_getter("_hello_time"), _make_setter("_hello_time", float)) ageing_time = property(_make_getter("_ageing_time"), _make_setter("_ageing_time", float)) max_age = property(_make_getter("_max_age"), _make_setter("_max_age", float)) @classmethod def upgrade(cls, iface, *kargs, **kwargs): """Upgrade a interface to a bridge.""" return cls(iface.index, iface.name, iface.up, iface.mtu, iface.lladdr, iface.broadcast, iface.multicast, iface.arp, *kargs, **kwargs) def __init__(self, index = None, name = None, up = None, mtu = None, lladdr = None, broadcast = None, multicast = None, arp = None, stp = None, forward_delay = None, hello_time = None, ageing_time = None, max_age = None): super(bridge, self).__init__(index, name, up, mtu, lladdr, broadcast, multicast, arp) self.stp = stp self.forward_delay = forward_delay self.hello_time = hello_time self.ageing_time = ageing_time self.max_age = max_age def __repr__(self): s = "%s.%s(index = %s, name = %s, up = %s, mtu = %s, lladdr = %s, " s += "broadcast = %s, multicast = %s, arp = %s, stp = %s, " s += "forward_delay = %s, hello_time = %s, ageing_time = %s, " s += "max_age = %s)" return s % (self.__module__, self.__class__.__name__, self.index.__repr__(), self.name.__repr__(), self.up.__repr__(), self.mtu.__repr__(), self.lladdr.__repr__(), self.broadcast.__repr__(), self.multicast.__repr__(), self.arp.__repr__(), self.stp.__repr__(), self.forward_delay.__repr__(), self.hello_time.__repr__(), self.ageing_time.__repr__(), self.max_age.__repr__()) def __sub__(self, o): r = super(bridge, self).__sub__(o) if type(o) == interface: return r r.stp = None if self.stp == o.stp else self.stp r.hello_time = None if self.hello_time == o.hello_time else \ self.hello_time r.forward_delay = None if self.forward_delay == o.forward_delay else \ self.forward_delay r.ageing_time = None if self.ageing_time == o.ageing_time else \ self.ageing_time r.max_age = None if self.max_age == o.max_age else self.max_age return r class address(object): """Class for internal use. It is mostly a data container used to easily pass information around; with some convenience methods. __eq__ and __hash__ are defined just to be able to easily find duplicated addresses.""" # broadcast is not taken into account for differentiating addresses def __eq__(self, o): if not isinstance(o, address): return False return (self.family == o.family and self.address == o.address and self.prefix_len == o.prefix_len) def __hash__(self): h = (self.address.__hash__() ^ self.prefix_len.__hash__() ^ self.family.__hash__()) return h class ipv4address(address): def __init__(self, address, prefix_len, broadcast): self.address = address self.prefix_len = int(prefix_len) self.broadcast = broadcast self.family = socket.AF_INET def __repr__(self): s = "%s.%s(address = %s, prefix_len = %d, broadcast = %s)" return s % (self.__module__, self.__class__.__name__, self.address.__repr__(), self.prefix_len, self.broadcast.__repr__()) class ipv6address(address): def __init__(self, address, prefix_len): self.address = address self.prefix_len = int(prefix_len) self.family = socket.AF_INET6 def __repr__(self): s = "%s.%s(address = %s, prefix_len = %d)" return s % (self.__module__, self.__class__.__name__, self.address.__repr__(), self.prefix_len) class route(object): tipes = ["unicast", "local", "broadcast", "multicast", "throw", "unreachable", "prohibit", "blackhole", "nat"] tipe = property(_make_getter("_tipe", tipes.__getitem__), _make_setter("_tipe", tipes.index)) prefix = property(_make_getter("_prefix"), _make_setter("_prefix", _non_empty_str)) prefix_len = property(_make_getter("_plen"), lambda s, v: setattr(s, "_plen", int(v or 0))) nexthop = property(_make_getter("_nexthop"), _make_setter("_nexthop", _non_empty_str)) interface = property(_make_getter("_interface"), _make_setter("_interface", _positive)) metric = property(_make_getter("_metric"), lambda s, v: setattr(s, "_metric", int(v or 0))) def __init__(self, tipe = "unicast", prefix = None, prefix_len = 0, nexthop = None, interface = None, metric = 0): self.tipe = tipe self.prefix = prefix self.prefix_len = prefix_len self.nexthop = nexthop self.interface = interface self.metric = metric assert nexthop or interface def __repr__(self): s = "%s.%s(tipe = %s, prefix = %s, prefix_len = %s, nexthop = %s, " s += "interface = %s, metric = %s)" return s % (self.__module__, self.__class__.__name__, self.tipe.__repr__(), self.prefix.__repr__(), self.prefix_len.__repr__(), self.nexthop.__repr__(), self.interface.__repr__(), self.metric.__repr__()) def __eq__(self, o): if not isinstance(o, route): return False return (self.tipe == o.tipe and self.prefix == o.prefix and self.prefix_len == o.prefix_len and self.nexthop == o.nexthop and self.interface == o.interface and self.metric == o.metric) # helpers def _get_if_name(iface): if isinstance(iface, interface): if iface.name != None: return iface.name if isinstance(iface, str): return iface return get_if(iface).name # XXX: ideally this should be replaced by netlink communication # Interface handling # FIXME: try to lower the amount of calls to retrieve data!! def get_if_data(): """Gets current interface information. Returns a tuple (byidx, bynam) in which each element is a dictionary with the same data, but using different keys: interface indexes and interface names. In each dictionary, values are interface objects. """ ipdata = backticks([IP_PATH, "-o", "link", "list"]) byidx = {} bynam = {} for line in ipdata.split("\n"): if line == "": continue match = re.search(r'^(\d+):\s+(.*)', line) idx = int(match.group(1)) match = re.search(r'^(\d+): (\S+): <(\S+)> mtu (\d+) qdisc \S+' + r'.*link/\S+(?: ([0-9a-f:]+) brd ([0-9a-f:]+))?', line) flags = match.group(3).split(",") i = interface( index = match.group(1), name = match.group(2), up = "UP" in flags, mtu = match.group(4), lladdr = match.group(5), arp = not ("NOARP" in flags), broadcast = match.group(6), multicast = "MULTICAST" in flags) byidx[idx] = bynam[i.name] = i return byidx, bynam def get_if(iface): ifdata = get_if_data() if isinstance(iface, interface): if iface.index != None: return ifdata[0][iface.index] else: return ifdata[1][iface.name] if isinstance(iface, int): return ifdata[0][iface] return ifdata[1][iface] def create_if_pair(if1, if2): assert if1.name and if2.name cmd = [[], []] iface = [if1, if2] for i in (0, 1): cmd[i] = ["name", iface[i].name] if iface[i].lladdr: cmd[i] += ["address", iface[i].lladdr] if iface[i].broadcast: cmd[i] += ["broadcast", iface[i].broadcast] if iface[i].mtu: cmd[i] += ["mtu", str(iface[i].mtu)] cmd = [IP_PATH, "link", "add"] + cmd[0] + ["type", "veth", "peer"] + cmd[1] execute(cmd) try: set_if(if1) set_if(if2) except: (t, v, bt) = sys.exc_info() try: del_if(if1) del_if(if2) except: pass raise t, v, bt interfaces = get_if_data()[1] return interfaces[if1.name], interfaces[if2.name] def del_if(iface): ifname = _get_if_name(iface) execute([IP_PATH, "link", "del", ifname]) def set_if(iface, recover = True): def do_cmds(cmds, orig_iface): for c in cmds: try: execute(c) except: if recover: set_if(orig_iface, recover = False) # rollback raise orig_iface = get_if(iface) diff = iface - orig_iface # Only set what's needed # Name goes first if diff.name: _ils = [IP_PATH, "link", "set", "dev"] cmds = [_ils + [orig_iface.name, "name", diff.name]] if orig_iface.up: # iface needs to be down cmds = [_ils + [orig_iface.name, "down"], cmds[0], _ils + [diff.name, "up"]] do_cmds(cmds, orig_iface) # I need to use the new name after a name change, duh! _ils = [IP_PATH, "link", "set", "dev", diff.name or orig_iface.name] cmds = [] if diff.lladdr: if orig_iface.up: # iface needs to be down cmds.append(_ils + ["down"]) cmds.append(_ils + ["address", diff.lladdr]) if orig_iface.up and diff.up == None: # restore if it was up and it's not going to be set later cmds.append(_ils + ["up"]) if diff.mtu: cmds.append(_ils + ["mtu", str(diff.mtu)]) if diff.broadcast: cmds.append(_ils + ["broadcast", diff.broadcast]) if diff.multicast != None: cmds.append(_ils + ["multicast", "on" if diff.multicast else "off"]) if diff.arp != None: cmds.append(_ils + ["arp", "on" if diff.arp else "off"]) if diff.up != None: cmds.append(_ils + ["up" if diff.up else "down"]) do_cmds(cmds, orig_iface) def change_netns(iface, netns): ifname = _get_if_name(iface) execute([IP_PATH, "link", "set", "dev", ifname, "netns", str(netns)]) # Address handling def get_addr_data(): ipdata = backticks([IP_PATH, "addr", "list"]) byidx = {} bynam = {} current = None for line in ipdata.split("\n"): if line == "": continue match = re.search(r'^(\d+):\s+(\S+):', line) if match: # First line of output. idx = int(match.group(1)) name = match.group(2) current = name if name in bynam: raise RuntimeError("Invalid `ip' command output") bynam[name] = byidx[idx] = [] continue if not current: raise RuntimeError("Invalid `ip' command output") match = re.search(r'^\s*inet ([0-9.]+)/(\d+)(?: brd ([0-9.]+))?', line) if match: bynam[current].append(ipv4address( address = match.group(1), prefix_len = match.group(2), broadcast = match.group(3))) continue match = re.search(r'^\s*inet6 ([0-9a-f:]+)/(\d+)', line) if match: bynam[current].append(ipv6address( address = match.group(1), prefix_len = match.group(2))) continue # Extra info, ignored. continue return byidx, bynam def add_addr(iface, address): ifname = _get_if_name(iface) addresses = get_addr_data()[1][ifname] assert address not in addresses cmd = [IP_PATH, "addr", "add", "dev", ifname, "local", "%s/%d" % (address.address, int(address.prefix_len))] if hasattr(address, "broadcast"): cmd += ["broadcast", address.broadcast if address.broadcast else "+"] execute(cmd) def del_addr(iface, address): ifname = _get_if_name(iface) addresses = get_addr_data()[1][ifname] assert address in addresses cmd = [IP_PATH, "addr", "del", "dev", ifname, "local", "%s/%d" % (address.address, int(address.prefix_len))] execute(cmd) def set_addr(iface, addresses, recover = True): ifname = _get_if_name(iface) addresses = get_addr_data()[1][ifname] to_remove = set(orig_addresses) - set(addresses) to_add = set(addresses) - set(orig_addresses) for a in to_remove: try: del_addr(ifname, a) except: if recover: set_addr(orig_addresses, recover = False) # rollback raise for a in to_add: try: add_addr(ifname, a) except: if recover: set_addr(orig_addresses, recover = False) # rollback raise # Bridge handling def _sysfs_read_br(brname): def readval(fname): f = file(fname) return f.readline().strip() p = "/sys/class/net/%s/bridge/" % brname p2 = "/sys/class/net/%s/brif/" % brname try: os.stat(p) except: return None return dict( stp = readval(p + "stp_state"), forward_delay = float(readval(p + "forward_delay")) / 100, hello_time = float(readval(p + "hello_time")) / 100, ageing_time = float(readval(p + "ageing_time")) / 100, max_age = float(readval(p + "max_age")) / 100, ports = os.listdir(p2)) def get_bridge_data(): # brctl stinks too much; it is better to directly use sysfs, it is # probably stable by now byidx = {} bynam = {} ports = {} ifdata = get_if_data() for iface in ifdata[0].values(): brdata = _sysfs_read_br(iface.name) if brdata == None: continue ports[iface.index] = [ifdata[1][x].index for x in brdata["ports"]] del brdata["ports"] bynam[iface.name] = byidx[iface.index] = \ bridge.upgrade(iface, **brdata) return byidx, bynam, ports def get_bridge(br): iface = get_if(br) brdata = _sysfs_read_br(iface.name) #ports = [ifdata[1][x].index for x in brdata["ports"]] del brdata["ports"] return bridge.upgrade(iface, **brdata) def create_bridge(br): if isinstance(br, str): br = interface(name = br) assert br.name execute([BRCTL_PATH, "addbr", br.name]) try: set_if(br) except: (t, v, bt) = sys.exc_info() try: del_bridge(br) except: pass raise t, v, bt return get_if_data()[1][br.name] def del_bridge(br): brname = _get_if_name(br) execute([BRCTL_PATH, "delbr", brname]) def set_bridge(br, recover = True): def saveval(fname, val): f = file(fname, "w") f.write(str(val)) f.close() def do_cmds(basename, cmds, orig_br): for n, v in cmds: try: saveval(basename + n, v) except: if recover: set_bridge(orig_br, recover = False) # rollback set_if(orig_br, recover = False) # rollback raise orig_br = get_bridge(br) diff = br - orig_br # Only set what's needed cmds = [] if diff.stp != None: cmds.append(("stp_state", int(diff.stp))) if diff.forward_delay != None: cmds.append(("forward_delay", int(diff.forward_delay))) if diff.hello_time != None: cmds.append(("hello_time", int(diff.hello_time))) if diff.ageing_time != None: cmds.append(("ageing_time", int(diff.ageing_time))) if diff.max_age != None: cmds.append(("max_age", int(diff.max_age))) set_if(diff) name = diff.name if diff.name != None else orig_br.name do_cmds("/sys/class/net/%s/bridge/" % name, cmds, orig_br) def add_bridge_port(br, iface): ifname = _get_if_name(iface) brname = _get_if_name(br) execute([BRCTL_PATH, "addif", brname, ifname]) def del_bridge_port(br, iface): ifname = _get_if_name(iface) brname = _get_if_name(br) execute([BRCTL_PATH, "delif", brname, ifname]) # Routing def get_all_route_data(): ipdata = backticks([IP_PATH, "-o", "route", "list"]) # "table", "all" ipdata += backticks([IP_PATH, "-o", "-f", "inet6", "route", "list"]) ifdata = get_if_data()[1] ret = [] for line in ipdata.split("\n"): if line == "": continue match = re.match(r'(?:(unicast|local|broadcast|multicast|throw|' + r'unreachable|prohibit|blackhole|nat) )?' + r'(\S+)(?: via (\S+))? dev (\S+).*(?: metric (\d+))?', line) if not match: raise RuntimeError("Invalid output from `ip route': `%s'" % line) tipe = match.group(1) or "unicast" prefix = match.group(2) nexthop = match.group(3) interface = ifdata[match.group(4)] metric = match.group(5) if prefix == "default" or re.search(r'/0$', prefix): prefix = None prefix_len = 0 else: match = re.match(r'([0-9a-f:.]+)(?:/(\d+))?$', prefix) prefix = match.group(1) prefix_len = int(match.group(2) or 32) ret.append(route(tipe, prefix, prefix_len, nexthop, interface.index, metric)) return ret def get_route_data(): # filter out non-unicast routes return [x for x in get_all_route_data() if x.tipe == "unicast"] def add_route(route): # Cannot really test this #if route in get_all_route_data(): # raise ValueError("Route already exists") _add_del_route("add", route) def del_route(route): # Cannot really test this #if route not in get_all_route_data(): # raise ValueError("Route does not exist") _add_del_route("del", route) def _add_del_route(action, route): cmd = [IP_PATH, "route", action] if route.tipe != "unicast": cmd += [route.tipe] if route.prefix: cmd += ["%s/%d" % (route.prefix, route.prefix_len)] else: cmd += ["default"] if route.nexthop: cmd += ["via", route.nexthop] if route.interface: cmd += ["dev", _get_if_name(route.interface)] execute(cmd) # TC stuff def get_tc_tree(): tcdata = backticks([TC_PATH, "qdisc", "show"]) data = {} for line in tcdata.split("\n"): if line == "": continue match = re.match(r'qdisc (\S+) ([0-9a-f]+):[0-9a-f]* dev (\S+) ' + r'(?:parent ([0-9a-f]+):[0-9a-f]*|root)\s*(.*)', line) if not match: raise RuntimeError("Invalid output from `tc qdisc': `%s'" % line) qdisc = match.group(1) handle = match.group(2) iface = match.group(3) parent = match.group(4) # or None extra = match.group(5) if iface not in data: data[iface] = {} if parent not in data[iface]: data[iface][parent] = [] data[iface][parent] += [[handle, qdisc, parent, extra]] tree = {} for iface in data: def gen_tree(data, data_node): children = [] node = {"handle": data_node[0], "qdisc": data_node[1], "extra": data_node[3], "children": []} if data_node[0] in data: for h in data[data_node[0]]: node["children"].append(gen_tree(data, h)) return node tree[iface] = gen_tree(data[iface], data[iface][None][0]) return tree _multipliers = {"M": 1000000, "K": 1000} _dividers = {"m": 1000, "u": 1000000} def _parse_netem_delay(line): ret = {} match = re.search(r'delay ([\d.]+)([mu]?)s(?: +([\d.]+)([mu]?)s)?' + r'(?: *([\d.]+)%)?(?: *distribution (\S+))?', line) if not match: return ret delay = float(match.group(1)) if match.group(2): delay /= _dividers[match.group(2)] ret["delay"] = delay if match.group(3): delay_jitter = float(match.group(3)) if match.group(4): delay_jitter /= _dividers[match.group(4)] ret["delay_jitter"] = delay_jitter if match.group(5): ret["delay_correlation"] = float(match.group(5)) / 100 if match.group(6): ret["delay_distribution"] = match.group(6) return ret def _parse_netem_loss(line): ret = {} match = re.search(r'loss ([\d.]+)%(?: *([\d.]+)%)?', line) if not match: return ret ret["loss"] = float(match.group(1)) / 100 if match.group(2): ret["loss_correlation"] = float(match.group(2)) / 100 return ret def _parse_netem_dup(line): ret = {} match = re.search(r'duplicate ([\d.]+)%(?: *([\d.]+)%)?', line) if not match: return ret ret["dup"] = float(match.group(1)) / 100 if match.group(2): ret["dup_correlation"] = float(match.group(2)) / 100 return ret def _parse_netem_corrupt(line): ret = {} match = re.search(r'corrupt ([\d.]+)%(?: *([\d.]+)%)?', line) if not match: return ret ret["corrupt"] = float(match.group(1)) / 100 if match.group(2): ret["corrupt_correlation"] = float(match.group(2)) / 100 return ret def get_tc_data(): tree = get_tc_tree() ifdata = get_if_data() ret = {} for i in ifdata[0]: ret[i] = {"qdiscs": {}} if ifdata[0][i].name not in tree: continue node = tree[ifdata[0][i].name] if not node["children"]: if node["qdisc"] == "mq" or node["qdisc"] == "pfifo_fast" \ or node["qdisc"][1:] == "fifo": continue if node["qdisc"] == "netem": tbf = None netem = node["extra"], node["handle"] elif node["qdisc"] == "tbf": tbf = node["extra"], node["handle"] netem = None else: ret[i] = "foreign" continue else: if node["qdisc"] != "tbf" or len(node["children"]) != 1 or \ node["children"][0]["qdisc"] != "netem" or \ node["children"][0]["children"]: ret[i] = "foreign" continue tbf = node["extra"], node["handle"] netem = node["children"][0]["extra"], \ node["children"][0]["handle"] if tbf: ret[i]["qdiscs"]["tbf"] = tbf[1] match = re.search(r'rate (\d+)([MK]?)bit', tbf[0]) if not match: ret[i] = "foreign" continue bandwidth = int(match.group(1)) if match.group(2): bandwidth *= _multipliers[match.group(2)] ret[i]["bandwidth"] = bandwidth if netem: ret[i]["qdiscs"]["netem"] = netem[1] ret[i].update(_parse_netem_delay(netem[0])) ret[i].update(_parse_netem_loss(netem[0])) ret[i].update(_parse_netem_dup(netem[0])) ret[i].update(_parse_netem_corrupt(netem[0])) return ret, ifdata[0], ifdata[1] def clear_tc(iface): iface = get_if(iface) tcdata = get_tc_data()[0] if tcdata[iface.index] == None: return # Any other case, we clean execute([TC_PATH, "qdisc", "del", "dev", iface.name, "root"]) def set_tc(iface, bandwidth = None, delay = None, delay_jitter = None, delay_correlation = None, delay_distribution = None, loss = None, loss_correlation = None, dup = None, dup_correlation = None, corrupt = None, corrupt_correlation = None): use_netem = bool(delay or delay_jitter or delay_correlation or delay_distribution or loss or loss_correlation or dup or dup_correlation or corrupt or corrupt_correlation) iface = get_if(iface) tcdata, ifdata = get_tc_data()[0:2] commands = [] if tcdata[iface.index] == 'foreign': # Avoid the overhead of calling tc+ip again commands.append([TC_PATH, "qdisc", "del", "dev", iface.name, "root"]) tcdata[iface.index] = {'qdiscs': []} has_netem = 'netem' in tcdata[iface.index]['qdiscs'] has_tbf = 'tbf' in tcdata[iface.index]['qdiscs'] if not bandwidth and not use_netem: if has_netem or has_tbf: clear_tc(iface) return if has_netem == use_netem and has_tbf == bool(bandwidth): cmd = "change" else: # Too much work to do better :) if has_netem or has_tbf: commands.append([TC_PATH, "qdisc", "del", "dev", iface.name, "root"]) cmd = "add" if bandwidth: rate = "%dbit" % int(bandwidth) mtu = ifdata[iface.index].mtu burst = max(mtu, int(bandwidth) / HZ) limit = burst * 2 # FIXME? handle = "1:" if cmd == "change": handle = "%d:" % int(tcdata[iface.index]["qdiscs"]["tbf"]) command = [TC_PATH, "qdisc", cmd, "dev", iface.name, "root", "handle", handle, "tbf", "rate", rate, "limit", str(limit), "burst", str(burst)] commands.append(command) if use_netem: handle = "2:" if cmd == "change": handle = "%d:" % int(tcdata[iface.index]["qdiscs"]["netem"]) command = [TC_PATH, "qdisc", cmd, "dev", iface.name, "handle", handle] if bandwidth: parent = "1:" if cmd == "change": parent = "%d:" % int(tcdata[iface.index]["qdiscs"]["tbf"]) command += ["parent", parent] else: command += ["root"] command += ["netem"] if delay: command += ["delay", "%fs" % delay] if delay_jitter: command += ["%fs" % delay_jitter] if delay_correlation: if not delay_jitter: raise ValueError("delay_correlation requires delay_jitter") command += ["%f%%" % (delay_correlation * 100)] if delay_distribution: if not delay_jitter: raise ValueError("delay_distribution requires delay_jitter") command += ["distribution", delay_distribution] if loss: command += ["loss", "%f%%" % (loss * 100)] if loss_correlation: command += ["%f%%" % (loss_correlation * 100)] if dup: command += ["duplicate", "%f%%" % (dup * 100)] if dup_correlation: command += ["%f%%" % (dup_correlation * 100)] if corrupt: command += ["corrupt", "%f%%" % (corrupt * 100)] if corrupt_correlation: command += ["%f%%" % (corrupt_correlation * 100)] commands.append(command) for c in commands: execute(c) def create_tap(iface, use_pi = False, tun = False): """Creates a tap/tun device and returns the associated file descriptor""" if isinstance(iface, str): iface = interface(name = iface) assert iface.name IFF_TUN = 0x0001 IFF_TAP = 0x0002 IFF_NO_PI = 0x1000 TUNSETIFF = 0x400454ca if tun: mode = IFF_TUN else: mode = IFF_TAP if not use_pi: mode |= IFF_NO_PI fd = os.open("/dev/net/tun", os.O_RDWR) err = fcntl.ioctl(fd, TUNSETIFF, struct.pack("16sH", iface.name, mode)) if err < 0: os.close(fd) raise RuntimeError("Could not configure device %s" % iface.name) try: set_if(iface) except: os.close(fd) raise interfaces = get_if_data()[1] return interfaces[iface.name], fd nemu-0.2/src/nemu/node.py0000644000175000017500000001711712225521027016150 0ustar tinchotincho00000000000000# vim:ts=4:sw=4:et:ai:sts=4 # -*- coding: utf-8 -*- # Copyright 2010, 2011 INRIA # Copyright 2011 Martín Ferrari # # This file is part of Nemu. # # Nemu is free software: you can redistribute it and/or modify it under the # terms of the GNU General Public License version 2, as published by the Free # Software Foundation. # # Nemu 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 # Nemu. If not, see . import os, socket, sys, traceback, unshare, weakref from nemu.environ import * import nemu.interface, nemu.protocol, nemu.subprocess_ __all__ = ['Node', 'get_nodes', 'import_if'] class Node(object): _nodes = weakref.WeakValueDictionary() _nextnode = 0 @staticmethod def get_nodes(): s = sorted(Node._nodes.items(), key = lambda x: x[0]) return [x[1] for x in s] def __init__(self, nonetns = False, forward_X11 = False): """Create a new node in the emulation. Implemented as a separate process in a new network name space. Requires root privileges to run. If nonetns is true, the network name space is not created and can be run as a normal user, for testing.""" # Initialize attributes, in case something fails during __init__ self._pid = self._slave = None self._processes = weakref.WeakValueDictionary() self._interfaces = weakref.WeakValueDictionary() self._auto_interfaces = [] # just to keep them alive! fd, pid = _start_child(nonetns) self._pid = pid debug("Node(0x%x).__init__(), pid = %s" % (id(self), pid)) self._slave = nemu.protocol.Client(fd, fd) if forward_X11: self._slave.enable_x11_forwarding() Node._nodes[Node._nextnode] = self Node._nextnode += 1 # Bring loopback up if not nonetns: self.get_interface("lo").up = True def __del__(self): debug("Node(0x%x).__del__()" % id(self)) self.destroy() def destroy(self): if not self._pid: return debug("Node(0x%x).destroy()" % id(self)) for p in self._processes.values(): p.destroy() self._processes.clear() # Use get_interfaces to force a rescan for i in self.get_interfaces(): i.destroy() self._interfaces.clear() if self._slave: self._slave.shutdown() exitcode = eintr_wrapper(os.waitpid, self._pid, 0)[1] if exitcode != 0: error("Node(0x%x) process %d exited with non-zero status: %d" % (id(self), self._pid, exitcode)) self._pid = self._slave = None @property def pid(self): return self._pid # Subprocesses def _add_subprocess(self, subprocess): self._processes[subprocess.pid] = subprocess def Subprocess(self, *kargs, **kwargs): return nemu.subprocess_.Subprocess(self, *kargs, **kwargs) def Popen(self, *kargs, **kwargs): return nemu.subprocess_.Popen(self, *kargs, **kwargs) def system(self, *kargs, **kwargs): return nemu.subprocess_.system(self, *kargs, **kwargs) def backticks(self, *kargs, **kwargs): return nemu.subprocess_.backticks(self, *kargs, **kwargs) def backticks_raise(self, *kargs, **kwargs): return nemu.subprocess_.backticks_raise(self, *kargs, **kwargs) # Interfaces def _add_interface(self, interface): self._interfaces[interface.index] = interface def add_if(self, **kwargs): i = nemu.interface.NodeInterface(self) for k, v in kwargs.items(): setattr(i, k, v) return i def add_tap(self, use_pi = False, **kwargs): i = nemu.interface.TapNodeInterface(self, use_pi) for k, v in kwargs.items(): setattr(i, k, v) return i def add_tun(self, use_pi = False, **kwargs): i = nemu.interface.TunNodeInterface(self, use_pi) for k, v in kwargs.items(): setattr(i, k, v) return i def import_if(self, interface): return nemu.interface.ImportedNodeInterface(self, interface) def del_if(self, iface): """Doesn't destroy the interface if it wasn't created by us.""" del self._interfaces[iface.index] iface.destroy() def get_interface(self, name): return [i for i in self.get_interfaces() if i.name == name][0] def get_interfaces(self): if not self._slave: return [] ifaces = self._slave.get_if_data() for i in ifaces: if i not in self._interfaces: iface = nemu.interface.ImportedNodeInterface(self, i, migrate = False) self._auto_interfaces.append(iface) # keep it referenced! self._interfaces[i] = iface # by the way, clean up _interfaces for i in list(self._interfaces): # copy before deleting! if i not in ifaces: notice("Node(0x%x): interface #%d went away." % (id(self), i)) self._interfaces[i].destroy() del self._interfaces[i] return sorted(self._interfaces.values(), key = lambda x: x.index) def route(self, tipe = 'unicast', prefix = None, prefix_len = 0, nexthop = None, interface = None, metric = 0): return nemu.iproute.route(tipe, prefix, prefix_len, nexthop, interface.index if interface else None, metric) def add_route(self, *args, **kwargs): # Accepts either a route object or all its constructor's parameters if len(args) == 1 and not kwargs: r = args[0] else: r = self.route(*args, **kwargs) return self._slave.add_route(r) def del_route(self, *args, **kwargs): if len(args) == 1 and not kwargs: r = args[0] else: r = self.route(*args, **kwargs) return self._slave.del_route(r) def get_routes(self): return self._slave.get_route_data() # Handle the creation of the child; parent gets (fd, pid), child creates and # runs a Server(); never returns. # Requires CAP_SYS_ADMIN privileges to run. def _start_child(nonetns): # Create socket pair to communicate (s0, s1) = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM, 0) # Spawn a child that will run in a loop pid = os.fork() if pid: s1.close() return (s0, pid) # FIXME: clean up signal handers, atexit functions, etc. try: # pragma: no cover # coverage doesn't seem to understand fork s0.close() srv = nemu.protocol.Server(s1, s1) if not nonetns: # create new name space unshare.unshare(unshare.CLONE_NEWNET) # Enable packet forwarding execute([SYSCTL_PATH, '-w', 'net.ipv4.ip_forward=1']) execute([SYSCTL_PATH, '-w', 'net.ipv6.conf.default.forwarding=1']) srv.run() except BaseException, e: s = "Slave node aborting: %s\n" % str(e) sep = "=" * 70 + "\n" sys.stderr.write(s + sep) traceback.print_exc(file=sys.stdout) sys.stderr.write(sep) try: # try to pass the error to parent, if possible s1.send("500 " + s) except: pass os._exit(1) os._exit(0) # pragma: no cover # NOTREACHED get_nodes = Node.get_nodes import_if = nemu.interface.ImportedInterface nemu-0.2/src/nemu/subprocess_.py0000644000175000017500000003653012225521027017552 0ustar tinchotincho00000000000000# vim:ts=4:sw=4:et:ai:sts=4 # -*- coding: utf-8 -*- # Copyright 2010, 2011 INRIA # Copyright 2011 Martín Ferrari # # This file is part of Nemu. # # Nemu is free software: you can redistribute it and/or modify it under the # terms of the GNU General Public License version 2, as published by the Free # Software Foundation. # # Nemu 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 # Nemu. If not, see . import fcntl, grp, os, pickle, pwd, signal, select, sys, time, traceback from nemu.environ import eintr_wrapper __all__ = [ 'PIPE', 'STDOUT', 'Popen', 'Subprocess', 'spawn', 'wait', 'poll', 'get_user', 'system', 'backticks', 'backticks_raise' ] # User-facing interfaces KILL_WAIT = 3 # seconds class Subprocess(object): """Class that allows the execution of programs inside a nemu Node. This is the base class for all process operations, Popen provides a more high level interface.""" # FIXME default_user = None def __init__(self, node, argv, executable = None, stdin = None, stdout = None, stderr = None, shell = False, cwd = None, env = None, user = None): self._slave = node._slave """Forks and execs a program, with stdio redirection and user switching. A nemu Node to run the program is is specified as the first parameter. The program is specified by `executable', if it does not contain any slash, the PATH environment variable is used to search for the file. The `user` parameter, if not None, specifies a user name to run the command as, after setting its primary and secondary groups. If a numerical UID is given, a reverse lookup is performed to find the user name and then set correctly the groups. To run the program in a different directory than the current one, it should be set in `cwd'. If specified, `env' replaces the caller's environment with the dictionary provided. The standard input, output, and error of the created process will be redirected to the file descriptors specified by `stdin`, `stdout`, and `stderr`, respectively. These parameters must be open file objects, integers, or None (for no redirection). Note that the descriptors will not be closed by this class. Exceptions occurred while trying to set up the environment or executing the program are propagated to the parent.""" if user == None: user = Subprocess.default_user if isinstance(argv, str): argv = [ argv ] if shell: argv = [ '/bin/sh', '-c' ] + argv # Initialize attributes that would be used by the destructor if spawn # fails self._pid = self._returncode = None # confusingly enough, to go to the function at the top of this file, # I need to call it thru the communications protocol: remember that # happens in another process! self._pid = self._slave.spawn(argv, executable = executable, stdin = stdin, stdout = stdout, stderr = stderr, cwd = cwd, env = env, user = user) node._add_subprocess(self) @property def pid(self): """The real process ID of this subprocess.""" return self._pid def poll(self): """Checks status of program, returns exitcode or None if still running. See Popen.poll.""" if self._returncode == None: self._returncode = self._slave.poll(self._pid) return self.returncode def wait(self): """Waits for program to complete and returns the exitcode. See Popen.wait""" if self._returncode == None: self._returncode = self._slave.wait(self._pid) return self.returncode def signal(self, sig = signal.SIGTERM): """Sends a signal to the process.""" if self._returncode == None: self._slave.signal(self._pid, sig) @property def returncode(self): """When the program has finished (and has been waited for with communicate, wait, or poll), returns the signal that killed the program, if negative; otherwise, it is the exit code of the program. """ if self._returncode == None: return None if os.WIFSIGNALED(self._returncode): return -os.WTERMSIG(self._returncode) if os.WIFEXITED(self._returncode): return os.WEXITSTATUS(self._returncode) raise RuntimeError("Invalid return code") # pragma: no cover def __del__(self): self.destroy() def destroy(self): if self._returncode != None or self._pid == None: return self.signal() now = time.time() while time.time() - now < KILL_WAIT: if self.poll() != None: return time.sleep(0.1) sys.stderr.write("WARNING: killing forcefully process %d.\n" % self._pid) self.signal(signal.SIGKILL) self.wait() PIPE = -1 STDOUT = -2 class Popen(Subprocess): """Higher-level interface for executing processes, that tries to emulate the stdlib's subprocess.Popen as much as possible.""" def __init__(self, node, argv, executable = None, stdin = None, stdout = None, stderr = None, bufsize = 0, shell = False, cwd = None, env = None, user = None): """As in Subprocess, `node' specifies the nemu Node to run in. The `stdin', `stdout', and `stderr' parameters also accept the special values subprocess.PIPE or subprocess.STDOUT. Check the stdlib's subprocess module for more details. `bufsize' specifies the buffer size for the buffered IO provided for PIPE'd descriptors. """ self.stdin = self.stdout = self.stderr = None self._pid = self._returncode = None fdmap = { "stdin": stdin, "stdout": stdout, "stderr": stderr } # if PIPE: all should be closed at the end for k, v in fdmap.items(): if v == None: continue if v == PIPE: r, w = os.pipe() if k == "stdin": self.stdin = os.fdopen(w, 'wb', bufsize) fdmap[k] = r else: setattr(self, k, os.fdopen(r, 'rb', bufsize)) fdmap[k] = w elif isinstance(v, int): pass else: fdmap[k] = v.fileno() if stderr == STDOUT: fdmap['stderr'] = fdmap['stdout'] super(Popen, self).__init__(node, argv, executable = executable, stdin = fdmap['stdin'], stdout = fdmap['stdout'], stderr = fdmap['stderr'], shell = shell, cwd = cwd, env = env, user = user) # Close pipes, they have been dup()ed to the child for k, v in fdmap.items(): if getattr(self, k) != None: eintr_wrapper(os.close, v) def communicate(self, input = None): """See Popen.communicate.""" # FIXME: almost verbatim from stdlib version, need to be removed or # something wset = [] rset = [] err = None out = None if self.stdin != None: self.stdin.flush() if input: wset.append(self.stdin) else: self.stdin.close() if self.stdout != None: rset.append(self.stdout) out = [] if self.stderr != None: rset.append(self.stderr) err = [] offset = 0 while rset or wset: r, w, x = select.select(rset, wset, []) if self.stdin in w: wrote = os.write(self.stdin.fileno(), #buffer(input, offset, select.PIPE_BUF)) buffer(input, offset, 512)) # XXX: py2.7 offset += wrote if offset >= len(input): self.stdin.close() wset = [] for i in self.stdout, self.stderr: if i in r: d = os.read(i.fileno(), 1024) # No need for eintr wrapper if d == "": i.close rset.remove(i) else: if i == self.stdout: out.append(d) else: err.append(d) if out != None: out = ''.join(out) if err != None: err = ''.join(err) self.wait() return (out, err) def system(node, args): """Emulates system() function, if `args' is an string, it uses `/bin/sh' to exexecute it, otherwise is interpreted as the argv array to call execve.""" shell = isinstance(args, str) return Popen(node, args, shell = shell).wait() def backticks(node, args): """Emulates shell backticks, if `args' is an string, it uses `/bin/sh' to exexecute it, otherwise is interpreted as the argv array to call execve.""" shell = isinstance(args, str) return Popen(node, args, shell = shell, stdout = PIPE).communicate()[0] def backticks_raise(node, args): """Emulates shell backticks, if `args' is an string, it uses `/bin/sh' to exexecute it, otherwise is interpreted as the argv array to call execve. Raises an RuntimeError if the return value is not 0.""" shell = isinstance(args, str) p = Popen(node, args, shell = shell, stdout = PIPE) out = p.communicate()[0] ret = p.returncode if ret > 0: raise RuntimeError("Command failed with return code %d." % ret) if ret < 0: raise RuntimeError("Command killed by signal %d." % -ret) return out # ======================================================================= # # Server-side code, called from nemu.protocol.Server def spawn(executable, argv = None, cwd = None, env = None, close_fds = False, stdin = None, stdout = None, stderr = None, user = None): """Internal function that performs all the dirty work for Subprocess, Popen and friends. This is executed in the slave process, directly from the protocol.Server class. Parameters have the same meaning as the stdlib's subprocess.Popen class, with one addition: the `user` parameter, if not None, specifies a user name to run the command as, after setting its primary and secondary groups. If a numerical UID is given, a reverse lookup is performed to find the user name and then set correctly the groups. When close_fds is True, it closes all file descriptors bigger than 2. It can also be an iterable of file descriptors to close after fork. Note that 'std{in,out,err}' must be None, integers, or file objects, PIPE is not supported here. Also, the original descriptors are not closed. """ userfd = [stdin, stdout, stderr] filtered_userfd = filter(lambda x: x != None and x >= 0, userfd) for i in range(3): if userfd[i] != None and not isinstance(userfd[i], int): userfd[i] = userfd[i].fileno() # pragma: no cover # Verify there is no clash assert not (set([0, 1, 2]) & set(filtered_userfd)) if user != None: user, uid, gid = get_user(user) home = pwd.getpwuid(uid)[5] groups = [x[2] for x in grp.getgrall() if user in x[3]] if not env: env = dict(os.environ) env['HOME'] = home env['USER'] = user (r, w) = os.pipe() pid = os.fork() if pid == 0: # pragma: no cover # coverage doesn't seem to understand fork try: # Set up stdio piping for i in range(3): if userfd[i] != None and userfd[i] >= 0: os.dup2(userfd[i], i) if userfd[i] != i and userfd[i] not in userfd[0:i]: eintr_wrapper(os.close, userfd[i]) # only in child! # Set up special control pipe eintr_wrapper(os.close, r) flags = fcntl.fcntl(w, fcntl.F_GETFD) fcntl.fcntl(w, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC) if close_fds == True: for i in xrange(3, MAXFD): if i != w: try: os.close(i) except: pass elif close_fds != False: for i in close_fds: os.close(i) # changing process group id # (it is necessary to kill the forked subprocesses) os.setpgrp() if user != None: # Change user os.setgid(gid) os.setgroups(groups) os.setuid(uid) if cwd != None: os.chdir(cwd) if not argv: argv = [ executable ] if '/' in executable: # Should not search in PATH if env != None: os.execve(executable, argv, env) else: os.execv(executable, argv) else: # use PATH if env != None: os.execvpe(executable, argv, env) else: os.execvp(executable, argv) raise RuntimeError("Unreachable reached!") except: try: (t, v, tb) = sys.exc_info() # Got the child_traceback attribute trick from Python's # subprocess.py v.child_traceback = "".join( traceback.format_exception(t, v, tb)) eintr_wrapper(os.write, w, pickle.dumps(v)) eintr_wrapper(os.close, w) #traceback.print_exc() except: traceback.print_exc() os._exit(1) eintr_wrapper(os.close, w) # read EOF for success, or a string as error info s = "" while True: s1 = eintr_wrapper(os.read, r, 4096) if s1 == "": break s += s1 eintr_wrapper(os.close, r) if s == "": return pid # It was an error eintr_wrapper(os.waitpid, pid, 0) exc = pickle.loads(s) # XXX: sys.excepthook #print exc.child_traceback raise exc def poll(pid): """Check if the process already died. Returns the exit code or None if the process is still alive.""" r = os.waitpid(pid, os.WNOHANG) if r[0]: return r[1] return None def wait(pid): """Wait for process to die and return the exit code.""" return eintr_wrapper(os.waitpid, pid, 0)[1] def get_user(user): "Take either an username or an uid, and return a tuple (user, uid, gid)." if str(user).isdigit(): uid = int(user) try: user = pwd.getpwuid(uid)[0] except KeyError: raise ValueError("UID %d does not exist" % int(user)) else: try: uid = pwd.getpwnam(str(user))[2] except KeyError: raise ValueError("User %s does not exist" % str(user)) gid = pwd.getpwuid(uid)[3] return user, uid, gid # internal stuff, do not look! try: MAXFD = os.sysconf("SC_OPEN_MAX") except: # pragma: no cover MAXFD = 256 nemu-0.2/src/nemu/protocol.py0000644000175000017500000007704412225521027017071 0ustar tinchotincho00000000000000# vim:ts=4:sw=4:et:ai:sts=4 # -*- coding: utf-8 -*- # Copyright 2010, 2011 INRIA # Copyright 2011 Martín Ferrari # # This file is part of Nemu. # # Nemu is free software: you can redistribute it and/or modify it under the # terms of the GNU General Public License version 2, as published by the Free # Software Foundation. # # Nemu 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 # Nemu. If not, see . import base64, errno, os, passfd, re, select, signal, socket, sys, tempfile import time, traceback, unshare import nemu.subprocess_, nemu.iproute from nemu.environ import * try: from cPickle import loads, dumps except: from pickle import loads, dumps # ============================================================================ # Server-side protocol implementation # # Protocol definition # ------------------- # # First key: command # Second key: sub-command or None # Value: pair of format strings for mandatory and optional parameters. # The format string is a chain of "s" for string and "i" for integer _proto_commands = { "QUIT": { None: ("", "") }, "HELP": { None: ("", "") }, "X11": { "SET": ("ss", ""), "SOCK": ("", "") }, "IF": { "LIST": ("", "i"), "SET": ("iss", "s*"), "RTRN": ("ii", ""), "DEL": ("i", "") }, "ADDR": { "LIST": ("", "i"), "ADD": ("isi", "s"), "DEL": ("iss", "s") }, "ROUT": { "LIST": ("", ""), "ADD": ("bbibii", ""), "DEL": ("bbibii", "") }, "PROC": { "CRTE": ("b", "b*"), "POLL": ("i", ""), "WAIT": ("i", ""), "KILL": ("i", "i") }, } # Commands valid only after PROC CRTE _proc_commands = { "HELP": { None: ("", "") }, "QUIT": { None: ("", "") }, "PROC": { "USER": ("b", ""), "CWD": ("b", ""), "ENV": ("bb", "b*"), "SIN": ("", ""), "SOUT": ("", ""), "SERR": ("", ""), "RUN": ("", ""), "ABRT": ("", ""), } } KILL_WAIT = 3 # seconds class Server(object): """Class that implements the communication protocol and dispatches calls to the required functions. Also works as the main loop for the slave process.""" def __init__(self, rfd, wfd): debug("Server(0x%x).__init__()" % id(self)) # Dictionary of valid commands self._commands = _proto_commands # Flag to stop the server self._closed = False # Set to keep track of started processes self._children = set() # Buffer and flag for PROC mode self._proc = None # temporary xauth files self._xauthfiles = {} # X11 forwarding info self._xfwd = None self._xsock = None self._rfd = _get_file(rfd, "r") self._wfd = _get_file(wfd, "w") def clean(self): try: for pid in self._children: # -PID to kill to whole process group os.kill(-pid, signal.SIGTERM) now = time.time() while time.time() - now < KILL_WAIT: ch = set(self._children) for pid in ch: try: if nemu.subprocess_.poll(pid): self._children.remove(pid) except OSError, e: if e.errno == errno.ECHILD: self._children.remove(pid) else: raise if not ch: break time.sleep(0.1) for pid in self._children: warning("Killing forcefully process %d." % pid) # -PID to kill to whole process group os.kill(-pid, signal.SIGKILL) for pid in self._children: try: nemu.subprocess_.poll(pid) except OSError, e: if e.errno != errno.ECHILD: raise finally: for f in self._xauthfiles.values(): try: os.unlink(f) except: pass def reply(self, code, text): "Send back a reply to the client; handle multiline messages" if not hasattr(text, '__iter__'): text = [ text ] clean = [] # Split lines with embedded \n for i in text: clean.extend(i.splitlines()) for i in range(len(clean) - 1): s = str(code) + "-" + clean[i] + "\n" self._wfd.write(s) debug(" %s" % s) s = str(code) + " " + clean[-1] + "\n" self._wfd.write(s) debug(" %s" % s) return def readline(self): "Read a line from the socket and detect connection break-up." # FIXME: should use the eintr_wrapper from environ; why was I using # readline instead of read? while True: try: line = self._rfd.readline() except IOError, e: line = None if e.errno == errno.EINTR: continue else: raise break if not line: self._closed = True return None debug(" %s" % line) return line.rstrip() def readcmd(self): """Main entry point: read and parse a line from the client, handle argument validation and return a tuple (function, command_name, arguments)""" line = self.readline() if not line: return None args = line.split() cmd1 = args[0].upper() if cmd1 not in self._commands: self.reply(500, "Unknown command %s." % cmd1) return None del args[0] cmd2 = None subcommands = self._commands[cmd1] if subcommands.keys() != [ None ]: if len(args) < 1: self.reply(500, "Incomplete command.") return None cmd2 = args[0].upper() del args[0] if cmd2 and cmd2 not in subcommands: self.reply(500, "Unknown sub-command for %s: %s." % (cmd1, cmd2)) return None (mandatory, optional) = subcommands[cmd2] argstemplate = mandatory + optional if cmd2: cmdname = "%s %s" % (cmd1, cmd2) funcname = "do_%s_%s" % (cmd1, cmd2) else: cmdname = cmd1 funcname = "do_%s" % cmd1 if not hasattr(self, funcname): # pragma: no cover self.reply(500, "Not implemented.") return None if len(args) < len(mandatory): self.reply(500, "Missing mandatory arguments for %s." % cmdname) return None if (not argstemplate or argstemplate[-1] != "*") and \ len(args) > len(argstemplate): self.reply(500, "Too many arguments for %s." % cmdname) return None j = 0 for i in range(len(args)): if argstemplate[j] == '*': j = j - 1 if argstemplate[j] == 'i': try: args[i] = int(args[i]) except: self.reply(500, "Invalid parameter %s: must be an integer." % args[i]) return None elif argstemplate[j] == 'b': try: args[i] = _db64(args[i]) except TypeError: self.reply(500, "Invalid parameter: not base-64 encoded.") return None elif argstemplate[j] != 's': # pragma: no cover raise RuntimeError("Invalid argument template: %s" % _argstmpl) # Nothing done for "s" parameters j += 1 func = getattr(self, funcname) debug("Command: %s, args: %s" % (cmdname, args)) return (func, cmdname, args) def run(self): """Main loop; reads commands until the server is shut down or the connection is terminated.""" self.reply(220, "Hello."); while not self._closed: cmd = self.readcmd() if cmd == None: continue try: cmd[0](cmd[1], *cmd[2]) except: (t, v, tb) = sys.exc_info() v.child_traceback = "".join( traceback.format_exception(t, v, tb)) self.reply(550, ["# Exception data follows:", _b64(dumps(v, protocol = 2))]) try: self._rfd.close() self._wfd.close() except: pass self.clean() debug("Server(0x%x) exiting" % id(self)) # FIXME: cleanup # Commands implementation def do_HELP(self, cmdname): reply = ["Available commands:"] for c in sorted(self._commands): for sc in sorted(self._commands[c]): if sc: reply.append("%s %s" % (c, sc)) else: reply.append(c) self.reply(200, reply) def do_QUIT(self, cmdname): self.reply(221, "Sayounara."); self._closed = True def do_PROC_CRTE(self, cmdname, executable, *argv): self._proc = { 'executable': executable, 'argv': argv } self._commands = _proc_commands self.reply(200, "Entering PROC mode.") def do_PROC_USER(self, cmdname, user): self._proc['user'] = user self.reply(200, "Program will run as `%s'." % user) def do_PROC_CWD(self, cmdname, dir): self._proc['cwd'] = dir self.reply(200, "CWD set to `%s'." % dir) def do_PROC_ENV(self, cmdname, *env): if len(env) % 2: self.reply(500, "Invalid number of arguments for PROC ENV: must be even.") return self._proc['env'] = {} for i in range(len(env)/2): self._proc['env'][env[i * 2]] = env[i * 2 + 1] self.reply(200, "%d environment definition(s) read." % (len(env) / 2)) def do_PROC_SIN(self, cmdname): self.reply(354, "Pass the file descriptor now, with `%s\\n' as payload." % cmdname) try: fd, payload = passfd.recvfd(self._rfd, len(cmdname) + 1) except (IOError, RuntimeError), e: self.reply(500, "Error receiving FD: %s" % str(e)) return if payload[0:len(cmdname)] != cmdname: self.reply(500, "Invalid payload: %s." % payload) return m = {'PROC SIN': 'stdin', 'PROC SOUT': 'stdout', 'PROC SERR': 'stderr'} self._proc[m[cmdname]] = fd self.reply(200, 'FD saved as %s.' % m[cmdname]) # Same code for the three commands do_PROC_SOUT = do_PROC_SERR = do_PROC_SIN def do_PROC_RUN(self, cmdname): params = self._proc params['close_fds'] = True # forced self._proc = None self._commands = _proto_commands if 'env' not in params: params['env'] = dict(os.environ) # copy xauth = None if self._xfwd: display, protoname, hexkey = self._xfwd user = params['user'] if 'user' in params else None try: fd, xauth = tempfile.mkstemp() os.close(fd) # stupid xauth format: needs the 'hostname' for local # connections execute([XAUTH_PATH, "-f", xauth, "add", "%s/unix:%d" % (socket.gethostname(), display), protoname, hexkey]) if user: user, uid, gid = nemu.subprocess_.get_user(user) os.chown(xauth, uid, gid) params['env']['DISPLAY'] = "127.0.0.1:%d" % display params['env']['XAUTHORITY'] = xauth except Exception, e: warning("Cannot forward X: %s" % e) try: os.unlink(xauth) except: pass else: if 'DISPLAY' in params['env']: del params['env']['DISPLAY'] try: chld = nemu.subprocess_.spawn(**params) finally: # I can close the fds now for d in ('stdin', 'stdout', 'stderr'): if d in params: os.close(params[d]) self._children.add(chld) self._xauthfiles[chld] = xauth self.reply(200, "%d running." % chld) def do_PROC_ABRT(self, cmdname): self._proc = None self._commands = _proto_commands self.reply(200, "Aborted.") def do_PROC_POLL(self, cmdname, pid): if pid not in self._children: self.reply(500, "Process does not exist.") return if cmdname == 'PROC POLL': ret = nemu.subprocess_.poll(pid) else: ret = nemu.subprocess_.wait(pid) if ret != None: self._children.remove(pid) if pid in self._xauthfiles: try: os.unlink(self._xauthfiles[pid]) except: pass del self._xauthfiles[pid] self.reply(200, "%d exitcode." % ret) else: self.reply(450, "Not finished yet.") # Same code for the two commands do_PROC_WAIT = do_PROC_POLL def do_PROC_KILL(self, cmdname, pid, sig): if pid not in self._children: self.reply(500, "Process does not exist.") return if signal: # -PID to kill to whole process group os.kill(-pid, sig) else: os.kill(-pid, signal.SIGTERM) self.reply(200, "Process signalled.") def do_IF_LIST(self, cmdname, ifnr = None): if ifnr == None: ifdata = nemu.iproute.get_if_data()[0] else: ifdata = nemu.iproute.get_if(ifnr) self.reply(200, ["# Interface data follows.", _b64(dumps(ifdata, protocol = 2))]) def do_IF_SET(self, cmdname, ifnr, *args): if len(args) % 2: self.reply(500, "Invalid number of arguments for IF SET: must be even.") return d = {'index': ifnr} for i in range(len(args) / 2): d[str(args[i * 2])] = args[i * 2 + 1] iface = nemu.iproute.interface(**d) nemu.iproute.set_if(iface) self.reply(200, "Done.") def do_IF_RTRN(self, cmdname, ifnr, ns): nemu.iproute.change_netns(ifnr, ns) self.reply(200, "Done.") def do_IF_DEL(self, cmdname, ifnr): nemu.iproute.del_if(ifnr) self.reply(200, "Done.") def do_ADDR_LIST(self, cmdname, ifnr = None): addrdata = nemu.iproute.get_addr_data()[0] if ifnr != None: addrdata = addrdata[ifnr] self.reply(200, ["# Address data follows.", _b64(dumps(addrdata, protocol = 2))]) def do_ADDR_ADD(self, cmdname, ifnr, address, prefixlen, broadcast = None): if address.find(":") < 0: # crude, I know a = nemu.iproute.ipv4address(address, prefixlen, broadcast) else: a = nemu.iproute.ipv6address(address, prefixlen) nemu.iproute.add_addr(ifnr, a) self.reply(200, "Done.") def do_ADDR_DEL(self, cmdname, ifnr, address, prefixlen): if address.find(":") < 0: # crude, I know a = nemu.iproute.ipv4address(address, prefixlen, None) else: a = nemu.iproute.ipv6address(address, prefixlen) nemu.iproute.del_addr(ifnr, a) self.reply(200, "Done.") def do_ROUT_LIST(self, cmdname): rdata = nemu.iproute.get_route_data() self.reply(200, ["# Routing data follows.", _b64(dumps(rdata, protocol = 2))]) def do_ROUT_ADD(self, cmdname, tipe, prefix, prefixlen, nexthop, ifnr, metric): nemu.iproute.add_route(nemu.iproute.route(tipe, prefix, prefixlen, nexthop, ifnr or None, metric)) self.reply(200, "Done.") def do_ROUT_DEL(self, cmdname, tipe, prefix, prefixlen, nexthop, ifnr, metric): nemu.iproute.del_route(nemu.iproute.route(tipe, prefix, prefixlen, nexthop, ifnr or None, metric)) self.reply(200, "Done.") def do_X11_SET(self, cmdname, protoname, hexkey): if not XAUTH_PATH: self.reply(500, "Impossible to forward X: xauth not present") return skt, port = None, None try: skt, port = find_listen_port(min_port = 6010, max_port = 6099) except: self.reply(500, "Cannot allocate a port for X forwarding.") return display = port - 6000 self.reply(200, "Socket created on port %d. Use X11 SOCK to get the " "file descriptor " "(fixed 1-byte payload before protocol response).") self._xfwd = display, protoname, hexkey self._xsock = skt def do_X11_SOCK(self, cmdname): if not self._xsock: self.reply(500, "X forwarding not set up.") return # Needs to be a separate command to handle synch & buffering issues try: passfd.sendfd(self._wfd, self._xsock.fileno(), "1") except: # need to fill the buffer on the other side, nevertheless self._wfd.write("1") self.reply(500, "Error sending file descriptor.") return self._xsock = None self.reply(200, "Will set up X forwarding.") # ============================================================================ # # Client-side protocol implementation. # class Client(object): """Client-side implementation of the communication protocol. Acts as a RPC service.""" def __init__(self, rfd, wfd): debug("Client(0x%x).__init__()" % id(self)) self._rfd = _get_file(rfd, "r") self._wfd = _get_file(wfd, "w") self._forwarder = None # Wait for slave to send banner self._read_and_check_reply() def __del__(self): debug("Client(0x%x).__del__()" % id(self)) self.shutdown() def _send_cmd(self, *args): if not self._wfd: raise RuntimeError("Client already shut down.") s = " ".join(map(str, args)) + "\n" self._wfd.write(s) def _read_reply(self): """Reads a (possibly multi-line) response from the server. Returns a tuple containing (code, text)""" if not self._rfd: raise RuntimeError("Client already shut down.") text = [] while True: line = eintr_wrapper(self._rfd.readline).rstrip() if not line: raise RuntimeError("Protocol error, empty line received") m = re.search(r'^(\d{3})([ -])(.*)', line) if not m: raise RuntimeError("Protocol error, read: %r" % line) status = m.group(1) text.append(m.group(3)) if m.group(2) == " ": break return (int(status), "\n".join(text)) def _read_and_check_reply(self, expected = 2): """Reads a response and raises an exception if the first digit of the code is not the expected value. If expected is not specified, it defaults to 2.""" code, text = self._read_reply() if code == 550: # exception e = loads(_db64(text.partition("\n")[2])) raise e if code / 100 != expected: raise RuntimeError("Error from slave: %d %s" % (code, text)) return text def shutdown(self): "Tell the client to quit." if not self._wfd: return debug("Client(0x%x).shutdown()" % id(self)) self._send_cmd("QUIT") self._read_and_check_reply() self._rfd.close() self._rfd = None self._wfd.close() self._wfd = None if self._forwarder: os.kill(self._forwarder, signal.SIGTERM) self._forwarder = None def _send_fd(self, name, fd): "Pass a file descriptor" self._send_cmd("PROC", name) self._read_and_check_reply(3) try: passfd.sendfd(self._wfd, fd, "PROC " + name) except: # need to fill the buffer on the other side, nevertheless self._wfd.write("=" * (len(name) + 5) + "\n") # And also read the expected error self._read_and_check_reply(5) raise self._read_and_check_reply() def spawn(self, argv, executable = None, stdin = None, stdout = None, stderr = None, cwd = None, env = None, user = None): """Start a subprocess in the slave; the interface resembles subprocess.Popen, but with less functionality. In particular stdin/stdout/stderr can only be None or a open file descriptor. See nemu.subprocess_.spawn for details.""" if executable == None: executable = argv[0] params = ["PROC", "CRTE", _b64(executable)] for i in argv: params.append(_b64(i)) self._send_cmd(*params) self._read_and_check_reply() # After this, if we get an error, we have to abort the PROC try: if user != None: self._send_cmd("PROC", "USER", _b64(user)) self._read_and_check_reply() if cwd != None: self._send_cmd("PROC", "CWD", _b64(cwd)) self._read_and_check_reply() if env != None: params = [] for k, v in env.items(): params.extend([_b64(k), _b64(v)]) self._send_cmd("PROC", "ENV", *params) self._read_and_check_reply() if stdin != None: self._send_fd("SIN", stdin) if stdout != None: self._send_fd("SOUT", stdout) if stderr != None: self._send_fd("SERR", stderr) except: self._send_cmd("PROC", "ABRT") self._read_and_check_reply() raise self._send_cmd("PROC", "RUN") pid = int(self._read_and_check_reply().split()[0]) return pid def poll(self, pid): """Equivalent to Popen.poll(), checks if the process has finished. Returns the exitcode if finished, None otherwise.""" self._send_cmd("PROC", "POLL", pid) code, text = self._read_reply() if code / 100 == 2: exitcode = int(text.split()[0]) return exitcode if code / 100 == 4: return None else: raise RuntimeError("Error on command: %d %s" % (code, text)) def wait(self, pid): """Equivalent to Popen.wait(). Waits for the process to finish and returns the exitcode.""" self._send_cmd("PROC", "WAIT", pid) text = self._read_and_check_reply() exitcode = int(text.split()[0]) return exitcode def signal(self, pid, sig = signal.SIGTERM): """Equivalent to Popen.send_signal(). Sends a signal to the child process; signal defaults to SIGTERM.""" if sig: self._send_cmd("PROC", "KILL", pid, sig) else: self._send_cmd("PROC", "KILL", pid) self._read_and_check_reply() def get_if_data(self, ifnr = None): if ifnr: self._send_cmd("IF", "LIST", ifnr) else: self._send_cmd("IF", "LIST") data = self._read_and_check_reply() return loads(_db64(data.partition("\n")[2])) def set_if(self, interface): cmd = ["IF", "SET", interface.index] for k in interface.changeable_attributes: v = getattr(interface, k) if v != None: cmd += [k, str(v)] self._send_cmd(*cmd) self._read_and_check_reply() def del_if(self, ifnr): self._send_cmd("IF", "DEL", ifnr) self._read_and_check_reply() def change_netns(self, ifnr, netns): self._send_cmd("IF", "RTRN", ifnr, netns) self._read_and_check_reply() def get_addr_data(self, ifnr = None): if ifnr: self._send_cmd("ADDR", "LIST", ifnr) else: self._send_cmd("ADDR", "LIST") data = self._read_and_check_reply() return loads(_db64(data.partition("\n")[2])) def add_addr(self, ifnr, address): if hasattr(address, "broadcast") and address.broadcast: self._send_cmd("ADDR", "ADD", ifnr, address.address, address.prefix_len, address.broadcast) else: self._send_cmd("ADDR", "ADD", ifnr, address.address, address.prefix_len) self._read_and_check_reply() def del_addr(self, ifnr, address): self._send_cmd("ADDR", "DEL", ifnr, address.address, address.prefix_len) self._read_and_check_reply() def get_route_data(self): self._send_cmd("ROUT", "LIST") data = self._read_and_check_reply() return loads(_db64(data.partition("\n")[2])) def add_route(self, route): self._add_del_route("ADD", route) def del_route(self, route): self._add_del_route("DEL", route) def _add_del_route(self, action, route): args = ["ROUT", action, _b64(route.tipe), _b64(route.prefix), route.prefix_len or 0, _b64(route.nexthop), route.interface or 0, route.metric or 0] self._send_cmd(*args) self._read_and_check_reply() def set_x11(self, protoname, hexkey): # Returns a socket ready to accept() connections self._send_cmd("X11", "SET", protoname, hexkey) self._read_and_check_reply() # Receive the socket self._send_cmd("X11", "SOCK") fd, payload = passfd.recvfd(self._rfd, 1) self._read_and_check_reply() skt = socket.fromfd(fd, socket.AF_INET, socket.SOCK_DGRAM) os.close(fd) # fromfd dup()'s return skt def enable_x11_forwarding(self): xinfo = _parse_display() if not xinfo: raise RuntimeError("Impossible to forward X: DISPLAY variable not " "set or invalid") xauthdpy, sock, addr = xinfo if not XAUTH_PATH: raise RuntimeError("Impossible to forward X: xauth not present") auth = backticks([XAUTH_PATH, "list", xauthdpy]) match = re.match(r"\S+\s+(\S+)\s+(\S+)\n", auth) if not match: raise RuntimeError("Impossible to forward X: invalid DISPLAY") protoname, hexkey = match.groups() server = self.set_x11(protoname, hexkey) self._forwarder = _spawn_x11_forwarder(server, sock, addr) def _b64(text): if text == None: # easier this way text = '' text = str(text) if len(text) == 0 or filter(lambda x: ord(x) <= ord(" ") or ord(x) > ord("z") or x == "=", text): return "=" + base64.b64encode(text) else: return text def _db64(text): if not text or text[0] != '=': return text return base64.b64decode(text[1:]) def _get_file(fd, mode): # Since fdopen insists on closing the fd on destruction, I need to dup() if hasattr(fd, "fileno"): nfd = os.dup(fd.fileno()) else: nfd = os.dup(fd) return os.fdopen(nfd, mode, 1) def _parse_display(): if "DISPLAY" not in os.environ: return None dpy = os.environ["DISPLAY"] match = re.search(r"^(.*):(\d+)(?:\.\d+)?$", dpy) if not match: return None host, number = match.groups() if host and host != "unix": sock = (socket.AF_INET, socket.SOCK_STREAM, 0) addr = (host, 6000 + int(number)) else: sock = (socket.AF_UNIX, socket.SOCK_STREAM, 0) addr = ("/tmp/.X11-unix/X%s" % number) # Xauth does not work with DISPLAYs of the form localhost:X. if host and host != "localhost": xauthdpy = dpy else: xauthdpy = "unix:%s" % number return xauthdpy, sock, addr def _spawn_x11_forwarder(server, xsock, xaddr): server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.listen(10) # arbitrary pid = os.fork() if pid: return pid # XXX: clear signals, etc try: _x11_forwarder(server, xsock, xaddr) except: traceback.print_exc(file=sys.stderr) os._exit(1) def _x11_forwarder(server, xsock, xaddr): def clean(idx, toread, fd): # silently discards any buffer! fd1 = fd fd2 = idx[fd]["wr"] try: fd1.close() except: pass try: fd2.close() except: pass del idx[fd1] if fd1 in toread: toread.remove(fd1) del idx[fd2] if fd2 in toread: toread.remove(fd2) toread = set([server]) idx = {} while(True): towrite = [x["wr"] for x in idx.values() if x["buf"]] (rr, wr, er) = select.select(toread, towrite, []) if server in rr: xconn = socket.socket(*xsock) xconn.connect(xaddr) client, addr = server.accept() toread.add(client) toread.add(xconn) idx[client] = { "rd": client, "wr": xconn, "buf": [], "closed": False } idx[xconn] = { "rd": xconn, "wr": client, "buf": [], "closed": False } continue for fd in rr: chan = idx[fd] try: s = os.read(fd.fileno(), 4096) except OSError, e: if e.errno == errno.ECONNRESET: clean(idx, toread, fd) continue elif e.errno == errno.EINTR: continue else: raise if s == "": # fd closed for read toread.remove(fd) chan["closed"] = True if not chan["buf"]: # close the writing side try: chan["wr"].shutdown(socket.SHUT_WR) except: pass # might fail sometimes else: chan["buf"].append(s) for fd in wr: chan = idx[idx[fd]["wr"]] try: x = os.write(fd.fileno(), chan["buf"][0]) except OSError, e: if e.errno == errno.EINTR: continue if e.errno == errno.EPIPE or e.errno == errno.ECONNRESET: clean(idx, toread, fd) continue raise if x < len(chan["buf"][0]): chan["buf"][0] = chan["buf"][x:] else: del chan["buf"][0] if not chan["buf"] and chan["closed"]: chan["wr"].shutdown(socket.SHUT_WR) chan["buf"] = None # clean-up for chan in idx.values(): if chan["rd"] not in idx: # already deleted continue twin = idx[chan["wr"]] if not chan["closed"] or chan["buf"] or not twin["closed"] \ or twin["buf"]: continue clean(idx, toread, chan["rd"]) nemu-0.2/protocol.txt0000644000175000017500000000610512225521027015513 0ustar tinchotincho00000000000000Protocol format: ---------------- RFC 2821-like. At start-up, server sends 220 code and greeting text To close the connection and the node, client sends QUIT command, and server replies with 221 code. Command Subcmd Arguments Response Effect QUIT 221 Close the netns IF LIST [if#] 200 serialised data ip link list IF SET if# k v k v... 200/500 ip link set (1) IF RTRN if# ns 200/500 ip link set netns $ns IF DEL if# 200/500 ip link del ADDR LIST [if#] 200 serialised data ip addr list ADDR ADD if# addr_spec 200/500 ip addr add ADDR DEL if# addr_spec 200/500 ip addr del ROUT LIST 200 serialised data ip route list ROUT ADD route_spec 200/500 ip route add ROUT DEL route_spec 200/500 ip route del PROC CRTE argv0 argv1... 200/500 (2) PROC USER username 200/500 (3) PROC CWD cwd 200/500 (3) PROC ENV k v k v... 200/500 (3) PROC SIN 354+200/500 (4) PROC SOUT 354+200/500 (4) PROC SERR 354+200/500 (4) PROC RUN 200 /500 (5) PROC ABRT 200 (5) PROC POLL 200 /450/500 check if process alive PROC WAIT 200 /500 waitpid(pid) PROC KILL 200/500 kill(pid, signal) X11 354+200/500 (6) (1) valid arguments: mtu , up <0|1>, name , lladdr , broadcast , multicast <0|1>, arp <0|1>. (2) After PROC CRTE, only secondary PROC cmds are accepted until finished. The parameters are parsed as base64-encoded strings if they start with a '=' character. (3) Secondary PROC commands, only valid after PROC CRTE. All parameters parsed as base64-encoded strings. Arguments for PROC ENV are pairs of key-value to set up the process environment. (4) Secondary PROC commands, only valid after PROC CRTE. Server reply 354 and waits for a file descriptor to be passed along with a duplicate of the same command. Answers 200/500 after processing the file descriptor. (5) Secondary PROC commands, unconditionally end the PROC transaction. If RUN was successful, the process is started and the process ID is returned as the first token of the reply. (6) Enable X11 forwarding, using the specified protocol and data for authentication. A opened socket ready to receive X connections is passed over the channel. Answers 200/500 after transmitting the file descriptor. Sample session -------------- Parent calls socketpair(), fork() and unshare(); thus creating a new netns; protocol exchanges occur through the socket. 220 Hello IF LIST 200-[{id: 1, mtu: 16436, name: lo, up: true}, {id: 10, 200 lladdr: '12:34:56:78:9a:bc', mtu: 1500, name: eth0, up: true}] IF SET 10 MTU 1492 200 Ok. ADDR ADD 10 10.0.0.1 24 10.0.0.255 200 Ok. ADDR DEL 10 192.168.1.1 24 500 Address does not exist. PROC CRTE /bin/sh sh -c sleep 10 200 Entering PROC mode. PROC USER nobody 200 Program will run as `nobody'. PROC CWD / 200 CWD set to /. PROC SIN 354 Waiting for FD. Server calls recvmsg() Client calls sendmsg() 200 FD received OK. PROC RUN 200 1649 pid process started. PROC WAIT 1649 Time passes... 200 0 exit code QUIT 221 Exiting... nemu-0.2/setup.py0000755000175000017500000000120012225521027014615 0ustar tinchotincho00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # vim: ts=4:sw=4:et:ai:sts=4 from distutils.core import setup, Extension, Command setup( name = 'nemu', version = '0.2', description = 'A lightweight network emulator embedded in a small ' 'python library.', author = 'Martín Ferrari, Alina Quereilhac', author_email = 'martin.ferrari@gmail.com, aquereilhac@gmail.com', url = 'http://code.google.com/p/nemu/', license = 'GPLv2', platforms = 'Linux', packages = ['nemu'], package_dir = {'': 'src'} ) nemu-0.2/Makefile0000644000175000017500000000364312225521027014555 0ustar tinchotincho00000000000000SRC = src TEST = test BUILDDIR = build DISTDIR = dist COVERDIR = coverage SRCFILES := $(shell find $(SRC) -type f) # stupid distutils, it's broken in so many ways SUBBUILDDIR := $(shell python -c 'import distutils.util, sys; \ print "lib.%s-%s" % (distutils.util.get_platform(), \ sys.version[0:3])') PYTHON25 := $(shell python -c 'import sys; v = sys.version_info; \ print (1 if v[0] <= 2 and v[1] <= 5 else 0)') ifeq ($(PYTHON25),0) BUILDDIR := $(BUILDDIR)/$(SUBBUILDDIR) else BUILDDIR := $(BUILDDIR)/lib endif COVERAGE := $(or $(shell which coverage), $(shell which python-coverage), \ coverage) all: build_stamp build_stamp: $(SRCFILES) ./setup.py build touch $@ install: all ./setup.py install test: all retval=0; \ for i in `find "$(TEST)" -perm -u+x -type f`; do \ echo $$i; \ PYTHONPATH="$(BUILDDIR)" $$i || retval=$$?; \ done; exit $$retval coverage: coverage_stamp $(COVERAGE) -r -m `find "$(BUILDDIR)" -name \\*.py -type f` coverage-report: coverage_stamp rm -rf $(COVERDIR) $(COVERAGE) -b -d $(COVERDIR) `find "$(BUILDDIR)" -name \\*.py -type f` @echo "Coverage report created in $(COVERDIR)/index.html" coverage_stamp: build_stamp if [ `id -u` -ne 0 ]; then \ echo "Coverage needs to be run as root."; false; fi for i in `find "$(TEST)" -perm -u+x -type f`; do \ set -e; \ PYTHONPATH="$(BUILDDIR)" $(COVERAGE) -x $$i; \ done $(COVERAGE) -c touch $@ clean: ./setup.py clean rm -f `find -name \*.pyc` .coverage *.pcap *_stamp rm -rf $(COVERDIR) #$(MAKE) -C $(CURDIR)/benchmarks/ clean distclean: clean rm -rf "$(DISTDIR)" MANIFEST: distclean find . -path ./.hg -prune -o -path ./build -prune -o \ -path ./benchmarks -prune -o \ -name \*.pyc -prune -o -name \*.swp -prune -o \ -name MANIFEST -prune -o -name .hg\* -prune -o \ -type f -print | sed 's#^\./##' | sort > MANIFEST dist: MANIFEST ./setup.py sdist .PHONY: clean distclean dist test coverage install MANIFEST nemu-0.2/PKG-INFO0000644000175000017500000000050012235530226014201 0ustar tinchotincho00000000000000Metadata-Version: 1.0 Name: nemu Version: 0.2 Summary: A lightweight network emulator embedded in a small python library. Home-page: http://code.google.com/p/nemu/ Author: Martín Ferrari, Alina Quereilhac Author-email: martin.ferrari@gmail.com, aquereilhac@gmail.com License: GPLv2 Description: UNKNOWN Platform: Linux nemu-0.2/examples/0000755000175000017500000000000012235530226014727 5ustar tinchotincho00000000000000nemu-0.2/examples/sample.py0000755000175000017500000000436412225521027016572 0ustar tinchotincho00000000000000#!/usr/bin/env python # vim:ts=4:sw=4:et:ai:sts=4 import os, nemu, subprocess, time xterm = nemu.environ.find_bin("xterm") X = "DISPLAY" in os.environ and xterm # each Node is a netns node0 = nemu.Node(forward_X11 = X) node1 = nemu.Node(forward_X11 = X) node2 = nemu.Node(forward_X11 = X) print "Nodes started with pids: %s" % str((node0.pid, node1.pid, node2.pid)) # interface object maps to a veth pair with one end in a netns if0 = nemu.NodeInterface(node0) if1a = nemu.NodeInterface(node1) # Between node1 and node2, we use a P2P interface (if1b, if2) = nemu.P2PInterface.create_pair(node1, node2) switch0 = nemu.Switch( bandwidth = 100 * 1024 * 1024, delay = 0.1, # 100 ms delay_jitter = 0.01, # 10ms delay_correlation = 0.25, # 25% correlation loss = 0.005) # connect the interfaces switch0.connect(if0) switch0.connect(if1a) # bring the interfaces up switch0.up = if0.up = if1a.up = if1b.up = if2.up = True # Add IP addresses if0.add_v4_address(address = '10.0.0.1', prefix_len = 24) if1a.add_v4_address(address = '10.0.0.2', prefix_len = 24) if1b.add_v4_address(address = '10.0.1.1', prefix_len = 24) if2.add_v4_address(address = '10.0.1.2', prefix_len = 24) # Configure routing node0.add_route(prefix = '10.0.1.0', prefix_len = 24, nexthop = '10.0.0.2') node2.add_route(prefix = '10.0.0.0', prefix_len = 24, nexthop = '10.0.1.1') # Test connectivity first. Run process, hide output and check # return code null = file("/dev/null", "w") app0 = node0.Popen("ping -c 1 10.0.1.2", shell = True, stdout = null) ret = app0.wait() assert ret == 0 app1 = node2.Popen("ping -c 1 10.0.0.1", shell = True, stdout = null) ret = app1.wait() assert ret == 0 print "Connectivity IPv4 OK!" if X: app1 = node1.Popen("%s -geometry -0+0 -e %s -ni %s" % (xterm, nemu.environ.TCPDUMP_PATH, if1b.name), shell = True) time.sleep(3) app0 = node0.Popen("%s -geometry +0+0 -e ping -c 10 10.0.1.2" % xterm, shell = True) app0.wait() app1.signal() app1.wait() # Now test the network conditions # When using a args list, the shell is not needed app2 = node2.Popen(["ping", "-q", "-c100000", "-f", "10.0.1.2"], stdout = subprocess.PIPE) out, err = app2.communicate() print "Ping outout:" print out nemu-0.2/sample-api.txt0000644000175000017500000000753612225521027015713 0ustar tinchotincho00000000000000#/usr/bin/env python # vim:ts=4:sw=4:et:ai:sts=4 import nemu import signal # run_as: user to setuid() to before running applications (this is assumed to # run as root) nemu.config.run_as = 'nobody' # Clean-up is essential to avoid leaving bridge devices all over the place # (luckily, the veths die automatically). This installs signals and exit # handlers. nemu.set_cleanup_hooks(on_exit = True, on_signals = [signal.SIGTERM, signal.SIGINT]) # each Node is a netns a = nemu.Node() b = nemu.Node() print "Nodes started with pids: %d and %d" % (a.pid, b.pid) # interface object maps to a veth pair with one end in a netns if0 = a.add_if(lladdr = '42:71:e0:90:ca:42') # This is equivalent #if0 = nemu.NodeInterface(a) #if0.lladdr = '42:71:e0:90:ca:42' if1 = b.add_if(mtu = 1492) # for using with a tun device, to connect to the outside world if2 = b.import_if('tun0') # each Switch is a linux bridge, all the parameters are applied to the # associated interfaces as tc qdiscs. switch0 = nemu.Switch(bandwidth = 100 * 1024 * 1024, delay = 0.01, delay_jitter = 0.001, delay_correlation = 0.25, delay_distribution = 'normal', loss = 0.005, loss_correlation = 0.20, dup = 0.005, dup_correlation = 0.25, corrupt = 0.005, corrupt_correlation = 0.25) # connect to the bridge switch0.connect(if0) switch0.connect(if1) # Should be experimented with Tom Geoff's patch to see if the bridge could be # avoided; but for that the API would be slightly different, as these would be # point-to-point interfaces and links. # ppp0 = nemu.PPPSwitch(a, b, bandwidth = ....) # if0 = ppp0.interface(a) # For now, we have simple P2P interfaces: (pppa, pppb) = nemu.P2PInterface.create_pair(a, b) # Add and connect a tap device (as if a external router were plugged into a # switch) if2 = nemu.ImportedInterface('tap0') switch0.connect(if2) switch0.up = True if0.up = True if1.up = True # addresses as iproute if0.add_v4_address(addr = '10.0.0.1', prefix_len = 24) if0.add_v6_address(addr = 'fe80::222:19ff:fe22:615d', prefix_len = 64) if1.add_v4_address(addr = '10.0.0.2', prefix_len = 24, broadcast = '10.1.0.255') # ditto #a.add_route(prefix = '0', prefix_len = 0, nexthop = '10.0.0.2') a.add_default_route(nexthop = '10.0.0.2') b.add_route(prefix = '10.1.0.0', prefix_len = 16, nexthop = '10.0.0.1') b.add_route(prefix = '11.1.0.1', prefix_len = 32, device = if1) # Some inspection methods: they will not read internal data but query the # kernel addrs = if0.get_addresses() stats = if0.get_stats() routes = a.get_routes() ifaces = a.get_interfaces() nodes = nemu.get_nodes() switches = nemu.get_switches() stats = link0.get_stats() # Run a process in background import subprocess app0 = a.Popen("ping -c 3 10.0.0.2", shell = True, stdout = subprocess.PIPE) print app0.stdout.readline() app0.wait() # Run, capture output and wait() stdout = a.backticks(["ping", "-c", "3", "10.0.0.2"]) # Run an process with a pseudo-tty associated to it; provide a UNIX socket to # interact with the process app2 = a.start_tty_process("/bin/bash") # app2.sockname, app2.sockfd app2.wait() # Example to set up a linear topology def setup_linear_topology(n, bd, delay): nodes = [] for i in range(n): nodes.append(nemu.Node()) for i in range(n - 1): if1 = nodes[i].add_if() if2 = nodes[i + 1].add_if() if1.add_v4_address(addr = ('10.0.%d.2' % i), prefix_len = 24) if2.add_v4_address(addr = ('10.0.%d.1' % i), prefix_len = 24) switch = nemu.Switch(bandwidth = bd, delay = delay) switch.connect(if1) switch.connect(if2) for i in range(n): for j in range(n): if abs(i - j) <= 1: continue nodes[i].add_route(prefix = ('10.0.%d.0' % j), prefix_len = 24, nexthop = ('10.0.%d.%d' % ((i, 1) if i < j else (i - 1, 2))) ) return nodes nemu-0.2/test/0000755000175000017500000000000012235530226014070 5ustar tinchotincho00000000000000nemu-0.2/test/test_interfaces.py0000755000175000017500000002116412225521027017631 0ustar tinchotincho00000000000000#!/usr/bin/env python # vim:ts=4:sw=4:et:ai:sts=4 from test_util import get_devs, get_devs_netns from nemu.environ import * import nemu, test_util import os, unittest class TestUtils(unittest.TestCase): def test_utils(self): devs = get_devs() # There should be at least loopback! self.assertTrue(len(devs) > 0) self.assertTrue('lo' in devs) self.assertTrue(devs['lo']['up']) self.assertEquals(devs['lo']['lladdr'], '00:00:00:00:00:00') self.assertTrue( { 'address': '127.0.0.1', 'prefix_len': 8, 'broadcast': None, 'family': 'inet' } in devs['lo']['addr']) class TestIPRouteStuff(unittest.TestCase): def test_fix_lladdr(self): fl = nemu.iproute._fix_lladdr self.assertEquals(fl('42:71:e0:90:ca:42'), '42:71:e0:90:ca:42') self.assertEquals(fl('4271E090CA42'), '42:71:e0:90:ca:42', 'Normalization of link-level address: missing colons and ' 'upper caps') self.assertEquals(fl('2:71:E:90:CA:42'), '02:71:0e:90:ca:42', 'Normalization of link-level address: missing zeroes') self.assertEquals(fl('271E090CA42'), '02:71:e0:90:ca:42', 'Automatic normalization of link-level address: missing ' 'colons and zeroes') self.assertRaises(ValueError, fl, 'foo') self.assertRaises(ValueError, fl, '42:71:e0:90:ca42') self.assertRaises(ValueError, fl, '1234567890123') def test_any_to_bool(self): a2b = nemu.iproute._any_to_bool for i in (True, 2, 'trUe', '1', 'foo', 1.0, [1]): self.assertTrue(a2b(i)) for i in (False, 0, 'falsE', '0', '', 0.0, []): self.assertFalse(a2b(i)) def test_non_empty_str(self): nes = nemu.iproute._non_empty_str self.assertEquals(nes(''), None) self.assertEquals(nes('Foo'), 'Foo') self.assertEquals(nes(1), '1') def test_interface(self): i = nemu.iproute.interface(index = 1) self.assertRaises(AttributeError, setattr, i, 'index', 2) self.assertRaises(ValueError, setattr, i, 'mtu', -1) self.assertEquals(repr(i), 'nemu.iproute.interface(index = 1, ' 'name = None, up = None, mtu = None, lladdr = None, ' 'broadcast = None, multicast = None, arp = None)') i.name = 'foo'; i.up = 1; i.arp = True; i.mtu = 1500 self.assertEquals(repr(i), 'nemu.iproute.interface(index = 1, ' 'name = \'foo\', up = True, mtu = 1500, lladdr = None, ' 'broadcast = None, multicast = None, arp = True)') j = nemu.iproute.interface(index = 2) j.name = 'bar'; j.up = False; j.arp = 1 # Modifications to turn j into i. self.assertEquals(repr(i - j), 'nemu.iproute.interface(index = 1, ' 'name = \'foo\', up = True, mtu = 1500, lladdr = None, ' 'broadcast = None, multicast = None, arp = None)') # Modifications to turn i into j. self.assertEquals(repr(j - i), 'nemu.iproute.interface(index = 2, ' 'name = \'bar\', up = False, mtu = None, lladdr = None, ' 'broadcast = None, multicast = None, arp = None)') class TestInterfaces(unittest.TestCase): @test_util.skipUnless(os.getuid() == 0, "Test requires root privileges") def test_interface_creation(self): node0 = nemu.Node() ifaces = [] for i in range(5): ifaces.append(node0.add_if()) devs = get_devs_netns(node0) for i in range(5): self.assertTrue(devs['lo']['up']) self.assertTrue(ifaces[i].name in devs) node_devs = set(node0.get_interfaces()) self.assertTrue(set(ifaces).issubset(node_devs)) loopback = node_devs - set(ifaces) # should be! self.assertEquals(len(loopback), 1) self.assertEquals(loopback.pop().name, 'lo') devs = get_devs() for i in range(5): peer_name = nemu.iproute.get_if(ifaces[i].control.index).name self.assertTrue(peer_name in devs) @test_util.skipUnless(os.getuid() == 0, "Test requires root privileges") def test_interface_settings(self): node0 = nemu.Node() if0 = node0.add_if(lladdr = '42:71:e0:90:ca:42', mtu = 1492) self.assertEquals(if0.lladdr, '42:71:e0:90:ca:42', "Constructor parameters") self.assertEquals(if0.mtu, 1492, "Constructor parameters") if0.lladdr = '4271E090CA42' self.assertEquals(if0.lladdr, '42:71:e0:90:ca:42', """Normalization of link-level address: missing colons and upper caps""") if0.lladdr = '2:71:E0:90:CA:42' self.assertEquals(if0.lladdr, '02:71:e0:90:ca:42', """Normalization of link-level address: missing zeroes""") if0.lladdr = '271E090CA42' self.assertEquals(if0.lladdr, '02:71:e0:90:ca:42', """Automatic normalization of link-level address: missing colons and zeroes""") self.assertRaises(ValueError, setattr, if0, 'lladdr', 'foo') self.assertRaises(ValueError, setattr, if0, 'lladdr', '1234567890123') self.assertEquals(if0.mtu, 1492) # detected by setter self.assertRaises(ValueError, setattr, if0, 'mtu', 0) # error from ip self.assertRaises(RuntimeError, setattr, if0, 'mtu', 1) self.assertRaises(RuntimeError, setattr, if0, 'mtu', 65537) devs = get_devs_netns(node0) self.assertTrue(if0.name in devs) self.assertFalse(devs[if0.name]['up']) self.assertEquals(devs[if0.name]['lladdr'], if0.lladdr) self.assertEquals(devs[if0.name]['mtu'], if0.mtu) if0.up = True devs = get_devs_netns(node0) self.assertTrue(devs[if0.name]['up']) # Verify that data is actually read from the kernel r = node0.system([IP_PATH, "link", "set", if0.name, "mtu", "1500"]) self.assertEquals(r, 0) devs = get_devs_netns(node0) self.assertEquals(devs[if0.name]['mtu'], 1500) self.assertEquals(devs[if0.name]['mtu'], if0.mtu) # FIXME: get_stats @test_util.skipUnless(os.getuid() == 0, "Test requires root privileges") def test_interface_addresses(self): node0 = nemu.Node() if0 = node0.add_if() if0.add_v4_address(address = '10.0.0.1', prefix_len = 24, broadcast = '10.0.0.255') if0.add_v4_address(address = '10.0.2.1', prefix_len = 26) if0.add_v6_address(address = 'fe80::222:19ff:fe22:615d', prefix_len = 64) devs = get_devs_netns(node0) self.assertTrue( { 'address': '10.0.0.1', 'prefix_len': 24, 'broadcast': '10.0.0.255', 'family': 'inet' } in devs[if0.name]['addr']) self.assertTrue( { 'address': '10.0.2.1', 'prefix_len': 26, 'broadcast': '10.0.2.63', 'family': 'inet' } in devs[if0.name]['addr']) self.assertTrue( { 'address': 'fe80::222:19ff:fe22:615d', 'prefix_len': 64, 'family': 'inet6' } in devs[if0.name]['addr']) self.assertTrue(len(if0.get_addresses()) >= 2) self.assertEquals(if0.get_addresses(), devs[if0.name]['addr']) class TestWithDummy(unittest.TestCase): def setUp(self): self.cleanup = [] @test_util.skipUnless(os.getuid() == 0, "Test requires root privileges") @test_util.skipUnless( test_util.get_linux_ver() >= test_util.make_linux_ver("2.6.35"), "Test trigger a kernel bug on 2.6.34") def test_interface_migration(self): node = nemu.Node() self.dummyname = "dummy%d" % os.getpid() self.assertEquals(os.system("%s link add name %s type dummy" % (IP_PATH, self.dummyname)), 0) devs = get_devs() self.assertTrue(self.dummyname in devs) dummyidx = devs[self.dummyname]['idx'] if0 = node.import_if(self.dummyname) self.assertTrue(self.dummyname not in get_devs()) node_devs = dict([(i.index, i) for i in node.get_interfaces()]) self.assertTrue(dummyidx in node_devs) if0.lladdr = '42:71:e0:90:ca:43' if0.mtu = 1400 devs = get_devs_netns(node) self.assertTrue(if0.name in devs) self.assertEquals(devs[if0.name]['lladdr'], '42:71:e0:90:ca:43') self.assertEquals(devs[if0.name]['mtu'], 1400) node.destroy() self.assertTrue(self.dummyname in get_devs()) def tearDown(self): # oops here if hasattr(self, 'dummyname'): os.system("%s link del %s" % (IP_PATH, self.dummyname)) # FIXME: Links if __name__ == '__main__': unittest.main() nemu-0.2/test/test_core.py0000755000175000017500000002252412225521027016437 0ustar tinchotincho00000000000000#!/usr/bin/env python # vim:ts=4:sw=4:et:ai:sts=4 import grp, os, pwd, select, time, unittest import nemu, test_util class TestConfigure(unittest.TestCase): def test_config_run_as_static(self): # Don't allow root as default user self.assertRaises(AttributeError, setattr, nemu.config, 'run_as', 'root') self.assertRaises(AttributeError, setattr, nemu.config, 'run_as', 0) # Don't allow invalid users self.assertRaises(AttributeError, setattr, nemu.config, 'run_as', 'foobarbaz') # hope nobody has this user! self.assertRaises(AttributeError, setattr, nemu.config, 'run_as', 65536) try: pwd.getpwnam('nobody') nemu.config.run_as('nobody') self.assertEquals(nemu.config.run_as, 'nobody') except: pass class TestGlobal(unittest.TestCase): @test_util.skipUnless(os.getuid() == 0, "Test requires root privileges") def test_run_ping_p2pif(self): n1 = nemu.Node() n2 = nemu.Node() i1, i2 = nemu.P2PInterface.create_pair(n1, n2) i1.up = i2.up = True i1.lladdr = 'd6:4b:3f:f7:ff:7e' i2.lladdr = 'd6:4b:3f:f7:ff:7f' i1.add_v4_address('10.0.0.1', 24) i2.add_v4_address('10.0.0.2', 24) null = file('/dev/null', 'wb') a1 = n1.Popen(['ping', '-qc1', '10.0.0.2'], stdout = null) a2 = n2.Popen(['ping', '-qc1', '10.0.0.1'], stdout = null) self.assertEquals(a1.wait(), 0) self.assertEquals(a2.wait(), 0) # Test ipv6 autoconfigured addresses time.sleep(2) # Wait for autoconfiguration a1 = n1.Popen(['ping6', '-qc1', '-I', i1.name, 'fe80::d44b:3fff:fef7:ff7f'], stdout = null) a2 = n2.Popen(['ping6', '-qc1', '-I', i2.name, 'fe80::d44b:3fff:fef7:ff7e'], stdout = null) self.assertEquals(a1.wait(), 0) self.assertEquals(a2.wait(), 0) @test_util.skipUnless(os.getuid() == 0, "Test requires root privileges") def test_run_ping_node_if(self): n1 = nemu.Node() n2 = nemu.Node() i1 = n1.add_if() i2 = n2.add_if() i1.up = i2.up = True l = nemu.Switch() l.connect(i1) l.connect(i2) l.up = True i1.add_v4_address('10.0.0.1', 24) i2.add_v4_address('10.0.0.2', 24) null = file('/dev/null', 'wb') a1 = n1.Popen(['ping', '-qc1', '10.0.0.2'], stdout = null) a2 = n2.Popen(['ping', '-qc1', '10.0.0.1'], stdout = null) self.assertEquals(a1.wait(), 0) self.assertEquals(a2.wait(), 0) @test_util.skipUnless(os.getuid() == 0, "Test requires root privileges") def test_run_ping_routing_p2p(self): n1 = nemu.Node() n2 = nemu.Node() n3 = nemu.Node() i12, i21 = nemu.P2PInterface.create_pair(n1, n2) i23, i32 = nemu.P2PInterface.create_pair(n2, n3) i12.up = i21.up = i23.up = i32.up = True i12.add_v4_address('10.0.0.1', 24) i21.add_v4_address('10.0.0.2', 24) i23.add_v4_address('10.0.1.1', 24) i32.add_v4_address('10.0.1.2', 24) 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') null = file('/dev/null', 'wb') a1 = n1.Popen(['ping', '-qc1', '10.0.1.2'], stdout = null) a2 = n3.Popen(['ping', '-qc1', '10.0.0.1'], stdout = null) self.assertEquals(a1.wait(), 0) self.assertEquals(a2.wait(), 0) @test_util.skipUnless(os.getuid() == 0, "Test requires root privileges") def test_run_ping_routing(self): n1 = nemu.Node() n2 = nemu.Node() n3 = nemu.Node() i1 = n1.add_if() i2a = n2.add_if() i2b = n2.add_if() i3 = n3.add_if() i1.up = i2a.up = i2b.up = i3.up = True l1 = nemu.Switch() l2 = nemu.Switch() l1.connect(i1) l1.connect(i2a) l2.connect(i2b) l2.connect(i3) l1.up = l2.up = True 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) 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') null = file('/dev/null', 'wb') a1 = n1.Popen(['ping', '-qc1', '10.0.1.2'], stdout = null) a2 = n3.Popen(['ping', '-qc1', '10.0.0.1'], stdout = null) self.assertEquals(a1.wait(), 0) self.assertEquals(a2.wait(), 0) def _forward_packets(self, subproc, if1, if2): while(True): ready = select.select([if1.fd, if2.fd], [], [], 0.1)[0] if ready: s = os.read(ready[0], 65536) if ready[0] == if1.fd: os.write(if2.fd, s) else: os.write(if1.fd, s) if not s: break if subproc.poll() != None: break @test_util.skipUnless(os.getuid() == 0, "Test requires root privileges") def test_run_ping_tun(self): """This test simulates a point to point connection between two hosts using two tun devices.""" n1 = nemu.Node() n2 = nemu.Node() # Use PI, so that's tested too. tun1 = n1.add_tun(use_pi = True) tun2 = n2.add_tun(use_pi = True) tun1.up = tun2.up = True tun1.add_v4_address('10.0.1.1', 24) tun2.add_v4_address('10.0.1.2', 24) null = file('/dev/null', 'wb') a = n1.Popen(['ping', '-qc1', '10.0.1.2'], stdout = null) self._forward_packets(a, tun1, tun2) self.assertEquals(a.wait(), 0) @test_util.skipUnless(os.getuid() == 0, "Test requires root privileges") def test_run_ping_tap(self): """This test simulates a point to point connection between two hosts using two tap devices.""" n1 = nemu.Node() n2 = nemu.Node() tap1 = n1.add_tap() tap2 = n2.add_tap() tap1.up = tap2.up = True tap1.add_v4_address('10.0.1.1', 24) tap2.add_v4_address('10.0.1.2', 24) null = file('/dev/null', 'wb') a = n1.Popen(['ping', '-qc1', '10.0.1.2'], stdout = null) self._forward_packets(a, tap1, tap2) self.assertEquals(a.wait(), 0) @test_util.skipUnless(os.getuid() == 0, "Test requires root privileges") def test_run_ping_tap_routing(self): """This test simulates a point to point connection between two hosts using two tap devices, and normal connections with other two, to use routing.""" n1 = nemu.Node() n2 = nemu.Node() n3 = nemu.Node() n4 = nemu.Node() i1 = n1.add_if() i2 = n2.add_if() tap1 = n2.add_tap() tap2 = n3.add_tap() i3 = n3.add_if() i4 = n4.add_if() i1.up = i2.up = tap1.up = tap2.up = i3.up = i4.up = True l1 = nemu.Switch() l2 = nemu.Switch() l1.connect(i1) l1.connect(i2) l2.connect(i3) l2.connect(i4) l1.up = l2.up = True i1.add_v4_address('10.0.0.1', 24) i2.add_v4_address('10.0.0.2', 24) tap1.add_v4_address('10.0.1.1', 24) tap2.add_v4_address('10.0.1.2', 24) i3.add_v4_address('10.0.2.1', 24) i4.add_v4_address('10.0.2.2', 24) n1.add_route(prefix = '10.0.1.0', prefix_len = 24, nexthop = '10.0.0.2') n1.add_route(prefix = '10.0.2.0', prefix_len = 24, nexthop = '10.0.0.2') n2.add_route(prefix = '10.0.2.0', prefix_len = 24, nexthop = '10.0.1.2') n3.add_route(prefix = '10.0.0.0', prefix_len = 24, nexthop = '10.0.1.1') n4.add_route(prefix = '10.0.1.0', prefix_len = 24, nexthop = '10.0.2.1') n4.add_route(prefix = '10.0.0.0', prefix_len = 24, nexthop = '10.0.2.1') null = file('/dev/null', 'wb') a = n1.Popen(['ping', '-qc1', '10.0.2.2'], stdout = null) self._forward_packets(a, tap1, tap2) self.assertEquals(a.wait(), 0) class TestX11(unittest.TestCase): @test_util.skipUnless("DISPLAY" in os.environ, "Test requires working X11") @test_util.skipUnless(nemu.environ.XDPYINFO_PATH, "Test requires xdpyinfo") def test_run_xdpyinfo(self): xdpy = nemu.environ.XDPYINFO_PATH info = nemu.environ.backticks([xdpy]) # remove first line, contains the display name info = info.partition("\n")[2] n = nemu.Node(nonetns = True, forward_X11 = True) info2 = n.backticks([xdpy]) info2 = info2.partition("\n")[2] self.assertEquals(info, info2) @test_util.skipUnless(os.getuid() == 0, "Test requires root privileges") @test_util.skipUnless("DISPLAY" in os.environ, "Test requires working X11") @test_util.skipUnless(nemu.environ.XDPYINFO_PATH, "Test requires xdpyinfo") def test_run_xdpyinfo_netns(self): xdpy = nemu.environ.XDPYINFO_PATH info = nemu.environ.backticks([xdpy]) # remove first line, contains the display name info = info.partition("\n")[2] n = nemu.Node(forward_X11 = True) info2 = n.backticks([xdpy]) info2 = info2.partition("\n")[2] self.assertEquals(info, info2) if __name__ == '__main__': unittest.main() nemu-0.2/test/test_switch.py0000755000175000017500000001067412225521027017013 0ustar tinchotincho00000000000000#!/usr/bin/env python # vim:ts=4:sw=4:et:ai:sts=4 import os, unittest import nemu, test_util, nemu.environ class TestSwitch(unittest.TestCase): @test_util.skipUnless(os.getuid() == 0, "Test requires root privileges") def setUp(self): n1 = nemu.Node() n2 = nemu.Node() i1 = n1.add_if() i2 = n2.add_if() l = nemu.Switch() l.connect(i1) l.connect(i2) self.stuff = (n1, n2, i1, i2, l) @test_util.skipUnless(os.getuid() == 0, "Test requires root privileges") def test_switch_base(self): (n1, n2, i1, i2, l) = self.stuff l.mtu = 3000 ifdata = nemu.iproute.get_if_data()[0] self.assertEquals(ifdata[l.index].mtu, 3000) self.assertEquals(ifdata[i1.control.index].mtu, 3000, "MTU propagation") self.assertEquals(ifdata[i2.control.index].mtu, 3000, "MTU propagation") i1.mtu = i2.mtu = 3000 self.assertEquals(ifdata[l.index].up, False) self.assertEquals(ifdata[i1.control.index].up, False, "UP propagation") self.assertEquals(ifdata[i2.control.index].up, False, "UP propagation") l.up = True ifdata = nemu.iproute.get_if_data()[0] self.assertEquals(ifdata[i1.control.index].up, True, "UP propagation") self.assertEquals(ifdata[i2.control.index].up, True, "UP propagation") tcdata = nemu.iproute.get_tc_data()[0] self.assertEquals(tcdata[i1.control.index], {"qdiscs": {}}) self.assertEquals(tcdata[i2.control.index], {"qdiscs": {}}) @test_util.skipUnless(os.getuid() == 0, "Test requires root privileges") def test_switch_changes(self): (n1, n2, i1, i2, l) = self.stuff # Test strange rules handling os.system(("%s qd add dev %s root prio bands 3 " + "priomap 1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1") % (nemu.environ.TC_PATH, i1.control.name)) tcdata = nemu.iproute.get_tc_data()[0] self.assertEquals(tcdata[i1.control.index], "foreign") l.set_parameters(bandwidth = 13107200) # 100 mbits tcdata = nemu.iproute.get_tc_data()[0] self.assertEquals(tcdata[i1.control.index], {"bandwidth": 13107000, "qdiscs": {"tbf": "1"}}) # Test tc replacements self._test_tbf() # none => tbf self._test_both() # tbf => both self._test_netem() # both => netem self._test_tbf() # netem => tbf self._test_netem() # tbf => netem self._test_none() # netem => none self._test_netem() # none => netem self._test_both() # netem => both self._test_tbf() # both => tbf self._test_none() # tbf => none self._test_both() # none => both self._test_none() # both => none def _test_none(self): (n1, n2, i1, i2, l) = self.stuff l.set_parameters() tcdata = nemu.iproute.get_tc_data()[0] self.assertEquals(tcdata[i1.control.index], {"qdiscs": {}}) self.assertEquals(tcdata[i2.control.index], {"qdiscs": {}}) def _test_tbf(self): (n1, n2, i1, i2, l) = self.stuff l.set_parameters(bandwidth = 13107200) # 100 mbits tcdata = nemu.iproute.get_tc_data()[0] self.assertEquals(tcdata[i1.control.index], # adjust for tc rounding {"bandwidth": 13107000, "qdiscs": {"tbf": "1"}}) self.assertEquals(tcdata[i2.control.index], {"bandwidth": 13107000, "qdiscs": {"tbf": "1"}}) def _test_netem(self): (n1, n2, i1, i2, l) = self.stuff l.set_parameters(delay = 0.001) # 1ms tcdata = nemu.iproute.get_tc_data()[0] self.assertEquals(tcdata[i1.control.index], {"delay": 0.001, "qdiscs": {"netem": "2"}}) self.assertEquals(tcdata[i2.control.index], {"delay": 0.001, "qdiscs": {"netem": "2"}}) def _test_both(self): (n1, n2, i1, i2, l) = self.stuff l.set_parameters(bandwidth = 13107200, delay = 0.001) # 100 mbits, 1ms tcdata = nemu.iproute.get_tc_data()[0] self.assertEquals(tcdata[i1.control.index], {"bandwidth": 13107000, "delay": 0.001, "qdiscs": {"tbf": "1", "netem": "2"}}) self.assertEquals(tcdata[i2.control.index], {"bandwidth": 13107000, "delay": 0.001, "qdiscs": {"tbf": "1", "netem": "2"}}) if __name__ == "__main__": unittest.main() nemu-0.2/test/test_routing.py0000755000175000017500000000400412225521027017167 0ustar tinchotincho00000000000000#!/usr/bin/env python # vim:ts=4:sw=4:et:ai:sts=4 import nemu, test_util import os, unittest class TestRouting(unittest.TestCase): @test_util.skip("Programatic detection of duplicate routes not implemented") def test_base_routing(self): node = nemu.Node(nonetns = True) routes = node.get_routes() # main netns routes! if(len(routes)): self.assertRaises(RuntimeError, node.add_route, routes[0]) routes[0].metric += 1 # should be enough to make it unique self.assertRaises(RuntimeError, node.del_route, routes[0]) @test_util.skipUnless(os.getuid() == 0, "Test requires root privileges") def test_routing(self): node = nemu.Node() self.assertEquals(len(node.get_routes()), 0) if0 = node.add_if() if0.add_v4_address('10.0.0.1', 24) if0.up = True routes = node.get_routes() self.assertEquals(routes, [node.route(prefix = '10.0.0.0', prefix_len = 24, interface = if0)]) node.add_route(nexthop = '10.0.0.2') # default route node.add_route(prefix = '10.1.0.0', prefix_len = 16, nexthop = '10.0.0.3') node.add_route(prefix = '11.1.0.1', prefix_len = 32, interface = if0) routes = node.get_routes() self.assertTrue(node.route(nexthop = '10.0.0.2', interface = if0) in routes) self.assertTrue(node.route(prefix = '10.1.0.0', prefix_len = 16, nexthop = '10.0.0.3', interface = if0) in routes) self.assertTrue(node.route(prefix = '11.1.0.1', prefix_len = 32, interface = if0) in routes) node.del_route(nexthop = '10.0.0.2') # default route node.del_route(prefix = '10.1.0.0', prefix_len = 16, nexthop = '10.0.0.3') node.del_route(prefix = '11.1.0.1', prefix_len = 32, interface = if0) node.del_route(prefix = '10.0.0.0', prefix_len = 24, interface = if0) self.assertEquals(node.get_routes(), []) if __name__ == '__main__': unittest.main() nemu-0.2/test/test_subprocess.py0000755000175000017500000002712412225521027017700 0ustar tinchotincho00000000000000#!/usr/bin/env python # vim:ts=4:sw=4:et:ai:sts=4 import nemu, test_util import nemu.subprocess_ as sp import grp, os, pwd, signal, socket, sys, time, unittest def _stat(path): try: return os.stat(user) except: return None def _getpwnam(user): try: return pwd.getpwnam(user) except: return None def _getpwuid(uid): try: return pwd.getpwuid(uid) except: return None def _readall(fd): s = "" while True: try: s1 = os.read(fd, 4096) except OSError, e: if e.errno == errno.EINTR: continue else: raise if s1 == "": break s += s1 return s _longstring = "Long string is long!\n" * 1000 class TestSubprocess(unittest.TestCase): def _check_ownership(self, user, pid): uid = pwd.getpwnam(user)[2] gid = pwd.getpwnam(user)[3] groups = [x[2] for x in grp.getgrall() if user in x[3]] stat = open("/proc/%d/status" % pid) while True: data = stat.readline() fields = data.split() if fields[0] == 'Uid:': self.assertEquals(fields[1:4], (uid,) * 4) if fields[0] == 'Gid:': self.assertEquals(fields[1:4], (gid,) * 4) if fields[0] == 'Groups:': self.assertEquals(set(fields[1:]), set(groups)) break stat.close() def setUp(self): self.nouid = 65535 while _getpwuid(self.nouid): self.nouid -= 1 self.nouser = 'foobar' while _getpwnam(self.nouser): self.nouser += '_' self.nofile = '/foobar' while _stat(self.nofile): self.nofile += '_' @test_util.skipUnless(os.getuid() == 0, "Test requires root privileges") def test_spawn_chuser(self): user = 'nobody' pid = sp.spawn('/bin/sleep', ['/bin/sleep', '100'], user = user) self._check_ownership(user, pid) os.kill(pid, signal.SIGTERM) self.assertEquals(sp.wait(pid), signal.SIGTERM) @test_util.skipUnless(os.getuid() == 0, "Test requires root privileges") def test_Subprocess_chuser(self): node = nemu.Node(nonetns = True) user = 'nobody' p = node.Subprocess(['/bin/sleep', '1000'], user = user) self._check_ownership(user, p.pid) p.signal() self.assertEquals(p.wait(), -signal.SIGTERM) def test_spawn_basic(self): # User does not exist self.assertRaises(ValueError, sp.spawn, '/bin/sleep', ['/bin/sleep', '1000'], user = self.nouser) self.assertRaises(ValueError, sp.spawn, '/bin/sleep', ['/bin/sleep', '1000'], user = self.nouid) # Invalid CWD: it is a file self.assertRaises(OSError, sp.spawn, '/bin/sleep', cwd = '/bin/sleep') # Invalid CWD: does not exist self.assertRaises(OSError, sp.spawn, '/bin/sleep', cwd = self.nofile) # Exec failure self.assertRaises(OSError, sp.spawn, self.nofile) # Test that the environment is cleared: sleep should not be found # XXX: This should be a python bug: if I don't set PATH explicitly, it # uses a default search path self.assertRaises(OSError, sp.spawn, 'sleep', env = {'PATH': ''}) r, w = os.pipe() p = sp.spawn('/bin/echo', ['echo', 'hello world'], stdout = w) os.close(w) self.assertEquals(_readall(r), "hello world\n") os.close(r) # Check poll. while True: ret = sp.poll(p) if ret is not None: self.assertEquals(ret, 0) break time.sleep(0.2) # Wait a little bit. # It cannot be wait()ed again. self.assertRaises(OSError, sp.wait, p) r0, w0 = os.pipe() r1, w1 = os.pipe() p = sp.spawn('/bin/cat', stdout = w0, stdin = r1, close_fds = [r0, w1]) os.close(w0) os.close(r1) self.assertEquals(sp.poll(p), None) os.write(w1, "hello world\n") os.close(w1) self.assertEquals(_readall(r0), "hello world\n") os.close(r0) self.assertEquals(sp.wait(p), 0) def test_Subprocess_basic(self): node = nemu.Node(nonetns = True) # User does not exist self.assertRaises(ValueError, node.Subprocess, ['/bin/sleep', '1000'], user = self.nouser) self.assertRaises(ValueError, node.Subprocess, ['/bin/sleep', '1000'], user = self.nouid) # Invalid CWD: it is a file self.assertRaises(OSError, node.Subprocess, '/bin/sleep', cwd = '/bin/sleep') # Invalid CWD: does not exist self.assertRaises(OSError, node.Subprocess, '/bin/sleep', cwd = self.nofile) # Exec failure self.assertRaises(OSError, node.Subprocess, self.nofile) # Test that the environment is cleared: sleep should not be found self.assertRaises(OSError, node.Subprocess, 'sleep', env = {'PATH': ''}) # Argv self.assertRaises(OSError, node.Subprocess, 'true; false') self.assertEquals(node.Subprocess('true').wait(), 0) self.assertEquals(node.Subprocess('true; false', shell = True).wait(), 1) # Piping r, w = os.pipe() p = node.Subprocess(['echo', 'hello world'], stdout = w) os.close(w) self.assertEquals(_readall(r), "hello world\n") os.close(r) p.wait() # cwd r, w = os.pipe() p = node.Subprocess('/bin/pwd', stdout = w, cwd = "/") os.close(w) self.assertEquals(_readall(r), "/\n") os.close(r) p.wait() p = node.Subprocess(['sleep', '100']) self.assertTrue(p.pid > 0) self.assertEquals(p.poll(), None) # not finished p.signal() p.signal() # verify no-op (otherwise there will be an exception) self.assertEquals(p.wait(), -signal.SIGTERM) self.assertEquals(p.wait(), -signal.SIGTERM) # no-op self.assertEquals(p.poll(), -signal.SIGTERM) # no-op # destroy p = node.Subprocess(['sleep', '100']) pid = p.pid os.kill(pid, 0) # verify process still there p.destroy() self.assertRaises(OSError, os.kill, pid, 0) # should be dead by now # forceful destroy # Command: ignore SIGTERM, write \n to synchronise and then sleep while # closing stdout (so _readall finishes) cmd = 'trap "" TERM; echo; exec sleep 100 > /dev/null' r, w = os.pipe() p = node.Subprocess(cmd, shell = True, stdout = w) os.close(w) self.assertEquals(_readall(r), "\n") # wait for trap to be installed os.close(r) pid = p.pid os.kill(pid, 0) # verify process still there # Avoid the warning about the process being killed orig_stderr = sys.stderr sys.stderr = open("/dev/null", "w") p.destroy() sys.stderr = orig_stderr self.assertRaises(OSError, os.kill, pid, 0) # should be dead by now p = node.Subprocess(['sleep', '100']) os.kill(p.pid, signal.SIGTERM) time.sleep(0.2) p.signal() # since it has not been waited for, it should not raise self.assertEquals(p.wait(), -signal.SIGTERM) def test_Popen(self): node = nemu.Node(nonetns = True) # repeat test with Popen interface r0, w0 = os.pipe() r1, w1 = os.pipe() p = node.Popen('cat', stdout = w0, stdin = r1) os.close(w0) os.close(r1) os.write(w1, "hello world\n") os.close(w1) self.assertEquals(_readall(r0), "hello world\n") os.close(r0) # now with a socketpair, not using integers (s0, s1) = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM, 0) p = node.Popen('cat', stdout = s0, stdin = s0) s0.close() s1.send("hello world\n") self.assertEquals(s1.recv(512), "hello world\n") s1.close() # pipes p = node.Popen('cat', stdin = sp.PIPE, stdout = sp.PIPE) p.stdin.write("hello world\n") p.stdin.close() self.assertEquals(p.stdout.readlines(), ["hello world\n"]) self.assertEquals(p.stderr, None) self.assertEquals(p.wait(), 0) p = node.Popen('cat', stdin = sp.PIPE, stdout = sp.PIPE) self.assertEquals(p.communicate(_longstring), (_longstring, None)) p = node.Popen('cat', stdin = sp.PIPE, stdout = sp.PIPE) p.stdin.write(_longstring) self.assertEquals(p.communicate(), (_longstring, None)) p = node.Popen('cat', stdin = sp.PIPE) self.assertEquals(p.communicate(), (None, None)) p = node.Popen('cat >&2', shell = True, stdin = sp.PIPE, stderr = sp.PIPE) p.stdin.write("hello world\n") p.stdin.close() self.assertEquals(p.stderr.readlines(), ["hello world\n"]) self.assertEquals(p.stdout, None) self.assertEquals(p.wait(), 0) p = node.Popen(['sh', '-c', 'cat >&2'], stdin = sp.PIPE, stderr = sp.PIPE) self.assertEquals(p.communicate(_longstring), (None, _longstring)) # p = node.Popen(['sh', '-c', 'cat >&2'], stdin = sp.PIPE, stdout = sp.PIPE, stderr = sp.STDOUT) p.stdin.write("hello world\n") p.stdin.close() self.assertEquals(p.stdout.readlines(), ["hello world\n"]) self.assertEquals(p.stderr, None) self.assertEquals(p.wait(), 0) p = node.Popen(['sh', '-c', 'cat >&2'], stdin = sp.PIPE, stdout = sp.PIPE, stderr = sp.STDOUT) self.assertEquals(p.communicate(_longstring), (_longstring, None)) # p = node.Popen(['tee', '/dev/stderr'], stdin = sp.PIPE, stdout = sp.PIPE, stderr = sp.STDOUT) p.stdin.write("hello world\n") p.stdin.close() self.assertEquals(p.stdout.readlines(), ["hello world\n"] * 2) self.assertEquals(p.stderr, None) self.assertEquals(p.wait(), 0) p = node.Popen(['tee', '/dev/stderr'], stdin = sp.PIPE, stdout = sp.PIPE, stderr = sp.STDOUT) self.assertEquals(p.communicate(_longstring[0:512]), (_longstring[0:512] * 2, None)) # p = node.Popen(['tee', '/dev/stderr'], stdin = sp.PIPE, stdout = sp.PIPE, stderr = sp.PIPE) p.stdin.write("hello world\n") p.stdin.close() self.assertEquals(p.stdout.readlines(), ["hello world\n"]) self.assertEquals(p.stderr.readlines(), ["hello world\n"]) self.assertEquals(p.wait(), 0) p = node.Popen(['tee', '/dev/stderr'], stdin = sp.PIPE, stdout = sp.PIPE, stderr = sp.PIPE) self.assertEquals(p.communicate(_longstring), (_longstring, ) * 2) def test_backticks(self): node = nemu.Node(nonetns = True) self.assertEquals(node.backticks("echo hello world"), "hello world\n") self.assertEquals(node.backticks(r"echo hello\ \ world"), "hello world\n") self.assertEquals(node.backticks(["echo", "hello", "world"]), "hello world\n") self.assertEquals(node.backticks("echo hello world > /dev/null"), "") self.assertEquals(node.backticks_raise("true"), "") self.assertRaises(RuntimeError, node.backticks_raise, "false") self.assertRaises(RuntimeError, node.backticks_raise, "kill $$") def test_system(self): node = nemu.Node(nonetns = True) self.assertEquals(node.system("true"), 0) self.assertEquals(node.system("false"), 1) if __name__ == '__main__': unittest.main() nemu-0.2/test/test_protocol.py0000755000175000017500000001242012225521027017342 0ustar tinchotincho00000000000000#!/usr/bin/env python # vim:ts=4:sw=4:et:ai:sts=4 import nemu.protocol import os, socket, sys, threading, unittest class TestServer(unittest.TestCase): def test_server_startup(self): # Test the creation of the server object with different ways of passing # the file descriptor; and check the banner. (s0, s1) = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM, 0) (s2, s3) = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM, 0) def test_help(fd): fd.write("HELP\n") # should be more than one line self.assertEquals(fd.readline()[0:4], "200-") while True: l = fd.readline() self.assertEquals(l[0:3], "200") if l[3] == ' ': break def run_server(): srv = nemu.protocol.Server(s0, s0) srv.run() srv = nemu.protocol.Server(s2.fileno(), s2.fileno()) srv.run() t = threading.Thread(target = run_server) t.start() s = os.fdopen(s1.fileno(), "r+", 1) self.assertEquals(s.readline()[0:4], "220 ") test_help(s) s.close() s0.close() s = os.fdopen(s3.fileno(), "r+", 1) self.assertEquals(s.readline()[0:4], "220 ") test_help(s) s.close() s2.close() t.join() def test_server_clean(self): (s0, s1) = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM, 0) def run_server(): nemu.protocol.Server(s0, s0).run() t = threading.Thread(target = run_server) t.start() cli = nemu.protocol.Client(s1, s1) null = file('/dev/null', 'wb') argv = [ '/bin/sh', '-c', 'yes' ] pid = cli.spawn(argv, stdout = null) self.assertTrue(os.path.exists("/proc/%d" % pid)) # try to exit while there are still processes running cli.shutdown() t.join() # Check that the process was killed. # We are asumming that the pid is not going to be reused fast enough # to generate a false possitive. self.assertFalse(os.path.exists("/proc/%d" % pid)) def test_spawn_recovery(self): (s0, s1) = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM, 0) def run_server(): nemu.protocol.Server(s0, s0).run() t = threading.Thread(target = run_server) t.start() cli = nemu.protocol.Client(s1, s1) # make PROC SIN fail self.assertRaises(OSError, cli.spawn, "/bin/true", stdin = -1) # check if the protocol is in a sane state: # PROC CWD should not be valid cli._send_cmd("PROC", "CWD", "/") self.assertRaises(RuntimeError, cli._read_and_check_reply) # Force a KeyError, and check that the exception is received correctly cli._send_cmd("IF", "LIST", "-1") self.assertRaises(KeyError, cli._read_and_check_reply) cli.shutdown() t.join() def test_basic_stuff(self): (s0, s1) = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM, 0) srv = nemu.protocol.Server(s0, s0) s1 = s1.makefile("r+", 1) def check_error(self, cmd, code = 500): s1.write("%s\n" % cmd) self.assertEquals(srv.readcmd(), None) self.assertEquals(s1.readline()[0:4], "%d " % code) def check_ok(self, cmd, func, args): s1.write("%s\n" % cmd) ccmd = " ".join(cmd.upper().split()[0:2]) if func == None: self.assertEquals(srv.readcmd()[1:3], (ccmd, args)) else: self.assertEquals(srv.readcmd(), (func, ccmd, args)) check_ok(self, "quit", srv.do_QUIT, []) check_ok(self, " quit ", srv.do_QUIT, []) # protocol error check_error(self, "quit 1") # Not allowed in normal mode check_error(self, "proc user") check_error(self, "proc sin") check_error(self, "proc sout") check_error(self, "proc serr") check_error(self, "proc cwd") check_error(self, "proc env") check_error(self, "proc abrt") check_error(self, "proc run") check_ok(self, "if list", srv.do_IF_LIST, []) check_ok(self, "if list 1", srv.do_IF_LIST, [1]) check_error(self, "proc poll") # missing arg check_error(self, "proc poll 1 2") # too many args check_error(self, "proc poll a") # invalid type check_error(self, "proc") # incomplete command check_error(self, "proc foo") # unknown subcommand check_error(self, "foo bar") # unknown check_ok(self, "proc crte /bin/sh", srv.do_PROC_CRTE, ['/bin/sh']) # Commands that would fail, but the parsing is correct check_ok(self, "proc poll 0", None, [0]) check_ok(self, "proc wait 0", None, [0]) check_ok(self, "proc kill 0", None, [0]) check_ok(self, "proc crte =", srv.do_PROC_CRTE, [""]) # empty b64 check_error(self, "proc crte =a") # invalid b64 # simulate proc mode srv._commands = nemu.protocol._proc_commands check_error(self, "proc crte foo") check_error(self, "proc poll 0") check_error(self, "proc wait 0") check_error(self, "proc kill 0") if __name__ == '__main__': unittest.main() nemu-0.2/test/test_util.py0000644000175000017500000000544412225521027016463 0ustar tinchotincho00000000000000#!/usr/bin/env python # vim:ts=4:sw=4:et:ai:sts=4 import os, re, subprocess, sys import nemu.subprocess_ from nemu.environ import * def process_ipcmd(str): cur = None out = {} for line in str.split("\n"): if line == "": cur = None continue match = re.search(r'^(\d+): (\S+): <(\S+)> mtu (\d+) qdisc (\S+)', line) if match != None: cur = match.group(2) out[cur] = { 'idx': int(match.group(1)), 'flags': match.group(3).split(","), 'mtu': int(match.group(4)), 'qdisc': match.group(5), 'addr': [] } out[cur]['up'] = 'UP' in out[cur]['flags'] continue # Assume cur is defined assert cur != None match = re.search(r'^\s+link/\S*(?: ([0-9a-f:]+))?(?: |$)', line) if match != None: out[cur]['lladdr'] = match.group(1) continue match = re.search(r'^\s+inet ([0-9.]+)/(\d+)(?: brd ([0-9.]+))?', line) if match != None: out[cur]['addr'].append({ 'address': match.group(1), 'prefix_len': int(match.group(2)), 'broadcast': match.group(3), 'family': 'inet'}) continue match = re.search(r'^\s+inet6 ([0-9a-f:]+)/(\d+)(?: |$)', line) if match != None: out[cur]['addr'].append({ 'address': match.group(1), 'prefix_len': int(match.group(2)), 'family': 'inet6'}) continue match = re.search(r'^\s{4}', line) assert match != None return out def get_devs(): outdata = backticks([IP_PATH, "addr", "list"]) return process_ipcmd(outdata) def get_devs_netns(node): out = nemu.subprocess_.backticks_raise(node, [IP_PATH, "addr", "list"]) return process_ipcmd(out) def make_linux_ver(string): match = re.match(r'(\d+)\.(\d+)(?:\.(\d+))?(.*)', string) version, patchlevel, sublevel, extraversion = match.groups() if not sublevel: sublevel = 0 return (int(version) << 16) + (int(patchlevel) << 8) + int(sublevel) def get_linux_ver(): return make_linux_ver(os.uname()[2]) # Unittest from Python 2.6 doesn't have these decorators def _bannerwrap(f, text): name = f.__name__ def banner(*args, **kwargs): sys.stderr.write("*** WARNING: Skipping test %s: `%s'\n" % (name, text)) return None return banner def skip(text): return lambda f: _bannerwrap(f, text) def skipUnless(cond, text): return (lambda f: _bannerwrap(f, text)) if not cond else lambda f: f def skipIf(cond, text): return (lambda f: _bannerwrap(f, text)) if cond else lambda f: f nemu-0.2/test/test_node.py0000755000175000017500000000457712225521027016444 0ustar tinchotincho00000000000000#!/usr/bin/env python # vim:ts=4:sw=4:et:ai:sts=4 import nemu, nemu.environ, test_util import os, signal, subprocess, sys, time import unittest class TestNode(unittest.TestCase): @test_util.skipUnless(os.getuid() == 0, "Test requires root privileges") def test_node(self): node = nemu.Node() self.failIfEqual(node.pid, os.getpid()) self.failIfEqual(node.pid, None) # check if it really exists os.kill(node.pid, 0) nodes = nemu.get_nodes() self.assertEquals(nodes, [node]) self.assertTrue(node.get_interface("lo").up) @test_util.skip("Not implemented") def test_detect_fork(self): # Test that nemu recognises a fork chld = os.fork() if chld == 0: if len(nemu.get_nodes()) == 0: os._exit(0) os._exit(1) (pid, exitcode) = os.waitpid(chld, 0) self.assertEquals(exitcode, 0, "Node does not recognise forks") @test_util.skipUnless(os.getuid() == 0, "Test requires root privileges") def test_cleanup(self): def create_stuff(): a = nemu.Node() b = nemu.Node() ifa = a.add_if() ifb = b.add_if() switch = nemu.Switch() switch.connect(ifa) switch.connect(ifb) # Test automatic destruction orig_devs = len(test_util.get_devs()) create_stuff() self.assertEquals(nemu.get_nodes(), []) self.assertEquals(orig_devs, len(test_util.get_devs())) # Test at_exit hooks orig_devs = len(test_util.get_devs()) chld = os.fork() if chld == 0: # TODO: not implemented. #nemu.set_cleanup_hooks(on_exit = True, on_signals = []) create_stuff() os._exit(0) os.waitpid(chld, 0) self.assertEquals(orig_devs, len(test_util.get_devs())) # Test signal hooks orig_devs = len(test_util.get_devs()) chld = os.fork() if chld == 0: # TODO: not implemented. #nemu.set_cleanup_hooks(on_exit = False, # on_signals = [signal.SIGTERM]) create_stuff() while True: time.sleep(10) os.kill(chld, signal.SIGTERM) os.waitpid(chld, 0) self.assertEquals(orig_devs, len(test_util.get_devs())) if __name__ == '__main__': unittest.main()