From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-18.6 required=3.0 tests=BAYES_00,DKIM_INVALID, DKIM_SIGNED,HEADER_FROM_DIFFERENT_DOMAINS,INCLUDES_CR_TRAILER,INCLUDES_PATCH, MAILING_LIST_MULTI,MENTIONS_GIT_HOSTING,SPF_HELO_NONE,SPF_PASS autolearn=ham autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id 70AA5C4338F for ; Thu, 19 Aug 2021 10:27:13 +0000 (UTC) Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by mail.kernel.org (Postfix) with ESMTPS id 8EF1C60BD3 for ; Thu, 19 Aug 2021 10:27:12 +0000 (UTC) DMARC-Filter: OpenDMARC Filter v1.4.1 mail.kernel.org 8EF1C60BD3 Authentication-Results: mail.kernel.org; dmarc=fail (p=none dis=none) header.from=redhat.com Authentication-Results: mail.kernel.org; spf=pass smtp.mailfrom=nongnu.org Received: from localhost ([::1]:50190 helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1mGfGR-0008M8-Bv for qemu-devel@archiver.kernel.org; Thu, 19 Aug 2021 06:27:11 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]:38130) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1mGfEq-00071V-Dz for qemu-devel@nongnu.org; Thu, 19 Aug 2021 06:25:32 -0400 Received: from us-smtp-delivery-124.mimecast.com ([170.10.133.124]:27015) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1mGfEg-0003XZ-CE for qemu-devel@nongnu.org; Thu, 19 Aug 2021 06:25:31 -0400 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1629368718; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding; bh=CiS9nc8vpC8rZW4Hm6tSwk2MiEcMDZPBbRX9R3m0zJc=; b=bbsjAs54T/ak9BUFrN7o8T9IhLT0vOAJ2hVIsxJXROlNWv/iJKTb4qeFPZpXxOEquGBx8r kRoLrBMiUQvkWRKF2pOGRgTCQpVuTKy3DeI4I40JQJ7XZ/OPF8UzkFaHtp6yeM/PrnFg8p h9kjyyZ2szARsE9RUDCPSAHJvz0bu/8= Received: from mimecast-mx01.redhat.com (mimecast-mx01.redhat.com [209.132.183.4]) (Using TLS) by relay.mimecast.com with ESMTP id us-mta-248-iNg9wP-tPnChIuQLJilCgA-1; Thu, 19 Aug 2021 06:25:13 -0400 X-MC-Unique: iNg9wP-tPnChIuQLJilCgA-1 Received: from smtp.corp.redhat.com (int-mx04.intmail.prod.int.phx2.redhat.com [10.5.11.14]) (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by mimecast-mx01.redhat.com (Postfix) with ESMTPS id 7B851190D341 for ; Thu, 19 Aug 2021 10:25:12 +0000 (UTC) Received: from localhost (unknown [10.39.193.71]) by smtp.corp.redhat.com (Postfix) with ESMTPS id 3CF075DA2D; Thu, 19 Aug 2021 10:25:02 +0000 (UTC) From: Hanna Reitz To: qemu-devel@nongnu.org Subject: [qemu-web PATCH] Add a blog post about FUSE block exports Date: Thu, 19 Aug 2021 12:25:01 +0200 Message-Id: <20210819102501.69638-1-hreitz@redhat.com> MIME-Version: 1.0 X-Scanned-By: MIMEDefang 2.79 on 10.5.11.14 Authentication-Results: relay.mimecast.com; auth=pass smtp.auth=CUSA124A263 smtp.mailfrom=hreitz@redhat.com X-Mimecast-Spam-Score: 0 X-Mimecast-Originator: redhat.com Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: quoted-printable Received-SPF: pass client-ip=170.10.133.124; envelope-from=hreitz@redhat.com; helo=us-smtp-delivery-124.mimecast.com X-Spam_score_int: -34 X-Spam_score: -3.5 X-Spam_bar: --- X-Spam_report: (-3.5 / 5.0 requ) BAYES_00=-1.9, DKIMWL_WL_HIGH=-0.7, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, RCVD_IN_DNSWL_LOW=-0.7, RCVD_IN_MSPIKE_H2=-0.001, SPF_HELO_NONE=0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.23 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Cc: Kevin Wolf , Hanna Reitz , Stefan Hajnoczi Errors-To: qemu-devel-bounces+qemu-devel=archiver.kernel.org@nongnu.org Sender: "Qemu-devel" This post explains when FUSE block exports are useful, how they work, and that it is fun to export an image file on its own path so it looks like your image file (in whatever format it was) is a raw image now. Signed-off-by: Hanna Reitz --- You can also find this patch here: https://gitlab.com/hreitz/qemu-web fuse-blkexport-v1 My first patch to qemu-web, so I hope I am not doing anything overly stupid here (adding SVGs with extremely long lines comes to mind)... --- _posts/2021-08-18-fuse-blkexport.md | 488 ++++++++++++++++++++++ screenshots/2021-08-18-block-graph-a.svg | 2 + screenshots/2021-08-18-block-graph-b.svg | 2 + screenshots/2021-08-18-block-graph-c.svg | 2 + screenshots/2021-08-18-block-graph-d.svg | 2 + screenshots/2021-08-18-block-graph-e.svg | 2 + screenshots/2021-08-18-root-directory.svg | 2 + screenshots/2021-08-18-root-file.svg | 2 + 8 files changed, 502 insertions(+) create mode 100644 _posts/2021-08-18-fuse-blkexport.md create mode 100644 screenshots/2021-08-18-block-graph-a.svg create mode 100644 screenshots/2021-08-18-block-graph-b.svg create mode 100644 screenshots/2021-08-18-block-graph-c.svg create mode 100644 screenshots/2021-08-18-block-graph-d.svg create mode 100644 screenshots/2021-08-18-block-graph-e.svg create mode 100644 screenshots/2021-08-18-root-directory.svg create mode 100644 screenshots/2021-08-18-root-file.svg diff --git a/_posts/2021-08-18-fuse-blkexport.md b/_posts/2021-08-18-fuse-b= lkexport.md new file mode 100644 index 0000000..e6a55d0 --- /dev/null +++ b/_posts/2021-08-18-fuse-blkexport.md @@ -0,0 +1,488 @@ +--- +layout: post +title: "Exporting block devices as raw image files with FUSE" +date: 2021-08-18 18:00:00 +0200 +author: Hanna Reitz +categories: [storage, features, tutorials] +--- +Sometimes, there is a VM disk image whose contents you want to manipulate +without booting the VM. For raw images, that process is usually fairly si= mple, +because most Linux systems bring tools for the job, e.g.: +* *dd* to just copy data to and from given offsets, +* *parted* to manipulate the partition table, +* *kpartx* to present all partitions as block devices, +* *mount* to access filesystems=E2=80=99 contents. + +Sadly, but naturally, such tools only work for raw images, and not for ima= ges +e.g. in QEMU=E2=80=99s qcow2 format. To access such an image=E2=80=99s co= ntent, the format has +to be translated to create a raw image, for example by: +* Exporting the image file with `qemu-nbd -c` as an NBD block device file, +* Converting between image formats using `qemu-img convert`, +* Accessing the image from a guest, where it appears as a normal block dev= ice. + +Unfortunately, none of these methods is perfect: `qemu-nbd -c` generally +requires root rights, converting to a temporary raw copy requires addition= al +disk space and the conversion process takes time, and accessing the image = from a +guest is just quite cumbersome in general (and also specifically something= that +we set out to avoid in the first sentence of this blog post). + +As of QEMU 6.0, there is another method, namely FUSE block exports. +Conceptually, these are rather similar to using `qemu-nbd -c`, but they do= not +require root rights. + +**Note**: FUSE block exports are a feature that can be enabled or disabled +during the build process with `--enable-fuse` or `--disable-fuse`, respect= ively; +omitting either configure option will enable the feature if and only if li= bfuse3 +is present. It is possible that the QEMU build you are using does not hav= e FUSE +block export support, because it was not compiled in. + +FUSE (*Filesystem in Userspace*) is a technology to let userspace processe= s +provide filesystem drivers. For example, *sshfs* is a program that allows +mounting remote directories from a machine accessible via SSH. + +QEMU can use FUSE to make a virtual block device appear as a normal file o= n the +host, so that tools like *kpartx* can interact with it regardless of the i= mage +format. + +## Background information + +### File mounts + +A perhaps little-known fact is that, on Linux, filesystems do not need to = have +a root directory, they only need to have a root node. A filesystem that o= nly +provides a single regular file is perfectly valid. + +Conceptually, every filesystem is a tree, and mounting works by replacing = one +subtree of the global VFS tree by the mounted filesystem=E2=80=99s tree. = Normally, a +filesystem=E2=80=99s root node is a directory, like in the following examp= le: + +|![Regular filesystem: Root directory is mounted to a directory mount poin= t](/screenshots/2021-08-18-root-directory.svg)| +|:--:| +|*Fig. 1: Mounting a regular filesystem with a directory as its root node*= | + +Here, the directory `/foo` and its content (the files `/foo/a` and `/foo/b= `) are +shadowed by the new filesystem (showing `/foo/x` and `/foo/y`). + +Note that a filesystem=E2=80=99s root node generally has no name. After m= ounting, the +filesystem=E2=80=99s root directory=E2=80=99s name is determined by the or= iginal name of the +mount point. + +Because a tree does not need to have multiple nodes but may consist of jus= t a +single leaf, a filesystem with a file for its root node works just as well= , +though: + +|![Mounting a file root node to a regular file mount point](/screenshots/2= 021-08-18-root-file.svg)| +|:--:| +|*Fig. 2: Mounting a filesystem with a regular (unnamed) file as its root = node*| + +Here, FS B only consists of a single node, a regular file with no name. (= As +above, a filesystem=E2=80=99s root node is generally unnamed.) Consequentl= y, the mount +point for it must also be a regular file (`/foo/a` in our example), and ju= st +like before, the content of `/foo/a` is shadowed, and when opening it, one= will +instead see the contents of FS B=E2=80=99s unnamed root node. + +### QEMU block exports + +QEMU allows exporting block nodes via various protocols (as of 6.0: NBD, +vhost-user, FUSE). A block node is an element of QEMU=E2=80=99s block gra= ph (see e.g. +[Managing the New Block Layer](http://events17.linuxfoundation.org/sites/e= vents/files/slides/talk\_11.pdf), +a talk given at KVM Forum 2017), which can for example be attached to gues= t +devices. Here is a very simple example: + +|![Block graph: image file <-> file node (label: prot-node) <-> qcow2 node= (label: fmt-node) <-> virtio-blk guest device](/screenshots/2021-08-18-blo= ck-graph-a.svg)| +|:--:| +|*Fig. 3: A simple block graph for attaching a qcow2 image to a virtio-blk= guest device*| + +This is the simplest example for a block graph that connects a *virtio-blk= * +guest device to a qcow2 image file. The *file* block driver, instanced in= the +form of a block node named *prot-node*, accesses the actual file and provi= des +the node above it access to the raw content. This node above, named *fmt-= node*, +is handled by the *qcow2* block driver, which is capable of interpreting t= he +qcow2 format. Parents of this node will therefore see the actual content = of the +virtual disk that is represented by the qcow2 image. There is only one pa= rent +here, which is the *virtio-blk* guest device, which will thus see the virt= ual +disk. + +The command line to achieve the above could look something like this: +``` +$ qemu-system-x86_64 \ + -blockdev node-name=3Dprot-node,driver=3Dfile,filename=3D$image_path \ + -blockdev node-name=3Dfmt-node,driver=3Dqcow2,file=3Dprot-node \ + -device virtio-blk,drive=3Dfmt-node +``` + +Besides attaching guest devices to block nodes, you can also export them f= or +users outside of qemu, for example via NBD. Say you have a QMP channel op= en for +the QEMU instance above, then you could do this: +```json +{ + "execute": "nbd-server-start", + "arguments": { + "addr": { + "type": "inet", + "data": { + "host": "localhost", + "port": "10809" + } + } + } +} +{ + "execute": "block-export-add", + "arguments": { + "type": "nbd", + "id": "fmt-node-export", + "node-name": "fmt-node", + "name": "guest-disk" + } +} +``` + +This opens an NBD server on `localhost:10809`, which exports *fmt-node* (u= nder +the NBD export name *guest-disk*). The block graph looks as follows: + +|![Same block graph as fig. 3, but with an NBD server attached to fmt-node= ](/screenshots/2021-08-18-block-graph-b.svg)| +|:--:| +|*Fig. 4: Block graph extended by an NBD server*| + +NBD clients connecting to this server will see the raw disk as seen by the +guest =E2=80=93 we have *exported* the guest disk: + +``` +$ qemu-img info nbd://localhost/guest-disk +image: nbd://localhost:10809/guest-disk +file format: raw +virtual size: 20 GiB (21474836480 bytes) +disk size: unavailable +``` + +### QEMU storage daemon + +If you are not running a guest, and so do not need guest devices, but all = you +want is to use the QEMU block layer (for example to interpret the qcow2 fo= rmat) +and export nodes from the block graph, then you can use the more lightweig= ht +QEMU storage daemon instead of a full-blown QEMU process: + +``` +$ qemu-storage-daemon \ + --blockdev node-name=3Dprot-node,driver=3Dfile,filename=3D$image_path = \ + --blockdev node-name=3Dfmt-node,driver=3Dqcow2,file=3Dprot-node \ + --nbd-server addr.type=3Dinet,addr.host=3Dlocalhost,addr.port=3D10809 = \ + --export type=3Dnbd,id=3Dfmt-node-export,node-name=3Dfmt-node,name=3Dg= uest-disk +``` + +Which creates the following block graph: + +|![Block graph: image file <-> file node (label: prot-node) <-> qcow2 node= (label: fmt-node) <-> NBD server](/screenshots/2021-08-18-block-graph-c.sv= g)| +|:--:| +|*Fig. 5: Exporting a qcow2 image over NBD*| + +## FUSE block exports + +Besides NBD exports, QEMU also supports vhost-user and FUSE exports. FUSE= block +exports make QEMU become a FUSE driver that provides a filesystem that con= sists +of only a single node, namely a regular file that has the raw contents of = the +exported block node. QEMU will automatically mount this filesystem on a g= iven +existing regular file (which acts as the mount point, as described in the +=E2=80=9CFile mounts=E2=80=9D section). + +Thus, FUSE exports can be used like this: + +``` +$ touch mount-point + +$ qemu-storage-daemon \ + --blockdev node-name=3Dprot-node,driver=3Dfile,filename=3D$image_path = \ + --blockdev node-name=3Dfmt-node,driver=3Dqcow2,file=3Dprot-node \ + --export type=3Dfuse,id=3Dfmt-node-export,node-name=3Dfmt-node,mountpo= int=3Dmount-point +``` + +The mount point now appears as the raw VM disk that is stored in the qcow2 +image: +``` +$ qemu-img info mount-point +image: mount-point +file format: raw +virtual size: 20 GiB (21474836480 bytes) +disk size: 196 KiB +``` + +And *mount* tells us that this is indeed its own filesystem: +``` +$ mount | grep mount-point +/dev/fuse on /tmp/mount-point type fuse (rw,nosuid,nodev,relatime,user_id= =3D1000,group_id=3D100,default_permissions,allow_other,max_read=3D67108864) +``` + +The block graph looks like this: + +|![Block graph: image file <-> file node (label: prot-node) <-> qcow2 node= (label: fmt-node) <-> FUSE server <-> exported file](/screenshots/2021-08-= 18-block-graph-d.svg)| +|:--:| +|*Fig. 6: Exporting a qcow2 image over FUSE*| + +Closing the storage daemon (e.g. with Ctrl-C) automatically unmounts the e= xport, +turning the mount point back into an empty normal file: + +``` +$ mount | grep -c mount-point +0 + +$ qemu-img info mount-point +image: mount-point +file format: raw +virtual size: 0 B (0 bytes) +disk size: 0 B +``` + +## Mounting an image on itself + +So far, we have seen what FUSE exports are, how they work, and how they ca= n be +used. Now let=E2=80=99s add an interesting twist. + +### What happens to the old tree under a mount point? + +Mounting a filesystem only shadows the mount point=E2=80=99s original cont= ent, it does +not remove it. The original content can no longer be looked up via its +(absolute) path, but it is still there, much like a file that has been unl= inked +but is still open in some process. Here is an example: + +First, create some file in some directory, and have some process keep it o= pen: + +``` +$ mkdir foo + +$ echo 'Is anyone there?' > foo/bar + +$ irb +irb(main):001:0> f =3D File.open('foo/bar', 'r+') +=3D> # +irb(main):002:0> ^Z +[1] + 35494 suspended irb +``` + +Next, mount something on the directory: + +``` +$ sudo mount -t tmpfs tmpfs foo +``` + +The file cannot be found anymore (because *foo*=E2=80=99s content is shado= wed by the +mounted filesystem), but the process who kept it open can still read from = it, +and write to it: +``` +$ ls foo + +$ cat foo/bar +cat: foo/bar: No such file or directory + +$ fg +f.read +irb(main):002:0> f.read +=3D> "Is anyone there?\n" +irb(main):003:0> f.puts('Hello from the shadows!') +=3D> nil +irb(main):004:0> exit + +$ ls foo + +$ cat foo/bar +cat: foo/bar: No such file or directory +``` + +Unmounting the filesystem lets us see our file again, with its updated con= tent: +``` +$ sudo umount foo + +$ ls foo +bar + +$ cat foo/bar +Is anyone there? +Hello from the shadows! +``` + +### Letting a FUSE export shadow its image file + +The same principle applies to file mounts: The original inode is shadowed = (along +with its content), but it is still there for any process that opened it be= fore +the mount occurred. Because QEMU (or the storage daemon) opens the image = file +before mounting the FUSE export, you can therefore specify an image=E2=80= =99s path as +the mount point for its corresponding export: + +``` +$ qemu-img create -f qcow2 foo.qcow2 20G +Formatting 'foo.qcow2', fmt=3Dqcow2 cluster_size=3D65536 extended_l2=3Doff= compression_type=3Dzlib size=3D21474836480 lazy_refcounts=3Doff refcount_b= its=3D16 + +$ qemu-img info foo.qcow2 +image: foo.qcow2 +file format: qcow2 +virtual size: 20 GiB (21474836480 bytes) +disk size: 196 KiB +cluster_size: 65536 +Format specific information: + compat: 1.1 + compression type: zlib + lazy refcounts: false + refcount bits: 16 + corrupt: false + extended l2: false + +$ qemu-storage-daemon \ + --blockdev node-name=3Dnode0,driver=3Dqcow2,file.driver=3Dfile,file.fi= lename=3Dfoo.qcow2 \ + --export type=3Dfuse,id=3Dnode0-export,node-name=3Dnode0,mountpoint=3D= foo.qcow2 & +[1] 40843 + +$ qemu-img info foo.qcow2 +image: foo.qcow2 +file format: raw +virtual size: 20 GiB (21474836480 bytes) +disk size: 196 KiB + +$ kill %1 +[1] + 40843 done qemu-storage-daemon --blockdev --export +``` + +In graph form, that looks like this: + +|![Two graphs: First, foo.qcow2 is opened by QEMU; second, a FUSE server e= xports the raw disk under foo.qcow2, thus shadowing the original foo.qcow2]= (/screenshots/2021-08-18-block-graph-e.svg)| +|:--:| +|*Fig. 6: Exporting a qcow2 image via FUSE on its own path*| + +QEMU (or the storage daemon in this case) keeps the original (qcow2) file = open, +and so it keeps access to it, even after the mount. However, any other pr= ocess +that opens the image by name (i.e. `open("foo.qcow2")`) will open the raw = disk +image exported by QEMU. Therefore, it looks like the qcow2 image is in ra= w +format now. + +### `qemu-fuse-disk-export.py` + +Because the QEMU storage daemon command line tends to become kind of long,= I=E2=80=99ve +written a script to facilitate the process: +[qemu-fuse-disk-export.py](https://gitlab.com/hreitz/qemu-scripts/-/blob/m= ain/qemu-fuse-disk-export.py) +([direct download link](https://gitlab.com/hreitz/qemu-scripts/-/raw/main/= qemu-fuse-disk-export.py?inline=3Dfalse)). +This script automatically detects the image format, and its `--daemonize` = option +allows safe use in scripts, where it is important that the process blocks = until +the export is fully set up. + +Using `qemu-fuse-disk-export.py`, the above example looks like this: +``` +$ qemu-img info foo.qcow2 | grep 'file format' +file format: qcow2 + +$ qemu-fuse-disk-export.py foo.qcow2 & +[1] 13339 +All exports set up, ^C to revert + +$ qemu-img info foo.qcow2 | grep 'file format' +file format: raw + +$ kill -SIGINT %1 +[1] + 13339 done qemu-fuse-disk-export.py foo.qcow2 + +$ qemu-img info foo.qcow2 | grep 'file format' +file format: qcow2 +``` + +Or, with `--daemonize`/`-d`: +``` +$ qemu-img info foo.qcow2 | grep 'file format' +file format: qcow2 + +$ qemu-fuse-disk-export.py -dp qfde.pid foo.qcow2 + +$ qemu-img info foo.qcow2 | grep 'file format' +file format: raw + +$ kill -SIGINT $(cat qfde.pid) + +$ qemu-img info foo.qcow2 | grep 'file format' +file format: qcow2 +``` + +## Bringing it all together + +Now we know how to make disk images in any format understood by QEMU appea= r as +raw images. We can thus run any application on them that works with such = raw +disk images: + +``` +$ qemu-fuse-disk-export.py \ + -dp qfde.pid \ + Arch-Linux-x86_64-basic-20210711.28787.qcow2 + +$ parted Arch-Linux-x86_64-basic-20210711.28787.qcow2 p +WARNING: You are not superuser. Watch out for permissions. +Model: (file) +Disk /tmp/Arch-Linux-x86_64-basic-20210711.28787.qcow2: 42.9GB +Sector size (logical/physical): 512B/512B +Partition Table: gpt +Disk Flags: + +Number Start End Size File system Name Flags + 1 1049kB 2097kB 1049kB bios_grub + 2 2097kB 42.9GB 42.9GB btrfs + +$ sudo kpartx -av Arch-Linux-x86_64-basic-20210711.28787.qcow2 +add map loop0p1 (254:0): 0 2048 linear 7:0 2048 +add map loop0p2 (254:1): 0 83881951 linear 7:0 4096 + +$ sudo mount /dev/mapper/loop0p2 /mnt/tmp + +$ ls /mnt/tmp +bin boot dev etc home lib lib64 mnt opt proc root run sbin s= rv +swap sys tmp usr var + +$ echo 'Hello, qcow2 image!' > /mnt/tmp/home/arch/hello + +$ sudo umount /mnt/tmp + +$ sudo kpartx -d Arch-Linux-x86_64-basic-20210711.28787.qcow2 +loop deleted : /dev/loop0 + +$ kill -SIGINT $(cat qfde.pid) +``` + +And launching the image, in the guest we see: +``` +[arch@archlinux ~] cat hello +Hello, qcow2 image! +``` + +## A note on `allow_other` + +In the example presented in the above section, we access the exported imag= e with +a different user than the one who exported it (to be specific, we export i= t as a +normal user, and then access it as root). This does not work prior to QEM= U 6.1: + +``` +$ qemu-fuse-disk-export.py -dp qfde.pid foo.qcow2 + +$ sudo stat foo.qcow2 +stat: cannot statx 'foo.qcow2': Permission denied +``` + +QEMU 6.1 has introduced support for FUSE=E2=80=99s `allow_other` mount opt= ion. Without +that option, only the user who exported the image has access to it. By de= fault, +if the system allows for non-root users to add `allow_other` to FUSE mount +options, QEMU will add it, and otherwise omit it. It does so by simply +attempting to mount the export with `allow_other` first, and if that fails= , it +will try again without. (You can also force the behavior with the +`allow_other=3D(on|off|auto)` export parameter.) + +Non-root users can pass `allow_other` if and only if `/etc/fuse.conf` cont= ains +the `user_allow_other` option. + +## Conclusion + +As shown in this blog post, FUSE block exports are a relatively simple way= to +access images in any format understood by QEMU as if they were raw images. +Any tool that can manipulate raw disk images can thus manipulate images in= any +format, simply by having the QEMU storage daemon provide a translation lay= er. +By mounting the FUSE export on the original image path, this translation l= ayer +will effectively be invisible, and the original image will look like it is= in +raw format, so it can directly be accessed by those tools. + +The current main disadvantage of FUSE exports is that they offer relativel= y bad +performance. That should be fine as long as your use case is just light +manipulation of some VM images, like manually modifying some files on them= . +However, we did not yet really try to optimize performance, so if more ser= ious +use cases appear that would require better performance, we can try. diff --git a/screenshots/2021-08-18-block-graph-a.svg b/screenshots/2021-08= -18-block-graph-a.svg new file mode 100644 index 0000000..bea1ba3 --- /dev/null +++ b/screenshots/2021-08-18-block-graph-a.svg @@ -0,0 +1,2 @@ + += diff --git a/screenshots/2021-08-18-block-graph-b.svg b/screenshots/2021-08= -18-block-graph-b.svg new file mode 100644 index 0000000..5bf98af --- /dev/null +++ b/screenshots/2021-08-18-block-graph-b.svg @@ -0,0 +1,2 @@ + +<= g id=3D"text3313" stroke-width=3D".26458px" aria-label=3D"qcow2"><= path id=3D"path77" d=3D"m-62.815-20.016h0.6339v3.8585h-0.6339zm0-1.5021h0.6= 339v0.80271h-0.6339z"/>= <= path id=3D"path70" d=3D"m83.231 30.659v0.31006h-2.9146q0.04134 0.65457 0.39= 274 0.99908 0.35484 0.34106 0.9853 0.34106 0.36518 0 0.70624-0.08957 0.3445= 1-0.08957 0.68213-0.26872v0.59945q-0.34106 0.1447-0.69936 0.22049-0.35829 0= .07579-0.72692 0.07579-0.92329 0-1.4642-0.53744-0.53744-0.53744-0.53744-1.4= 538 0-0.9474 0.50987-1.5021 0.51332-0.55811 1.3815-0.55811 0.77859 0 1.2299= 0.50298 0.45475 0.49954 0.45475 1.3608zm-0.6339-0.18604q-0.0069-0.52021-0.= 29283-0.83027-0.2825-0.31006-0.75103-0.31006-0.53054 0-0.85094 0.29972-0.31= 695 0.29972-0.36518 0.84405z"/> diff --git a/screenshots/2021-08-18-block-graph-c.svg b/screenshots/2021-08= -18-block-graph-c.svg new file mode 100644 index 0000000..bcdf868 --- /dev/null +++ b/screenshots/2021-08-18-block-graph-c.svg @@ -0,0 +1,2 @@ + + diff --git a/screenshots/2021-08-18-block-graph-d.svg b/screenshots/2021-08= -18-block-graph-d.svg new file mode 100644 index 0000000..67c1e81 --- /dev/null +++ b/screenshots/2021-08-18-block-graph-d.svg @@ -0,0 +1,2 @@ + +<= rect id=3D"rect22371" x=3D"-66.56" y=3D"-29.781" width=3D"29.28" height=3D"= 17.692" fill=3D"#fff" fill-rule=3D"evenodd" stop-color=3D"#000000" stroke= =3D"#808080" stroke-width=3D".52917"/><= path id=3D"path89" d=3D"m34.487 33.747v0.21704h-2.0402q0.02894 0.4582 0.274= 92 0.69935 0.24839 0.23874 0.68971 0.23874 0.25563 0 0.49437-0.0627 0.24116= -0.0627 0.47749-0.1881v0.41961q-0.23874 0.10129-0.48955 0.15434-0.2508 0.05= 306-0.50884 0.05306-0.6463 0-1.0249-0.3762-0.3762-0.3762-0.3762-1.0177 0-0.= 66318 0.35691-1.0514 0.35932-0.39067 0.96704-0.39067 0.54501 0 0.86093 0.35= 209 0.31833 0.34968 0.31833 0.95257zm-0.44373-0.13022q-0.0048-0.36415-0.204= 98-0.58119-0.19775-0.21704-0.52572-0.21704-0.37138 0-0.59566 0.20981-0.2218= 6 0.20981-0.25563 0.59083z"/><= path id=3D"path149" d=3D"m78.072 102.17v1.0955h1.4401v0.49265h-1.4401v2.094= 6q0 0.42719 0.16192 0.596 0.16192 0.16881 0.565 0.16881h0.71314v0.50643h-0.= 77515q-0.71314 0-1.006-0.28594-0.29283-0.28595-0.29283-0.9853v-2.0946h-1.03= 01v-0.49265h1.0301v-1.0955z"/> diff --git a/screenshots/2021-08-18-block-graph-e.svg b/screenshots/2021-08= -18-block-graph-e.svg new file mode 100644 index 0000000..4bcc615 --- /dev/null +++ b/screenshots/2021-08-18-block-graph-e.svg @@ -0,0 +1,2 @@ + +<= rect id=3D"rect9379" x=3D"120.42" y=3D"48.868" width=3D"33.768" height=3D"1= 3.398" rx=3D"1.7964" ry=3D"1.8948" fill=3D"none" stop-color=3D"#000000" str= oke=3D"#000" stroke-width=3D".52917"/><= /g>= <= path id=3D"path174" d=3D"m31.878 103.71q-0.48231 0-0.73036 0.37551-0.24805 = 0.37552-0.24805 1.1128 0 0.7338 0.24805 1.1128 0.24805 0.37551 0.73036 0.37= 551 0.48576 0 0.73381-0.37551 0.24805-0.37897 0.24805-1.1128 0-0.73725-0.24= 805-1.1128-0.24805-0.37551-0.73381-0.37551zm0-0.53744q0.80271 0 1.2265 0.52= 021 0.42719 0.52021 0.42719 1.5055 0 0.98874-0.42375 1.509-0.42375 0.51677-= 1.2299 0.51677-0.80271 0-1.2265-0.51677-0.42375-0.52021-0.42375-1.509 0-0.9= 853 0.42375-1.5055t1.2265-0.52021z"/> diff --git a/screenshots/2021-08-18-root-directory.svg b/screenshots/2021-0= 8-18-root-directory.svg new file mode 100644 index 0000000..f9449f1 --- /dev/null +++ b/screenshots/2021-08-18-root-directory.svg @@ -0,0 +1,2 @@ + +<= path id=3D"path26121" transform=3D"scale(-.6)" d=3D"m8.7186 4.0337-10.926-4= .0177 10.926-4.0177c-1.7455 2.3721-1.7354 5.6175-6e-7 8.0354z" fill=3D"cont= ext-stroke" fill-rule=3D"evenodd" stroke-linejoin=3D"round" stroke-width=3D= ".625"/>= = <= path id=3D"path128" d=3D"m117 3.164v2.3289h-0.63389v-2.3082q0-0.54777-0.213= 6-0.81993-0.2136-0.27216-0.64079-0.27216-0.51332 0-0.8096 0.32728t-0.29628 = 0.89228v2.1807h-0.63734v-3.8585h0.63734v0.59945q0.22738-0.34795 0.53399-0.5= 2021 0.31006-0.17225 0.71314-0.17225 0.6649 0 1.006 0.41341 0.34106 0.40997= 0.34106 1.2092z"/> diff --git a/screenshots/2021-08-18-root-file.svg b/screenshots/2021-08-18-= root-file.svg new file mode 100644 index 0000000..b7b2797 --- /dev/null +++ b/screenshots/2021-08-18-root-file.svg @@ -0,0 +1,2 @@ + +<= path id=3D"path26121" transform=3D"scale(-.6)" d=3D"m8.7186 4.0337-10.926-4= .0177 10.926-4.0177c-1.7455 2.3721-1.7354 5.6175-6e-7 8.0354z" fill=3D"cont= ext-stroke" fill-rule=3D"evenodd" stroke-linejoin=3D"round" stroke-width=3D= ".625"/>= <= path id=3D"path25696" d=3D"m4.5344 23.535v22.663" fill=3D"none" stroke=3D"#= 000" stroke-linecap=3D"round" stroke-width=3D".52917" style=3D"paint-order:= normal"/> --=20 2.31.1