NFS - Automate attaching / detaching of shares

From Blue-IT.org Wiki

Contents

History

  • --Apos 21:51, 30 August 2007 (CEST) Changed username udevmount to netmount.
  • --Apos 00:55, 27 August 2007 (CEST) All parts working. Verified on my working pc (ubuntu feisty), server (ubuntu feisty) and laptop (ubuntu feisty).

Other Links

Prerequisites

Prerequisites

  • Administrative privileges to the system
  • Profound knowledge of
    • user administration,
    • bash editing,
    • nfs admnistration
  • The working directory for scripts is
/root/bin
  • Everything was implemented and tested on ubuntu systems, a network with three pc's.
  • In principle scalable to any system. Each nfs client is first pinged and then handles mount/umount commands itself via ssh.
  • Requires a new unpriviledged user on each client. This user is in the sudoers list, allowed to do the mount and unmount command. This user cannot (!) be logged in, it has no password. Only key-autentification works, thereby permitting passwordless ssh connections -on and only those - from server to client.
  • This solution requires that both, nfs client and server are permanently connected through the network.

Motivation: add or removing shares

The intention for this HowTo was the problem, that in case you are removing an exported - and mounted - nfs file system from a network this will have severe side effects on the clients:

  • All applications depending on the file system will fail / hang
  • The desktop manager which will be left in an unusable state for minutes
  • Drives will not be synced, data loss can occur

Solution: Publisher / Subscriber Pattern

If a nfs exported filesystem is removed from the nfs server, it must also be removed on all computers that are connected to the nfs server.

The server therefore has to have a list of these computers. In speach of the publisher/subscriber pattern he is the publisher.

Network with NFS Server abnd Clients

The nfs clients are the subscribers. They subscribe to the server. The administrator is doing this in our approach by writing them into the servers clientlist.

To provide mechanisms to add or remove files on the clients, the server must be able to connect somehow to the clients - and that's the problem of most of the current nfs servers implementations: Mostly, just the clients connect in certain time intervals to the nfs servers in the network and then wait fore a certain amount of time for the drive to reappears.

The publisher/subscriber pattern however wants the subscribers (clients) to be reported immediately, when the system state changed, e.g. a drive is unmounted.

And remember the following: any PC can be publisher and/or subscriber. So a mixed setup with more than one nfs server is no problem. Each server cares for it's own clients, no matter what other servers do.

On my home's setup, my server PC exports shares covering a backup drive via USB and a home directory. The second one is a laptop and exports his desktop. The third one is the working horse and exports music and video data. Each server pc cares for his clients, he takes care for unmounting his exported shares on its addicted clients.

Prepare your workstation / server

First open an editor

Login as root open the following files with your favorite editor.

gedit /etc/fstab \
      /etc/exports \
      /root/bin/nfs_clientlist.sh \
      /root/bin/mount_nfs_exports_on_attached_clients \
      /root/bin/mount_nfs_on_client \
      /root/bin/nfs_udev_mount.sh

These are the files we will alter/write. The last two have to be made executable via chmod 755.

/etc/exports

The /etc/exports delivers every information we need.

  • nfs share's mountpoints
  • possibly connected clients

Identify shares

It is easy to extract all exported shares of the nfs server with a little bash and awk charm and store it in a bash variable called EXPORTS (we will integrate this charm into a bigger script later):

EXPORTS=$(cat /etc/exports | grep -v ^# | grep " " \
| cut -f 1 | awk '{system("echo -n " $1 "----")}' \
| sed s/----/"  "/g)

It filters out all commentary lines, all empty lines, and writes the extracted directory names - devided by spaces - into the string $EXPORTS.

On server: Create a clientlist

It could be possible, to extract information out of the /etc/exports with a little bash magic. Feel free to do this. It also might be possible to write all this in perl ;) Sice now I found no time to do this. Drop me a mail, if you have an idea..

In this solution I hold the variables of the nfs clients in a simple configuration file, called nfs_clientlist.sh.

There is also a $HOSTNAME line. If you just have one network card, configured correctly your /etc/hosts and this hostname is used for your nfs server network wide, then you can leave this alone.

In my case, I use 2 network cards with different names, so I need this line.

cd /root/bin
vim nfs_clientlist.sh
############################################################
### PLEASE EDIT ############################################
############################################################

# All CLIENTS that use the nfs server on this machine
CLIENTLIST="client1 client2 client3"

