#!/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