Home | History | Annotate | Line # | Download | only in newsyslog
newsyslog.c revision 1.28
      1 /*	$NetBSD: newsyslog.c,v 1.28 2000/07/10 11:15:07 ad Exp $	*/
      2 
      3 /*
      4  * Copyright (c) 1999, 2000 Andrew Doran <ad (at) NetBSD.org>
      5  * All rights reserved.
      6  *
      7  * Redistribution and use in source and binary forms, with or without
      8  * modification, are permitted provided that the following conditions
      9  * are met:
     10  * 1. Redistributions of source code must retain the above copyright
     11  *    notice, this list of conditions and the following disclaimer.
     12  * 2. Redistributions in binary form must reproduce the above copyright
     13  *    notice, this list of conditions and the following disclaimer in the
     14  *    documentation and/or other materials provided with the distribution.
     15  *
     16  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
     17  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
     18  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
     19  * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
     20  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
     21  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
     22  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
     23  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
     24  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
     25  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
     26  * SUCH DAMAGE.
     27  *
     28  */
     29 
     30 /*
     31  * This file contains changes from the Open Software Foundation.
     32  */
     33 
     34 /*
     35  * Copyright 1988, 1989 by the Massachusetts Institute of Technology
     36  *
     37  * Permission to use, copy, modify, and distribute this software
     38  * and its documentation for any purpose and without fee is
     39  * hereby granted, provided that the above copyright notice
     40  * appear in all copies and that both that copyright notice and
     41  * this permission notice appear in supporting documentation,
     42  * and that the names of M.I.T. and the M.I.T. S.I.P.B. not be
     43  * used in advertising or publicity pertaining to distribution
     44  * of the software without specific, written prior permission.
     45  * M.I.T. and the M.I.T. S.I.P.B. make no representations about
     46  * the suitability of this software for any purpose.  It is
     47  * provided "as is" without express or implied warranty.
     48  *
     49  */
     50 
     51 /*
     52  * newsyslog(1) - a program to roll over log files provided that specified
     53  * critera are met, optionally preserving a number of historical log files.
     54  */
     55 
     56 #include <sys/cdefs.h>
     57 #ifndef lint
     58 __RCSID("$NetBSD: newsyslog.c,v 1.28 2000/07/10 11:15:07 ad Exp $");
     59 #endif /* not lint */
     60 
     61 #include <sys/types.h>
     62 #include <sys/time.h>
     63 #include <sys/stat.h>
     64 #include <sys/param.h>
     65 
     66 #include <ctype.h>
     67 #include <fcntl.h>
     68 #include <grp.h>
     69 #include <pwd.h>
     70 #include <signal.h>
     71 #include <stdio.h>
     72 #include <stdlib.h>
     73 #include <stdarg.h>
     74 #include <string.h>
     75 #include <time.h>
     76 #include <unistd.h>
     77 #include <errno.h>
     78 #include <err.h>
     79 #include <util.h>
     80 
     81 #include "pathnames.h"
     82 
     83 #define	PRHDRINFO(x)	((void)(verbose ? printf x : 0))
     84 #define	PRINFO(x)	((void)(verbose ? printf("  ") + printf x : 0))
     85 
     86 #define	CE_COMPACT	1	/* Compact the achived log files */
     87 #define	CE_BINARY	2	/* Logfile is a binary file/non-syslog */
     88 #define	CE_NOSIGNAL	4	/* Don't send a signal when trimmed */
     89 #define CE_CREATE	8	/* Create log file if none exists */
     90 
     91 struct conf_entry {
     92 	uid_t	uid;			/* Owner of log */
     93 	gid_t	gid;			/* Group of log */
     94 	mode_t	mode;			/* File permissions */
     95 	int	numhist;		/* Number of historical logs to keep */
     96 	size_t	maxsize;		/* Maximum log size */
     97 	int	maxage;			/* Hours between log trimming */
     98 	int	flags;			/* Flags (CE_*) */
     99 	int	signum;			/* Signal to send */
    100 	char	pidfile[MAXPATHLEN];	/* File containing PID to signal */
    101 	char	logfile[MAXPATHLEN];	/* Path to log file */
    102 };
    103 
    104 int     verbose = 0;			/* Be verbose */
    105 int	noaction = 0;			/* Take no action */
    106 char    hostname[MAXHOSTNAMELEN + 1];	/* Hostname, stripped of domain */
    107 
    108 int	main(int, char **);
    109 int	parse(struct conf_entry *, FILE *, size_t *);
    110 
    111 void	log_create(struct conf_entry *);
    112 void	log_examine(struct conf_entry *, int);
    113 void	log_trim(struct conf_entry *);
    114 void	log_trimmed(struct conf_entry *);
    115 
    116 int	getsig(const char *);
    117 int	isnumber(const char *);
    118 int	parseuserspec(const char *, struct passwd **, struct group **);
    119 pid_t	readpidfile(const char *);
    120 void	usage(void);
    121 
    122 /*
    123  * Program entry point.
    124  */
    125 int
    126 main(int argc, char **argv)
    127 {
    128 	struct conf_entry ent;
    129 	FILE *fd;
    130 	char *p, *cfile;
    131 	int c, force, needroot;
    132 	size_t lineno;
    133 
    134 	force = 0;
    135 	needroot = 1;
    136 	cfile = _PATH_NEWSYSLOGCONF;
    137 
    138 	gethostname(hostname, sizeof(hostname));
    139 	hostname[sizeof(hostname) - 1] = '\0';
    140 
    141 	/* Truncate domain */
    142 	if ((p = strchr(hostname, '.')) != NULL)
    143 		*p = '\0';
    144 
    145 	/* Parse command line options */
    146 	while ((c = getopt(argc, argv, "f:nrvF")) != -1) {
    147 		switch (c) {
    148 		case 'f':
    149 			cfile = optarg;
    150 			break;
    151 		case 'n':
    152 			noaction = 1;
    153 			verbose = 0;
    154 			break;
    155 		case 'r':
    156 			needroot = 0;
    157 			break;
    158 		case 'v':
    159 			verbose = 1;
    160 			break;
    161 		case 'F':
    162 			force = 1;
    163 			break;
    164 		default:
    165 			usage();
    166 			/* NOTREACHED */
    167 		}
    168 	}
    169 
    170 	if (needroot && geteuid() != 0)
    171 		errx(EXIT_FAILURE, "must be run as root");
    172 
    173 	if (strcmp(cfile, "-") == 0)
    174 		fd = stdin;
    175 	else if ((fd = fopen(cfile, "rt")) == NULL)
    176 		err(EXIT_FAILURE, "%s", cfile);
    177 
    178 	for (lineno = 0; !parse(&ent, fd, &lineno);)
    179 		log_examine(&ent, force);
    180 
    181 	if (fd != stdin)
    182 		fclose(fd);
    183 
    184 	exit(EXIT_SUCCESS);
    185 	/* NOTREACHED */
    186 }
    187 
    188 /*
    189  * Parse a single line from the configuration file.
    190  */
    191 int
    192 parse(struct conf_entry *log, FILE *fd, size_t *_lineno)
    193 {
    194 	char *line, *q, **ap, *argv[10];
    195 	struct passwd *pw;
    196 	struct group *gr;
    197 	int nf, lineno, i;
    198 
    199 	if ((line = fparseln(fd, NULL, _lineno, NULL, 0)) == NULL)
    200 		return (-1);
    201 	lineno = (int)*_lineno;
    202 
    203 	for (ap = argv, nf = 0; (*ap = strsep(&line, " \t")) != NULL;)
    204 		if (**ap != '\0') {
    205 			if (++nf == sizeof(argv) / sizeof(argv[0])) {
    206 				warnx("config line %d: too many fields",
    207 				    lineno);
    208 				return (-1);
    209 			}
    210 			ap++;
    211 		}
    212 
    213 	if (nf == 0)
    214 		return (0);
    215 
    216 	if (nf < 6)
    217 		errx(EXIT_FAILURE, "config line %d: too few fields", lineno);
    218 
    219 	ap = argv;
    220 	strlcpy(log->logfile, *ap++, sizeof(log->logfile));
    221 
    222 	if (strchr(*ap, ':') != NULL) {
    223 		if (parseuserspec(*ap++, &pw, &gr)) {
    224 			warnx("config line %d: unknown user/group", lineno);
    225 			return (-1);
    226 		}
    227 		log->uid = pw->pw_uid;
    228 		log->gid = gr->gr_gid;
    229 		if (nf < 7)
    230 			errx(EXIT_FAILURE, "config line %d: too few fields",
    231 			    lineno);
    232 	}
    233 
    234 	if (sscanf(*ap++, "%o", &i) != 1) {
    235 		warnx("config line %d: bad permissions", lineno);
    236 		return (-1);
    237 	}
    238 	log->mode = (mode_t)i;
    239 
    240 	if (sscanf(*ap++, "%d", &log->numhist) != 1) {
    241 		warnx("config line %d: bad log count", lineno);
    242 		return (-1);
    243 	}
    244 
    245 	if (isdigit(**ap))
    246 		log->maxsize = atoi(*ap);
    247 	else if (**ap == '*')
    248 		log->maxsize = (size_t)-1;
    249 	else {
    250 		warnx("config line %d: bad log size", lineno);
    251 		return (-1);
    252 	}
    253 	ap++;
    254 
    255 	if (isdigit(**ap))
    256 		log->maxage = atoi(*ap);
    257 	else if (**ap == '*')
    258 		log->maxage = -1;
    259 	else {
    260 		warnx("config line %d: bad log age", lineno);
    261 		return (-1);
    262 	}
    263 	ap++;
    264 
    265 	log->flags = 0;
    266 	for (q = *ap++; q != NULL && *q != '\0'; q++) {
    267 		switch (tolower(*q)) {
    268 		case 'b':
    269 			log->flags |= CE_BINARY;
    270 			break;
    271 		case 'c':
    272 			log->flags |= CE_CREATE;
    273 			break;
    274 		case 'n':
    275 			log->flags |= CE_NOSIGNAL;
    276 			break;
    277 		case 'z':
    278 			log->flags |= CE_COMPACT;
    279 			break;
    280 		case '-':
    281 			break;
    282 		default:
    283 			warnx("config line %d: bad flags", lineno);
    284 			return (-1);
    285 		}
    286 	}
    287 
    288 	if (*ap != NULL && **ap == '/')
    289 		strlcpy(log->pidfile, *ap++, sizeof(log->pidfile));
    290 	else
    291 		log->pidfile[0] = '\0';
    292 
    293 	if (*ap != NULL && (log->signum = getsig(*ap++)) < 0) {
    294 		warnx("config line %d: bad signal type", lineno);
    295 		return (-1);
    296 	} else
    297 		log->signum = SIGHUP;
    298 
    299 	return (0);
    300 }
    301 
    302 /*
    303  * Examine a log file.  If the trim conditions are met, call log_trim() to
    304  * trim the log file.
    305  */
    306 void
    307 log_examine(struct conf_entry *ent, int force)
    308 {
    309 	struct stat sb;
    310 	size_t size;
    311 	int age;
    312 	char tmp[MAXPATHLEN];
    313 	time_t now;
    314 
    315 	if (ent->logfile[0] == '\0')
    316 		return;
    317 
    318 	PRHDRINFO(("\n%s <%d%s>: ", ent->logfile, ent->numhist,
    319 	    (ent->flags & CE_COMPACT) != 0 ? "Z" : ""));
    320 
    321 	if (stat(ent->logfile, &sb) < 0) {
    322 		if (errno == ENOENT && (ent->flags & CE_CREATE) != 0) {
    323 			PRHDRINFO(("creating; "));
    324 			if (!noaction)
    325 				log_create(ent);
    326 			else {
    327 				PRHDRINFO(("can't proceed with `-n'\n"));
    328 				return;
    329 			}
    330 			if (stat(ent->logfile, &sb))
    331 				err(EXIT_FAILURE, "%s", ent->logfile);
    332 		} else if (errno == ENOENT) {
    333 			PRHDRINFO(("does not exist --> skip log\n"));
    334 			return;
    335 		} else if (errno != 0)
    336 			err(EXIT_FAILURE, "%s", ent->logfile);
    337 	}
    338 
    339 
    340 	size = ((size_t)sb.st_blocks * S_BLKSIZE) >> 10;
    341 
    342 	now = time(NULL);
    343 	strlcpy(tmp, ent->logfile, sizeof(tmp));
    344 	strlcat(tmp, ".0", sizeof(tmp));
    345 	if (stat(tmp, &sb) < 0) {
    346 		strlcat(tmp, ".gz", sizeof(tmp));
    347 		if (stat(tmp, &sb) < 0)
    348 			age = -1;
    349 		else
    350 			age = (int)(now - sb.st_mtime + 1800) / 3600;
    351 	} else
    352 		age = (int)(now - sb.st_mtime + 1800) / 3600;
    353 
    354 	if (verbose) {
    355 		if (ent->maxsize != (size_t)-1)
    356 			PRHDRINFO(("size (Kb): %lu [%lu] ",
    357 				(u_long)size,
    358 				(u_long)ent->maxsize));
    359 		if (ent->maxage > 0)
    360 			PRHDRINFO(("age (hr): %d [%d] ", age, ent->maxage));
    361 	}
    362 
    363 	/*
    364 	 * Note: if maxage is used as a trim condition, we need at least one
    365 	 * historical log file to determine the `age' of the active log file.
    366 	 */
    367 	if ((ent->maxage > 0 && (age >= ent->maxage || age < 0)) ||
    368 	    size >= ent->maxsize || force) {
    369 		PRHDRINFO(("--> trim log\n"));
    370 		log_trim(ent);
    371 	} else
    372 		PRHDRINFO(("--> skip log\n"));
    373 }
    374 
    375 /*
    376  * Trim the specified log file.
    377  */
    378 void
    379 log_trim(struct conf_entry *log)
    380 {
    381 	char file1[MAXPATHLEN], file2[MAXPATHLEN];
    382  	char zfile1[MAXPATHLEN], zfile2[MAXPATHLEN];
    383 	int i;
    384 	struct stat st;
    385 	pid_t pid;
    386 
    387 	/* Remove oldest log */
    388 	snprintf(file1, sizeof(file1), "%s.%d", log->logfile,
    389 	    log->numhist - 1);
    390 	strcpy(zfile1, file1);
    391 	strlcat(zfile1, ".gz", sizeof(zfile1));
    392 
    393 	PRINFO(("rm -f %s\n", file1));
    394 	if (!noaction)
    395 		unlink(file1);
    396 	PRINFO(("rm -f %s\n", zfile1));
    397 	if (!noaction)
    398 		unlink(zfile1);
    399 
    400 	/* Move down log files */
    401 	for (i = log->numhist - 1; i != 0; i--) {
    402 		strcpy(file2, file1);
    403 		snprintf(file1, sizeof(file1), "%s.%d", log->logfile, i - 1);
    404 		strcpy(zfile1, file1);
    405 		strcpy(zfile2, file2);
    406 
    407 		if (lstat(file1, &st) != 0) {
    408 			strlcat(zfile1, ".gz", sizeof(zfile1));
    409 			strlcat(zfile2, ".gz", sizeof(zfile2));
    410 			if (lstat(zfile1, &st) != 0)
    411 				continue;
    412 		}
    413 
    414 		PRINFO(("mv %s %s\n", zfile1, zfile2));
    415 		if (!noaction)
    416 			if (rename(zfile1, zfile2))
    417 				err(EXIT_FAILURE, "%s", zfile1);
    418 		PRINFO(("chmod %o %s\n", log->mode, zfile2));
    419 		if (!noaction)
    420 			if (chmod(zfile2, log->mode))
    421 				err(EXIT_FAILURE, "%s", zfile2);
    422 		PRINFO(("chown %d:%d %s\n", log->uid, log->gid, zfile2));
    423 		if (!noaction)
    424 			if (chown(zfile2, log->uid, log->gid))
    425 				err(EXIT_FAILURE, "%s", zfile2);
    426 	}
    427 
    428 	log_trimmed(log);
    429 
    430 	if (log->numhist == 0) {
    431 		PRINFO(("rm -f %s\n", log->logfile));
    432 		if (!noaction)
    433 			if (unlink(log->logfile))
    434 				err(EXIT_FAILURE, "%s", log->logfile);
    435 	} else {
    436 		PRINFO(("mv %s %s\n", log->logfile, file1));
    437 		if (!noaction)
    438 			if (rename(log->logfile, file1))
    439 				err(EXIT_FAILURE, "%s", log->logfile);
    440 	}
    441 
    442 	log_create(log);
    443 	log_trimmed(log);
    444 
    445 	PRINFO(("chmod %o %s\n", log->mode, log->logfile));
    446 	if (!noaction)
    447 		if (chmod(log->logfile, log->mode))
    448 			err(EXIT_FAILURE, "%s", log->logfile);
    449 
    450 	if ((log->flags & CE_NOSIGNAL) == 0) {
    451 		if (log->pidfile[0] != '\0')
    452 			pid = readpidfile(log->pidfile);
    453 		else
    454 			pid = readpidfile(_PATH_SYSLOGDPID);
    455 
    456 		if (pid != (pid_t)-1) {
    457 			PRINFO(("kill -%s %d\n", sys_signame[log->signum],
    458 			    pid));
    459 			if (!noaction)
    460 				if (kill(pid, log->signum))
    461 					warn("kill");
    462 		}
    463 	}
    464 
    465 	if ((log->flags & CE_COMPACT) != 0) {
    466 		PRINFO(("gzip %s.0\n", log->logfile));
    467 		if (!noaction) {
    468 			if ((pid = fork()) < 0)
    469 				err(EXIT_FAILURE, "fork");
    470 			else if (pid == 0) {
    471 				snprintf(file1, sizeof(file1), "%s.0",
    472 				    log->logfile);
    473 				execl(_PATH_GZIP, "gzip", "-f", file1, NULL);
    474 				err(EXIT_FAILURE, _PATH_GZIP);
    475 			}
    476 		}
    477 	}
    478 }
    479 
    480 /*
    481  * Write an entry to the log file recording the fact that it was trimmed.
    482  */
    483 void
    484 log_trimmed(struct conf_entry *log)
    485 {
    486 	FILE *fd;
    487 	time_t now;
    488 	char *daytime;
    489 
    490 	if ((log->flags & CE_BINARY) != 0)
    491 		return;
    492 	PRINFO(("(append rotation notice to %s)\n", log->logfile));
    493 	if (noaction)
    494 		return;
    495 
    496 	if ((fd = fopen(log->logfile, "at")) == NULL)
    497 		err(EXIT_FAILURE, "%s", log->logfile);
    498 
    499 	now = time(NULL);
    500 	daytime = ctime(&now) + 4;
    501 	daytime[15] = '\0';
    502 
    503 	fprintf(fd, "%s %s newsyslog[%ld]: log file turned over\n", daytime,
    504 	    hostname, (long)getpid());
    505 	fclose(fd);
    506 }
    507 
    508 /*
    509  * Create a new log file.  This routine does not obey `noaction'.
    510  */
    511 void
    512 log_create(struct conf_entry *ent)
    513 {
    514 	int fd;
    515 
    516 	PRINFO(("(create new log)\n"));
    517 	if (noaction)
    518 		return;
    519 
    520 	if ((fd = creat(ent->logfile, ent->mode)) < 0)
    521 		err(EXIT_FAILURE, "%s", ent->logfile);
    522 	if (fchown(fd, ent->uid, ent->gid) < 0)
    523 		err(EXIT_FAILURE, "%s", ent->logfile);
    524 	close(fd);
    525 }
    526 
    527 /*
    528  * Display program usage information.
    529  */
    530 void
    531 usage(void)
    532 {
    533 
    534 	fprintf(stderr, "usage: newsyslog [-Frv] [-f config-file]\n");
    535 	exit(EXIT_FAILURE);
    536 }
    537 
    538 /*
    539  * Return non-zero if a string represents a decimal value.
    540  */
    541 int
    542 isnumber(const char *string)
    543 {
    544 
    545 	while (isdigit(*string))
    546 		string++;
    547 
    548 	return (*string == '\0');
    549 }
    550 
    551 /*
    552  * Given a signal name, attempt to find the corresponding signal number.
    553  */
    554 int
    555 getsig(const char *sig)
    556 {
    557 	char *p;
    558 	int n;
    559 
    560 	if (isnumber(sig)) {
    561 		n = (int)strtol(sig, &p, 0);
    562 		if (p != '\0' || n < 0 || n >= NSIG)
    563 			return (-1);
    564 		return (n);
    565 	}
    566 
    567 	if (strncasecmp(sig, "SIG", 3) == 0)
    568 		sig += 3;
    569 	for (n = 1; n < NSIG; n++)
    570 		if (strcasecmp(sys_signame[n], sig) == 0)
    571 			return (n);
    572 	return (-1);
    573 }
    574 
    575 /*
    576  * Given a path to a PID file, return the PID contained within.
    577  */
    578 pid_t
    579 readpidfile(const char *file)
    580 {
    581 	FILE *fd;
    582 	char line[BUFSIZ];
    583 	pid_t pid;
    584 
    585 #ifdef notyet
    586 	if (file[0] != '/')
    587 		snprintf(tmp, sizeof(tmp), "%s%s", _PATH_VARRUN, file);
    588 	else
    589 		strlcpy(tmp, file, sizeof(tmp));
    590 #endif
    591 
    592 	if ((fd = fopen(file, "rt")) == NULL) {
    593 		warn("%s", file);
    594 		return (-1);
    595 	}
    596 
    597 	if (fgets(line, sizeof(line) - 1, fd) != NULL) {
    598 		line[sizeof(line) - 1] = '\0';
    599 		pid = (pid_t)strtol(line, NULL, 0);
    600 	}
    601 
    602 	fclose(fd);
    603 	return (pid);
    604 }
    605 
    606 /*
    607  * Parse a user:group specification.
    608  *
    609  * XXX This is over the top for newsyslog(1).  It should be moved to libutil.
    610  */
    611 int
    612 parseuserspec(const char *name, struct passwd **pw, struct group **gr)
    613 {
    614 	char buf[MAXLOGNAME * 2 + 2], *group;
    615 
    616 	strlcpy(buf, name, sizeof(buf));
    617 	*gr = NULL;
    618 
    619 	/*
    620 	 * Before attempting to use '.' as a separator, see if the whole
    621 	 * string resolves as a user name.
    622 	 */
    623 	if ((*pw = getpwnam(buf)) != NULL) {
    624 		*gr = getgrgid((*pw)->pw_gid);
    625 		return (0);
    626 	}
    627 
    628 	/* Split the user and group name. */
    629 	if ((group = strchr(buf, ':')) != NULL ||
    630 	    (group = strchr(buf, '.')) != NULL)
    631 		*group++ = '\0';
    632 
    633 	if (isnumber(buf))
    634 		*pw = getpwuid((uid_t)atoi(buf));
    635 	else
    636 		*pw = getpwnam(buf);
    637 
    638 	/*
    639 	 * Find the group.  If a group wasn't specified, use the user's
    640 	 * `natural' group.  We get to this point even if no user was found.
    641 	 * This is to allow the caller to get a better idea of what went
    642 	 * wrong, if anything.
    643 	 */
    644 	if (group == NULL || *group == '\0') {
    645 		if (*pw == NULL)
    646 			return (-1);
    647 		*gr = getgrgid((*pw)->pw_gid);
    648 	} else if (isnumber(group))
    649 		*gr = getgrgid((gid_t)atoi(group));
    650 	else
    651 		*gr = getgrnam(group);
    652 
    653 	return (*pw != NULL && *gr != NULL ? 0 : -1);
    654 }
    655