# The hostname of this machine.
# Might be different with more than on network card.
#THISHOST=myServerName
THISHOST=$HOSTNAME

############################################################

Trigger the remote mount/unmount of shares

For the nfs servers machine to be able to trigger the mount/unmount on connected clients there might be two way's:

  • Writing a client/server program that communicates on a special port.
  • Issuing a bash mount/umount via ssh on the remote machine

The first approach might be possible in e.g. perl/pyhton, but for my purposes connection/reconnecting via ssh fits my needs.

Prepare the clients in the network

To be able to trigger the mount/unmount on each client we need a user has the following abilities:

  • sudo for mount/umount
  • passwordless login via ssh from nfs server

Some of you could fear if they hear passwordless login. But this is only for a special unpriviledged user that has no password. So ssh logins are only possible via su -c (root) and if the client has the appropriate encrypted ssh-key from the server. No one, even not on localhost can login as this user.

On client: Create an unpriviledged user with ssh

Log in as root and create an unpriviledged user netmount with a unique user id (in my case 130) and give it the password netmount. Because we enable passwordless login via ssh a secure password makes no sin. Afterwards we create the ssh keys. Please press just ENTER until the key generation is finished. We do not use a ssh password!

useradd -g 0 -m -u 130 -s /bin/bash netmount \
passwd netmount

Next we need a standard unix password. Later we will remove it. We login

su netmount -c /bin/bash

As root again, add the user to the sudoers file:

visudo

Add the two lines

netmount ALL=(ALL) NOPASSWD: /bin/mount
netmount ALL=(ALL) NOPASSWD: /bin/umount

In the next step, we create the ssh keys. You can skip this, the script of the next section automatically checks, if an corresponding key exists.

If you like to create a new on, press Enter two times without creating a password for the ssh-key when asked so:

ssh-keygen -t dsa
exit

Prepare the server

On server: Export the ssh keys to each client

On each nfs server you have to attach the generated public key to the authorized_keys2 file of the client you want to connect to.

  • Switch on all nfs client pc's.
  • Don't forget to edit your nfs_clientlist.sh in advance. The following script uses this file to connect to all clients.
 vim /root/bin/nfs_clientlist.sh
  • On server NOW login as user netmount:
su netmount -c /bin/bash

Use a little helper script to create and distribute the ssh keys.

cd /root/bin
vim ssh_helper.sh

The script will perform all tasks for you:

  • Connection to the client via ssh
  • Checking, if a previous id_dsa file on the client exists
  • Checking, if the connection fails due to changed keys

If something goes wrong and you only have a few computers than the best is, to remove every /home/netmount/.ssh directory on every pc and start over from scratch.

#!/bin/bash
#
cat <<EOF
---------------------------------------------------------------------
This scripts exports THIS computers public ssh key to
a clients's ~/.ssh/authorized_keys2 file.

This will enable passworless login from THIS pc to the client.

You need to specify at least the IP/alias of the other pc!
You optionally can specify another username than the actual one.

Usage: ssh_export ip_of_remote_client  [alternate_username]
---------------------------------------------------------------------

EOF

client="$1"
NAME="$2"
THIS_HOSTNAME="$HOSTNAME"

[ "$NAME" ] || NAME="${USER}"
[ "$client" ]  || echo "ERROR: You have to specify at least a client IP or alias."
[ "$client" ]  || exit 1

cd ~/.ssh

echo -n "* Check connection to client ... "
if ping -c 1 $client > /dev/null
then
	echo OK.
else 
	echo Please check the connection.
	echo   - Aborting here.
	exit 1
fi

echo "* Create ssh dir on client, if it not already exists ... "
ssh ${NAME}@$client "mkdir -p ~/.ssh; chmod 700 ~/.ssh"

echo "* Check if a local pub key exist and/or create one ... "
PUB_KEY="id_dsa.pub"
if test -e $PUB_KEY
then
	echo "   - A file named $PUB_KEY exists."
else
	echo "   - Creating a new one."
	ssh-keygen -t dsa 
fi

echo "* Check, if an older key was exported already in former time to the client."
if
        ssh ${client} "if (cat ~/.ssh/authorized_keys2 | grep ${NAME}@${THIS_HOSTNAME});\
                then echo  - WARNING: An older key was exported before to $client. \
			\n  - Please fix manually.; exit 1; \
                fi";
then
        echo "  - Authorized keys file is clean."
else 
	echo "  - Aborting here."
	exit 1
