Path: blob/main/tests/integration_tests/functional/test_drives.py
1958 views
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.1# SPDX-License-Identifier: Apache-2.02"""Tests for guest-side operations on /drives resources."""34import os5import platform6import pytest78import framework.utils as utils910import host_tools.drive as drive_tools11import host_tools.network as net_tools # pylint: disable=import-error12import host_tools.logging as log_tools1314PARTUUID = {"x86_64": "0eaa91a0-01", "aarch64": "7bf14469-01"}15MB = 1024 * 1024161718def test_rescan_file(test_microvm_with_ssh, network_config):19"""Verify that rescan works with a file-backed virtio device."""20test_microvm = test_microvm_with_ssh21test_microvm.spawn()2223# Set up the microVM with 1 vCPUs, 256 MiB of RAM, 0 network ifaces and24# a root file system with the rw permission. The network interface is25# added after we get a unique MAC and IP.26test_microvm.basic_config()2728_tap, _, _ = test_microvm_with_ssh.ssh_network_config(network_config, '1')2930block_size = 231# Add a scratch block device.32fs = drive_tools.FilesystemFile(33os.path.join(test_microvm.fsfiles, 'scratch'),34size=block_size35)36test_microvm.add_drive(37'scratch',38fs.path,39)4041test_microvm.start()4243ssh_connection = net_tools.SSHConnection(test_microvm.ssh_config)44_check_block_size(ssh_connection, '/dev/vdb', fs.size())4546# Check if reading from the entire disk results in a file of the same size47# or errors out, after a truncate on the host.48truncated_size = block_size//249utils.run_cmd(f"truncate --size {truncated_size}M {fs.path}")50block_copy_name = "dev_vdb_copy"51_, _, stderr = ssh_connection.execute_command(52f"dd if=/dev/vdb of={block_copy_name} bs=1M count={block_size}")53assert "dd: error reading '/dev/vdb': Input/output error" in stderr.read()54_check_file_size(ssh_connection, f'{block_copy_name}',55truncated_size * MB)5657response = test_microvm.drive.patch(58drive_id='scratch',59path_on_host=test_microvm.create_jailed_resource(fs.path),60)61assert test_microvm.api_session.is_status_no_content(response.status_code)6263_check_block_size(64ssh_connection,65'/dev/vdb',66fs.size()67)686970def test_device_ordering(test_microvm_with_ssh, network_config):71"""Verify device ordering.7273The root device should correspond to /dev/vda in the guest and74the order of the other devices should match their configuration order.75"""76test_microvm = test_microvm_with_ssh77test_microvm.spawn()7879# Add first scratch block device.80fs1 = drive_tools.FilesystemFile(81os.path.join(test_microvm.fsfiles, 'scratch1'),82size=12883)84test_microvm.add_drive(85'scratch1',86fs1.path87)8889# Set up the microVM with 1 vCPUs, 256 MiB of RAM, 0 network ifaces,90# a read-write root file system (this is the second block device added).91# The network interface is added after we get a unique MAC and IP.92test_microvm.basic_config()9394# Add the third block device.95fs2 = drive_tools.FilesystemFile(96os.path.join(test_microvm.fsfiles, 'scratch2'),97size=51298)99test_microvm.add_drive(100'scratch2',101fs2.path102)103104_tap, _, _ = test_microvm_with_ssh.ssh_network_config(network_config, '1')105106test_microvm.start()107108# Determine the size of the microVM rootfs in bytes.109try:110result = utils.run_cmd(111'du --apparent-size --block-size=1 {}'112.format(test_microvm.rootfs_file),113)114except ChildProcessError:115pytest.skip('Failed to get microVM rootfs size: {}'116.format(result.stderr))117118assert len(result.stdout.split()) == 2119rootfs_size = result.stdout.split('\t')[0]120121# The devices were added in this order: fs1, rootfs, fs2.122# However, the rootfs is the root device and goes first,123# so we expect to see this order: rootfs, fs1, fs2.124# The devices are identified by their size.125ssh_connection = net_tools.SSHConnection(test_microvm.ssh_config)126_check_block_size(ssh_connection, '/dev/vda', rootfs_size)127_check_block_size(ssh_connection, '/dev/vdb', fs1.size())128_check_block_size(ssh_connection, '/dev/vdc', fs2.size())129130131def test_rescan_dev(test_microvm_with_ssh, network_config):132"""Verify that rescan works with a device-backed virtio device."""133test_microvm = test_microvm_with_ssh134test_microvm.spawn()135session = test_microvm.api_session136137# Set up the microVM with 1 vCPUs, 256 MiB of RAM, 0 network ifaces and138# a root file system with the rw permission. The network interface is139# added after we get a unique MAC and IP.140test_microvm.basic_config()141142_tap, _, _ = test_microvm_with_ssh.ssh_network_config(network_config, '1')143144# Add a scratch block device.145fs1 = drive_tools.FilesystemFile(os.path.join(test_microvm.fsfiles, 'fs1'))146test_microvm.add_drive(147'scratch',148fs1.path149)150151test_microvm.start()152153ssh_connection = net_tools.SSHConnection(test_microvm.ssh_config)154155_check_block_size(ssh_connection, '/dev/vdb', fs1.size())156157fs2 = drive_tools.FilesystemFile(158os.path.join(test_microvm.fsfiles, 'fs2'),159size=512160)161162losetup = ['losetup', '--find', '--show', fs2.path]163loopback_device = None164result = None165try:166result = utils.run_cmd(losetup)167loopback_device = result.stdout.rstrip()168except ChildProcessError:169pytest.skip('failed to create a lookback device: ' +170f'stdout={result.stdout}, stderr={result.stderr}')171172try:173response = test_microvm.drive.patch(174drive_id='scratch',175path_on_host=test_microvm.create_jailed_resource(loopback_device),176)177assert session.is_status_no_content(response.status_code)178179_check_block_size(ssh_connection, '/dev/vdb', fs2.size())180finally:181if loopback_device:182utils.run_cmd(['losetup', '--detach', loopback_device])183184185def test_non_partuuid_boot(test_microvm_with_ssh, network_config):186"""Test the output reported by blockdev when booting from /dev/vda."""187test_microvm = test_microvm_with_ssh188test_microvm.spawn()189190# Sets up the microVM with 1 vCPUs, 256 MiB of RAM, no network ifaces and191# a root file system with the rw permission. The network interfaces is192# added after we get a unique MAC and IP.193test_microvm.basic_config(vcpu_count=1)194195_tap, _, _ = test_microvm.ssh_network_config(network_config, '1')196197# Add another read-only block device.198fs = drive_tools.FilesystemFile(199os.path.join(test_microvm.fsfiles, 'readonly')200)201test_microvm.add_drive(202'scratch',203fs.path,204is_read_only=True205)206207test_microvm.start()208209# Prepare the input for doing the assertion210assert_dict = {}211# Keep an array of strings specifying the location where some string212# from the output is located.213# 1-0 means line 1, column 0.214keys_array = ['1-0', '1-8', '2-0']215# Keep a dictionary where the keys are the location and the values216# represent the input to assert against.217assert_dict[keys_array[0]] = 'rw'218assert_dict[keys_array[1]] = '/dev/vda'219assert_dict[keys_array[2]] = 'ro'220_check_drives(test_microvm, assert_dict, keys_array)221222223def test_partuuid_boot(test_microvm_with_partuuid, network_config):224"""Test the output reported by blockdev when booting with PARTUUID."""225test_microvm = test_microvm_with_partuuid226test_microvm.spawn()227228# Set up the microVM with 1 vCPUs, 256 MiB of RAM, no network ifaces and229# a root file system with the rw permission. The network interfaces is230# added after we get a unique MAC and IP.231test_microvm.basic_config(232vcpu_count=1,233add_root_device=False234)235236_tap, _, _ = test_microvm.ssh_network_config(network_config, '1')237238# Add the root block device specified through PARTUUID.239test_microvm.add_drive(240'rootfs',241test_microvm.rootfs_file,242root_device=True,243partuuid=PARTUUID[platform.machine()]244)245246test_microvm.start()247248assert_dict = {}249keys_array = ['1-0', '1-8', '2-0', '2-7']250assert_dict[keys_array[0]] = "rw"251assert_dict[keys_array[1]] = '/dev/vda'252assert_dict[keys_array[2]] = 'rw'253assert_dict[keys_array[3]] = '/dev/vda1'254_check_drives(test_microvm, assert_dict, keys_array)255256257def test_partuuid_update(test_microvm_with_ssh, network_config):258"""Test successful switching from PARTUUID boot to /dev/vda boot."""259test_microvm = test_microvm_with_ssh260test_microvm.spawn()261262# Set up the microVM with 1 vCPUs, 256 MiB of RAM, 0 network ifaces and263# a root file system with the rw permission. The network interfaces is264# added after we get a unique MAC and IP.265test_microvm.basic_config(266vcpu_count=1,267add_root_device=False268)269270_tap, _, _ = test_microvm.ssh_network_config(network_config, '1')271272# Add the root block device specified through PARTUUID.273test_microvm.add_drive(274'rootfs',275test_microvm.rootfs_file,276root_device=True,277partuuid='0eaa91a0-01'278)279280# Update the root block device to boot from /dev/vda.281test_microvm.add_drive(282'rootfs',283test_microvm.rootfs_file,284root_device=True,285)286287test_microvm.start()288289# Assert that the final booting method is from /dev/vda.290assert_dict = {}291keys_array = ['1-0', '1-8']292assert_dict[keys_array[0]] = 'rw'293assert_dict[keys_array[1]] = '/dev/vda'294_check_drives(test_microvm, assert_dict, keys_array)295296297def test_patch_drive(test_microvm_with_ssh, network_config):298"""Test replacing the backing filesystem after guest boot works."""299test_microvm = test_microvm_with_ssh300test_microvm.spawn()301302# Set up the microVM with 1 vCPUs, 256 MiB of RAM, 1 network iface, a root303# file system with the rw permission, and a scratch drive.304test_microvm.basic_config()305306_tap, _, _ = test_microvm.ssh_network_config(network_config, '1')307308fs1 = drive_tools.FilesystemFile(309os.path.join(test_microvm.fsfiles, 'scratch')310)311test_microvm.add_drive(312'scratch',313fs1.path314)315316test_microvm.start()317318# Updates to `path_on_host` with a valid path are allowed.319fs2 = drive_tools.FilesystemFile(320os.path.join(test_microvm.fsfiles, 'otherscratch'), size=512321)322response = test_microvm.drive.patch(323drive_id='scratch',324path_on_host=test_microvm.create_jailed_resource(fs2.path)325)326assert test_microvm.api_session.is_status_no_content(response.status_code)327328ssh_connection = net_tools.SSHConnection(test_microvm.ssh_config)329330# The `lsblk` command should output 2 lines to STDOUT: "SIZE" and the size331# of the device, in bytes.332blksize_cmd = "lsblk -b /dev/vdb --output SIZE"333size_bytes_str = "536870912" # = 512 MiB334_, stdout, stderr = ssh_connection.execute_command(blksize_cmd)335assert stderr.read() == ''336stdout.readline() # skip "SIZE"337assert stdout.readline().strip() == size_bytes_str338339340def test_no_flush(test_microvm_with_ssh, network_config):341"""Verify default block ignores flush."""342test_microvm = test_microvm_with_ssh343test_microvm.spawn()344345test_microvm.basic_config(346vcpu_count=1,347add_root_device=False348)349350_tap, _, _ = test_microvm.ssh_network_config(network_config, '1')351352# Add the block device353test_microvm.add_drive(354'rootfs',355test_microvm.rootfs_file,356root_device=True,357)358359# Configure the metrics.360metrics_fifo_path = os.path.join(test_microvm.path, 'metrics_fifo')361metrics_fifo = log_tools.Fifo(metrics_fifo_path)362response = test_microvm.metrics.put(363metrics_path=test_microvm.create_jailed_resource(metrics_fifo.path)364)365assert test_microvm.api_session.is_status_no_content(response.status_code)366367test_microvm.start()368369# Verify all flush commands were ignored during boot.370fc_metrics = test_microvm.flush_metrics(metrics_fifo)371assert fc_metrics['block']['flush_count'] == 0372373# Have the guest drop the caches to generate flush requests.374ssh_connection = net_tools.SSHConnection(test_microvm.ssh_config)375cmd = "sync; echo 1 > /proc/sys/vm/drop_caches"376_, _, stderr = ssh_connection.execute_command(cmd)377assert stderr.read() == ''378379# Verify all flush commands were ignored even after380# dropping the caches.381fc_metrics = test_microvm.flush_metrics(metrics_fifo)382assert fc_metrics['block']['flush_count'] == 0383384385def test_flush(test_microvm_with_ssh, network_config):386"""Verify block with flush actually flushes."""387test_microvm = test_microvm_with_ssh388test_microvm.spawn()389390test_microvm.basic_config(391vcpu_count=1,392add_root_device=False393)394395_tap, _, _ = test_microvm.ssh_network_config(network_config, '1')396397# Add the block device with explicitly enabling flush.398test_microvm.add_drive(399'rootfs',400test_microvm.rootfs_file,401root_device=True,402cache_type="Writeback",403)404405# Configure metrics, to get later the `flush_count`.406metrics_fifo_path = os.path.join(test_microvm.path, 'metrics_fifo')407metrics_fifo = log_tools.Fifo(metrics_fifo_path)408response = test_microvm.metrics.put(409metrics_path=test_microvm.create_jailed_resource(metrics_fifo.path)410)411assert test_microvm.api_session.is_status_no_content(response.status_code)412413test_microvm.start()414415# Have the guest drop the caches to generate flush requests.416ssh_connection = net_tools.SSHConnection(test_microvm.ssh_config)417cmd = "sync; echo 1 > /proc/sys/vm/drop_caches"418_, _, stderr = ssh_connection.execute_command(cmd)419assert stderr.read() == ''420421# On average, dropping the caches right after boot generates422# about 6 block flush requests.423fc_metrics = test_microvm.flush_metrics(metrics_fifo)424assert fc_metrics['block']['flush_count'] > 0425426427def test_block_default_cache_old_version(test_microvm_with_ssh):428"""Verify that saving a snapshot for old versions works correctly."""429test_microvm = test_microvm_with_ssh430test_microvm.spawn()431432test_microvm.basic_config(433vcpu_count=1,434add_root_device=False435)436437# Add the block device with explicitly enabling flush.438test_microvm.add_drive(439'rootfs',440test_microvm.rootfs_file,441root_device=True,442cache_type="Writeback",443)444445test_microvm.start()446447# Pause the VM to create the snapshot.448response = test_microvm.vm.patch(state='Paused')449assert test_microvm.api_session.is_status_no_content(response.status_code)450451# Create the snapshot for a version without block cache type.452response = test_microvm.snapshot.create(453mem_file_path='memfile',454snapshot_path='snapsfile',455diff=False,456version='0.24.0'457)458assert test_microvm.api_session.is_status_no_content(response.status_code)459460# We should find a warning in the logs for this case as this461# cache type was not supported in 0.24.0 and we should default462# to "Unsafe" mode.463log_data = test_microvm.log_data464assert "Target version does not implement the current cache type. "\465"Defaulting to \"unsafe\" mode." in log_data466467468def check_iops_limit(ssh_connection, block_size, count, min_time, max_time):469"""Verify if the rate limiter throttles block iops using dd."""470obs = block_size471byte_count = block_size * count472dd = "dd if=/dev/zero of=/dev/vdb ibs={} obs={} count={} oflag=direct"\473.format(block_size, obs, count)474print("Running cmd {}".format(dd))475# Check write iops (writing with oflag=direct is more reliable).476exit_code, _, stderr = ssh_connection.execute_command(dd)477assert exit_code == 0478479# "dd" writes to stderr by design. We drop first lines480stderr.readline().strip()481stderr.readline().strip()482dd_result = stderr.readline().strip()483484# Interesting output looks like this:485# 4194304 bytes (4.2 MB, 4.0 MiB) copied, 0.0528524 s, 79.4 MB/s486tokens = dd_result.split()487488# Check total read bytes.489assert int(tokens[0]) == byte_count490# Check duration.491assert float(tokens[7]) > min_time492assert float(tokens[7]) < max_time493494495def test_patch_drive_limiter(test_microvm_with_ssh, network_config):496"""Test replacing the drive rate-limiter after guest boot works."""497test_microvm = test_microvm_with_ssh498test_microvm.jailer.daemonize = False499test_microvm.spawn()500# Set up the microVM with 2 vCPUs, 512 MiB of RAM, 1 network iface, a root501# file system with the rw permission, and a scratch drive.502test_microvm.basic_config(vcpu_count=2,503mem_size_mib=512,504boot_args='console=ttyS0 reboot=k panic=1')505506_tap, _, _ = test_microvm.ssh_network_config(network_config, '1')507508fs1 = drive_tools.FilesystemFile(509os.path.join(test_microvm.fsfiles, 'scratch'),510size=512511)512response = test_microvm.drive.put(513drive_id='scratch',514path_on_host=test_microvm.create_jailed_resource(fs1.path),515is_root_device=False,516is_read_only=False,517rate_limiter={518'bandwidth': {519'size': 10 * MB,520'refill_time': 100521},522'ops': {523'size': 100,524'refill_time': 100525}526}527)528assert test_microvm.api_session.is_status_no_content(response.status_code)529test_microvm.start()530ssh_connection = net_tools.SSHConnection(test_microvm.ssh_config)531532# Validate IOPS stays within above configured limits.533# For example, the below call will validate that reading 1000 blocks534# of 512b will complete in at 0.8-1.2 seconds ('dd' is not very accurate,535# so we target to stay within 20% error).536check_iops_limit(ssh_connection, 512, 1000, 0.8, 1.2)537check_iops_limit(ssh_connection, 4096, 1000, 0.8, 1.2)538539# Patch ratelimiter540response = test_microvm.drive.patch(541drive_id='scratch',542rate_limiter={543'bandwidth': {544'size': 100 * MB,545'refill_time': 100546},547'ops': {548'size': 200,549'refill_time': 100550}551}552)553assert test_microvm.api_session.is_status_no_content(response.status_code)554555check_iops_limit(ssh_connection, 512, 2000, 0.8, 1.2)556check_iops_limit(ssh_connection, 4096, 2000, 0.8, 1.2)557558# Patch ratelimiter559response = test_microvm.drive.patch(560drive_id='scratch',561rate_limiter={562'ops': {563'size': 1000,564'refill_time': 100565}566}567)568assert test_microvm.api_session.is_status_no_content(response.status_code)569570check_iops_limit(ssh_connection, 512, 10000, 0.8, 1.2)571check_iops_limit(ssh_connection, 4096, 10000, 0.8, 1.2)572573574def _check_block_size(ssh_connection, dev_path, size):575_, stdout, stderr = ssh_connection.execute_command(576'blockdev --getsize64 {}'.format(dev_path)577)578579assert stderr.read() == ''580assert stdout.readline().strip() == str(size)581582583def _check_file_size(ssh_connection, dev_path, size):584_, stdout, stderr = ssh_connection.execute_command(585'stat --format=%s {}'.format(dev_path)586)587588assert stderr.read() == ''589assert stdout.readline().strip() == str(size)590591592def _process_blockdev_output(blockdev_out, assert_dict, keys_array):593blockdev_out_lines = blockdev_out.splitlines()594595for key in keys_array:596line = int(key.split('-')[0])597col = int(key.split('-')[1])598blockdev_out_line = blockdev_out_lines[line]599assert blockdev_out_line.split(" ")[col] == assert_dict[key]600601602def _check_drives(test_microvm, assert_dict, keys_array):603ssh_connection = net_tools.SSHConnection(test_microvm.ssh_config)604605_, stdout, stderr = ssh_connection.execute_command('blockdev --report')606assert stderr.read() == ''607_process_blockdev_output(608stdout.read(),609assert_dict,610keys_array)611612613