Commit 4658545e authored by Leigh Stoller's avatar Leigh Stoller

A bunch of lastlogin changes! The user and experiment information

pages now show the lastlogin info that is gathered from sshd syslog
reporting to users. That info is parsed by security/genlastlog.c, and
entered into the DB in the nodeuidlastlogin and uidnodelastlogin
tables. If not obvious from the names, for each user we want the last time
they logged in anyplace, and for each node we want the last time anyone
logged into it. The latter is obviously more useful for scheduling
purposes. All of the various images have new /etc/syslog.conf files,
and the 6.2 got new sshd_configs (all cvsup'ed with kill -HUP). There
is an entry in boss:/etc/crontab and users:/etc/syslog.conf. All of
this is decribed in greater detail in security/genlastlog.c.
parent fbe3c88e
......@@ -1030,7 +1030,7 @@ outfiles="$outfiles Makeconf GNUmakefile \
lib/GNUmakefile \
os/GNUmakefile os/split-image.sh os/imagezip/GNUmakefile \
pxe/GNUmakefile pxe/proxydhcp.restart pxe/bootinfo.restart \
security/GNUmakefile security/paperbag \
security/GNUmakefile security/paperbag security/genlastlog.wrapper \
tbsetup/GNUmakefile tbsetup/console_setup tbsetup/mkacct-ctrl \
tbsetup/console_reset tbsetup/bwconfig tbsetup/power_rpc27.pm \
tbsetup/os_load tbsetup/os_setup tbsetup/mkprojdir tbsetup/power \
......
......@@ -152,7 +152,7 @@ outfiles="$outfiles Makeconf GNUmakefile \
lib/GNUmakefile \
os/GNUmakefile os/split-image.sh os/imagezip/GNUmakefile \
pxe/GNUmakefile pxe/proxydhcp.restart pxe/bootinfo.restart \
security/GNUmakefile security/paperbag \
security/GNUmakefile security/paperbag security/genlastlog.wrapper \
tbsetup/GNUmakefile tbsetup/console_setup tbsetup/mkacct-ctrl \
tbsetup/console_reset tbsetup/bwconfig tbsetup/power_rpc27.pm \
tbsetup/os_load tbsetup/os_setup tbsetup/mkprojdir tbsetup/power \
......
......@@ -70,7 +70,7 @@ use Exporter;
DBQuoteSpecial UNIX2DBUID ExpState SetExpState ProjLeader
ExpNodes DBDateTime DefaultImageID GroupLeader TBGroupUnixInfo
TBValidNodeLogType TBValidNodeName TBSetNodeLogEntry
TBSetSchedReload MapNodeOSID TBLockExp TBUnLockExp
TBSetSchedReload MapNodeOSID TBLockExp TBUnLockExp TBSetExpSwapTime
);
# Must come after package declaration!
......@@ -749,6 +749,28 @@ sub SetExpState($$$)
return 1;
}
#
# Set the swap in/out time for an experiment.
#
# usage: TBSetExpSwapTime(char *pid, char *eid)
# returns 1 if okay.
# returns 0 if an invalid pid/eid or if an error.
#
sub TBSetExpSwapTime($$)
{
my($pid, $eid) = @_;
my $query_result =
DBQueryWarn("update experiments set expt_swapped=now() ".
"where eid='$eid' and pid='$pid'");
if (! $query_result ||
$query_result->numrows == 0) {
return 0;
}
return 1;
}
#
# Lock Experiment.
#
......
......@@ -9,17 +9,18 @@ SUBDIR = security
include $(OBJDIR)/Makeconf
BINS = suexec lastlogin
SCRIPTS = paperbag plasticwrap
SCRIPTS = plasticwrap
SBINS = paperbag genlastlog genlastlog.wrapper
#
# Force dependencies on the scripts so that they will be rerun through
# configure if the .in file is changed.
#
all: $(BINS) $(SCRIPTS)
all: $(BINS) $(SCRIPTS) $(SBINS)
include $(TESTBED_SRCDIR)/GNUmakerules
CFLAGS = -O -g
CFLAGS += -O2 -g
suexec: suexec.c suexec.h
$(CC) $(CFLAGS) \
......@@ -33,8 +34,16 @@ lastlogin: lastlogin.c
-DUSERSVAR='"$(prefix)/usersvar"' \
-o lastlogin $<
genlastlog: genlastlog.c
$(CC) $(CFLAGS) -I/usr/local/include \
-DTBDBNAME='"$(TBDBNAME)"' \
-DUSERSVAR='"$(prefix)/usersvar"' \
-DOURDOMAIN='"$(OURDOMAIN)"' \
-DUSERNODE='"$(USERNODE)"' \
-o genlastlog $< -L/usr/local/lib/mysql -lmysqlclient
install: $(addprefix $(INSTALL_LIBEXECDIR)/, $(BINS)) \
$(addprefix $(INSTALL_SBINDIR)/, paperbag)
$(addprefix $(INSTALL_SBINDIR)/, $(SBINS))
@echo "Don't forget to do a post-install as root"
post-install:
......
/*
* This odd program is used to generate last login information on a per
* user and per node basis. That is, for each user we want the last time
* they logged in anyplace, and for each node we want the last time anyone
* logged into it. The latter is obviously more useful for scheduling
* purposes.
*
* We get this information from all of the syslog entries that are reported in
* by all the nodes when people ssh login. We have set up each experimental
* node to report auth.info to @users.emulab.net. Note that the start flags
* to syslogd (on users) must be changed to allows these incoming UDP packets.
* In /etc/rc.conf:
*
* syslogd_flags="-a 155.101.132.0/22"
*
* This program parses that file and inserts a bunch of entries into the DB.
* See the update comands below.
*
* This program should be run from cron on a periodic basis, say twice an
* hour. Also note that there should be an entry in /etc/syslog.conf on
* the users node that sends auth.info to /var/log/logins, which should
* periodically cleaned with an entry in /etc/newsyslog.conf to keep it from
* filling up the filesystem. The /etc/crontab entry should look like this:
*
* 5,35 * * * * root /usr/testbed/sbin/genlastlog.wrapper
*
* and the /etc/newsyslog.conf entry should look like this:
*
* /var/log/logins 640 7 500 * Z
*
* To prevent information loss that can occurr if the log file is rolled,
* we actually read the first roll file (it is gzipped of course). This
* causes us to do some extra work, but its not too bad.
*/
#include <stdio.h>
#include <time.h>
#include <sys/types.h>
#include <utmp.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pwd.h>
#include <setjmp.h>
#include <sys/fcntl.h>
#include <sys/param.h>
#include <sys/syslog.h>
#include <stdarg.h>
#include <assert.h>
#include <netdb.h>
#include <sys/socket.h>
#include <mysql/mysql.h>
#include <zlib.h>
/*
* This is the NFS mountpoint where users:/var is mounted.
*/
#ifndef USERSVAR
#define USERSVAR "/usr/testbed/usersvar"
#endif
#define LOGINS "log/logins"
#define SSHD "sshd"
static char *progname;
static int debug = 0;
static int mydb_update(char *query, ...);
static int doit(gzFile *infp);
static char opshostname[MAXHOSTNAMELEN];
static jmp_buf deadline;
static int deadfl;
static void
usage(void)
{
fprintf(stderr, "Usage: %s\n", progname);
exit(-1);
}
static void
dead()
{
deadfl = 1;
longjmp(deadline, 1);
}
int
main(int argc, char **argv)
{
gzFile *infp;
char buf[BUFSIZ], *bp;
struct hostent *he;
progname = argv[0];
if (argc != 1)
usage();
openlog("genlastlog", LOG_PID, LOG_USER);
syslog(LOG_NOTICE, "genlastlog starting");
/*
* We need the canonical hostname for the usersnode so that we can
* ignore those logins.
*/
if ((he = gethostbyname(USERNODE)) == NULL) {
syslog(LOG_ERR, "gethostname %s: %s",
USERNODE, hstrerror(h_errno));
exit(-1);
}
strncpy(opshostname, he->h_name, strlen(opshostname));
if (bp = strchr(opshostname, '.'))
*bp = 0;
sprintf(buf, "%s/%s.0.gz", USERSVAR, LOGINS);
/*
* Use setjmp and timer to prevent NFS lockup.
*/
if (setjmp(deadline) == 0) {
alarm(15);
if ((infp = gzopen(buf, "r")) == NULL) {
syslog(LOG_ERR, "Opening %s: %m", buf);
}
else {
doit(infp);
gzclose(infp);
}
}
alarm(0);
sprintf(buf, "%s/%s", USERSVAR, LOGINS);
if (setjmp(deadline) == 0) {
alarm(15);
if ((infp = gzopen(buf, "r")) == NULL) {
syslog(LOG_ERR, "Opening %s: %m", buf);
}
else {
doit(infp);
gzclose(infp);
}
}
alarm(0);
syslog(LOG_NOTICE, "genlastlog ending");
exit(0);
}
static int
doit(gzFile *infp)
{
int i, skip = 0;
time_t curtime, ll_time;
char *user, node[64], prog[128];
char buf[BUFSIZ], *bp;
struct tm tm;
while (1) {
if (gzgets(infp, buf, BUFSIZ) == NULL)
break;
/*
* If the line does not contain a newline, then we skip it
* and try to sync up again. We consider ourselves synced
* when the buffer contains a newline in it.
*/
if (buf[strlen(buf) - 1] != '\n') {
skip = 1;
continue;
}
if (skip) {
skip = 0;
continue;
}
/*
* Thank dog for strptime! Convert the syslog timestamp
* into a tm, and then into regular unix time.
*/
time(&curtime);
localtime_r(&curtime, &tm);
if ((bp = strptime(buf, "%b %e %T", &tm)) == NULL) {
continue;
}
ll_time = mktime(&tm);
/*
* Scanf the next part, which looks like:
*
* node progname[pid]:
*
* Ensure we match the proper number of items.
*/
bzero(node, sizeof(node));
if ((sscanf(bp, "%s %s:", node, prog) != 2))
continue;
/*
* Only sshd matters to us, but not on the operations
* node, since we get that info via lastlogin.
*/
if (strncmp(prog, SSHD, strlen(SSHD)) ||
strncmp(node, opshostname, strlen(node)) == 0)
continue;
/*
* Okay, these kinds of strings matter.
*
* FreeBSD: "Accepted rsa for USER"
* Linux 6.2: "log: RSA authentication for USER"
* Linux 7.1: "session opened for user USER"
*/
#define L1 "Accepted rsa for "
#define L2 "session opened for user "
#define L3 "log: RSA authentication for "
/* Skip to end of program[pid]: and trailing space */
bp = strchr(bp, ':');
bp += 2;
if (strncmp(bp, L1, strlen(L1)) == 0) {
bp += strlen(L1);
}
else if (strncmp(bp, L2, strlen(L2)) == 0) {
bp += strlen(L2);
}
else if (strncmp(bp, L3, strlen(L3)) == 0) {
bp += strlen(L3);
}
else {
continue;
}
/*
* The login name is the next token.
*/
if (! (user = strsep(&bp, " ")))
continue;
/* We do not care about ROOT logins. */
if (strcasecmp(user, "ROOT") == 0)
continue;
if (mydb_update("replace into uidnodelastlogin "
"(uid, node_id, date, time) "
"values ('%s', '%s', "
" FROM_UNIXTIME(%ld, '%%Y-%%m-%%d'), "
" FROM_UNIXTIME(%ld, '%%T')) ",
user, node, ll_time, ll_time) < 0)
break;
if (mydb_update("replace into nodeuidlastlogin "
"(node_id, uid, date, time) "
"values ('%s', '%s', "
" FROM_UNIXTIME(%ld, '%%Y-%%m-%%d'), "
" FROM_UNIXTIME(%ld, '%%T')) ",
node, user, ll_time, ll_time) < 0)
break;
}
return 0;
}
static int
mydb_update(char *query, ...)
{
static MYSQL db;
static int inited;
char querybuf[BUFSIZ];
va_list ap;
int n;
va_start(ap, query);
n = vsnprintf(querybuf, sizeof(querybuf), query, ap);
if (n > sizeof(querybuf)) {
syslog(LOG_ERR, "query too long for buffer");
return 1;
}
if (debug) {
printf("%s\n", querybuf);
return 0;
}
if (! inited) {
mysql_init(&db);
if (mysql_real_connect(&db, 0, 0, 0, TBDBNAME, 0, 0, 0) == 0) {
syslog(LOG_ERR, "%s: connect failed: %s",
TBDBNAME, mysql_error(&db));
return -1;
}
inited = 1;
}
if (mysql_real_query(&db, querybuf, n) != 0) {
syslog(LOG_ERR, "%s: query failed: %s",
TBDBNAME, mysql_error(&db));
return -1;
}
return 0;
}
#!/usr/bin/perl -wT
use English;
#
# usage: genlastlog.wrapper
#
#
# Configure variables
#
my $TB = "@prefix@";
my $TBOPS = "@TBOPSEMAIL@";
my $genlastlog = "$TB/sbin/genlastlog";
# un-taint path
$ENV{'PATH'} = '/bin:/usr/bin:/usr/sbin:/usr/local/bin';
delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
#
# Turn off line buffering on output
#
$| = 1;
#
# Testbed Support libraries
#
use lib "@prefix@/lib";
use libdb;
use libtestbed;
#
# Only root and TB Admins.
#
if ($UID && !TBAdmin($UID)) {
die("*** $0:\n".
" You must be root or a TB administrator\n");
}
#
# Run actual program. We only care about non-zero exit status, in which
# case we notify tbops.
#
if (system("$genlastlog")) {
SENDMAIL($TBOPS, "TESTBED: Genlastlog Failed",
"Please look at the syslog entries for genlastlog!");
}
exit(0);
/*
* A little ditty to pull the last log info out and report a list of
* the last time each person has logged in.
* This program is invoked from the web interface to determine the last
* time a user logged in on the users node. We parse the lastlog
* file from the users node via NFS (see USERSVAR below), and spit out
* the last time he/she logged in. This gives us usage information to aid
* in scheduling and to determine who is really active.
*
* Without a -u specification, print out a list of all users one per line
* with the date of their last login.
*
* The output format is simple:
*
* [uid] date time hostname
*
* where [uid] is added when doing all users, and date, time, hostname all
* come from the lastlog data structure (see utmp.h).
*/
#include <stdio.h>
#include <time.h>
#include <sys/types.h>
......@@ -12,14 +26,20 @@
#include <pwd.h>
#include <setjmp.h>
/*
* This is the NFS mountpoint where users:/var is mounted.
*/
#ifndef USERSVAR
#define USERSVAR "/usr/testbed/usersvar"
#endif
#define LASTLOG "log/lastlog"
static char *progname;
static int doentry(FILE *fp, uid_t uid, int umode);
static jmp_buf deadline;
static int deadfl;
static int debug;
void
usage(void)
......@@ -39,7 +59,7 @@ int
main(int argc, char **argv)
{
FILE *fp;
char buf[BUFSIZ], *uidarg;
char buf[BUFSIZ], *uidarg, *pcarg = 0;
uid_t uid;
int ch, rval, umode = 0;
......@@ -51,13 +71,17 @@ main(int argc, char **argv)
progname = argv[0];
while ((ch = getopt(argc, argv, "u:")) != -1)
while ((ch = getopt(argc, argv, "u:d")) != -1)
switch(ch) {
case 'u':
umode = 1;
uidarg = optarg;
break;
case 'd':
debug = 1;
break;
case 'h':
case '?':
default:
......@@ -86,13 +110,15 @@ main(int argc, char **argv)
}
}
sprintf(buf, "%s/log/lastlog", USERSVAR);
sprintf(buf, "%s/%s", USERSVAR, LASTLOG);
/*
* Protect against NFS timeout.
*/
if (setjmp(deadline) == 0) {
alarm(3);
fp = fopen(buf, "r");
}
alarm(0);
if (deadfl) {
......@@ -153,11 +179,24 @@ doentry(FILE *fp, uid_t uid, int umode)
strftime(buf, sizeof(buf),
"20%y-%m-%d %H:%M:%S", localtime(&ll.ll_time));
if (! ll.ll_host[0])
strcpy(ll.ll_host, "unknown");
if (umode)
printf("%s\n", buf);
printf("%s %.*s", buf, sizeof(ll.ll_host), ll.ll_host);
else
printf("%u %s\n", uid, buf);
printf("%u %s %.*s", uid, buf,
sizeof(ll.ll_host), ll.ll_host);
if (debug) {
struct passwd *pw = getpwuid(uid);
if (pw) {
printf(" (%s)", pw->pw_name);
}
}
printf("\n");
}
return 1;
}
......@@ -165,6 +165,9 @@ if (system("genelists")) {
#
}
# Accounting info.
TBSetExpSwapTime($pid, $eid);
if (!SetExpState($pid, $eid, EXPTSTATE_ACTIVE)) {
print STDERR "*** Failed to set experiment state!\n";
cleanup();
......
......@@ -134,6 +134,9 @@ DBQueryWarn("DELETE from delays where pid='$pid' and eid='$eid'")
DBQueryWarn("DELETE from vlans where pid='$pid' and eid='$eid'")
or $errors++;
# Accounting info.
TBSetExpSwapTime($pid, $eid);
if ($errors == 0) {
SetExpState($pid, $eid, EXPTSTATE_SWAPPED)
or $errors++;
......
......@@ -800,6 +800,63 @@ function TBExptState($pid, $eid)
return $state;
}
function TBUidNodeLastLogin($uid)
{
$query_result =
DBQueryFatal("select * from uidnodelastlogin where uid='$uid'");
if (mysql_num_rows($query_result) == 0) {
return 0;
}
$row = mysql_fetch_array($query_result);
return $row;
}
#
# Return the last login for the node, but only after the experiment was
# created (swapped in).
#
function TBNodeUidLastLogin($node_id)
{
$query_result =
DBQueryFatal("select n.* from nodeuidlastlogin as n ".
"left join reserved as r on n.node_id=r.node_id ".
"left join experiments as e ".
" on r.eid=e.eid and r.pid=e.pid ".
"where r.node_id='$node_id' and ".
" DATE_ADD(n.date, INTERVAL n.time HOUR_SECOND) >= ".
" e.expt_swapped");
if (mysql_num_rows($query_result) == 0) {
return 0;
}
$row = mysql_fetch_array($query_result);
return $row;
}
#
# Return the last login for all of the nodes in an experiment, but only
# after the experiment was created (swapped in).
#
function TBExpUidLastLogins($pid, $eid)
{
$query_result =
DBQueryFatal("select n.* from nodeuidlastlogin as n ".
"left join reserved as r on n.node_id=r.node_id ".
"left join experiments as e ".
" on r.eid=e.eid and r.pid=e.pid ".
"where r.pid='$pid' and r.eid='$eid' and ".
" DATE_ADD(n.date, INTERVAL n.time HOUR_SECOND) >= ".
" e.expt_swapped ".
" order by n.date DESC");
if (mysql_num_rows($query_result) == 0) {
return 0;
}
$row = mysql_fetch_array($query_result);
return $row;
}
#
# DB Interface.
#
......
......@@ -208,6 +208,7 @@ function LASTUSERSLOGIN($uid) {
if ($uid) {
$uidarg = "-u $uid";
}
$cmdandargs = "$TBLIBEXEC_DIR/lastlogin $uidarg";
$result = exec($cmdandargs, $output, $retval);
......@@ -225,22 +226,43 @@ function LASTUSERSLOGIN($uid) {
}
return 0;
}
$hashtable = array();
#
# In single user mode, return a hash of date, time, node.
#
if ($uid) {
return $output[0];
if (! $output[0])
return 0;
$parts = explode(" ", $output[0]);
$hashtable["date"] = $parts[0];
$hashtable["time"] = $parts[1];
$hashtable["node_id"] = $parts[2];
return $hashtable;
}
#
# Convert to a hash table.
# In all user mode, return a hash of hashes, indexed by uid.
#
$hashtable = array();
for ($i = 0; $i < count($output); $i++) {
$utable = array();
$parts = explode(" ", $output[$i]);
$hashtable["$parts[0]"] = "$parts[1] $parts[2]";
$utable["date"] = $parts[1];
$utable["time"] = $parts[2];
$utable["node_id"] = $parts[3];
$hashtable["$parts[0]"] = $utable;
}
return $hashtable;
}
function LASTNODELOGIN($node)
{
}
#
# Beware empty spaces (cookies)!
#
......
......@@ -46,10 +46,12 @@ if (mysql_num_rows($experiments_result)) {
<tr>
<td width=8%>PID</td>
<td width=8%>EID</td>
<td width=3%>PCs</td>
<td width=3%>Sharks</td>
<td width=4% align=center>Terminate</td>
<td width=70%>Name</td>
<td width=3%>PCs</td>\n";
if ($isadmin)
echo "<td width=17% align=center>Last Login</td>\n";
echo " <td width=60%>Name</td>
<td width=4%>Head UID</td>
</tr>\n";
......@@ -83,12 +85,20 @@ if (mysql_num_rows($experiments_result)) {
<td><A href='showproject.php3?pid=$pid'>$pid</A></td>
<td><A href='showexp.php3?pid=$pid&eid=$eid'>
$eid</A></td>
<td>".$usage["pc"]." &nbsp;</td>
<td>".$usage["shark"]." &nbsp;</td>
<td align=center>
<A href='endexp.php3?pid=$pid&eid=$eid'>
<img alt=\"o\" src=\"redball.gif\"></A></td>
<td>$name</td>
<td>".$usage["pc"]." &nbsp;</td>\n";
if ($isadmin) {
$foo = "N/A";
if ($lastexpnodelogins = TBExpUidLastLogins($pid, $eid)) {
$foo = $lastexpnodelogins["date"] . " " .
"(" . $lastexpnodelogins["uid"] . ")";
}
echo "<td>$foo</td>\n";
}
echo "<td>$name</td>
<td><A href='showuser.php3?target_uid=$huid'>
$huid</A></td>
</tr>\n";
......
......@@ -292,8 +292,21 @@ function SHOWUSER($uid) {
#
if (($lastweblogin = LASTWEBLOGIN($uid)) == 0)