fi

echo "* Copy the public key of THIS pc to the client ... "
scp id_dsa.pub ${NAME}@$client:~/.ssh/id_dsa.pub_${HOSTNAME}

echo "* We make an entry into the authorized_keys file on the client ... "
ssh ${NAME}@$client "cat ~/.ssh/id_dsa.pub_${HOSTNAME} >> ~/.ssh/authorized_keys2; \
   rm ~/.ssh/id_dsa.pub_${HOSTNAME};"

echo "* Secure the local public key ... "
chmod 600 id_dsa.pub

echo ;
echo "* LET'S TEST IT:"
echo "  Now we test with running the following terminal command."
echo "  You should NOT be prompted by a password."
echo ;

if
        ssh ${NAME}@$client "echo   - This is a message on $client."
then 
        echo "  - Congratulation: If you was NOT promptd for a password,"
	echo "     you can login passwordless to your client $client."
else
	echo "  - WARNING: There was an error with the passwordless login to $client."
        exit 1
fi

echo ;
echo ;
echo "** Program ended."
echo ;
echo If you like to remove the automatic login, you have to 
echo remove the public key in the file /home/${NAME}/.ssh/authorized_keys2
echo on your clients - $client - computer

Execute the script:

sh ssh_helper.sh

Test it

If you now try to connect to your client via ssh you should not be prompted for a password.

If not, then something went wrong. In this case please first repeat all steps within the ssh-key chain and assure this will work!

Back on prepared clients

On client: Remove the password of user netmount

Since we have passwordless ssh connection from the server to each client, we don't need and don't want a login for the user netmount on a local client.

So on every client do (as root)

sudo passwd -d netmount

This assures, that only servers, that are authenticated via the ssh-keychain can contact the client.

