1#!/usr/bin/env bash 2# group: rw 3# 4# Test FUSE exports (in ways that are not captured by the generic 5# tests) 6# 7# Copyright (C) 2020 Red Hat, Inc. 8# 9# This program is free software; you can redistribute it and/or modify 10# it under the terms of the GNU General Public License as published by 11# the Free Software Foundation; either version 2 of the License, or 12# (at your option) any later version. 13# 14# This program is distributed in the hope that it will be useful, 15# but WITHOUT ANY WARRANTY; without even the implied warranty of 16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17# GNU General Public License for more details. 18# 19# You should have received a copy of the GNU General Public License 20# along with this program. If not, see <http://www.gnu.org/licenses/>. 21# 22 23seq=$(basename "$0") 24echo "QA output created by $seq" 25 26status=1 # failure is the default! 27 28_cleanup() 29{ 30 _cleanup_qemu 31 _cleanup_test_img 32 rmdir "$EXT_MP" 2>/dev/null 33 rm -f "$EXT_MP" 34 rm -f "$COPIED_IMG" 35} 36trap "_cleanup; exit \$status" 0 1 2 3 15 37 38# get standard environment, filters and checks 39. ./common.rc 40. ./common.filter 41. ./common.qemu 42 43# Generic format, but needs a plain filename 44_supported_fmt generic 45if [ "$IMGOPTSSYNTAX" = "true" ]; then 46 _unsupported_fmt $IMGFMT 47fi 48# We need the image to have exactly the specified size, and VPC does 49# not allow that by default 50_unsupported_fmt vpc 51 52_supported_proto file # We create the FUSE export manually 53_supported_os Linux # We need /dev/urandom 54 55# $1: Export ID 56# $2: Options (beyond the node-name and ID) 57# $3: Expected return value (defaults to 'return') 58# $4: Node to export (defaults to 'node-format') 59fuse_export_add() 60{ 61 # The grep -v is a filter for errors when /etc/fuse.conf does not contain 62 # user_allow_other. (The error is benign, but it is printed by fusermount 63 # on the first mount attempt, so our export code cannot hide it.) 64 _send_qemu_cmd $QEMU_HANDLE \ 65 "{'execute': 'block-export-add', 66 'arguments': { 67 'type': 'fuse', 68 'id': '$1', 69 'node-name': '${4:-node-format}', 70 $2 71 } }" \ 72 "${3:-return}" \ 73 | _filter_imgfmt \ 74 | grep -v 'option allow_other only allowed if' 75} 76 77# $1: Export ID 78fuse_export_del() 79{ 80 _send_qemu_cmd $QEMU_HANDLE \ 81 "{'execute': 'block-export-del', 82 'arguments': { 83 'id': '$1' 84 } }" \ 85 'return' 86 87 _send_qemu_cmd $QEMU_HANDLE \ 88 '' \ 89 'BLOCK_EXPORT_DELETED' 90} 91 92# Return the length of the protocol file 93# $1: Protocol node export mount point 94# $2: Original file (to compare) 95get_proto_len() 96{ 97 len1=$(stat -c '%s' "$1") 98 len2=$(stat -c '%s' "$2") 99 100 if [ "$len1" != "$len2" ]; then 101 echo 'ERROR: Length of export and original differ:' >&2 102 echo "$len1 != $len2" >&2 103 else 104 echo '(OK: Lengths of export and original are the same)' >&2 105 fi 106 107 echo "$len1" 108} 109 110COPIED_IMG="$TEST_IMG.copy" 111EXT_MP="$TEST_IMG.fuse" 112 113echo '=== Set up ===' 114 115# Create image with random data 116_make_test_img 64M 117$QEMU_IO -c 'write -s /dev/urandom 0 64M' "$TEST_IMG" | _filter_qemu_io 118 119_launch_qemu 120_send_qemu_cmd $QEMU_HANDLE \ 121 "{'execute': 'qmp_capabilities'}" \ 122 'return' 123 124# Separate blockdev-add calls for format and protocol so we can remove 125# the format layer later on 126_send_qemu_cmd $QEMU_HANDLE \ 127 "{'execute': 'blockdev-add', 128 'arguments': { 129 'driver': 'file', 130 'node-name': 'node-protocol', 131 'filename': '$TEST_IMG' 132 } }" \ 133 'return' 134 135_send_qemu_cmd $QEMU_HANDLE \ 136 "{'execute': 'blockdev-add', 137 'arguments': { 138 'driver': '$IMGFMT', 139 'node-name': 'node-format', 140 'file': 'node-protocol' 141 } }" \ 142 'return' 143 144echo 145echo '=== Mountpoint not present ===' 146 147rmdir "$EXT_MP" 2>/dev/null 148rm -f "$EXT_MP" 149output=$(fuse_export_add 'export-err' "'mountpoint': '$EXT_MP'" error) 150 151if echo "$output" | grep -q "Parameter 'type' does not accept value 'fuse'"; then 152 _notrun 'No FUSE support' 153fi 154 155echo "$output" 156 157echo 158echo '=== Mountpoint is a directory ===' 159 160mkdir "$EXT_MP" 161fuse_export_add 'export-err' "'mountpoint': '$EXT_MP'" error 162rmdir "$EXT_MP" 163 164echo 165echo '=== Mountpoint is a regular file ===' 166 167touch "$EXT_MP" 168fuse_export_add 'export-mp' "'mountpoint': '$EXT_MP'" 169 170# Check that the export presents the same data as the original image 171$QEMU_IMG compare -f raw -F $IMGFMT -U "$EXT_MP" "$TEST_IMG" 172 173# Some quick chmod tests 174stat -c 'Permissions pre-chmod: %a' "$EXT_MP" 175 176# Verify that we cannot set +w 177chmod u+w "$EXT_MP" 2>&1 | _filter_testdir | _filter_imgfmt 178stat -c 'Permissions post-+w: %a' "$EXT_MP" 179 180# But that we can set, say, +x (if we are so inclined) 181chmod u+x "$EXT_MP" 2>&1 | _filter_testdir | _filter_imgfmt 182stat -c 'Permissions post-+x: %a' "$EXT_MP" 183 184echo 185echo '=== Mount over existing file ===' 186 187# This is the coolest feature of FUSE exports: You can transparently 188# make images in any format appear as raw images 189fuse_export_add 'export-img' "'mountpoint': '$TEST_IMG'" 190 191# Accesses both exports at the same time, so we get a concurrency test 192$QEMU_IMG compare -f raw -F raw -U "$EXT_MP" "$TEST_IMG" 193 194# Just to be sure, we later want to compare the data offline. Also, 195# this allows us to see that cp works without complaining. 196# (This is not a given, because cp will expect a short read at EOF. 197# Internally, qemu does not allow short reads, so we have to check 198# whether the FUSE export driver lets them work.) 199cp "$TEST_IMG" "$COPIED_IMG" 200 201# $TEST_IMG will be in mode 0400 because it is read-only; we are going 202# to write to the copy, so make it writable 203chmod 0600 "$COPIED_IMG" 204 205echo 206echo '=== Double export ===' 207 208# We have already seen that exporting a node twice works fine, but you 209# cannot export anything twice on the same mount point. The reason is 210# that qemu has to stat the given mount point, and this would have to 211# be answered by the same qemu instance if it already has an export 212# there. However, it cannot answer the stat because it is itself 213# caught up in that same stat. 214fuse_export_add 'export-err' "'mountpoint': '$EXT_MP'" error 215 216echo 217echo '=== Remove export ===' 218 219# Double-check that $EXT_MP appears as a non-empty file (the raw image) 220$QEMU_IMG info -f raw "$EXT_MP" | grep 'virtual size' 221 222fuse_export_del 'export-mp' 223 224# See that the file appears empty again 225$QEMU_IMG info -f raw "$EXT_MP" | grep 'virtual size' 226 227echo 228echo '=== Writable export ===' 229 230fuse_export_add 'export-mp' "'mountpoint': '$EXT_MP', 'writable': true" 231 232# Check that writing to the read-only export fails 233output=$($QEMU_IO -f raw -c 'write -P 42 1M 64k' "$TEST_IMG" 2>&1 \ 234 | _filter_qemu_io | _filter_testdir | _filter_imgfmt) 235 236# Expected reference output: Opening the file fails because it has no 237# write permission 238reference="Could not open 'TEST_DIR/t.IMGFMT': Permission denied" 239 240if echo "$output" | grep -q "$reference"; then 241 echo "Writing to read-only export failed: OK" 242elif echo "$output" | grep -q "write failed: Permission denied"; then 243 # With CAP_DAC_OVERRIDE (e.g. when running this test as root), the export 244 # can be opened regardless of its file permissions, but writing will then 245 # fail. This is not the result for which we want to test, so count this as 246 # a SKIP. 247 _casenotrun "Opening RO export as R/W succeeded, perhaps because of" \ 248 "CAP_DAC_OVERRIDE" 249 250 # Still, write this to the reference output to make the test pass 251 echo "Writing to read-only export failed: OK" 252else 253 echo "Writing to read-only export failed: ERROR" 254 echo "$output" 255fi 256 257# But here it should work 258$QEMU_IO -f raw -c 'write -P 42 1M 64k' "$EXT_MP" | _filter_qemu_io 259 260# (Adjust the copy, too) 261$QEMU_IO -f raw -c 'write -P 42 1M 64k' "$COPIED_IMG" | _filter_qemu_io 262 263echo 264echo '=== Resizing exports ===' 265 266# Here, we need to export the protocol node -- the format layer may 267# not be growable, simply because the format does not support it. 268 269# Remove all exports and the format node first so permissions will not 270# get in the way 271fuse_export_del 'export-mp' 272fuse_export_del 'export-img' 273 274_send_qemu_cmd $QEMU_HANDLE \ 275 "{'execute': 'blockdev-del', 276 'arguments': { 277 'node-name': 'node-format' 278 } }" \ 279 'return' 280 281# Now export the protocol node 282fuse_export_add \ 283 'export-mp' \ 284 "'mountpoint': '$EXT_MP', 'writable': true" \ 285 'return' \ 286 'node-protocol' 287 288echo 289echo '--- Try growing non-growable export ---' 290 291# Get the current size so we can write beyond the EOF 292orig_len=$(get_proto_len "$EXT_MP" "$TEST_IMG") 293orig_disk_usage=$(stat -c '%b' "$TEST_IMG") 294 295# Should fail (exports are non-growable by default) 296# (Note that qemu-io can never write beyond the EOF, so we have to use 297# dd here) 298dd if=/dev/zero of="$EXT_MP" bs=1 count=64k seek=$orig_len 2>&1 \ 299 | _filter_testdir | _filter_imgfmt 300 301echo 302echo '--- Resize export ---' 303 304# But we can truncate it explicitly; even with fallocate 305fallocate -o "$orig_len" -l 64k "$EXT_MP" 306 307new_len=$(get_proto_len "$EXT_MP" "$TEST_IMG") 308if [ "$new_len" != "$((orig_len + 65536))" ]; then 309 echo 'ERROR: Unexpected post-truncate image size:' 310 echo "$new_len != $((orig_len + 65536))" 311else 312 echo 'OK: Post-truncate image size is as expected' 313fi 314 315new_disk_usage=$(stat -c '%b' "$TEST_IMG") 316if [ "$new_disk_usage" -gt "$orig_disk_usage" ]; then 317 echo 'OK: Disk usage grew with fallocate' 318else 319 echo 'ERROR: Disk usage did not grow despite fallocate:' 320 echo "$orig_disk_usage => $new_disk_usage" 321fi 322 323echo 324echo '--- Try growing growable export ---' 325 326# Now export as growable 327fuse_export_del 'export-mp' 328fuse_export_add \ 329 'export-mp' \ 330 "'mountpoint': '$EXT_MP', 'writable': true, 'growable': true" \ 331 'return' \ 332 'node-protocol' 333 334# Now we should be able to write beyond the EOF 335dd if=/dev/zero of="$EXT_MP" bs=1 count=64k seek=$new_len 2>&1 \ 336 | _filter_testdir | _filter_imgfmt 337 338new_len=$(get_proto_len "$EXT_MP" "$TEST_IMG") 339if [ "$new_len" != "$((orig_len + 131072))" ]; then 340 echo 'ERROR: Unexpected post-grow image size:' 341 echo "$new_len != $((orig_len + 131072))" 342else 343 echo 'OK: Post-grow image size is as expected' 344fi 345 346echo 347echo '--- Shrink export ---' 348 349# Now go back to the original size 350truncate -s "$orig_len" "$EXT_MP" 351 352new_len=$(get_proto_len "$EXT_MP" "$TEST_IMG") 353if [ "$new_len" != "$orig_len" ]; then 354 echo 'ERROR: Unexpected post-truncate image size:' 355 echo "$new_len != $orig_len" 356else 357 echo 'OK: Post-truncate image size is as expected' 358fi 359 360echo 361echo '=== Tear down ===' 362 363_send_qemu_cmd $QEMU_HANDLE \ 364 "{'execute': 'quit'}" \ 365 'return' 366 367wait=yes _cleanup_qemu 368 369echo 370echo '=== Compare copy with original ===' 371 372$QEMU_IMG compare -f raw -F $IMGFMT "$COPIED_IMG" "$TEST_IMG" 373 374# success, all done 375echo "*** done" 376rm -f $seq.full 377status=0 378