Skip to content

Commit 0808731

Browse files
committed
tests: microvm: consolidate microvm paths and chroot
Currently the dir structure looks like: /srv/.../microvm-dir/chroot and we tend to create files in microvm-dir and then hardlink them into the chroot dir. While we are simplifying it, also do away with chroot/path/fsfiles and convert more strings to `Path`. Signed-off-by: Pablo Barbáchano <[email protected]>
1 parent 2f92299 commit 0808731

24 files changed

+185
-279
lines changed

tests/README.md

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -349,25 +349,6 @@ pass for the PR to be merged.
349349
Add a new function annotated with `#[test]` in
350350
[`integration_tests.rs`](../src/vmm/tests/integration_tests.rs).
351351
352-
## Working With Guest Files
353-
354-
There are helper methods for writing to and reading from a guest filesystem. For
355-
example, to overwrite the guest init process and later extract a log:
356-
357-
```python
358-
def test_with_any_microvm_and_my_init(test_microvm_any):
359-
# [...]
360-
test_microvm_any.slot.fsfiles['mounted_root_fs'].copy_to(my_init, 'sbin/')
361-
# [...]
362-
test_microvm_any.slot.fsfiles['mounted_root_fs'].copy_from('logs/', 'log')
363-
```
364-
365-
`copy_to()` source paths are relative to the host root and destination paths are
366-
relative to the `mounted_root_fs` root. Vice versa for `copy_from()`.
367-
368-
Copying files to/from a guest file system while the guest is running results in
369-
undefined behavior.
370-
371352
## Example Manual Testrun
372353
373354
Running on an EC2 `.metal` instance with an `Amazon Linux 2` AMI:

tests/framework/http_api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ class Api:
9797

9898
def __init__(self, api_usocket_full_name):
9999
self.socket = api_usocket_full_name
100-
url_encoded_path = urllib.parse.quote_plus(api_usocket_full_name)
100+
url_encoded_path = urllib.parse.quote_plus(str(self.socket))
101101
self.endpoint = DEFAULT_SCHEME + url_encoded_path
102102
self.session = Session()
103103

tests/framework/jailer.py

