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