|
1 | | -#!/usr/bin/env python3 |
| 1 | +#!/usr/bin/env python |
2 | 2 |
|
3 | 3 | """ |
4 | 4 | nsenter - run program with namespaces of other processes |
5 | 5 | """ |
6 | 6 |
|
7 | 7 | import argparse |
8 | 8 | import ctypes |
| 9 | +import errno |
9 | 10 | import os |
10 | 11 | import logging |
11 | 12 | from pathlib import Path |
|
14 | 15 | except ImportError: |
15 | 16 | from contextlib2 import ExitStack |
16 | 17 |
|
17 | | -NAMESPACE_NAMES = frozenset('mnt ipc net pid user uts'.split()) |
| 18 | +NAMESPACE_NAMES = frozenset(['mnt', 'ipc', 'net', 'pid', 'user', 'uts']) |
18 | 19 |
|
19 | | -log = logging.getLogger('nsenter') |
20 | 20 |
|
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 |
27 | 41 | """ |
28 | | - return Path('/proc') / str(process) / 'ns' / ns_type |
29 | 42 |
|
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) |
31 | 45 |
|
| 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 | + )) |
32 | 51 |
|
33 | | -class Namespace(object): |
34 | | - def __init__(self, pid, ns_type): |
35 | 52 | self.pid = pid |
36 | 53 | 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() |
40 | 57 | self.target_fileno = self.target_fd.fileno() |
41 | 58 |
|
| 59 | + self.parent_fd = self._nsfd('self', ns_type).open() |
| 60 | + self.parent_fileno = self.parent_fd.fileno() |
| 61 | + |
42 | 62 | __init__.__annotations__ = {'pid': str, 'ns_type': str} |
43 | 63 |
|
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 |
47 | 67 |
|
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""" |
51 | 81 | try: |
52 | 82 | self.target_fd.close() |
53 | 83 | except: |
54 | 84 | pass |
55 | 85 | self.parent_fd.close() |
56 | 86 |
|
| 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__) |
57 | 110 |
|
58 | | -def main(): |
59 | | - parser = argparse.ArgumentParser(description=__doc__) |
60 | 111 | 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 | + |
62 | 116 | 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 | + |
65 | 127 | parser.add_argument('command', nargs='*', default=['/bin/sh']) |
66 | 128 |
|
67 | 129 | args = parser.parse_args() |
68 | 130 |
|
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 | + |
77 | 154 |
|
78 | | -if __name__ == '__main__': |
| 155 | +if __name__ == '__main__': # pragma: no cover |
79 | 156 | main() |
0 commit comments