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