#!/bin/sh
#
# vm-mount.sh - mount Windows NTFS partitions residing on LVM volumes.
#
# Copyright (C) 2024 Heiko Sieger
# https://heiko-sieger.info
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
# Usage: sudo vm-mount.sh vm_volume
#
# where "vm_volume" is the name of the volume as found in
# "/dev/mapper/", for example "volume_group-windows_lv"
#
# Dependencies
# ============
#
# The script requires the following:
# 'kpartx' from package 'multipath-tools'
# 'virsh' from package 'libvirt'
# 'notify-send' from package 'libnotify'
#
vm_volume="$1"
user=$(logname)
rc=0
export $(dbus-launch)
mount_vol ()
{
mkdir -p "$mount_path"
rw_mode="$1"
case $rw_mode in
rw)
notify-send "$vm_name is shut off" "Mounting volume $vm_volume in read-write mode." -i dialog-information
;;
ro)
notify-send "$vm_name is running" "Mounting volume $vm_volume in read-only mode." -i dialog-information
;;
*)
exit 1
;;
esac
# mount -t ntfs3 -o "$rw_mode",iocharset=utf8,dmask=027,fmask=137,uid=$(id -u $user),gid=$(id -g $user),discard "$mount_dev" "$mount_path"
mount -t ntfs-3g -o "$rw_mode",nls=utf8,umask=000,dmask=027,fmask=137,uid=$(id -u $user),gid=$(id -g $user),windows_names "$mount_dev" "$mount_path"
rc=$?
}
umount_vol ()
{
# Loop until vm_mount can be unmounted
while fuser -sMm "$mount_path"; do
notify-send -wt 10000 "Cannot unmount $vm_volume - device is busy" "Close all files in $vm_volume and click message to try again." -i dialog-warning
done
umount "$mount_path" && notify-send "Info" "Unmounted volume $vm_volume." -i dialog-information
sleep 2
rmdir "$mount_path" || notify-send -t 0 "Error" "Could not remove directory $mount_path" -i dialog-error
kpartx -d "$vm_path" || notify-send -t 0 "Error" "Could not unmap $vm_volume" -i dialog-error
}
# See that the required packages are installed
if ! type notify-send 2>2; then echo "Error: notify-send from package libnotify is not installed. Abort."; exit 2; fi
if ! type kpartx 2>2; then notify-send -t 0 "Error" "kpartx from package multipath-tools is not installed. Abort." -i dialog-error; exit 2; fi
if ! type virsh 2>2; then notify-send -t 0 "Error" "virsh from package libvirt is not installed. Abort." -i dialog-error; exit 2; fi
# Run as root
if [ $UID -ne 0 ]; then
notify-send -t 0 "Error" "Script must be run as 'root' (sudo). Abort." -i dialog-error
exit 3
fi
if [ -z "$vm_volume" ]; then
notify-send -t 0 "Usage error" "Usage: sudo ./vm-mount.sh volume_group-logical_volume" -i dialog-error
exit 4
fi
vm_path="/dev/mapper/${vm_volume}"
mount_path="/mnt/$vm_volume"
# Check input. If vm_path doesn't exist, exit
if [ ! -b "$vm_path" ]; then
notify-send -t 0 "Error" "Volume $vm_volume does not exist. Abort." -i dialog-error
exit 5
fi
# Determine vm_name associated with vm_volume
vm_dev="$(echo $vm_volume | sed 's|-|/|')"
found="no"
domains="$(virsh list --all --name)"
for vm_name in $domains; do
if [[ "$(virsh -q domblklist $vm_name | awk '{ print $NF }' | grep -x /dev/$vm_dev)" ]]; then
found="yes"; break
fi
done
if [[ $found != "yes" ]]; then
notify-send -t 0 "Error" "$vm_volume not associated with any VM. Abort." -i dialog-error
exit 6
fi
# See if the volume is already mounted and if it is of type "fuseblk" or ntfs3.
if cat /proc/mounts | grep -qE "${mount_path} (fuseblk|ntfs3)"; then
umount_vol
else
# Create a mapper device for each partition in the vm_volume and
# assign the largest partition to $dev
kpartx -av "$vm_path"
if [ -b "${vm_path}p1" ]; then
dev="$(lsblk -no NAME,SIZE ${vm_path}p? | sort -h -k 2 | awk '{ print $(NF-1) }')"
dev="$(echo $dev | awk '{ print $NF }')"
else
if [ -b "${vm_path}1" ]; then
dev="$(lsblk -no NAME,SIZE ${vm_path}? | sort -h -k 2 | awk '{ print $(NF-1) }')"
dev="$(echo $dev | awk '{ print $NF }')"
else
dev="$vm_volume"
fi
fi
mount_dev="/dev/mapper/$dev"
# Make sure mount_dev is of type ntfs
if [[ "$(lsblk -no FSTYPE ${mount_dev})" != "ntfs" ]]; then
notify-send -t 0 "Mount error" "$vm_volume is not an NTFS file system! Abort." -i dialog-error
kpartx -d "$vm_path"
exit 7
fi
# Mount rw if VM is shutoff, else mount ro
if virsh list --name --state-shutoff | grep -xq $vm_name; then
mount_vol rw
else
mount_vol ro
fi
if [ $rc = 0 ] ; then
# Open file browser as user 1000
# sudo -u \#1000 -H nohup xdg-open "$mount_path" &
sudo -u "$user" -H nohup xdg-open "$mount_path" &
exit 0
else
notify-send -t 0 "Mount error" "Could not mount volume $vm_volume !" -i dialog-error
# clean up
kpartx -d "$vm_path"
rmdir "$mount_path"
exit 8
fi
fi
#EOF
# exit codes:
# exit 1 - unknown error
# exit 2 - dependencies not met / missing package
# exit 3 - script called without 'sudo' root privileges
# exit 4 - usage error - missing vm_volume parameter
# exit 5 - vm_volume not found
# exit 6 - vm_volume not associated with a VM
# exit 7 - vm_volume is not an NTFS file system
# exit 8 - could not mount vm_volume