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