Wednesday, June 13, 2012

Setting up a network-based, centralized home directory as a service

As I mentioned in my last post, I just built out a dedicated server at home, and I'm migrating a bunch of services to it. One reason I build this server in the first place was so that I could set up roaming home directories for all my Linux PCs in the house.

I tried a few configurations before landing on the one described here. I had problems where Ubuntu 12.04 would fail to boot if the system failed to mount the network drive properly when the mount was in the fstab. Ubuntu 10.04 would error on boot, but give you the opportunity to skip the mount and continue booting. However, if the share was mounted to /home on the client machine, logging into a desktop environment would fail on account of none of the config files being available. Even when the mount worked properly, having it mounted at /home caused performance problems. Relying on the network for processes like booting and logging into a desktop ended up being a deal-breaker for me having automounted network home directories.

But I did settle on a solution that works well. In brief, I servicized the mounting and unmounting of the network drive, and the control script I wrote for it also places netdrive symbolic links in each non-system user's home directory if a directory on the share exists in their name. Here's how it's done.

There are two parts to this configuration - a server and some clients. Let's set up the server.

My server is running Debian Squeeze, but these instructions should translate well to other distributions. First, install the NFS server:

sudo apt-get install nfs-kernel-server

Then set the config to share the /home directory over the network. Add this to the end of /etc/exports:

/home   192.168.0.0/255.255.255.0(rw,sync,fsid=0,no_subtree_check)

You should modify that to suit your specific needs. /home is the directory on the server to share. 192.168.0.0/255.255.255.0 means that the only systems who are allowed to access it will be on the 192.168.0.x subnet, which, for me, means everything behind my router. This share won't be accessible to anyone on the public side of my router. Finally, the (rw,sync,fsid=0,no_subtree_check) part are options that say, respectively, "Mount it read/write so users can save files here," "Write files to disk synchronously so there aren't sudden unmounting problems," "If the NFS server is NFS4, treat this directory as the root of all shared directories," and "Speed things up by allowing all subdirectories of the share to be accessed."

Restart the NFS server:

sudo service nfs-kernel-server restart

If you encounter problems, check /var/log/daemon.log for something to Google with.

I had firewall problems here. Mounting an NFS share on a remote server uses lots of different ports, and I was unable to identify them all. I ended up adding a rule to my firewall to let anything behind my router (that 192.168.0.x subnet) access any ports on the server. For me, this isn't a security issue. You should consider your situation and make that decision for yourself. At any rate, here's my command to make that firewall rule:

sudo ufw allow from 192.168.0.0/24 to any

Now get onto a client machine and try to mount it manually just to be sure it works:

sudo mount -t nfs -o proto=tcp,port=2049 192.168.0.100:/home /mnt

If you get no errors, try...

ls /mnt

...and check if what you see is what you expect. If so, unmount it:

sudo umount /mnt

Assuming that everything works, it's time to set up a control script. The script below is hardcoded to mount the NFS share to /mnt/netdrive. You should probably read through it and understand it before running it to be sure it won't interfere with things on your system. If it's going to work for you, paste it into /etc/init.d/netdrive or wherever else you decide.

safe_start () {

    if [ ! -d /mnt/netdrive ]; then
        mkdir /mnt/netdrive
    fi

    if grep -qs '/mnt/netdrive' /proc/mounts; then
        echo "Something is already mounted at /mnt/netdrive [ FAIL ]"
        exit 1
    else
        mount -t nfs -o proto=tcp,port=2049 192.168.0.100:/home /mnt/netdrive
    fi

    for u in `grep '/home' /etc/passwd | cut -d: -f1`; do
        if [ -d /mnt/netdrive/$u ]; then
            HOME=`grep "$u" /etc/passwd | cut -d: -f6`
            if [ -e $HOME/netdrive ]; then
                rm $HOME/netdrive
            fi
            ln -s /mnt/netdrive/$u $HOME/netdrive
        fi
    done
}

safe_unmount () {
    FILES_IN_USE=`lsof | grep '/mnt/netdrive' | awk '{print $9}'`
    if [ ! "$FILES_IN_USE" = "" ]; then
        echo "The following files are located on the netdrive and are still in use:"
        echo $FILES_IN_USE
        exit 1
    fi

    unmount
}

unmount () {
    umount /mnt/netdrive
    for u in `ls /home`; do
        if [ -d /home/$u/netdrive ]; then
            rm $u/netdrive;
        fi
    done
}

show_status () {
    if grep -qs '/mnt/netdrive' /proc/mounts; then
        echo "Netdrive is mounted."
    else
        echo "Netdrive is not mounted."
    fi
}