Lines changed: 20 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,6 @@ class JailerContext:
2525
Each microvm will have a jailer configuration associated with it.
2626
"""
2727

28-
# Keep in sync with parameters from code base.
29-
jailer_id = None
30-
exec_file = None
31-
uid = None
32-
gid = None
33-
chroot_base = None
34-
daemonize = None
35-
new_pid_ns = None
36-
extra_args = None
37-
api_socket_name = None
38-
cgroups = None
39-
resource_limits = None
40-
cgroup_ver = None
41-
parent_cgroup = None
42-
4328
def __init__(
4429
self,
4530
jailer_id,
@@ -64,11 +49,12 @@ def __init__(
6449
further adjusted by each test even with None values.
6550
"""
6651
self.jailer_id = jailer_id
67-
assert jailer_id is not None
52+
assert jailer_id
6853
self.jailer_bin_path = jailer_binary_path
6954
self.exec_file = exec_file
7055
self.uid = uid
7156
self.gid = gid
57+
assert chroot_base is not None
7258
self.chroot_base = Path(chroot_base)
7359
self.netns = netns
7460
self.daemonize = daemonize
@@ -79,7 +65,6 @@ def __init__(
7965
self.resource_limits = resource_limits
8066
self.cgroup_ver = cgroup_ver
8167
self.parent_cgroup = parent_cgroup
82-
assert chroot_base is not None
8368

8469
# Disabling 'too-many-branches' warning for this function as it needs to
8570
# check every argument, so the number of branches will increase
@@ -95,8 +80,7 @@ def construct_param_list(self):
9580
jailer_param_list = [str(self.jailer_bin_path)]
9681

9782
# Pretty please, try to keep the same order as in the code base.
98-
if self.jailer_id is not None:
99-
jailer_param_list.extend(["--id", str(self.jailer_id)])
83+
jailer_param_list.extend(["--id", str(self.jailer_id)])
10084
if self.exec_file is not None:
10185
jailer_param_list.extend(["--exec-file", str(self.exec_file)])
10286
if self.uid is not None:
@@ -134,17 +118,14 @@ def construct_param_list(self):
134118

135119
# pylint: enable=too-many-branches
136120

137-
def chroot_base_with_id(self):
138-
"""Return the MicroVM chroot base + MicroVM ID."""
139-
return self.chroot_base / Path(self.exec_file).name / self.jailer_id
121+
@property
122+
def chroot(self):
123+
"""Return where the jailer will place the chroot"""
124+
return self.chroot_base / self.exec_file.name / self.jailer_id / "root"
140125

141126
def api_socket_path(self):
142127
"""Return the MicroVM API socket path."""
143-
return os.path.join(self.chroot_path(), self.api_socket_name)
144-
145-
def chroot_path(self):
146-
"""Return the MicroVM chroot path."""
147-
return self.chroot_base_with_id() / "root"
128+
return self.chroot / self.api_socket_name
148129

149130
def jailed_path(self, file_path, create=False, subdir="."):
150131
"""Create a hard link or block special device owned by uid:gid.
@@ -154,17 +135,16 @@ def jailed_path(self, file_path, create=False, subdir="."):
154135
valid within the jail.
155136
"""
156137
file_path = Path(file_path)
157-
chroot_path = Path(self.chroot_path())
158-
global_p = chroot_path / subdir / file_path.name
138+
global_p = self.chroot / subdir / file_path.name
159139
global_p.parent.mkdir(parents=True, exist_ok=True)
160140
jailed_p = Path("/") / subdir / file_path.name
161-
if create:
141+
if create and not global_p.exists():
162142
stat_src = file_path.stat()
163143
if file_path.is_block_device():
164144
perms = stat.S_IRUSR | stat.S_IWUSR
165145
os.mknod(global_p, mode=stat.S_IFBLK | perms, device=stat_src.st_rdev)
166146
else:
167-
stat_dst = chroot_path.stat()
147+
stat_dst = self.chroot.stat()
168148
if stat_src.st_dev == stat_dst.st_dev:
169149
# if they are in the same device, hardlink
170150
global_p.unlink(missing_ok=True)
@@ -173,14 +153,14 @@ def jailed_path(self, file_path, create=False, subdir="."):
173153
# otherwise, copy
174154
shutil.copyfile(file_path, global_p)
175155

176-
os.chown(global_p, self.uid, self.gid)
156+
os.chown(global_p, self.uid, self.gid)
177157
return str(jailed_p)
178158

179159
def setup(self):
180160
"""Set up this jailer context."""
181-
os.makedirs(self.chroot_base, exist_ok=True)
161+
os.makedirs(self.chroot, exist_ok=True)
182162
# Copy the /etc/localtime file in the jailer root
183-
self.jailed_path("/etc/localtime", subdir="etc")
163+
self.jailed_path("/etc/localtime", create=True, subdir="etc")
184164

185165
def cleanup(self):
186166
"""Clean up this jailer context."""
@@ -228,30 +208,25 @@ def _kill_cgroup_tasks(self, controller):
228208
disappears. The retry function that calls this code makes
229209
sure we do not timeout.
230210
"""
231-
# pylint: disable=subprocess-run-check
232-
tasks_file = "/sys/fs/cgroup/{}/{}/{}/tasks".format(
233-
controller, FC_BINARY_NAME, self.jailer_id
211+
tasks_file = Path(
212+
f"/sys/fs/cgroup/{controller}/{FC_BINARY_NAME}/{self.jailer_id}/tasks"
234213
)
235214

236215
# If tests do not call start on machines, the cgroups will not be
237216
# created.
238-
if not os.path.exists(tasks_file):
217+
if not tasks_file.exists():
239218
return True
240219

241-
cmd = "cat {}".format(tasks_file)
242-
result = utils.check_output(cmd)
243-
244-
tasks_split = result.stdout.splitlines()
245-
for task in tasks_split:
246-
if os.path.exists("/proc/{}".format(task)):
220+
for task in tasks_file.read_text(encoding="ascii").splitlines():
221+
if Path(f"/proc/{task}").exists():
247222
raise TimeoutError
248223
return True
249224

250225
@property
251226
def pid(self):
252227
"""Return the PID of the jailed process"""
253228
# Read the PID stored inside the file.
254-
pid_file = Path(self.chroot_path()) / (self.exec_file.name + ".pid")
229+
pid_file = self.chroot / (self.exec_file.name + ".pid")
255230
if not pid_file.exists():
256231
return None
257232
return int(pid_file.read_text(encoding="ascii"))

tests/framework/microvm.py

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -380,14 +380,6 @@ def firecracker_version(self):
380380
_, stdout, _ = utils.check_output(f"{self.fc_binary_path} --version")
381381
return re.match(r"^Firecracker v(.+)", stdout.partition("\n")[0]).group(1)
382382

383-
@property
384-
def path(self):
385-
"""Return the path on disk used that represents this microVM."""
386-
return self.jailer.chroot_base_with_id()
387-
388-
# some functions use this
389-
fsfiles = path
390-
391383
@property
392384
def id(self):
393385
"""Return the unique identifier of this microVM."""
@@ -462,14 +454,16 @@ def create_jailed_resource(self, path):
462454
"""Create a hard link to some resource inside this microvm."""
463455
return self.jailer.jailed_path(path, create=True)
464456

465-
def get_jailed_resource(self, path):
466-
"""Get the relative jailed path to a resource."""
457+
def jail_path(self, path):
458+
"""Get the relative jailed path to a resource.
459+
460+
Also fix permissions if needed"""
467461
return self.jailer.jailed_path(path, create=False)
468462

469463
@property
470464
def chroot(self):
471465
"""Get the chroot of this microVM."""
472-
return self.jailer.chroot_path()
466+
return self.jailer.chroot
473467

474468
def pin_vmm(self, cpu_id: int) -> bool:
475469
"""Pin the firecracker process VMM thread to a cpu list."""
@@ -544,23 +538,24 @@ def spawn(
544538
self.api = Api(self.jailer.api_socket_path())
545539

546540
if log_file is not None:
547-
self.log_file = Path(self.path) / log_file
541+
self.log_file = self.chroot / log_file
548542
self.log_file.touch()
549-
self.create_jailed_resource(self.log_file)
550543
# The default value for `level`, when configuring the logger via cmd
551544
# line, is `Info`. We set the level to `Debug` to also have the boot
552545
# time printed in the log.
553-
self.jailer.extra_args.update({"log-path": log_file, "level": log_level})
546+
self.jailer.extra_args["log-path"] = self.jail_path(log_file)
547+
self.jailer.extra_args["level"] = log_level
554548
if log_show_level:
555549
self.jailer.extra_args["show-level"] = None
556550
if log_show_origin:
557551
self.jailer.extra_args["show-log-origin"] = None
558552

559553
if metrics_path is not None:
560-
self.metrics_file = Path(self.path) / metrics_path
554+
self.metrics_file = self.chroot / metrics_path
561555
self.metrics_file.touch()
562-
self.create_jailed_resource(self.metrics_file)
563-
self.jailer.extra_args.update({"metrics-path": self.metrics_file.name})
556+
self.jailer.extra_args["metrics-path"] = self.jail_path(
557+
self.metrics_file.name
558+
)
564559
else:
565560
assert not emit_metrics
566561

@@ -784,9 +779,9 @@ def patch_drive(self, drive_id, file=None):
784779
if file:
785780
self.api.drive.patch(
786781
drive_id=drive_id,
787-
path_on_host=self.create_jailed_resource(file.path),
782+
path_on_host=self.create_jailed_resource(file),
788783
)
789-
self.disks[drive_id] = Path(file.path)
784+
self.disks[drive_id] = Path(file)
790785
else:
791786
self.api.drive.patch(drive_id=drive_id)
792787

@@ -942,7 +937,7 @@ def ssh_iface(self, iface_idx=0):
942937
ssh_key=self.ssh_key,
943938
user="root",
944939
host=guest_ip,
945-
control_path=Path(self.chroot()) / f"ssh-{iface_idx}.sock",
940+
control_path=self.chroot / f"ssh-{iface_idx}.sock",
946941
on_error=self._dump_debug_information,
947942
)
948943
self._connections.append(connection)
@@ -1031,7 +1026,7 @@ def build(self, kernel=None, rootfs=None, **kwargs):
10311026
# copy only iff not a read-only rootfs
10321027
rootfs_path = rootfs
10331028
if rootfs_path.suffix != ".squashfs":
1034-
rootfs_path = Path(vm.path) / rootfs.name
1029+
rootfs_path = vm.chroot / rootfs.name
10351030
shutil.copyfile(rootfs, rootfs_path)
10361031
vm.rootfs_file = rootfs_path
10371032
vm.ssh_key = ssh_key
@@ -1049,9 +1044,9 @@ def kill(self):
10491044
for vm in self.vms:
10501045
vm.kill()
10511046
vm.jailer.cleanup()
1052-
chroot_base_with_id = vm.jailer.chroot_base_with_id()
1053-
if len(vm.jailer.jailer_id) > 0 and chroot_base_with_id.exists():
1054-
shutil.rmtree(chroot_base_with_id)
1047+
chroot = vm.jailer.chroot
1048+
if len(vm.jailer.jailer_id) > 0 and chroot.exists():
1049+
shutil.rmtree(chroot)
10551050
vm.netns.cleanup()
10561051

10571052
self.vms.clear()

tests/framework/utils_vsock.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def start_guest_echo_server(vm):
107107
# Give the server time to initialise
108108
time.sleep(1)
109109

110-
return os.path.join(vm.jailer.chroot_path(), VSOCK_UDS_PATH)
110+
return vm.chroot / VSOCK_UDS_PATH
111111

112112

113113
def check_host_connections(uds_path, blob_path, blob_hash):
@@ -156,7 +156,7 @@ def check_guest_connections(vm, server_port_path, blob_path, blob_hash):
156156

157157
# Link the listening Unix socket into the VM's jail, so that
158158
# Firecracker can connect to it.
159-
vm.create_jailed_resource(server_port_path)
159+
vm.jail_path(server_port_path)
160160

161161
# Increase maximum process count for the ssh service.
162162
# Avoids: "bash: fork: retry: Resource temporarily unavailable"
@@ -205,7 +205,7 @@ def make_host_port_path(uds_path, port):
205205
def _vsock_connect_to_guest(uds_path, port):
206206
"""Return a Unix socket, connected to the guest vsock port `port`."""
207207
sock = socket(AF_UNIX, SOCK_STREAM)
208-
sock.connect(uds_path)
208+
sock.connect(str(uds_path))
209209

210210
buf = bytearray("CONNECT {}\n".format(port).encode("utf-8"))
211211
sock.send(buf)
@@ -238,7 +238,7 @@ def check_vsock_device(vm, bin_vsock_path, test_fc_session_root_path, ssh_connec
238238
_copy_vsock_data_to_guest(ssh_connection, blob_path, vm_blob_path, bin_vsock_path)
239239

240240
# Test vsock guest-initiated connections.
241-
path = os.path.join(vm.path, make_host_port_path(VSOCK_UDS_PATH, ECHO_SERVER_PORT))
241+
path = vm.chroot / make_host_port_path(VSOCK_UDS_PATH, ECHO_SERVER_PORT)
242242
check_guest_connections(vm, path, vm_blob_path, blob_hash)
243243

244244
# Test vsock host-initiated connections.

tests/host_tools/drive.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def __init__(self, path: str = None, size: int = 256, fs_format: str = "ext4"):
3131
if fs_format not in self.KNOWN_FILEFS_FORMATS:
3232
raise ValueError("Format not in: + " + str(self.KNOWN_FILEFS_FORMATS))
3333
# Here we append the format as a
34-
path = os.path.join(path + "." + fs_format)
34+
path = os.path.join(str(path) + "." + fs_format)
3535

3636
if os.path.isfile(path):
3737
raise FileExistsError("File already exists: " + path)

0 commit comments

Comments
 (0)