Welcome, guest | Sign In | My Account | Store | Cart

It took me a while to figure out the file format for /var/log/lastlog on *NIX type machines. Some of the sysadmins out there may find it usefull to be able to determine the last time of login for an account without using any other apps.

Python, 40 lines
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#! /usr/bin/env python

import struct

def getrecord(file,uid, preserve = False):
  """Returns [int(unix_time),string(device),string(host)] from the lastlog formated file object, set preserve = True to preserve your position within the file"""
  position = file.tell()
  recordsize = struct.calcsize('L32s256s')
  file.seek(recordsize*uid)
  data = file.read(recordsize)
  if preserve:
    file.seek(position)
  try:
    returnlist =  list(struct.unpack('L32s256s',data))
    returnlist[1] = returnlist[1].replace('\x00','')
    returnlist[2] = returnlist[2].replace('\x00','')
    return returnlist
  except:
    return False

if __name__ == '__main__':
  import sys
  import pwd
  import time

  try:
    llfile = open("/var/log/lastlog",'r')
  except:
    print "Unable to open /var/log/lastlog"
    sys.exit(1)

  for user in pwd.getpwall():
    record = getrecord(llfile,user[2])
    if record and record[0] > 0:
      print '%16s\t\t%s\t%s' % (user[0],time.ctime(record[0]),record[2])
    elif record:
      print '%16s\t\tNever logged in' % (user[0],)
    else:
      pass
  llfile.close()

It took me a while to figure out the file format for /var/log/lastlog on NIX type machines. This file contains recordsizelargest_uid records, each record is accessed by seeking to the uid*recordsize and reading recordsize bytes. Each record consists of a long unsigned integer (important to note that on some machines this is the same size as an unsigned integer but may differ), up to a 16 character string denoting the device the connection was made with (i.e. tty0,:0,pts1), and a 256 character string denoting the host from which the connection was made. I tried to make this implementation as simple and flexible as possible and stick to the standard library. I welcome any suggestions for making this more efficient, usefull, elegant, etc.

4 comments

Tim Heaney 15 years, 6 months ago  # | flag

Small change for x86_64. I can't imagine having access to the lastlog file, but not the lastlog program. As you say, the file format is system dependent and the lastlog program has been compiled with the system header files that define what that is. So something like os.popen('lastlog') is probably a better way to get this information from within Python.

Anyway, to get your program to work on my x86_64 machine, I had to add an initial = to the struct format. That is, I added the line

lastlogformat = '=L32s256s'

and then replaced both occurrences of your format string with lastlogformat.

Mike Lowe (author) 15 years, 6 months ago  # | flag

I overlooked the byteorder, that is a very good suggestion.

This most likely will never be of use to you, but I have clusters where I need to aggregate several lastlog files. I also have 40000 users and would like to avoid the extra over head of pipes, file opens for every query, and nonsquential access if I step through all of the uid's.

Mike Lowe (author) 15 years, 6 months ago  # | flag

On further examination '=' prefix works for x86 and x86_64, but not for ia64. I am not sure what the best way to make this work on all arches.

Diane Trout 10 years, 1 month ago  # | flag

Those should be C-style null terminated strings in the file, so instead of .replace('\x00',''), wouldn't the following work better?

try:
  returnlist =  list(struct.unpack(LASTLOG_FMT, data))
  returnlist[1] = returnlist[1][:returnlist[1].index('\x00')]
  returnlist[2] = returnlist[2][:returnlist[2].index('\x00')]
  return returnlist
except:
  return False