Mounting a KVM disk image without KVM

I've wanted several times to mount KVM disk images under the KVM host operating system, and I keep forgetting the technical details. Marco has needed this too, I remember, so I thought I'd write a post about it to remind me next time. The obvious answer is losetup, and that works great by itself if you use whole disks (e.g. /dev/sdb) for your filesystem in the guest OS. If you partition, there's a hitch: losetup puts the whole device onto (for example) /dev/loop0 and doesn't automatically give you a loop0p1, etc for the partitions. I asked the Internets about this and got back a lot of blog posts about using fdisk and a calculator to compute partition offsets within the disk image, then map the block file with those offsets in losetup. But nobody should have to live that way, and I knew there was a better approach.

A basic solution

That better approach is the Linux device mapper + the kpartx command. kpartx will scan a block device — e.g., your /dev/loop0 — and create device-mapped specials under /dev/mapper for the partitions. Then you can address individual partitions. (N.B. When done, before you can losetup -d the greater block device, you have to delete the device-mapped specials!) Here's a bit of terminal action to illustrate:

# First map the loop device to your KVM disk image.
$ losetup -fv /kvm/watercooler.mwt2.org.img 
Loop device is /dev/loop0

# No partitions!    
$ ls /dev/loop0* /dev/mapper/loop0*
ls: /dev/mapper/loop0*: No such file or directory
/dev/loop0

# Use kpartx -a to discover and device-map the partitions.
$ kpartx -av /dev/loop0
add map loop0p1 : 0 204800 linear /dev/loop0 2048
add map loop0p2 : 0 16930816 linear /dev/loop0 206848
add map loop0p3 : 0 16416768 linear /dev/loop0 17137664

# Look, partitions.
$ ls /dev/loop0* /dev/mapper/loop0*
/dev/loop0  /dev/mapper/loop0p1  /dev/mapper/loop0p2  /dev/mapper/loop0p3

# Fiddle with /dev/loop0p1 et al.

# Try to un-loop the file.  You can't, because kpartx's partition devices are still mapped.
$ losetup -d /dev/loop0
ioctl: LOOP_CLR_FD: Device or resource busy

# Ask kpartx to unmap.
$ kpartx -dv /dev/loop0
del devmap : loop0p1
del devmap : loop0p2
del devmap : loop0p3

# Now you can un-loop.
$ losetup -d /dev/loop0

The with command

Good and well. What if I want to do this often and easily? I once wrote a program named with that helps with such tasks. With is similar to the with construct in some high-level programming languages, which generally sets up a condition, performs a task, and then breaks down the condition. The with program performs shell-based pre and post actions, with another task sandwiched in the middle. It has a few ways of doing that, but easiest is with a with helper. Here's a snippet of what using with looks like:

$ with -v loop /kvm/watercooler.mwt2.org.img: /bin/ls -l /dev/mapper
Executing precommand: "with.loop" "/kvm/watercooler.mwt2.org.img"
add map loop0p1 : 0 204800 linear /dev/loop0 2048
add map loop0p2 : 0 16930816 linear /dev/loop0 206848
add map loop0p3 : 0 16416768 linear /dev/loop0 17137664
Executing command: "/bin/ls" "-l" "/dev/mapper"
total 0
crw------- 1 root root  10, 236 Mar 28 16:47 control
brw-r----- 1 root disk 253,   0 May 30 13:36 loop0p1
brw-r----- 1 root disk 253,   1 May 30 13:36 loop0p2
brw-r----- 1 root disk 253,   2 May 30 13:36 loop0p3
Executing postcommand: "with.loop" "/kvm/watercooler.mwt2.org.img"
del devmap : loop0p1
del devmap : loop0p2
del devmap : loop0p3

$ /bin/ls -l /dev/mapper
total 0
crw------- 1 root root 10, 236 Mar 28 16:47 control

In this code, the -v option makes with show the commands it's running. loop /kvm/watercooler.mwt2.org.img is a with helper that can perform setup and teardown for the condition I want to the true while executing /bin/ls -l: namely, that my /kvm/watercooler.mwt2.org.img file is loop-mapped and that its partitions are device-mapped. If you wanted to do a bunch of miscellaneous stuff with the devices, you could use:

$ with loop /kvm/watercooler.mwt2.org.img:

(When no arguments follow the colon, with launches your current $SHELL.)

For lack of a better way to link to with, here's a link to a Bitbucket repository that has it. I'll move other useful shell tools in here as the mood strikes me. Here's the helper script for this particular task. It just needs to be in your $PATH, to be named with.loop, and to be executable.

#!/bin/sh

start () {
    mapfile="$1"
    device=$(losetup -fv "$mapfile" | awk '{print $NF}')
    kpartx -av $device
    # write the device name back to the controlling "with" instance
    printf >&$WITH_FD $device
}

stop () {
    mapfile="$1"
    device="$WITH_DATA"
    kpartx -dv $device
    losetup -d $device
}

case "$WITH_MODE" in
    start)  start "$@";;
    stop)   stop "$@";;
esac