blogroll tags

Auto-poweroff that server in your cellar

Our cellar houses an old grey box that acts as a home server for the family. It's quite useful in a number of ways, as a file server, web server, database server, and so on. It also, traditionally, had a habit of wasting power—it's so much nicer to just have the machine running when you use it. But, with the wonders of Wake on LAN, even the “I'm too lazy to run into the cellar” argument has lost any validity it might have had.

So much for turning the box on, how about turning it off? Figuring out when nobody is using the machine and then remembering to turn it off as well is hardly a task for a mere mortal. So I wrote a script that does it for me. is_anyone_here.py checks whether anyone is logged in, and looks for any evidence of recent usage. It was written on/for a Debian GNU/Linux (lenny) system with vsftpd and samba, and may require some modifications to work properly in your environment.

Have a look at the whole script after the break.

#!/usr/bin/env python
# Python 2.4 or newer. Python 3 untested.

"""
usage: is_anyone_here.py [-v] [{mitutes}]

  -v         print what the script is doing.
  {minutes}  number of minutes in the past to check for human
             activity.

An exit status of 0 means that nobody appears to be using the system.
A status of 1 means that there are humans lurking somewhere.
"""

import sys
import os
import re
from subprocess import Popen, PIPE
from datetime import *

_VERBOSE = False

def _dbg(s):
    if _VERBOSE:
        print (s)

def main(argv):
    MINUTES = 0
    if len(argv) > 1 and argv[1] == '-v':
        globals()['_VERBOSE'] = True
    elif len(argv) > 1 and argv[1] == '-h':
        print __doc__
        return True
    if len(argv) > 1 and argv[-1].isdigit():
        MINUTES = int(argv[-1])
        _dbg("Checking for humans within the past %d minutes." % MINUTES)

    if not check_who():
        return False
    if not check_smbstatus():
        return False
    if MINUTES > 0:
        if not check_auth_log(MINUTES):
            return False
        if not check_vsftpd_log(MINUTES):
            return False
        if not check_smb_log(MINUTES):
            return False

    return True

def check_who():
    who = Popen(['who'], stdout=PIPE)
    output = who.communicate()[0].strip() + '\nEOF'
    nusers = len(output.strip().split('\n'))-1
    _dbg("%d users logged in." % nusers)
    if nusers > 0:
        return False
    else:
        return True

def check_smbstatus():
    who = Popen(['smbstatus', '-S'], stdout=PIPE)
    outlines = who.communicate()[0].strip().split('\n')
    in_content = False
    nusers = 0
    for l in outlines:
        # crop header
        if l.startswith('------------------'):
            in_content = True
        elif in_content: nusers += 1

    _dbg("%d users using samba." % nusers)

    if nusers > 0:
        return False
    else:
        return True

def check_auth_log(m):
    f = file('/var/log/auth.log', 'r')
    now = datetime.now()
    for line in f:
        # ignore cron and at sessions: they don't qualify as human use.
        if not 'cron:session' in line and not 'atd:session' in line:
            lastline = line
    # insert the current year, since auth.log is sloppy like that.
    # this will cause problems at the beginning of the year, but I
    # can live with that.
    datestring = re.sub(r'^(\S+ +\d+ +\S{8}).*$',
                        r'%d \1' % now.year,
                        lastline).strip()
    _dbg("last auth.log entry at %s" % datestring)
    date = datetime.strptime(datestring, '%Y %b %d %H:%M:%S')
    if now - date > timedelta(minutes=m):
        return True
    else:
        return False

def check_vsftpd_log(m):
    f = file('/var/log/vsftpd.log', 'r')
    now = datetime.now()
    for line in f:
        lastline = line
    datestring = re.match(r'^(\S+ +\S+ +\d+ +\S{8} +\d{4}).*$',
                          lastline).group(1)
    _dbg("last vsftpd.log entry at %s" % datestring)
    date = datetime.strptime(datestring, '%a %b %d %H:%M:%S %Y')
    if now - date > timedelta(minutes=m):
        return True
    else:
        return False

def check_smb_log(m):
    files = os.listdir('/var/log/samba')
    now = datetime.now()
    date = datetime(1900,1,1)
    # ignore log.smbd and log.nmbd - they contain crap like
    # "can't connect to CUPS."
    for f in (file('/var/log/samba/' + fn, 'r')
              for fn in files if fn.startswith('log.')
                                 and fn not in ('log.smbd', 'log.nmbd')):
        lastline = None
        for line in f:
            if line.startswith('['):
                lastline = line
        if lastline is not None:
            thisdate = datetime.strptime(
                        re.match(r'^\[(.*?),.*$', lastline).group(1),
                        '%Y/%m/%d %H:%M:%S')
            if thisdate > date:
                date = thisdate
                _dbg("found samba log entry from %s" % thisdate)

    if now - date > timedelta(minutes=m):
        return True
    else:
        return False


if __name__ == '__main__':
    # dates in log files are C-locale.
    import locale
    locale.setlocale(locale.LC_ALL, 'C')

    if main(sys.argv):
        exit(0)
    else:
        _dbg("the system is in use by people.")
        exit(1)

UPDATE 2010-05-29: Tiny bug in the script removed. Might work now if it didn't before.

This just looks for evidence of humans. If you don't use vsftpd or samba, you'd have to remove those function calls in main. To shut down the system when nobody is using it, you could add something like this to your /etc/crontab:

*/15 * * * * root is_anyone_here.py 40 && poweroff

This would power down the machine after 40 minutes of boredom.

:
:
:
: