master
text 500 lines 13.8 KB
Raw
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