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