| 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 | |
| 23 | seq=$(basename "$0") |
| 24 | echo "QA output created by $seq" |
| 25 | |
| 26 | status=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 | } |
| 36 | trap "_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 |
| 45 | if [ "$IMGOPTSSYNTAX" = "true" ]; then |
| 46 | _unsupported_fmt $IMGFMT |
| 47 | fi |
| 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 | _require_disk_usage |
| 55 | |
| 56 | _flaky_test https://gitlab.com/qemu-project/qemu/-/work_items/3514 |
| 57 | |
| 58 | # $1: Export ID |
| 59 | # $2: Options (beyond the node-name and ID) |
| 60 | # $3: Expected return value (defaults to 'return') |
| 61 | # $4: Node to export (defaults to 'node-format') |
| 62 | fuse_export_add() |
| 63 | { |
| 64 | # The grep -v is a filter for errors when /etc/fuse.conf does not contain |
| 65 | # user_allow_other. (The error is benign, but it is printed by fusermount |
| 66 | # on the first mount attempt, so our export code cannot hide it.) |
| 67 | _send_qemu_cmd $QEMU_HANDLE \ |
| 68 | "{'execute': 'block-export-add', |
| 69 | 'arguments': { |
| 70 | 'type': 'fuse', |
| 71 | 'id': '$1', |
| 72 | 'node-name': '${4:-node-format}', |
| 73 | $2 |
| 74 | } }" \ |
| 75 | "${3:-return}" \ |
| 76 | | _filter_imgfmt \ |
| 77 | | grep -v 'option allow_other only allowed if' |
| 78 | } |
| 79 | |
| 80 | # $1: Export ID |
| 81 | fuse_export_del() |
| 82 | { |
| 83 | capture_events="BLOCK_EXPORT_DELETED" \ |
| 84 | _send_qemu_cmd $QEMU_HANDLE \ |
| 85 | "{'execute': 'block-export-del', |
| 86 | 'arguments': { |
| 87 | 'id': '$1' |
| 88 | } }" \ |
| 89 | 'return' |
| 90 | |
| 91 | _wait_event $QEMU_HANDLE \ |
| 92 | 'BLOCK_EXPORT_DELETED' |
| 93 | } |
| 94 | |
| 95 | # Return the length of the protocol file |
| 96 | # $1: Protocol node export mount point |
| 97 | # $2: Original file (to compare) |
| 98 | get_proto_len() |
| 99 | { |
| 100 | len1=$(stat -c '%s' "$1") |
| 101 | len2=$(stat -c '%s' "$2") |
| 102 | |
| 103 | if [ "$len1" != "$len2" ]; then |
| 104 | echo 'ERROR: Length of export and original differ:' >&2 |
| 105 | echo "$len1 != $len2" >&2 |
| 106 | else |
| 107 | echo '(OK: Lengths of export and original are the same)' >&2 |
| 108 | fi |
| 109 | |
| 110 | echo "$len1" |
| 111 | } |
| 112 | |
| 113 | COPIED_IMG="$TEST_IMG.copy" |
| 114 | EXT_MP="$TEST_IMG.fuse" |
| 115 | |
| 116 | echo '=== Set up ===' |
| 117 | |
| 118 | # Create image with random data |
| 119 | _make_test_img 64M |
| 120 | $QEMU_IO -c 'write -s /dev/urandom 0 64M' "$TEST_IMG" | _filter_qemu_io |
| 121 | |
| 122 | _launch_qemu |
| 123 | _send_qemu_cmd $QEMU_HANDLE \ |
| 124 | "{'execute': 'qmp_capabilities'}" \ |
| 125 | 'return' |
| 126 | |
| 127 | # Separate blockdev-add calls for format and protocol so we can remove |
| 128 | # the format layer later on |
| 129 | _send_qemu_cmd $QEMU_HANDLE \ |
| 130 | "{'execute': 'blockdev-add', |
| 131 | 'arguments': { |
| 132 | 'driver': 'file', |
| 133 | 'node-name': 'node-protocol', |
| 134 | 'filename': '$TEST_IMG' |
| 135 | } }" \ |
| 136 | 'return' |
| 137 | |
| 138 | _send_qemu_cmd $QEMU_HANDLE \ |
| 139 | "{'execute': 'blockdev-add', |
| 140 | 'arguments': { |
| 141 | 'driver': '$IMGFMT', |
| 142 | 'node-name': 'node-format', |
| 143 | 'file': 'node-protocol' |
| 144 | } }" \ |
| 145 | 'return' |
| 146 | |
| 147 | echo |
| 148 | echo '=== Mountpoint not present ===' |
| 149 | |
| 150 | rmdir "$EXT_MP" 2>/dev/null |
| 151 | rm -f "$EXT_MP" |
| 152 | output=$(fuse_export_add 'export-err' "'mountpoint': '$EXT_MP'" error) |
| 153 | |
| 154 | if echo "$output" | grep -q "Parameter 'type' does not accept value 'fuse'"; then |
| 155 | _notrun 'No FUSE support' |
| 156 | fi |
| 157 | |
| 158 | echo "$output" |
| 159 | |
| 160 | echo |
| 161 | echo '=== Mountpoint is a directory ===' |
| 162 | |
| 163 | mkdir "$EXT_MP" |
| 164 | fuse_export_add 'export-err' "'mountpoint': '$EXT_MP'" error |
| 165 | rmdir "$EXT_MP" |
| 166 | |
| 167 | echo |
| 168 | echo '=== Mountpoint is a regular file ===' |
| 169 | |
| 170 | touch "$EXT_MP" |
| 171 | fuse_export_add 'export-mp' "'mountpoint': '$EXT_MP'" |
| 172 | |
| 173 | # Check that the export presents the same data as the original image |
| 174 | $QEMU_IMG compare -f raw -F $IMGFMT -U "$EXT_MP" "$TEST_IMG" |
| 175 | |
| 176 | # Some quick chmod tests |
| 177 | stat -c 'Permissions pre-chmod: %a' "$EXT_MP" |
| 178 | |
| 179 | # Verify that we cannot set +w |
| 180 | chmod u+w "$EXT_MP" 2>&1 | _filter_testdir | _filter_imgfmt |
| 181 | stat -c 'Permissions post-+w: %a' "$EXT_MP" |
| 182 | |
| 183 | # Same for other flags, like, say +x |
| 184 | chmod u+x "$EXT_MP" 2>&1 | _filter_testdir | _filter_imgfmt |
| 185 | stat -c 'Permissions post-+x: %a' "$EXT_MP" |
| 186 | |
| 187 | echo |
| 188 | echo '=== Mount over existing file ===' |
| 189 | |
| 190 | # This is the coolest feature of FUSE exports: You can transparently |
| 191 | # make images in any format appear as raw images |
| 192 | fuse_export_add 'export-img' "'mountpoint': '$TEST_IMG'" |
| 193 | |
| 194 | # Accesses both exports at the same time, so we get a concurrency test |
| 195 | $QEMU_IMG compare -f raw -F raw -U "$EXT_MP" "$TEST_IMG" |
| 196 | |
| 197 | # Just to be sure, we later want to compare the data offline. Also, |
| 198 | # this allows us to see that cp works without complaining. |
| 199 | # (This is not a given, because cp will expect a short read at EOF. |
| 200 | # Internally, qemu does not allow short reads, so we have to check |
| 201 | # whether the FUSE export driver lets them work.) |
| 202 | cp "$TEST_IMG" "$COPIED_IMG" |
| 203 | |
| 204 | # $TEST_IMG will be in mode 0400 because it is read-only; we are going |
| 205 | # to write to the copy, so make it writable |
| 206 | chmod 0600 "$COPIED_IMG" |
| 207 | |
| 208 | echo |
| 209 | echo '=== Double export ===' |
| 210 | |
| 211 | # We have already seen that exporting a node twice works fine, but you |
| 212 | # cannot export anything twice on the same mount point. The reason is |
| 213 | # that qemu has to stat the given mount point, and this would have to |
| 214 | # be answered by the same qemu instance if it already has an export |
| 215 | # there. However, it cannot answer the stat because it is itself |
| 216 | # caught up in that same stat. |
| 217 | fuse_export_add 'export-err' "'mountpoint': '$EXT_MP'" error |
| 218 | |
| 219 | echo |
| 220 | echo '=== Remove export ===' |
| 221 | |
| 222 | # Double-check that $EXT_MP appears as a non-empty file (the raw image) |
| 223 | $QEMU_IMG info -f raw "$EXT_MP" | grep 'virtual size' | head -n 1 |
| 224 | |
| 225 | fuse_export_del 'export-mp' |
| 226 | |
| 227 | # See that the file appears empty again |
| 228 | $QEMU_IMG info -f raw "$EXT_MP" | grep 'virtual size' | head -n 1 |
| 229 | |
| 230 | echo |
| 231 | echo '=== Writable export ===' |
| 232 | |
| 233 | fuse_export_add 'export-mp' "'mountpoint': '$EXT_MP', 'writable': true" |
| 234 | |
| 235 | # Check that writing to the read-only export fails |
| 236 | output=$($QEMU_IO -f raw -c 'write -P 42 1M 64k' "$TEST_IMG" 2>&1 \ |
| 237 | | _filter_qemu_io | _filter_testdir | _filter_imgfmt) |
| 238 | |
| 239 | # Expected reference output: Opening the file fails because it has no |
| 240 | # write permission |
| 241 | reference="Could not open 'TEST_DIR/t.IMGFMT': Read-only file system" |
| 242 | |
| 243 | if echo "$output" | grep -q "$reference"; then |
| 244 | echo "Writing to read-only export failed: OK" |
| 245 | elif echo "$output" | grep -q "write failed: Permission denied"; then |
| 246 | # With CAP_DAC_OVERRIDE (e.g. when running this test as root), the export |
| 247 | # can be opened regardless of its file permissions, but writing will then |
| 248 | # fail. This is not the result for which we want to test, so count this as |
| 249 | # a SKIP. |
| 250 | _casenotrun "Opening RO export as R/W succeeded, perhaps because of" \ |
| 251 | "CAP_DAC_OVERRIDE" |
| 252 | |
| 253 | # Still, write this to the reference output to make the test pass |
| 254 | echo "Writing to read-only export failed: OK" |
| 255 | else |
| 256 | echo "Writing to read-only export failed: ERROR" |
| 257 | echo "$output" |
| 258 | fi |
| 259 | |
| 260 | # But here it should work |
| 261 | $QEMU_IO -f raw -c 'write -P 42 1M 64k' "$EXT_MP" | _filter_qemu_io |
| 262 | |
| 263 | # (Adjust the copy, too) |
| 264 | $QEMU_IO -f raw -c 'write -P 42 1M 64k' "$COPIED_IMG" | _filter_qemu_io |
| 265 | |
| 266 | echo |
| 267 | echo '=== Resizing exports ===' |
| 268 | |
| 269 | # Here, we need to export the protocol node -- the format layer may |
| 270 | # not be growable, simply because the format does not support it. |
| 271 | |
| 272 | # Remove all exports and the format node first so permissions will not |
| 273 | # get in the way |
| 274 | fuse_export_del 'export-mp' |
| 275 | fuse_export_del 'export-img' |
| 276 | |
| 277 | _send_qemu_cmd $QEMU_HANDLE \ |
| 278 | "{'execute': 'blockdev-del', |
| 279 | 'arguments': { |
| 280 | 'node-name': 'node-format' |
| 281 | } }" \ |
| 282 | 'return' |
| 283 | |
| 284 | # Now export the protocol node |
| 285 | fuse_export_add \ |
| 286 | 'export-mp' \ |
| 287 | "'mountpoint': '$EXT_MP', 'writable': true" \ |
| 288 | 'return' \ |
| 289 | 'node-protocol' |
| 290 | |
| 291 | echo |
| 292 | echo '--- Try growing non-growable export ---' |
| 293 | |
| 294 | # Get the current size so we can write beyond the EOF |
| 295 | orig_len=$(get_proto_len "$EXT_MP" "$TEST_IMG") |
| 296 | orig_disk_usage=$(disk_usage "$TEST_IMG") |
| 297 | |
| 298 | # Should fail (exports are non-growable by default) |
| 299 | # (Note that qemu-io can never write beyond the EOF, so we have to use |
| 300 | # dd here) |
| 301 | dd if=/dev/zero of="$EXT_MP" bs=1 count=64k seek=$orig_len \ |
| 302 | conv=notrunc 2>&1 \ |
| 303 | | _filter_testdir | _filter_imgfmt |
| 304 | |
| 305 | # And one really squarely post-EOF write |
| 306 | dd if=/dev/zero of="$EXT_MP" bs=1 count=1 seek=$((orig_len + 32 * 1024)) \ |
| 307 | conv=notrunc 2>&1 \ |
| 308 | | _filter_testdir | _filter_imgfmt |
| 309 | |
| 310 | # Half-post-EOF reads |
| 311 | dd if="$EXT_MP" of=/dev/null bs=1 count=64k skip=$((orig_len - 32 * 1024)) \ |
| 312 | 2>&1 | _filter_testdir | _filter_imgfmt |
| 313 | |
| 314 | # And one really squarely post-EOF read |
| 315 | dd if="$EXT_MP" of=/dev/null bs=1 count=1 skip=$((orig_len + 32 * 1024)) \ |
| 316 | 2>&1 | _filter_testdir | _filter_imgfmt |
| 317 | |
| 318 | echo |
| 319 | echo '--- Resize export ---' |
| 320 | |
| 321 | # But we can truncate it explicitly; even with fallocate |
| 322 | # (Make sure we extend it to a length not divisible by 128k, we need that below) |
| 323 | bs=$((128 * 1024)) |
| 324 | extend_to=$(((orig_len + bs - 1) / bs * bs + bs / 2)) |
| 325 | extend_by=$((extend_to - orig_len)) |
| 326 | |
| 327 | fallocate -o "$orig_len" -l $extend_by "$EXT_MP" |
| 328 | |
| 329 | new_len=$(get_proto_len "$EXT_MP" "$TEST_IMG") |
| 330 | if [ "$new_len" != "$extend_to" ]; then |
| 331 | echo 'ERROR: Unexpected post-truncate image size:' |
| 332 | echo "$new_len != $extend_to" |
| 333 | else |
| 334 | echo 'OK: Post-truncate image size is as expected' |
| 335 | fi |
| 336 | |
| 337 | new_disk_usage=$(disk_usage "$TEST_IMG") |
| 338 | if [ "$new_disk_usage" -gt "$orig_disk_usage" ]; then |
| 339 | echo 'OK: Disk usage grew with fallocate' |
| 340 | else |
| 341 | echo 'ERROR: Disk usage did not grow despite fallocate:' |
| 342 | echo "$orig_disk_usage => $new_disk_usage" |
| 343 | fi |
| 344 | |
| 345 | # Use this opportunity to test a read access across the (now no longer so much |
| 346 | # aligned) EOF. dd can only do requests with a length of its block size, and |
| 347 | # all of its seek/skip values are in bs units, so it is hard to do a request |
| 348 | # across the EOF if the EOF is at a power of two (64M). |
| 349 | dd if="$EXT_MP" of=/dev/null bs=$bs count=2 skip=$((extend_to / bs)) \ |
| 350 | 2>&1 | _filter_testdir | _filter_imgfmt |
| 351 | |
| 352 | echo |
| 353 | echo '--- Try growing growable export ---' |
| 354 | |
| 355 | # Now export as growable |
| 356 | fuse_export_del 'export-mp' |
| 357 | fuse_export_add \ |
| 358 | 'export-mp' \ |
| 359 | "'mountpoint': '$EXT_MP', 'writable': true, 'growable': true" \ |
| 360 | 'return' \ |
| 361 | 'node-protocol' |
| 362 | |
| 363 | # Now we should be able to write beyond the EOF |
| 364 | dd if=/dev/zero of="$EXT_MP" bs=1 count=64k seek=$new_len conv=notrunc 2>&1 \ |
| 365 | | _filter_testdir | _filter_imgfmt |
| 366 | |
| 367 | new_len=$(get_proto_len "$EXT_MP" "$TEST_IMG") |
| 368 | if [ "$new_len" != "$((extend_to + 65536))" ]; then |
| 369 | echo 'ERROR: Unexpected post-grow image size:' |
| 370 | echo "$new_len != $((extend_to + 65536))" |
| 371 | else |
| 372 | echo 'OK: Post-grow image size is as expected' |
| 373 | fi |
| 374 | |
| 375 | echo |
| 376 | echo '--- Shrink export ---' |
| 377 | |
| 378 | # Now go back to the original size |
| 379 | truncate -s "$orig_len" "$EXT_MP" |
| 380 | |
| 381 | new_len=$(get_proto_len "$EXT_MP" "$TEST_IMG") |
| 382 | if [ "$new_len" != "$orig_len" ]; then |
| 383 | echo 'ERROR: Unexpected post-truncate image size:' |
| 384 | echo "$new_len != $orig_len" |
| 385 | else |
| 386 | echo 'OK: Post-truncate image size is as expected' |
| 387 | fi |
| 388 | |
| 389 | echo |
| 390 | echo '=== Tear down ===' |
| 391 | |
| 392 | _send_qemu_cmd $QEMU_HANDLE \ |
| 393 | "{'execute': 'quit'}" \ |
| 394 | 'return' |
| 395 | |
| 396 | wait=yes _cleanup_qemu |
| 397 | |
| 398 | echo |
| 399 | echo '=== Compare copy with original ===' |
| 400 | |
| 401 | $QEMU_IMG compare -f raw -F $IMGFMT "$COPIED_IMG" "$TEST_IMG" |
| 402 | _cleanup_test_img |
| 403 | |
| 404 | echo |
| 405 | echo '=== Writing zeroes while unmapping ===' |
| 406 | # Regression test for https://gitlab.com/qemu-project/qemu/-/issues/1507 |
| 407 | _make_test_img 64M |
| 408 | $QEMU_IO -c 'write -s /dev/urandom 0 64M' "$TEST_IMG" | _filter_qemu_io |
| 409 | |
| 410 | _launch_qemu |
| 411 | _send_qemu_cmd $QEMU_HANDLE \ |
| 412 | "{'execute': 'qmp_capabilities'}" \ |
| 413 | 'return' |
| 414 | |
| 415 | _send_qemu_cmd $QEMU_HANDLE \ |
| 416 | "{'execute': 'blockdev-add', |
| 417 | 'arguments': { |
| 418 | 'driver': '$IMGFMT', |
| 419 | 'node-name': 'node-format', |
| 420 | 'file': { |
| 421 | 'driver': 'file', |
| 422 | 'filename': '$TEST_IMG' |
| 423 | } |
| 424 | } }" \ |
| 425 | 'return' |
| 426 | |
| 427 | fuse_export_add 'export' "'mountpoint': '$EXT_MP', 'writable': true" |
| 428 | |
| 429 | # Try writing zeroes by unmapping |
| 430 | $QEMU_IO -f raw -c 'write -zu 0 64M' "$EXT_MP" | _filter_qemu_io |
| 431 | |
| 432 | # Check the result |
| 433 | $QEMU_IO -f raw -c 'read -P 0 0 64M' "$EXT_MP" | _filter_qemu_io |
| 434 | |
| 435 | _send_qemu_cmd $QEMU_HANDLE \ |
| 436 | "{'execute': 'quit'}" \ |
| 437 | 'return' |
| 438 | |
| 439 | wait=yes _cleanup_qemu |
| 440 | |
| 441 | # Check the original image |
| 442 | $QEMU_IO -c 'read -P 0 0 64M' "$TEST_IMG" | _filter_qemu_io |
| 443 | |
| 444 | _cleanup_test_img |
| 445 | |
| 446 | echo |
| 447 | echo '=== Multi-threading ===' |
| 448 | |
| 449 | # Just set up a null block device, export it (with multi-threading), and run |
| 450 | # qemu-img bench on it (to get parallel requests) |
| 451 | |
| 452 | _launch_qemu |
| 453 | _send_qemu_cmd $QEMU_HANDLE \ |
| 454 | "{'execute': 'qmp_capabilities'}" \ |
| 455 | 'return' |
| 456 | |
| 457 | _send_qemu_cmd $QEMU_HANDLE \ |
| 458 | "{'execute': 'blockdev-add', |
| 459 | 'arguments': { |
| 460 | 'driver': 'null-co', |
| 461 | 'node-name': 'null' |
| 462 | } }" \ |
| 463 | 'return' |
| 464 | |
| 465 | for id in iothread{0,1,2,3}; do |
| 466 | _send_qemu_cmd $QEMU_HANDLE \ |
| 467 | "{'execute': 'object-add', |
| 468 | 'arguments': { |
| 469 | 'qom-type': 'iothread', |
| 470 | 'id': '$id' |
| 471 | } }" \ |
| 472 | 'return' |
| 473 | done |
| 474 | |
| 475 | echo |
| 476 | |
| 477 | iothreads="['iothread0', 'iothread1', 'iothread2', 'iothread3']" |
| 478 | fuse_export_add \ |
| 479 | 'export' \ |
| 480 | "'mountpoint': '$EXT_MP', 'iothread': $iothreads" \ |
| 481 | 'return' \ |
| 482 | 'null' |
| 483 | |
| 484 | echo |
| 485 | $QEMU_IMG bench -f raw "$EXT_MP" | |
| 486 | sed -e 's/[0-9.]\+ seconds/X.XXX seconds/' |
| 487 | echo |
| 488 | |
| 489 | fuse_export_del 'export' |
| 490 | |
| 491 | _send_qemu_cmd $QEMU_HANDLE \ |
| 492 | "{'execute': 'quit'}" \ |
| 493 | 'return' |
| 494 | |
| 495 | wait=yes _cleanup_qemu |
| 496 | |
| 497 | # success, all done |
| 498 | echo "*** done" |
| 499 | rm -f $seq.full |
| 500 | status=0 |