genlastlog.c 9.33 KB
Newer Older
Leigh Stoller's avatar
Leigh Stoller committed
1
/*
2
 * Copyright (c) 2000-2014 University of Utah and the Flux Group.
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
 * 
 * {{{EMULAB-LICENSE
 * 
 * This file is part of the Emulab network testbed software.
 * 
 * This file is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or (at
 * your option) any later version.
 * 
 * This file is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public
 * License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with this file.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * }}}
Leigh Stoller's avatar
Leigh Stoller committed
22 23
 */

24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
/*
 * 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.
40
 * See the update comands below.
41
 *
42 43
 * The entry in users:/etc/syslog.conf to capture the syslog data coming
 * from the nodes:
44
 *
45
 *	auth.info			/var/log/logins
46
 *
47 48 49 50 51 52
 * while on each experimental node:
 * 
 *	auth.info			@users.emulab.net
 *
 * Of course, you need to make sure /var/log/logins is cleaned periodically,
 * so put an entry in /etc/newsyslog.conf:
53 54 55
 *
 *	/var/log/logins           640  7     500 *     Z
 *
56 57 58 59
 * To prevent information loss that can occurr between the primary file
 * and the first roll file (logins.0.gz), the program actually reads the
 * first roll file (it is gzipped of course). This causes us to do some
 * extra work, but thats okay since we really do not want to lose that data.
60
 */
61

62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
#include <stdio.h>
#include <time.h>
#include <sys/types.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>
79
#include "tbdb.h"
80 81 82 83 84 85 86 87 88 89 90

/*
 * 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"

91 92 93 94
#ifndef LOG_TESTBED
#define LOG_TESTBED	LOG_USER
#endif

95 96
static char		*progname;
static int		debug = 0;
97
static int		doit(gzFile infp);
98 99 100 101 102 103 104
static char		opshostname[MAXHOSTNAMELEN];
static jmp_buf		deadline;
static int		deadfl;

static void
usage(void)
{
105
	fprintf(stderr, "Usage: %s [-a]\n", progname);
106 107 108 109 110 111 112 113 114 115 116 117 118
	exit(-1);
}

static void
dead()
{
	deadfl = 1;
	longjmp(deadline, 1);
}

int
main(int argc, char **argv)
{
119
	gzFile	        infp;
120
	char		buf[BUFSIZ], *bp, **aliases;
121
	struct hostent  *he;
122 123 124
	int		ch, errors = 0;
	int		backcount = 0;

125 126
	progname = argv[0];

127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
	while ((ch = getopt(argc, argv, "a:")) != -1)
		switch(ch) {
		case 'a':
			backcount = atoi(optarg);
			break;
			
		case 'h':
		case '?':
		default:
			usage();
		}
	argc -= optind;
	argv += optind;

	if (argc)
142 143
		usage();

144
	openlog("genlastlog", LOG_PID, LOG_TESTBED);
145 146
	syslog(LOG_NOTICE, "genlastlog starting");

147 148 149 150 151
	if (!dbinit()) {
		syslog(LOG_ERR, "Could not connect to DB!");
		exit(1);
	}

152 153
	/*
	 * We need the canonical hostname for the usersnode so that we can
154
	 * put those logins in another table.
155 156 157 158 159 160
	 */
	if ((he = gethostbyname(USERNODE)) == NULL) {
		syslog(LOG_ERR, "gethostname %s: %s",
		       USERNODE, hstrerror(h_errno));
		exit(-1);
	}
161 162
	strncpy(opshostname, he->h_name, sizeof(opshostname));

163
	if ((bp = strchr(opshostname, '.')) != 0)
164 165
		*bp = 0;

