diff options
Diffstat (limited to 'node-admin/scripts/pyroute2/netns')
-rw-r--r-- | node-admin/scripts/pyroute2/netns/__init__.py | 123 | ||||
-rw-r--r-- | node-admin/scripts/pyroute2/netns/nslink.py | 310 | ||||
-rw-r--r-- | node-admin/scripts/pyroute2/netns/process/__init__.py | 39 | ||||
-rw-r--r-- | node-admin/scripts/pyroute2/netns/process/base_p2.py | 7 | ||||
-rw-r--r-- | node-admin/scripts/pyroute2/netns/process/base_p3.py | 7 | ||||
-rw-r--r-- | node-admin/scripts/pyroute2/netns/process/proxy.py | 163 |
6 files changed, 649 insertions, 0 deletions
diff --git a/node-admin/scripts/pyroute2/netns/__init__.py b/node-admin/scripts/pyroute2/netns/__init__.py new file mode 100644 index 00000000000..696ff3a14a6 --- /dev/null +++ b/node-admin/scripts/pyroute2/netns/__init__.py @@ -0,0 +1,123 @@ +# By Peter V. Saveliev https://pypi.python.org/pypi/pyroute2. Dual licensed under the Apache 2 and GPLv2+ see https://github.com/svinota/pyroute2 for License details. +''' +Network namespaces management +============================= + +Pyroute2 provides basic namespaces management support. The +`netns` module contains several tools for that. + +Please be aware, that in order to run system calls the +library uses `ctypes` module. It can fail on platforms +where SELinux is enforced. If the Python interpreter, +loading this module, dumps the core, one can check the +SELinux state with `getenforce` command. + +''' + +import os +import errno +import ctypes +import sys + +if sys.maxsize > 2**32: + __NR_setns = 308 +else: + __NR_setns = 346 + +CLONE_NEWNET = 0x40000000 +MNT_DETACH = 0x00000002 +MS_BIND = 4096 +MS_REC = 16384 +MS_SHARED = 1 << 20 +NETNS_RUN_DIR = '/var/run/netns' + + +def listnetns(): + ''' + List available network namespaces. + ''' + try: + return os.listdir(NETNS_RUN_DIR) + except OSError as e: + if e.errno == errno.ENOENT: + return [] + else: + raise + + +def create(netns, libc=None): + ''' + Create a network namespace. + ''' + libc = libc or ctypes.CDLL('libc.so.6', use_errno=True) + # FIXME validate and prepare NETNS_RUN_DIR + + netnspath = '%s/%s' % (NETNS_RUN_DIR, netns) + netnspath = netnspath.encode('ascii') + netnsdir = NETNS_RUN_DIR.encode('ascii') + + # init netnsdir + try: + os.mkdir(netnsdir) + except OSError as e: + if e.errno != errno.EEXIST: + raise + + # this code is ported from iproute2 + done = False + while libc.mount(b'', netnsdir, b'none', MS_SHARED | MS_REC, None) != 0: + if done: + raise OSError(ctypes.get_errno(), 'share rundir failed', netns) + if libc.mount(netnsdir, netnsdir, b'none', MS_BIND, None) != 0: + raise OSError(ctypes.get_errno(), 'mount rundir failed', netns) + done = True + + # create mountpoint + os.close(os.open(netnspath, os.O_RDONLY | os.O_CREAT | os.O_EXCL, 0)) + + # unshare + if libc.unshare(CLONE_NEWNET) < 0: + raise OSError(ctypes.get_errno(), 'unshare failed', netns) + + # bind the namespace + if libc.mount(b'/proc/self/ns/net', netnspath, b'none', MS_BIND, None) < 0: + raise OSError(ctypes.get_errno(), 'mount failed', netns) + + +def remove(netns, libc=None): + ''' + Remove a network namespace. + ''' + libc = libc or ctypes.CDLL('libc.so.6', use_errno=True) + netnspath = '%s/%s' % (NETNS_RUN_DIR, netns) + netnspath = netnspath.encode('ascii') + libc.umount2(netnspath, MNT_DETACH) + os.unlink(netnspath) + + +def setns(netns, flags=os.O_CREAT, libc=None): + ''' + Set netns for the current process. + + The flags semantics is the same as for the `open(2)` + call: + + - O_CREAT -- create netns, if doesn't exist + - O_CREAT | O_EXCL -- create only if doesn't exist + ''' + libc = libc or ctypes.CDLL('libc.so.6', use_errno=True) + netnspath = '%s/%s' % (NETNS_RUN_DIR, netns) + netnspath = netnspath.encode('ascii') + + if netns in listnetns(): + if flags & (os.O_CREAT | os.O_EXCL) == (os.O_CREAT | os.O_EXCL): + raise OSError(errno.EEXIST, 'netns exists', netns) + else: + if flags & os.O_CREAT: + create(netns, libc=libc) + + nsfd = os.open(netnspath, os.O_RDONLY) + ret = libc.syscall(__NR_setns, nsfd, CLONE_NEWNET) + if ret != 0: + raise OSError(ctypes.get_errno(), 'failed to open netns', netns) + return nsfd diff --git a/node-admin/scripts/pyroute2/netns/nslink.py b/node-admin/scripts/pyroute2/netns/nslink.py new file mode 100644 index 00000000000..67d5fff0921 --- /dev/null +++ b/node-admin/scripts/pyroute2/netns/nslink.py @@ -0,0 +1,310 @@ +# By Peter V. Saveliev https://pypi.python.org/pypi/pyroute2. Dual licensed under the Apache 2 and GPLv2+ see https://github.com/svinota/pyroute2 for License details. +''' +NetNS +===== + +A NetNS object is IPRoute-like. It runs in the main network +namespace, but also creates a proxy process running in +the required netns. All the netlink requests are done via +that proxy process. + +NetNS supports standard IPRoute API, so can be used instead +of IPRoute, e.g., in IPDB:: + + # start the main network settings database: + ipdb_main = IPDB() + # start the same for a netns: + ipdb_test = IPDB(nl=NetNS('test')) + + # create VETH + ipdb_main.create(ifname='v0p0', kind='veth', peer='v0p1').commit() + + # move peer VETH into the netns + with ipdb_main.interfaces.v0p1 as veth: + veth.net_ns_fd = 'test' + + # please keep in mind, that netns move clears all the settings + # on a VETH interface pair, so one should run netns assignment + # as a separate operation only + + # assign addresses + # please notice, that `v0p1` is already in the `test` netns, + # so should be accessed via `ipdb_test` + with ipdb_main.interfaces.v0p0 as veth: + veth.add_ip('172.16.200.1/24') + veth.up() + with ipdb_test.interfaces.v0p1 as veth: + veth.add_ip('172.16.200.2/24') + veth.up() + +Please review also the test code, under `tests/test_netns.py` for +more examples. + +By default, NetNS creates requested netns, if it doesn't exist, +or uses existing one. To control this behaviour, one can use flags +as for `open(2)` system call:: + + # create a new netns or fail, if it already exists + netns = NetNS('test', flags=os.O_CREAT | os.O_EXIST) + + # create a new netns or use existing one + netns = NetNS('test', flags=os.O_CREAT) + + # the same as above, the default behaviour + netns = NetNS('test') + +To remove a network namespace:: + + from pyroute2 import NetNS + netns = NetNS('test') + netns.close() + netns.remove() + +One should stop it first with `close()`, and only after that +run `remove()`. + +''' + +import os +import errno +import atexit +import select +import struct +import threading +import traceback +from socket import SOL_SOCKET +from socket import SO_RCVBUF +from pyroute2.config import MpPipe +from pyroute2.config import MpProcess +from pyroute2.iproute import IPRoute +from pyroute2.netlink.nlsocket import NetlinkMixin +from pyroute2.netlink.rtnl.iprsocket import MarshalRtnl +from pyroute2.iproute import IPRouteMixin +from pyroute2.netns import setns +from pyroute2.netns import remove + + +def NetNServer(netns, rcvch, cmdch, flags=os.O_CREAT): + ''' + The netns server supposed to be started automatically by NetNS. + It has two communication channels: one simplex to forward incoming + netlink packets, `rcvch`, and other synchronous duplex to get + commands and send back responses, `cmdch`. + + Channels should support standard socket API, should be compatible + with poll/select and should be able to transparently pickle objects. + NetNS uses `multiprocessing.Pipe` for this purpose, but it can be + any other implementation with compatible API. + + The first parameter, `netns`, is a netns name. Depending on the + `flags`, the netns can be created automatically. The `flags` semantics + is exactly the same as for `open(2)` system call. + + ... + + The server workflow is simple. The startup sequence:: + + 1. Create or open a netns. + + 2. Start `IPRoute` instance. It will be used only on the low level, + the `IPRoute` will not parse any packet. + + 3. Start poll/select loop on `cmdch` and `IPRoute`. + + On the startup, the server sends via `cmdch` the status packet. It can be + `None` if all is OK, or some exception. + + Further data handling, depending on the channel, server side:: + + 1. `IPRoute`: read an incoming netlink packet and send it unmodified + to the peer via `rcvch`. The peer, polling `rcvch`, can handle + the packet on its side. + + 2. `cmdch`: read tuple (cmd, argv, kwarg). If the `cmd` starts with + "send", then take `argv[0]` as a packet buffer, treat it as one + netlink packet and substitute PID field (offset 12, uint32) with + its own. Strictly speaking, it is not mandatory for modern netlink + implementations, but it is required by the protocol standard. + + ''' + try: + nsfd = setns(netns, flags) + except OSError as e: + cmdch.send(e) + return e.errno + except Exception as e: + cmdch.send(OSError(errno.ECOMM, str(e), netns)) + return 255 + + # + try: + ipr = IPRoute() + rcvch_lock = ipr._sproxy.lock + ipr._s_channel = rcvch + poll = select.poll() + poll.register(ipr, select.POLLIN | select.POLLPRI) + poll.register(cmdch, select.POLLIN | select.POLLPRI) + except Exception as e: + cmdch.send(e) + return 255 + + # all is OK so far + cmdch.send(None) + # 8<------------------------------------------------------------- + while True: + events = poll.poll() + for (fd, event) in events: + if fd == ipr.fileno(): + bufsize = ipr.getsockopt(SOL_SOCKET, SO_RCVBUF) // 2 + with rcvch_lock: + rcvch.send(ipr.recv(bufsize)) + elif fd == cmdch.fileno(): + try: + cmdline = cmdch.recv() + if cmdline is None: + poll.unregister(ipr) + poll.unregister(cmdch) + ipr.close() + os.close(nsfd) + return + (cmd, argv, kwarg) = cmdline + if cmd[:4] == 'send': + # Achtung + # + # It's a hack, but we just have to do it: one + # must use actual pid in netlink messages + # + # FIXME: there can be several messages in one + # call buffer; but right now we can ignore it + msg = argv[0][:12] + msg += struct.pack("I", os.getpid()) + msg += argv[0][16:] + argv = list(argv) + argv[0] = msg + cmdch.send(getattr(ipr, cmd)(*argv, **kwarg)) + except Exception as e: + e.tb = traceback.format_exc() + cmdch.send(e) + + +class NetNSProxy(object): + + netns = 'default' + flags = os.O_CREAT + + def __init__(self, *argv, **kwarg): + self.cmdlock = threading.Lock() + self.rcvch, rcvch = MpPipe() + self.cmdch, cmdch = MpPipe() + self.server = MpProcess(target=NetNServer, + args=(self.netns, rcvch, cmdch, self.flags)) + self.server.start() + error = self.cmdch.recv() + if error is not None: + self.server.join() + raise error + else: + atexit.register(self.close) + + def recv(self, bufsize, flags=0): + return self.rcvch.recv() + + def close(self): + self.cmdch.send(None) + self.server.join() + + def proxy(self, cmd, *argv, **kwarg): + with self.cmdlock: + self.cmdch.send((cmd, argv, kwarg)) + response = self.cmdch.recv() + if isinstance(response, Exception): + raise response + return response + + def fileno(self): + return self.rcvch.fileno() + + def bind(self, *argv, **kwarg): + if 'async' in kwarg: + kwarg['async'] = False + return self.proxy('bind', *argv, **kwarg) + + def send(self, *argv, **kwarg): + return self.proxy('send', *argv, **kwarg) + + def sendto(self, *argv, **kwarg): + return self.proxy('sendto', *argv, **kwarg) + + def getsockopt(self, *argv, **kwarg): + return self.proxy('getsockopt', *argv, **kwarg) + + def setsockopt(self, *argv, **kwarg): + return self.proxy('setsockopt', *argv, **kwarg) + + +class NetNSocket(NetlinkMixin, NetNSProxy): + + def bind(self, *argv, **kwarg): + return NetNSProxy.bind(self, *argv, **kwarg) + + def close(self): + NetNSProxy.close(self) + + def _sendto(self, *argv, **kwarg): + return NetNSProxy.sendto(self, *argv, **kwarg) + + def _recv(self, *argv, **kwarg): + return NetNSProxy.recv(self, *argv, **kwarg) + + +class NetNS(IPRouteMixin, NetNSocket): + ''' + NetNS is the IPRoute API with network namespace support. + + **Why not IPRoute?** + + The task to run netlink commands in some network namespace, being in + another network namespace, requires the architecture, that differs + too much from a simple Netlink socket. + + NetNS starts a proxy process in a network namespace and uses + `multiprocessing` communication channels between the main and the proxy + processes to route all `recv()` and `sendto()` requests/responses. + + **Any specific API calls?** + + Nope. `NetNS` supports all the same, that `IPRoute` does, in the same + way. It provides full `socket`-compatible API and can be used in + poll/select as well. + + The only difference is the `close()` call. In the case of `NetNS` it + is **mandatory** to close the socket before exit. + + **NetNS and IPDB** + + It is possible to run IPDB with NetNS:: + + from pyroute2 import NetNS + from pyroute2 import IPDB + + ip = IPDB(nl=NetNS('somenetns')) + ... + ip.release() + + Do not forget to call `release()` when the work is done. It will shut + down `NetNS` instance as well. + ''' + def __init__(self, netns, flags=os.O_CREAT): + self.netns = netns + self.flags = flags + super(NetNS, self).__init__() + self.marshal = MarshalRtnl() + + def post_init(self): + pass + + def remove(self): + ''' + Try to remove this network namespace from the system. + ''' + remove(self.netns) diff --git a/node-admin/scripts/pyroute2/netns/process/__init__.py b/node-admin/scripts/pyroute2/netns/process/__init__.py new file mode 100644 index 00000000000..a211a3993f6 --- /dev/null +++ b/node-admin/scripts/pyroute2/netns/process/__init__.py @@ -0,0 +1,39 @@ +# By Peter V. Saveliev https://pypi.python.org/pypi/pyroute2. Dual licensed under the Apache 2 and GPLv2+ see https://github.com/svinota/pyroute2 for License details. +import types +import subprocess + + +class MetaPopen(type): + ''' + API definition for NSPopen. + + All this stuff is required to make `help()` function happy. + ''' + def __init__(cls, *argv, **kwarg): + super(MetaPopen, cls).__init__(*argv, **kwarg) + # copy docstrings and create proxy slots + cls.api = {} + for attr_name in dir(subprocess.Popen): + attr = getattr(subprocess.Popen, attr_name) + cls.api[attr_name] = {} + cls.api[attr_name]['callable'] = \ + isinstance(attr, (types.MethodType, types.FunctionType)) + cls.api[attr_name]['doc'] = attr.__doc__ \ + if hasattr(attr, '__doc__') else None + + def __dir__(cls): + return list(cls.api.keys()) + ['release'] + + def __getattribute__(cls, key): + try: + return type.__getattribute__(cls, key) + except AttributeError: + attr = getattr(subprocess.Popen, key) + if isinstance(attr, (types.MethodType, types.FunctionType)): + def proxy(*argv, **kwarg): + return attr(*argv, **kwarg) + proxy.__doc__ = attr.__doc__ + proxy.__objclass__ = cls + return proxy + else: + return attr diff --git a/node-admin/scripts/pyroute2/netns/process/base_p2.py b/node-admin/scripts/pyroute2/netns/process/base_p2.py new file mode 100644 index 00000000000..402329846c0 --- /dev/null +++ b/node-admin/scripts/pyroute2/netns/process/base_p2.py @@ -0,0 +1,7 @@ +# By Peter V. Saveliev https://pypi.python.org/pypi/pyroute2. Dual licensed under the Apache 2 and GPLv2+ see https://github.com/svinota/pyroute2 for License details. +from pyroute2.netns.process import MetaPopen + + +class NSPopenBase(object): + + __metaclass__ = MetaPopen diff --git a/node-admin/scripts/pyroute2/netns/process/base_p3.py b/node-admin/scripts/pyroute2/netns/process/base_p3.py new file mode 100644 index 00000000000..1356958e24a --- /dev/null +++ b/node-admin/scripts/pyroute2/netns/process/base_p3.py @@ -0,0 +1,7 @@ +# By Peter V. Saveliev https://pypi.python.org/pypi/pyroute2. Dual licensed under the Apache 2 and GPLv2+ see https://github.com/svinota/pyroute2 for License details. +from pyroute2.netns.process import MetaPopen + + +class NSPopenBase(object, metaclass=MetaPopen): + + pass diff --git a/node-admin/scripts/pyroute2/netns/process/proxy.py b/node-admin/scripts/pyroute2/netns/process/proxy.py new file mode 100644 index 00000000000..11695ac3d67 --- /dev/null +++ b/node-admin/scripts/pyroute2/netns/process/proxy.py @@ -0,0 +1,163 @@ +# By Peter V. Saveliev https://pypi.python.org/pypi/pyroute2. Dual licensed under the Apache 2 and GPLv2+ see https://github.com/svinota/pyroute2 for License details. +''' +NSPopen +======= + +The `NSPopen` class has nothing to do with netlink at +all, but it is required to have a reasonable network +namespace support. + +''' + + +import types +import atexit +import threading +import subprocess +from pyroute2.netns import setns +from pyroute2.config import MpQueue +from pyroute2.config import MpProcess +try: + from pyroute2.netns.process.base_p3 import NSPopenBase +except Exception: + from pyroute2.netns.process.base_p2 import NSPopenBase + + +def _handle(result): + if result['code'] == 500: + raise result['data'] + elif result['code'] == 200: + return result['data'] + else: + raise TypeError('unsupported return code') + + +def NSPopenServer(nsname, flags, channel_in, channel_out, argv, kwarg): + # set netns + try: + setns(nsname, flags=flags) + except Exception as e: + channel_out.put(e) + return + # create the Popen object + child = subprocess.Popen(*argv, **kwarg) + # send the API map + channel_out.put(None) + + while True: + # synchronous mode + # 1. get the command from the API + call = channel_in.get() + # 2. stop? + if call['name'] == 'release': + break + # 3. run the call + try: + attr = getattr(child, call['name']) + if isinstance(attr, types.MethodType): + result = attr(*call['argv'], **call['kwarg']) + else: + result = attr + channel_out.put({'code': 200, 'data': result}) + except Exception as e: + channel_out.put({'code': 500, 'data': e}) + child.wait() + + +class NSPopen(NSPopenBase): + ''' + A proxy class to run `Popen()` object in some network namespace. + + Sample to run `ip ad` command in `nsname` network namespace:: + + nsp = NSPopen('nsname', ['ip', 'ad'], stdout=subprocess.PIPE) + print(nsp.communicate()) + nsp.wait() + nsp.release() + + The only difference in the `release()` call. It explicitly ends + the proxy process and releases all the resources. + ''' + + def __init__(self, nsname, *argv, **kwarg): + ''' + The only differences from the `subprocess.Popen` init are: + * `nsname` -- network namespace name + * `flags` keyword argument + + All other arguments are passed directly to `subprocess.Popen`. + + Flags usage samples. Create a network namespace, if it doesn't + exist yet:: + + import os + nsp = NSPopen('nsname', ['command'], flags=os.O_CREAT) + + Create a network namespace only if it doesn't exist, otherwise + fail and raise an exception:: + + import os + nsp = NSPopen('nsname', ['command'], flags=os.O_CREAT | os.O_EXCL) + ''' + # create a child + self.nsname = nsname + if 'flags' in kwarg: + self.flags = kwarg.pop('flags') + else: + self.flags = 0 + self.channel_out = MpQueue() + self.channel_in = MpQueue() + self.lock = threading.Lock() + self.released = False + self.server = MpProcess(target=NSPopenServer, + args=(self.nsname, + self.flags, + self.channel_out, + self.channel_in, + argv, kwarg)) + # start the child and check the status + self.server.start() + response = self.channel_in.get() + if isinstance(response, Exception): + self.server.join() + raise response + else: + atexit.register(self.release) + + def release(self): + ''' + Explicitly stop the proxy process and release all the + resources. The `NSPopen` object can not be used after + the `release()` call. + ''' + with self.lock: + if self.released: + return + self.released = True + self.channel_out.put({'name': 'release'}) + self.channel_out.close() + self.channel_in.close() + self.server.join() + + def __dir__(self): + return list(self.api.keys()) + ['release'] + + def __getattribute__(self, key): + try: + return object.__getattribute__(self, key) + except AttributeError: + with self.lock: + if self.released: + raise RuntimeError('the object is released') + + if self.api.get(key) and self.api[key]['callable']: + def proxy(*argv, **kwarg): + self.channel_out.put({'name': key, + 'argv': argv, + 'kwarg': kwarg}) + return _handle(self.channel_in.get()) + proxy.__doc__ = self.api[key]['doc'] + return proxy + else: + self.channel_out.put({'name': key}) + return _handle(self.channel_in.get()) |