newsyslog.c revision 59003
1/*
2 * This file contains changes from the Open Software Foundation.
3 */
4
5/*
6
7Copyright 1988, 1989 by the Massachusetts Institute of Technology
8
9Permission to use, copy, modify, and distribute this software
10and its documentation for any purpose and without fee is
11hereby granted, provided that the above copyright notice
12appear in all copies and that both that copyright notice and
13this permission notice appear in supporting documentation,
14and that the names of M.I.T. and the M.I.T. S.I.P.B. not be
15used in advertising or publicity pertaining to distribution
16of the software without specific, written prior permission.
17M.I.T. and the M.I.T. S.I.P.B. make no representations about
18the suitability of this software for any purpose.  It is
19provided "as is" without express or implied warranty.
20
21*/
22
23/*
24 *      newsyslog - roll over selected logs at the appropriate time,
25 *              keeping the a specified number of backup files around.
26 */
27
28#ifndef lint
29static const char rcsid[] =
30"$FreeBSD: head/usr.sbin/newsyslog/newsyslog.c 59003 2000-04-04 08:38:30Z hm $";
31
32#endif				/* not lint */
33
34#define OSF
35#ifndef COMPRESS_POSTFIX
36#define COMPRESS_POSTFIX ".gz"
37#endif
38
39#include <ctype.h>
40#include <err.h>
41#include <fcntl.h>
42#include <grp.h>
43#include <paths.h>
44#include <pwd.h>
45#include <signal.h>
46#include <stdio.h>
47#include <stdlib.h>
48#include <string.h>
49#include <time.h>
50#include <unistd.h>
51#include <sys/types.h>
52#include <sys/stat.h>
53#include <sys/param.h>
54#include <sys/wait.h>
55
56#include "pathnames.h"
57
58#define kbytes(size)  (((size) + 1023) >> 10)
59#ifdef _IBMR2
60/* Calculates (db * DEV_BSIZE) */
61#define dbtob(db)  ((unsigned)(db) << UBSHIFT)
62#endif
63
64#define CE_COMPACT 1		/* Compact the achived log files */
65#define CE_BINARY  2		/* Logfile is in binary, don't add */
66 /* status messages */
67#define	CE_TRIMAT  4		/* trim at a specific time */
68
69#define NONE -1
70
71struct conf_entry {
72	char *log;		/* Name of the log */
73	char *pid_file;		/* PID file */
74	int uid;		/* Owner of log */
75	int gid;		/* Group of log */
76	int numlogs;		/* Number of logs to keep */
77	int size;		/* Size cutoff to trigger trimming the log */
78	int hours;		/* Hours between log trimming */
79	time_t trim_at;		/* Specific time to do trimming */
80	int permissions;	/* File permissions on the log */
81	int flags;		/* Flags (CE_COMPACT & CE_BINARY)  */
82	int sig;		/* Signal to send */
83	struct conf_entry *next;/* Linked list pointer */
84};
85
86int verbose = 0;		/* Print out what's going on */
87int needroot = 1;		/* Root privs are necessary */
88int noaction = 0;		/* Don't do anything, just show it */
89int force = 0;			/* Force the trim no matter what */
90char *conf = _PATH_CONF;	/* Configuration file to use */
91time_t timenow;
92
93#define MIN_PID         5
94#define MAX_PID		99999	/* was lower, see /usr/include/sys/proc.h */
95char hostname[MAXHOSTNAMELEN + 1];	/* hostname */
96char *daytime;			/* timenow in human readable form */
97
98static struct conf_entry *parse_file();
99static char *sob(char *p);
100static char *son(char *p);
101static char *missing_field(char *p, char *errline);
102static void do_entry(struct conf_entry * ent);
103static void PRS(int argc, char **argv);
104static void usage();
105static void dotrim(char *log, char *pid_file, int numdays, int falgs, int perm, int owner_uid, int group_gid, int sig);
106static int log_trim(char *log);
107static void compress_log(char *log);
108static int sizefile(char *file);
109static int age_old_log(char *file);
110static pid_t get_pid(char *pid_file);
111static time_t parse8601(const char *s);
112
113int
114main(argc, argv)
115	int argc;
116	char **argv;
117{
118	struct conf_entry *p, *q;
119
120	PRS(argc, argv);
121	if (needroot && getuid() && geteuid())
122		errx(1, "must have root privs");
123	p = q = parse_file();
124
125	while (p) {
126		do_entry(p);
127		p = p->next;
128		free((char *) q);
129		q = p;
130	}
131	return (0);
132}
133
134static void
135do_entry(ent)
136	struct conf_entry *ent;
137
138{
139	int size, modtime;
140	char *pid_file;
141
142	if (verbose) {
143		if (ent->flags & CE_COMPACT)
144			printf("%s <%dZ>: ", ent->log, ent->numlogs);
145		else
146			printf("%s <%d>: ", ent->log, ent->numlogs);
147	}
148	size = sizefile(ent->log);
149	modtime = age_old_log(ent->log);
150	if (size < 0) {
151		if (verbose)
152			printf("does not exist.\n");
153	} else {
154		if (ent->flags & CE_TRIMAT) {
155			if (timenow < ent->trim_at
156			    || difftime(timenow, ent->trim_at) >= 60 * 60) {
157				if (verbose)
158					printf("--> will trim at %s",
159					    ctime(&ent->trim_at));
160				return;
161			} else if (verbose && ent->hours <= 0) {
162				printf("--> time is up\n");
163			}
164		}
165		if (verbose && (ent->size > 0))
166			printf("size (Kb): %d [%d] ", size, ent->size);
167		if (verbose && (ent->hours > 0))
168			printf(" age (hr): %d [%d] ", modtime, ent->hours);
169		if (force || ((ent->size > 0) && (size >= ent->size)) ||
170		    (ent->hours <= 0 && (ent->flags & CE_TRIMAT)) ||
171		    ((ent->hours > 0) && ((modtime >= ent->hours)
172			    || (modtime < 0)))) {
173			if (verbose)
174				printf("--> trimming log....\n");
175			if (noaction && !verbose) {
176				if (ent->flags & CE_COMPACT)
177					printf("%s <%dZ>: trimming\n",
178					    ent->log, ent->numlogs);
179				else
180					printf("%s <%d>: trimming\n",
181					    ent->log, ent->numlogs);
182			}
183			if (ent->pid_file) {
184				pid_file = ent->pid_file;
185			} else {
186				/* Only try to notify syslog if we are root */
187				if (needroot)
188					pid_file = _PATH_SYSLOGPID;
189				else
190					pid_file = NULL;
191			}
192			dotrim(ent->log, pid_file, ent->numlogs,
193			    ent->flags, ent->permissions, ent->uid, ent->gid, ent->sig);
194		} else {
195			if (verbose)
196				printf("--> skipping\n");
197		}
198	}
199}
200
201static void
202PRS(argc, argv)
203	int argc;
204	char **argv;
205{
206	int c;
207	char *p;
208
209	timenow = time((time_t *) 0);
210	daytime = ctime(&timenow) + 4;
211	daytime[15] = '\0';
212
213	/* Let's get our hostname */
214	(void) gethostname(hostname, sizeof(hostname));
215
216	/* Truncate domain */
217	if ((p = strchr(hostname, '.'))) {
218		*p = '\0';
219	}
220	optind = 1;		/* Start options parsing */
221	while ((c = getopt(argc, argv, "nrvFf:t:")) != -1)
222		switch (c) {
223		case 'n':
224			noaction++;
225			break;
226		case 'r':
227			needroot = 0;
228			break;
229		case 'v':
230			verbose++;
231			break;
232		case 'f':
233			conf = optarg;
234			break;
235		case 'F':
236			force++;
237			break;
238		default:
239			usage();
240		}
241}
242
243static void
244usage()
245{
246	fprintf(stderr, "usage: newsyslog [-Fnrv] [-f config-file]\n");
247	exit(1);
248}
249
250/* Parse a configuration file and return a linked list of all the logs
251 * to process
252 */
253static struct conf_entry *
254parse_file()
255{
256	FILE *f;
257	char line[BUFSIZ], *parse, *q;
258	char *errline, *group;
259	struct conf_entry *first = NULL;
260	struct conf_entry *working = NULL;
261	struct passwd *pass;
262	struct group *grp;
263	int eol;
264
265	if (strcmp(conf, "-"))
266		f = fopen(conf, "r");
267	else
268		f = stdin;
269	if (!f)
270		err(1, "%s", conf);
271	while (fgets(line, BUFSIZ, f)) {
272		if ((line[0] == '\n') || (line[0] == '#'))
273			continue;
274		errline = strdup(line);
275		if (!first) {
276			working = (struct conf_entry *) malloc(sizeof(struct conf_entry));
277			first = working;
278		} else {
279			working->next = (struct conf_entry *) malloc(sizeof(struct conf_entry));
280			working = working->next;
281		}
282
283		q = parse = missing_field(sob(line), errline);
284		parse = son(line);
285		if (!*parse)
286			errx(1, "malformed line (missing fields):\n%s", errline);
287		*parse = '\0';
288		working->log = strdup(q);
289
290		q = parse = missing_field(sob(++parse), errline);
291		parse = son(parse);
292		if (!*parse)
293			errx(1, "malformed line (missing fields):\n%s", errline);
294		*parse = '\0';
295		if ((group = strchr(q, ':')) != NULL ||
296		    (group = strrchr(q, '.')) != NULL) {
297			*group++ = '\0';
298			if (*q) {
299				if (!(isnumber(*q))) {
300					if ((pass = getpwnam(q)) == NULL)
301						errx(1,
302						    "error in config file; unknown user:\n%s",
303						    errline);
304					working->uid = pass->pw_uid;
305				} else
306					working->uid = atoi(q);
307			} else
308				working->uid = NONE;
309
310			q = group;
311			if (*q) {
312				if (!(isnumber(*q))) {
313					if ((grp = getgrnam(q)) == NULL)
314						errx(1,
315						    "error in config file; unknown group:\n%s",
316						    errline);
317					working->gid = grp->gr_gid;
318				} else
319					working->gid = atoi(q);
320			} else
321				working->gid = NONE;
322
323			q = parse = missing_field(sob(++parse), errline);
324			parse = son(parse);
325			if (!*parse)
326				errx(1, "malformed line (missing fields):\n%s", errline);
327			*parse = '\0';
328		} else
329			working->uid = working->gid = NONE;
330
331		if (!sscanf(q, "%o", &working->permissions))
332			errx(1, "error in config file; bad permissions:\n%s",
333			    errline);
334
335		q = parse = missing_field(sob(++parse), errline);
336		parse = son(parse);
337		if (!*parse)
338			errx(1, "malformed line (missing fields):\n%s", errline);
339		*parse = '\0';
340		if (!sscanf(q, "%d", &working->numlogs))
341			errx(1, "error in config file; bad number:\n%s",
342			    errline);
343
344		q = parse = missing_field(sob(++parse), errline);
345		parse = son(parse);
346		if (!*parse)
347			errx(1, "malformed line (missing fields):\n%s", errline);
348		*parse = '\0';
349		if (isdigit(*q))
350			working->size = atoi(q);
351		else
352			working->size = -1;
353
354		working->flags = 0;
355		q = parse = missing_field(sob(++parse), errline);
356		parse = son(parse);
357		eol = !*parse;
358		*parse = '\0';
359		{
360			char *ep;
361			u_long ul;
362
363			ul = strtoul(q, &ep, 10);
364			if (ep == q)
365				working->hours = 0;
366			else if (*ep == '*')
367				working->hours = -1;
368			else if (ul > INT_MAX)
369				errx(1, "interval is too large:\n%s", errline);
370			else
371				working->hours = ul;
372
373			if (*ep != '\0' && *ep != '@' && *ep != '*')
374				errx(1, "malformed interval/at:\n%s", errline);
375			if (*ep == '@') {
376				if ((working->trim_at = parse8601(ep + 1))
377				    == (time_t) - 1)
378					errx(1, "malformed at:\n%s", errline);
379				working->flags |= CE_TRIMAT;
380			}
381		}
382
383		if (eol)
384			q = NULL;
385		else {
386			q = parse = sob(++parse);	/* Optional field */
387			parse = son(parse);
388			if (!*parse)
389				eol = 1;
390			*parse = '\0';
391		}
392
393		while (q && *q && !isspace(*q)) {
394			if ((*q == 'Z') || (*q == 'z'))
395				working->flags |= CE_COMPACT;
396			else if ((*q == 'B') || (*q == 'b'))
397				working->flags |= CE_BINARY;
398			else if (*q != '-')
399				errx(1, "illegal flag in config file -- %c", *q);
400			q++;
401		}
402
403		if (eol)
404			q = NULL;
405		else {
406			q = parse = sob(++parse);	/* Optional field */
407			parse = son(parse);
408			if (!*parse)
409				eol = 1;
410			*parse = '\0';
411		}
412
413		working->pid_file = NULL;
414		if (q && *q) {
415			if (*q == '/')
416				working->pid_file = strdup(q);
417			else if (isdigit(*q))
418				goto got_sig;
419			else
420				errx(1, "illegal pid file or signal number in config file:\n%s", errline);
421		}
422		if (eol)
423			q = NULL;
424		else {
425			q = parse = sob(++parse);	/* Optional field */
426			*(parse = son(parse)) = '\0';
427		}
428
429		working->sig = SIGHUP;
430		if (q && *q) {
431			if (isdigit(*q)) {
432		got_sig:
433				working->sig = atoi(q);
434			} else {
435		err_sig:
436				errx(1, "illegal signal number in config file:\n%s", errline);
437			}
438			if (working->sig < 1 || working->sig >= NSIG)
439				goto err_sig;
440		}
441		free(errline);
442	}
443	if (working)
444		working->next = (struct conf_entry *) NULL;
445	(void) fclose(f);
446	return (first);
447}
448
449static char *
450missing_field(p, errline)
451	char *p, *errline;
452{
453	if (!p || !*p)
454		errx(1, "missing field in config file:\n%s", errline);
455	return (p);
456}
457
458static void
459dotrim(log, pid_file, numdays, flags, perm, owner_uid, group_gid, sig)
460	char *log;
461	char *pid_file;
462	int numdays;
463	int flags;
464	int perm;
465	int owner_uid;
466	int group_gid;
467	int sig;
468{
469	char file1[MAXPATHLEN + 1], file2[MAXPATHLEN + 1];
470	char zfile1[MAXPATHLEN + 1], zfile2[MAXPATHLEN + 1];
471	int notified, need_notification, fd, _numdays;
472	struct stat st;
473	pid_t pid;
474
475#ifdef _IBMR2
476/* AIX 3.1 has a broken fchown- if the owner_uid is -1, it will actually */
477/* change it to be owned by uid -1, instead of leaving it as is, as it is */
478/* supposed to. */
479	if (owner_uid == -1)
480		owner_uid = geteuid();
481#endif
482
483	/* Remove oldest log */
484	(void) sprintf(file1, "%s.%d", log, numdays);
485	(void) strcpy(zfile1, file1);
486	(void) strcat(zfile1, COMPRESS_POSTFIX);
487
488	if (noaction) {
489		printf("rm -f %s\n", file1);
490		printf("rm -f %s\n", zfile1);
491	} else {
492		(void) unlink(file1);
493		(void) unlink(zfile1);
494	}
495
496	/* Move down log files */
497	_numdays = numdays;	/* preserve */
498	while (numdays--) {
499		(void) strcpy(file2, file1);
500		(void) sprintf(file1, "%s.%d", log, numdays);
501		(void) strcpy(zfile1, file1);
502		(void) strcpy(zfile2, file2);
503		if (lstat(file1, &st)) {
504			(void) strcat(zfile1, COMPRESS_POSTFIX);
505			(void) strcat(zfile2, COMPRESS_POSTFIX);
506			if (lstat(zfile1, &st))
507				continue;
508		}
509		if (noaction) {
510			printf("mv %s %s\n", zfile1, zfile2);
511			printf("chmod %o %s\n", perm, zfile2);
512			printf("chown %d.%d %s\n",
513			    owner_uid, group_gid, zfile2);
514		} else {
515			(void) rename(zfile1, zfile2);
516			(void) chmod(zfile2, perm);
517			(void) chown(zfile2, owner_uid, group_gid);
518		}
519	}
520	if (!noaction && !(flags & CE_BINARY))
521		(void) log_trim(log);	/* Report the trimming to the old log */
522
523	if (!_numdays) {
524		if (noaction)
525			printf("rm %s\n", log);
526		else
527			(void) unlink(log);
528	} else {
529		if (noaction)
530			printf("mv %s to %s\n", log, file1);
531		else
532			(void) rename(log, file1);
533	}
534
535	if (noaction)
536		printf("Start new log...");
537	else {
538		fd = creat(log, perm);
539		if (fd < 0)
540			err(1, "can't start new log");
541		if (fchown(fd, owner_uid, group_gid))
542			err(1, "can't chmod new log file");
543		(void) close(fd);
544		if (!(flags & CE_BINARY))
545			if (log_trim(log))	/* Add status message */
546				err(1, "can't add status message to log");
547	}
548	if (noaction)
549		printf("chmod %o %s...\n", perm, log);
550	else
551		(void) chmod(log, perm);
552
553	pid = 0;
554	need_notification = notified = 0;
555	if (pid_file != NULL) {
556		need_notification = 1;
557		pid = get_pid(pid_file);
558	}
559	if (pid) {
560		if (noaction) {
561			notified = 1;
562			printf("kill -%d %d\n", sig, (int) pid);
563		} else if (kill(pid, sig))
564			warn("can't notify daemon, pid %d", (int) pid);
565		else {
566			notified = 1;
567			if (verbose)
568				printf("daemon pid %d notified\n", (int) pid);
569		}
570	}
571	if ((flags & CE_COMPACT)) {
572		if (need_notification && !notified)
573			warnx("log not compressed because daemon not notified");
574		else if (noaction)
575			printf("Compress %s.0\n", log);
576		else {
577			if (notified) {
578				if (verbose)
579					printf("small pause to allow daemon to close log\n");
580				sleep(10);
581			}
582			compress_log(log);
583		}
584	}
585}
586
587/* Log the fact that the logs were turned over */
588static int
589log_trim(log)
590	char *log;
591{
592	FILE *f;
593
594	if ((f = fopen(log, "a")) == NULL)
595		return (-1);
596	fprintf(f, "%s %s newsyslog[%d]: logfile turned over\n",
597	    daytime, hostname, (int) getpid());
598	if (fclose(f) == EOF)
599		err(1, "log_trim: fclose:");
600	return (0);
601}
602
603/* Fork of /usr/ucb/compress to compress the old log file */
604static void
605compress_log(log)
606	char *log;
607{
608	pid_t pid;
609	char tmp[MAXPATHLEN + 1];
610
611	(void) sprintf(tmp, "%s.0", log);
612	pid = fork();
613	if (pid < 0)
614		err(1, "fork");
615	else if (!pid) {
616		(void) execl(_PATH_GZIP, _PATH_GZIP, "-f", tmp, 0);
617		err(1, _PATH_GZIP);
618	}
619}
620
621/* Return size in kilobytes of a file */
622static int
623sizefile(file)
624	char *file;
625{
626	struct stat sb;
627
628	if (stat(file, &sb) < 0)
629		return (-1);
630	return (kbytes(dbtob(sb.st_blocks)));
631}
632
633/* Return the age of old log file (file.0) */
634static int
635age_old_log(file)
636	char *file;
637{
638	struct stat sb;
639	char tmp[MAXPATHLEN + sizeof(".0") + sizeof(COMPRESS_POSTFIX) + 1];
640
641	(void) strcpy(tmp, file);
642	if (stat(strcat(tmp, ".0"), &sb) < 0)
643		if (stat(strcat(tmp, COMPRESS_POSTFIX), &sb) < 0)
644			return (-1);
645	return ((int) (timenow - sb.st_mtime + 1800) / 3600);
646}
647
648static pid_t
649get_pid(pid_file)
650	char *pid_file;
651{
652	FILE *f;
653	char line[BUFSIZ];
654	pid_t pid = 0;
655
656	if ((f = fopen(pid_file, "r")) == NULL)
657		warn("can't open %s pid file to restart a daemon",
658		    pid_file);
659	else {
660		if (fgets(line, BUFSIZ, f)) {
661			pid = atol(line);
662			if (pid < MIN_PID || pid > MAX_PID) {
663				warnx("preposterous process number: %d", (int) pid);
664				pid = 0;
665			}
666		} else
667			warn("can't read %s pid file to restart a daemon",
668			    pid_file);
669		(void) fclose(f);
670	}
671	return pid;
672}
673
674/* Skip Over Blanks */
675char *
676sob(p)
677	register char *p;
678{
679	while (p && *p && isspace(*p))
680		p++;
681	return (p);
682}
683
684/* Skip Over Non-Blanks */
685char *
686son(p)
687	register char *p;
688{
689	while (p && *p && !isspace(*p))
690		p++;
691	return (p);
692}
693
694/*
695 * Parse a limited subset of ISO 8601.
696 * The specific format is as follows:
697 *
698 *	[CC[YY[MM[DD]]]][THH[MM[SS]]]	(where `T' is the literal letter)
699 *
700 * We don't accept a timezone specification; missing fields (including
701 * timezone) are defaulted to the current date but time zero.
702 */
703static time_t
704parse8601(const char *s)
705{
706	char *t;
707	struct tm tm, *tmp;
708	u_long ul;
709
710	tmp = localtime(&timenow);
711	tm = *tmp;
712
713	tm.tm_hour = tm.tm_min = tm.tm_sec = 0;
714
715	ul = strtoul(s, &t, 10);
716	if (*t != '\0' && *t != 'T')
717		return -1;
718
719	/*
720	 * Now t points either to the end of the string (if no time was
721	 * provided) or to the letter `T' which separates date and time in
722	 * ISO 8601.  The pointer arithmetic is the same for either case.
723	 */
724	switch (t - s) {
725	case 8:
726		tm.tm_year = ((ul / 1000000) - 19) * 100;
727		ul = ul % 1000000;
728	case 6:
729		tm.tm_year = tm.tm_year - (tm.tm_year % 100);
730		tm.tm_year += ul / 10000;
731		ul = ul % 10000;
732	case 4:
733		tm.tm_mon = (ul / 100) - 1;
734		ul = ul % 100;
735	case 2:
736		tm.tm_mday = ul;
737	case 0:
738		break;
739	default:
740		return -1;
741	}
742
743	/* sanity check */
744	if (tm.tm_year < 70 || tm.tm_mon < 0 || tm.tm_mon > 12
745	    || tm.tm_mday < 1 || tm.tm_mday > 31)
746		return -1;
747
748	if (*t != '\0') {
749		s = ++t;
750		ul = strtoul(s, &t, 10);
751		if (*t != '\0' && !isspace(*t))
752			return -1;
753
754		switch (t - s) {
755		case 6:
756			tm.tm_sec = ul % 100;
757			ul /= 100;
758		case 4:
759			tm.tm_min = ul % 100;
760			ul /= 100;
761		case 2:
762			tm.tm_hour = ul;
763		case 0:
764			break;
765		default:
766			return -1;
767		}
768
769		/* sanity check */
770		if (tm.tm_sec < 0 || tm.tm_sec > 60 || tm.tm_min < 0
771		    || tm.tm_min > 59 || tm.tm_hour < 0 || tm.tm_hour > 23)
772			return -1;
773	}
774	return mktime(&tm);
775}
776