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
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.