]>
Commit | Line | Data |
---|---|---|
e6c79647 | 1 | #!/usr/bin/env bash |
9dd003a9 | 2 | # group: rw |
e6c79647 HR |
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 | { | |
8fc54f94 HR |
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.) | |
e6c79647 HR |
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}" \ | |
8fc54f94 HR |
73 | | _filter_imgfmt \ |
74 | | grep -v 'option allow_other only allowed if' | |
e6c79647 HR |
75 | } |
76 | ||
77 | # $1: Export ID | |
78 | fuse_export_del() | |
79 | { | |
effd60c8 | 80 | capture_events="BLOCK_EXPORT_DELETED" \ |
e6c79647 HR |
81 | _send_qemu_cmd $QEMU_HANDLE \ |
82 | "{'execute': 'block-export-del', | |
83 | 'arguments': { | |
84 | 'id': '$1' | |
85 | } }" \ | |
86 | 'return' | |
87 | ||
effd60c8 | 88 | _wait_event $QEMU_HANDLE \ |
e6c79647 HR |
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 | ||
ea29331b | 151 | if echo "$output" | grep -q "Parameter 'type' does not accept value 'fuse'"; then |
e6c79647 HR |
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 | ||
f29add26 HR |
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 | ||
e6c79647 HR |
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) | |
74163add | 220 | $QEMU_IMG info -f raw "$EXT_MP" | grep 'virtual size' | head -n 1 |
e6c79647 HR |
221 | |
222 | fuse_export_del 'export-mp' | |
223 | ||
224 | # See that the file appears empty again | |
74163add | 225 | $QEMU_IMG info -f raw "$EXT_MP" | grep 'virtual size' | head -n 1 |
e6c79647 HR |
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 | |
e2eec281 HR |
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 | |
e6c79647 HR |
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" | |
27e0d8b5 HC |
373 | _cleanup_test_img |
374 | ||
375 | echo | |
376 | echo '=== Writing zeroes while unmapping ===' | |
377 | # Regression test for https://gitlab.com/qemu-project/qemu/-/issues/1507 | |
378 | _make_test_img 64M | |
379 | $QEMU_IO -c 'write -s /dev/urandom 0 64M' "$TEST_IMG" | _filter_qemu_io | |
380 | ||
381 | _launch_qemu | |
382 | _send_qemu_cmd $QEMU_HANDLE \ | |
383 | "{'execute': 'qmp_capabilities'}" \ | |
384 | 'return' | |
385 | ||
386 | _send_qemu_cmd $QEMU_HANDLE \ | |
387 | "{'execute': 'blockdev-add', | |
388 | 'arguments': { | |
389 | 'driver': '$IMGFMT', | |
390 | 'node-name': 'node-format', | |
391 | 'file': { | |
392 | 'driver': 'file', | |
393 | 'filename': '$TEST_IMG' | |
394 | } | |
395 | } }" \ | |
396 | 'return' | |
397 | ||
398 | fuse_export_add 'export' "'mountpoint': '$EXT_MP', 'writable': true" | |
399 | ||
400 | # Try writing zeroes by unmapping | |
401 | $QEMU_IO -f raw -c 'write -zu 0 64M' "$EXT_MP" | _filter_qemu_io | |
402 | ||
403 | # Check the result | |
404 | $QEMU_IO -f raw -c 'read -P 0 0 64M' "$EXT_MP" | _filter_qemu_io | |
405 | ||
406 | _send_qemu_cmd $QEMU_HANDLE \ | |
407 | "{'execute': 'quit'}" \ | |
408 | 'return' | |
409 | ||
410 | wait=yes _cleanup_qemu | |
411 | ||
412 | # Check the original image | |
413 | $QEMU_IO -c 'read -P 0 0 64M' "$TEST_IMG" | _filter_qemu_io | |
414 | ||
415 | _cleanup_test_img | |
e6c79647 HR |
416 | |
417 | # success, all done | |
418 | echo "*** done" | |
419 | rm -f $seq.full | |
420 | status=0 |