Skip to content
This repository was archived by the owner on Dec 5, 2022. It is now read-only.

Commit e75de61

Browse files
committed
Merge pull request #6 from cnelson/setns-via-path
Added support for entering namespaces using absolute paths
2 parents 998b9ab + 246265a commit e75de61

File tree

3 files changed

+117
-31
lines changed

3 files changed

+117
-31
lines changed

README.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ Example usage from Python:
3838
# output network interfaces as seen from within the mypid's net NS:
3939
subprocess.check_output(['ip', 'a'])
4040
41+
# or enter an arbitrary namespace:
42+
with Namespace('/var/run/netns/foo', 'net'):
43+
# output network interfaces as seen from within the mypid's net NS:
44+
subprocess.check_output(['ip', 'a'])
4145
4246
.. _nsenter: http://man7.org/linux/man-pages/man1/nsenter.1.html
4347
.. _blog post "Entering Kernel Namespaces from Python": http://tech.zalando.com/posts/entering-kernel-namespaces-with-python.html

nsenter/__init__.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,28 @@ class Namespace(object):
2222
"""A context manager for entering namespaces
2323
2424
Args:
25-
pid: The PID for the owner of the namespace to enter
25+
pid: The PID for the owner of the namespace to enter, or an absolute
26+
path to a file which represents a namespace handle.
27+
2628
ns_type: The type of namespace to enter must be one of
27-
mnt ipc net pid user uts
29+
mnt ipc net pid user uts. If pid is an absolute path, this
30+
much match the type of namespace it represents
31+
2832
proc: The path to the /proc file system. If running in a container
29-
the host proc file system may be binded mounted in a different
30-
location
33+
the host proc file system may be binded mounted in a different
34+
location
3135
3236
Raises:
3337
IOError: A non existent PID was provided
3438
ValueError: An improper ns_type was provided
3539
OSError: Unable to enter or exit the namespace
3640
3741
Example:
38-
with Namespace(<pid>, <ns_type>):
42+
with Namespace(916, 'net'):
43+
#do something in the namespace
44+
pass
45+
46+
with Namespace('/var/run/netns/foo', 'net'):
3947
#do something in the namespace
4048
pass
4149
"""
@@ -53,7 +61,13 @@ def __init__(self, pid, ns_type, proc='/proc'):
5361
self.ns_type = ns_type
5462
self.proc = proc
5563

56-
self.target_fd = self._nsfd(pid, ns_type).open()
64+
# if it's numeric, then it's a pid, else assume it's a path
65+
try:
66+
pid = int(pid)
67+
self.target_fd = self._nsfd(pid, ns_type).open()
68+
except ValueError:
69+
self.target_fd = Path(pid).open()
70+
5771
self.target_fileno = self.target_fd.fileno()
5872

5973
self.parent_fd = self._nsfd('self', ns_type).open()
@@ -82,7 +96,9 @@ def _close_files(self):
8296
self.target_fd.close()
8397
except:
8498
pass
85-
self.parent_fd.close()
99+
100+
if self.parent_fd is not None:
101+
self.parent_fd.close()
86102

87103
def __enter__(self):
88104
self._log.debug('Entering %s namespace %s', self.ns_type, self.pid)

tests.py

Lines changed: 90 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,135 @@
1+
import ctypes
2+
import os
3+
import subprocess
4+
import tempfile
15
import unittest
26

3-
import subprocess, os, errno
7+
from nsenter import Namespace, NAMESPACE_NAMES
48

5-
from nsenter import Namespace, NAMESPACE_NAMES
69

710
class TestNamespaces(unittest.TestCase):
811

12+
_libc = ctypes.CDLL('libc.so.6', use_errno=True)
13+
914
def setUp(self):
1015
"""Spawn a child process so we have a PID to enter"""
11-
16+
1217
self._child = subprocess.Popen(['/bin/cat'])
1318

1419
def tearDown(self):
1520
"""SIGTERM the child process"""
16-
21+
1722
self._child.terminate()
1823
self._child.wait()
1924