166 167
	while (backcount) {
		sprintf(buf, "%s/%s.%d.gz", USERSVAR, LOGINS, backcount);
168

169 170 171 172 173 174 175 176 177 178 179 180 181 182
		/*
		 * 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);
				errors++;
			}
			else {
				doit(infp);
				gzclose(infp);
			}
183
		}
184 185
		backcount--;
		alarm(0);
186 187 188 189 190 191 192 193 194
	}
	
	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);
195
			errors++;
196 197 198 199 200 201 202 203 204
		}
		else {
			doit(infp);
			gzclose(infp);
		}
	}
	alarm(0);

	syslog(LOG_NOTICE, "genlastlog ending");
205
	exit(errors);
206 207 208 209
	
}

static int
210
doit(gzFile infp)
211 212 213
{
	int		i, skip = 0;
	time_t		curtime, ll_time;
214 215
	char		*user, node[TBDB_FLEN_NODEID * 2], prog[128];
	char		buf[BUFSIZ], *bp, uid_idx[128], tmp[BUFSIZ];
216
	struct tm	tm;
217 218 219
	MYSQL_RES	*dbres;
	MYSQL_ROW	dbrow;
	
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
	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;
236
 		}
237 238 239 240 241 242 243 244 245 246 247 248

		/*
		 * 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);

249 250 251 252 253 254 255 256 257 258 259
		/*
		 * If the constructed time is in the future, then we have
		 * year off by one (cause we are possibly looking at files
		 * created in the previous year). Set the year back by one,
		 * and redo.
		 */
		if (ll_time > curtime) {
			tm.tm_year--;
			ll_time = mktime(&tm);
		}

260 261 262 263 264 265 266 267 268 269 270 271
		/*
		 * 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;

		/*
272
		 * Only sshd matters to us.
273
		 */
274
		if (strncmp(prog, SSHD, strlen(SSHD)))
275 276 277 278 279 280 281 282
			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"
283 284 285
		 *      (several ssh2): "Accepted publickey for USER"
		 *      (several ssh2): "Accepted password for USER"
		 *      (several ssh2): "Accepted keyboard-interactive for USER"
286 287 288 289
		 */
#define L1	"Accepted rsa for "
#define L2	"session opened for user "
#define L3	"log: RSA authentication for "
290 291
#define L4	"Accepted publickey for "
#define L5	"Accepted password for "
292
#define L6	"Accepted keyboard-interactive for "
293
		
294 295 296 297 298
		/* Skip to end of program[pid]: and trailing space */
		bp = strchr(bp, ':');
		bp += 2;

		if (strncmp(bp, L1, strlen(L1)) == 0) {
299
		  /*fprintf(stdout,"Hit L1: ");*/
300 301 302
			bp += strlen(L1);
		}
		else if (strncmp(bp, L2, strlen(L2)) == 0) {
303
		  /*fprintf(stdout,"Hit L2: ");*/
304 305 306
			bp += strlen(L2);
		}
		else if (strncmp(bp, L3, strlen(L3)) == 0) {
307
		  /*fprintf(stdout,"Hit L3: ");*/
308 309
			bp += strlen(L3);
		}
310 311 312 313 314 315 316 317
		else if (strncmp(bp, L4, strlen(L4)) == 0) {
		  /*fprintf(stdout,"Hit L4: ");*/
			bp += strlen(L4);
		}
		else if (strncmp(bp, L5, strlen(L5)) == 0) {
		  /*fprintf(stdout,"Hit L5: ");*/
			bp += strlen(L5);
		}
318 319 320 321
		else if (strncmp(bp, L6, strlen(L6)) == 0) {
		  /*fprintf(stdout,"Hit L6: ");*/
			bp += strlen(L6);
		}
322 323 324 325 326 327 328 329 330
		else {
			continue;
		}

		/*
		 * The login name is the next token.
		 */
		if (! (user = strsep(&bp, " ")))
			continue;
331
		/*fprintf(stdout,"%s on %s\n",user,node);*/
332 333 334 335 336

		/* We do not care about ROOT logins. */
		if (strcasecmp(user, "ROOT") == 0)
			continue;

337 338
		dbres = mydb_query("select uid_idx from users where uid='%s' "
				   "and status!='archived' and status!='nonlocal'",
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362
				   1, user);

		if (!dbres) {
			syslog(LOG_ERR, "DB error getting user %s", user);
			continue;
		}

		if (!mysql_num_rows(dbres)) {
			syslog(LOG_INFO, "No DB record for user %s", user);
			mysql_free_result(dbres);
			continue;
		}
		dbrow = mysql_fetch_row(dbres);
		strncpy(uid_idx, dbrow[0], sizeof(uid_idx));
		mysql_free_result(dbres);

		/*
		 * Safety first. 
		 */
		mydb_escape_string(tmp, uid_idx, strlen(uid_idx));
		strcpy(uid_idx, tmp);
		mydb_escape_string(tmp, node, strlen(node));
		strcpy(node, tmp);

363
		if (mydb_update("replace into uidnodelastlogin "
364 365
				"(uid, uid_idx, node_id, date, time) "
				"values ('%s', '%s', '%s', "
366 367
				"        FROM_UNIXTIME(%ld, '%%Y-%%m-%%d'), "
				"        FROM_UNIXTIME(%ld, '%%T')) ",
368
				user, uid_idx, node, ll_time, ll_time) == 0)
369 370
			break;

371
		if (strncmp(node, opshostname, strlen(node)) == 0 ||
372 373
		    strncmp(node, "ops", strlen(node)) == 0) {
			if (mydb_update("replace into userslastlogin "
374 375
					"(uid, uid_idx, date, time) "
					"values ('%s', '%s', "
376 377
					"  FROM_UNIXTIME(%ld, '%%Y-%%m-%%d'), "
					"  FROM_UNIXTIME(%ld, '%%T')) ",
378
					user, uid_idx, ll_time, ll_time) == 0)
379 380 381 382
				break;
		}
		else {
			if (mydb_update("replace into nodeuidlastlogin "
383 384
					"(node_id, uid_idx, uid, date, time) "
					"values ('%s', '%s', '%s', "
385 386
					"  FROM_UNIXTIME(%ld, '%%Y-%%m-%%d'), "
					"  FROM_UNIXTIME(%ld, '%%T')) ",
387
					node, uid_idx, user, ll_time, ll_time) == 0)
388 389
				break;
		}
390 391 392
	}
	return 0;
}