Search code examples
linuxmountsysfsblock-device

How to get the block device name for a file?


This is yak-shaving an X-Y problem So I shall start from the beginning.

I wish to test the behaviour of a program which copies files from a source drive to a target drive which tries to be clever about whether the source directories are remote or local

See also What is the most efficient way to copy many files programmatically?

I'm testing for correctness only. The test platform does not have to measure performance reliably (though that would be nice). The point is to test scenarios the program is meant to handle like identifying the source or destination as a local drive using XFS or a remote drive using sshfs or nfs.

For this I want to set up some test file systems and copy data between them. This is similar to this question - Can I create a filesystem e.g. xfs in a Docker Container as part of an integration test?

I intend to include these tests in the automated test suite which has to run both locally and under Azure devops.

For local filesystems my plan is a strategy like this:

dd if=/dev/zero of=xfs.img bs=1M count=250
/usr/sbin/mkfs.xfs xfs.img
mkdir -p mnt/xfs1
guestmount -a xfs.img -m /dev/sda mntxfs

To mount an image in user space I have decided/discovered that guestmount is the best option. fusermount and udiskctl still require superuser privileges. Likewise I do not wish to wrestle with --privileged on the docker containers.

Vagrant would definitely solve the problem but its overkill if I can do it in a script or docker. (On Azure vagrant would be running inside docker anyway).

If I use guestmount -i I get:

guestmount: no operating system was found on this disk

So I must use -m which requires knowing the correct block device For instance:

guestmount -a xfs.img -m /dev/sda mntxfs

The issue here is identifying the block device containing xfs.img.

I can use df or fndmnt to find the filesystem/partition:

>findmnt -no source -T xfs.img
/dev/mapper/rhel-home

but guestmount does not like logical volumes:

>guestmount -a xfs.img -m /dev/mapper/rhel-home mntxfs
libguestfs: error: vfs_type: vfs_type_stub: /dev/mapper/rhel-home: No such file or directory
libguestfs: error: mount_options: mount_options_stub: /dev/mapper/rhel-home: No such file or directory
guestmount: /dev/mapper/rhel-home could not be mounted.
guestmount: Did you mean to mount one of these filesystems?
guestmount:     /dev/sda (xfs)

So the question is how to find the block device name for a file (or a partition)

You can get the device name for a partition:

ls -l /sys/class/block/sda1
lrwxrwxrwx. 1 root root 0 May 12 10:08 /sys/class/block/sda1 -> ../../devices/pci0000:ae/0000:ae:00.0/0000:af:00.0/host0/target0:2:0/0:2:0:0/block/sda/sda1
>lsblk -ndo pkname /dev/sda1
sda

but this fails for a logical volume:

>lsblk /dev/mapper/rhel-home 
NAME      MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
rhel-home 253:2    0  1.8T  0 lvm  /home

The information is there in lsblk but it is hard to parse:

>lsblk -a
NAME          MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
loop0           7:0    0  250M  0 loop /run/media/root/075889d6-43c8-45da-a1fb-d9b99d98716a
sda             8:0    0  931G  0 disk 
sda1          8:1    0    1G  0 part /boot
sda2          8:2    0  930G  0 part 
 rhel-root 253:0    0   70G  0 lvm  /
 rhel-swap 253:1    0    4G  0 lvm  [SWAP]
 rhel-home 253:2    0  1.8T  0 lvm  /home
sdb             8:16   0  931G  0 disk 
sdb1          8:17   0  931G  0 part 
 rhel-home 253:2    0  1.8T  0 lvm  /home

If you have a safe reliable way to get sdb or sda from rhel-home here please tell me. (I have no idea why /home on both sda and sdb here - if you do please explain)

It may be easier with the -fs option:

>lsblk -fs
NAME      FSTYPE      LABEL UUID                                   MOUNTPOINT
rhel-root xfs               eced9cd8-d1ac-4541-b596-09839ad91afb   /
sda2    LVM2_member       rqDCVg-8XR9-Gu0K-eNL1-vZt4-2wgr-yyRL4a 
  sda                                                            
rhel-home xfs               6be4c859-ff15-4242-aace-05f6afac9d93   /home
sda2    LVM2_member       rqDCVg-8XR9-Gu0K-eNL1-vZt4-2wgr-yyRL4a 
 sda                                                            
sdb1    LVM2_member       y772ZY-jJsE-AoOy-pVqf-71vC-aU79-pXxq6z 
 sdb                                   

The information is in /sys somewhere but I'm not sure how to extract it.

>grep -r rhel-home /sys 2>/dev/null
/sys/devices/virtual/block/dm-2/dm/name:rhel-home

I tried looking at the source for lsblk but so far have not worked out how it generates its tree.

I can identify that its a logical volme from lsblk by grepping for lvm

>lsblk | grep rhel-home 
rhel-home 253:2    0  1.8T  0 lvm  /home
rhel-home 253:2    0  1.8T  0 lvm  /home

If we know its a logical volume the question becomes https://serverfault.com/questions/461385/how-to-find-the-physical-volumes-that-hold-a-logical-volume-in-lvm lvs requires root:

>lvs -o +devices
 WARNING: Running as a non-root user. Functionality may be unavailable.
 /run/lock/lvm/P_global:aux: open failed: Permission denied

I can use the device number to locate the partition via:

