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

Commit 1fe58aa

Browse files
committed
Merge pull request #3 from cnelson/refactor_and_cleanup
Refactor and cleanup
2 parents 960cc9e + 3155ad0 commit 1fe58aa

File tree

4 files changed

+165
-43
lines changed

4 files changed

+165
-43
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ dist
33
*.egg*
44
*.pyc
55
.tox
6+
.coverage

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ NSEnter
55
This Python package allows entering Linux kernel namespaces (mount, IPC, net, PID, user and UTS) by doing the "setns" syscall.
66
The command line interface tries to be similar to the nsenter_ C program.
77

8-
Requires Python 3.4.
8+
Requires Python 2.6 or higher
99

1010
See the introductory `blog post "Entering Kernel Namespaces from Python"`_.
1111

nsenter/__init__.py

Lines changed: 113 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
#!/usr/bin/env python3
1+
#!/usr/bin/env python
22

33
"""
44
nsenter - run program with namespaces of other processes
55
"""
66

77
import argparse
88
import ctypes
9+
import errno
910
import os
1011
import logging
1112
from pathlib import Path
@@ -14,66 +15,142 @@
1415
except ImportError:
1516
from contextlib2 import ExitStack
1617

17-
NAMESPACE_NAMES = frozenset('mnt ipc net pid user uts'.split())
18+
NAMESPACE_NAMES = frozenset(['mnt', 'ipc', 'net', 'pid', 'user', 'uts'])
1819

19-
log = logging.getLogger('nsenter')
2020

