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

Commit adcfe14

Browse files
committed
large refactor; Moved module level code into class; added docstrings, exception handling, and more tests;
1 parent 960cc9e commit adcfe14

File tree

3 files changed

+157
-42
lines changed

3 files changed

+157
-42
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

nsenter/__init__.py

Lines changed: 104 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import argparse
88
import ctypes
9+
import errno
910
import os
1011
import logging
1112
from pathlib import Path
@@ -14,66 +15,133 @@
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')
21+
class Namespace(object):
22+
"""A context manager for entering namespaces
2223
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
2328
24-
def nsfd(process, ns_type):
25-
"""
26-
Returns the namespace file descriptor for process (self or PID) and namespace type
27-
"""
28-
return Path('/proc') / str(process) / 'ns' / ns_type
29+
Raises:
30+
IOError: A non existent PID was provided
31+
ValueError: An improper ns_type was provided
32+
OSError: Unable to enter or exit the namespace
2933
30-
nsfd.__annotations__ = {'process': str, 'ns_type': str, 'return': Path}
34+
Example:
35+
with Namespace(<pid>, <ns_type>):
36+
#do something in the namespace
37+
pass
38+
"""
3139

40+
_log = logging.getLogger(__name__)
41+
_libc = ctypes.CDLL('libc.so.6', use_errno=True)
3242

33-
class Namespace(object):
3443
def __init__(self, pid, ns_type):
44+
if ns_type not in NAMESPACE_NAMES:
45+
raise ValueError('ns_type must be one of {0}'.format(
46+
', '.join(NAMESPACE_NAMES)
47+
))
48+
3549
self.pid = pid
3650
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()
51+
52+
self.target_fd = self._nsfd(pid, ns_type).open()
4053
self.target_fileno = self.target_fd.fileno()
4154

42-
__init__.__annotations__ = {'pid': str, 'ns_type': str}
55+
self.parent_fd = self._nsfd('self', ns_type).open()
56+
self.parent_fileno = self.parent_fd.fileno()
4357

44-
def __enter__(self):
45-
log.debug('Entering %s namespace %s', self.ns_type, self.pid)
46-
libc.setns(self.target_fileno, 0)
58+
__init__.__annotations__ = {'pid': str, 'ns_type': str}
4759

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)
60+
def _nsfd(self, pid, ns_type):
61+
"""Utility method to build a pathlib.Path instance pointing at the
62+
requested namespace entry
63+
64+
Args:
65+
pid: The PID
66+
ns_type: The namespace type to enter
67+
68+
Returns:
69+
pathlib.Path pointing to the /proc namespace entry
70+
"""
71+
return Path('/proc') / str(pid) / 'ns' / ns_type
72+
73+
_nsfd.__annotations__ = {'process': str, 'ns_type': str, 'return': Path}
74+
75+
def _close_files(self):
76+
"""Utility method to close our open file handles"""
5177
try:
5278
self.target_fd.close()
5379
except:
5480
pass
55-
self.parent_fd.close()
81+
self.parent_fd.close()
82+
83+
def __enter__(self):
84+
self._log.debug('Entering %s namespace %s', self.ns_type, self.pid)
5685

86+
if self._libc.setns(self.target_fileno, 0) == -1:
87+
e = ctypes.get_errno()
88+
self._close_files()
89+
raise OSError(e, errno.errorcode[e])
5790

58-
def main():
59-
parser = argparse.ArgumentParser(description=__doc__)
91+
def __exit__(self, type, value, tb):
92+
self._log.debug('Leaving %s namespace %s', self.ns_type, self.pid)
93+
94+
if self._libc.setns(self.parent_fileno, 0) == -1:
95+
e = ctypes.get_errno()
96+
self._close_files()
97+
raise OSError(e, errno.errorcode[e])
98+
99+
self._close_files()
100+
101+
def main(): #pragma: no cover
102+
"""Command line interface to the Namespace Contet Manager"""
103+
104+
parser = argparse.ArgumentParser(prog='nsenter', description=__doc__)
105+
60106
parser.add_argument('--target', '-t', required=True, metavar='PID',
61-
help='Specify a target process to get contexts from.')
107+
help='A target process to get contexts from')
108+
109+
group = parser.add_argument_group('Namespaces')
110+
62111
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')
112+
group.add_argument('--{0}'.format(ns), action='store_true',
113+
help='Enter the {0} namespace'.format(ns))
114+
115+
parser.add_argument('--all', action='store_true',
116+
help='Enter all namespaces')
117+
65118
parser.add_argument('command', nargs='*', default=['/bin/sh'])
66119

67120
args = parser.parse_args()
68121

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)
77-
78-
if __name__ == '__main__':
122+
#make sure we have --all or at least one namespace
123+
if (True not in [getattr(args, ns) for ns in NAMESPACE_NAMES]
124+
and not args.all):
125+
parser.error('You must specify at least one namespace')
126+
127+
try:
128+
with ExitStack() as stack:
129+
namespaces = []
130+
for ns in NAMESPACE_NAMES:
131+
if getattr(args, ns) or args.all:
132+
namespaces.append(Namespace(args.target, ns))
133+
134+
for ns in namespaces:
135+
stack.enter_context(ns)
136+
137+
os.execlp(args.command[0], *args.command)
138+
except IOError as exc:
139+
parser.error('Unable to access PID: {0}'.format(exc))
140+
except OSError as exc:
141+
parser.error('Unable to enter {0} namespace: {1}'.format(
142+
ns.ns_type, exc
143+
))
144+
145+
146+
if __name__ == '__main__': #pragma: no cover
79147
main()

tests.py

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,70 @@
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+
Must have CAP_SYS_ADMIN to run these tests properly
24+
"""
25+
26+
#Can't use the assertRaises context manager in python2.6
27+
def do_test():
28+
for name in filter(lambda x: x != 'user', NAMESPACE_NAMES):
29+
with Namespace(self._child.pid, name):
30+
pass
31+
32+
#if we aren't root (technically: CAP_SYS_ADMIN)
33+
#then we'll get OSError (EPERM) for all our tests
34+
if os.geteuid() != 0:
35+
self.assertRaises(OSError, do_test)
36+
else:
37+
do_test()
38+
39+
def test_user_namespace(self):
40+
"""Test entering a non-existent namespace"""
41+
42+
def do_test():
43+
with Namespace(self._child.pid, 'user'):
44+
pass
45+
46+
#This process doesn't have a user namespace
47+
#So this will OSError(EINVAL)
48+
self.assertRaises(OSError, do_test)
49+
50+
def test_bad_namespace(self):
51+
"""Test entering a bad namespace type"""
52+
53+
def do_test():
54+
with Namespace(self._child.pid, 'foo'):
55+
pass
56+
self.assertRaises(ValueError, do_test)
57+
58+
def test_bad_pid(self):
59+
"""Test entering bad pid's name space"""
60+
61+
def do_test():
62+
with Namespace('foo', 'net'):
63+
pass
64+
65+
self.assertRaises(IOError, do_test)
66+
2067

21-
pass
2268

2369
if __name__ == '__main__':
2470
unittest.main()

0 commit comments

Comments
 (0)