308 (10819B)
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 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') 59 fuse_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 78 fuse_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) 95 get_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 110 COPIED_IMG="$TEST_IMG.copy" 111 EXT_MP="$TEST_IMG.fuse" 112 113 echo '=== 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 144 echo 145 echo '=== Mountpoint not present ===' 146 147 rmdir "$EXT_MP" 2>/dev/null 148 rm -f "$EXT_MP" 149 output=$(fuse_export_add 'export-err' "'mountpoint': '$EXT_MP'" error) 150 151 if echo "$output" | grep -q "Parameter 'type' does not accept value 'fuse'"; then 152 _notrun 'No FUSE support' 153 fi 154 155 echo "$output" 156 157 echo 158 echo '=== Mountpoint is a directory ===' 159 160 mkdir "$EXT_MP" 161 fuse_export_add 'export-err' "'mountpoint': '$EXT_MP'" error 162 rmdir "$EXT_MP" 163 164 echo 165 echo '=== Mountpoint is a regular file ===' 166 167 touch "$EXT_MP" 168 fuse_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 174 stat -c 'Permissions pre-chmod: %a' "$EXT_MP" 175 176 # Verify that we cannot set +w 177 chmod u+w "$EXT_MP" 2>&1 | _filter_testdir | _filter_imgfmt 178 stat -c 'Permissions post-+w: %a' "$EXT_MP" 179 180 # But that we can set, say, +x (if we are so inclined) 181 chmod u+x "$EXT_MP" 2>&1 | _filter_testdir | _filter_imgfmt 182 stat -c 'Permissions post-+x: %a' "$EXT_MP" 183 184 echo 185 echo '=== 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 189 fuse_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.) 199 cp "$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 203 chmod 0600 "$COPIED_IMG" 204 205 echo 206 echo '=== 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. 214 fuse_export_add 'export-err' "'mountpoint': '$EXT_MP'" error 215 216 echo 217 echo '=== 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 222 fuse_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 227 echo 228 echo '=== Writable export ===' 229 230 fuse_export_add 'export-mp' "'mountpoint': '$EXT_MP', 'writable': true" 231 232 # Check that writing to the read-only export fails 233 output=$($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 238 reference="Could not open 'TEST_DIR/t.IMGFMT': Permission denied" 239 240 if echo "$output" | grep -q "$reference"; then 241 echo "Writing to read-only export failed: OK" 242 elif 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" 252 else 253 echo "Writing to read-only export failed: ERROR" 254 echo "$output" 255 fi 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 263 echo 264 echo '=== 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 271 fuse_export_del 'export-mp' 272 fuse_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 282 fuse_export_add \ 283 'export-mp' \ 284 "'mountpoint': '$EXT_MP', 'writable': true" \ 285 'return' \ 286 'node-protocol' 287 288 echo 289 echo '--- Try growing non-growable export ---' 290 291 # Get the current size so we can write beyond the EOF 292 orig_len=$(get_proto_len "$EXT_MP" "$TEST_IMG") 293 orig_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) 298 dd if=/dev/zero of="$EXT_MP" bs=1 count=64k seek=$orig_len 2>&1 \ 299 | _filter_testdir | _filter_imgfmt 300 301 echo 302 echo '--- Resize export ---' 303 304 # But we can truncate it explicitly; even with fallocate 305 fallocate -o "$orig_len" -l 64k "$EXT_MP" 306 307 new_len=$(get_proto_len "$EXT_MP" "$TEST_IMG") 308 if [ "$new_len" != "$((orig_len + 65536))" ]; then 309 echo 'ERROR: Unexpected post-truncate image size:' 310 echo "$new_len != $((orig_len + 65536))" 311 else 312 echo 'OK: Post-truncate image size is as expected' 313 fi 314 315 new_disk_usage=$(stat -c '%b' "$TEST_IMG") 316 if [ "$new_disk_usage" -gt "$orig_disk_usage" ]; then 317 echo 'OK: Disk usage grew with fallocate' 318 else 319 echo 'ERROR: Disk usage did not grow despite fallocate:' 320 echo "$orig_disk_usage => $new_disk_usage" 321 fi 322 323 echo 324 echo '--- Try growing growable export ---' 325 326 # Now export as growable 327 fuse_export_del 'export-mp' 328 fuse_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 335 dd if=/dev/zero of="$EXT_MP" bs=1 count=64k seek=$new_len 2>&1 \ 336 | _filter_testdir | _filter_imgfmt 337 338 new_len=$(get_proto_len "$EXT_MP" "$TEST_IMG") 339 if [ "$new_len" != "$((orig_len + 131072))" ]; then 340 echo 'ERROR: Unexpected post-grow image size:' 341 echo "$new_len != $((orig_len + 131072))" 342 else 343 echo 'OK: Post-grow image size is as expected' 344 fi 345 346 echo 347 echo '--- Shrink export ---' 348 349 # Now go back to the original size 350 truncate -s "$orig_len" "$EXT_MP" 351 352 new_len=$(get_proto_len "$EXT_MP" "$TEST_IMG") 353 if [ "$new_len" != "$orig_len" ]; then 354 echo 'ERROR: Unexpected post-truncate image size:' 355 echo "$new_len != $orig_len" 356 else 357 echo 'OK: Post-truncate image size is as expected' 358 fi 359 360 echo 361 echo '=== Tear down ===' 362 363 _send_qemu_cmd $QEMU_HANDLE \ 364 "{'execute': 'quit'}" \ 365 'return' 366 367 wait=yes _cleanup_qemu 368 369 echo 370 echo '=== Compare copy with original ===' 371 372 $QEMU_IMG compare -f raw -F $IMGFMT "$COPIED_IMG" "$TEST_IMG" 373 374 # success, all done 375 echo "*** done" 376 rm -f $seq.full 377 status=0