If you need to recover the ssh authentication you have to add a pasword (sudo passwd netmount again and proceed as before.

The mount / umount script

Naming conventions for mountpoints

On the clients the /etc/fstab should look like this

# on client
vim /etc/fstab
[...]
nfs_server:/media/MySpecialHarddisk  /media/MySpecialHarddisk  nfs  defaults  0 0

On the server the /etc/fstab should mount the device the same way accordingly:

# on nfs server
vim /etc/fstab

/dev/disk/MySpecialHarddisk  /media/MySpecialHarddisk  ext3 \
                rw,noauto,sync,noexec,users  0 0

Remark the mountpoint /media/MySpecialHarddisk that has to be identical on all pc's in the network for this device.

In the next section you'll find a script, that helps you easily configure your system according to this needs.

Create the fstab, exports, udev entries

  • You can use the following script, but you should first read the whole article. Then you will be able to understand, what this script provides you with - and why.

The naming convention for equal names in fstab, exports and - later - udev.rules could lead to less flexible mountpoint names. Dealing with symlinks could fix the problem.

On the other side this could lead to a network wide unification, which isn't such bad thing.

The next script will help integrate a new drive in your network. Run it in a terminal, it gives you templates to copy and paste into the appropriate configuration files.

  • This script will change NO files.
cd /root/bin
vim add_nfs_device.sh
#! /bin/sh
#
# add_nfs_device.sh
#
# Add a new udev device on the nfs server
# that matches the naming conventions

#################################################
#### EDIT TO FIT YOUR NEEDS #####################
#################################################

# Used for devicename, symlink, mountpoint, export
NAME_OF_DEVICE="Tevion_1_240MB"

# Udev params
UDEV_SUBSYSTEMS="usb"
UDEV_KERNEL="sd?a"
ATTRS_SERIAL="200501CAF55"
ENV_PRODUCT="67b/3507/1"
FILE_UDEV=/etc/udev/rules.d/10-local.rules

# local fstab
FILE_FSTAB=/etc/fstab
FSTAB_MOUNT_DIR=/media
FSTAB_FILESYSTEM="ext3"
#FSTAB_MOUNT_OPTS="rw,noauto,sync,dirsync,noexec,nodev,noatime,users"
FSTAB_MOUNT_OPTS="rw,noauto,rsize=8192,wsize=8192,timeo=14,intr"
FSTAB_BOOT_OPTS="0 0"

# nfs exports
FILE_EXPORTS=/etc/exports
NFS_OPTIONS="rw,sync,no_root_squash,no_subtree_check"
CLIENTLIST="ibmr31" # Overwritten, when /root/bin/nfs_clientlist.sh exists


#################################################
#### Lets go ####################################
#################################################
echo "#-----------------------------------------"
echo "# Writing the udev rule"
echo "#-----------------------------------------"
cat - <<EOF
# added by `pwd`/$0
# External ${UDEV_SUBSYSTEMS} device - ${NAME_OF_DEVICE}
ACTION=="add",  SUBSYSTEMS=="${UDEV_SUBSYSTEMS}", KERNEL=="${UDEV_KERNEL}", ATTRS{serial}=="${ATTRS_SERIAL}", NAME="${NAME_OF_DEVICE}", SYMLINK:="disk/${NAME_OF_DEVICE}", GROUP="users", MODE="660", RUN+="/root/bin/udev_mount.sh $env{ACTION} '${NAME_OF_DEVICE}' &"
ACTION=="remove", ENV{PRODUCT}=="${ENV_PRODUCT}", RUN+="/root/bin/udev_mount.sh $env{ACTION} '${NAME_OF_DEVICE}' &", OPTIONS=="ignore_remove"
EOF

echo "#-----------------------------------------"
echo "# fstab"
echo "#-----------------------------------------"
cat - <<EOF
# added by `pwd`/$0
# External ${UDEV_SUBSYSTEMS} device - ${NAME_OF_DEVICE}
/dev/disk/${NAME_OF_DEVICE}  ${FSTAB_MOUNT_DIR}/${NAME_OF_DEVICE}  ${FSTAB_FILESYSTEM} ${FSTAB_MOUNT_OPTS} ${FSTAB_BOOT_OPTS}
EOF

echo "#-----------------------------------------"
echo "# exports"
echo "#-----------------------------------------"
# source CLIENTLIST if exit
if test -e /root/bin/nfs_clientlist.sh
then
 . /root/bin/nfs_clientlist.sh
fi

NFS_STRING=${FSTAB_MOUNT_DIR}/${NAME_OF_DEVICE}

for client in $CLIENTLIST
do
       NFS_STRING="${NFS_STRING} ${client}(${NFS_OPTIONS})"
done

# write the file
cat - <<EOF
# added by `pwd`/$0
# External ${UDEV_SUBSYSTEMS} device - ${NAME_OF_DEVICE}
${NFS_STRING}
EOF

Edit header of the script to your needs and run it in a terminal.

sh add_nfs_device.sh

the output will provide you with the necessary config file entries. What remains now is to add the device into the fstab on the attached nfs client.

On client: Write the essential mount / umount script

Two scripts will trigger the mount/umount on each client.

They are called with the parameter [mount|umount]. The callers are e.g. an init script (etc/init.d/...), udev (rules file) or it will be called manually on login/logout (e.g. of your desktop manager)or whatever else you can imagine. We will look at this piont later.

First

For now let's write the first script down. It is just a wrapper for calling the real worker, that is doing the mount/umount work on each client:

cd /root/bin
vim mount_nfs_exports_on_attached_clients
#! /bin/bash
#
# /root/bin/mount_nfs_exports_on_attached_clients.

# Umount/mount all exported nfs mount on attached clients.
# Do an appropriate fstab entry on these clients.
#
# The used mount directories AND client names MUST 
# match the udev and fstab entries 
# on thes machines - which should be the same - exactly.

export HOME=/root
export USER=root
export PATH="$PATH:/usr/local/bin:/sbin/:/usr/bin:/usr/sbin/:/bin"
. /root/bin/nfs_clientlist.sh

# Info  (-t) for logger
ME="mount_nfs"

# check the command line parameter
MOUNT="$1"
if [[ ${MOUNT} =~ [mount|umount] ]] 
then
logger -t $ME "STARTING:   $THISHOST"
logger -t $ME "CLIENTLIST: $CLIENTLIST"
else
   logger -t $ME "ERROR: only mount or umount as parameter are allowed." \
   exit 1
fi

# force remount on network CLIENTLIST
for client in $CLIENTLIST
do
    /bin/bash /root/bin/mount_nfs_on_client ${MOUNT} ${client} &
done

Second

The second script does the real work:

vim mount_nfs_on_client
#! /bin/bash
#
# /root/bin/mount_nfs_on_client

# Umount/mount all exported nfs mounts on the attached clients.
# Do an appropriate fstab entry on these clients.
#
# The used mount directories AND client names MUST 
# match the udev and fstab entries 
# on thes machines - which should be the same - exactly.

export PATH="$PATH:/usr/local/bin:/sbin/:/usr/bin:/usr/sbin/:/bin"
. /root/bin/nfs_clientlist.sh

# Next line grabs all nfs exports
# We have to take care for the mount order.
# "sort" is doing that for us.
EXPORTS=""
EXPORTS_MOUNT="$(cat /etc/exports | grep -v ^# | grep " " | \
   cut -f 1 | sort | awk '{system("echo -n " $1 "----")}' | \
   sed s/----/"  "/g)"
EXPORTS_UMOUNT="$(cat /etc/exports | grep -v ^# | grep " " | \
   cut -f 1 | sort | sort -r | awk '{system("echo -n " $1 "----")}' | \
   sed s/----/"  "/g)"

# Info  (-t) for logger
ME="mount_nfs_client"

# check the command line parameter
MOUNT="$1"
if [[ ${MOUNT} ]] 
then
logger -t $ME "STARTING:   $THISHOST"
else
   logger -t $ME "ERROR: mount parmam not defined." \
   exit 1
fi

CLIENT="$2"
if [[ ${CLIENT} ]]
then
   logger -t $ME "CLIENT:     $CLIENT"
else
   logger -t $ME "ERROR: a client has to be attached." \
   exit 1
fi

# Manage exports
if [ "${MOUNT}" = "mount" ]
then
 EXPORTS=" $EXPORTS_MOUNT "
else
 EXPORTS=" $EXPORTS_UMOUNT "
fi

if [ "${MOUNT}" = "umount" ]
then
 EXPORTS=" $EXPORTS_UMOUNT "
fi

logger -t $ME "EXPORTS:    $EXPORTS"
sync

# force remount on network CLIENT
if ping -c2 $CLIENT >> /dev/null
then
 
 for export in $EXPORTS
 do

  if [ "${MOUNT}" = "mount" ]
  then

    logger -t $ME "Trying mount..."
    
    # Check, if subdirectories exist (nfs can also mount subdirs!)
    if test -d "${export}"
    then

      logger -t $ME "Trying mount _${export}_ on _${CLIENT}_..."
      
      # For removable devices check, if they are 
      # not already mounted on the client
      su netmount -c "/usr/bin/ssh netmount@${CLIENT} \
      '/bin/mount | cut -d \" \" -f1 | grep -x ${THISHOST}:${export} \
       || sudo /bin/mount ${THISHOST}:${export} \
       && echo ${THISHOST}:${export} already mounted;'" \
       | logger -t $ME 

    fi
  fi
  
  if [ "${MOUNT}" = "umount" ]
  then

    logger -t $ME "Trying umount..."
    
    su netmount -c "/usr/bin/ssh netmount@${CLIENT} \
    'sync;sudo /bin/umount ${THISHOST}:${export}'" \
    | logger -t $ME

  fi

 done

fi

Make both scripts executable by everyone, but editable only by root:

chmod 755 mount_nfs_*
chmod root:root mount_nfs_*

Run the script

Test the script

Login as root. By simply running (as root)

/bin/bash /root/bin/mount_nfs_exports_on_attached_clients [u]mount

in a terminal on the server, you can test, if everything works. On your clients, the nfs shares should be mounted/unmounted. Prove on the client with e.g.

watch -n2 df -h

and syslog

tail -f /var/log/syslog

On server: Call the script on boot

To trigger all attached clients of the system, you should call the script in your systems shutdown or boot scripts. On my ubuntu system this works like this:

/etc/init.d/rmnologin

E.g. call the script in the file /etc/init.d/rmnologin. It is the last script called on the system before the login prompt appears and therefore a good choice for every runlevel:

vim /etc/init.d/rmnologin

Add the line

# Mount nfs shares on connected pcs
/bin/bash /root/bin/mount_nfs_exports_on_attached_clients mount
  • Pay attention that you are calling the script with /bin/bash, the borne again shell and no other one. I encountered trouble e.g. with ubuntu's dash (symlinked to /bin/sh) bacause I am using some special bash features like [[ ]].
  • Pay attention to the parameter mount to call the script. This mounts all shares on this PC according to fstab.

/etc/rc.local

Running the script via /etc/rc.local didn't work for me, because network connections where not aware when running this script.

On server: Call the script on shutdown

Edit a shutdown runlevel script

Unfortunatly there is not runlevel script for shutdown on ubuntu.

So I devired the rc.local accordingly:

vim /etc/init.d/rc.shutdown
#! /bin/sh

PATH=/sbin:/bin:/usr/sbin:/usr/bin
[ -f /etc/default/rcS ] && . /etc/default/rcS
. /lib/lsb/init-functions

do_stop() {
       if [ -x /etc/rc.shutdown ]; then
               log_begin_msg "Running local shutdown scripts (/etc/rc.shutdown)"
               /etc/rc.shutdown
               log_end_msg $?
       fi
}

case "$1" in
   start)
       # DO nothing
       ;;
   restart|reload|force-reload)
       echo "Error: argument '$1' not supported" >&2
       exit 3
       ;;
   stop)
       do_stop
       ;;
   *)
       echo "Usage: $0 stop" >&2
       exit 3
       ;;
