xref: /illumos-gate/usr/src/cmd/logadm/main.c (revision 4de2612967d06c4fdbf524a62556a1e8118a006f)
1 /*
2  * CDDL HEADER START
3  *
4  * The contents of this file are subject to the terms of the
5  * Common Development and Distribution License, Version 1.0 only
6  * (the "License").  You may not use this file except in compliance
7  * with the License.
8  *
9  * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
10  * or http://www.opensolaris.org/os/licensing.
11  * See the License for the specific language governing permissions
12  * and limitations under the License.
13  *
14  * When distributing Covered Code, include this CDDL HEADER in each
15  * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
16  * If applicable, add the following below this CDDL HEADER, with the
17  * fields enclosed by brackets "[]" replaced with your own identifying
18  * information: Portions Copyright [yyyy] [name of copyright owner]
19  *
20  * CDDL HEADER END
21  */
22 /*
23  * Copyright 2004 Sun Microsystems, Inc.  All rights reserved.
24  * Use is subject to license terms.
25  *
26  * logadm/main.c -- main routines for logadm
27  *
28  * this program is 90% argument processing, 10% actions...
29  */
30 
31 #pragma ident	"%Z%%M%	%I%	%E% SMI"
32 
33 #include <stdio.h>
34 #include <stdlib.h>
35 #include <unistd.h>
36 #include <strings.h>
37 #include <libintl.h>
38 #include <locale.h>
39 #include <sys/types.h>
40 #include <sys/stat.h>
41 #include <sys/wait.h>
42 #include <sys/filio.h>
43 #include <time.h>
44 #include "err.h"
45 #include "lut.h"
46 #include "fn.h"
47 #include "opts.h"
48 #include "conf.h"
49 #include "glob.h"
50 #include "kw.h"
51 
52 /* forward declarations for functions in this file */
53 static void usage(const char *msg);
54 static void commajoin(const char *lhs, void *rhs, void *arg);
55 static void doaftercmd(const char *lhs, void *rhs, void *arg);
56 static void dologname(struct fn *fnp, struct opts *clopts);
57 static boolean_t rotatelog(struct fn *fnp, struct opts *opts);
58 static void rotateto(struct fn *fnp, struct opts *opts, int n,
59     struct fn *recentlog, boolean_t isgz);
60 static void expirefiles(struct fn *fnp, struct opts *opts);
61 static void dorm(struct opts *opts, const char *msg, struct fn *fnp);
62 static void docmd(struct opts *opts, const char *msg, const char *cmd,
63     const char *arg1, const char *arg2, const char *arg3);
64 
65 /* our configuration file, unless otherwise specified by -f */
66 static char *Default_conffile = "/etc/logadm.conf";
67 
68 /* default pathnames to the commands we invoke */
69 static char *Sh = "/bin/sh";
70 static char *Mv = "/bin/mv";
71 static char *Cp = "/bin/cp";
72 static char *Rm = "/bin/rm";
73 static char *Touch = "/bin/touch";
74 static char *Chmod = "/bin/chmod";
75 static char *Chown = "/bin/chown";
76 static char *Gzip = "/bin/gzip";
77 static char *Mkdir = "/bin/mkdir";
78 
79 /* return from time(0), gathered early on to avoid slewed timestamps */
80 time_t Now;
81 
82 /* list of before commands that have been executed */
83 static struct lut *Beforecmds;
84 
85 /* list of after commands to execute before exiting */
86 static struct lut *Aftercmds;
87 
88 /* list of conffile entry names that are considered "done" */
89 static struct lut *Donenames;
90 
91 /* table that drives argument parsing */
92 static struct optinfo Opttable[] = {
93 	{ "e", OPTTYPE_STRING,	NULL,			OPTF_CLI|OPTF_CONF },
94 	{ "f", OPTTYPE_STRING,	NULL,			OPTF_CLI },
95 	{ "h", OPTTYPE_BOOLEAN,	NULL,			OPTF_CLI },
96 	{ "N", OPTTYPE_BOOLEAN,	NULL,			OPTF_CLI|OPTF_CONF },
97 	{ "n", OPTTYPE_BOOLEAN,	NULL,			OPTF_CLI },
98 	{ "r", OPTTYPE_BOOLEAN,	NULL,			OPTF_CLI },
99 	{ "V", OPTTYPE_BOOLEAN,	NULL,			OPTF_CLI },
100 	{ "v", OPTTYPE_BOOLEAN,	NULL,			OPTF_CLI },
101 	{ "w", OPTTYPE_STRING,	NULL,			OPTF_CLI },
102 	{ "p", OPTTYPE_INT,	opts_parse_seconds,	OPTF_CLI|OPTF_CONF },
103 	{ "P", OPTTYPE_INT,	opts_parse_ctime,	OPTF_CLI|OPTF_CONF },
104 	{ "s", OPTTYPE_INT,	opts_parse_bytes,	OPTF_CLI|OPTF_CONF },
105 	{ "a", OPTTYPE_STRING,	NULL,			OPTF_CLI|OPTF_CONF },
106 	{ "b", OPTTYPE_STRING,	NULL,			OPTF_CLI|OPTF_CONF },
107 	{ "c", OPTTYPE_BOOLEAN, NULL,			OPTF_CLI|OPTF_CONF },
108 	{ "g", OPTTYPE_STRING,	NULL,			OPTF_CLI|OPTF_CONF },
109 	{ "m", OPTTYPE_INT,	opts_parse_atopi,	OPTF_CLI|OPTF_CONF },
110 	{ "M", OPTTYPE_STRING,	NULL,			OPTF_CLI|OPTF_CONF },
111 	{ "o", OPTTYPE_STRING,	NULL,			OPTF_CLI|OPTF_CONF },
112 	{ "R", OPTTYPE_STRING,	NULL,			OPTF_CLI|OPTF_CONF },
113 	{ "t", OPTTYPE_STRING,	NULL,			OPTF_CLI|OPTF_CONF },
114 	{ "z", OPTTYPE_INT,	opts_parse_atopi,	OPTF_CLI|OPTF_CONF },
115 	{ "A", OPTTYPE_INT,	opts_parse_seconds,	OPTF_CLI|OPTF_CONF },
116 	{ "C", OPTTYPE_INT,	opts_parse_atopi,	OPTF_CLI|OPTF_CONF },
117 	{ "E", OPTTYPE_STRING,	NULL,			OPTF_CLI|OPTF_CONF },
118 	{ "S", OPTTYPE_INT,	opts_parse_bytes,	OPTF_CLI|OPTF_CONF },
119 	{ "T", OPTTYPE_STRING,	NULL,			OPTF_CLI|OPTF_CONF },
120 };
121 
122 /*
123  * only the "fhnVv" options are allowed in the first form of this command,
124  * so this defines the list of options that are an error in they appear
125  * in the first form.  In other words, it is not allowed to run logadm
126  * with any of these options unless at least one logname is also provided.
127  */
128 #define	OPTIONS_NOT_FIRST_FORM	"eNrwpPsabcgmoRtzACEST"
129 
130 /* text that we spew with the -h flag */
131 #define	HELP1 \
132 "Usage: logadm [options]\n"\
133 "       (processes all entries in /etc/logadm.conf or conffile given by -f)\n"\
134 "   or: logadm [options] logname...\n"\
135 "       (processes the given lognames)\n"\
136 "\n"\
137 "General options:\n"\
138 "        -e mailaddr     mail errors to given address\n"\
139 "        -f conffile     use conffile instead of /etc/logadm.conf\n"\
140 "        -h              display help\n"\
141 "        -N              not an error if log file nonexistent\n"\
142 "        -n              show actions, don't perform them\n"\
143 "        -r              remove logname entry from conffile\n"\
144 "        -V              ensure conffile entries exist, correct\n"\
145 "        -v              print info about actions happening\n"\
146 "        -w entryname    write entry to config file\n"\
147 "\n"\
148 "Options which control when a logfile is rotated:\n"\
149 "(default is: -s1b -p1w if no -s or -p)\n"\
150 "        -p period       only rotate if period passed since last rotate\n"\
151 "        -P timestamp    used to store rotation date in conffile\n"\
152 "        -s size         only rotate if given size or greater\n"\
153 "\n"
154 #define	HELP2 \
155 "Options which control how a logfile is rotated:\n"\
156 "(default is: -t '$file.$n', owner/group/mode taken from log file)\n"\
157 "        -a cmd          execute cmd after taking actions\n"\
158 "        -b cmd          execute cmd before taking actions\n"\
159 "        -c              copy & truncate logfile, don't rename\n"\
160 "        -g group        new empty log file group\n"\
161 "        -m mode         new empty log file mode\n"\
162 "        -M cmd          execute cmd to rotate the log file\n"\
163 "        -o owner        new empty log file owner\n"\
164 "        -R cmd          run cmd on file after rotate\n"\
165 "        -t template     template for naming old logs\n"\
166 "        -z count        gzip old logs except most recent count\n"\
167 "\n"\
168 "Options which control the expiration of old logfiles:\n"\
169 "(default is: -C10 if no -A, -C, or -S)\n"\
170 "        -A age          expire logs older than age\n"\
171 "        -C count        expire old logs until count remain\n"\
172 "        -E cmd          run cmd on file to expire\n"\
173 "        -S size         expire until space used is below size \n"\
174 "        -T pattern      pattern for finding old logs\n"
175 
176 /*
177  * main -- where it all begins
178  */
179 /*ARGSUSED*/
180 int
181 main(int argc, char *argv[])
182 {
183 	struct opts *clopts;		/* from parsing command line */
184 	const char *conffile;		/* our configuration file */
185 	struct fn_list *lognames;	/* list of lognames we're processing */
186 	struct fn *fnp;
187 	char *val;
188 
189 	(void) setlocale(LC_ALL, "");
190 
191 #if !defined(TEXT_DOMAIN)
192 #define	TEXT_DOMAIN "SYS_TEST"	/* only used if Makefiles don't define it */
193 #endif
194 
195 	(void) textdomain(TEXT_DOMAIN);
196 
197 	/* we only print times into the conffile, so make them uniform */
198 	(void) setlocale(LC_TIME, "C");
199 
200 	/* give our name to error routines & skip it for arg parsing */
201 	err_init(*argv++);
202 	(void) setlinebuf(stdout);
203 
204 	if (putenv("PATH=/bin"))
205 		err(EF_SYS, "putenv PATH");
206 	if (putenv("TZ=UTC"))
207 		err(EF_SYS, "putenv TZ");
208 	tzset();
209 
210 	(void) umask(0);
211 
212 	Now = time(0);
213 
214 	/* check for (undocumented) debugging environment variables */
215 	if (val = getenv("_LOGADM_DEFAULT_CONFFILE"))
216 		Default_conffile = val;
217 	if (val = getenv("_LOGADM_DEBUG"))
218 		Debug = atoi(val);
219 	if (val = getenv("_LOGADM_SH"))
220 		Sh = val;
221 	if (val = getenv("_LOGADM_MV"))
222 		Mv = val;
223 	if (val = getenv("_LOGADM_CP"))
224 		Cp = val;
225 	if (val = getenv("_LOGADM_RM"))
226 		Rm = val;
227 	if (val = getenv("_LOGADM_TOUCH"))
228 		Touch = val;
229 	if (val = getenv("_LOGADM_CHMOD"))
230 		Chmod = val;
231 	if (val = getenv("_LOGADM_CHOWN"))
232 		Chown = val;
233 	if (val = getenv("_LOGADM_GZIP"))
234 		Gzip = val;
235 	if (val = getenv("_LOGADM_MKDIR"))
236 		Mkdir = val;
237 
238 	opts_init(Opttable, sizeof (Opttable) / sizeof (struct optinfo));
239 
240 	/* parse command line arguments */
241 	if (SETJMP)
242 		usage("bailing out due to command line errors");
243 	else
244 		clopts = opts_parse(argv, OPTF_CLI);
245 
246 	if (Debug) {
247 		(void) fprintf(stderr, "command line opts:");
248 		opts_print(clopts, stderr, NULL);
249 		(void) fprintf(stderr, "\n");
250 	}
251 
252 	/*
253 	 * There are many moods of logadm:
254 	 *
255 	 *	1. "-h" for help was given.  We spew a canned help
256 	 *	   message and exit, regardless of any other options given.
257 	 *
258 	 *	2. "-r" or "-w" asking us to write to the conffile.  Lots
259 	 *	   of argument checking, then we make the change to conffile
260 	 *	   and exit.  (-r processing actually happens in dologname().)
261 	 *
262 	 *	3. "-V" to search/verify the conffile was given.  We do
263 	 *	   the appropriate run through the conffile and exit.
264 	 *	   (-V processing actually happens in dologname().)
265 	 *
266 	 *	4. No lognames were given, so we're being asked to go through
267 	 *	   every entry in conffile.  We verify that only the options
268 	 *	   that make sense for this form of the command are present
269 	 *	   and fall into the main processing loop below.
270 	 *
271 	 *	5. lognames were given, so we fall into the main processing
272 	 *	   loop below to work our way through them.
273 	 *
274 	 * The last two cases are where the option processing gets more
275 	 * complex.  Each time around the main processing loop, we're
276 	 * in one of these cases:
277 	 *
278 	 *	A. No cmdargs were found (we're in case 4), the entry
279 	 *	   in conffile supplies no log file names, so the entry
280 	 *	   name itself is the logfile name (or names, if it globs
281 	 *	   to multiple file names).
282 	 *
283 	 *	B. No cmdargs were found (we're in case 4), the entry
284 	 *	   in conffile gives log file names that we then loop
285 	 *	   through and rotate/expire.  In this case, the entry
286 	 *	   name is specifically NOT one of the log file names.
287 	 *
288 	 *	C. We're going through the cmdargs (we're in case 5),
289 	 *	   the entry in conffile either doesn't exist or it exists
290 	 *	   but supplies no log file names, so the cmdarg itself
291 	 *	   is the log file name.
292 	 *
293 	 *	D. We're going through the cmdargs (we're in case 5),
294 	 *	   a matching entry in conffile supplies log file names
295 	 *	   that we then loop through and rotate/expire.  In this
296 	 *	   case the entry name is specifically NOT one of the log
297 	 *	   file names.
298 	 *
299 	 * As we're doing all this, any options given on the command line
300 	 * override any found in the conffile, and we apply the defaults
301 	 * for rotation conditions and expiration conditions, etc. at the
302 	 * last opportunity, when we're sure they haven't been overridden
303 	 * by an option somewhere along the way.
304 	 *
305 	 */
306 
307 	/* help option overrides anything else */
308 	if (opts_count(clopts, "h")) {
309 		(void) fputs(HELP1, stderr);
310 		(void) fputs(HELP2, stderr);
311 		err_done(0);
312 		/*NOTREACHED*/
313 	}
314 
315 	/* detect illegal option combinations */
316 	if (opts_count(clopts, "rwV") > 1)
317 		usage("Only one of -r, -w, or -V may be used at a time.");
318 	if (opts_count(clopts, "cM") > 1)
319 		usage("Only one of -c or -M may be used at a time.");
320 
321 	/* arrange for error output to be mailed if clopts includes -e */
322 	if (opts_count(clopts, "e"))
323 		err_mailto(opts_optarg(clopts, "e"));
324 
325 	/* this implements the default conffile */
326 	if ((conffile = opts_optarg(clopts, "f")) == NULL)
327 		conffile = Default_conffile;
328 	if (opts_count(clopts, "v"))
329 		(void) out("# loading %s\n", conffile);
330 	conf_open(conffile, opts_count(clopts, "Vn") == 0);
331 
332 	/* handle conffile write option */
333 	if (opts_count(clopts, "w")) {
334 		if (Debug)
335 			(void) fprintf(stderr,
336 			    "main: add/replace conffile entry: <%s>\n",
337 			    opts_optarg(clopts, "w"));
338 		conf_replace(opts_optarg(clopts, "w"), clopts);
339 		conf_close(clopts);
340 		err_done(0);
341 		/*NOTREACHED*/
342 	}
343 
344 	/*
345 	 * lognames is either a list supplied on the command line,
346 	 * or every entry in the conffile if none were supplied.
347 	 */
348 	lognames = opts_cmdargs(clopts);
349 	if (fn_list_empty(lognames)) {
350 		/*
351 		 * being asked to do all entries in conffile
352 		 *
353 		 * check to see if any options were given that only
354 		 * make sense when lognames are given specifically
355 		 * on the command line.
356 		 */
357 		if (opts_count(clopts, OPTIONS_NOT_FIRST_FORM))
358 			usage("some options require logname argument");
359 		if (Debug)
360 			(void) fprintf(stderr,
361 			    "main: run all entries in conffile\n");
362 		lognames = conf_entries();
363 	}
364 
365 	/* foreach logname... */
366 	fn_list_rewind(lognames);
367 	while ((fnp = fn_list_next(lognames)) != NULL) {
368 		if (lut_lookup(Donenames, fn_s(fnp)) != NULL) {
369 			if (Debug)
370 				(void) fprintf(stderr,
371 				    "main: logname already done: <%s>\n",
372 				    fn_s(fnp));
373 			continue;
374 		}
375 		if (SETJMP)
376 			err(EF_FILE, "bailing out on logname \"%s\" "
377 			    "due to errors", fn_s(fnp));
378 		else
379 			dologname(fnp, clopts);
380 	}
381 
382 	/* execute any after commands */
383 	lut_walk(Aftercmds, doaftercmd, clopts);
384 
385 	/* write out any conffile changes */
386 	conf_close(clopts);
387 
388 	err_done(0);
389 	/*NOTREACHED*/
390 	return (0);	/* for lint's little mind */
391 }
392 
393 /* spew a message, then a usage message, then exit */
394 static void
395 usage(const char *msg)
396 {
397 	if (msg)
398 		err(0, "%s\nUse \"logadm -h\" for help.", msg);
399 	else
400 		err(EF_RAW, "Use \"logadm -h\" for help.\n");
401 }
402 
403 /* helper function used by doaftercmd() to join mail addrs with commas */
404 /*ARGSUSED1*/
405 static void
406 commajoin(const char *lhs, void *rhs, void *arg)
407 {
408 	struct fn *fnp = (struct fn *)arg;
409 
410 	if (*fn_s(fnp))
411 		fn_putc(fnp, ',');
412 	fn_puts(fnp, lhs);
413 }
414 
415 /* helper function used by main() to run "after" commands */
416 static void
417 doaftercmd(const char *lhs, void *rhs, void *arg)
418 {
419 	struct opts *opts = (struct opts *)arg;
420 	struct lut *addrs = (struct lut *)rhs;
421 
422 	if (addrs) {
423 		struct fn *fnp = fn_new(NULL);
424 
425 		/*
426 		 * addrs contains list of email addrs that should get
427 		 * the error output when this after command is executed.
428 		 */
429 		lut_walk(addrs, commajoin, fnp);
430 		err_mailto(fn_s(fnp));
431 	}
432 
433 	docmd(opts, "-a cmd", Sh, "-c", lhs, NULL);
434 }
435 
436 /* main logname processing */
437 static void
438 dologname(struct fn *fnp, struct opts *clopts)
439 {
440 	const char *logname = fn_s(fnp);
441 	struct opts *cfopts;
442 	struct opts *allopts;
443 	struct fn_list *logfiles;
444 	struct fn_list *globbedfiles;
445 	struct fn *nextfnp;
446 
447 	/* look up options set by config file */
448 	cfopts = conf_opts(logname);
449 
450 	if (opts_count(clopts, "v"))
451 		(void) out("# processing logname: %s\n", logname);
452 
453 	if (Debug) {
454 		(void) fprintf(stderr, "dologname: logname <%s>\n", logname);
455 		(void) fprintf(stderr, "conffile opts:");
456 		opts_print(cfopts, stderr, NULL);
457 		(void) fprintf(stderr, "\n");
458 	}
459 
460 	/* handle conffile lookup option */
461 	if (opts_count(clopts, "V")) {
462 		/* lookup an entry in conffile */
463 		if (Debug)
464 			(void) fprintf(stderr,
465 			    "dologname: lookup conffile entry\n");
466 		if (conf_lookup(logname)) {
467 			opts_printword(logname, stdout);
468 			opts_print(cfopts, stdout, NULL);
469 			(void) out("\n");
470 		} else
471 			err_exitcode(1);
472 		return;
473 	}
474 
475 	/* handle conffile removal option */
476 	if (opts_count(clopts, "r")) {
477 		if (Debug)
478 			(void) fprintf(stderr,
479 			    "dologname: remove conffile entry\n");
480 		if (conf_lookup(logname))
481 			conf_replace(logname, NULL);
482 		else
483 			err_exitcode(1);
484 		return;
485 	}
486 
487 	/* generate combined options */
488 	allopts = opts_merge(cfopts, clopts);
489 
490 	/* arrange for error output to be mailed if allopts includes -e */
491 	if (opts_count(allopts, "e"))
492 		err_mailto(opts_optarg(allopts, "e"));
493 	else
494 		err_mailto(NULL);
495 
496 	/* this implements the default rotation rules */
497 	if (opts_count(allopts, "sp") == 0) {
498 		if (opts_count(clopts, "v"))
499 			(void) out(
500 			    "#     using default rotate rules: -s1b -p1w\n");
501 		(void) opts_set(allopts, "s", "1b");
502 		(void) opts_set(allopts, "p", "1w");
503 	}
504 
505 	/* this implements the default expiration rules */
506 	if (opts_count(allopts, "ACS") == 0) {
507 		if (opts_count(clopts, "v"))
508 			(void) out("#     using default expire rule: -C10\n");
509 		(void) opts_set(allopts, "C", "10");
510 	}
511 
512 	/* this implements the default template */
513 	if (opts_count(allopts, "t") == 0) {
514 		if (opts_count(clopts, "v"))
515 			(void) out("#     using default template: $file.$n\n");
516 		(void) opts_set(allopts, "t", "$file.$n");
517 	}
518 
519 	if (Debug) {
520 		(void) fprintf(stderr, "merged opts:");
521 		opts_print(allopts, stderr, NULL);
522 		(void) fprintf(stderr, "\n");
523 	}
524 
525 	/*
526 	 * if the conffile entry supplied log file names, then
527 	 * logname is NOT one of the log file names (it was just
528 	 * the entry name in conffile).
529 	 */
530 	logfiles = opts_cmdargs(cfopts);
531 	if (Debug) {
532 		(void) fprintf(stderr, "dologname: logfiles from cfopts:\n");
533 		fn_list_rewind(logfiles);
534 		while ((nextfnp = fn_list_next(logfiles)) != NULL)
535 			(void) fprintf(stderr, "    <%s>\n", fn_s(nextfnp));
536 	}
537 	if (fn_list_empty(logfiles))
538 		globbedfiles = glob_glob(fnp);
539 	else
540 		globbedfiles = glob_glob_list(logfiles);
541 
542 	/* go through the list produced by glob expansion */
543 	fn_list_rewind(globbedfiles);
544 	while ((nextfnp = fn_list_next(globbedfiles)) != NULL)
545 		if (rotatelog(nextfnp, allopts))
546 			expirefiles(nextfnp, allopts);
547 
548 	fn_list_free(globbedfiles);
549 	opts_free(allopts);
550 }
551 
552 
553 /* absurdly long buffer lengths for holding user/group/mode strings */
554 #define	TIMESTRMAX	100
555 #define	MAXATTR		100
556 
557 /* rotate a log file if necessary, returns true if ok to go on to expire step */
558 static boolean_t
559 rotatelog(struct fn *fnp, struct opts *opts)
560 {
561 	char *fname = fn_s(fnp);
562 	struct stat stbuf;
563 	char nowstr[TIMESTRMAX];
564 	struct fn *recentlog = fn_new(NULL);	/* for -R cmd */
565 	char ownerbuf[MAXATTR];
566 	char groupbuf[MAXATTR];
567 	char modebuf[MAXATTR];
568 	const char *owner;
569 	const char *group;
570 	const char *mode;
571 
572 	if (Debug)
573 		(void) fprintf(stderr, "rotatelog: fname <%s>\n", fname);
574 
575 	if (opts_count(opts, "p") && opts_optarg_int(opts, "p") == OPTP_NEVER)
576 		return (B_TRUE);	/* "-p never" forced no rotate */
577 
578 	/* prepare the keywords */
579 	kw_init(fnp, NULL);
580 	if (Debug > 1) {
581 		(void) fprintf(stderr, "rotatelog keywords:\n");
582 		kw_print(stderr);
583 	}
584 
585 	if (lstat(fname, &stbuf) < 0) {
586 		if (opts_count(opts, "N"))
587 			return (1);
588 		err(EF_WARN|EF_SYS, "%s", fname);
589 		return (B_FALSE);
590 	}
591 
592 	if ((stbuf.st_mode & S_IFMT) == S_IFLNK) {
593 		err(EF_WARN, "%s is a symlink", fname);
594 		return (B_FALSE);
595 	}
596 
597 	if ((stbuf.st_mode & S_IFMT) != S_IFREG) {
598 		err(EF_WARN, "%s is not a regular file", fname);
599 		return (B_FALSE);
600 	}
601 
602 	/* see if size condition is present, and return if not met */
603 	if (opts_count(opts, "s") && stbuf.st_size < opts_optarg_int(opts, "s"))
604 		return (B_TRUE);
605 
606 	/* see if age condition is present, and return if not met */
607 	if (opts_count(opts, "p")) {
608 		int when = opts_optarg_int(opts, "p");
609 		struct opts *cfopts;
610 
611 		/* unless rotate forced by "-p now", see if period has passed */
612 		if (when != OPTP_NOW) {
613 			/*
614 			 * "when" holds the number of seconds that must have
615 			 * passed since the last time this log was rotated.
616 			 * of course, running logadm can take a little time
617 			 * (typically a second or two, but longer if the
618 			 * conffile has lots of stuff in it) and that amount
619 			 * of time is variable, depending on system load, etc.
620 			 * so we want to allow a little "slop" in the value of
621 			 * "when".  this way, if a log should be rotated every
622 			 * week, and the number of seconds passed is really a
623 			 * few seconds short of a week, we'll go ahead and
624 			 * rotate the log as expected.
625 			 *
626 			 */
627 			if (when >= 60 * 60)
628 				when -= 59;
629 
630 			/*
631 			 * last rotation is recorded as argument to -P,
632 			 * but if logname isn't the same as log file name
633 			 * then the timestamp would be recorded on a
634 			 * separate line in the conf file.  so if we
635 			 * haven't seen a -P already, we check to see if
636 			 * it is part of a specific entry for the log
637 			 * file name.  this handles the case where the
638 			 * logname is "apache", it supplies a log file
639 			 * name like "/var/apache/logs/[a-z]*_log",
640 			 * which expands to multiple file names.  if one
641 			 * of the file names is "/var/apache/logs/access_log"
642 			 * the the -P will be attached to a line with that
643 			 * logname in the conf file.
644 			 */
645 			if (opts_count(opts, "P")) {
646 				int last = opts_optarg_int(opts, "P");
647 
648 				/* return if not enough time has passed */
649 				if (Now - last < when)
650 					return (B_TRUE);
651 			} else if ((cfopts = conf_opts(fname)) != NULL &&
652 			    opts_count(cfopts, "P")) {
653 				int last = opts_optarg_int(cfopts, "P");
654 
655 				/*
656 				 * just checking this means this entry
657 				 * is now "done" if we're going through
658 				 * the entire conffile
659 				 */
660 				Donenames = lut_add(Donenames, fname, "1");
661 
662 				/* return if not enough time has passed */
663 				if (Now - last < when)
664 					return (B_TRUE);
665 			}
666 		}
667 	}
668 
669 	if (Debug)
670 		(void) fprintf(stderr, "rotatelog: conditions met\n");
671 
672 	/* rename the log file */
673 	rotateto(fnp, opts, 0, recentlog, B_FALSE);
674 
675 	/* determine owner, group, mode for empty log file */
676 	if (opts_count(opts, "o"))
677 		(void) strlcpy(ownerbuf, opts_optarg(opts, "o"), MAXATTR);
678 	else {
679 		(void) snprintf(ownerbuf, MAXATTR, "%ld", stbuf.st_uid);
680 	}
681 	owner = ownerbuf;
682 	if (opts_count(opts, "g"))
683 		group = opts_optarg(opts, "g");
684 	else {
685 		(void) snprintf(groupbuf, MAXATTR, "%ld", stbuf.st_gid);
686 		group = groupbuf;
687 	}
688 	(void) strlcat(ownerbuf, ":", MAXATTR - strlen(ownerbuf));
689 	(void) strlcat(ownerbuf, group, MAXATTR - strlen(ownerbuf));
690 	if (opts_count(opts, "m"))
691 		mode = opts_optarg(opts, "m");
692 	else {
693 		(void) snprintf(modebuf, MAXATTR,
694 		    "%03lo", stbuf.st_mode & 0777);
695 		mode = modebuf;
696 	}
697 
698 	/* create the empty log file */
699 	docmd(opts, NULL, Touch, fname, NULL, NULL);
700 	docmd(opts, NULL, Chown, owner, fname, NULL);
701 	docmd(opts, NULL, Chmod, mode, fname, NULL);
702 
703 	/* execute post-rotation command */
704 	if (opts_count(opts, "R")) {
705 		struct fn *rawcmd = fn_new(opts_optarg(opts, "R"));
706 		struct fn *cmd = fn_new(NULL);
707 
708 		kw_init(recentlog, NULL);
709 		(void) kw_expand(rawcmd, cmd, 0, B_FALSE);
710 		docmd(opts, "-R cmd", Sh, "-c", fn_s(cmd), NULL);
711 		fn_free(rawcmd);
712 		fn_free(cmd);
713 	}
714 	fn_free(recentlog);
715 
716 	/*
717 	 * add "after" command to list of after commands.  we also record
718 	 * the email address, if any, where the error output of the after
719 	 * command should be sent.  if the after command is already on
720 	 * our list, add the email addr to the list the email addrs for
721 	 * that command (the after command will only be executed once,
722 	 * so the error output gets mailed to every address we've come
723 	 * across associated with this command).
724 	 */
725 	if (opts_count(opts, "a")) {
726 		const char *cmd = opts_optarg(opts, "a");
727 		struct lut *addrs = (struct lut *)lut_lookup(Aftercmds, cmd);
728 		if (opts_count(opts, "e"))
729 			addrs = lut_add(addrs, opts_optarg(opts, "e"), NULL);
730 		Aftercmds = lut_add(Aftercmds, opts_optarg(opts, "a"), addrs);
731 	}
732 
733 	/* record the rotation date */
734 	(void) strftime(nowstr, sizeof (nowstr),
735 	    "%a %b %e %T %Y", gmtime(&Now));
736 	if (opts_count(opts, "v"))
737 		(void) out("#     recording rotation date %s for %s\n",
738 		    nowstr, fname);
739 	conf_set(fname, "P", STRDUP(nowstr));
740 	Donenames = lut_add(Donenames, fname, "1");
741 	return (B_TRUE);
742 }
743 
744 /* rotate files "up" according to current template */
745 static void
746 rotateto(struct fn *fnp, struct opts *opts, int n, struct fn *recentlog,
747     boolean_t isgz)
748 {
749 	struct fn *template = fn_new(opts_optarg(opts, "t"));
750 	struct fn *newfile = fn_new(NULL);
751 	struct fn *dirname;
752 	int hasn;
753 	struct stat stbuf;
754 
755 	/* expand template to figure out new filename */
756 	hasn = kw_expand(template, newfile, n, isgz);
757 
758 	if (Debug)
759 		(void) fprintf(stderr, "rotateto: %s -> %s (%d)\n", fn_s(fnp),
760 		    fn_s(newfile), n);
761 
762 	/* if filename is there already, rotate "up" */
763 	if (hasn && lstat(fn_s(newfile), &stbuf) != -1)
764 		rotateto(newfile, opts, n + 1, recentlog, isgz);
765 	else if (hasn && opts_count(opts, "z")) {
766 		struct fn *gzfnp = fn_dup(newfile);
767 		/*
768 		 * since we're compressing old files, see if we
769 		 * about to rotate into one.
770 		 */
771 		fn_puts(gzfnp, ".gz");
772 		if (lstat(fn_s(gzfnp), &stbuf) != -1)
773 			rotateto(gzfnp, opts, n + 1, recentlog, B_TRUE);
774 		fn_free(gzfnp);
775 	}
776 
777 	/* first time through run "before" cmd if not run already */
778 	if (n == 0 && opts_count(opts, "b")) {
779 		const char *cmd = opts_optarg(opts, "b");
780 
781 		if (lut_lookup(Beforecmds, cmd) == NULL) {
782 			docmd(opts, "-b cmd", Sh, "-c", cmd, NULL);
783 			Beforecmds = lut_add(Beforecmds, cmd, "1");
784 		}
785 	}
786 
787 	/* ensure destination directory exists */
788 	dirname = fn_dirname(newfile);
789 	docmd(opts, "verify directory exists", Mkdir, "-p",
790 	    fn_s(dirname), NULL);
791 	fn_free(dirname);
792 
793 	/* do the rename */
794 	if (opts_count(opts, "c")) {
795 		docmd(opts, "rotate log file via copy (-c flag)",
796 		    Cp, "-fp", fn_s(fnp), fn_s(newfile));
797 		docmd(opts, "truncate log file (-c flag)",
798 		    Cp, "-f", "/dev/null", fn_s(fnp));
799 	} else if (n == 0 && opts_count(opts, "M")) {
800 		struct fn *rawcmd = fn_new(opts_optarg(opts, "M"));
801 		struct fn *cmd = fn_new(NULL);
802 
803 		/* use specified command to mv the log file */
804 		kw_init(fnp, newfile);
805 		(void) kw_expand(rawcmd, cmd, 0, B_FALSE);
806 		docmd(opts, "-M cmd", Sh, "-c", fn_s(cmd), NULL);
807 		fn_free(rawcmd);
808 		fn_free(cmd);
809 	} else
810 		/* common case: we call "mv" to handle the actual rename */
811 		docmd(opts, "rotate log file", Mv, "-f",
812 		    fn_s(fnp), fn_s(newfile));
813 
814 	/* gzip the old log file  according to -z count */
815 	if (!isgz && opts_count(opts, "z")) {
816 		int count = opts_optarg_int(opts, "z");
817 
818 		if (Debug)
819 			(void) fprintf(stderr, "rotateto z count %d\n", count);
820 
821 		if (count <= n) {
822 			docmd(opts, "compress old log (-z flag)", Gzip,
823 			    "-f", fn_s(newfile), NULL);
824 			fn_puts(newfile, ".gz");
825 		}
826 	}
827 
828 	/* first time through, gather interesting info for caller */
829 	if (n == 0)
830 		fn_renew(recentlog, fn_s(newfile));
831 }
832 
833 /* expire phase of logname processing */
834 static void
835 expirefiles(struct fn *fnp, struct opts *opts)
836 {
837 	char *fname = fn_s(fnp);
838 	struct fn *template;
839 	struct fn *pattern;
840 	struct fn_list *files;
841 	struct fn *nextfnp;
842 	int count;
843 	size_t size;
844 
845 	if (Debug)
846 		(void) fprintf(stderr, "expirefiles: fname <%s>\n", fname);
847 
848 	/* return if no potential expire conditions */
849 	if (opts_count(opts, "AS") == 0 && opts_optarg_int(opts, "C") == 0)
850 		return;
851 
852 	kw_init(fnp, NULL);
853 	if (Debug > 1) {
854 		(void) fprintf(stderr, "expirefiles keywords:\n");
855 		kw_print(stderr);
856 	}
857 
858 	/* see if pattern was supplied by user */
859 	if (opts_count(opts, "T")) {
860 		template = fn_new(opts_optarg(opts, "T"));
861 		pattern = glob_to_reglob(template);
862 	} else {
863 		/* nope, generate pattern based on rotation template */
864 		template = fn_new(opts_optarg(opts, "t"));
865 		pattern = fn_new(NULL);
866 		(void) kw_expand(template, pattern, -1,
867 		    opts_count(opts, "z") != 0);
868 	}
869 
870 	/* match all old log files (hopefully not any others as well!) */
871 	files = glob_reglob(pattern);
872 
873 	if (Debug) {
874 		(void) fprintf(stderr, "expirefiles: pattern <%s>\n",
875 		    fn_s(pattern));
876 		fn_list_rewind(files);
877 		while ((nextfnp = fn_list_next(files)) != NULL)
878 			(void) fprintf(stderr, "    <%s>\n", fn_s(nextfnp));
879 	}
880 
881 	/* see if count causes expiration */
882 	if ((count = opts_optarg_int(opts, "C")) > 0) {
883 		int needexpire = fn_list_count(files) - count;
884 
885 		if (Debug)
886 			(void) fprintf(stderr, "expirefiles: needexpire %d\n",
887 			    needexpire);
888 
889 		while (needexpire > 0 &&
890 		    ((nextfnp = fn_list_popoldest(files)) != NULL)) {
891 			dorm(opts, "expire by count rule", nextfnp);
892 			fn_free(nextfnp);
893 			needexpire--;
894 		}
895 	}
896 
897 	/* see if total size causes expiration */
898 	if (opts_count(opts, "S") && (size = opts_optarg_int(opts, "S")) > 0) {
899 		while (fn_list_totalsize(files) > size &&
900 		    ((nextfnp = fn_list_popoldest(files)) != NULL)) {
901 				dorm(opts, "expire by size rule", nextfnp);
902 				fn_free(nextfnp);
903 		}
904 	}
905 
906 	/* see if age causes expiration */
907 	if (opts_count(opts, "A")) {
908 		int mtime = (int)time(0) - opts_optarg_int(opts, "A");
909 
910 		while ((nextfnp = fn_list_popoldest(files)) != NULL)
911 			if (fn_getstat(nextfnp)->st_mtime < mtime) {
912 				dorm(opts, "expire by age rule", nextfnp);
913 				fn_free(nextfnp);
914 			} else {
915 				fn_free(nextfnp);
916 				break;
917 			}
918 	}
919 
920 	fn_free(template);
921 	fn_list_free(files);
922 }
923 
924 /* execute a command to remove an expired log file */
925 static void
926 dorm(struct opts *opts, const char *msg, struct fn *fnp)
927 {
928 	if (opts_count(opts, "E")) {
929 		struct fn *rawcmd = fn_new(opts_optarg(opts, "E"));
930 		struct fn *cmd = fn_new(NULL);
931 
932 		/* user supplied cmd, expand $file */
933 		kw_init(fnp, NULL);
934 		(void) kw_expand(rawcmd, cmd, 0, B_FALSE);
935 		docmd(opts, msg, Sh, "-c", fn_s(cmd), NULL);
936 		fn_free(rawcmd);
937 		fn_free(cmd);
938 	} else
939 		docmd(opts, msg, Rm, "-f", fn_s(fnp), NULL);
940 }
941 
942 /* execute a command, producing -n and -v output as necessary */
943 static void
944 docmd(struct opts *opts, const char *msg, const char *cmd,
945     const char *arg1, const char *arg2, const char *arg3)
946 {
947 	int pid;
948 	int errpipe[2];
949 
950 	/* print info about command if necessary */
951 	if (opts_count(opts, "vn")) {
952 		const char *simplecmd;
953 
954 		if ((simplecmd = strrchr(cmd, '/')) == NULL)
955 			simplecmd = cmd;
956 		else
957 			simplecmd++;
958 		(void) out("%s", simplecmd);
959 		if (arg1)
960 			(void) out(" %s", arg1);
961 		if (arg2)
962 			(void) out(" %s", arg2);
963 		if (arg3)
964 			(void) out(" %s", arg3);
965 		if (msg)
966 			(void) out(" # %s", msg);
967 		(void) out("\n");
968 	}
969 
970 	if (opts_count(opts, "n"))
971 		return;		/* -n means don't really do it */
972 
973 	/*
974 	 * run the cmd and see if it failed.  this function is *not* a
975 	 * generic command runner -- we depend on some knowledge we
976 	 * have about the commands we run.  first of all, we expect
977 	 * errors to spew something to stderr, and that something is
978 	 * typically short enough to fit into a pipe so we can wait()
979 	 * for the command to complete and then fetch the error text
980 	 * from the pipe.  we also expect the exit codes to make sense.
981 	 * notice also that we only allow a command name which is an
982 	 * absolute pathname, and two args must be supplied (the
983 	 * second may be NULL, or they may both be NULL).
984 	 */
985 	if (pipe(errpipe) < 0)
986 		err(EF_SYS, "pipe");
987 
988 	if ((pid = fork()) < 0)
989 		err(EF_SYS, "fork");
990 	else if (pid) {
991 		int wstat;
992 		int count;
993 
994 		/* parent */
995 		(void) close(errpipe[1]);
996 		if (waitpid(pid, &wstat, 0) < 0)
997 			err(EF_SYS, "waitpid");
998 
999 		/* check for stderr output */
1000 		if (ioctl(errpipe[0], FIONREAD, &count) >= 0 && count) {
1001 			err(EF_WARN, "command failed: %s%s%s%s%s%s%s",
1002 				cmd,
1003 				(arg1) ? " " : "",
1004 				(arg1) ? arg1 : "",
1005 				(arg2) ? " " : "",
1006 				(arg2) ? arg2 : "",
1007 				(arg3) ? " " : "",
1008 				(arg3) ? arg3 : "");
1009 			err_fromfd(errpipe[0]);
1010 		} else if (WIFSIGNALED(wstat))
1011 			err(EF_WARN,
1012 			    "command died, signal %d: %s%s%s%s%s%s%s",
1013 				WTERMSIG(wstat),
1014 				cmd,
1015 				(arg1) ? " " : "",
1016 				(arg1) ? arg1 : "",
1017 				(arg2) ? " " : "",
1018 				(arg2) ? arg2 : "",
1019 				(arg3) ? " " : "",
1020 				(arg3) ? arg3 : "");
1021 		else if (WIFEXITED(wstat) && WEXITSTATUS(wstat))
1022 			err(EF_WARN,
1023 			    "command error, exit %d: %s%s%s%s%s%s%s",
1024 				WEXITSTATUS(wstat),
1025 				cmd,
1026 				(arg1) ? " " : "",
1027 				(arg1) ? arg1 : "",
1028 				(arg2) ? " " : "",
1029 				(arg2) ? arg2 : "",
1030 				(arg3) ? " " : "",
1031 				(arg3) ? arg3 : "");
1032 
1033 		(void) close(errpipe[0]);
1034 	} else {
1035 		/* child */
1036 		(void) dup2(errpipe[1], fileno(stderr));
1037 		(void) close(errpipe[0]);
1038 		(void) execl(cmd, cmd, arg1, arg2, arg3, 0);
1039 		perror(cmd);
1040 		_exit(1);
1041 	}
1042 }
1043