#!/usr/bin/env bash # group: rw # # Test FUSE exports (in ways that are not captured by the generic # tests) # # Copyright (C) 2020 Red Hat, Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # seq=$(basename "$0") echo "QA output created by $seq" status=1 # failure is the default! _cleanup() { _cleanup_qemu _cleanup_test_img rmdir "$EXT_MP" 2>/dev/null rm -f "$EXT_MP" rm -f "$COPIED_IMG" } trap "_cleanup; exit \$status" 0 1 2 3 15 # get standard environment, filters and checks . ./common.rc . ./common.filter . ./common.qemu # Generic format, but needs a plain filename _supported_fmt generic if [ "$IMGOPTSSYNTAX" = "true" ]; then _unsupported_fmt $IMGFMT fi # We need the image to have exactly the specified size, and VPC does # not allow that by default _unsupported_fmt vpc _supported_proto file # We create the FUSE export manually _supported_os Linux # We need /dev/urandom # $1: Export ID # $2: Options (beyond the node-name and ID) # $3: Expected return value (defaults to 'return') # $4: Node to export (defaults to 'node-format') fuse_export_add() { # The grep -v is a filter for errors when /etc/fuse.conf does not contain # user_allow_other. (The error is benign, but it is printed by fusermount # on the first mount attempt, so our export code cannot hide it.) _send_qemu_cmd $QEMU_HANDLE \ "{'execute': 'block-export-add', 'arguments': { 'type': 'fuse', 'id': '$1', 'node-name': '${4:-node-format}', $2 } }" \ "${3:-return}" \ | _filter_imgfmt \ | grep -v 'option allow_other only allowed if' } # $1: Export ID fuse_export_del() { _send_qemu_cmd $QEMU_HANDLE \ "{'execute': 'block-export-del', 'arguments': { 'id': '$1' } }" \ 'return' _send_qemu_cmd $QEMU_HANDLE \ '' \ 'BLOCK_EXPORT_DELETED' } # Return the length of the protocol file # $1: Protocol node export mount point # $2: Original file (to compare) get_proto_len() { len1=$(stat -c '%s' "$1") len2=$(stat -c '%s' "$2") if [ "$len1" != "$len2" ]; then echo 'ERROR: Length of export and original differ:' >&2 echo "$len1 != $len2" >&2 else echo '(OK: Lengths of export and original are the same)' >&2 fi echo "$len1" } COPIED_IMG="$TEST_IMG.copy" EXT_MP="$TEST_IMG.fuse" echo '=== Set up ===' # Create image with random data _make_test_img 64M $QEMU_IO -c 'write -s /dev/urandom 0 64M' "$TEST_IMG" | _filter_qemu_io _launch_qemu _send_qemu_cmd $QEMU_HANDLE \ "{'execute': 'qmp_capabilities'}" \ 'return' # Separate blockdev-add calls for format and protocol so we can remove # the format layer later on _send_qemu_cmd $QEMU_HANDLE \ "{'execute': 'blockdev-add', 'arguments': { 'driver': 'file', 'node-name': 'node-protocol', 'filename': '$TEST_IMG' } }" \ 'return' _send_qemu_cmd $QEMU_HANDLE \ "{'execute': 'blockdev-add', 'arguments': { 'driver': '$IMGFMT', 'node-name': 'node-format', 'file': 'node-protocol' } }" \ 'return' echo echo '=== Mountpoint not present ===' rmdir "$EXT_MP" 2>/dev/null rm -f "$EXT_MP" output=$(fuse_export_add 'export-err' "'mountpoint': '$EXT_MP'" error) if echo "$output" | grep -q "Parameter 'type' does not accept value 'fuse'"; then _notrun 'No FUSE support' fi echo "$output" echo echo '=== Mountpoint is a directory ===' mkdir "$EXT_MP" fuse_export_add 'export-err' "'mountpoint': '$EXT_MP'" error rmdir "$EXT_MP" echo echo '=== Mountpoint is a regular file ===' touch "$EXT_MP" fuse_export_add 'export-mp' "'mountpoint': '$EXT_MP'" # Check that the export presents the same data as the original image $QEMU_IMG compare -f raw -F $IMGFMT -U "$EXT_MP" "$TEST_IMG" # Some quick chmod tests stat -c 'Permissions pre-chmod: %a' "$EXT_MP" # Verify that we cannot set +w chmod u+w "$EXT_MP" 2>&1 | _filter_testdir | _filter_imgfmt stat -c 'Permissions post-+w: %a' "$EXT_MP" # But that we can set, say, +x (if we are so inclined) chmod u+x "$EXT_MP" 2>&1 | _filter_testdir | _filter_imgfmt stat -c 'Permissions post-+x: %a' "$EXT_MP" echo echo '=== Mount over existing file ===' # This is the coolest feature of FUSE exports: You can transparently # make images in any format appear as raw images fuse_export_add 'export-img' "'mountpoint': '$TEST_IMG'" # Accesses both exports at the same time, so we get a concurrency test $QEMU_IMG compare -f raw -F raw -U "$EXT_MP" "$TEST_IMG" # Just to be sure, we later want to compare the data offline. Also, # this allows us to see that cp works without complaining. # (This is not a given, because cp will expect a short read at EOF. # Internally, qemu does not allow short reads, so we have to check # whether the FUSE export driver lets them work.) cp "$TEST_IMG" "$COPIED_IMG" # $TEST_IMG will be in mode 0400 because it is read-only; we are going # to write to the copy, so make it writable chmod 0600 "$COPIED_IMG" echo echo '=== Double export ===' # We have already seen that exporting a node twice works fine, but you # cannot export anything twice on the same mount point. The reason is # that qemu has to stat the given mount point, and this would have to # be answered by the same qemu instance if it already has an export # there. However, it cannot answer the stat because it is itself # caught up in that same stat. fuse_export_add 'export-err' "'mountpoint': '$EXT_MP'" error echo echo '=== Remove export ===' # Double-check that $EXT_MP appears as a non-empty file (the raw image) $QEMU_IMG info -f raw "$EXT_MP" | grep 'virtual size' fuse_export_del 'export-mp' # See that the file appears empty again $QEMU_IMG info -f raw "$EXT_MP" | grep 'virtual size' echo echo '=== Writable export ===' fuse_export_add 'export-mp' "'mountpoint': '$EXT_MP', 'writable': true" # Check that writing to the read-only export fails output=$($QEMU_IO -f raw -c 'write -P 42 1M 64k' "$TEST_IMG" 2>&1 \ | _filter_qemu_io | _filter_testdir | _filter_imgfmt) # Expected reference output: Opening the file fails because it has no # write permission reference="Could not open 'TEST_DIR/t.IMGFMT': Permission denied" if echo "$output" | grep -q "$reference"; then echo "Writing to read-only export failed: OK" elif echo "$output" | grep -q "write failed: Permission denied"; then # With CAP_DAC_OVERRIDE (e.g. when running this test as root), the export # can be opened regardless of its file permissions, but writing will then # fail. This is not the result for which we want to test, so count this as # a SKIP. _casenotrun "Opening RO export as R/W succeeded, perhaps because of" \ "CAP_DAC_OVERRIDE" # Still, write this to the reference output to make the test pass echo "Writing to read-only export failed: OK" else echo "Writing to read-only export failed: ERROR" echo "$output" fi # But here it should work $QEMU_IO -f raw -c 'write -P 42 1M 64k' "$EXT_MP" | _filter_qemu_io # (Adjust the copy, too) $QEMU_IO -f raw -c 'write -P 42 1M 64k' "$COPIED_IMG" | _filter_qemu_io echo echo '=== Resizing exports ===' # Here, we need to export the protocol node -- the format layer may # not be growable, simply because the format does not support it. # Remove all exports and the format node first so permissions will not # get in the way fuse_export_del 'export-mp' fuse_export_del 'export-img' _send_qemu_cmd $QEMU_HANDLE \ "{'execute': 'blockdev-del', 'arguments': { 'node-name': 'node-format' } }" \ 'return' # Now export the protocol node fuse_export_add \ 'export-mp' \ "'mountpoint': '$EXT_MP', 'writable': true" \ 'return' \ 'node-protocol' echo echo '--- Try growing non-growable export ---' # Get the current size so we can write beyond the EOF orig_len=$(get_proto_len "$EXT_MP" "$TEST_IMG") orig_disk_usage=$(stat -c '%b' "$TEST_IMG") # Should fail (exports are non-growable by default) # (Note that qemu-io can never write beyond the EOF, so we have to use # dd here) dd if=/dev/zero of="$EXT_MP" bs=1 count=64k seek=$orig_len 2>&1 \ | _filter_testdir | _filter_imgfmt echo echo '--- Resize export ---' # But we can truncate it explicitly; even with fallocate fallocate -o "$orig_len" -l 64k "$EXT_MP" new_len=$(get_proto_len "$EXT_MP" "$TEST_IMG") if [ "$new_len" != "$((orig_len + 65536))" ]; then echo 'ERROR: Unexpected post-truncate image size:' echo "$new_len != $((orig_len + 65536))" else echo 'OK: Post-truncate image size is as expected' fi new_disk_usage=$(stat -c '%b' "$TEST_IMG") if [ "$new_disk_usage" -gt "$orig_disk_usage" ]; then echo 'OK: Disk usage grew with fallocate' else echo 'ERROR: Disk usage did not grow despite fallocate:' echo "$orig_disk_usage => $new_disk_usage" fi echo echo '--- Try growing growable export ---' # Now export as growable fuse_export_del 'export-mp' fuse_export_add \ 'export-mp' \ "'mountpoint': '$EXT_MP', 'writable': true, 'growable': true" \ 'return' \ 'node-protocol' # Now we should be able to write beyond the EOF dd if=/dev/zero of="$EXT_MP" bs=1 count=64k seek=$new_len 2>&1 \ | _filter_testdir | _filter_imgfmt new_len=$(get_proto_len "$EXT_MP" "$TEST_IMG") if [ "$new_len" != "$((orig_len + 131072))" ]; then echo 'ERROR: Unexpected post-grow image size:' echo "$new_len != $((orig_len + 131072))" else echo 'OK: Post-grow image size is as expected' fi echo echo '--- Shrink export ---' # Now go back to the original size truncate -s "$orig_len" "$EXT_MP" new_len=$(get_proto_len "$EXT_MP" "$TEST_IMG") if [ "$new_len" != "$orig_len" ]; then echo 'ERROR: Unexpected post-truncate image size:' echo "$new_len != $orig_len" else echo 'OK: Post-truncate image size is as expected' fi echo echo '=== Tear down ===' _send_qemu_cmd $QEMU_HANDLE \ "{'execute': 'quit'}" \ 'return' wait=yes _cleanup_qemu echo echo '=== Compare copy with original ===' $QEMU_IMG compare -f raw -F $IMGFMT "$COPIED_IMG" "$TEST_IMG" # success, all done echo "*** done" rm -f $seq.full status=0