esac

Now edit the script file, which will be called from the above init script when shutdown occurs:

vim /etc/rc.shutdown
#!/bin/sh -e
#
# rc.local
#
# This script is executed when machine is shutting down or entering an runlevel 
# without network support.
#
# Make sure that the script will "exit 0" on success or any other
# value on error.
#
# In order to enable or disable this script just change the execution
# bits.
#
# By default this script does nothing.

# Unmount nfs shares on attached clients
/bin/bash /root/bin/mount_nfs_exports_on_attached_clients umount

exit 0

Update the runlevels

And add it to the runlevels (debian way)

update-rc.d -f rc.shutdown defaults

A more restrictive way to do this is

update-rc.d rc.shutdown start 01 2 3 4 5 . stop 01 0 1 6 .

The last approach assures that the script starts before all others.

This works out of the box. If not see section troubleshooting at the end of this article.

You can try, if your script works with

/etc/init.d/rc.shutdown stop

On server: gdm configuration

My laptop is shutting down too fast, so the network connection is lost, before the nfs clients could be contacted. The problem also occurs, when using NetworkManager for network connections (e.g. wireless card).

gdm shutdown / reboot / suspend / hibernate

In general the file

/etc/gdm/gdm.conf

is responsible for the reboot and halt commands:

[...]
RebootCommand=/sbin/shutdown -r now "Rebooted via gdm."
HaltCommand=/sbin/shutdown -h now "Shut Down via gdm."
SuspendCommand=/usr/sbin/pm-suspend
HibernateCommand=/usr/sbin/pm-hibernate
[...]

