Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aos
GitHub Repository: aos/firecracker
Path: blob/main/tests/framework/builder.py
1956 views
1
# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
# SPDX-License-Identifier: Apache-2.0
3
"""Define some helpers methods to create microvms from artifacts."""
4
5
import json
6
import os
7
import shutil
8
import tempfile
9
from pathlib import Path
10
from conftest import init_microvm, _test_images_s3_bucket
11
from framework.defs import DEFAULT_TEST_SESSION_ROOT_PATH
12
from framework.artifacts import (
13
ArtifactCollection, Artifact, DiskArtifact, Snapshot,
14
SnapshotType, NetIfaceConfig
15
)
16
import framework.utils as utils
17
import host_tools.logging as log_tools
18
import host_tools.network as net_tools
19
20
21
class VmInstance:
22
"""A class that describes a microvm instance resources."""
23
24
def __init__(self, config, kernel, disks, ssh_key, vm):
25
"""Initialize a Vm configuration based on artifacts."""
26
self._config = config
27
self._kernel = kernel
28
self._disks = disks
29
self._ssh_key = ssh_key
30
self._vm = vm
31
32
@property
33
def config(self):
34
"""Return machine config artifact."""
35
return self._config
36
37
@property
38
def kernel(self):
39
"""Return the kernel artifact."""
40
return self._kernel
41
42
@property
43
def disks(self):
44
"""Return an array of block file paths."""
45
return self._disks
46
47
@property
48
def ssh_key(self):
49
"""Return ssh key artifact linked to the root block device."""
50
return self._ssh_key
51
52
@property
53
def vm(self):
54
"""Return the Microvm object instance."""
55
return self._vm
56
57
58
class MicrovmBuilder:
59
"""Build fresh microvms or restore from snapshot."""
60
61
ROOT_PREFIX = "fctest-"
62
63
_root_path = None
64
65
def __init__(self, bin_cloner_path):
66
"""Initialize microvm root and cloning binary."""
67
self.bin_cloner_path = bin_cloner_path
68
self.init_root_path()
69
70
@property
71
def root_path(self):
72
"""Return the root path of the microvm."""
73
return self._root_path
74
75
def init_root_path(self):
76
"""Initialize microvm root path."""
77
self._root_path = tempfile.mkdtemp(
78
prefix=MicrovmBuilder.ROOT_PREFIX,
79
dir=f"{DEFAULT_TEST_SESSION_ROOT_PATH}")
80
81
def build(self,
82
kernel: Artifact,
83
disks: [DiskArtifact],
84
ssh_key: Artifact,
85
config: Artifact,
86
net_ifaces=None,
87
diff_snapshots=False,
88
cpu_template=None,
89
fc_binary=None,
90
jailer_binary=None,
91
use_ramdisk=False):
92
"""Build a fresh microvm."""
93
vm = init_microvm(self.root_path, self.bin_cloner_path,
94
fc_binary, jailer_binary)
95
96
# Start firecracker.
97
vm.spawn(use_ramdisk=use_ramdisk)
98
99
# Link the microvm to kernel, rootfs, ssh_key artifacts.
100
vm.kernel_file = kernel.local_path()
101
vm.rootfs_file = disks[0].local_path()
102
# copy rootfs to ramdisk if needed
103
jailed_rootfs_path = vm.copy_to_jail_ramfs(vm.rootfs_file) if \
104
use_ramdisk else vm.create_jailed_resource(vm.rootfs_file)
105
106
# Download ssh key into the microvm root.
107
ssh_key.download(vm.path)
108
vm.ssh_config['ssh_key_path'] = ssh_key.local_path()
109
os.chmod(vm.ssh_config['ssh_key_path'], 0o400)
110
111
# Provide a default network configuration.
112
if net_ifaces is None or len(net_ifaces) == 0:
113
ifaces = [NetIfaceConfig()]
114
else:
115
ifaces = net_ifaces
116
117
# Configure network interfaces using artifacts.
118
for iface in ifaces:
119
vm.create_tap_and_ssh_config(host_ip=iface.host_ip,
120
guest_ip=iface.guest_ip,
121
netmask_len=iface.netmask,
122
tapname=iface.tap_name)
123
guest_mac = net_tools.mac_from_ip(iface.guest_ip)
124
response = vm.network.put(
125
iface_id=iface.dev_name,
126
host_dev_name=iface.tap_name,
127
guest_mac=guest_mac,
128
allow_mmds_requests=True,
129
)
130
assert vm.api_session.is_status_no_content(response.status_code)
131
132
with open(config.local_path()) as microvm_config_file:
133
microvm_config = json.load(microvm_config_file)
134
135
response = vm.basic_config(
136
add_root_device=False,
137
boot_args='console=ttyS0 reboot=k panic=1'
138
)
139
140
# Add the root file system with rw permissions.
141
response = vm.drive.put(
142
drive_id='rootfs',
143
path_on_host=jailed_rootfs_path,
144
is_root_device=True,
145
is_read_only=False
146
)
147
assert vm.api_session \
148
.is_status_no_content(response.status_code), \
149
response.text
150
151
# Apply the microvm artifact configuration and template.
152
response = vm.machine_cfg.put(
153
vcpu_count=int(microvm_config['vcpu_count']),
154
mem_size_mib=int(microvm_config['mem_size_mib']),
155
ht_enabled=bool(microvm_config['ht_enabled']),
156
track_dirty_pages=diff_snapshots,
157
cpu_template=cpu_template,
158
)
159
assert vm.api_session.is_status_no_content(response.status_code)
160
161
vm.vcpus_count = int(microvm_config['vcpu_count'])
162
163
return VmInstance(config, kernel, disks, ssh_key, vm)
164
165
# This function currently returns the vm and a metrics_fifo which
166
# is needed by the performance integration tests.
167
# TODO: Move all metrics functionality to microvm (encapsulating the fifo)
168
# so we do not need to move it around polluting the code.
169
def build_from_snapshot(self,
170
snapshot: Snapshot,
171
resume=False,
172
# Enable incremental snapshot capability.
173
diff_snapshots=False,
174
use_ramdisk=False,
175
fc_binary=None, jailer_binary=None):
176
"""Build a microvm from a snapshot artifact."""
177
vm = init_microvm(self.root_path, self.bin_cloner_path,
178
fc_binary, jailer_binary,)
179
vm.spawn(log_level='Error', use_ramdisk=use_ramdisk)
180
vm.api_session.untime()
181
182
metrics_file_path = os.path.join(vm.path, 'metrics.log')
183
metrics_fifo = log_tools.Fifo(metrics_file_path)
184
response = vm.metrics.put(
185
metrics_path=vm.create_jailed_resource(metrics_fifo.path)
186
)
187
assert vm.api_session.is_status_no_content(response.status_code)
188
189
# Hardlink all the snapshot files into the microvm jail.
190
jailed_mem = vm.copy_to_jail_ramfs(snapshot.mem) if use_ramdisk else \
191
vm.create_jailed_resource(snapshot.mem)
192
jailed_vmstate = vm.copy_to_jail_ramfs(snapshot.vmstate) \
193
if use_ramdisk else vm.create_jailed_resource(snapshot.vmstate)
194
195
assert len(snapshot.disks) > 0, "Snapshot requires at least one disk."
196
_jailed_disks = []
197
for disk in snapshot.disks:
198
_jailed_disks.append(vm.copy_to_jail_ramfs(disk) if use_ramdisk
199
else vm.create_jailed_resource(disk))
200
201
vm.ssh_config['ssh_key_path'] = snapshot.ssh_key.local_path()
202
203
# Create network interfaces.
204
for iface in snapshot.net_ifaces:
205
vm.create_tap_and_ssh_config(host_ip=iface.host_ip,
206
guest_ip=iface.guest_ip,
207
netmask_len=iface.netmask,
208
tapname=iface.tap_name)
209
response = vm.snapshot.load(mem_file_path=jailed_mem,
210
snapshot_path=jailed_vmstate,
211
diff=diff_snapshots,
212
resume=resume)
213
status_ok = vm.api_session.is_status_no_content(response.status_code)
214
215
# Verify response status and cleanup if needed before assert.
216
if not status_ok:
217
# Destroy VM here before we assert.
218
vm.kill()
219
del vm
220
221
assert status_ok, response.text
222
223
# Return a resumed microvm.
224
return vm, metrics_fifo
225
226
def build_from_artifacts(self, config,
227
kernel, disks, cpu_template,
228
net_ifaces=None, diff_snapshots=False,
229
fc_binary=None, jailer_binary=None):
230
"""Spawns a new Firecracker and applies specified config."""
231
artifacts = ArtifactCollection(_test_images_s3_bucket())
232
# Pick the first artifact in the set.
233
config = artifacts.microvms(keyword=config)[0]
234
kernel = artifacts.kernels(keyword=kernel)[0]
235
disks = artifacts.disks(keyword=disks)
236
config.download()
237
kernel.download()
238
attached_disks = []
239
for disk in disks:
240
disk.download()
241
attached_disks.append(disk.copy())
242
243
# SSH key is attached to root disk artifact.
244
# Builder will download ssh key in the VM root.
245
ssh_key = disks[0].ssh_key()
246
# Create a fresh microvm from artifacts.
247
return self.build(kernel=kernel,
248
disks=attached_disks,
249
ssh_key=ssh_key,
250
config=config,
251
net_ifaces=net_ifaces,
252
diff_snapshots=diff_snapshots,
253
cpu_template=cpu_template,
254
fc_binary=fc_binary,
255
jailer_binary=jailer_binary)
256
257
def build_vm_nano(self, fc_binary=None, jailer_binary=None,
258
net_ifaces=None, diff_snapshots=False):
259
"""Create a clean VM in an initial state."""
260
return self.build_from_artifacts("2vcpu_256mb",
261
"vmlinux-4.14",
262
"ubuntu-18.04",
263
None,
264
net_ifaces=net_ifaces,
265
diff_snapshots=diff_snapshots,
266
fc_binary=fc_binary,
267
jailer_binary=jailer_binary)
268
269
def build_vm_micro(self, fc_binary=None, jailer_binary=None,
270
net_ifaces=None, diff_snapshots=False):
271
"""Create a clean VM in an initial state."""
272
return self.build_from_artifacts("2vcpu_512mb",
273
"vmlinux-4.14",
274
"ubuntu-18.04",
275
None,
276
net_ifaces=net_ifaces,
277
diff_snapshots=diff_snapshots,
278
fc_binary=fc_binary,
279
jailer_binary=jailer_binary)
280
281
def cleanup(self):
282
"""Clean up this builder context."""
283
if self._root_path:
284
shutil.rmtree(self._root_path, ignore_errors=True)
285
286
def __del__(self):
287
"""Teardown the object."""
288
self.cleanup()
289
290
291
class SnapshotBuilder: # pylint: disable=too-few-public-methods
292
"""Create a snapshot from a running microvm."""
293
294
def __init__(self, microvm):
295
"""Initialize the snapshot builder."""
296
self._microvm = microvm
297
298
def create_snapshot_dir(self):
299
"""Create dir and files for saving snapshot state and memory."""
300
chroot_path = self._microvm.jailer.chroot_path()
301
snapshot_dir = os.path.join(chroot_path, "snapshot")
302
Path(snapshot_dir).mkdir(parents=True, exist_ok=True)
303
cmd = 'chown {}:{} {}'.format(self._microvm.jailer.uid,
304
self._microvm.jailer.gid,
305
snapshot_dir)
306
utils.run_cmd(cmd)
307
return snapshot_dir
308
309
def create(self,
310
disks,
311
ssh_key: Artifact,
312
snapshot_type: SnapshotType = SnapshotType.FULL,
313
target_version: str = None,
314
mem_file_name: str = "vm.mem",
315
snapshot_name: str = "vm.vmstate",
316
net_ifaces=None,
317
use_ramdisk=False):
318
"""Create a Snapshot object from a microvm and artifacts."""
319
if use_ramdisk:
320
snaps_dir = self._microvm.jailer.chroot_ramfs_path()
321
mem_full_path = os.path.join(snaps_dir, mem_file_name)
322
vmstate_full_path = os.path.join(snaps_dir, snapshot_name)
323
324
memsize = self._microvm.machine_cfg.configuration['mem_size_mib']
325
# Pre-allocate ram for memfile to eliminate allocation variability.
326
utils.run_cmd('dd if=/dev/zero of={} bs=1M count={}'.format(
327
mem_full_path, memsize
328
))
329
cmd = 'chown {}:{} {}'.format(
330
self._microvm.jailer.uid,
331
self._microvm.jailer.gid,
332
mem_full_path
333
)
334
utils.run_cmd(cmd)
335
else:
336
snaps_dir = self.create_snapshot_dir()
337
mem_full_path = os.path.join(snaps_dir, mem_file_name)
338
vmstate_full_path = os.path.join(snaps_dir, snapshot_name)
339
340
snaps_dir_name = os.path.basename(snaps_dir)
341
self._microvm.pause_to_snapshot(
342
mem_file_path=os.path.join('/', snaps_dir_name, mem_file_name),
343
snapshot_path=os.path.join('/', snaps_dir_name, snapshot_name),
344
diff=snapshot_type == SnapshotType.DIFF,
345
version=target_version)
346
347
# Create a copy of the ssh_key artifact.
348
ssh_key_copy = ssh_key.copy()
349
return Snapshot(mem=mem_full_path,
350
vmstate=vmstate_full_path,
351
# TODO: To support more disks we need to figure out a
352
# simple and flexible way to store snapshot artifacts
353
# in S3. This should be done in a PR where we add tests
354
# that resume from S3 snapshot artifacts.
355
disks=disks,
356
net_ifaces=net_ifaces or [NetIfaceConfig()],
357
ssh_key=ssh_key_copy)
358
359