Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aos
GitHub Repository: aos/firecracker
Path: blob/main/tests/framework/jailer.py
1957 views
1
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
# SPDX-License-Identifier: Apache-2.0
3
"""Define a class for creating the jailed context."""
4
5
import os
6
import shutil
7
import stat
8
from pathlib import Path
9
from retry.api import retry_call
10
import framework.utils as utils
11
import framework.defs as defs
12
from framework.defs import FC_BINARY_NAME
13
14
# Default name for the socket used for API calls.
15
DEFAULT_USOCKET_NAME = 'run/firecracker.socket'
16
# The default location for the chroot.
17
DEFAULT_CHROOT_PATH = f'{defs.DEFAULT_TEST_SESSION_ROOT_PATH}/jailer'
18
19
20
class JailerContext:
21
"""Represents jailer configuration and contains jailer helper functions.
22
23
Each microvm will have a jailer configuration associated with it.
24
"""
25
26
# Keep in sync with parameters from code base.
27
jailer_id = None
28
exec_file = None
29
numa_node = None
30
uid = None
31
gid = None
32
chroot_base = None
33
netns = None
34
daemonize = None
35
new_pid_ns = None
36
extra_args = None
37
api_socket_name = None
38
cgroups = None
39
40
def __init__(
41
self,
42
jailer_id,
43
exec_file,
44
numa_node=None,
45
uid=1234,
46
gid=1234,
47
chroot_base=DEFAULT_CHROOT_PATH,
48
netns=None,
49
daemonize=True,
50
new_pid_ns=False,
51
cgroups=None,
52
**extra_args
53
):
54
"""Set up jailer fields.
55
56
This plays the role of a default constructor as it populates
57
the jailer's fields with some default values. Each field can be
58
further adjusted by each test even with None values.
59
"""
60
self.jailer_id = jailer_id
61
self.exec_file = exec_file
62
self.numa_node = numa_node
63
self.uid = uid
64
self.gid = gid
65
self.chroot_base = chroot_base
66
self.netns = netns if netns is not None else jailer_id
67
self.daemonize = daemonize
68
self.new_pid_ns = new_pid_ns
69
self.extra_args = extra_args
70
self.api_socket_name = DEFAULT_USOCKET_NAME
71
self.cgroups = cgroups
72
self.ramfs_subdir_name = 'ramfs'
73
self._ramfs_path = None
74
75
def __del__(self):
76
"""Cleanup this jailer context."""
77
self.cleanup()
78
79
# Disabling 'too-many-branches' warning for this function as it needs to
80
# check every argument, so the number of branches will increase
81
# with every new argument.
82
# pylint: disable=too-many-branches
83
def construct_param_list(self):
84
"""Create the list of parameters we want the jailer to start with.
85
86
We want to be able to vary any parameter even the required ones as we
87
might want to add integration tests that validate the enforcement of
88
mandatory arguments.
89
"""
90
jailer_param_list = []
91
92
# Pretty please, try to keep the same order as in the code base.
93
if self.jailer_id is not None:
94
jailer_param_list.extend(['--id', str(self.jailer_id)])
95
if self.exec_file is not None:
96
jailer_param_list.extend(['--exec-file', str(self.exec_file)])
97
if self.numa_node is not None:
98
jailer_param_list.extend(['--node', str(self.numa_node)])
99
if self.uid is not None:
100
jailer_param_list.extend(['--uid', str(self.uid)])
101
if self.gid is not None:
102
jailer_param_list.extend(['--gid', str(self.gid)])
103
if self.chroot_base is not None:
104
jailer_param_list.extend(
105
['--chroot-base-dir', str(self.chroot_base)]
106
)
107
if self.netns is not None:
108
jailer_param_list.extend(['--netns', str(self.netns_file_path())])
109
if self.daemonize:
110
jailer_param_list.append('--daemonize')
111
if self.new_pid_ns:
112
jailer_param_list.append('--new-pid-ns')
113
if self.cgroups is not None:
114
for cgroup in self.cgroups:
115
jailer_param_list.extend(['--cgroup', str(cgroup)])
116
# applying neccessory extra args if needed
117
if len(self.extra_args) > 0:
118
jailer_param_list.append('--')
119
for key, value in self.extra_args.items():
120
jailer_param_list.append('--{}'.format(key))
121
if value is not None:
122
jailer_param_list.append(value)
123
if key == "api-sock":
124
self.api_socket_name = value
125
return jailer_param_list
126
# pylint: enable=too-many-branches
127
128
def chroot_base_with_id(self):
129
"""Return the MicroVM chroot base + MicroVM ID."""
130
return os.path.join(
131
self.chroot_base if self.chroot_base is not None
132
else DEFAULT_CHROOT_PATH,
133
Path(self.exec_file).name,
134
self.jailer_id
135
)
136
137
def api_socket_path(self):
138
"""Return the MicroVM API socket path."""
139
return os.path.join(self.chroot_path(), self.api_socket_name)
140
141
def chroot_path(self):
142
"""Return the MicroVM chroot path."""
143
return os.path.join(self.chroot_base_with_id(), 'root')
144
145
def chroot_ramfs_path(self):
146
"""Return the MicroVM chroot ramfs subfolder path."""
147
return os.path.join(self.chroot_path(), self.ramfs_subdir_name)
148
149
def jailed_path(self, file_path, create=False, create_jail=False):
150
"""Create a hard link or block special device owned by uid:gid.
151
152
Create a hard link or block special device from the specified file,
153
changes the owner to uid:gid, and returns a path to the file which is
154
valid within the jail.
155
"""
156
file_name = os.path.basename(file_path)
157
global_p = os.path.join(self.chroot_path(), file_name)
158
if create_jail:
159
os.makedirs(self.chroot_path(), exist_ok=True)
160
jailed_p = os.path.join("/", file_name)
161
if create:
162
stat_result = os.stat(file_path)
163
if stat.S_ISBLK(stat_result.st_mode):
164
cmd = [
165
'mknod', global_p, 'b',
166
str(os.major(stat_result.st_rdev)),
167
str(os.minor(stat_result.st_rdev))
168
]
169
utils.run_cmd(cmd)
170
else:
171
cmd = 'ln -f {} {}'.format(file_path, global_p)
172
utils.run_cmd(cmd)
173
cmd = 'chown {}:{} {}'.format(self.uid, self.gid, global_p)
174
utils.run_cmd(cmd)
175
return jailed_p
176
177
def copy_into_root(self, file_path, create_jail=False):
178
"""Copy a file in the jail root, owned by uid:gid.
179
180
Copy a file in the jail root, creating the folder path if
181
not existent, then change their owner to uid:gid.
182
"""
183
global_path = os.path.join(
184
self.chroot_path(), file_path.strip(os.path.sep))
185
if create_jail:
186
os.makedirs(self.chroot_path(), exist_ok=True)
187
188
os.makedirs(os.path.dirname(global_path), exist_ok=True)
189
190
shutil.copy(file_path, global_path)
191
192
cmd = 'chown {}:{} {}'.format(
193
self.uid, self.gid, global_path)
194
utils.run_cmd(cmd)
195
196
def netns_file_path(self):
197
"""Get the host netns file path for a jailer context.
198
199
Returns the path on the host to the file which represents the netns,
200
and which must be passed to the jailer as the value of the --netns
201
parameter, when in use.
202
"""
203
if self.netns:
204
return '/var/run/netns/{}'.format(self.netns)
205
return None
206
207
def netns_cmd_prefix(self):
208
"""Return the jailer context netns file prefix."""
209
if self.netns:
210
return 'ip netns exec {} '.format(self.netns)
211
return ''
212
213
def setup(self, use_ramdisk=False):
214
"""Set up this jailer context."""
215
os.makedirs(
216
self.chroot_base if self.chroot_base is not None
217
else DEFAULT_CHROOT_PATH,
218
exist_ok=True
219
)
220
221
if use_ramdisk:
222
self._ramfs_path = self.chroot_ramfs_path()
223
os.makedirs(self._ramfs_path, exist_ok=True)
224
ramdisk_name = 'ramfs-{}'.format(self.jailer_id)
225
utils.run_cmd(
226
'mount -t ramfs -o size=1M {} {}'.format(
227
ramdisk_name, self._ramfs_path
228
)
229
)
230
cmd = 'chown {}:{} {}'.format(
231
self.uid, self.gid, self._ramfs_path
232
)
233
utils.run_cmd(cmd)
234
235
if self.netns:
236
utils.run_cmd('ip netns add {}'.format(self.netns))
237
238
def cleanup(self):
239
"""Clean up this jailer context."""
240
# pylint: disable=subprocess-run-check
241
if self._ramfs_path:
242
utils.run_cmd(
243
'umount {}'.format(self._ramfs_path), ignore_return_code=True
244
)
245
246
if self.jailer_id is not None:
247
shutil.rmtree(self.chroot_base_with_id(), ignore_errors=True)
248
249
if self.netns \
250
and os.path.exists("/var/run/netns/{}".format(self.netns)):
251
utils.run_cmd('ip netns del {}'.format(self.netns))
252
253
# Remove the cgroup folders associated with this microvm.
254
# The base /sys/fs/cgroup/<controller>/firecracker folder will remain,
255
# because we can't remove it unless we're sure there's no other running
256
# microVM.
257
258
if self.cgroups:
259
controllers = set()
260
261
# Extract the controller for every cgroup that needs to be set.
262
for cgroup in self.cgroups:
263
controllers.add(cgroup.split('.')[0])
264
265
for controller in controllers:
266
# Obtain the tasks from each cgroup and wait on them before
267
# removing the microvm's associated cgroup folder.
268
try:
269
retry_call(
270
f=self._kill_cgroup_tasks,
271
fargs=[controller],
272
exceptions=TimeoutError,
273
max_delay=5
274
)
275
except TimeoutError:
276
pass
277
278
# Remove cgroups and sub cgroups.
279
back_cmd = r'-depth -type d -exec rmdir {} \;'
280
cmd = 'find /sys/fs/cgroup/{}/{}/{} {}'.format(
281
controller,
282
FC_BINARY_NAME,
283
self.jailer_id,
284
back_cmd
285
)
286
# We do not need to know if it succeeded or not; afterall,
287
# we are trying to clean up resources created by the jailer
288
# itself not the testing system.
289
utils.run_cmd(cmd, ignore_return_code=True)
290
291
def _kill_cgroup_tasks(self, controller):
292
"""Simulate wait on pid.
293
294
Read the tasks file and stay there until /proc/{pid}
295
disappears. The retry function that calls this code makes
296
sure we do not timeout.
297
"""
298
# pylint: disable=subprocess-run-check
299
tasks_file = '/sys/fs/cgroup/{}/{}/{}/tasks'.format(
300
controller,
301
FC_BINARY_NAME,
302
self.jailer_id
303
)
304
305
# If tests do not call start on machines, the cgroups will not be
306
# created.
307
if not os.path.exists(tasks_file):
308
return True
309
310
cmd = 'cat {}'.format(tasks_file)
311
result = utils.run_cmd(cmd)
312
313
tasks_split = result.stdout.splitlines()
314
for task in tasks_split:
315
if os.path.exists("/proc/{}".format(task)):
316
raise TimeoutError
317
return True
318
319