You can write a simple wrapper script and call the commands mentioned in the section before to unmount nfs shares before shutting down the machine.

The same you can do for suspend and hibernation.

gdm logout/login

You can then try to trigger the mount/umount script via the gnome login/logout scripts which you find in the following directories

# Run something ...

# ...before the desktop mangager session starts:
/etc/gdm/PreSession/Default

# ... after the login:
/etc/gdm/PostLogin/Default

# ... after the gnome session ends
# e.g. pressing the logout button
/etc/gdm/PostSession/Default

Put the following command at the beginning of these files and change use mount and umount for whatever is needed:

/bin/bash /root/bin/mount_nfs_exports_on_attached_clients mount
/bin/bash /root/bin/mount_nfs_exports_on_attached_clients umount

On server: Using udev to mount/umount drives

Purpose:

  • You want to share removable devices

If you don't use removable devices, like an USB backup drive or hot swappable drives, than you can skip the next paragraphs.

Therefore we have to consider additional prerequisites:

  • Editing special udev rules for the actions add/remove
  • Unique naming conventions for udev-rules and fstab

Udev Rule for an external USB drive

The udev rule we intend to write has to take care of

  • Giving the device to be mounted a unique name
  • Triggering our mount_nfs_shares... script according to a remove or adding of the device
  • Rejecting a buggy udev behaviour, that unpredictably fires on remove actions

The following udev rule suffices this:

vim /etc/udev/rules.d/10-local.rules

Unfortunately there is no variable handling in udev rules. So we have to specify the name of our device two or three times. This name must exactly match the entry in the /etc/fstab !!! Use the script I provided earlier in this article to produce convenient templates (section Create the fstab, exports, udev entries):

# LOCAL UDEV-RULES
#
# You can get infos about devices with
#
#   e.g. udevinfo  -a -p $(udevinfo -q path -n /dev/sda)
#        udevmonitor --env | grep PRODUCT