21-
libc = ctypes.CDLL('libc.so.6')
22-
23-
24-
def nsfd(process, ns_type):
25-
"""
26-
Returns the namespace file descriptor for process (self or PID) and namespace type
21+
class Namespace(object):
22+
"""A context manager for entering namespaces
23+
24+
Args:
25+
pid: The PID for the owner of the namespace to enter
26+
ns_type: The type of namespace to enter must be one of
27+
mnt ipc net pid user uts
28+
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
31+
32+
Raises:
33+
IOError: A non existent PID was provided
34+
ValueError: An improper ns_type was provided
35+
OSError: Unable to enter or exit the namespace
36+
37+
Example:
38+
with Namespace(<pid>, <ns_type>):
39+
#do something in the namespace
40+
pass
2741
"""
28-
return Path('/proc') / str(process) / 'ns' / ns_type
2942

30-
nsfd.__annotations__ = {'process': str, 'ns_type': str, 'return': Path}
43+
_log = logging.getLogger(__name__)
44+
_libc = ctypes.CDLL('libc.so.6', use_errno=True)
3145

46+
def __init__(self, pid, ns_type, proc='/proc'):
47+
if ns_type not in NAMESPACE_NAMES:
48+
raise ValueError('ns_type must be one of {0}'.format(
49+
', '.join(NAMESPACE_NAMES)
50+
))
3251

33-
class Namespace(object):
34-
def __init__(self, pid, ns_type):
3552
self.pid = pid
3653
self.ns_type = ns_type
37-
self.parent_fd = nsfd('self', ns_type).open()
38-
self.parent_fileno = self.parent_fd.fileno()
39-
self.target_fd = nsfd(pid, ns_type).open()
54+
self.proc = proc
55+
56+
self.target_fd = self._nsfd(pid, ns_type).open()
4057
self.target_fileno = self.target_fd.fileno()
4158

59+
self.parent_fd = self._nsfd('self', ns_type).open()
60+
self.parent_fileno = self.parent_fd.fileno()
61+
4262
__init__.__annotations__ = {'pid': str, 'ns_type': str}
4363

44-
def __enter__(self):
45-
log.debug('Entering %s namespace %s', self.ns_type, self.pid)
46-
libc.setns(self.target_fileno, 0)
64+
def _nsfd(self, pid, ns_type):
65+
"""Utility method to build a pathlib.Path instance pointing at the
66+
requested namespace entry
4767
48-
def __exit__(self, type, value, tb):
49-
log.debug('Leaving %s namespace %s', self.ns_type, self.pid)
50-
libc.setns(self.parent_fileno, 0)
68+
Args:
69+
pid: The PID
70+
ns_type: The namespace type to enter
71+
72+
Returns:
73+
pathlib.Path pointing to the /proc namespace entry
74+
"""
75+
return Path(self.proc) / str(pid) / 'ns' / ns_type
76+
77+
_nsfd.__annotations__ = {'process': str, 'ns_type': str, 'return': Path}
78+
79+
def _close_files(self):
80+
"""Utility method to close our open file handles"""
5181
try:
5282
self.target_fd.close()
5383
except:
5484
pass
5585
self.parent_fd.close()
5686

87+
def __enter__(self):
88+
self._log.debug('Entering %s namespace %s', self.ns_type, self.pid)
89+
90+
if self._libc.setns(self.target_fileno, 0) == -1:
91+
e = ctypes.get_errno()
92+
self._close_files()
93+
raise OSError(e, errno.errorcode[e])
94+
95+
def __exit__(self, type, value, tb):
96+
self._log.debug('Leaving %s namespace %s', self.ns_type, self.pid)
97+
98+
if self._libc.setns(self.parent_fileno, 0) == -1:
99+
e = ctypes.get_errno()
100+
self._close_files()
101+
raise OSError(e, errno.errorcode[e])
102+
103+
self._close_files()
104+
105+
106+
def main(): # pragma: no cover
107+
"""Command line interface to the Namespace context manager"""
108+
109+
parser = argparse.ArgumentParser(prog='nsenter', description=__doc__)
57110

58-
def main():
59-
parser = argparse.ArgumentParser(description=__doc__)
60111
parser.add_argument('--target', '-t', required=True, metavar='PID',
61-
help='Specify a target process to get contexts from.')
112+
help='A target process to get contexts from')
113+
114+
group = parser.add_argument_group('Namespaces')
115+
62116
for ns in NAMESPACE_NAMES:
63-
parser.add_argument('--{0}'.format(ns), action='store_true', help='Enter the {0} namespace'.format(ns))
64-
parser.add_argument('--all', action='store_true', help='Enter all namespaces')
117+
group.add_argument('--{0}'.format(ns),
118+
action='store_true',
119+
help='Enter the {0} namespace'.format(ns)
120+
)
121+
122+
parser.add_argument('--all',
123+
action='store_true',
124+
help='Enter all namespaces'
125+
)
126+
65127
parser.add_argument('command', nargs='*', default=['/bin/sh'])
66128

67129
args = parser.parse_args()
68130

69-
with ExitStack() as stack:
70-
namespaces = []
71-
for ns in NAMESPACE_NAMES:
72-
if getattr(args, ns) or args.all:
73-
namespaces.append(Namespace(args.target, ns))
74-
for ns in namespaces:
75-
stack.enter_context(ns)
76-
os.execl(args.command[0], *args.command)
131+
# make sure we have --all or at least one namespace
132+
if (True not in [getattr(args, ns) for ns in NAMESPACE_NAMES]
133+
and not args.all):
134+
parser.error('You must specify at least one namespace')
135+
136+
try:
137+
with ExitStack() as stack:
138+
namespaces = []
139+
for ns in NAMESPACE_NAMES:
140+
if getattr(args, ns) or args.all:
141+
namespaces.append(Namespace(args.target, ns))
142+
143+
for ns in namespaces:
144+
stack.enter_context(ns)
145+
146+
os.execlp(args.command[0], *args.command)
147+
except IOError as exc:
148+
parser.error('Unable to access PID: {0}'.format(exc))
149+
except OSError as exc:
150+
parser.error('Unable to enter {0} namespace: {1}'.format(
151+
ns.ns_type, exc
152+
))
153+
77154

78-
if __name__ == '__main__':
155+
if __name__ == '__main__': # pragma: no cover
79156
main()

tests.py

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,68 @@
11
import unittest
22

3-
import subprocess
3+
import subprocess, os, errno
44

55
from nsenter import Namespace, NAMESPACE_NAMES
66

77
class TestNamespaces(unittest.TestCase):
88

99
def setUp(self):
10+
"""Spawn a child process so we have a PID to enter"""
11+
1012
self._child = subprocess.Popen(['/bin/cat'])
1113

1214
def tearDown(self):
15+
"""SIGTERM the child process"""
16+
1317
self._child.terminate()
1418
self._child.wait()
1519

16-
def test_namespace(self):
17-
for name in NAMESPACE_NAMES:
18-
with Namespace(self._child.pid, name):
19-
assert True
20+
def test_namespaces_except_user(self):
21+
"""Test entering all namespaces execept user
22+
"""
23+
24+
#Can't use the assertRaises context manager in python2.6
25+
def do_test():
26+
for name in filter(lambda x: x != 'user', NAMESPACE_NAMES):
27+
with Namespace(self._child.pid, name):
28+
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()
36+
37+
def test_user_namespace(self):
38+
"""Test entering a non-existent namespace"""
39+
40+
def do_test():
41+
with Namespace(self._child.pid, 'user'):
42+
pass
43+
44+
#This process doesn't have a user namespace
45+
#So this will OSError(EINVAL)
46+
self.assertRaises(OSError, do_test)
47+
48+
def test_bad_namespace(self):
49+
"""Test entering a bad namespace type"""
50+
51+
def do_test():
52+
with Namespace(self._child.pid, 'foo'):
53+
pass
54+
self.assertRaises(ValueError, do_test)
55+
56+
def test_bad_pid(self):
57+
"""Test entering bad pid's name space"""
58+
59+
def do_test():
60+
with Namespace('foo', 'net'):
61+
pass
62+
63+
self.assertRaises(IOError, do_test)
64+
2065

21-
pass
2266

2367
if __name__ == '__main__':
2468
unittest.main()

0 commit comments

Comments
 (0)