show_help () {
    echo "This script accepts the following commands: start, stop, forcestop, status, help"
}

case $1 in
    "start" )
        safe_start ;;
    "stop" )
        safe_unmount ;;
    "forcestop" )
        unmount ;;
    "status" )
        show_status ;;
    * )
        show_help ;;
esac

exit 0

Apply permissions:

sudo chmod 755 /etc/init.d/netdrive

Now you can run...

sudo /etc/init.d/netdrive start

...to mount it, or give it stop instead to stop it safely. The script will refuse to unmount the drive if it has files open still. If you want to override that check, use forcestop. status will tell you if the network drive is mounted or not. help will give you a basic usage message. When mounting, this script will also place the netdrive symlink in users' home directories, so, for example, /home/ryan/netdrive is mapped to /mnt/netdrive/ryan.

Now, I still wanted my drive to mount on boot. So I threw a symlink in the startup:

sudo ln -s /etc/init.d/netdrive /etc/rc2.d/S10netdrive

That's my Ubuntu 12.04 machine. I can't remember at what point in the startup procedure I placed in on my Ubuntu 10.04 box, but it's somewhat discretionary anyway.

So reboot your client and make sure you boot with a working netdrive symlink in your home directory. And done!

Monday, June 11, 2012

Setting up a headless Calibre server

My wife and I like to read books on the go, and we use an Android app called Aldiko (it's great) to read epubs. I have loads of epubs on my computer, and I have traditionally used Calibre's server feature to share them with our phones. In the past, I've kind of clobbered together things on an Ubuntu box that also served as my main desktop computer. That worked well in the past, but I've recently set up a headless Debian Squeeze server, and I decided to migrate my Calibre server there. I had some setbacks, and thought it would be worth it to document the process here, and how I got things to work successfully.

My server runs headless, and that's the first problem with Calibre. The version of it in the Debian Squeeze repositories doesn't have a standalone server mode that is configurable without using the graphical UI. I can't have that, so I decided to get newer binaries installed. Ordinarily, I wouldn't recommend this, especially on a system like Debian whose community prides itself on the stability of their software packages, but this is a functional necessity, so I'll take it. This command, ripped straight from the official Calibre website, worked for me:

sudo python -c "import sys; py3 = sys.version_info[0] > 2; u = __import__('urllib.request' if py3 else 'urllib', fromlist=1); exec(u.urlopen('http://status.calibre-ebook.com/linux_installer').read()); main()"

When prompted, I told it to install to ''/opt/calibre'' so it wouldn't conflict with any system libs or binaries.

That means that ''/opt/calibre/calibre-server'' is the standalone daemon for serving e-books.

I wanted to servicize it, so I wrote this init script and placed it at ''/etc/init.d/calibre-server'':

#!/bin/bash

CALIBRE_LIBRARY_PATH="/home/shared/Calibre Library"
PIDFILE=/tmp/calibre-server.pid
USER=calibre
PORT=8081

start() {
        echo "Starting Calibre server..."
        su -c "calibre-server --with-library=\"$CALIBRE_LIBRARY_PATH\" -p $PORT --pidfile=$PIDFILE --daemonize" & 
        if [ $? -ne 0 ]; then
                echo "Could not start calibre-server."
        fi
}

stop() {
        echo "Stopping Calibre server..."
        if [ -e $PIDFILE ]; then
                read PID < $PIDFILE
                ps aux | grep "$PID" | grep 'calibre-server' > /dev/null
                RUNNING=$?
                if [ $RUNNING -eq 0 ]; then
                        kill $PID
                        if [ $? -eq 0 ]; then
                                rm $PIDFILE
                        fi
                else
                        echo "Could not find a calibre-server process with PID $PID."
                fi
        else
                echo "Could not find pidfile: $PIDFILE"
        fi
}

restart() {
        stop
        start
}

status() {
        if [ -e $PIDFILE ]; then
                read PID < $PIDFILE
                echo "calibre-server is running with PID $PID."
        else
                echo "calibre-server is not running."
        fi
}

unknown() {
        echo "Unrecognized command: $1"
        echo "Try one of the following: (start|stop|restart|status)"
}

case $1 in
        start ) 
                start
                ;;
        stop )
                stop
                ;;
        restart )
                restart
                ;;
        status )
                status
                ;;
        * )
                unknown
                ;;
esac

You can change the variables at the top to run the server differently. Once this is given execute permissions, you can start the server with:

/etc/init.d/calibre-server start

And stopped with

/etc/init.d/calibre-server stop

It looks like the ''service'' command works, too:

service calibre-server start

I know there are some other problems I need to work out (how to import new books, for example), but this seems like a good start. Anyone have any tweaks or additions to note?