# External USB 3,5 MySpecialHarddisk
ACTION=="add",   SUBSYSTEMS=="usb", KERNEL=="sd?1", \
ATTRS{serial}=="200501CAF55", NAME="MySpecialHarddisk", \
SYMLINK:="disk/MySpecialHarddisk", GROUP="users", MODE="660", \
RUN+="/bin/bash /root/bin/nfs_udev_mount.sh $env{ACTION} 'MySpecialHarddisk' &"

ACTION=="remove", ENV{PRODUCT}=="67b/3507/1", \
RUN+="/bin/bash /root/bin/nfs_udev_mount.sh $env{ACTION} 'MySpecialHarddisk' &", \
OPTIONS=="ignore_remove"

Problems with ACTION remove

The add action was no problem. On remove action however, the ATTRS{} key for the device is not avaiable anymore, when the device was removed. As an effect, the remove action of the udev rule is never called.

It took me over a year to figure a trick out, to handle this problem: instead of ATTRS{} use the product id of the device, which will be available all the time.

With

udevmonitor --env | grep PRODUCT

you will get the PRODUCT_ID that matches the disconnected device. I is used with "ENV{PRODUCT}" in the udev rule.

Reload the rules

 udevcontrol reload_rules

Write the mount / umount script for udev

As you can see, this latter udev rule triggers a script called nfs_udev_mount.sh. It is responsible for

  • mount local devices
  • calling the main nfs_mount script
  • takes care or the buggy udev when removing devices

Since until now (August 2007) udev/hal fires unpredictable on removing a device. this means, that an attached script will be executed several times.

Therefore i blocked the script execution for 15 seconds (alter BLOCK_FOR_SECONDS to change this behavior). This should be enough on any system but is not a save solution for the problem. I hope further versions of udev/hal will fix this.

This script takes two parameters, that are triggerd within the udev rule:

  • the action that occurs: remove->umount, add->mount
  • the name of the device: mount/umount according to /etc/fstab
#! /bin/bash
#
# /root/bin/nfs_udev_mount.sh

# mount tevion according to /etc/udev/rules.d/10-local.rules
# Do an appropriate fstab entry, so mount -a
# will do the rest

export PATH="$PATH:/usr/local/bin:/sbin/:/usr/bin:/usr/sbin/:/bin"

ACTION=$1
NAME=$2
ME=udev_nfs_mount
LOCKFILE=/tmp/udev_mount.lock
TIMESTAMP=`cat $LOCKFILE`
myTime=`date +%s`
BLOCK_FOR_SECONDS=15

MOUNT_DIR=/media
MOUNTPOINT=${MOUNT_DIR}/${NAME}
NFS_MOUNT_SCRIPT=/root/bin/mount_nfs_exports_on_attached_clients

# Troubleshooting of fireing hal - udev events
test -f $LOCKFILE || echo $myTime > $LOCKFILE
if [ $myTime -lt $TIMESTAMP ]
then
  #logger -t udev_mount.sh "exiting ..."
  exit 1
else
  #logger -t udev_mount.sh "setting new timestamp ..."
  expr $myTime + $BLOCK_FOR_SECONDS > $LOCKFILE
fi


###################################################
# NOW LETS go

logger -t $ME "${NAME} ${ACTION} OK" 

# --------------------------------------------------
# mount local devices
if [ "$ACTION" == "add" ]
then
 if mount | grep ${MOUNTPOINT}
 then
  echo already mounted ...
 else
  mkdir -p ${MOUNTPOINT}
  mount ${MOUNTPOINT}

  # force remount on network clients
  /bin/bash ${NFS_MOUNT_SCRIPT} mount

 fi
fi

if [ "$ACTION" == "remove" ]
then
  # force umount on network clients
  /bin/bash ${NFS_MOUNT_SCRIPT} umount

  #The following is a bad idea, cause udev handles this
  #umount -f /media/${NAME}

fi

logger -t $ME "$ACTION of $NAME finished."

For security reasons do

chmod 755 nfs_udev_mount.sh
chmod root:root nfs_udev_mount.sh

Test everything

Add or remove (hotswap) a device on the server, and look what your clients are doing. Shutdown or reboot your nfs server. On all your clients should seamlessly appear/disappear the devices.

Open a terminal on the client and watch what happens:

watch -n2 df -h

What says syslog?

tail -f /var/log/syslog

What says udev?

udevmonitor --env

No desktop manager should hang anymore when shutting down your server or adding / removing your backup device ;)

Discussion

Dataloss

This approach will not hinder dataloss. If you opened a document, and shut down the nfs server, the document will be lost. Also the application might hang and the desktop manager. So this scenario is not handled in any way, and cannot be - ever - because the nfs server never ever will be aware, which recourse might use it's file system.