20-
def test_namespaces_except_user(self):
21-
"""Test entering all namespaces execept user
22-
"""
25+
def test_namespace_non_exist_path(self):
26+
"""Test entering a non-existent path"""
27+
28+
def do_test():
29+
fd, filename = tempfile.mkstemp()
30+
os.close(fd)
31+
os.remove(filename)
32+
33+
with Namespace(filename, 'net'):
34+
pass
35+
36+
self.assertRaises(IOError, do_test)
37+
38+
def test_namespace_plain_file_path(self):
39+
"""Test entering a plain file path"""
40+
41+
fd, filename = tempfile.mkstemp()
42+
os.close(fd)
43+
44+
def do_test():
45+
with Namespace(filename, 'net'):
46+
pass
47+
48+
self.assertRaises(OSError, do_test)
49+
50+
os.remove(filename)
51+
52+
def test_namespace_directory_path(self):
53+
"""Test entering a directory path"""
54+
55+
def do_test():
56+
with Namespace('/tmp', 'net'):
57+
pass
58+
59+
self.assertRaises(IOError, do_test)
60+
61+
@unittest.skipIf(os.geteuid() != 0, "Must be root to bind mount")
62+
def test_namespace_good_path(self):
63+
"""Test entering an arbirtrary namespace"""
64+
65+
try:
66+
# get the path to it's network namespace
67+
ns_path = os.path.join('/proc', str(self._child.pid), 'ns', 'net')
68+
69+
# bind mount it to a temp location
70+
fd, filename = tempfile.mkstemp()
71+
os.close(fd)
72+
73+
assert self._libc.mount(ns_path.encode('ascii'), filename.encode('ascii'), 0, 4096, 0) == 0
74+
75+
# enter the bind mount
76+
with Namespace(filename, 'net'):
77+
pass
78+
79+
finally:
80+
# ensure we clean up the bind
81+
self._libc.umount(filename.encode('ascii'))
82+
os.remove(filename)
83+
84+
@unittest.skipIf(os.geteuid() != 0, "Must be root to setns()")
85+
def test_namespaces_as_root(self):
86+
"""Test entering all namespaces the pid has as root"""
87+
88+
for name in filter(lambda x: x != 'user', NAMESPACE_NAMES):
89+
if os.path.exists(os.path.join('/proc', str(self._child.pid), 'ns', name)):
90+
with Namespace(self._child.pid, name):
91+
pass
92+
93+
@unittest.skipIf(os.geteuid() == 0, "Must not be root to trigger OSError")
94+
def test_namespaces_except_user_as_normal(self):
95+
"""Test entering all namespaces execept user as non-root"""
2396

24-
#Can't use the assertRaises context manager in python2.6
2597
def do_test():
2698
for name in filter(lambda x: x != 'user', NAMESPACE_NAMES):
2799
with Namespace(self._child.pid, name):
28100
pass
29-
30-
#if we aren't root (technically: CAP_SYS_ADMIN)
31-
#then we'll get OSError (EPERM) for all our tests
32-
if os.geteuid() != 0:
33-
self.assertRaises(OSError, do_test)
34-
else:
35-
do_test()
36101

102+
self.assertRaises(OSError, do_test)
103+
104+
@unittest.skipIf(os.geteuid() != 0, "Must be root to setns()")
37105
def test_user_namespace(self):
38106
"""Test entering a non-existent namespace"""
39-
107+
40108
def do_test():
41109
with Namespace(self._child.pid, 'user'):
42110
pass
43111

44-
#This process doesn't have a user namespace
45-
#So this will OSError(EINVAL)
46-
self.assertRaises(OSError, do_test)
112+
# this will railse a IOError on python2 and OSError on python 3
113+
# as the file for this namespace does not exist!
114+
self.assertRaises((IOError, OSError), do_test)
47115

48116
def test_bad_namespace(self):
49117
"""Test entering a bad namespace type"""
50-
118+
51119
def do_test():
52120
with Namespace(self._child.pid, 'foo'):
53121
pass
54122
self.assertRaises(ValueError, do_test)
55123

56124
def test_bad_pid(self):
57-
"""Test entering bad pid's name space"""
58-
125+
"""Test entering bad pid's namespace"""
126+
59127
def do_test():
60128
with Namespace('foo', 'net'):
61129
pass
62130

63131
self.assertRaises(IOError, do_test)
64132

65133

66-
67134
if __name__ == '__main__':
68135
unittest.main()
69-

0 commit comments

Comments
 (0)