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