I tested the worst case with openoffice 2.2, with recovered the crashed document read-only after the next boot of the nfs server - that is better than a lost document.

And: i did not have to remount my nfs share by hand - that is a lot of comfort ;)

Clientlist

There is just one nfs clientlist for each server. This issue might be solved, if I find the time to extract each client out of the exports file.

Time critical

The scripts are a little bit time critical. They assume, that all clients/servers are running and the network connections are o.k. There are no further checks.

Each client is pinged two times before the ssh command will be executed. It takes a few seconds to mount / remount the devices, but it is done for each client in an a separate process.

The whole procedure takes about a second for each share. I messure about 3-5 seconds for 5 shares each of my pc's.

Ping security risc

One is the fact, that ping is used to detect running clients. This might be a problem in secure networks, where ping is not enabled, or firewalls are blocking it.

But consider: waiting for an unsuccessful ssh command to response is more time consuming.

User netmount security risc

Another is, that the netmount user could be a security whole. The only way to decouple this, is to write a real wrapper or client/server application, that runs's as root and tunnels this aspect.

But consider

  • netmount has no password, only ssh authenticated servers can connect to an authenticated client. To install the ssh authentication you need root access on the client/server and install a password.
  • You cannot login with the user netmount, not even from localhost.
  • All scripts are possessed by root:root and can only be written by root:root

Not handy for big networks

For the system administrator it might be a little bit complicated to handle all the files, but with just one nfs server it boiles down to

  • Add a custom udev rule
  • Unify the fstab mountpoints and udev names for the devices
  • edit the nfs_clients.sh with clientlist and local hostname.

But a good system administrator can handle all this with a little bash magic ;)

Troubleshooting

System hangs nevertheless

Probably you disconnected the network. A permannt network connection is one of the prerequisites for a nfs network (see intro of this article).

First reconnect or restart the nfs server that causes the problem. Then wait a little bit - the lost share should reappear, the hung system work.

Then shutdown the system normally and your scripts should work.

Path

On some systems, your environment will not match the requirements.

  • Is your shell /bin/bash, is it installed?
  • Is your mount command in /bin/mount (same for umount)?

General

Open all this files in one editor on your nfs server as root:

gedit /etc/udev/rules.d/10-local.rules \
      /etc/fstab \
      /etc/exports \
      /etc/hosts \
      /root/bin/nfs_clientlist.sh \
      /root/bin/mount_nfs_exports_on_attached_clients \
      /root/bin/nfs_udev_mount.sh

On your nfs client open

gedit /etc/fstab
  • The mountpoints under /media, the device names for /etc/udev/rules.d/10-local.rules and /etc/fstab are unique
  • Are all nfs impors named correct in /etc/fstab according to the naming conventions on the server. Same unique device name, same mountpoint.
  • When you are using more than on network card (think about the wlan card on your laptop), check if you are using the correct IP/name mapping.

Alter the runlevels

Here another way for initialisation of the rc.shutdown script:

Remove the old links:

$ update-rc.d -f rc.shutdown remove
Removing any system startup links for /etc/init.d/rc.shutdown ...
  /etc/rc0.d/K20rc.shutdown
  /etc/rc1.d/K20rc.shutdown
  /etc/rc2.d/S20rc.shutdown
  /etc/rc3.d/S20rc.shutdown
  /etc/rc4.d/S20rc.shutdown
  /etc/rc5.d/S20rc.shutdown
  /etc/rc6.d/K20rc.shutdown

Add rc.shutdown to be started at first:

$ update-rc.d rc.shutdown start 01 2 3 4 5 . stop 01 0 1 6 .
Adding system startup for /etc/init.d/rc.shutdown ...
  /etc/rc0.d/K01rc.shutdown -> ../init.d/rc.shutdown
  /etc/rc1.d/K01rc.shutdown -> ../init.d/rc.shutdown
  /etc/rc6.d/K01rc.shutdown -> ../init.d/rc.shutdown
  /etc/rc2.d/S01rc.shutdown -> ../init.d/rc.shutdown
  /etc/rc3.d/S01rc.shutdown -> ../init.d/rc.shutdown
  /etc/rc4.d/S01rc.shutdown -> ../init.d/rc.shutdown
  /etc/rc5.d/S01rc.shutdown -> ../init.d/rc.shutdown