>grep -r 253:2 /sys/devices/ 2>/dev/null
/sys/devices/virtual/block/dm-2/dev:253:2
>ls /sys/devices/virtual/block/dm-2/slaves/
sda2  sdb1

So I can do it. Its just extremely painful. There is far too much to go wrong if I want to put this in script.

Surely there is a better way?

guestmount already knows the block device as it suggests it. Parsing its error message would be an improvement on the above shenanigans.

I would also like to understand the layout of the relevant parts of /sys so I could write a better script, or a compiled program or library routine to do this.

See also Emulate a hard drive in Linux


Solution

  • Here is the hideous script I created to do this. Please let there be a better way!

    #!/bin/bash
    #
    # Given a file identify the block device on which it resides
    #
    
    PROGNAME=`basename $0`
    STATUS=0
    VERBOSE=0
    
    showUsage() {
        echo "usage:"
        echo "  $PROGNAME <filename>"
    }
    
    showHelp() {
        showUsage
        echo
        echo "$PROGNAME is a simple utility to identify the block device on which a file resides"
    }
    
    usageError() {
        if [ -z "$1" ]; then
        echo "$PROGNAME: error: incorrect usage" >&2
        else
        echo "$PROGNAME: error: incorrect usage: $1" >&2
        fi
        showUsage >&2
        exit 1
    }
    
    reportError() {
        echo "$PROGNAME: error: $1" >&2
        STATUS=1
    }
    
    reportFileError() {    
        echo "$1: error: $2" >&2
        STATUS=1
    }
    
    checkStatus() {
        RET=$?
        if [ $RET -ne 0 ]; then
        STATUS=$RET
        reportError "$1"
        exit $RET
        fi
    }
    
    while [ $# -gt 0 ]; do
       case "$1" in
       --)
          shift
          break;;
       --help)
          showHelp
          exit 0
          break;;
       --verbose)
          VERBOSE=1
          shift;;
       --*)
          usageError "unknown option \"$1\""
          shift;;
       *)
          break;;
       esac
    done
    
    FILE="$1"
    if [ $VERBOSE -eq 1 ]; then
        echo FILE=$FILE
    fi
    FILE=`readlink -f $FILE`
    
    if [ -z "$1" ]; then
        usageError "expected a filename"
        exit 1
    elif [ ! -e $FILE ]; then
        reportFileError "$FILE" "file does not exist"
        exit 1
    fi
    
    FILESYSTEM=`findmnt -no source -T $FILE`
    checkStatus "could not identify mount point"
    
    
    if [ $VERBOSE -eq 1 ]; then
        echo FILESYSTEM=$FILESYSTEM
    fi
    
    FS=`basename $FILESYSTEM`
    SYSLINE=`grep -rs $FS /sys/devices 2>/dev/null`
    test ! -z "$SYSLINE" 
    checkStatus "could not find device in /sys/devices"
    
    if [ $FS == "fuse" ]; then
        reportFileError "$FILE" "is on a fuse filesystem"
        exit 1
    fi
    
    echo $SYSLINE | grep -q virtual
    if [ $? -eq 0 ]; then
        LOGICALVOLUME=1
        if [ $VERBOSE -eq 1 ]; then
        echo "file is on a logical volume"
        fi
    else
        LOGICALVOLUME=0
    
    fi
    
    DEVICENO=`lsblk | grep $FS | awk '{ print $2; }' | head -n 1`
    checkStatus "could identify device number"
    test ! -z $DEVICENO
    checkStatus "could identify device number"
    
    if [ $VERBOSE -eq 1 ]; then
        echo "Device number $DEVICENO"
    fi
    
    if [ $LOGICALVOLUME -eq 1 ]; then
        DEVDIR=`grep -rs ${DEVICENO}$ /sys/devices/ 2>&1`
        DEVDIR=`dirname $DEVDIR`    
        DEVICE=`ls $DEVDIR/slaves | head -n 1`
        if [ $VERBOSE -eq 1 ]; then
        echo DEVICE=/dev/$DEVICE
        fi
        DEVICENO=`lsblk | grep $DEVICE | awk '{ print $2; }' | head -n 1`
        checkStatus "could identify parent device number"
        if [ $VERBOSE -eq 1 ]; then
        echo "Parent Device number $DEVICENO"
        fi
    fi
        
    # The first grep does not like the first colon so we double grep
    # Maybe a /sys pecularity?
    DEVLINE=`grep -rs ${DEVICENO}$ /sys/devices/ 2>&1 | grep :${DEVICENO}`
    checkStatus "could identify device from number in sys"
    
    DEVICE=`echo $DEVLINE | sed -e s_.*/block/__ | cut -f 1 -d/`
    if [ $VERBOSE -eq 1 ]; then
        echo -n "block device: "
    fi
    echo "/dev/$DEVICE"
    
    exit $STATUS
    

    This appears to work for any normal file or directory Here is some example output.

    Where /home is a logical volume.

    >./getblkdev.sh --verbose /home
    FILE=/home
    FILESYSTEM=/dev/mapper/rhel-home
    file is on a logical volume
    Device number 253:2
    DEVICE=/dev/sda2
    Parent Device number 8:2
    block device: /dev/sda
    

    Where /boot is a regular partition:

    >./getblkdev.sh --verbose /boot
    FILE=/boot
    FILESYSTEM=/dev/sda1
    Device number 8:1
    block device: /dev/sda
    

    Please suggest improvements in comments or with your own alternative answers be they scripts, programs in other languages or even pseudo-code with a better plan.