Suspend NAS when idle

My NAS with 4TB RAID-5 storage consumes round about 40-45 Watt. Not very much for a "full featured" Athlon X2 4600+ Linux systems, but running 24/7 it produces around 70 EUR per year on energy costs, so I would like to suspend it to ram when idleing. But since I don't want to press a button or running wake-on-lan manually when watching a video, my HTPCs (a Mac Mini running Ubuntu and a Raspberry Pi running XBian) should wake it automatically when accessing the media directory.

So, let's start with the server, I'll write a second post for the client part:

I want it to suspend to ram, but not if

  • a user is connected to the system using sshfs
  • a user has been connected within the last 30 minutes (it's likely that he will come back, soon)
  • a screen session is active (for example when reencoding videos in background)
  • the system is active for less than 30 minutes

Required tools:

  • ethtool
  • pm-utils

First of all, ensure that the system can be woken up. There are a lot of manuals how to do this, but I like this one most because it works for every attached network device:

Create "/etc/udev/rules.d/50-wol.rules" with the following contents:

ACTION=="add", SUBSYSTEM=="net", KERNEL=="eth*", RUN+="/usr/bin/ethtool -s %k wol g"

Next, we need to know when a user logged on or of. I've implemented this by storing the username in a file named by the process pid in /var/run/system-login/active/ and /var/run/system-login/active/ using pam_exec.so. First, create "/usr/local/sbin/system-login" and make the file executable:

#!/bin/bash

# Check if process is alive using pgrep -P $pid
ppid="$(ps -p $$ -o ppid= | tr -d ' ')"

activepath="/var/run/system-login/active"
inactivepath="/var/run/system-login/inactive"

case "$PAM_TYPE" in
    'open_session')
        test -d "$activepath" || mkdir -p "$activepath"
        echo "$PAM_USER" > "$activepath/$ppid"
    ;;

    'close_session')
        test -d "$inactivepath" || mkdir -p "$inactivepath"
        test -e "$activepath/$ppid" && mv "$activepath/$ppid" "$inactivepath/$ppid"
    ;;
esac

Next, add this to the according files under /etc/pam.d:

session    optional   pam_exec.so          quiet /usr/local/sbin/system-login

Using Arch Linux, this would be /etc/pam.d/system-login. On Ubuntu, you can use /etc/pam.d/common-session or append it directly to /etc/pam.d/sshd. Now, reboot, login and ensure that /var/run/system-login/active contains nothing else but your current login. Otherwise you would have to choose another pam.d file or add additional tests to the script.

But how to check how long the system is active? "uptime" tells us nothing about resumes from suspend, so we have to add this information manually. Create "/etc/pm/sleep.d/50update-last-resume" and make it executable:

#!/bin/sh

if [ -n "$1" ] && ([ "$1" = "resume" ] || [ "$1" = "thaw" ]); then
    touch "/var/run/last-resume"
fi

Run "pm-suspend", resume the system and check whether a file called /var/run/last-resume has been created.

Finally, we need a script that checks all conditions and tells us if we should suspend or not. The following code, stored in /usr/local/sbin/should-suspend (and made executable, of course) will do this:

#!/bin/bash

result=0
timeout=30
NOW=`date +%s`

# Check system uptime
test -e /var/run/last-resume || touch /var/run/last-resume
if find /var/run/ -maxdepth 1 -type f -name last-resume -cmin -$timeout | grep -q .; then
    let age=(NOW-$(stat -c %Z /var/run/last-resume))/60
    echo "uptime to short ($age min)"
    result=1
fi

# Check for active screen sessions
if pgrep -l '^screen$'; then
    result=1
fi

# Check for active user
if [ -d "/var/run/system-login/active" ]; then
    for n in $(find /var/run/system-login/active -type f -printf "%f\n"); do
        if pgrep -P "$n" > /dev/null; then
            result=1
            filename="/var/run/system-login/active/$n"
            let age=(NOW-$(stat -c %Z "$filename"))/60
            echo "active login: $n (`cat "$filename"`, $age min)"
        else
            rm "/var/run/system-login/active/$n"
        fi
    done
fi

# Check for logouts that where at least $timeout minutes ago
if [ -d "/var/run/system-login/inactive" ]; then
    for n in $(find /var/run/system-login/inactive -cmin -$timeout -type f -printf "%f\n"); do
        result=1
        filename="/var/run/system-login/inactive/$n"
        let age=(NOW-$(stat -c %Z "$filename"))/60
        echo "inactive login: $n (`cat "$filename"`, $age min)"
    done
fi

exit $result

If everything's fine, this script will print nothing and exit with 0, otherwise it will exit with 1 and print the reasons.

Last, add a cronjob in /etc/cron.d/suspend-on-idle that check's every five minutes if the conditions are met and call pm-suspend:

SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root
*/5 * * * * root /usr/local/sbin/should-suspend > /dev/null && pm-suspend

This is it. Let the system idle for a while, check if it suspends and if you can wake it using wake-on-lan.

Please note: Since I'm having a Linux-only enviroment, this will only work with SSHFS/SFTP/SCP mounts, not with SMB. I'm not sure if it is possible to adapt this technique to these protocols. Maybe SMB fires a PAM event, than it should be possible to bind system-login to this.

Comments

Great page, works very nice.

Two comments:

1. When running lightdm display manager user "lightdm" will prevent suspend.

Modification of the file "system-login" will help

....
case "$PAM_TYPE" in
'open_session')
test -d "$activepath" || mkdir -p "$activepath"
# modification needed since lightdm display manager might be active...
if [ "$PAM_USER" != "lightdm" ]; then
echo "$PAM_USER" > "$activepath/$ppid"
fi
;;
...

2. "uptime to short" in "should-suspend" should read "uptime too short"

Thanks again for sharing your code!
Martin.