Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aos
GitHub Repository: aos/firecracker
Path: blob/main/tests/framework/artifacts.py
1956 views
1
# Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
# SPDX-License-Identifier: Apache-2.0
3
"""Define classes for interacting with CI artifacts in s3."""
4
5
import os
6
import platform
7
import tempfile
8
from shutil import copyfile
9
from enum import Enum
10
from stat import S_IREAD, S_IWRITE
11
from pathlib import Path
12
import boto3
13
import botocore.client
14
from framework.defs import DEFAULT_TEST_SESSION_ROOT_PATH
15
from framework.utils import compare_versions
16
from host_tools.snapshot_helper import merge_memory_bitmaps
17
18
19
ARTIFACTS_LOCAL_ROOT = f"{DEFAULT_TEST_SESSION_ROOT_PATH}/ci-artifacts"
20
21
22
class ArtifactType(Enum):
23
"""Supported artifact types."""
24
25
MICROVM = "microvm"
26
KERNEL = "kernel"
27
DISK = "disk"
28
SSH_KEY = "ssh_key"
29
MEM = "mem"
30
VMSTATE = "vmstate"
31
MISC = "misc"
32
SNAPSHOT = "snapshot"
33
FC = "firecracker"
34
JAILER = "jailer"
35
36
37
class Artifact:
38
"""A generic read-only artifact manipulation class."""
39
40
LOCAL_ARTIFACT_DIR = f"{DEFAULT_TEST_SESSION_ROOT_PATH}/local-artifacts"
41
42
def __init__(self,
43
bucket,
44
key,
45
artifact_type=ArtifactType.MISC,
46
local_folder=None,
47
is_copy=False):
48
"""Initialize bucket, key and type."""
49
self._bucket = bucket
50
self._key = key
51
self._local_folder = local_folder
52
self._type = artifact_type
53
self._is_copy = is_copy
54
55
@property
56
def type(self):
57
"""Return the artifact type."""
58
return self._type
59
60
@property
61
def key(self):
62
"""Return the artifact key."""
63
return self._key
64
65
@property
66
def bucket(self):
67
"""Return the artifact bucket."""
68
return self._bucket
69
70
def name(self):
71
"""Return the artifact name."""
72
return Path(self.key).name
73
74
def base_name(self):
75
"""Return the base name (without extension)."""
76
return Path(self.key).stem
77
78
def local_dir(self):
79
"""Return the directory containing the downloaded artifact."""
80
assert self._local_folder is not None
81
return "{}/{}/{}".format(
82
self._local_folder,
83
self.type.value,
84
platform.machine(),
85
)
86
87
def download(self, target_folder=ARTIFACTS_LOCAL_ROOT, force=False):
88
"""Save the artifact in the folder specified target_path."""
89
assert self.bucket is not None
90
self._local_folder = target_folder
91
Path(self.local_dir()).mkdir(parents=True, exist_ok=True)
92
if force or not os.path.exists(self.local_path()):
93
self._bucket.download_file(self._key, self.local_path())
94
# Artifacts are read only by design.
95
os.chmod(self.local_path(), S_IREAD)
96
97
def local_path(self):
98
"""Return the local path where the file was downloaded."""
99
# The file path format: <target_folder>/<type>/<platform>/<name>
100
return "{}/{}".format(
101
self.local_dir(),
102
self.name()
103
)
104
105
def copy(self, file_name=None):
106
"""Create a writeable copy of the artifact."""
107
assert os.path.exists(self.local_path()), """File {} not found.
108
call download() first.""".format(self.local_path())
109
110
# The file path for this artifact copy.
111
new_dir = "{}/{}/{}".format(
112
Artifact.LOCAL_ARTIFACT_DIR,
113
self.type.value,
114
platform.machine()
115
)
116
117
if file_name is None:
118
# The temp file suffix is the artifact type.
119
suffix = "-{}".format(self.type.value)
120
# The key for the new artifact is the full path to the file.
121
new_key = tempfile.mktemp(dir=new_dir, suffix=suffix)
122
else:
123
# Caller specified new name.
124
new_key = os.path.join(new_dir, file_name)
125
126
# Create directories if needed.
127
Path(new_dir).mkdir(parents=True, exist_ok=True)
128
# Copy to local artifact.
129
copyfile(self.local_path(), new_key)
130
# Make it writeable.
131
os.chmod(new_key, S_IREAD | S_IWRITE)
132
# Local folder of the new artifact.
133
local_folder = Artifact.LOCAL_ARTIFACT_DIR
134
# Calls to download() on the new Artifact are guarded by a
135
# bucket assert.
136
return Artifact(None, new_key,
137
artifact_type=self.type,
138
local_folder=local_folder, is_copy=True)
139
140
def __del__(self):
141
"""Teardown the object."""
142
if self._is_copy and os.path.exists(self._key):
143
os.remove(self._key)
144
145
146
class SnapshotArtifact:
147
"""Manages snapshot S3 artifact objects."""
148
149
def __init__(self,
150
bucket,
151
key,
152
artifact_type=ArtifactType.SNAPSHOT):
153
"""Initialize bucket, key and type."""
154
self._bucket = bucket
155
self._type = artifact_type
156
self._key = key
157
158
self._mem = Artifact(self._bucket, "{}vm.mem".format(key),
159
artifact_type=ArtifactType.MISC)
160
self._vmstate = Artifact(self._bucket, "{}vm.vmstate".format(key),
161
artifact_type=ArtifactType.MISC)
162
self._ssh_key = Artifact(self._bucket, "{}ssh_key".format(key),
163
artifact_type=ArtifactType.SSH_KEY)
164
self._disks = []
165
166
disk_prefix = "{}disk".format(key)
167
snaphot_disks = self._bucket.objects.filter(Prefix=disk_prefix)
168
169
for disk in snaphot_disks:
170
artifact = Artifact(self._bucket, disk.key,
171
artifact_type=ArtifactType.DISK)
172
self._disks.append(artifact)
173
174
# Get the name of the snapshot folder.
175
snapshot_name = self.name
176
self._local_folder = os.path.join(ARTIFACTS_LOCAL_ROOT,
177
self.type.value,
178
snapshot_name)
179
180
@property
181
def type(self):
182
"""Return the artifact type."""
183
return self._type
184
185
@property
186
def key(self):
187
"""Return the artifact key."""
188
return self._key
189
190
@property
191
def mem(self):
192
"""Return the memory artifact."""
193
return self._mem
194
195
@property
196
def vmstate(self):
197
"""Return the vmstate artifact."""
198
return self._vmstate
199
200
@property
201
def ssh_key(self):
202
"""Return the vmstate artifact."""
203
return self._ssh_key
204
205
@property
206
def disks(self):
207
"""Return the disk artifacts."""
208
return self._disks
209
210
@property
211
def name(self):
212
"""Return the name of the artifact."""
213
return self._key.strip('/').split('/')[-1]
214
215
def download(self):
216
"""Download artifacts and return a Snapshot object."""
217
self.mem.download(self._local_folder)
218
self.vmstate.download(self._local_folder)
219
# SSH key is not needed by microvm, it is needed only by
220
# test functions.
221
self.ssh_key.download(self._local_folder)
222
223
for disk in self.disks:
224
disk.download(self._local_folder)
225
os.chmod(disk.local_path(), 0o700)
226
227
def copy(self, vm_root_folder):
228
"""Copy artifacts and return a Snapshot object."""
229
assert self._local_folder is not None
230
231
dst_mem_path = os.path.join(vm_root_folder, self.mem.name())
232
dst_state_file = os.path.join(vm_root_folder, self.vmstate.name())
233
dst_ssh_key = os.path.join(vm_root_folder, self.ssh_key.name())
234
235
# Copy mem, state & ssh_key.
236
copyfile(self.mem.local_path(), dst_mem_path)
237
copyfile(self.vmstate.local_path(), dst_state_file)
238
copyfile(self.ssh_key.local_path(), dst_ssh_key)
239
# Set proper permissions for ssh key.
240
os.chmod(dst_ssh_key, 0o400)
241
242
disk_paths = []
243
for disk in self.disks:
244
dst_disk_path = os.path.join(vm_root_folder, disk.name())
245
copyfile(disk.local_path(), dst_disk_path)
246
disk_paths.append(dst_disk_path)
247
248
return Snapshot(dst_mem_path,
249
dst_state_file,
250
disks=disk_paths,
251
net_ifaces=None,
252
ssh_key=dst_ssh_key)
253
254
255
class DiskArtifact(Artifact):
256
"""Provides access to associated ssh key artifact."""
257
258
def ssh_key(self):
259
"""Return a ssh key artifact."""
260
# Replace extension.
261
key_file_path = str(Path(self.key).with_suffix('.id_rsa'))
262
return Artifact(self.bucket,
263
key_file_path,
264
artifact_type=ArtifactType.SSH_KEY)
265
266
267
class FirecrackerArtifact(Artifact):
268
"""Provides access to associated jailer artifact."""
269
270
def jailer(self):
271
"""Return a jailer binary artifact."""
272
# Jailer and FC binaries have different extensions and share
273
# file name when stored in S3:
274
# Firecracker binary: v0.22.firecrcker
275
# Jailer binary: v0.23.0.jailer
276
jailer_path = str(Path(self.key).with_suffix('.jailer'))
277
return Artifact(self.bucket,
278
jailer_path,
279
artifact_type=ArtifactType.JAILER)
280
281
@property
282
def version(self):
283
"""Return the artifact's version: `X.Y.Z`."""
284
# Get the filename, remove the extension and trim the leading 'v'.
285
return os.path.splitext(os.path.basename(self.key))[0][1:]
286
287
288
class ArtifactCollection:
289
"""Provides easy access to different artifact types."""
290
291
MICROVM_CONFIG_EXTENSION = ".json"
292
MICROVM_KERNEL_EXTENSION = ".bin"
293
MICROVM_DISK_EXTENSION = ".ext4"
294
MICROVM_VMSTATE_EXTENSION = ".vmstate"
295
MICROVM_MEM_EXTENSION = ".mem"
296
FC_EXTENSION = ".firecracker"
297
JAILER_EXTENSION = ".jailer"
298
299
PLATFORM = platform.machine()
300
301
# S3 bucket structure.
302
ARTIFACTS_ROOT = 'ci-artifacts'
303
ARTIFACTS_DISKS = '/disks/' + PLATFORM + "/"
304
ARTIFACTS_KERNELS = '/kernels/' + PLATFORM + "/"
305
ARTIFACTS_MICROVMS = '/microvms/'
306
ARTIFACTS_SNAPSHOTS = '/snapshots/' + PLATFORM + "/"
307
ARTIFACTS_BINARIES = '/binaries/' + PLATFORM + "/"
308
309
def __init__(
310
self,
311
bucket
312
):
313
"""Initialize S3 client."""
314
config = botocore.client.Config(signature_version=botocore.UNSIGNED)
315
# pylint: disable=E1101
316
# fixes "E1101: Instance of '' has no 'Bucket' member (no-member)""
317
self.bucket = boto3.resource('s3', config=config).Bucket(bucket)
318
319
def _fetch_artifacts(self,
320
artifact_dir,
321
artifact_ext,
322
artifact_type,
323
artifact_class,
324
keyword=None):
325
artifacts = []
326
prefix = ArtifactCollection.ARTIFACTS_ROOT + artifact_dir
327
files = self.bucket.objects.filter(Prefix=prefix)
328
for file in files:
329
if (
330
# Filter by extensions.
331
file.key.endswith(artifact_ext)
332
# Filter by userprovided keyword if any.
333
and (keyword is None or keyword in file.key)
334
):
335
artifacts.append(artifact_class(self.bucket,
336
file.key,
337
artifact_type=artifact_type))
338
return artifacts
339
340
def snapshots(self, keyword=None):
341
"""Return snapshot artifacts for the current arch."""
342
# Snapshot artifacts are special since they need to contain
343
# a variable number of files: mem, state, disks, ssh key.
344
# To simplify the way we retrieve and store snapshot artifacts
345
# we are going to group all snapshot file in a folder and the
346
# "keyword" parameter will filter this folder name.
347
#
348
# Naming convention for files within the snapshot below.
349
# Snapshot folder /ci-artifacts/snapshots/x86_64/fc_snapshot_v0.22:
350
# - vm.mem
351
# - vm.vmstate
352
# - disk0 <---- this is the root disk
353
# - disk1
354
# - diskN
355
# - ssh_key
356
357
artifacts = []
358
prefix = ArtifactCollection.ARTIFACTS_ROOT
359
prefix += ArtifactCollection.ARTIFACTS_SNAPSHOTS
360
snaphot_dirs = self.bucket.objects.filter(Prefix=prefix)
361
for snapshot_dir in snaphot_dirs:
362
key = snapshot_dir.key
363
# Filter out the snapshot artifacts root folder.
364
# Select only files with specified keyword.
365
if (key[-1] == "/" and key != prefix and
366
(keyword is None or keyword in snapshot_dir.key)):
367
artifact_type = ArtifactType.SNAPSHOT
368
artifacts.append(SnapshotArtifact(self.bucket,
369
key,
370
artifact_type=artifact_type))
371
372
return artifacts
373
374
def microvms(self, keyword=None):
375
"""Return microvms artifacts for the current arch."""
376
return self._fetch_artifacts(
377
ArtifactCollection.ARTIFACTS_MICROVMS,
378
ArtifactCollection.MICROVM_CONFIG_EXTENSION,
379
ArtifactType.MICROVM,
380
Artifact,
381
keyword=keyword
382
)
383
384
def firecrackers(self, keyword=None, older_than=None):
385
"""Return fc/jailer artifacts for the current arch."""
386
firecrackers = self._fetch_artifacts(
387
ArtifactCollection.ARTIFACTS_BINARIES,
388
ArtifactCollection.FC_EXTENSION,
389
ArtifactType.FC,
390
FirecrackerArtifact,
391
keyword=keyword
392
)
393
394
# Filter out binaries with versions newer than the `older_than` arg.
395
if older_than is not None:
396
return list(filter(
397
lambda fc: compare_versions(fc.version, older_than) <= 0,
398
firecrackers
399
))
400
401
return firecrackers
402
403
def firecracker_versions(self, older_than=None):
404
"""Return fc/jailer artifacts' versions for the current arch."""
405
return [fc.base_name()[1:]
406
for fc in self.firecrackers(older_than=older_than)]
407
408
def kernels(self, keyword=None):
409
"""Return kernel artifacts for the current arch."""
410
return self._fetch_artifacts(
411
ArtifactCollection.ARTIFACTS_KERNELS,
412
ArtifactCollection.MICROVM_KERNEL_EXTENSION,
413
ArtifactType.KERNEL,
414
Artifact,
415
keyword=keyword
416
)
417
418
def disks(self, keyword=None):
419
"""Return disk artifacts for the current arch."""
420
return self._fetch_artifacts(
421
ArtifactCollection.ARTIFACTS_DISKS,
422
ArtifactCollection.MICROVM_DISK_EXTENSION,
423
ArtifactType.DISK,
424
DiskArtifact,
425
keyword=keyword
426
)
427
428
429
class ArtifactSet:
430
"""Manages a set of artifacts with the same type."""
431
432
def __init__(self, artifacts):
433
"""Initialize type and artifact array."""
434
self._type = None
435
self._artifacts = []
436
self.insert(artifacts)
437
438
def insert(self, artifacts):
439
"""Add artifacts to set."""
440
if artifacts is not None and len(artifacts) > 0:
441
self._type = self._type or artifacts[0].type
442
for artifact in artifacts:
443
assert artifact.type == self._type
444
self._artifacts.append(artifact)
445
446
@property
447
def artifacts(self):
448
"""Return the artifacts array."""
449
return self._artifacts
450
451
def __len__(self):
452
"""Return the artifacts array len."""
453
return len(self._artifacts)
454
455
456
class SnapshotType(Enum):
457
"""Supported snapshot types."""
458
459
FULL = 0
460
DIFF = 1
461
462
463
class Snapshot:
464
"""Manages Firecracker snapshots."""
465
466
def __init__(self, mem, vmstate, disks, net_ifaces, ssh_key):
467
"""Initialize mem, vmstate, disks, key."""
468
assert mem is not None
469
assert vmstate is not None
470
assert disks is not None
471
assert ssh_key is not None
472
self._mem = mem
473
self._vmstate = vmstate
474
self._disks = disks
475
self._ssh_key = ssh_key
476
self._net_ifaces = net_ifaces
477
478
def rebase_snapshot(self, base):
479
"""Rebases current incremental snapshot onto a specified base layer."""
480
merge_memory_bitmaps(base.mem, self.mem)
481
self._mem = base.mem
482
483
def cleanup(self):
484
"""Delete the backing files from disk."""
485
os.remove(self._mem)
486
os.remove(self._vmstate)
487
488
@property
489
def mem(self):
490
"""Return the mem file path."""
491
return self._mem
492
493
@property
494
def vmstate(self):
495
"""Return the vmstate file path."""
496
return self._vmstate
497
498
@property
499
def disks(self):
500
"""Return the disk file paths."""
501
return self._disks
502
503
@property
504
def ssh_key(self):
505
"""Return the ssh key file path."""
506
return self._ssh_key
507
508
@property
509
def net_ifaces(self):
510
"""Return the list of net interface configs."""
511
return self._net_ifaces
512
513
514
# Default configuration values for network interfaces.
515
DEFAULT_HOST_IP = "192.168.0.1"
516
DEFAULT_GUEST_IP = "192.168.0.2"
517
DEFAULT_TAP_NAME = "tap0"
518
DEFAULT_DEV_NAME = "eth0"
519
DEFAULT_NETMASK = 30
520
521
522
class NetIfaceConfig:
523
"""Defines a network interface configuration."""
524
525
def __init__(self,
526
host_ip=DEFAULT_HOST_IP,
527
guest_ip=DEFAULT_GUEST_IP,
528
tap_name=DEFAULT_TAP_NAME,
529
dev_name=DEFAULT_DEV_NAME,
530
netmask=DEFAULT_NETMASK):
531
"""Initialize object."""
532
self._host_ip = host_ip
533
self._guest_ip = guest_ip
534
self._tap_name = tap_name
535
self._dev_name = dev_name
536
self._netmask = netmask
537
538
@property
539
def host_ip(self):
540
"""Return the host IP."""
541
return self._host_ip
542
543
@property
544
def guest_ip(self):
545
"""Return the guest IP."""
546
return self._guest_ip
547
548
@property
549
def tap_name(self):
550
"""Return the tap device name."""
551
return self._tap_name
552
553
@property
554
def dev_name(self):
555
"""Return the guest device name."""
556
return self._dev_name
557
558
@property
559
def netmask(self):
560
"""Return the netmask."""
561
return self._netmask
562
563