]> git.ipfire.org Git - thirdparty/lldpd.git/blame - tests/integration/fixtures/namespaces.py
Merge pull request #285 from vincentbernat/feature/linux-caps
[thirdparty/lldpd.git] / tests / integration / fixtures / namespaces.py
CommitLineData
e0a84778
VB
1import contextlib
2import ctypes
3import errno
4import os
5import pyroute2
6import pytest
7import signal
98745499 8import multiprocessing
e0a84778
VB
9
10# All allowed namespace types
11NAMESPACE_FLAGS = dict(mnt=0x00020000,
12 uts=0x04000000,
13 ipc=0x08000000,
14 user=0x10000000,
15 pid=0x20000000,
16 net=0x40000000)
17STACKSIZE = 1024*1024
18
19libc = ctypes.CDLL('libc.so.6', use_errno=True)
20
21
22@contextlib.contextmanager
23def keep_directory():
24 """Restore the current directory on exit."""
25 pwd = os.getcwd()
26 try:
27 yield
28 finally:
29 os.chdir(pwd)
30
31
08e05799
VB
32def mount_sys(target="/sys"):
33 flags = [2 | 4 | 8] # MS_NOSUID | MS_NODEV | MS_NOEXEC
34 flags.append(1 << 18) # MS_PRIVATE
35 flags.append(1 << 19) # MS_SLAVE
36 for fl in flags:
37 ret = libc.mount(b"none",
38 target.encode('ascii'),
39 b"sysfs",
40 fl,
41 None)
42 if ret == -1:
43 e = ctypes.get_errno()
44 raise OSError(e, os.strerror(e))
45
46
98745499
VB
47def mount_tmpfs(target, private=False):
48 flags = [0]
49 if private:
50 flags.append(1 << 18) # MS_PRIVATE
51 flags.append(1 << 19) # MS_SLAVE
52 for fl in flags:
53 ret = libc.mount(b"none",
54 target.encode('ascii'),
55 b"tmpfs",
56 fl,
57 None)
58 if ret == -1:
59 e = ctypes.get_errno()
60 raise OSError(e, os.strerror(e))
61
62
63def _mount_proc(target):
64 flags = [2 | 4 | 8] # MS_NOSUID | MS_NODEV | MS_NOEXEC
65 flags.append(1 << 18) # MS_PRIVATE
66 flags.append(1 << 19) # MS_SLAVE
67 for fl in flags:
68 ret = libc.mount(b"proc",
69 target.encode('ascii'),
70 b"proc",
71 fl,
72 None)
73 if ret == -1:
74 e = ctypes.get_errno()
75 raise OSError(e, os.strerror(e))
76
77
78def mount_proc(target="/proc"):
79 # We need to be sure /proc is correct. We do that in another
80 # process as this doesn't play well with setns().
81 if not os.path.isdir(target):
82 os.mkdir(target)
83 p = multiprocessing.Process(target=_mount_proc, args=(target,))
84 p.start()
85 p.join()
86
87
e0a84778
VB
88class Namespace(object):
89 """Combine several namespaces into one.
90
91 This gets a list of namespace types to create and combine into one. The
92 combined namespace can be used as a context manager to enter all the
93 created namespaces and exit them at the end.
94 """
95
96 def __init__(self, *namespaces):
97 self.namespaces = namespaces
98 for ns in namespaces:
99 assert ns in NAMESPACE_FLAGS
100
101 # Get a pipe to signal the future child to exit
102 self.pipe = os.pipe()
103
104 # First, create a child in the given namespaces
105 child = ctypes.CFUNCTYPE(ctypes.c_int)(self.child)
106 child_stack = ctypes.create_string_buffer(STACKSIZE)
107 child_stack_pointer = ctypes.c_void_p(
108 ctypes.cast(child_stack,
109 ctypes.c_void_p).value + STACKSIZE)
110 flags = signal.SIGCHLD
111 for ns in namespaces:
112 flags |= NAMESPACE_FLAGS[ns]
113 pid = libc.clone(child, child_stack_pointer, flags)
114 if pid == -1:
115 e = ctypes.get_errno()
116 raise OSError(e, os.strerror(e))
117
118 # If a user namespace, map UID 0 to the current one
119 if 'user' in namespaces:
120 uid_map = '0 {} 1'.format(os.getuid())
121 gid_map = '0 {} 1'.format(os.getgid())
122 with open('/proc/{}/uid_map'.format(pid), 'w') as f:
123 f.write(uid_map)
124 with open('/proc/{}/setgroups'.format(pid), 'w') as f:
125 f.write('deny')
126 with open('/proc/{}/gid_map'.format(pid), 'w') as f:
127 f.write(gid_map)
128
129 # Retrieve a file descriptor to this new namespace
130 self.next = [os.open('/proc/{}/ns/{}'.format(pid, x),
131 os.O_RDONLY) for x in namespaces]
132
133 # Keep a file descriptor to our old namespaces
134 self.previous = [os.open('/proc/self/ns/{}'.format(x),
135 os.O_RDONLY) for x in namespaces]
136
137 # Tell the child all is done and let it die
138 os.close(self.pipe[0])
139 if 'pid' not in namespaces:
140 os.close(self.pipe[1])
ad8971ec 141 self.pipe = None
e0a84778
VB
142 os.waitpid(pid, 0)
143
ad8971ec
VB
144 def __del__(self):
145 for fd in self.next:
146 os.close(fd)
147 for fd in self.previous:
148 os.close(fd)
149 if self.pipe is not None:
150 os.close(self.pipe[1])
151
e0a84778
VB
152 def child(self):
153 """Cloned child.
154
155 Just be here until our parent extract the file descriptor from
156 us.
157
158 """
159 os.close(self.pipe[1])
160
161 # For a network namespace, enable lo
162 if 'net' in self.namespaces:
163 ipr = pyroute2.IPRoute()
164 lo = ipr.link_lookup(ifname='lo')[0]
165 ipr.link('set', index=lo, state='up')
166 # For a mount namespace, make it private
167 if 'mnt' in self.namespaces:
168 libc.mount(b"none", b"/", None,
169 # MS_REC | MS_PRIVATE
170 16384 | (1 << 18),
171 None)
172
173 while True:
174 try:
175 os.read(self.pipe[0], 1)
176 except OSError as e:
177 if e.errno in [errno.EAGAIN, errno.EINTR]:
178 continue
179 break
180
181 os._exit(0)
182
183 def fd(self, namespace):
184 """Return the file descriptor associated to a namespace"""
185 assert namespace in self.namespaces
186 return self.next[self.namespaces.index(namespace)]
187
188 def __enter__(self):
189 with keep_directory():
190 for n in self.next:
191 if libc.setns(n, 0) == -1:
192 ns = self.namespaces[self.next.index(n)] # NOQA
193 e = ctypes.get_errno()
194 raise OSError(e, os.strerror(e))
195
196 def __exit__(self, *exc):
197 with keep_directory():
198 err = None
199 for p in reversed(self.previous):
200 if libc.setns(p, 0) == -1 and err is None:
201 ns = self.namespaces[self.previous.index(p)] # NOQA
202 e = ctypes.get_errno()
203 err = OSError(e, os.strerror(e))
204 if err:
205 raise err
206
207 def __repr__(self):
208 return 'Namespace({})'.format(", ".join(self.namespaces))
209
210
211class NamespaceFactory(object):
212 """Dynamically create namespaces as they are created.
213
214 Those namespaces are namespaces for IPC, net, mount and UTS. PID
215 is a bit special as we have to keep a process for that. We don't
216 do that to ensure that everything is cleaned
217 automatically. Therefore, the child process is killed as soon as
218 we got a file descriptor to the namespace. We don't use a user
219 namespace either because we are unlikely to be able to exit it.
220
221 """
222
98745499 223 def __init__(self, tmpdir):
e0a84778 224 self.namespaces = {}
98745499 225 self.tmpdir = tmpdir
e0a84778
VB
226
227 def __call__(self, ns):
228 """Return a namespace. Create it if it doesn't exist."""
229 if ns in self.namespaces:
230 return self.namespaces[ns]
98745499 231
e0a84778 232 self.namespaces[ns] = Namespace('ipc', 'net', 'mnt', 'uts')
98745499
VB
233 with self.namespaces[ns]:
234 mount_proc()
235 mount_sys()
236 # Also setup the "namespace-dependant" directory
237 self.tmpdir.join("ns").ensure(dir=True)
238 mount_tmpfs(str(self.tmpdir.join("ns")), private=True)
239
e0a84778
VB
240 return self.namespaces[ns]
241
242
243@pytest.fixture
98745499
VB
244def namespaces(tmpdir):
245 return NamespaceFactory(tmpdir)